05. 객체 지향 설계 5원칙 - SOLID
객체 지향과 관련된 검색을 하면 SOLID원칙이 종종 발견이 되는데요. 이번 장에서 이 SOLID원칙이 어떤 것을 의미하는지를 다루고 있습니다. 물론 5가지 원칙을 이해하는 것과 이것을 적용하는 것과는 별개의 문제이지만 그래도 이를 잘 숙지하고 있어야 필요에 따라 잘 적용할 수 있다고 생각합니다. 먼저 5원칙은 다음을 의미합니다.
SRP(Single Responsibility Principle) : 단일 책임 원칙
OCP(Open Close Principle) : 개방 폐쇄 원칙
LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
DIP(Dependency Inversion Principle) : 의존 역전 원칙
이렇게만 살펴보면 전혀 감이 안오지 않나요? 갑자기 개방 폐쇄가 왜 나오고, 역전이 왜 나오며, 리스코프는 또 누구인지.. 하나씩 예를 들어가면서 설명하겠습니다.
SRP - 단일 책임 원칙
"어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다" - 로버트 C.마틴
단일 책임 원칙은 하나의 클래스에 하나의 책임, 역할을 부여하라는 것입니다. 책에 이와 관련된 좋은 예가 있어서 이를 인용하겠습니다.1
class 강아지 {
final static Boolean 수컷 = true;
final static Boolean 암컷 = false;
Boolean 성별;
void 소변보다() {
if(this.성별 == 수컷) {
// 한쪽 다리를 들고 소변을 본다.
} else {
// 뒷다리 두 개를 굽혀 앉은 자세로 소변을 본다.
}
}
}
위 코드의 경우 강아지에 암컷, 수컷의 역할을 모두 부여하고 있습니다. 이는 단일 책임 원칙을 지키지 않은 것입니다. 따라서 위 코드를 다음과 같이 리팩토링 할 수 있습니다.
abstract class 강아지 {
abstract void 소변보다()
}
class 수컷강아지 extends 강아지 {
void 소변보다() {
// 한쪽 다리를 들고 소변을 본다.
}
}
class 암컷강아지 extends 강아지 {
void 소변보다() {
// 뒷다리 두 개를 굽혀 앉은 자세로 소변을 본다.
}
}
강아지 추상 클래스를 상속받음으로써 수컷과 암컷의 역할을 나눈 것입니다. 위 과정을 보면 알 듯이 단일 책임 원칙은 모델링 과정과 관련이 있습니다. 따라서 애플리케이션 경계를 정하고 추상화를 통해 클래스를 선별하고 속성과 메서드를 설계할 때 이와 같은 원칙을 고려하여야 합니다.
OCP - 개방 폐쇄 원칙
"자신의 확장에는 열려 있고, 주변의 변화에 대해서는 닫혀 있어야 한다"
자신의 확장은 어떤 것이고, 주변의 변화란 어떤 것일까? 이 원칙 또한 책에 있는 예를 바탕으로 설명해보겠습니다2. 한 운전자가 있습니다. 이 운전자는 마티즈를 샀습니다. 이 마티즈는 창문수동개방()
, 기어수동조작()
이라는 기능을 할 수 있습니다 .몇 년 뒤 돈을 모아 이 운전자는 쏘나타를 샀는데요. 이 쏘나타는 창문자동개방()
, 기어자동조작()
이라는 기능을 제공합니다. 그럼 운전자는 운전하던 습관을 다 바꿔야할까요? 운전자의 입장에서 주변 변화(자동차의 변경)에 따른 변화가 요구됩니다. 하지만 객체지향적으로 다른 해법이 있습니다.
객체지향적으론 상위 클래스 또는 인터페이스를 중간에 두어 차의 종류가 바뀌더라도 운전자에 영향을 받지 않게 합니다. 즉 위에서 말씀드린대로 서술하면 자동차 입장에서는 자신의 확장에 열려있고(다양한 차 종류), 운전자 입장에서는 주변의 변화(차 종류의 변경)에는 폐쇄되어 있는 것입니다.
실제 이 예와 더불어 언급하고 있는 것이 자바와 데이터베이스 사이에 있는 인터페이스에 대해서 입니다. 데이터베이스의 종류는 아주 다양하고 또 바뀔지도 모릅니다. 따라서 그 사이에 인터페이스를 두어 데이터베이스 인터페이스 입장에서 다양한 데이터베이스 종류에 대해선 열려있고, 데이터베이스를 사용하는 쪽의 입장에서는 데이터베이스의 변화에 폐쇄되어 있는 것입니다.
LSP - 리스코프 치환 원칙
"서브 타입은 언제나 자신의기반 타입(base type)으로 교체할 수 있어야 한다" - 로버트 C.마틴
이 부분은 앞서 상속에 관해 설명할 때도 언급하였던 부분입니다. 즉 하위 클래스의 인스턴스는 상위 객체 참조 변수에 대입하였을 때 상위 클래스의 인스턴스 역할을 하는데 문제가 없어야 한다는 것입니다.
아버지 춘향이 = new 딸();
위 관계가 상속관계가 아니라는 것도 리스코프 치환 원칙과 연관하여 생각하면 이해됩니다. 하위 클래스인 딸의 인스턴스를 상위 클래스인 아버지 인스턴스로 대입하였을 때 문제가 없어야한다는 것인데, 딸이 아버지 역할을 한다는게 말이 안되는거죠. 연극도 아니구요.
동물 뽀로로 = new 펭귄();
위 관계는 상속관계인데요. 위와 똑같은 원칙을 적용해보면 하위 클래스인 펭귄의 인스턴스를 상위 클래스인 동물 인스턴스로 대입했을 때 문제가 없어야 합니다. 펭귄이 동물의 역할을 할 수 있다는 점에서 이 원칙이 성립하는 것이죠.
ISP - 인터페이스 분리 원칙
"클라이어트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안 된다" - 로버트 C.마틴
맨 처음 원칙 '단일 책임 원칙'에서 하나의 클래스에 다양한 역할(책임)이 있을 때 하나의 역할(책임)만 할 수 있도록 클래스 분리를 했습니다. 이 때 분리를 할 경우 위의 예와 같이 각각의 클래스를 따로따로 만들 수 있지만(암컷 강아지 클래스, 수컷 강아지 클래스), 여러 인터페이스로 분할(암컷 강아지 인터페이스, 수컷 강아지 인터페이스)하여 역할에 맞게 제한할 수도 있습니다! 같은 문제에 대해서 두 가지의 해법이 존재하는 것이죠3.
앞서 상속에서도 의문이 있던 부분인데요. 인터페이스 분리 원칙하면 이 얘기가 항상 따라나온다고 합니다. 왜 그럴까요?
상위 클래스는 풍성할수록 좋고, 인터페이스는 작을수록 좋다
먼저 상위 클래스를 풍성하다는 말은 하위 클래스에서 같은 성격, 성질의 변수, 메서드가 공통적으로 사용되고 있는 부분을 최대한 상위 클래스에서 구현하라는 것입니다. 예를들어 다음과 같은 빈약한 상위 클래스와 이를 상속받는 하위 클래스들이 있다고 생각해봅시다.
물론 극단적인 예이긴 하지만 위 그림을 보면 상위 클래스에서 공통으로 변수를 선언하고 추상 메서드 등으로 정의할 수 있는 부분이 몇 가지(주민등록번호, 자다() 등등) 보이는데도 불구하고 하위 클래스에 각기 구현한 것을 볼 수 있습니다. 이렇게 구현 후 객체생성을 하위 클래스로 하고 객체참조변수 타입을 상위 클래스로 하였을 경우 불편한 상황이 발생합니다. 왜냐하면 하위 클래스에 있는 메서드나 변수를 사용하기 위해선 상위 클래스 타입으로는 사용할 수 없으니 캐스팅이 필요하기 때문입니다. 사실 공통된 부분이었기 때문에 상위 클래스에 선언해두면 다형성에 의해 자식 클래스에서 구현한대로 실행되는데 말이죠. 이는 상속의 혜택을 제대로 못 누리고 있는 것입니다.
이제는 왜 인터페이스를 최소화해야하는지 알아보겠습니다. 이전까지 책임(역할)을 나누기 위해 여러 클래스로 분리하거나 다양한 인터페이스로 분리할 수 있다고 말씀드렸는데요. 이 때 다양한 책임(역할)을 나누기 위해 인터페이스로 분리하였다고 생각해봅시다. 이 경우 어떤 클래스에서 많이 정의되어 있는 것을 인터페이스를 상속받아 사용하면 그 클래스는 버그가 생길 가능성이 높습니다. 예를들어 사람이라는 클래스에서 '학생'이라는 인터페이스도 상속받고, '남자친구'라는 인터페이스를 받았을 때, 그리고 '학생'과 연결되는 '선생님'이 '남자친구'와 연결되는 '여자친구'와 동일한 사람으로 생각해봅시다. 이 경우 인터페이스로 받으면 구현을 강제하기 때문에 '학생'과 '남자친구' 부분을 구현하고 사용하게 됩니다. 이 때 '남자친구', '선생님' 역할과 상충되는 부분으로 인해 드라마에서 볼 듯한 '너는 학생이고, 난 선생이야' 같은 갈등(모순) 상황이 발생할 수 있는 것입니다. 따라서 그 역할에 충실한 최소한의 기능만 공개하여 사용하기를 저자는 강조합니다.
DIP - 의존 역전 원칙
"자신보다 변경하기 쉬운 것에 의존하지 마라"
이것 역시 책의 예시를 보면서 생각해봅시다. 4
위 그림을 보면 기존에 '자동차'는 '스노우 타이어'에 의존하였습니다. 그런데 겨울이 지나고 '스노우 타이어'에서 다른 타이어로 바뀌면 어떻게 할까요? 이렇게 될 경우 '자동차'에서 다른 타이어로 바꿔주기 위한 변경이 요구됩니다. 하지만 의존역전원칙을 적용해 좀 더 추상적인 '타이어'라는 인터페이스에 의존하게 하면 '자동차'에서 변경이 요구되지 않습니다. 때에 따라 '타이어'를 구현하는 부분만 바꾸어주면 되기 때문입니다. 이처럼 기존에는 '스노우타이어'가 어디에도 의존하지 않았는데 '타이어'라는 인터페이스에 의존하게 되었고, 자동차는 기존의 구체적 타이어에서 추상적인 '타이어' 인터페이스에 의존함으로써 의존 관계가 바뀐 것을 볼 수 있습니다.
혹시 아셨던 분도 계셨을지 모르겠지만 이 부분은 이전에 'OCP - 개방 폐쇄 원칙'에 맞게 리팩토링 하는 과정과 유사합니다. 객체지향적으로 해결하다보면 실제 여러 원칙들이 녹아있는 경우가 많은 것이죠.
이렇게하여 5가지 원칙을 모두 살펴보았습니다. 솔직히 100% 이해될 거라 기대하지도 않았지만 알쏭달쏭하네요. 무슨 말 인지는 알겠지만 제가 이해한 것과 저자가 의도한 것이 일치하는지도 잘 모르겠습니다. 이 부분은 직접 코드를 구현하면서 제가 이해한대로 설계하여 구현해보고 그 원칙이 적용되었는지 코드리뷰를 받아보면서 알 수 있을 것 같습니다. 앞으로 5가지 원칙과 관련된 부분을 구현하게 되었을 경우, 그리고 좀 더 이해가 높아지는 경험을 하였을 때 다시 정리해보겠습니다!
'Book > programming' 카테고리의 다른 글
프로가 되기 위한 웹기술 입문2 (0) | 2018.11.08 |
---|---|
프로가 되기 위한 웹기술 입문1 (0) | 2018.11.07 |
스프링 입문을 위한 자바 객체지향의 원리와 이해5 (0) | 2018.10.21 |
스프링 입문을 위한 자바 객체지향의 원리와 이해4 (0) | 2018.10.18 |
스프링 입문을 위한 자바 객체지향의 원리와 이해3 (0) | 2018.10.18 |