본문 바로가기
Language/Java

객체 지향 설계의 5대 원칙 (SOLID)

by jaee_ 2021. 9. 27.
본 글은 📚 스프링 입문을 위한 자바 객체지향의 원리와 이해 를 읽고 정리한 내용입니다.

 

객체 지향 설계의 5대 원칙이란 객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계해 나가는 방법이나 원칙을 말한다. 

 

객체 지향 설계 5원칙(SOLID)은 로 아래와 같다.

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

위의 원칙들은 응집도는 높이고, 결합도는 낮추라는 고전 원칙을 객체 지향 관점에서 재정립한 것이라고 할 수 있다. 

🔍 참고
좋은 소프트웨어 설계를 위해서는 결합도(coupling)을 낮추고 응집도(cohension)는 높이는 것이 바람직하다.

결합도는 모듈(클래스) 간의 상호 의존 정도로서 결합도가 낮으면 모듈 간의 상호 의존성이 줄어들어 객체의 재사용이나 수정, 유지보수가 용이하다. 

응집도는 하나의 모듈 내부에 존재하는 구성 요소들의 기능적 관련성으로, 응집도가 높은 모듈은 하나의 책임에 집중하고 독립성이 높아져 재사용이나 기능의 수정, 유지보수가 용이하다. 

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

 

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. - 로버트 C.마틴


클래스의 역할과 책임에 따라 분리해서 각각 하나의 역할과 책임만 갖게 하는 것이 단일 책임 원칙이다. 단일 책임 원칙은 속성, 메소드, 패키지, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있는 개념이다. 

 

단일 책임 원칙을 지키지 못하는 경우를 살펴보자.

 

속성이 단일 책임 원칙을 지키지 못하는 경우

  • 남자는 군대를 가고, 여자는 군대를 가지 않는다고 가정하자. 그런데 사람 클래스에 군번 속성이 있다면 여자의 군번에는 어떤 값을 입력해야할까? 단일 책임 원칙을 지키지 못하는 것이다. 이를 리팩터링 시키면 남자 클래스와 여자 클래스로 분할하고 남자 클래스에만 군번 속성을 갖게 하면 된다. 만약 여자 클래스와 남자 클래스에 공통점이 없다면 사람 클래스는 제거하면 되고, 공통점이 많다면 사람 클래스를 상위 클래스로 해서 공통점을 사람 클래스에 두고 남자 클래스와 여자 클래스는 사람 클래스를 상속하고 차이점만 각자 구현하면 된다.
  • 또한, 하나의 속성이 여러 의미를 갖는 경우도 단일 책임 원칙을 지키지 못하는 경우이다. 이를 지키지 못하면 분기처리를 위한 if문이 등장할 수 있다. 

아래의 예제를 통해 속성이 여러 의미를 갖는 경우의 이해를 도와보자.

public class 강아지 {

    final static Boolean 수컷 = true;
    final static Boolean 암컷 = false;
    Boolean 성별;

    void 배변활동() {
        if (this.성별 == 수컷) {
            // 한 쪽 다리를 들고 소변을 봄
        }else{
            // 앉은 자세로 소변을 봄
        }
    }
}

분기처리를 위해 if 문이 등장했다. 이를 단일 책임 원칙을 적용하여 리팩터링 해보자.

public abstract class 강아지2 {

    abstract void 배변활동();
}

class 암컷강아지 extends 강아지2 {

    @Override
    void 배변활동() {
        // 앉은 자세로 소변
    }
}

class 수컷강아지 extends 강아지2 {

    @Override
    void 배변활동() {
        // 한 쪽 다리를 들고 소변
    }
}

if 문을 적지 않아도 분기를 할 수 있는 것을 볼 수 있다. 

 

객체 지향 4대 특성과 단일 책임 원칙과 가장 관계가 깊은 것은 바로 모델링 과정을 담당하는 추상화이다. 애플리케이션의 경계를 정하고 추상화를 통해 클래스들을 선별하고 속성과 메소드를 설계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들이자. 또한 리팩터링을 통해 코드를 개선할 떄도 단일 책임 원칙을 적용할 곳이 있는지 꼼꼼히 확인하는 습관을 들이자. 


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

 

 

소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.  - 로버트 C.마틴

위의 말은 즉 자신의 확장에는 열려있고, 주변의 변화에 대해서는 닫혀있어야 한다는 말이다. 

개방 폐쇄 원칙의 대표적인 예로 데이터베이스 프로그래밍을 할 때 JDBC를 들 수 있다. JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL로 바뀌더라도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없다. 데이터베이스를 변경할 때 자바 애플리케이션은 JDBC 인터페이스라고 하는 완충 장치로 인해 변화에 영향을 받지 않는다. 즉, 자바 애플리케이션은 데이터베이스라고 하는 주변의 변화에 닫혀 있는 것이고, 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 것이다. 

 

 

 

 

또한, 자바에서도 개방 폐쇄 원칙이 적용되어 있다. 자바 개발자는 작성하고 있는 소스코드가 어느 운영체제에서 구동될지 모르지만 운영체제 별 JVM과 목적 파일(.class)가 있기 때문에 개발자는 운영체제에 대해서는 걱정하지 않아도 된다. 개발자가 작성한 소스코드는 운영체제의 변화에는 닫혀있고, 각 운영체제 별  JVM은 확장에 열려 있는 구조가 되는 것이다. 즉, 개발자의 소스코드와 운영체제별 JVM사이에는 목적 파일이라고 하는 완충 장치가 있는 것이다. 

 

개방 폐쇄 원칙을 무시하고 프로ㅓ그램을 작성하면 객체 지향 프로그래밍의 가장 큰 장점이 유연성, 재사용성, 유지보수성 등을 얻을 수 없다. 따라서 객체 지향 프로그래밍에서 개방 폐쇄 원칙은 반드시 지켜야 할 원칙이다. 


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

서브 타입은 언제나 자신의 기반 타입(base type)으로 교체할 수 있어야 한다.  - 로버트 C. 마틴

객체 지향에서의 상속은 조직도나 계층도가 아닌 분류도가 되어야 한다. 객체 지향의 상속은 다음의 조건을 만족해야 한다. 

  • 하위 클래스 is a kind of 상위 클래스 - 하위 분류는 상위 분류의 한 종류다.
  • 구현 클래스 is able to 인터페이스 - 구현 분류는 인터페이스할 수 있어야 한다.

위 두 개의 문장대로 구현된 프로그램이라면 이미 리스코프 치환 법칙을 잘 지키고 있다고 할 수 있다. 하지만 위의 문장대로 구현되지 않은 코드가 존재할 수 있는데 바로 조직도나 계층도 형태로 구축된 경우다. 아래의 코드를 보자

아버지 춘향이 = new 딸();

상위 클래스의 객체 참조 변수에는 하위 클래스를 할당할 수 있다. 그런데 위의 코드는 뭔가 이상해보이지 않는가? 춘향이는 아버지형 객체 참조 변수이기에 아버지 객체가 가진 행위(메소드)를 할 수 있어야 하는데 춘향이에게 아버지의 어떤 역할을 시킬 수 있을까? 이번에는 동물 클래스와 이를 상속(확장)하는 펭귄 클래스를 보자. 즉, 븐류도 형태인 경우를 살펴보자.

동물 뽀로로 = new 펭귄();

펭귄 한 마리가 태어나 뽀로로라 이름을 짓고 동물의 행위(메소드)를 하게 하는 데에 전혀 이상함이 없다. 아버지 - 딸 구조(계층도/조직도)는 리스코프 치환 원칙을 위배하고 있는 것이며, 동물 - 펭귄 구조(분류도)는 리스코프 치환 원칙을 만족하는 것이다. 

하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다. - 로버트 C. 마틴

분류도 형태의 상속인 경우 하위에 존재하는 것들은 상위에 있는 것들의 역할을 하는 데 전혀 문제가 없다. 즉, 리스코프 치환 원칙은 객체 지향의 상속이라는 특성을 올바르게 활용하면 자연스럽게 얻게 되는 것이다. 


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

클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안 된다.  - 로버트 C.마틴

단일 책임 원칙(SRP) 예제를 살펴보자.

다양한 책임을 가진 남자 클래스

 

이를 단일 책임 원칙을 적용해 단일 책임을 가진 여러 클래스로 분리하면 아래와 같다.

단일 책임 원칙을 적용해 남자 클래스를 단일 책임을 가진 여러 클래스로 분리

이는 인터페이스 분리 원칙으로도 해결이 가능하다. 아래의 사진을 보자.

인터페이스 분리 원칙을 적용한 남자 클래스

결론적으로 단일 책임 원칙(SRP)과 인터페이스 분리 원칙(ISP)은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다. 프로젝트 요구사항과 설계자의 취향에 따라 단일 책임 원칙이나 인터페이스 분리 원칙 중 하나를 선택해서 설계할 수 있다. 하지만 특별한 경우가 아니라면 단일 책임 원칙을 적용하는 것이 더 좋은 해결책이라고 할 수 있다. 

 

인터페이스 분리 원칙을 이야기할 때 항상 등장하는 원칙 중 하나로 인터페이스 최소주의 원칙이라는 것이 있다. 인터페이스를 통해 매소드를 외부에 제공할 때는 최소한의 메소드만 제공하라는 것이다. 남자친구 인터페이스에 사격하기() 메소드를 제공할 필요도 없고 제공해서도 안된다는 것이다. 

 

상위클래스는 풍성할수록 좋고 인터페이스는 작을수록 좋다고 한다. 그 이유를 좀 더 살펴보자.

 

우선 상위 클래스가 풍성할수록 좋은 이유를 먼저 살펴보자. 리스코프 치환 원칙(LSP)에 따라 하위 객체는 상위 객체의 행위(Method)를 할 수 있다. 상위클래스가 풍성할수록 불필요한 형변환을 할 필요가 없어지고, 추상메소드를 사용함으로써 하위클래스의 목적에 맞게 재정의할 수 있다. 

 

인터페이스는 그 역할에 충실한 최소한의 기능을 수행해야 한다. 객체 지향 세계에서도 같은 원리가 적용된다. 마지막으로, 인터페이스는 "~할 수 있는 (is able to)" 기준으로 만드는 것이 정석이라는 것을 꼭 기억하자. 


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

" 고차원 모듈은 저차원 모듈에 의존하면 안 된다. 이 두 모듈은 모두 다른 추상화된 것에 의존해야 한다. "
" 추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다. "

" 자주 변경되는 구체(Concrete) 클래스에 의존하지 마라. "
- 로버트 C.마틴

위의 말을 한마디로 정리하자면 "자신보다 변하기 쉬운 것에 의존하지 마라" 로 말할 수 있다. 

 

의존 역전 원칙 적용 전

위의 그림을 보면 자동차가 스노우타이어에 의존하고 있다. 자동차는 추상화된 개념으로 변하기 쉽지 않다. 반면, 스노우타이어는 타이어의 구체화된 버전으로 계절이 변할때 변경해줘야 한다. 즉 위의 그림은 추상화된 것이 구체적인 것에 의존하고 있는 그림이다. 이 그림에 의존 역전 원칙을 적용하면 아래의 그림과 같이 표현할 수 있다.

의존 역전 원칙 적용 후

타이어라는 추상적인 인터페이스를 만들고 자동차가 타이어에 의존하게 함으로써 스노우타이어에서 일반타이어로 변경해도 자동차는 이제 그 영향을 받지 않는 형태로 구성된다. 그리고 스노우 타이어는 그 누구에게도 의존하지 않았었는데 타이어에 의존함으로써 구제척인 것이 추상화된 것에 의존하는 형태가 되었다. 바로 의존의 방향이 역전된 것이다. 이처럼 자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어 변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다. 

 

상위 클래스일수록, 인터페이스일수록, 추상 클래스일수록 변하지 않을 가능성이 높기에 하위 클래스나 구체 클래스가 아닌 상위 클래스, 인터페이스, 추상 클래스를 통해 의존하라는 것이 바로 의존 역전의 원칙이다. 


정리 - 객체 지향 세계와 SOLID

SOLID는 객체 지향을 올바르게 프로그램에 녹여내기 위한 원칙이다. 객체 지향 4대 특성을 제대로 이해해야 SOLID를 제대로 이해하고 활용할 수 있다.

 

Soc(Separation Of Concerns)란 관심사의 분리를 말한다. 관심이 같은 것끼리는 하나의 객체 안으로 또는 친한 객체로 모으고, 관심이 다른 것은 가능한 한 따로 떨어져 서로 영향을 주지 않도록 분리하라는 것이다. 하나의 속성, 하나의 메소드, 하나의 클래스, 하나의 모듈, 또는 하나의 패키지에는 하나의 관심사만 들어 있어야 한다는 것이 SoC이다. 관심사가 다르고 변화의 시기가 다르면 분리해야 한다는 것이다. SoC를 적용하면 자연스럽게 단일 책임 원칙(SRP), 인터페이스 분리 원칙(ISP), 개방 폐쇄 원칙(OCP) 에 도달하게 된다. 스프링 또한 SoC를 통해 SOLID를 극한까지 적용하고 있다. 

 

  • SRP (단일 책임 원칙) : 어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. 
  • OCP (개방 폐쇄 원칙) : 자신의 확장에는 열려있어야 하고 주변의 변경에 대해서는 닫혀있어야 한다. 
  • LSP (리스코프 치환 원칙) : 서브 타입은 언제나 자신의 기본 타입으로 교체할 수 있어야 한다. (분류도)
  • ISP (인터페이스 분리 원칙) : 클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안된다. 
  • DIP (의존 역전 원칙) : 자신보다 변하기 쉬운 것에 의존하면 안된다. (구체화 -> 추상화에 의존)

 

댓글