슬픈 야옹이 2023. 10. 13. 17:02

지난 포스트에서 발생한 문제를 해결하기 위해,

외부에서 구현 객체를 생성하고 OrderServiceImpl과 같은 클라이언트에게 이를 주입해주는 역할의 필요성을 얘기하였다.

https://debuggingworld.tistory.com/91

 

13. 새로운 할인 정책 적용과 문제점

지난 포스트에서 추가한 새로운 할인 정책을 적용해보자. https://debuggingworld.tistory.com/90 12. 새로운 할인 정책 개발 서비스 개발 과정에서, 다음과 같은 상황이 발생했다고 가정해보자. 기획자: 회

debuggingworld.tistory.com

 

갑자기 새로운 개념이 도입된 느낌이 들 수 있지만, 이는 지금껏 해왔던 "역할과 구현의 분리"라는 개념을 벗어나지 않는다.

 

이를 설명하기 위해 강의에서는 어플리케이션을 하나의 공연에 비유한다.

 

 

관심사의 분리

어플리케이션을 하나의 공연이라 생각해보자.

 

각각의 인터페이스는 배역에, 구현체는 배역을 연기할 배우에 비유할 수 있다.

 

그렇다면 각 배역에 배우를 캐스팅하는 것은 누구의 역할인가? 아마 감독(혹은 PD, 기획자)의 역할일 것이다.

 

공연을 위한 배역에 배우를 캐스팅하는 것은 배우의 역할이 아니다.

 

이전 코드는 마치 배역을 맡은 배우가 특정 배역에 누구를 캐스팅할지도 정하는 것과 같다.

 

이를테면 범죄도시의 마동석이 상대 악역 배우를 직접 섭외해오는 것과 같다.

그러면 마동석은 자신의 배역은 물론 상대 배우 섭외라는 다양한 책임을 가지게 된다.

 

지금까지 작성한 OrderServiceImpl의 코드도 마찬가지다.

OrderServiceImpl은 마동석이다(?)

기존 코드는 OrderServiceImpl이 MemberRepository와 DiscountPolicy의 구현체를 자신이 직접 선택한다.

 

그러나 OrderServiceImpl은 인터페이스를 통해 기능만 받아오고 자신의 로직에 집중해야지, 인터페이스의 구현체가 무엇인지 신경쓰지 않아야 한다.

 

즉 누군가가 이러한 구현체의 생성 및 관리를 맡아서 해주어야 한다. 어플리케이션에도 감독이 필요하다.

 

정리하자면 다음과 같다.

  • 배우(구현체)는 본인의 역할인 배역(인터페이스)을 수행하는 것에만 집중해야 한다.
  • 각 배우는 상대 배역에 어떤 배우가 선택되더라도 똑같이 공연을 수행할 수 있어야 한다.
  • 공연을 구성하고, 담당 배우를 섭외 및 배정하는 책임을 담당할 별도의 공연 감독이 필요하다.
  • 공연 감독을 만들고, 배우와 감독의 책임을 확실히 분리해야 한다.

 

 

AppConfig의 등장

어플리케이션의 전체 동작 방식을 구성하고, 구현체를 생성 및 연결하는 별도의 설정 클래스를 만든다.

 

core 패키지 하위에 AppConfig 클래스를 생성한다.

AppConfig 클래스 생성

 

AppConfig.java

 

AppConfig.java

package hdxian.hdxianspringcore;

import hdxian.hdxianspringcore.discount.DiscountPolicy;
import hdxian.hdxianspringcore.discount.FixDiscountPolicy;
import hdxian.hdxianspringcore.member.MemberRepository;
import hdxian.hdxianspringcore.member.MemberService;
import hdxian.hdxianspringcore.member.MemberServiceImpl;
import hdxian.hdxianspringcore.member.MemoryMemberRepository;
import hdxian.hdxianspringcore.order.OrderService;
import hdxian.hdxianspringcore.order.OrderServiceImpl;

// 애플리케이션 전체를 설정하고 구성함.
// 앞으로 애플리케이션에 대한 환경 설정은 모두 이 클래스에서 수행한다.
public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }

    public OrderService orderService() {
        return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
    }


}

 

AppConfig 클래스는 어플리케이션의 실제 동작에 필요한 구현 객체를 생성한다.

이를테면 MemberServiceImpl, OrderServiceImpl, MemoryMemberRepository, FixDiscountPolicy다.

 

memberService(), orderService()를 보면 OrderServiceImpl과 MemberServiceImpl에게 필요한 객체를 생성자를 통해 주입(연결)해준다.

 

다음으로 MemberServiceImpl과 OrderServiceImpl이 생성자를 통해 의존 관계를 주입받도록 수정한다.

MemberServiceImpl.java

 

MemberServiceImpl.java

package hdxian.hdxianspringcore.member;

public class MemberServiceImpl implements MemberService {

//    private final MemberRepository memberRepository = new MemoryMemberRepository();

    private final MemberRepository memberRepository;

    // memberRepository에 들어갈 구현체를 생성자를 통해 전달받는다.
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Override
    public void join(Member member) {
        memberRepository.save(member);
    }

    @Override
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
}

 

 

OrderServiceImpl.java

 

OrderServiceImpl.java

package hdxian.hdxianspringcore.order;

import hdxian.hdxianspringcore.discount.DiscountPolicy;
import hdxian.hdxianspringcore.discount.FixDiscountPolicy;
import hdxian.hdxianspringcore.discount.RateDiscountPolicy;
import hdxian.hdxianspringcore.member.Member;
import hdxian.hdxianspringcore.member.MemberRepository;
import hdxian.hdxianspringcore.member.MemoryMemberRepository;

public class OrderServiceImpl implements OrderService {

//    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        // OrderService는 할인에 관여하지 않고 discountPolicy에 member정보를 넘기기만 한다.
        // 단일 책임 원칙이 잘 지켜진 사례. 할인 정책이 변경되어도 OrderService는 변화가 없음.
        int discountPrice = discountPolicy.discount(member, itemPrice); // discount()는 할인 액수를 리턴함.

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

}

지금 단계에서 발생하는 경고는 코드 변경이 아직 반영되지 않은 곳(테스트 코드 등)에서 나는 오류이므로 크게 신경쓰지 않아도 된다.

 

작성된 코드를 살펴보면, MemberServiceImpl에 MemoryMemberRepository의 흔적이 없어졌다.

 

이제 MemberServiceImpl은 MemberRepository 인터페이스에만 의존하며 (DIP), MemberRepository의 구현체가 변경되어도 영향받지 않는다. (OCP)

이는 MemberServiceImpl은 더 이상 MemberRepository의 구현체가 무엇인지 신경쓰지 않아도 된다는 의미다.

MemberServiceImpl은 생성자를 통해 전달되는 구현 객체가 무엇인지 알 수 없으며, 그것은 오직 외부의 AppConfig에서만 결정된다.

 

결과적으로 MemberServiceImpl은 이제부터 의존관계에 대해서는 신경쓰지 않고 로직의 실행에만 집중할 수 있게 된다.

배우와 감독의 역할이 분리된 것이다. 그리고 이러한 점들은 OrderServiceImpl도 마찬가지다.

 

 

AppConfig가 추가됨으로써 클래스 다이어그램에도 변화가 생겼는데, 다음과 같다.

AppConfig가 추가된 클래스 다이어그램 (강의자료 발췌)

 

객체를 생성하고 연결하는 역할을 AppConfig가 담당한다.

 

 

 

DI (Dependency Injection) - 의존관계 주입, 의존성 주입

Appconfig는 MemoryMemberRepository 객체를 생성하고, MemberServiceImpl을 생성할 때 이를 생성자를 통해 전달한다.

이것을 클라이언트인 MemberServiceImpl의 입장에서 보면, 의존관계를 마치 외부에서 주입해주는 것 같다고 하여 DI (Dependency Injection, 의존관계 주입, 의존성 주입)이라 한다.

 

 

AppConfig 실행

이제 AppConfig를 이용해 어플리케이션 코드를 다시 작성해보자.

 

MemberApp 클래스를 다음과 같이 수정한다.

MemberApp.java

 

MemberApp.java

package hdxian.hdxianspringcore;

import hdxian.hdxianspringcore.member.Grade;
import hdxian.hdxianspringcore.member.Member;
import hdxian.hdxianspringcore.member.MemberService;
import hdxian.hdxianspringcore.member.MemberServiceImpl;

public class MemberApp {

    public static void main(String[] args) {
        // AppConfig 객체를 생성하고, AppConfig 객체로부터 구현체를 생성한다.
        AppConfig appConfig = new AppConfig();

        // memberService에는 memberServiceImpl 객체가 들어있음.
        MemberService memberService = appConfig.memberService();
//        MemberService memberService = new MemberServiceImpl();
        Member member = new Member(1L, "memberA", Grade.VIP);

        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member: " + member.getName());
        System.out.println("findMember = " + findMember.getName());

    }

}

 

실행해보면 다음과 같이 정상적으로 동작한다.

실행 결과

 

 

OrderApp도 마찬가지로 수정한다.

OrderApp.java

 

OrderApp.java

package hdxian.hdxianspringcore;

import hdxian.hdxianspringcore.member.Grade;
import hdxian.hdxianspringcore.member.Member;
import hdxian.hdxianspringcore.member.MemberService;
import hdxian.hdxianspringcore.member.MemberServiceImpl;
import hdxian.hdxianspringcore.order.Order;
import hdxian.hdxianspringcore.order.OrderService;
import hdxian.hdxianspringcore.order.OrderServiceImpl;

public class OrderApp {

    public static void main(String[] args) {
//        MemberService memberService = new MemberServiceImpl(null);
//        OrderService orderService = new OrderServiceImpl(null, null);

        AppConfig appConfig = new AppConfig();
        // memberService는 MemberServiceImpl 객체를 참조함.
        MemberService memberService = appConfig.memberService();
        // orderService는 OrderServiceImpl 객체를 참조함.
        // AppConfig에서 MemoryMemberService, FixDiscountPolicy를 주입해줌.
        OrderService orderService = appConfig.orderService();
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(1L, "itemA", 10000);

        System.out.println("order = " + order);
        System.out.println("order.calculatePrice = " + order.calculatePrice());

    }

}

 

OrderApp 역시 실행하면 다음과 같이 정상적으로 실행된다.

실행 결과

 

 

 

테스트코드 수정

마지막으로 테스트코드를 수정해본다.

지금 테스트코드에는 변경된 설계가 반영되어있지 않아서 오류가 떠 있을 것이다.

 

MemberServiceTest를 수정한다.

MemberServiceTest

 

MemberServiceTest.java

package hdxian.hdxianspringcore.member;

import hdxian.hdxianspringcore.AppConfig;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

//    MemberService memberService = new MemberServiceImpl();
    MemberService memberService;

    // 각 테스트 메서드 실행 전에 실행됨.
    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }

    @Test
    void join() {
        // given
        Member member = new Member(1L, "memberA", Grade.VIP);

        // when
        memberService.join(member);
        Member findMember = memberService.findMember(1L);

        // then
        Assertions.assertThat(member).isEqualTo(findMember);

    }
}

기존에 직접 MemberServiceImpl을 생성한 것과 달리, AppConfig를 통해 MemberService 구현체를 전달받는다.

@BeforeEach 어노테이션이 붙은 메서드는 각 테스트 메서드가 동작할 때마다 먼저 한번씩 실행된다.

 

 

이어서 OrderServiceTest도 같은 방식으로 수정한다.

OrderServiceTest.java

 

OrderServiceTest.java

package hdxian.hdxianspringcore.order;

import hdxian.hdxianspringcore.AppConfig;
import hdxian.hdxianspringcore.member.Grade;
import hdxian.hdxianspringcore.member.Member;
import hdxian.hdxianspringcore.member.MemberService;
import hdxian.hdxianspringcore.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class OrderServiceTest {

//    MemberService memberService = new MemberServiceImpl();
//    OrderService orderService = new OrderServiceImpl();

    MemberService memberService;
    OrderService orderService;

    @BeforeEach
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
        orderService = appConfig.orderService();
    }

    @Test
    void createOrder() {
        // given
        // 원시 타입에는 null을 넣을 수 없어서 Long 타입을 씀.
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        // when
        Order order = orderService.createOrder(memberId, "itemName", 10000);

        // then
        // 할인액이 1000원이 맞는지 테스트
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);

    }

}

OrderServiceTest 역시 AppConfig 객체를 통해 구현체를 전달받는다.

차이점이라면 OrderService는 회원을 조회하는 동작이 포함되므로 MemberService를 통한 회원 가입 기능도 테스트에 포함되어야 한다는 점이다.

 

 

테스트 코드는 다른 테스트 코드에도 문제가 없어야 동작한다. 자세한 이유는 모르겠다.

 

각 테스트 코드를 다시 돌려서 통과하면 성공이다.

MemberServiceTest

 

OrderServiceTest

 

 

정리

기존의 코드는, 클라이언트가 인터페이스가 아닌 구현체에도 의존하는 문제가 있었다.

그래서 인터페이스에만 의존하도록 코드를 변경하려면, 외부에서 구현체를 생성하고 연결해주는 역할이 필요했다.

 

이를 공연 배우와 감독의 역할에 비유했으며, AppConfig 클래스를 통해 역할의 분리가 가능했다.

 

결과적으로 AppConfig를 통해 각 클래스가 인터페이스에만 의존하도록 설계를 변경하였고, 이를 통해 객체 지향의 원칙을 잘 준수한 프로그램을 만들 수 있었다.

 

포스팅이 평소보다 많이 길어졌다. 하지만 그만큼 중요한 내용이므로 집중해서 공부하면 좋을 듯 하다.