객체 지향 설계 5원칙 - SOLID


높은 응집도와 낮은 결합도

개방 폐쇄 원칙은 높은 응집도와 낮은 결합도라는 소프트웨어 개발의 원리로 설명이 가능하다.
응집도가 높다는 것은 하나의 모듈, 클래스가 하나의 책임 또는 관심사에만 집중되어 있다는 뜻이다.
불필요하거나 직접 관련이 없는 외부의 관심과 책임이 얽혀 있지 않으며, 하나의 공통 관심사는 한 클래스에 모여 있다.
높은 응집도는 클래스 레벨뿐 아니라 패키지, 컴포넌트, 모듈에 이르기까지 그 대상의 크기가 달라도 동일한 원리로 적용될 수 있다.


1. SRP - 단일 책임 원칙

“어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다.”

역할(책임)을 분리하라는 것이 단일 책임 원칙이다.
단일 책임 원칙은 속성, 메서드, 패키지, 모듈, 컴포넌트, 프레임워크 등에도 적용할 수 있다.

단일 책임 원칙은 잘된 경우보다 잘못된 경우를 살펴보는 게 이해하는 데 좋다.

여자는 반드시 대학교에 가고 남자는 절대로 가지 못하며 사람 클래스에 학번 속성이 있다고 가정하자.

class 사람 {  
    String 학번;  
}  

사람 여자 = new 사람();  
사람 남자 = new 남자();  

남자.학번 = "201732017";  

사람형 참조 변수 남자가 가진 학번 속성에 값을 할당하거나 읽어 오는 코드를 제제할 방법이 없다.


하나의 속성이 여러 의미를 갖는 경우도 단일 책임 원칙을 지키지 못하는 경우이며 if문을 여기저기 사용해야 할 수도 있다.

강아지 클래스를 만들고 소변보다() 메서드를 구현했다고 해보자.

class 강아지 {  

    final static Boolean 암컷 = ture;  
    final static Boolean 수컷 = false;  

    void 소변보다() {  
        if(this.성별 == 수컷) {   
            // 한쪽 다리를 들고 소변을 본다.   
        } else {  
            // 다리를 들고 소변을 보지 않는다.     
        }  
    }  

}    

강아지가 암컷이냐 수컷이냐에 따라서 소변보다() 메서드에서 분기 처리가 진행된다.
소변보다() 메서드가 암컷과 수컷 강아지의 행위를 전부 구현하려고 하기 때문에 단일 책임 원칙을 위배하고 있는 것이다.
메서드가 단일 책임 원칙을 지키지 않을 경우 나타나는 대표적인 상황이 분기 처리를 위한 if문이다.

단일 책임 원칙과 객체 지향 4대 특성은 어떻게 결부돼 있을까?
단일 책임 원칙과 가장 관계가 깊은 것은 바로 모델링 과정을 담당하는 추상화임을 알 수 있다.
애플리케이션의 경계를 정하고 추상화를 통해 클래스들을 선별하고 속성과 메서드를 설계할 때 반드시 단일 책임 원칙을 고려하는 습관을 들이자.


2. OCP - 개방 폐쇄 원칙

“소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에 대해서는 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다.”
→ 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.

            자바 애플리케이션    
                ^   
            JDBC 인터페이스    
                ^   
JDBC 드라이버(오라클), JDBC 드라이버(MySQL), JDBC 드라이버(MS-SQL)    
                ^     
        오라클, MySQL, MS-SQL    

JDBC를 사용하는 클라이언트는 데이터베이스가 오라클에서 MySQL로 바뀌더라도 Connection을 설정하는 부분 외에는 따로 수정할 필요가 없다.
자바 애플리케이션은 JDBC 인터페이스라고 하는 완충 장치로 인해 변화에 영향을 받지 않는다.
데이터베이스라고 하는 주변의 변화에 닫혀 있는 것이다. 데이터베이스를 교체한다는 것은 데이터베이스가 자신의 확장에는 열려 있다는 것이다.

개방 폐쇄 원칙을 무시하고 프로그램을 작성하면 객체 지향 프로그래밍의 가장 큰 장점인 유연성, 재사용성, 유지보수성 등을 얻을 수 없다.


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

“서브 타입은 언제나 자신의 기반 타입으로 교체할 수 있어야 한다.”

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

“하위 클래스의 인스턴스는 상위형 객체 참조 변수에 대입해 상위 클래스의 인스턴스 역할을 하는 데 문제가 없어야 한다.”

리스코프 치환 원칙


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

“클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다.”

단일 책임 원칙에서 제시한 해결책은 클래스를 토막내서 하나의 역할(책임)만 하는 다수의 클래스로 분할하는 것이었다.
그런데 꼭 그 방법뿐일까? 다른 선택 방법은 바로 ISP 즉, 인터페이스 분할 원칙이다.
결론적으로 단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)은 같은 문제에 대한 두 가지 다른 해결책이라고 볼 수 있다.

인터페이스 분할 원칙을 이야기할 때 등장하는 원칙 중 하나로 인터페이스 최소주의 원칙이라는 것이 있다.
인터페이스를 통해 메서드를 외부에 제공할 떄는 최소한의 메서드만 제공하라는 것이다.
상위 클래스는 풍성할수록 좋고, 인터페이스는 작을수록 좋다고 했다. 그 이유를 살펴보자.

리스코프 치환 원칙(LSP)에 따라 하위 객체는 상위 객체인 척 할 수 있다.

Class 사람 {
    String 이름;
    void 먹다() { . . . }
}

Class 학생 extends 사람 {
    String 생일;
    String 주민등록번호;
    String 학번;

    void 자다() { . . . }
    void 공부하다() { . . . }
}

Class 군인 extends 사람 {
    String 생일;
    String 주민등록번호;
    String 군번;

    void 자다() { . . . }
    void 훈련하다() { . . . }
} 
Class 사람 {
    String 이름;
    String 생일;
    String 주민등록번호;

    void 먹다() { . . . }
    void 자다() { . . . }
} 

Class 학생 extends 사람 {
    String 학번;

    void 공부하다() { . . . }
}

Class 군인 extends 사람 {
    String 군번;

    void 훈련하다() { . . . }
} 
public static void main(String[] args) {
    사람 김학생 = new 학생( . . . );
    사람 이군인 = new 군인( . . . );

    System.out.println(김학생.생일); // 사용불가
    System.out.println(이군인.생일); // 사용불가 

    System.out.println((학생)김학생.생일); // 캐스팅 필요 
    System.out.println((군인)이군인.생일); // 캐스팅 필요 

}

빈약한 상위 클래스를 이용한 경우 여기저기 형변환이 발생하면서 상속의 혜택을 제대로 누리지 못하고 있다.

public static void main(String[] args) {
    사람 김학생 = new 학생( . . . );
    사람 이군인 = new 군인( . . . );

    System.out.println(김학생.생일); 
    System.out.println(이군인.생일); 
  
    (학생)김학생.공부하다(); // 캐스팅 필요     
    (군인)이군인.훈련하다(); // 캐스팅 필요      
}

학생과 군인이 자는 행위가 똑같다고 볼 수는 없다. 하지만 기능은 둘 다 필요하다.
이 경우 사용할 수 있는 객체 지향 기법이 바로 추상 메서드다.


5. DIP - 의존 역전 원칙

“고차원 모듈은 저차원 모듈에 의존하면 안 된다. 두 모듈 모두 다른 추상화된 것에 의존해야 한다.”
“추상화된 것은 구체적인 것에 의존하면 안 된다. 구체적인 것이 추상화된 것에 의존해야 한다.”
“자주 변경되는 구체(Concrete) 클래스에 의존하지 마라.”

자동차와 스노우타이어 사이에 다음과 같은 의존 관계가 있다.

자동차 -> 스노우 타이어
자동차가 타이어에 의존한다.

그런데 자동차가 일반 타이어로 교체해야 한다고 가정해보자.
이런 경우 스노우 타이어를 일반 타이어로 교체할 때 자동차는 그 영향에 노출돼 있음을 알 수 있다.

자동차는 자기 자신보다 더 자주 변하는 스노우타이어에 의존하는 나쁜 관계를 갖고 있다.
다음과 같이 개선해보자.

자동차 --------> <<interface>> 타이어 
                        ^ 
          [스노우타이어][일반타이어][광폭타이어]

자동차가 구체적인 타이어들이 아닌 추상화된 타이어 인터페이스에만 의존하게 함으로써 자동차는 이제 타이어에 영향을 받지 않는 형태로 구성되었다.
이 설명은 바로 개방 폐쇄 원칙(OCP)에서도 나온 설명이다.

기존에는 스노우타이어가 그 무엇에도 의존하지 않는 클래스였는데
추상적인 것인 타이어 인터페이스에 의존하게 됐다. 이를 의존의 방향이 역전되었다고 한다.
자신보다 변하기 쉬운 것에 의존하던 것을 추상화된 인터페이스나 상위 클래스를 두어
변하기 쉬운 것의 변화에 영향받지 않게 하는 것이 의존 역전 원칙이다.


6. 정리