# Chapter05 설계 원칙, SOLID

# 목표

개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴 스터디를 진행하며 공부한 내용을 정리한다.

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

객체 지향의 기본은 책임을 할당하는 것이다. 단일 책임 원칙은 다음과 같은 간단한 규칙을 가진다.

  • 클래스는 단 한 개의 책임을 가져야 한다.

클래스가 여러 책임을 갖게 되면 그 클래스는 각 책임마다 변경되는 이유가 발생한다. 이것을 다른 말로 표현하면 클래스를 변경하는 이유는 단 한 개여야 한다. 하지만 한 개의 책임이란 정의가 명확하지 않고 책임을 도출하기 위해 다양한 경험이 필요하다.

단일 책임 원칙을 어길 경우 재사용을 어렵게 만든다. 책임을 적절히 나눠야 필요한 부분을 가져다 사용할 수 있기 때문에 둘 이상의 책임을 가진 클래스는 재사용에 부적절하다.

# 1.2 책임이란 변화에 대한 것

책임의 단위는 변화되는 부분에서 발견할 수 있다. 이러한 변화를 통해 별도로 분리되어야 할 책임이라는 것을 알 수 있다. 각각의 책임은 서로 다른 이유로 변경되고, 서로 다른 비율로 변경된다. 하지만 서로 다른 이유로 변경되는 것을 알아차리기 쉽지 않다. 이럴 땐 해당 메서드를 사용하는 사용자에 집중한다. 클래스의 사용자들이 서로 다른 메서드를 사용한다면 각각 다른 책임에 속할 가능성이 크기 때문에 책임 분리의 후보가 될 수 있다.

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

개방 폐쇄 원칙은 아래와 같이 설명할 수 있다.

  • 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.
  • 기능을 변경하거나 확장할 수 있지만 해당 기능을 사용하는 코드는 수정하지 말아야 한다.

개방 폐쇄 원칙을 구현할 수 있는 방법은 확장되는 부분을 추상화해서 표현하는 것이다. 특정 클래스에서 변화되는 부분을 인터페이스로 추상화하며 해당 기능을 고정할 수 있다.

간단한 예시로 이번 미션을 진행하며 Deck에 랜덤으로 섞인 카드 52장을 넣어야 한다. 만약 특정한 순서를 가진 덱을 구성해야 한다면 간단히 CardsGenerator를 구현하여 주입하면 된다. Deck 입장에서는 구현체에 대한 종속성을 가지고 있지 않기 때문에 기능을 확장할 때 자유롭다.

@FunctionalInterface
public interface CardsGenerator {

    List<Card> generate();
}
public class Deck {

    private final Queue<Card> values;

    public Deck(CardsGenerator cardsGenerator) { // 인터페이스의 의존성을 가짐
        this.values = new LinkedList<>(cardsGenerator.generate());
    }

    public Card pick() {
        if (values.isEmpty()) {
            throw new IllegalArgumentException("카드가 모두 소진되었습니다.");
        }
        return values.poll();
    }
}

개방 폐쇄 원칙 구현에는 상속을 이용한 방법도 있다. 상속은 상위 클래스의 기능을 그대로 사용하며 일부 구현을 오버라이딩할 수 있다. 상위 클래스의 코드를 변경하지 않고 추가된 기능을 작성할 수 있다. 하지만 이것은 상속 구조가 적절한지 먼저 판단해봐야 한다.

# 2.1 개방 폐쇄 원칙이 깨질 때의 주요 증상

# 다운 캐스팅

다운 캐스팅을 진행한다는 것은 결국 구체적인 클래스 정보를 알고 있다는 것이다. 이것은 결국 구체 클래스가 변경되거나 기능이 확장될 때 함께 수정될 여지가 있다. 수정을 야기한다는 것은 결국 변경에 닫혀 있지 않다는 것을 의미한다. instanceof와 같은 타입 확인 연산자가 사용된다면 해당 코드가 적절한지 확인해볼 필요가 있다.

# 비슷한 if-else 블록

이것은 외부에서 실제 행위를 주입 받으면 다양한 상황에서 주입된 전략을 활용하여 해결할 수 있다. 즉 추가된 전략만 외부에서 주입하면 된다. 이 파트를 읽으며 바로 전략 패턴이 떠올랐다.

# 2.2 개방 폐쇄 원칙은 유연함에 대한 것

개방 폐쇄 원칙은 결국 변경의 유연함과 관련된 원칙이다. 이러한 이점은 인터페이스를 통한 추상화와 상속을 통해서 얻을 수 있다. 또한 변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 도와준다.

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

개방 폐쇄 원칙은 추상화와 다형성을 이용하여 구현하였다. 리스코프 치환 원칙은 개방 폐쇄 원칙을 받쳐 주는 다형성에 관한 원칙을 제공한다.

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 상위 타입을 사용하는 프로그램은 정상적으로 동작해야 한다.

# 3.1 리스코프 치환 원칙을 지키지 않을 때의 문제

# 직사각형-정사각형 문제

가장 좋은 예시는 직사각형-정사각형 문제 (opens new window)이다.

직사각형-정사각형 문제는 개념적으로 상속 관계에 있는 것 처럼 보이지만 실제 구현에서는 상속 관계가 아닐 수 있다. 해당 예제에서는 상속을 통한 구현보다 별개의 타입을 구현하는 것이 더 바람직하다..

# 상위 타입에서 지정한 리턴 값의 범위에 해당되지 않는 값을 리턴하는 것

상위 타입을 상속한 하위 타입은 상위 타입에서 명시한 명세에 맞춰 기능을 확장해야 한다. 만약 이것이 지켜지지 않으면 사용하는 사용자는 해당 클래스를 신뢰하고 치환할 수 없다.

# 3.2 리스코프 치환 원칙은 계약과 확장에 대한 것

하위 타입이 명세에서 벗어난 동작을 하면, 해당 명세를 기반으로 작성된 코드는 비정상적으로 변하게 된다. 즉 하위 타입은 상위 타입에서 정의한 명세를 벗어나지 않고 구현해야 한다.

또한 리스코프 치환 원칙은 확장에 대한 것이다. 해당 원칙을 어기면 개방 폐쇄 원칙 또한 어길 가능성이 높아진다.

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

  • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.

실제 기재된 정의

실제 SOLID에서 인터페이스 분리 원칙의 정의는 클라이언트는 자신이 사용하는 메서드에만 의존해야 한다.이다.

책에서는 조금 더 이해하고 기억하기 쉽도록 정의를 변경하여 기재하였다.

하나의 타입에 여러 기능이 섞여 있을 경우 한 기능의 변화로 인해 다른 기능이 영향을 받을 가능성이 높아진다. 따라서 클라이언트 입장에서 사용하는 기능만 제공하도록 인터페이스를 분리함으로써 한 기능에 대한 변경의 여파를 최소화할 수 있다.

이러한 인터페이스 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하는 것이다. 각 클라이언트가 이용하는 기능을 중심으로 인터페이스를 분리함으로써, 클라이언트로부터 발생하는 인터페이스 변경의 여파가 다른 클라이언트에 미치는 영향을 최소화 한다.

# 한 클라이언트의 영향이 다른 클라이언트에 영향

# 한 클라이언트의 영향이 다른 클라이언트에 영향 없음

# 의존 역전 원칙 (Dependency inversion principle)

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모율이 고수준 모듈에 정의한 추상 타입에 의존해야 한다.

# 5.1 고수준 모듈이 저수준 모듈에 의존할 때의 문제

우리가 원하는 것은 저수준 모듈이 변경되더라도 고수준 모듈은 변경되지 않아야 한다.

# 5.2 의존 역전 원칙을 통한 변경의 유연함 확보

고수준 모듈이 저수준 모듈을 사용한다는 것은 고수준 모듈이 저수준 모듈에 의존한다는 것이다. 이것을 이뤄내기 위해서는 추상화를 활용한다.

고수준 모듈과 저수준 모듈이 모두 추상 타입에 의존하게 만든다. 이것은 결과적으로 리스코츠 치환 원칙과 개방 폐쇄 원칙의 기반을 만들어 준다.

# 5.3 소스 코드 의존과 런타임 의존

이러한 의존 역전 원칙을 사용하면 소스 코드 상에서의 의존은 역전되었지만 런타임에서의 의존은 고수준 모듈의 객체에서 저수준 모듈의 객체로 향하게 된다.

의존 역전 원칙은 런타임의 의존이 아닌 소스 코드의 의존을 역전시킴으로써 변경의 유연함을 확보할 수 있도록 만들어 주는 원칙이다. 즉 런타임에서의 의존을 역전시키는 것은 아니다.

# References

최범균 지음, 『개발자가 반드시 정복해야 할 객체지향과 디자인 패턴』, 인투북스(2014), p104-136.

#우아한테크코스 #개발자가 반드시 정복해야 할 객체지향과 디자인 패턴
last updated: 3/20/2022, 8:07:45 PM