SOLID. OOP를 구글링 하면 OOP의 특징과 함께 나오는 항상 나오는 약어이다.

각 원칙들은 서로 다른 내용이라기 보다는 밀접하게 연관되어있다.

또한 설계의 근본적인 목표는 성능보다는 유지보수를 향하고 있어야 한다.

 

객체 지향 프로그래밍의 5가지 설계 원칙

 

단일 책임 원칙 (Single Responsibility Principle, SRP)

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

책임은 변화에 대한 것이다.

 

여러개의 책임을 가지게 되면 각 책임마다 변경되는 이유가 발생하기 때문이다. 그래서 한 가지 이유로만 변경되려면 한 가지 책임만을 가져야 한다.

하지만 책임의 기준을 잡는것이 모호하고 어렵기 때문에 변경을 책임의 기준으로 잡으면 조금 더 용이하게 설계할 수 있다.

 

SRP를 지키지 않으면 연쇄작용이 발생할 가능성이 높아지기 때문에 유지보수가 어려워진다.

 

개방-폐쇄 원칙 (Open-Closed Principle, OCP)

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

개방 폐쇄 원칙은 유연함에 대한 것이다.

추상화와 다형성을 이용해서 OCP를 달성할 수 있고 상속을 통해서도 달성할 수 있다. (오버라이딩)

 

쉽게 얘기하면 기능의 변경이나 확장은 가능해야 하지만 해당 기능을 사용하는 코드는 수정하지 않아야 한다는 것이다.

메소드의 내부 구현이 바뀔수는 있어도 목적은 고정시킴으로써 인터페이스를 변경하지 않고도 그대로 사용이 가능해야 한다.

OCP가 깨진 코드에는 다운 캐스팅, 비슷한 if-else 블록이 존재하는 특징이 있다.

 

리스코프 치환 원칙 (Liskov Substitution Principle, LSP)

업 캐스팅 되어도 프로그램은 정상적으로 동작해야 한다.

리스코프 치환 원칙은 계약과 확장에 대한 것이다.

다형성을 이용해서 LSP를 달성할 수 있다.

 

상속 관계처럼 보이지만 실제로는 상속 관계가 아닌 경우 LSP의 위반이 발생한다. 예를 들어 사각형과 정사각형 클래스의 경우가 될 수 있다.

또한 잘못된 오버라이딩으로 상위 클래스에서 지정한 범위의 값을 반환하지 않는 경우에도 위반이 발생한다. 그렇기 때문에 오버라이딩을 하게 되면 반드시 반환값의 의미가 같아야 한다.

 

LSP를 어기면 OCP도 어길 가능성도 자연스레 높아진다.

 

int calculateDiscountAmount(Item* item)
{
    if (nullptr != dynamic_cast<SpecialItem*>(item))
        return 0;
        
    return item->getPrice() * discountRate;
}

위 코드처럼 다운 캐스팅으로 타입을 비교하여 예외를 처리하는 경우가 일반적인 LSP 위반 사례이다. 또한 기능을 확장하며 예외를 처리하는 과정에서 변경에는 닫혀 있어야 하는데 코드가 변경되었기 때문에 OCP도 위반된다.

 

class Item
{
public:
    virtual bool isDiscountAvailable() { return true; }    
};

class SpecialItem : public Item
{
public:
    virtual bool isDiscountAvailable() override { return false; }
};

int calculateDiscountAmount(Item* item)
{
    if (!item->isDiscountAvailable())
        return 0;
        
    return item->getPrice() * discountRate;
}

위와 같이 추후 기능이 확장된다 하더라도 다운캐스팅 및 코드의 변경 없이도 의도한 대로 동작할 수 있어야 LSP 및 OCP를 지킬 수 있다.

 

인터페이스 분리 원칙 (Interpace Segregation Principle, ISP)

클라이언트는 자신이 사용하는 메서드에만 의존해야 한다.

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

인터페이스 분리 원칙은 클라이언트에 대한 것이다.

 

C/C++같이 컴파일과 링크를 직접 해주는 언어를 사용할 때 장점이 잘 드러난다.

예를 들어 ArticleListUI, ArticleWriteUI, ArticleDeleteUI가 ArticleService 클래스의 헤더를 참조하고 있을 때,  ArticleService 클래스에서 읽기와 관련된 메소드가 변경된다고 하면 재컴파일 후에 목적 파일이 재생성 된다. 여기서 읽기 기능만 변경되었음에도 불구하고 전혀 상관없는 ListUI, WriteUI도 ArticleService 클래스의 헤더 파일을 참조하고 있기 때문에 다시 컴파일되며 목적 파일이 재생성 된다.

이런 경우 ArticleService 클래스를 각 기능으로 나누어서 분리하면 사용하지 않는 인터페이스에 변경이 발생하더라도 영향을 받지 않는다.

 

UML로 그리면 위와 같다

직관적으로 보이듯이 단일 책임 원칙과도 연결된다.

 

의존 역전 원칙 (Dependency Inversion Principle, DIP)

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

추상화를 이용해서 DIP를 달성할 수 있다.

 

 

고수준 모듈과 저수준 모듈의 예를 들면 아래와 같다.

◾ 고수준 모듈

- 바이트 데이터를 읽어와 암호화 하고 결과 바이트 데이터를 쓴다.

 

◾ 저수준 모듈

- 파일에서 바이트 데이터를 읽어온다.

- AES 알고리즘으로 암호화한다.

파일에 바이트 데이터를 쓴다.

 

고수준 모듈은 의미 있는 단일 기능을 제공하는 모듈이라고 정의할 수 있고 저수준 모듈은 고수준 모듈을 구현하기 위해 필요한 하위 기능의 실제 구현으로 정의할 수 있다.

즉, 고수준 모듈은 상위 수준에서 프로그램을 다룬다면 저수준 모듈은 상세가 어떻게 구현될지에 대해서 다루는 것이다.

프로젝트가 어느정도 안정화가 되면 상위 수준보다는 상세 수준에서 변경이 발생할 가능성이 높아진다.

 

한 가지 예시를 들어보자.

어떤 상품의 가격을 결정하는 정책이 있을 때 상위 수준(고수준 모듈)에서는 아래와 같은 결정이 내려질 수 있다.

◾ 쿠폰을 적용해서 가격 할인을 받을 수 있다.

◾ 쿠폰은 동시에 한 개만 적용 가능하다.

 

상위 수준에서의 쿠폰 정책은 한 번 안정화되면 쉽게 변하지 않지만 쿠폰은 상황에 따라 다양한 종류가 추가될 수 있다. 여기서 쿠폰을 이용한 가격 계산 모듈이 개별적인 쿠폰 구현에 의존하게 되면 새로운 쿠폰 구현이 추가되거나 변경될 때마다 가격 계산 모듈이 변경되는 상황이 발생된다.

저수준 모듈의 변경이 고수준 모듈의 변경을 초래하게 되는 것이다.

 

특정 클래스에 직접 의존하는 것이 아니라 추상화를 거쳐서 각 클래스들이 추상 타입에 의존하도록 만들면 저수준 모듈이 변경되더라도 고수준 모듈의 변경이 발생하지 않는다.

즉, DIP는 LSP와 OCP를 따르는 설계를 만들어 주는 기반이 된다.

 

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

 

정리

SOLID 원칙을 한 마디로 정의하자면 변화에 유연하게 대처할 수 있는 설계 원칙이다.

SRP와 ISP는 객체가 커지지 않도록 막아준다.

OCP는 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능 확장을 하면서 동시에 기존 코드를 수정하지 않도록 만들어준다. 여기서 변화되는 부분을 추상화할 수 있도록 도와주는 것이 DIP이고 다형성을 도와주는 것이 LSP이다.

또한 사용자 입장에서의 기능 사용을 중시하기 때문에 사용자 관점에서의 설계를 지향한다.

+ Recent posts