슬기로운 개발생활

[Java] 객체지향 설계 원칙 SOLID 알아보기

by coco3o
반응형

객체지향 프로그래밍 설계시 좋은 객체지향 설계를 위한 SOLID 라고 불리는 5가지 원칙이 있다.

 

SOLID 원칙은 소프트웨어를 설계함에 있어 이해하기 쉽고, 유연하며, 유지보수 및 확장이 편하다는 장점이 있다.

 

1. SRP(Single responsibility principle) : 단일 책임 원칙

2. OCP(Open-closed principle) : 개방-폐쇄 원칙

3. LSP(Liskov substitution principle) : 리스코프 치환 원칙

4. ISP(Interface segregation principle) : 인터페이스 분리 원칙

5.DIP(Dependency inversion principle) : 의존관계 역전 원칙


1. SRP - 단일 책임 원칙

한 클래스는 하나의 책임만 가져야 한다.

 

모든 클래스는 각각 하나의 책임만 가져야 하며, 수정할 이유는 단 한 가지여야 한다.

즉, 클래스는 그 책임을 완전히 캡슐화해야 함을 말한다.

 

예를들어, 결제 클래스가 있다치면 이 클래스는 오직 결제 기능만을 책임지고,

만약 이 클래스를 수정해야 한다면 결제에 관련된 문제일 뿐일 것이다.


2. OCP - 개방-폐쇄 원칙

확장에는 열려있고 변경에는 닫혀 있어야 한다.

 

소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려 있어야 하지만 변경에는 폐쇄적이어야 함을 의미한다.

즉, 기존의 코드를 변경하지 않으면서, 기능을 추가할 수 있도록 설계가 되는 원칙을 말한다.

 

예를들어, 캐릭터 하나를 생성한다고 할 때 각 캐릭터마다 움직임이 다를 경우,

움직임 패턴 구현을 하위 클래스에 맡긴다면 캐릭터 클래스의 수정은 필요없고(Closed),

움직임 패턴만 재정의 하면 된다.(Open)

 

개방-폐쇄 원칙을 적용하기 위한 중요 메커니즘은 추상화와 다형성이다.


3. LSP - 리스코프 치환 원칙

서브 타입은 언제나 자신의 기반 타입으로 변경할 수 있어야 한다.

 

상위 타입은 항상 하위 타입으로 대체될 수 있어야 함을 의미한다.

즉, 부모 클래스가 들어갈 자리에 자식 클래스를 넣어도 역할을 하는데 문제가 없어야 한다는 의미이다.

 

리스코프 치환 원칙 위배
리스코프 치환 원칙 준수

리스코프 치환 원칙은 다형성과 확장성을 극대화하며, 개방-폐쇄 원칙을 구성한다.


4. ISP - 인터페이스 분리 원칙

하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스가 낫다.

 

인터페이스 분리 원칙은 각 역할에 맞게 인터페이스를 분리하는 것이다.

인터페이스 내에 메소드는 최소한 일수록 좋다. 즉, 최소한의 기능만 제공하면서 하나의 역할에 집중하라는 뜻이다.

 

단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다.

가능한 최소한의 인터페이스를 사용하도록 하여 단일 책임을 강조한다고 볼 수 있다.

일반적으로 ISP보다 SRP 할 것을 권장하고 있다.


5. DIP - 의존관계 역전 원칙

구체적인 것이 추상화된 것에 의존해야 한다. 자주 변경되는 구체 클래스에 의존하지마라.

 

의존 관계를 맺을 때 고수준 모듈이 저수준 모듈에 직접 의존하는 것을 피해야 한다는 것이다.

  • 고수준 모듈 : 변경이 없는 추상화된 클래스(또는 인터페이스)
  • 저수준 모듈 : 위의 추상화된 클래스나 인터페이스를 상속받은 구현체 클래스

코드로 설명해보자면,

public interface Pizza {
    void prepare();
    void bake();
    void cut();
}

public class CheesePizza implements Pizza {
    @Override
    public void prepare() {
        System.out.println("Preparing Cheese Pizza...");
    }

    @Override
    public void bake() {
        System.out.println("Baking Cheese Pizza...");
    }

    @Override
    public void cut() {
        System.out.println("Cutting Cheese Pizza...");
    }
}

이렇게 Pizza라는 인터페이스(고수준 모듈)와 CheesePizza라는 구현 클래스(저수준 모듈)이 있을 때,

public class Chef {
    private final Pizza pizza;

    public Chef(Pizza pizza) {
        this.pizza = pizza;
    }
    
    public void makePizza() {
        pizza.prepare();
        pizza.bake();
        pizza.cut();
    }
}

public class Main {
    public static void main(String[] args) {
        Pizza cheesePizza = new CheesePizza();
        Chef chef = new Chef(cheesePizza);
        chef.makePizza();
    }
}

Chef 클래스에서 의존성 주입을 할 때 변하기 쉬운 CheesePizza 클래스에 직접 의존하지 말고 고수준 모듈인 Pizza 인터페이스를 의존하라는 의미이다.


참고자료 : 

https://hckcksrl.medium.com/solid-%EC%9B%90%EC%B9%99-182f04d0d2b

https://devlog-wjdrbs96.tistory.com/380

https://code-lab1.tistory.com/121

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기