[스프링/Spring] 객체 지향 설계와 DI(Dependency Injection)의 시작(feat. 생성자 주입)

kindof

·

2021. 8. 8. 22:46

👻 0. 들어가면서

이전 포스팅에서 스프링이 만들어지고 각광받는 이유가 "객체 지향 프로그래밍의 장점을 극대화할 수 있기 때문"이라고 했습니다. 

 

그리고 객체 지향 프로그래밍이란 '컴포넌트를 쉽고 유연하게 변경'하는 것을 목표로 하며, 이를 위해서는 역할(인터페이스)과 구현(객체)을 분리하는 과정이 핵심이라고 설명했습니다.

 

 

[스프링/Spring] 스프링(Spring)은 왜 만들어졌는가?

🏃 0. 들어가면서 정말 많은 기업에서 스프링을 쓰고 있다. 스프링이 무엇이길래, 어떤 점이 좋기에 개발 생태계에서 정말 중요한 프레임워크로 자리매김했을까. 이번 글에서는 스프링은 왜 만

studyandwrite.tistory.com

 

🤔 1. 객체 지향 프로그래밍을 위한 시도

자, 그렇다면 이제 "다형성을 활용해서 인터페이스 설계를 잘 하고 구현 객체끼리 의존성을 최소화해야지"라고 마음으로 아래와 같은 간단한 요구사항에 대한 코드를 작성해봅시다.

 

[요구사항]

1. 회원은 상품을 주문할 수 있다.

2. 회원 등급에 따라 할인 정책을 적용할 수 있다.

3. 할인 정책은 VIP회원 대상으로 1000원을 할인해준다(나중에 변경 가능해야 한다).

4. 할인 정책은 변경 가능성이 높다.

 

위 요구사항에 대한 회원 도메인이나 기타 내용들은 따로 설명하지 않아도 아래 코드에서 이해할 수 있기 때문에 설명은 생략하겠습니다.

 

여기서 주의깊게 봐야 할 부분은 "할인 정책이 변하더라도 클라이언트의 코드 수정없이 "할인"에 대한 코드만 바꿔서 객체지향적 프로그래밍을 할 수 있는가?"입니다.

 

1) 정말 간단하게 할인 정책을 관리할 인터페이스를 하나 생성해보겠습니다. 여러 가지 할인 정책들(구현체들)은 이 인터페이스에 의존할 것입니다. 제 의도는 일단 이 인터페이스 하나를 일단 생성해놓고, 할인 정책이 바뀔 때마다 discount를 다르게 적용하는 구현체들을 생성해서 쓰겠다는 생각입니다.

public interface DiscountPolicy {
    /**
     * @return 할인 대상 금액
     */
    int discount(Member member, int price);
}

 

2) 그리고 현재 시점의 요구 사항에 맞게 고정 할인 금액에 대한 클래스(FixDiscountPolicy)를 하나 생성하겠습니다. member가 VIP회원이면 disCountFixAmount인 1000원을 리턴해주고, 이를 Order클래스에서 받아 가격을 산정해주는 방식입니다(요구 사항에 따라 다르게 구현할 수 있습니다).

public class FixDiscountPolicy implements DiscountPolicy{

    private int discountFixAmount = 1000;   // 1000원 할인

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){ // enum type -> '==' 사용
            return discountFixAmount;
        }else{
            return 0;
        }
    }
}

 

3) 주문(Order) 클래스는 다음과 같습니다. 기본적으로 주문을 한 회원의 Id, 상품명, 가격, 할인 가격을 가지고 있습니다.

public class Order {

    private Long memberId;
    private String itemName;
    private int itemPrice;
    private int discountPrice;

    public Order(Long memberId, String itemName, int itemPrice, int discountPrice) {
        this.memberId = memberId;
        this.itemName = itemName;
        this.itemPrice = itemPrice;
        this.discountPrice = discountPrice;
    }

    public int calculatePrice(){
        return itemPrice - discountPrice;
    }
    
    ...
}

 

4) 그리고 주문 서비스를 관리하는 구현체는 아래와 같습니다. 고객을 생성하고, 위에서 고정 할인 정책 구현체를 통해 할인 가격을 계산하여 주문(Order)를 리턴해주는 로직입니다.

public class OrderServiceImpl implements OrderService{
    private final MemberRepository memberRepository = new MemoryMemberRepository();

    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();    

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member consumer = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(consumer, itemPrice);
        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}

 

여기까지 흐름을 따라왔다면(모든 코드를 다 써 놓진 않았지만...), 뭐 그렇게 나쁘지 않은 설계라고 생각할 수 있습니다.

인터페이스를 생성하고, 구현체들은 인터페이스를 적절히 구현하고 있으며 회원, 할인, 주문이라는 도메인과 서비스를 잘 분리하고 있죠.

 

5) 하지만 여기서 이제 할인 정책을 비율 할인 정책(10% 할인)으로 바꾸려면 어떻게 해야 할까요?🤔

 

6) 아래처럼 DiscountPolicy 인터페이스에 맞춰 RateDiscountPolicy를 구현하면 될 것 같습니다.

public class RateDiscountPolicy implements DiscountPolicy{
    private int discountRate = 10;
    
    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP){
            return price * discountRate / 100;
        }else{
            return 0;
        }
    }
}

 

7) 그리고 주문 서비스를 처리하는 OrderServiceImpl로 가서 코드를 조금 수정해주면 될 것 같습니다.

그리고 이제 '단 한 줄만 저렇게 수정해도 되니 아주 객체 지향적인 프로그래밍을 했구나.'라고 생각합니다. 문제는 여기에서 시작합니다.

 

FixDiscountPolicy를 RateDiscountPolicy로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야 합니다. 이 부분에서 객체 지향의 중요한 가치인 개방 폐쇄 원칙(OCP)과 의존 역전 원칙(DIP)을 위반하게 됩니다.

 

* OCP: 기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다.

* DIP: 추상화된 것은 구체적인 것에 의존하면 안된다. 구체적인 것이 추상화된 것에 의존해야 한다.

 

 

객체 지향 5대 원칙(SOLID)에 대해 궁금하신 분이 있다면 아래 포스트를 참고하면 좋을 것 같습니다.

 

객체지향 설계 5원칙 SOLID (SRP, OCP, LSP, ISP, DIP)

참조문서 http://www.nextree.co.kr/p6960/ http://slenderankle.tistory.com/162 1. SOLID란? 객체지향 설계는 긴 세월과 수많은 시행착오를 거치며 5가지 원칙이 정리되었다. 이것은 객체지향 설계의 5원칙이라..

sjh836.tistory.com

그렇다면 이 두 가지 원칙을 지키기 위해서는 어떻게 해야할까요? 바로 인터페이스에만 의존하도록 설계를 변경해야 합니다.

 

어떻게 구현체의 도움없이 인터페이스에만 의존해서 달라지는 상황에 대비할 수 있을까요? 여기까지의 배경을 알고 의존관계 주입(DI)에 대해 알아봐야 합니다.

 

🤭 2. 의존관계 주입(DI)

위에서 발생했던 문제의 핵심은 OrderServiceImpl이라는 구현 객체가 FixDiscountPolicy, RateDiscountPolicy 등의 객체에게 의존성을 갖게 된 것이었습니다.

 

그래서 지금부터 할 작업은 생성자 주입을 통해 OrderServiceImpl이라는 구현 객체가 FixDisocuntPolicy나 RateDiscountPolicy와 같은 구현 객체에 의존하지 않게 만드려고 합니다.

 

아래 코드를 볼까요? AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성하는 역할을 합니다.

public class AppConfig {

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

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

  

그리고 OrderServiceImpl에서 discountPolicy 설정을 아래와 같이 한다고 해봅시다.

public class OrderServiceImpl implements OrderService{
//    private final MemberRepository memberRepository = new MemoryMemberRepository();
//    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();    // 고정 할인 정책
//    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();     // 비율 할인 정책

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    // OrderServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 주입될지는 알 수 없다. - AppConfig에서 결정
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy){
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

주석으로 표시한 부분이 원래는 할인 정책에 의존성을 가지고 있었던 부분입니다. 할인 정책이 바뀌면 discountPolicy 자체를 수정해줘야 했었죠.

 

하지만 이제는 단순히 memberRepository와 discountPolicy를 선언만 해주고, 아래 코드와 같이 초기화는 AppConfig에서 정의한 OrderService()로 해줄 수 있게 됩니다.

public class orderServiceTest {

    MemberService memberService;
    OrderService orderService;

    @BeforeEach
    public void beforeEach(){
        // 의존관계 주입
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.OrderService();
    }

    @Test
    void createOrder(){
        Long memberId = 1L;
        Member member = new Member(memberId, "sunghyeon", Grade.VIP);
        memberService.signUp(member);

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


        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}

이제 할인 정책을 바꾸고 싶다면 OrderServiceImpl에서 바꾸는 것이 아니라, AppConfig에서 생성자 주입을 할 때 어떤 할인 정책을 주입할 것인지를 결정해주면 되는 것입니다.

 

 

📚 3. 나가면서

이번 시간에는 객체 지향 설계에서 중요한 DIP와 OCP를 어떻게 지킬 것인가를 예제 코드를 보면서 설명해보았습니다.

 

의존 관계를 구현 객체끼리 가지고 있게 되면 하나의 구현 객체가 바뀌면 다른 구현 객체에도 영향을 미치게 되므로 이 방식으로 해서는 안 된다는 것을 공부했고, 이를 해결하기 위해 어떤 구현 객체를 사용할 것인가를 결정해주는 AppConfig(생성자 주입을 담당하는 인터페이스)를 설계해보았습니다.  

 

이번 글에서 분명히 이해하고자 한 부분은 DI가 왜 필요한지입니다. 

 

다음 포스팅에서는 스프링 DI와 컨테이너에 대한 더 많은 이야기를 해보겠습니다.

 

4. Reference

- 자바 ORM 표준 JPA 프로그래밍 스프링 데이터 예제 프로젝트로 배우는 전자정부 표준 데이터베이스 프레임 - 김영한