돌아보기
- 1에서는 SOLID 원칙과 다형성에 대해서 잠시 언급했었다.
- 문제는 다형성을 사용하는것 만으로는 SOLID를 모두 지키면서 구현이 매우 힘들다는 점이다. 이번 포스트에서는 다형성의 한계점과 이를 어떻게 해결해볼 수 있는지에 대해 알아본다.
다형성의 한계점
- 주문 서비스(Service)와 할인 정책(Policy)를 생각해보자.
- 어떠한 상품을 주문할 때, 그 주문에 대한 할인 정책은 여러가지가 존재할 수 있을 것이다. 다음과 같은 상황을 생각해보자.
- discountPolicy는 인터페이스로 할인 정책의 "역할"을 가지고 있다.
- OrderServiceImpl은 주문 서비스의 역할을 담당하는 객체이다. 이 객체는 상품을 주문할 때 할인율에 대해서 알고 있어야 하므로 discountPolicy에 의존적이다.
- 만약 discountPolicy를 fixDiscountPolicy로 정했다면, 다음과 같은 코드를 사용해야 한다.
public class OrderServiceImpl implements OrderService{
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
...
}
- 여기까지는 좋다. 하지만 만약 discountPolicy를 rateDiscountPolicy로 바꾸어야 한다면 어떨까.
- 방법은 간단하다. discountPolicy를 새로운 객체를 가리키도록 하면 된다.
public class OrderServiceImpl implements OrderService{
//private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
...
}
- 방법에는 문제가 없다. 해당 방식은 정상적으로 동작할 것이고, 잘못된 점도 없다.
- 문제는 이러한 방법을 사용하는 것이 SOLID 원칙에 위배된다는 것이다.
OCP
- OCP는 이전에도 말했듯 개방 폐쇄 원칙으로 확장에는 열려 있어야 하나, 변경에는 닫혀 있어야 한다는 것을 의미한다.
- 변경에 닫혀 있어야 한다는 것은 확장을 진행할 때, 이 확장 때문에 사용자 측에서 변경이 일어나서는 안된다는 의미이다.
- 위에서 할인 정책인 discountPolicy가 필요하여 부른 사용자(Client)는 OrderServiceImpl 클래스이다. 즉 discountPolicy 측에서 할인 정책을 확장하기 위해 다른 구현체를 구현(Open)하여 사용하게 되더라도, OrderServiceImpl 클래스 코드에는 변경이 일어나서는 안된다(Close)는 것이다.
- 하지만 위의 코드를 보면 OrderServiceImpl의 코드가 변경된 것이 명확하다. 사실 이 방법 이외에 딱히 생각나는 다른 방법이 없다.
DIP
- 사실 위의 문제는 DIP를 위반하여 생기는 문제점이다.
- DIP는 프로그래머는 추상화에 의존해야 하며, 구체화에 의존하면 안된다는 것으로, 즉 인터페이스를 의존하게 해야지 구현체에 의존하게 하면 안된다는 것이다.
- 문제는 우리가 discountPolicy를 확인하기 위해 OrderServiceImpl은 인터페이스를 의존하는 것 처럼 보이지만 사실은 new를 통해 구현체에도 의존하고 있다는 것을 알 수 있다.
- 이는 당연한 것으로 Java에서 어떤 구현체를 넣지 않고 인터페이스로만 무언가를 한다는 것은 NPE(Null Pointer Exception)을 발생시킨다.
- 따라서 의존 관계에서 부터 우리가 원했던 그림과는 다른 그림으로 설정된다. 이 때문에 DIP가 위반되면서 OCP가 위반되는 결과가 유도된다. 그림으로 본다면 다음과 같게 될 것이다.
문제의 해결점
- 사실 위의 SOLID 원칙을 위반하는 것도 있지만, 만약 구현체를 변경해야 하거나 하는 경우, 저 discountPolicy를 의존하고 있는 모든 클라이언트 코드가 변경되어야 한다는 사실은 그닥 좋은 상황이 아니라는 것이다.
- 위의 다형성으로의 한계점이 들어나는 것은, DIP를 위배하여 일어나는 상황이기 때문에, DIP를 잘 지키도록 수정한다면 OCP가 위반될 상황이 만들어지지 않는다.
- 즉, 인터페이스에만 의존하도록 설계를 변경해야 한다. 코드로는 다음과 같도록 해야 할 것이다.
public class OrderServiceImpl implements OrderService{
private DiscountPolicy discountPolicy;
...
}
- 문제는 우리가 위에서 논의했던 것 처럼 구현체가 주입되어 있지 않고서는 위의 코드는 NPE 문제를 발생시킨다.
- 이 문제는 구현체가 주입되어 있지 않다는 것이 원인이기 때문에 다음과 같은 결론을 얻게 된다.
- 이 클라이언트 클래스의 외부 영역에서 누군가가 클라이언트 클래스인 OrderServiceImpl에 DiscountPolicy의 구현체를 대신 생성하고, 이를 discountPolicy에 주입해주어야 한다.
관심사의 분리
- 지금까지 다형성만을 사용하여 구현한 코드들을 생각해 보면, 다형성을 이용하지만 결론적으로 "누가" 그 역할에 들어갈 것인가 라는 질문에 대해 정하는 것은 클라이언트 코드라는 것을 떠올릴 수 있다.(new를 통해서 지정하는 것이 그 예시이다.)
- 문제는 각 객체들은 자신에게 주어진 하나의 책임을 수행하도록 장려해야지, 여러 책임을 지우게 된다면 문제점이 생긴다는 것이다.
- 예를 들어 OrderServiceImpl의 경우에는 데이터를 받고, 주문에 대한 데이터를 Repository에 넘기거나 다시 Controller에 넘기는 등, 비즈니스 로직을 수행하는 하나의 "책임"에 모든 역량을 집중시켜야 한다.
- 하지만 위의 코드를 보듯이 OrderServiceImpl은 할인 정책이 어떤 할인 정책인가를 직접 선택하는 "책임"을 가지고 있다. 즉, 2개의 책임을 현재 가지고 있다.
- 이는 SRP(단일 책임 원칙)에 대해서도 그닥 좋은 현상이 아니다.
- 따라서 하나의 책임을 잘 수행하도록 장려하기 위해, 외부의 어떤 관리자 클래스를 하나 만들어, 이 관리자 클래스에서 인터페이스에 들어갈 구현체를 선택하도록 해야 할 것이다.
- 그렇게 된다면, 클라이언트 클래스들은 직접 어떤 인터페이스에 대해 무엇이 올지 지정하지 않아도 되며, 그냥 아무것도 몰라도 받아서 사용하기만 하면 되게 되므로 책임 하나가 자연스럽게 소거되게 될 것이다.
관리자 클래스 구현
- 위에서 각 클라이언트 클래스들에 대해 직접 인터페이스 구현체를 생성하여 해당 부분에 주입할 수 있도록 하는 관리자 클래스를 만들 것이다.
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberService);
}
public OrderService orderService(){
return new OrderServiceImpl(
new MemeoryMemberRepository(),
new FixDiscountPolicy()
);
}
}
- OrderServiceImpl에 대한 코드는 다음과 같게 바뀔 것이다.
public class OrderServiceImpl implements OrderService{
private final DiscountPolicy discountPolicy;
private final MemberRepository memberRepository;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
- 코드를 보면 AppConfig 클래스에서 orderService를 통해 서비스 객체를 생성한다. 이때 생성에 사용할 구현체들을 AppConfig 클래스에서 지정하여 넣어주고 있다.
- OrderServiceImpl은 생성자에 AppConfig가 주는 구현체를 그냥 받아서 바로 초기화시켜주는 일만 수행한다. 이전의 코드와 같이 직접 구현체를 가르키고, 생성하지 않는다 이제 우리가 의도한 의존 관계가 만들어진다.
- 이제 OrderServiceImpl은 완벽하게 discountPolicy에 의존한다. 구현체의 경우에는 AppConfig에서 넣어주니 자신이 지정할 필요도 없으며 무엇이 들어오는지 알지도 못한다.
- 이제 OrderServiceImpl은 주입받은 구현체들로 자신의 기능을 수행하는데 모든 역량을 집중할 수 있게 된다.
- 동시에 추상화된 인터페이스에만 의존하기 때문에 DIP가 만족되며 DIP가 만족되기 때문에 OCP도 같이 만족되게 되는 것을 확인할 수 있다.
- 만약 할인 정책을 RateDiscountPolicy로 바꾼다고 생각해보면 AppConfig를 다음과 같이 수정하면 된다.
public class AppConfig{
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberService);
}
public OrderService orderService(){
return new OrderServiceImpl(
new MemeoryMemberRepository(),
//new FixDiscountPolicy()
new RateDiscountPolicy()
);
}
}
- AppConfig에서 OrderServiceImpl을 가진 모든 곳에 RateDiscountPolicy로 변경이 완료되었다. 더 좋은 점은 이렇게 한 줄의 코드만 변경함으로서 클라이언트 클래스에 어떠한 변경도 일으키지 않고(close) 확장을 이루어낸 것이다,(open)
- 이렇게 별도의 외부 클래스에서, 클라이언트 클래스로 어떠한 구현체를 생성자 또는 Setter등의 방식을 통해 주입하는 것을 의존성 주입(Dependency Injection)이라고 한다. 즉 우리가 알고 싶었던 의존성 주입의 개념이 유도되었다.
정리
- 다형성만을 사용한 코드는 클라이언트 클래스가 인터페이스만을 의존하도록 하는데 한계점이 존재한다. 즉 DIP를 온전히 지키면서 설계를 수행할 수 없다.
- 이로 인하여 추상 인터페이스만을 의존해야 했던 클라이언트 클래스가 인터페이스 뿐만이 아닌 구현체 클래스도 의존하게 되었고 이로 인하여 구현체 클래스가 변경되어야 한다면 클라이언트 클래스의 코드가 동일하게 바뀌어야 하는 문제점이 생겼다. 즉 OCP가 깨졌다.
- 이를 해결하기 위해, 인터페이스의 구현체를 클라이언트 클래스에서가 아닌 외부의 별도 클래스 또는 영역에서 생성하고, 이를 클라이언트 클래스에 주입하는 방식을 선택하였는데 이것이 바로 의존성 주입(Dependency Injection)이다.
- 다음에는 제어의 역전(IoC)에 대하여 알아보도록 하겠다.
'Spring & JPA > Spring' 카테고리의 다른 글
Spring Framework - Spring DI 컨테이너 (0) | 2023.05.06 |
---|---|
Spring Framework - IoC의 개념 (0) | 2023.05.06 |
Spring Framework - DI의 개념 - 1. 다형성과 SOLID 원칙 (0) | 2023.05.02 |
MVC 패턴에서 각 파트의 역할 (1) | 2023.04.30 |
Servlet 방식과 Spring 에서의 웹 개발 (0) | 2023.04.28 |