좋은 객체 지향 설계의 5가지 원칙(SOLID)

Date:    Updated:

카테고리:

SOLID 란

클린코드로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 아래와 같이 정리하였다.

  • SRP(Single Responsibility Principle) : 단일 책임 원칙
  • OCP(Open/Closed Principle) : 개방/폐쇄 원칙
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
  • DIP(Dependency Inversion Principle) : 의존관계 역전 원칙

위와 같은 5가지 원칙 SOLID를 지키면 시간이 지나도 변경이 용이하고, 유지보수와 확장이 쉬운 소프트웨어를 개발하는데 큰 도움이 된다.

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

SRP에 대해 찾아보면 SRP는 문자 그대로 클래스는 하나의 책임만 가져야 한다. 로 정의하고 있다.

즉, 각각의 클래스들은 단 하나의 책임을 수행하는데 집중되어야 한다는 의미로 클래스를 변경하려는 이유또한 단 하나여야 한다.

SRP 적용 전

class Employee {
    public String getName(){...}
    public String getDepartment(){...}
    public void getCoffee(){...}
    public void printReport(){...}
    public void saveFile(){...}
}

위 코드의 경우 getName 기능을 수정하기 위해서도 Employee 클래스를 수정하여야 하고 printReport 기능을 수정하기 위해서도 같은 클래스를 수정하여야 한다.

또한 각 기능들의 관심사가 유사할수록 정보를 가져오거나 혹은 변경이 일어날때 어딘가에서 서로 연결될 확률이 높다.

이러한 결합은 많은 변경사항을 발생시키며 변경에 의한 연쇄작용으로 인해 결국 유지보수 및 테스트가 어려워지는 문제점이 발생한다.

SRP 적용 후

class Employee {
    public String getName(){...}
    public String getDepartment(){...}
    public void getCoffee(){...}
}

class Printer {
    public void printReport(){...}
}

class Computer {
    public void saveFile(){...}
}

단순히 관련 업무를 분리하는 것이 아니라 클래스 간의 관계 복잡도를 줄이도록 설계해야한다.

  • 하나의 책임이라는 것은 모호하다. 적절하게 범위를 설정하여야 한다.
    • 범위는 클 수 있고, 작을 수 있다.
    • 범위는 문맥과 상황에 따라 다르다.
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것 이다.
  • SRP 원칙을 적용 했을 때 응집도는 높이고 결합도를 낮추는 결과를 가져온다.

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

소프트웨어 구성요소(컴포넌트, 클래스, 모듈, 함수)는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

즉, 구성요소 변경 시 기존 구성요소의 수정은 최소화 되어야하고 확장을 통한 재사용이 가능해야 한다.

객체지향의 특징인 추상화와 다형성을 통해 OCP를 적용할 수 있으며, 이를 적용하면 재사용 및 관리 가능한 코드가 된다.

Java의 JDBC를 예로 들어보자.

image

위 그림처럼 Oracle, MySQL 등 DB의 종류가 다르더라도 JDBC Driver Manager가 제공하는 Driver 인터페이스의 구현체를 통해

Connection 객체를 구현함으로써 DB 종류에 구애받지 않고 접근이 가능하다. (확장-열림)

또한 인터페이스의 구현체를 통해 구현하였기 때문에 기존 구성요소를 수정하지 않고 각기 다른 종류의 DB를 관리할 수 있다. (변경-닫힘)

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

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

즉, 자식(하위) 타입의 객체는 부모(상위) 타입의 객체가 수행 가능한 행위에 대해서 수행할 수 있어야 하며

이를 위해 부모 클래스와 자식 클래스 사이는 행위가 일관되어야 한다.

image

그림속 Sam과 Eden을 클래스로 생각해보자.

Sam(부모)은 커피를 만들 수 있지만 Eden(자식)은 커피를 만들 수 없다.

Sam의 자식인(Sam을 상속받은) Eden에게도 똑같이 커피를 만들어 달라고 요구하면 Eden도 부모의 행위를 수행할 수 있어야 하기 때문에

자식인 Eden도 동일하게 커피를 만들어주는 행위를 할 수 있어야 한다.

정리하면 부모 클래스를 상속한 자식 클래스는 부모 클래스의 역할을 정확히 해내야한다는 뜻이다.

class Sam {
    public String makeCoffee(){
        return "Cappuccino";
    }

    ...
}

class Eden extends Sam {
    @Override
    public String makeCoffee(){
        // I can't make coffee..
        return "Sorry!";
    }
    ...
}

위에 기술 했듯이 LSP를 만족하려면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도 프로그램 동작에는 문제가 없어야 한다.

이를 위해 부모 클래스와 자식 클래스 사이는 행위(명세)가 일관되어야 한다.

LSP를 지키기 위한 가장 간단한 해결 방법은 부모 클래스를 상속 하되 Override 하지 않는 것이다.

Override를 하더라도 부모클래스에서 명세한 기능들은 충실히 수행하고 그 뒤 추가 기능에 대해 신중히 고민해봐야 한다.

이를 위반하게 되면 OCP의 확장에 열려야한다는 규칙에 위배되므로 설계시 충분히 고려해야 한다.

✔ ISP(Interface Segregation Principle) - 인터페이스 분리 원칙

ISP는 클라이언트가 자신이 이용하지 않는 메서드에 의존하면 안된다는 의미로

특정 객체(인터페이스)의 관계없는 명세는 분리하여 책임을 최소화하여야 한다는 뜻이다. (= Fat interface를 방지하자)

이는 앞서 기술한 SRP(단일 책임 원칙)과 비슷하며 해당 원칙을 지킬 시 인터페이스가 명확해지고, 대체 가능성이 높아진다.

✔ DIP(Dependency Inversion Principle) - 의존관계 역전 원칙

“추상화에 의존해야하며 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

쉽게 이야기해서 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 의미이다.

앞서 설명한 OCP에서 예로 든 JDBC를 대상으로 설명을 해보면

JDBC는 Driver 인터페이스의 구현체를 통해 구현하게 되는데

이때 Driver 인터페이스가 오로지 Oracle DB만 접근 가능하게끔 명세가 구성되어있다고 가정해보자.

image

그렇다면 JDBC는 확장에 열린 설계가 되지 못하므로(다른 DB는 사용 불가하므로) OCP 또한 위반하게 된다.

객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존하게 되면 변경이 아주 어려워 진다.

역할과 구현을 철저히 분리하여 의존 관계를 맺을 때 변화하기 쉬운 것에 의존하기보다는, 변화하지 않는 것에 의존하게끔 설계해야 한다.

📣 Reference

본 포스팅은 김영한님의 강의를 듣고 스스로 정리 및 추가한 내용입니다.

스프링 핵심 원리 - 기본편
SOLID 원칙 3 - LSP: 리스코프 치환 원칙 (Liskov Substitution Principle)
객체지향 5원칙 : SOLID
[SOLID] 개방 폐쇄 원칙(OCP)이란?

댓글남기기