본문 바로가기

web +a

DIP, OCP 원칙이 깨지는 문제를 Spring으로 해결

요약 :

역할과 책임을 아주 잘 분리 & 다형성을 잘 이용하더라도 OCP, DIP 원칙을 깬다면 좋은 객체지향 설계가 아니다. 

대표적 예시 : 순수 자바 코드 & new 연산자로 구현하는 아래 예시

해결법 : Spring DI

- 그래서 DI컨테이너 원리를 직접 구현해볼 것임

- AppConfig를 통한 의존관계 주입

: 생성&주입을 서비스클래스가 직접 하지 않고 AppConfig가 하도록 분리한다.

: 더이상 서비스클래스는 의존관계에 신경쓰지 않도록 구현하자.

 


● Spring 안쓰고 자바로 간단히 구현했을 때 문제점

어떤 문제가 생길까?

 

① 먼저, 회원 도메인에서 문제점을 찾아보자.

* MemberServiceImpl은 private final MemberRepository 인스턴스 변수를 가지고 있는 상태.

 

 

참고 : 기본&중요 | SOLID 그리고 스프링

앞서 공부한 것처럼 아래와 같은 코드가 OCP, DIP 원칙을 깨는 코드이다.

private final MemberRepository memberRepository = new MemoryMemberRepository();

서비스 클래스는 저장소를 가지는데, 저장소 구현체가 변경될 경우 서비스 클래스 코드를 변경해야 하는 문제가 생긴다.

- 변경에 닫혀있어야 하는데 그러지 못하고 있고 (OCP를 깬다)

- 추상화에만 의존해야 하는데 구체화에도 의존해버린다 (DIP를 깬다)

recall : 좋은 설계란 다형성을 지키는 동시에 OCP, DIP도 지켜야 한다. 

 

cf. Test 관련 내용

더보기

∨ 테스트하기

: 서비스의 기능을 테스트해보려고 한다. 

 

!! 좋지 못한 예 : main에서 테스트

MemberApp에 psvm 간단히 작성 & 아래 내용처럼 테스트해보자. 

- 서비스 객체 만들고, 멤버 하나 만들어서 해당 서비스에 join() 통해 회원가입 시킨 뒤

- 서비스의 findMember()를 이용해보고 있다.

 

run 결과

 

그러나 main 메서드를 통해 확인하는 이러한 방법은 좋은 방법이 아니다.

눈으로 하나하나 검증해야 하기 때문이다.

∴ Junit이라는 test framework를 사용할 것이다.

 

MemberService의 기능을 테스트하고자

테스트 디렉토리에 다음과 MemberServiceTest 클래스를 만들고

junit 프레임워크를 통하여 Test하기

아래와 같이 서비스의 기능을 테스트해보고자 한다. 

given - when - then 구조를 사용해 테스트하고자 하는 바를 명확히 한다.

 

테스트 완료

 

 

 

② 주문&할인 도메인에서도 같은 문제가 생긴다.

아래는 주문&할인 클래스 다이어그램이다.

좋은 객체 지향 설계 방법대로 역할과 책임을 아주 잘 분리하여 구현한 모습이다.

그러나 이 역시도 다형성은 잘 지켰으나, 만약 할인정책을 변경하려고 할 때 OCP, DIP 원칙을 깰 수밖에 없다.

순수 자바 코드로 구현하면 그렇다는 것이다. Spring이 해결해 줄 것이다.

 

cf. Test 관련 내용

더보기

테스트클래스를 위와 같이 만들었다. 

- 일단 OrderService 테스트를 위해 MemberService, OrderService 객체는 가져야 하므로 선언

- junit의 @Test를 통해 유닛테스트 void createOrder()를 작성하자.

given : member를 memberService에 join시킨 상태임

when : orderService.createOrder(memberId, "itemA", 10000);

then : Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);

 

테스트 성공

 

 

 

 


∴ 위 방법 X

● 좋은 객체지향의 원리를 적용해야 한다

위 예제는 다형성을 잘 적용했지만 OCP, DIP 원칙을 깬다고 했다.

OCP, DIP 원칙을 지키게 잘 구현하는 과정에서 Spring 컨테이너의 역할을 이해하게 된다!

∨ 그런 구현을 직접 해보면서 Spring 컨테이너 원리를 느껴보자.

∨ 잘 느꼈다면 이제는 직접구현한 자바코드를 실제 Spring 컨테이너에서 동작하도록 슉 바꾸자.

 

할인정책을 갈아끼울 것이다. 그런데 OCP, DIP를 지키게끔.. 

Fix->Rate 정책으로 갈아끼울것임

 

∨ 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);
    }

}

 

+ 참고

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() {
	    ...
    }
}

 

테스트 완료

 

 


[ 다음시간 ]

이렇게 구현한 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

 

반응형
다른 블로그