[Spring] 좋은 객체 지향 설계의 5가지 원칙(SOLID)

클린코드(Clean Code)로 유명한 로버트 마틴이 좋은 객체 지향 설계의 5가지 원칙을 정리한 것이다. 이 5가지 원칙을 줄여 SOLID라 한다.


SRP

Single Responsibility principle(단일 책임 원칙)

하나의 클래스는 하나의 책임만 가져야 한다는 원칙이다. 하지만 정의만 이런 것이고 실제로는 하나의 책임이라는 것은 매우 모호하다. 책임이라는 것이 클 수도 있고 작을 수도 있다. 또한 문맥과 상황에 따라 다르다. 

여기서 중요한 기준은 변경이다. 즉, 변경이 있을 때 파급 효과가 적으면 SRP를 잘 따른 것이라 볼 수 있다. 

OCP

Open/Closed principle(개방-폐쇠 원칙)

5가지 중 가장 중요한 원칙으로, 소프트웨어 요소는 확장에는 열려 있으나, 변경에는 닫혀 있어야 한다는 원칙이다.
기능을 확장을 하려면 코드를 변경을 해야하는데 이게 어떻게 가능할까? 이때 객체 지향 언어에서의 다형성을 활용한다. 인터페이스를 구현한 새로운 클래스를 하나 만들어 새로운 기능을 구현하는 것이다. 이것은 기존 코드를 변경하는 것이 아니다.

 

하지만 구현에서는 문제가 없지만 적용에서 문제점이 발생한다. 아래 코드와 그림을 살펴보자.

MemeberRepository의 save 메서드를 메모리에 저장하는 메서드와 데이터베이스에 저장하는 메서드로 수정을 하고 싶다면, 다형성을 이용해 위의 그림처럼 구현하면 된다. 이때는 OCP가 잘 지켜진다. 두 메서드는 각각 다른 메서드이므로 서로의 코드에 영향을 주지 않기 때문이다. 하지만 이것을 MemberService 클래스에 적용할 때 문제가 발생한다. 

public class MemberService{

	//private MemberRepository memberRepository = new MemoryMemberRepository();
    	private MemberRepository memberRepository = new JdbcMemberRepository();
    
}

기존에 메모리에 저장하는 역할을 수행하다 데이터베이스에 저장하는 기능으로 바꾸기 위해서는 MemberService 클래스에서의 코드의 변경이 불가피하다. 즉, 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다. 분명 다형성을 사용했지만 OCP의 원칙을 지킬 수 없다. 

이 문제를 어떻게 해결할까? 객체를 생성하고 연관관계를 맺어주는 별도의 설정자가 필요하다. 이 별도의 생성자의 역할을 해주는 것이 스프링(스프링 컨테이너)이다. 이 OCP를 지키기 위해 DI, IOC 컨테이너 등이 필요한 것이다. 

LSP

Liskov substitution principle(리스코프 치환 원칙)

프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다는 원칙이다. 쉽게 얘기해서, 다형성에서 하위 클래스는 인터페이스의 규약을 다 지켜야 한다는 것이다. 예를 들어, 자동차 인터페이스가 있을 때 구현체가 악셀의 기능을 구현한다 해보자. 악셀을 밟았을 때 뒤로 가는 기능으로 구현할 수 있을 것이다. 하지만 이는 악셀을 밟으면 앞으로 가야한다는 자동차 인터페이스의 규약을 위반한 것이기 때문에 LSP를 위반한 것이다. 

ISP

Interface segregation principle(인터페이스 분리 원칙)

특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다는 원칙이다. 이를 활용하면 인터페이스가 명확해지고 대체 가능성이 높아진다. 

DIP

Dependency inversion principle(의존관계 역전 원칙)

의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것에 의존하기 보다는, 변화하기 어려운것, 거의 변화가 없는 것에 의존하라는 원칙이다. 쉽게 얘기해 구현 클래스에 의존하지 말고, 인터페이스에 의존하라는 뜻이다. 역할(Role)에 의존하게 해야 한다는 것이다. 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존하게 되면 변경이 매우 힘들어진다. 

 

위에서 설명한 코드에서 보여진 MemberService는 인터페이스에 의존하지만, 구현 클래스에도 동시에 의존한다. 
MemberRepository m = new MemoryMemberRepository(); 에서 MemberRepository 인터페이스에도 의존하고 구현 클래스인 MemoryMemberRepository 클래스에도 의존하고 있는 것을 볼 수 있다. 따라서 DIP를 위반하고 있다.


위 내용을 정리해보면 다음과 같다. 

  • 객체 지향의 핵심은 다형성이다. 
  • 다형성 만으로는 쉽게 부품을 갈아 끼우듯 개발할 수 없다.
  • 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함계 변경된다.
  • 다형성 만으로는 OCP, DIP를 지킬 수 없다.

따라서, SOLID를 지키기 위해 다형성 외에 뭔가 더 필요하다.