요약 :
역할과 책임을 아주 잘 분리 & 다형성을 잘 이용하더라도 OCP, DIP 원칙을 깬다면 좋은 객체지향 설계가 아니다.
대표적 예시 : 순수 자바 코드 & new 연산자로 구현하는 아래 예시
해결법 : Spring DI
- 그래서 DI컨테이너 원리를 직접 구현해볼 것임
- AppConfig를 통한 의존관계 주입
: 생성&주입을 서비스클래스가 직접 하지 않고 AppConfig가 하도록 분리한다.
: 더이상 서비스클래스는 의존관계에 신경쓰지 않도록 구현하자.
● Spring 안쓰고 자바로 간단히 구현했을 때 문제점
어떤 문제가 생길까?
① 먼저, 회원 도메인에서 문제점을 찾아보자.
* MemberServiceImpl은 private final MemberRepository 인스턴스 변수를 가지고 있는 상태.
앞서 공부한 것처럼 아래와 같은 코드가 OCP, DIP 원칙을 깨는 코드이다.
private final MemberRepository memberRepository = new MemoryMemberRepository();
서비스 클래스는 저장소를 가지는데, 저장소 구현체가 변경될 경우 서비스 클래스 코드를 변경해야 하는 문제가 생긴다.
- 변경에 닫혀있어야 하는데 그러지 못하고 있고 (OCP를 깬다)
- 추상화에만 의존해야 하는데 구체화에도 의존해버린다 (DIP를 깬다)
recall : 좋은 설계란 다형성을 지키는 동시에 OCP, DIP도 지켜야 한다.
cf. Test 관련 내용
∨ 테스트하기
: 서비스의 기능을 테스트해보려고 한다.
!! 좋지 못한 예 : main에서 테스트
MemberApp에 psvm 간단히 작성 & 아래 내용처럼 테스트해보자.
- 서비스 객체 만들고, 멤버 하나 만들어서 해당 서비스에 join() 통해 회원가입 시킨 뒤
- 서비스의 findMember()를 이용해보고 있다.
![](https://blog.kakaocdn.net/dn/RRuW7/btrGktPd8Uc/6rA0S7i5WMngGYUWBxVPKK/img.png)
![](https://blog.kakaocdn.net/dn/cNekoO/btrGgoOv5rY/FkxrxkrjIGrLl9yyMUZYik/img.png)
그러나 main 메서드를 통해 확인하는 이러한 방법은 좋은 방법이 아니다.
눈으로 하나하나 검증해야 하기 때문이다.
∴ Junit이라는 test framework를 사용할 것이다.
MemberService의 기능을 테스트하고자
테스트 디렉토리에 다음과 MemberServiceTest 클래스를 만들고
![](https://blog.kakaocdn.net/dn/bEIoNq/btrGn9IyW9p/J3ki00yKdoA234dy054Ui0/img.png)
![](https://blog.kakaocdn.net/dn/MA4yh/btrGhAm5SKp/BjymMQCmS5x1KivZEubJrK/img.png)
아래와 같이 서비스의 기능을 테스트해보고자 한다.
given - when - then 구조를 사용해 테스트하고자 하는 바를 명확히 한다.
![](https://blog.kakaocdn.net/dn/btNVyv/btrGlTFYgSv/JzmGsRTAZiRukbZuCASq00/img.png)
테스트 완료
![](https://blog.kakaocdn.net/dn/VQ8TB/btrGqIRF3GD/WMMTvpdpzL0aXIFeRDjRs1/img.png)
② 주문&할인 도메인에서도 같은 문제가 생긴다.
아래는 주문&할인 클래스 다이어그램이다.
좋은 객체 지향 설계 방법대로 역할과 책임을 아주 잘 분리하여 구현한 모습이다.
그러나 이 역시도 다형성은 잘 지켰으나, 만약 할인정책을 변경하려고 할 때 OCP, DIP 원칙을 깰 수밖에 없다.
순수 자바 코드로 구현하면 그렇다는 것이다. Spring이 해결해 줄 것이다.
cf. Test 관련 내용
![](https://blog.kakaocdn.net/dn/cgqW1S/btrGurCaqci/6FwIfAUVBuxL6wqP6xaNYk/img.png)
![](https://blog.kakaocdn.net/dn/bKoMv1/btrGuqDfZSt/TEPCwwxhwTkFTC6kXtMF6K/img.png)
테스트클래스를 위와 같이 만들었다.
- 일단 OrderService 테스트를 위해 MemberService, OrderService 객체는 가져야 하므로 선언
- junit의 @Test를 통해 유닛테스트 void createOrder()를 작성하자.
given : member를 memberService에 join시킨 상태임
when : orderService.createOrder(memberId, "itemA", 10000);
then : Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
테스트 성공
![](https://blog.kakaocdn.net/dn/TlLdW/btrGnWjV5iw/c99txGJBgFZztYIFu7KdH1/img.png)
∴ 위 방법 X
● 좋은 객체지향의 원리를 적용해야 한다
위 예제는 다형성을 잘 적용했지만 OCP, DIP 원칙을 깬다고 했다.
OCP, DIP 원칙을 지키게 잘 구현하는 과정에서 Spring 컨테이너의 역할을 이해하게 된다!
∨ 그런 구현을 직접 해보면서 Spring 컨테이너 원리를 느껴보자.
∨ 잘 느꼈다면 이제는 직접구현한 자바코드를 실제 Spring 컨테이너에서 동작하도록 슉 바꾸자.
할인정책을 갈아끼울 것이다. 그런데 OCP, DIP를 지키게끔..
∨ RateDiscountPolicy 구현 완료 후 Test까지 마침
● Test코드 작성은 중요하므로 빨리 익숙해지기 위해서 매번 접은글로 넣으려 한다.
class RateDiscountPolicyTest {
// 10% 할인이 관련 테스트
RateDiscountPolicy discountPolicy = new RateDiscountPolicy();
@Test
@DisplayName("VIP는 10% 할인이 적용되어야 한다.")
void vip_o() {
//given
Member member = new Member(1L, "memberVIP", Grade.VIP);
//when
int discount = discountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(1000);
}
@Test
@DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다.")
void vip_x() {
//given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);
//then
Assertions.assertThat(discount).isEqualTo(0);
}
}
![](https://blog.kakaocdn.net/dn/1HaTp/btrGvTSRhI1/Psg7s3UKEp2K0c4vJjL0HK/img.png)
+ 참고
Assertions는 static import하는 것을 추천하셨다.
import static org.assertj.core.api.Assertions.*;
//이전
Assertions.assertThat(discount).isEqualTo(1000);
//이후
assertThat(discount).isEqualTo(1000);
할인정책 추가 & 테스트까지 완료
∨ RateDiscountPolicy로 갈아끼우기
: OCP, DIP를 위반하지 않으면서 갈아끼워야 한다.
=> 즉, 구현체 말고 인터페이스에만 의존하도록(:DIP 지킴) 변경해야 한다.
(new연산자를 사용하는 그 예제의 실상은 구현체에 의존하고있는 상황이라고 거듭 말했다)
↓
일단 생각해볼 수 있는 것
① 구현체에 의존하지 않도록 변경 (인터페이스만 의존하도록 변경)
// 변경 전
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
// 변경 후
private DiscountPolicy discountPolicy;
그다음 생각나는 것..
② 구현체가 없는데 어떻게 실행? 그럼 NullPointerException인데?!
∨ 해결방법
: 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy 구현체를 대신 생성&주입해줘야 함
(즉, 생성&주입 역할은 또 따로 두어서 관심사를 분리! 디카프리오가 줄리엣역 배우를 직접 캐스팅하지는 X)
↓
AppConfig 등장
∨ 애플리케이션의 전체 동작 방식을 구성(config)
- 구현객체를 생성하고
- 연결해주는 책임을 가지는
별도의 설정 클래스!
∨ DI (dependency injection) 개념 시작
∨ AppConfig 클래스에서 생성자를 호출하여 구체적인 객체를 주입해준다.
- 이전방식 : MemberServiceImpl 클래스 소스코드에 구체화인 MemoryMemberRepository를 new로 직접 넣어버려서 DIP를 지키지 못함
- AppConfig 클래스 사용 : MemberServiceImpl 클래스에는 오로지 인터페이스 변수 memberRepository만을 두어 DIP를 지키도록 하고, 실제 구현체는 생성자를 통해서 주입해줄 수 있다.
이것이 DI (의존관계 주입) 개념이다.
MemberServiceImpl 클래스
// 전
private final MemberRepository memberRepository = new MemoryMemberRepository();
// 후
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
∨ 이제는 DIP를 만족할 수 있다. 이제는 MemberServiceImpl 클래스가 구체화가 아닌 인터페이스에만 의존하고 있다.
∨ 그럼 AppConfig에서 다음과 같은 코드를 통해 위 생성자를 호출하여 memberRepository에 주입가능하다.
=> "생성자 주입"
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
...
}
+ OrderService 클래스도 마찬가지로 변경해준다.
↓ 역시 DIP를 지키게끔 변경했다.
(구체화에도 의존한 이전과 달리 인터페이스에만 의존하고 있다)
public class OrderServiceImpl implements OrderService{
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
...
}
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
∨ 정리 :
- 어떤 구현체를 주입할지는 오로지 외부(AppConfig)에서 결정된다!
- 이제는 (예를들어) MemberRepository에 어떤 구현체가 들어올지는 전혀 신경쓰지 않고, 그냥 그 인터페이스의 어떤 기능을 이용할 건지만 고려하면 된다. 즉 의존관계 신경 쓰지 않고 서비스 실행에만 집중하여 구현해가면 된다.
- 관심사 분리 완료 : 객체를 실행하는 역할 & 객체를 생성주입하는 역할
이렇게 AppConfig를 통해 서비스클래스가 구현체가 아닌 인터페이스에만 의존하도록(DIP 준수) 변경해보았다.
참고) new AppConfig(); 하여 다음과 같이 사용한다.
AppConfig appConfig = new AppConfig();
MemberService memberService = appConfig.memberService();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);
cf. Test코드 관련
Test코드도 수정해준다.
클래스가 구현체에 의존한 부분만 수정하면 된다.
즉, AppConfig를 사용하도록 해서 의존관계를 주입해주면 된다.
새로 알게된 것: @BeforeEach 어노테이션
- 테스트 전 반드시 실행됨
- 각각의 테스트마다 실행됨
public class MemberServiceTest {
MemberService memberService;
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
}
@Test
void join() {
...
}
}
public class OrderServiceTest {
MemberService memberService;
OrderService orderService;
@BeforeEach
void beforeEach() {
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}
@Test
void createOrder() {
...
}
}
테스트 완료
![](https://blog.kakaocdn.net/dn/bqRQa6/btrGzddfmnk/KYOZb0wV76oTDcHrKPyZdK/img.png)
[ 다음시간 ]
이렇게 구현한 Appconfig는 문제점이 좀 있어서 리팩터링이 필요하다.
다음에 공부해보자.
public class AppConfig {
public MemberService memberService() {
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService() {
return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
}
2022.07.05 - [web] - AppConfig 리팩터링
AppConfig 리팩터링
방금 전에 AppConfig를 따로 두어서 생성&주입의 책임을 서비스 클래스의 외부(AppConfig)로 빼주었다. 의존관계 주입(DI)를 구현한 것이다. 그런데 단순히 구현해본 AppConfig는 리팩터링이 필요하다. 그
cherryjubilee.tistory.com
'web +a' 카테고리의 다른 글
AppConfig 리팩터링 (+ 역할 요약) (0) | 2022.07.05 |
---|---|
도메인 설계 내용 (작성중) (0) | 2022.07.04 |
기본&중요 | SOLID 그리고 스프링 (0) | 2022.07.04 |
스프링&웹 | 스프링 DI (+ 계획 변경..!! ㅠㅅ ㅠ) (0) | 2022.06.30 |
스프링&웹 | 각 레이어에 관련된 스프링 기능 (0) | 2022.06.29 |