프로그래밍/Java

[Java] 객체 지향 설계란? (SOLID)

Lim-Ky 2017. 8. 24. 02:08
반응형

이번 시간은 객체지향의 4대특성인

캡슐화, 상속, 추상화, 다형성 을 이용하여 객체 지향을 올바르게 설계할 수 있도록 도와주는 원칙들을 알아 볼까 한다.


객체 지향을 공부해봤고, 객체 지향으로 향 후 프로그래밍을 하려면 반드시 알아야 하는 중요한 원칙들이다. 굉장히 중요하기 때문에 하나하나 원칙들을 알아보자!


아무리 객체 지향 4대 특성인 캡슐화, 상속, 추상화, 다형성을 잘 한다고 해서 설계를 잘하는 것은 아니다. 물론 4대 특성의 묘미를 잘 살릴 수록 설계가 좋다. 하지만 설계 원칙이라는 게 엄연히 존재하고, 또 나름 중요하기 때문에 각 원칙의 맨 앞 알파벳만 따와 "SOLID" 라고까지 했을까?


SOLID 원칙들은 결국 자기 자신 클래스 안에 응집도는 내부적으로 높이고, 타 클래스들 간 결합도는 낮추는 High Cohesion - Loose Coupling 원칙을 객체 지향의 관점에서 도입한 것이다.

왜 그랬을까? 간단하다. 좋은 소프트웨어는 응집도가 높고 결합도가 낮기 때문이다.

결국 모듈 또는 클래스 당 하나의 책임을 주어 더욱더 독립된 모듈(클래스)을 만들기 위함이다.

이렇게 설계된 소프트웨어는 재 사용이 많아지고, 수정이 최소화 되기 때문에 결국 유지 보수가 용이해진다. 


자 그럼 객체 지향 설계 5대 원칙이 각각 무엇인지 아래를 보자.


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

이름으로 어느 정도 추론이 되는 원칙이 있고 아닌게 있다. 이제 하나하나 알아보자.


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

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

의역 : 클래스의 역할과 책임을 너무 많이 주지 마라.

즉 클래스를 설계할 때 어플리케이션의 경계를 정하고, 추상화를 통해 어플리케이션 경계 안에서 필요한 속성과 메서드를 선택하여 설계 해야 한다. 



예를 들어 사람이라는 클래스를 내가 정의한다고 해보자. 이름,나이,키,개발경력,개발언어,혈액형,병력 이라는 필드로 다 때려 박았다. (메서드는 생략한다)

이럴 경우 만약 개발스펙을 검토하는 어플리케이션이 있다고 치자. 개발스펙을 검토하는 어플리케이션에선 나는 사람이라는 클래스보다 지원자라는 클래스명이 더 어울리고, 필요한 필드는 개발경력, 개발언어 정도일 것이다. 

만약 병원어플리케이션이 있다고 치자. 병원어플리케이션에서 사람 클래스명 보다 환자라는 클래스명이 어울릴 것이고, 관심있는 필드는 이름,나이,키,혈액형,병력 정도 일 것이다. 

이렇게 사람이라는 클래스에 모든 사람에 관련된 모든 기능을 다 때려 박기보다 목적과 취지에 맞는 속성과 메서드로 구성 해야 한다. 즉 관련된 책임만 주라는 것이다. 이 말은 결국 SRP(단일 책임 원칙)은 추상화와 깊은 관련이 있다는 소리다. 

더 극단적인 경우를 생각해보자.


나의 인터넷 필명은 Limky(림키)이다. 나는 위와 같이 다양한 행위를 한다. 너무 많은 행위로 인해 상황에 맞는 행위를 하지 못하게 되면 어떻게 될까???
만약 내가 회사에서 개발하다() 를 하지 않고 잠자다() 를 했을 경우 나는 회사에서 짤 릴 것이다. 만약 내가 교회에 갔는데 기도하다()를 하지 않고 욕하다() 를 했을 경우 무슨 일이 일어날까 ㅎㅎ
즉 상황에 맞는 속성과 메서드로 책임을 적게 할당 해야 한다. 너무 많은 속성과 행위를 통한 과다 책임은 좋지 않다. 따라서 SRP(단일 책임 원칙)에 따라 각 상황에 맞게 나는 행동 해야 한다. 올바르게 다시 설계해보자.


Limky라는 클래스를 각 역할에 맞게 쪼개서 회사에서는 개발자 라는 클래스명으로 개발하다() 메서드만 구현하고, 교회에선 신도라는 클래스명으로 기도하다()라는 메서드를 구현해서 Limky라는 하나의 클래스로만 한꺼번에 구현하려 하지 말고, 역할에 맞게 클래스를 설계 해야 한다. 

또 한 가지 만약 상황에 맞는 적절한 행위가 이루어 지지 않는다면, 그로 인해 다른 속성과 메서드도 영향을 받게 되며 다른 상황에 맞는 행위가 지장 받게 될 수 도 있다.

더 나아가 속성, 메서드, 패키지, 모듈, 컴포넌트, 프레임워크 를 단일 책임을 주고, 독립적으로 모듈화 시키는 것이 바로 SRP(단일 책임 원칙) 이다. 


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


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


의역 : 자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다.


만약 A 드론과 연동하는 프로그램을 개발한다고 생각해보자.

하지만 새롭게 출시한 B 드론이 더 가격이 싸고 성능도 좋다. 그렇다면 A 드론과 연동한 프로그램 수정은 불가피하다. 이럴 경우 프로그램은 어느 회사의 제품 드론이 연동하던 프로그램의 수정을 최소화 하면서 확장적으로 다른 제품의 드론과 연동할 수 있어야 한다. 



만약 위에서 보는 것과 같이 직접적으로 각 사의 제품 클래스의 메서드를 호출하고 결합도 높게 설계를 했다면, 확장적이지 못할 뿐더러 많은 수정이 발생되어 유지 보수가 어려워진다.


따라서 OCP(개방 패쇄 원칙) 의거하여 수정해보자. 아래와 같이 상위클래스 또는 인터페이스를 중간에 두어 직접적인 연동은 피하게 설계한다. 드론연동프로그램은 따로 코드 수정이 없으면서도 다른 제품과의 연동엔 확장적이게 된다. 상위클래스나 인터페이스는 일종의 완충 장치인 것이다.



각 제품의 드론 클래스는 드론 interface를 implements하고 추상메서드인 날기(), 착지(), 상하좌우조작() 메서드를 @Override 해서 자기 입맛에 맞게 메서드를 재정의 하면 된다.


물론 예시가 그렇게 맘에 들진 않지만, 더 깔끔한 예시를 준비했다. 



ocp jdbc interface에 대한 이미지 검색결과


위 설계를 보면 PostgreSQL, Oracle, Sybase Database에 모두 확장적(개방)이면서 자바어플리케이션입장에서 수정은 폐쇄적인 것 임을 알아야 한다. 이것이 바로 OCP(개방 패쇄 원칙)이다.




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


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


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


의역2 : 즉 Upcating 된 객체 참조 변수가 논리적으로 그 역할이 문제가 없어야 한다.


LSP(리스코프 치환 원칙)은 인터페이스와 클래스 관계, 상위 클래스와 하위 클래스 관계를 얼마나 잘 논리적으로 설계 했느냐가 관건이다. 


상위 클래스와 하위 클래스는 무슨 관계인가?? 말 그대로 상위, 하위라고 불릴 수 있는 것은 상속관계가 성립된다는 뜻이며, 또 하위 클래스는 is a kind of 상위 클래스이다!! 즉 우리가 상속에 대해 오해하고있는 아버지와 아들 이런 계층적인 구조가 아니란 말이다. 상위,하위 클래스를 설계하는 것은 계층이 아니라 분류이다! 또한 상속 = 확장 이라고 생각 해야 한다.


잘못된 상속 관계 : 아버지와 아들 // 아들은 아버지의 한 종류이다??


올바른 상속 관계 : 포유류와 고래 // 고래는 포유류의 한 종류이다!!


또한 클래스와 인터페이스 관계를 설명하면, 구현 클래스는 인터페이스로 인해 인터페이스 할 수 있음을 뜻한다. 인터페이스 이름을 잘 보면 통상적으로 ~able을 붙인다는 것을 알 수 있다.

이 말은 ~할 수 있는 이라는 뜻이다. 예를 들어


Runnable : ~ 실행 할 수 있는

Closeable : ~ 닫을 수 있는

Openable : ~ 열 수 있는

Swimable : ~ 수영 할 수 있는


class 고래 implements Swimable(){

// 고래는 수영 할 수 있다. 

}


~able을 붙여서 문법적으로 안 맞는 것은 일단 신경쓰지 말고, 인터페이스는 위와 같이 ~able을 붙여서 네이밍하고 인터페이스를 implements한 구현 클래스는 인터페이스가 가지고 있는 기능을 할 수 있다로 생각하면 된다. 


자 LSP(리스코프 치환 원칙)에 대해서 다 배웠다. 처음 LSP(리스코프 치환 원칙)에 대해서 의역 한 말을 생각해보자.


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


자 LSP(리스코프 치환 원칙) 맞게 대입해보자. 자 다시 한번 LSP(리스코프 치환 원칙)은 하위클래스가 상위클래스 역할을 대신할 때 논리적으로 맞아 떨어져야 한다.


아버지클래스타입 홍길동 = new 아들(); // LSP(리스코프 치환 원칙) 위배.

포유류클래스타입  도커 = new 고래(); // 이 과정에서 자바는 내부적으로 자동 타입 변환을 해준다.


아들이 태어나 홍길동이라는 이름을 짓고 아버지의 행위를 한다??? 뭔가 어색하다..

고래 한마디라 태어나 도커라는 이름을 짓고 포유류의 행위를 한다. 깔끔하지 않은가?


객체 지향은 인간이 실세계를 보면서 느끼고 논리적으로 이해한 것과 똑같이 프로그래밍하는 게 목적이기 때문에 논리적으로 맞아 떨어져한다.




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


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


의역 : 상황과 관련 있는 메서드만 제공해라.

 

ISP(인터페이스 분리 원칙)은 SRP(단일 책임 원칙)과 같은 원인에 대한 다른 해결책을 제시하는 것이다. 너무 많은 책임을 주어 상황에 관련 되지 않은 메서드까지 구현했다면, SRP(단일 책임 원칙)은 그 클래스를 여러개의 클래스로 쪼개버린다. 하지만 ISP(인터페이스 분리 원칙)은 해당 클래스를 그냥 냅 두는 상태에서 인터페이스 최소주의 원칙에 따라 각 상황에 맞는 기능만 제공하도록 필터링 한다고 생각하면 쉽다. 자 글만 보면 이해가 안되기 때문에 그림을 보자.


Limky는 필자의 인터넷 필명이다.ㅎㅎ


만약 신도 역할만 해야 하는 상황에서 개발하기(), 요리하기(), 사격하기() 같이 관련 없는 메서드는 신도 역할에서 필요 없다. 다른 역할도 마찬가지다. 

이럴 경우 ISP(인터페이스 분할 원칙)을 적용하여, 각 역할에 맞는 메서드만 제공하도록 수정해보자.



어떤가? 깔끔하지 않은가. 각 상황에 맞게 인터페이스에서 제공해주는 메서드만 제공 받아 사용 할 수 있다. 코드로 이해해보자.


각각 인터페이스를 정의하고 상황에 맞는 메서드만 정의한다.



그 다음 다중 구현 (다중 상속같은 효과를 냄)을 통해 각각 메서드를 오바라이딩해서 재정의 한다.



자 만약 교회라는 공간에서 신도 역할을 해야 한다면!? 아래와 같이

참조 변수 타입을 Prayable 타입으로 정의 하고 Limky 인스턴스를 받으면 자동 타입 변환이 발생하면서 upcating 되기 때문에 limky 참조 변수는 기본적으로 상위클래스 즉 Prayable 의 역할만 하게 된다.


자동 타입 변환과 업캐스팅(upcating)에 대한 개념은 아래를 참고하세요.

2017/08/22 - [Java] - [Java] 타입 변환과 필드의 다형성




자 어떠한가?? 상황에 맞게 관련 메서드만 제한을 강제하여 사용할 수 있다.

ISP(인터페이스 분할 원칙)에는 또한 인터페이스 최소 주의와 함께 상위클래스는 풍성할 수록

인터페이스는 작을 수록 좋다는 개념이 있는데 따로 포스팅 할 예정이다.


(코딩적인 부분은 제가 생각한 것인데 혹시 잘못된 점 있으시면 말씀해주시기 바랍니다.)



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


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

"추상화 된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화 된 것에 의존해야 한다."

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


의역 : 자신보다 변하기 쉬운 것에 의존하지 마라.


자 마지막 원칙이다 힘들지만 집중하자. 이미 많은 원칙을 배웠기에 DIP(의존 역전 원칙)을 이해하는데 어렵지 않을 것이다. 말 그대로 자신보다 변하기 쉬운 것에 의존하지 마라는 말이다.

구체적으로 추상클래스 또는 상위클래스는 구체적인 구현클래스 또는 하위클래스에게 의존적이면 안된다. 왜냐면 구체적인 클래스는 코딩에 있어서 가장 전면적으로 노출되고 사용되기 때문에 변화에 민감하다. 만약 DIP(의존 역전 원칙)에 의해서 설계하지 않는다면, 구체화된 클래스가 수정될 때마다 상위클래스나 추상클래스가 변화 해야 하는데 또 그 상위 또 그 상위 계속 연관 되어 있는 클래스들이 수정되어야 한다. 따라서 하위클래스나 구체클래스에게 의존하면 안된다.


DIP(의존 역전 원칙)을 지키지 않는 설계를 보자. 마땅한 예제를 못 찾아 책을 참고했다.

출처 : 스프링을 위한 자바 객체 지향의 원리와 이해


문제점이 보이는가? 벤츠는 스노우타이어를 장착하고 있다. 하지만 스노우 타이어는 계절에 영향을 받기 때문에 겨울 폭설이 오는 경우가 아니면, 다른 타이어로 교체하는 것이 이치에 맞다.

벤츠라는 클래스가 자신보다 더 변화에 민감한 스노우 타이어를 의존하고 있는 것이다. 이 의존의 일방적인 방향을 역전 시켜보겠다. 



자신보다 변하기 쉬운 것에 의존하던 것을 추상화 된 인터페이스나 상위클래스를 두어 변하기 쉬운 것의 변화에 영향 받지 않게 의존 방향을 역전시켰다. 즉 벤츠클래스는 타이어 인터페이스에 의존하면서 직접적으로 스노우,일반,광폭 타이어와 의존하는 것을 피했다. 또 스노우, 일반, 광폭 타이어는 기존에는 어떤 것도 의존하지 않았지만, 인터페이스를 의존 해야 한다. 이것이 DIP(의존 역전 원칙)이다. 


또 한 가지 알아 둘 것은 상위로 갈 수록 더 추상적이고 변화에 민감하지 않고 수정 가능성이 낮아진다는 사실도 알아두면 좋다.


근데 여기서 잠깐!! 굉장히 비슷 비슷한 설계 구조다 바로 OCP(개방 폐쇄 원칙)을 설명할 때 나온 방법이다. 결국 하나의 설계 원칙 안에 다른 설계 원칙이 녹아져 있는 경우가 많다. 



정리


자 스크롤 압박을 잘 견디고 객체 지향 설계 5원칙을 배웠다.

객체 지향 OOP의 4대 요소인 캡슐화, 상속, 추상화, 다형성이라는 재료와 객체 지향 설계 SOLID 원칙을 의거해 객체 지향 설계를 어떻게 하는지 배웠다. 결국 설계를 잘 해야 프로그램 유지 보수 측면에서 굉장히 용이하고 개발 시간과 비용을 절감할 수 있다. 이 부분은 실무에서 정말 중요하다. 아무튼 상당히 중요한 부분이기 때문에 몇 번이고 곱씹고 다른 예제도 많이 참고해서 자신의 것으로 만들길 바란다!!







반응형