p176. 5장 객체 지향 설계 5원칙 - SOLID

 

 

객체지향 설계에서 중요한 개념인 객체지향의 5원칙에 대해서 알아보자. 책을 보면서 이해한 내용을 위주로 정리한 것이며, 그림으로 이해한 것은 책의 그림을 옮겨와서 정리하도록 하겠다.

 

 

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

 

 

 

1. SRP : 단일 책임 원칙

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

역할과 책임이 넘쳐나는 남자클래스....

위의 그림과 같은 남자 클래스가 있다고 가정해보겠다. 남자클래스는 역할과 책임이 너무 많다. 여자친구 클래스에 대해서는 남자친구역할만 해야하는데 어머니 클래스에 대한 아들역할도 동시에 해야하는 상황인 것이다. 

남자클래스의 메소드가(해야할 일) 너무 넘쳐난다... 이와 같은 설계는 단일 책임 원칙에 위반된 설계라고 할 수 있겠다.

 

남자 클래스 분리

남자 클래스의 역할과 책임을 분리해주기 위해서 역할별로 여러 클래스로 나누었다. 이제 다른 클래스에 대한 역할은 신경쓰지 않게 됨으로써 남자 클래스는 부담을 최소화 하였다. 이와 같은 설계원칙이 SRP이다.

 

이번엔 코드로 다른 예를 살펴보자.

 

class 사람{
	String 군번;
	...
}

사람 철수 = new 사람();
사람 영희 = new 영희();


영희.군번 = "1573007545";

 

남자는 무조껀 군대를 가야하고, 여자는 절대로 군대를 갈 수 없는 세상이라고 가정을 해보자. (ㅠ.ㅠ)

사람 클래스에 군번이라는 프로퍼티가 있으면 여자인 영희의 군번을 삽입하는 위의 코드는 이상하다.

 

class 남자{
	String 군번;
    ...
}

class 여자{
	...
}


남자 철수 = new 남자();
여자 영희 = new 여자();


남자.철수 = "1573007545";

 

사람 클래스를 남자클래스와 여자클래스로 나누었고, 사람클래스가 지어야할 역할과 책임을 두개의 클래스로 나눔으로써 줄여주었다. 남자클래스와 여자클래스에 공통으로 들어가는 프로퍼티가 있으면 사람 클래스를 상위클래스로 만들어서 살려놓으면 되고 그게 아니면 없애면 되겠다.

 

 

 

 

 

2. OCP : 개방 폐쇄의 원칙

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

OCP를 적용한 예

 

 

운전자 클래스가 있다고 가정해보자. 운전자 클래스 마티즈라는 클래스를 이용하고 있었다. 마티즈 클래스에는 수동창문개방() 과 수동기어조작() 메소드가 존재하였다. 그런데 어느날 운전자 클래스가 쏘나타 클래스로 사용을 변경하였다!!

쏘나타 클래스는 마티즈 클래스와 다르게 자동창문개방() 과 자동기어조작() 메소드를 가지고 있다.

그러면 운전자 클래스는 자동차를 바꾸었으니 알아서 메소드에 적응하여야 할까???

 

운전자 클래스는 자동차라는 상위클래스를 둠으로써 OCP를 적용한 설계를 할 수 있다.

자동차 클래스는 모든 자동차가 공통적으로 가지는 창문개방() 과 기어조작() 메소드를 가지고 있고, 하위 클래스들은 상위 클래스인 자동차 클래스를 상속함으로써 이러한 메소드를 오버라이딩하여 사용할 수 있다.

 

이 때, 자동차 입장에서는 하위 클래스로의 확장에 개방적이고, 운전자 입장에서는 변화에 대해서 폐쇄적이게 된다.

이러한 원칙이 바로 OCP 원칙이다.

 

 

또 다른 예를 상상해보자.

우리는 자바를 사용한다. 자바는 JVM이라는 가상머신이 존재하며 이는 운영체제별로 배포하여 사용자가 어느 운영체제에서 사용하든 .class파일(목적파일)만 있으면 된다.

이 때, 운영체제별 JVM은 확장에 대해서 개방적이고, 개발자의 코드는 변화에 대해서 폐쇄적이다.

 

 

 

 

 

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

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

 

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

이는 클래스와 인터페이스를 한 번에 이해할 수 있는 문장이다.

여기서 가장 중요한 점은 하위 클래스는 상위클래스의 한 종류 라는 것이다. 이 문장에서 유추할 수 있는 것은 상속은 계층도/조직도의 개념으로 이해하는것이 아니라 분류도의 개념으로 이해해야 한다는 것이다.

이게 무슨소리일까? 사진한개로 정리가 가능하다.

상속은 분류도로 이해해라

 

위의 그림을 보면서 생각을 해보자.

 

아버지 아기상어 = new 아들(); 

 

이게 말이 될까? 아들이 생겨서 아기상어라고 칭하는것까지 그럴수있는데 아기상어가 아버지의 역할을 맡고 아버지의 행동(메소드)를 하려고 하고있다...

 

어류 아기상어 = new 상어();

 

상어를 아기상어라 칭하며, 그 아기상어는 어류가 하는 행동(메소드)를 하게 하는것은 논리적으로 이상하지 않다.

이처럼, 상속은 계층도/조직도가 아닌 분류도로 이해하면 된다.

 

 

이와 같이 분류도인 경우로 상속을 이해한다면 하위에 존재하는 것을 상위에 있는 것들의 역할로 대체하는 것이 전혀 문제가 되지 않는다는 것이 감이 올 것이다. 그게 바로 LSP이다.

 

 

 

 

 

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

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

1번에 사용했던 원칙은 SRP였다. 그 예제를 다시 가져와서 생각해보자.

역할과 책임이 넘쳐나는 남자클래스....

우리는 이 역할과 책임이 넘쳐나는 남자클래스를 토막토막 잘라서 SRP원칙을 위배하지 않는선에서 설계해보았었다. 

조금 다른 방법으로 인터페이스를 활용해보자.

 

사진이 좀 깨지지만.... 남자 클래스의 역할을 나누어 인터페이스화 한다는 그림이다.

 

(책의 저자가 한 말이 너무 재밌어서 그대로 가져왔다 ㅋㅋㅋ) 남자 클래스를 토막내는 것이 아니라 자아 붕괴(?) 또는 다중 인격화(?) 시켜서 여자친구를 만날 때는 남자친구 역할만 할 수 있게 인터페이스를 제한하고, 어머니와 있을 때는 아들 인터페이스로 제한하고... 이렇게 하는 것이 바로 인터페이스 분할 원칙의 핵심이다.

 

단일 책임 원칙(SRP)과 인터페이스 분할 원칙(ISP)는 같은 문제에 대한 두가지 해결 방안이라고 볼 수 있다. 상황에 맞는 방안으로 해결하면 된다. (특별한 상황이 아니면 SRP을 적용하는 것이 더 좋은 해결책이라고 한다!!)

 

 

 

 

 

5. DIP : 의존 역전 원칙

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

 

스노우타이어 클래스에 의존하는 자동차 클래스

자동차 클래스가 스노우타이어 클래스에 의존하고 있다고 가정해보자. 스노우타이어는 겨울에만 사용되고 계절이 바뀌면 일반타이어로 바꾸어 껴주어야 한다. 자동차 클래스는 변화가 잦게 일어나는 클래스에 현재 의존하고 있는 상황이다.

 

 

추상화된 타이어 인터페이스에 의존하는 자동차 클래스

이는 추상화된 타이어 인터페이스에 자동차클래스를 의존시킴으로써 해결할 수 있다. 스노우타이어 클래스, 일반타이어 클래스, 광폭타이어 클래스는 각각 추상적인 타이어 인터페이스를 구현하는 클래스가 되었다.

 

 

 

기존에 스노우 타이어 클래스는 아무것이도 의존적이지 않는 것이었는데,  DIP원칙을 적용하여 리팩토링하면 추상적인 타이어 인터페이스에 의존적이게 되었다. 이로써 의존의 방향이 역전된 것이다. 이것이 DIP원칙이다.

토비의 스프링 ~p60

 

 

 

 

초난감 DAO를 리펙토링 하는 과정에서 알 수 있었던 템플릿 메소드 패턴과 팩토리 메소드 패턴.

 

 

리펙토링?

기존의 코드를 외부의 동작방식에는 변화 없이 내부 구조를 변경해서 재구성하는 작업 또는 기술

 

 

템플릿 메소드 패턴?

이는 상속을 통해 슈퍼클래스의 기능을 확장할 때 사용하는 가장 대표적인 방식이다. 변하지 않는 기능은 슈퍼클래스에서 만들어두고 자주 변경되며 확장할 기능은 서브클래스에 만들도록 한다.

슈퍼클래스에 기본적인 로직의 흐름을 만들고(커넥션 가져오기, SQL 생성, 실행, 반환), 그 기능의 일부를 추상 메소드나 오버라이딩이 가능한 protected 메소드 등으로 만든 뒤, 서브클래스에서 이런 메소드를 필요에 맞게 구현해서 사용하도록 하는 방식이다.

 

 

팩토리 메소드 패턴?

템플릿 메소드 패턴과 마찬가지고 상속을 통해 기능을 확장하게 하는 패턴이다. 슈퍼클래스 코드에서는 서브클래스에서 구현할 메소드를 호출에서 필요한 타입의 오브젝트를 가져와 사용한다. 

UserDAO에서 getConnection()메소드는 Connection타입 오브젝트를 생성한다는 기능을 정의해놓은 추상 메소드이다. 그리고 UserDAO의 서브클래스의 getConnection() 메소드는 어떤 Connection클래스의 오브젝트를 어떻게 생성할 것인지 결정하는 방법이다. 이렇게 서브클래스에서 구체적인 오브젝트 생성방법을 결정하게 하는 방식이다.

UserDAO는 Connection 인터페이스 타입의 오브젝트라는 것 이외에는 관심을 두지 않는다. 그저 Connection 인터페이스에 정의된 메소드를 사용할 뿐이다. 이를 사용하는 클래스는 서버의 DB커넥션 풀에서 가져올 수도 있고, 드라이버를 직접 이용해 새로운 DB커넥션을 만들수도 있고...

 

발췌 : 토비의 스프링 3.1 <UserDao의 팩토리 메소드 패턴>

 

+ Recent posts