3. 좋은 객체 지향 설계의 5가지 원칙 (SOLID)
지난 포스트에서 좋은 객체 지향 프로그래밍이란 프로그램의 유연한 변경이 가능하도록 하여 개발 생산성을 높이는 것이라 하였다.
그리고 이것을 위해서는 객체 지향의 다형성을 극대화하고, 객체 지향 설계의 5가지 원칙을 지켜야 한다.
지난 포스트에서 다형성에 대해 정리하였고, 이 포스트에서는 이어서 객체 지향 설계의 5가지 원칙에 대해 정리한다.
SOLID
클린 코드로 유명한 로버트 마틴이 정리한 좋은 객체 지향 설계의 5가지 원칙이다.
각각의 원칙은 기존에 존재하였으나, 좋은 객체 지향 설계라는 목적 아래 모아놓은 것.
좋은 객체 지향 설계의 5가지 원칙은 다음과 같으며, 각 원칙의 앞 글자를 따서 SOLID라 한다.
- SRP (Single Responsibility Principle, 단일 책임 원칙)
- OCP (Open-Closed Principle, 개방-폐쇄 원칙)
- LSP (Liskov Substitution Principle, 리스코프 치환 원칙)
- ISP (Interface Segregation Principle, 인터페이스 분리 원칙)
- DIP (Dependancy Inversion Principle, 의존관계 역전 원칙)
SRP (Single Responsibility Principle, 단일 책임 원칙)
한 클래스는 하나의 책임만을 가진다.
하나의 책임이라는 것은 사실 모호한 의미로, 맥락에 따라 다를 수 있다.
수정과 변경의 관점에서 본다면, 변경이 발생했을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것으로 볼 수 있다.
하나의 클래스가 여러 책임(역할, 인터페이스)를 맡으면 해당 클래스를 변경할 때 다른 코드가 영향을 받을 가능성이 클 것이다.
OCP (Open-Closed Principle, 개방-폐쇄 원칙)
소프트웨어 요소는 확장에는 열려있으나 변경에는 닫혀 있어야 한다.
얼핏 생각해보면 이해가 잘 가지 않는다. (나만 그런게 아니길 바란다)
소프트웨어를 확장하려면 기존 코드의 변경은 반드시 일어날 수 밖에 없다.
하지만 객체 지향의 다형성을 활용하면 소프트웨어 요소(코드 등)을 변경하지 않고 확장이 가능하다.
위 그림에서, MemberRepository 인터페이스를 변경하지 않고 구현 클래스를 하나 더 생성함으로써 기능 확장이 가능하다.
문제점
하지만 여기엔 문제점이 하나 있다. 다음 코드 예시를 살펴보자.
해당 코드는 바로 위 [다형성 활용 사례] 그림을 바탕으로 작성된 MemberService(클라이언트) 코드의 일부다.
위 예시에서는 MemberService 클라이언트가 구현 클래스를 직접 선택한다.
즉, 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다. 다형성을 사용했지만 개방-폐쇄 원칙이 지켜지지 않은 것.
따라서 다형성만으로는 개방-폐쇄 원칙을 지킬 수 없고, 객체 생성과 연관관계 설정을 위한 별도의 설정자가 추가로 필요하다.
이것은 스프링 컨테이너를 통해 해결 가능하며, 자세한 내용은 이후 예시 코드와 함께 확인하게 될 것이다.
LSP (Liskov Substitution Principle, 리스코프 치환 원칙)
프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
다형성에서, 하위 클래스는 상위 인터페이스의 규약을 모두 지켜야한다는 의미다.
단순히 문법상 호환을 의미하는 것이 아니라, 약속된 기능을 정상적으로 수행해야 함을 의미한다.
자동차 인터페이스를 예로 들면, 엑셀을 밟으면 뒤로 가도록 설계된 모델은 없다. 자동차 인터페이스에 엑셀을 밟으면 앞으로 가도록 정의되어 있기 때문이다.
이 리스코프 치환 원칙이 지켜져야 어떤 인터페이스에 대한 구현 클래스를 신뢰하고 사용할 수 있을 것이다.
ISP (Interface Segregation Principle, 인터페이스 분리 원칙)
여러 기능을 수행하는 범용 인터페이스보다 특정 기능를 맡은 여러 인터페이스로 분리 구현하는 것이 낫다.
인터페이스를 분리 구현하면 인터페이스가 명확해지고, 대체 가능성이 높아진다.
인터페이스가 변경됐을 때 파급 효과가 적어지기 때문이다.
예를 들어 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로,
사용자 클라이언트 -> 운전자 클라이언트, 정비사 클라이언트로 분리하는 것이 더 낫다는 것이다.
이러한 경우 정비 인터페이스가 변경이 생겨도 운전자 인터페이스나 클라이언트는 영향을 받지 않을 것이다.
DIP (Dependancy Inversion Principle, 의존관계 역전 원칙)
프로그램의 구현이 아닌 추상화에 의존해야 한다.
구현 클래스가 아닌 인터페이스에, 구현이 아닌 역할에 의존하라는 것이다.
클라이언트가 인터페이스가 의존해야 유연한 구현체 변경이 가능하기 때문이다.
인터페이스가 아닌 구현체에 의존한다면 변경이 아주 어려워질 것이다. (즉 좋은 객체 지향 프로그래밍을 하기 어렵다)
예를 들어 자동차 인터페이스 자체에 집중해야지, 특정 자동차에만 존재하는 특정 기능에 의존하면 자동차 모델을 유연하게 변경할 수 없는 것과 같다.
문제점
OCP에서 사용했던 해당 코드는 DIP에도 문제가 발생한다.
위 코드에서 MemberService 클라이언트는 MemberRepository 인터페이스에 의존하지만, 동시에 MemoryMemberRepository, JdbcMemberRepository 두 구현 클래스에도 의존한다.
즉 인터페이스에만 의존해야 하는 의존관계 역전 원칙이 지켜지지 않은 것.
여기서 의존이란, 클라이언트가 해당 클래스의 존재를 알고 있는 것을 의미한다.
MemberService에서 구현 클래스들의 존재를 알고 구현 클래스를 직접 선택하고 있으므로 MemberService는 구현 클래스에 의존한다고 볼 수 있다.
즉, MemberService 클래스는 설계 단계에서 MemberRepository 인터페이스에만 의존하도록 설계해야 한다.
정리
좋은 객체 지향 프로그래밍이란 유연한 변경이 가능하도록 프로그램을 설계하여 개발 생산성을 높이는 것을 의미한다.
이를 위해서 객체 지향의 다형성을 극대화하고 좋은 객체 지향 설계의 5가지 원칙(SOLID)를 준수해야 한다.
좋은 객체 지향 설계의 5가지 원칙(SOLID)이란
- SRP (Single Responsibility Principle, 단일 책임 원칙)
- OCP (Open-Closed Principle, 개방-폐쇄 원칙)
- LSP (Liskov Substitution Principle, 리스코프 치환 원칙)
- ISP (Interface Segregation Principle, 인터페이스 분리 원칙)
- DIP (Dependancy Inversion Principle, 의존관계 역전 원칙)
을 의미한다.
하지만 다형성만으로는 SOLID 중 OCP(개방-폐쇄 원칙), DIP(의존관계 역전 원칙)을 지킬 수 없다.
따라서 다형성 외에 또 다른 조치가 필요하며, 스프링 컨테이너를 통해 이것을 해결할 수 있다.
자세한 내용은 이후 코드를 작성해보며 학습하게 될 것.