지금까지 적용된 원칙

  • SRP, DIP, OCP

 

SRP 단일 책임 원칙

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 클라이언트 객체는 직접 구현 객체를 생성하고, 연결하고, 실행하는 다양한 책임을 가지고 있음
  • SRP 단일 책임 원칙을 따르면서 관심사를 분리
  • 구현 객체를 생성하고 연결하는 책임은 AppConfig가 담당
  • 클라이언트 객체는 실행하는 책임만 담당

 

DIP 의존관계 역전 원칙

  • 프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
  • 새로운 할인 정책을 개발하고, 적용하려고 하니 클라이언트 코드도 함께 변경해야 했다.
    • 기존 클라이언트 코드(OrderServiceImpl)는 DIP를 지키며 DiscountPolicy 추상화 인터페이스에 의존하는 것 같았지만, FixDiscountPolicy 구체화 구현 클래스에도 함께 의존했다.
  • 클라이언트 코드가 DiscountPolicy 추상화 인터페이스에만 의존하도록 코드를 변경했다.
    • 하지만 클라이언트 코드는 인터페이스만으로는 아무것도 실행할 수 없다.
  • AppConfig가 FixDiscountPolicy 객체 인스턴스를 클라이언트 코드 대신 생성해서 클라이언트 코드에 의존관계를 주입했다.
    • 이렇게해서 DIP 원칙을 따르면서 문제도 해결했다.

 

OCP

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다
  • 다형성 사용하고 클라이언트가 DIP를 지킴
  • 애플리케이션을 사용 영역과 구성 영역으로 나눔 AppConfig가 의존관계를 FixDiscountPolicy → RateDiscountPolicy 로 변경해서 클라이언트 코드에 주입하므로 클라이언트 코드는 변경하지 않아도 됨
  • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀 있다!

 

※ 본 게시글은 인프런의 스프링 핵심 원리 - 기본편(김영한)을 수강하고 정리한 내용입니다.

새로운 할인 정책 개발

  • 다형성으로 정률 할인 정책을 추가로 개발하는 것 자체는 문제가 없음

 

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

  • 정률 할인 정책을 적용하려고 하니 클라이언트 코드인 주문 서비스 구현체도 변경해야함
  • 주문 서비스 클라이언트가 인터페이스인 DiscountPolicy 뿐만 아니라, 구체 클래스인 FixDiscountPolicy도 함께 의존 ⇒ DIP 위반

 

관심사의 분리

  • 기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성하고 실행
  • AppConfig를 추가
    • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성하고 연결하는 책임을 부여
    • 클라이언트 객체는 자신의 역할을 실행하는 것에만 집중, 권한이 줄어듦(책임이 명확해짐)

 

AppConfig 리팩터링

  • 구성 정보에서 역할과 구현을 명확하게 분리
  • 역할이 잘 드러남
  • 중복 제거

 

새로운 구조와 할인 정책 적용

  • 정액 → 정률 할인 정책으로 변경
  • AppConfig의 등장으로 애플리케이션이 크게 사용 영역과 객체를 생성하고 구성(Configuration) 하는 영역으로 분리
  • 할인 정책을 변경해도 AppConfig가 있는 구성 영역만 변경하면 됨
    • 사용 영역은 변경할 필요가 없음
    • 물론 클라이언트 코드인 주문 서비스 코드도 변경하지 않음

 

※ 본 게시글은 인프런의 스프링 핵심 원리 - 기본편(김영한)을 수강하고 정리한 내용입니다.

  • 현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 보이지 않음

기대하는 그림

 

AppConfig 리펙터링

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

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

    private static MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
  • Ctrl + Alt + M 단축키 : Extract Method
    • new MemoryMemberRepository(), new FixDiscountPolicy() 부분을 Extract Method로 메소드로 만듦
    • 인텔리제이에서 자동으로 중복된 부분을 변경해줌
  • 역할이 드러나는 효과
    • MemberService에서 구현은 MemberServiceImpl을 쓸 것이다.
    • 그리고 MemberRepositroy에 대한건 MemoryMemberRepository를 쓸 것이다.
  • 설계에 대한 그림이 AppConfig에서 그대로 드러남
    • 역할과 구현 클래스가 한눈에 들어오게 됨
    • 애플리케이션 전체 구성이 어떻게 되어 있는지 빠르게 파악 가능

 

※ 본 게시글은 인프런의 스프링 핵심 원리 - 기본편(김영한)을 수강하고 정리한 내용입니다.

현재 OrderServiceImpl의 문제점

// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
  • 구현체가 없어서 NullPointerException이 발생
  • OrderServiceImpl이 구현체를 정해야 하는 상황
    • OrderServiceImpl이 직접 객체를 생성하고 구현체의 선택도 함
  • 관심사의 분리가 필요
    • 별도의 설정 클래스가 구현체를 생성하고 연결

AppConfig 등장

  • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해 구현 객체를 생성하고 연결하는 책임을 가지는 별도의 클래스

AppConfig와 MemberService 관련 코드 변경

  • 먼저 이전에 만든 MemberService와 관련된 코드부터 변경
//    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final MemberRepository memberRepository;

    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
  • MemberServiceImpl의 코드는 위와 같이 변경
    • 주석 처리된 부분이 이전 코드로, MemoryMemberRepository라는 구현체를 선택하고 있음
    • 변경된 코드는 구현체에 대한 정보가 존재하지 않음
    • DIP를 준수하는 코드가 됨
package hello.core;

import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;

public class AppConfig {

    public MemberService memberService() {
        return new MemberServiceImpl(new MemoryMemberRepository());
    }
}
  • AppConfig에서 MemberServiceImpl에 대한 구현체를 선택
  • 생성자 주입 기법

AppConfig와 OrderServiceImpl 관련 코드 변경

private final MemberRepository memberRepository = new MemoryMemberRepository();
private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
  • OrderServiceImpl은 구현체에 대한 정보가 두 개 필요함
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}
  • 따라서 생성자에 MemberRepository와 DiscountPolicy 두 개의 매개변수를 받음
  • OrderServiceImpl에는 구현체에 대한 정보가 없음
  • 생성자에 의해 구현체가 할당됨
  • 철저하게 DIP를 준수하는 코드(인터페이스에만 의존, 구체적인 클래스에 대해서 모름)
public OrderService orderService() {
    return new OrderServiceImpl(new MemoryMemberRepository(), new FixDiscountPolicy());
}
  • AppConfig에서 다음과 같이 두 구현체를 지정

지금까지 정리

  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성
    • MemberServiceImpl
    • MemoryMemberRepository
    • OrderServiceImpl
    • FixDiscountPolicy
  • AppConfig는 생성한 객체 인스턴스의 참조(래퍼런스)를 생성자를 통해서 주입(연결)
    • MemberServiceImpl → MemoryMemberRepository
    • OrderServiceImpl → MemoryMemberRepository, FixDiscountPolicy

변경된 MemberServiceImpl 코드

package hello.core.member;

public class MemberServiceImpl implements MemberService {

//    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final 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);
    }
}
  • 설계 변경으로 MemberServiceImpl은 MemoryMemberRepository를 의존하지 않음
    • 단지 MemberRepository 인터페이스만 의존
  • MemberServiceImpl 입장에서 생성자를 통해 어떤 구현 객체가 들어올지(주입될지) 알 수 없음
  • MemberServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부(AppConfig)에서 결정
    • 이전에는 MemberServiceImpl 안에서 결정
  • MemberServiceImpl은 이제부터 의존관계에 대한 고민은 외부에 맡기고 실행에만 집중

변경된 설계의 클래스 다이어그램

  • 객체의 생성과 연결은 AppConfig가 담당
  • DIP 완성: MemberServiceImpl은 MemberRepository인 추상에만 의존
    • 구체 클래스를 몰라도 됨
  • 관심사의 분리: 객체를 생성하고 연결하는 역할과 실행하는 역할이 명확하게 분리

객체 인스턴스 다이어그램

  • appConfig 객체가 memoryMemnerRepository(참조값: x001) 객체를 생성
    • 그 참조값을 memberServiceImpl을 생성하면서 생성자로 전달
  • 클라이언트인 memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입해주는 것 같음
    • 이러한 이유로 DI(Dependency Injection), 의존관계 주입 또는 의존성 주입이라고 함

변경된 OrderServiceImpl 코드

package hello.core.order;

import hello.core.discount.DiscountPolicy;
import hello.core.member.Member;
import hello.core.member.MemberRepository;

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

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);

        // OrderService 입장에서는 할인 정책에 대해서 모르고, discountPolicy에 위임함 -> 단일 책임 원칙 준수
        // 할인이 변경되어도 OrderService는 바뀌지 않음
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • import에 구현체에 대한 import가 모두 사라짐
  • 설계 변경으로 OrderServiceImpl은 FixDiscountPolicy를 의존하지 않음
    • 단지 DiscountPolicy 인터페이스에만 의존
  • OrderServiceImpl 입장에서 OrderServiceImpl의 생성자를 통해 어떤 구현 객체가 들어올지(주입될지)는 알 수 없음
  • 생성자를 통해서 어떤 구현 객체을 주입할지는 오직 외부(AppConfig)에서 결정
  • OrderServiceImpl은 이제부터 실행에만 집중

MemberApp 변경

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;

public class MemberApp {

    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
//        MemberService memberService = new MemberServiceImpl();
        MemberService memberService = appConfig.memberService();

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new member = " + member);
        System.out.println("findMember = " + findMember);
    }
}
  • memberService에 appConfig를 이용하여 객체 생성
  • 인터페이스에만 의존하고 구체 클래스에 의존하지 않음
  • 실행 결과는 동일

OrderApp 변경

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import hello.core.order.Order;
import hello.core.order.OrderService;

public class OrderApp {

    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();

        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

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

        System.out.println("order = " + order.toString());
    }
}
  • appConfig를 이용하여 객체 생성
  • 인터페이스에만 의존하고 구체 클래스에 의존하지 않음
  • 실행 결과는 동일

MemberServiceTest 변경

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

    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);
    }
}
  • @BeforeEach 어노테이션은 각 테스트의 실행 전에 실행 됨
  • beforeEach 메소드를 만들어 객체 생성

OrderServiceTest

package hello.core.order;

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

public class OrderServiceTest {

    MemberService memberService;
    OrderService orderService;

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

    @Test
    void createOrder() {
        // primitive type인 long으로 해도 되지만, 그러면 null이 들어갈 수 없음
        Long memberId = 1L;
        Member member = new Member(memberId, "memberA", Grade.VIP);
        memberService.join(member);

        Order order = orderService.createOrder(memberId, "itemA", 10000);
        // org.assertj.core.api를 사용해야 .을 이용한 메소드 체인을 사용 가능
        Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
    }
}
  • DIP를 지키게 됨

정리

  • AppConfig를 통해 관심사를 확실하게 분리
  • AppConfig는 구체 클래스를 선택
    • 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임
  • OrderServiceImpl은 기능을 실행하는 책임만 지게 됨

 

※ 본 게시글은 인프런의 스프링 핵심 원리 - 기본편(김영한)을 수강하고 정리한 내용입니다.

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

정액 할인 정책 → 정률 할인 정책으로 변경

  • OrderServiceImpl에서 다음 코드를 변경해야 함
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy(); 
private final DiscountPolicy discountPolicy = new RateDiscountPolicy();

문제점

  • 역할과 구현을 충실하게 분리 OK
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리 OK
  • OCP, DIP와 같은 객체지향 설계 원칙을 충실히 준수했다
    • 그렇게 보이지만 사실은 아님
  • 클래스 의존관계를 분석해보면 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존
    • 추상(인터페이스) 의존 : DiscountPolicy
    • 구체(구현) 클래스 : FixDiscountPolicy, RateDiscountPolicy

기대했던 의존관계

  • DiscountPolicy 인터페이스에만 의존하는것을 기대

실제 의존관계

  • OrderServiceImplDiscountPolicy 인터페이스 뿐만 아니라, FixDiscountPolicy 구체 클래스도 함께 의존하고 있음 ⇒ 실제 코드 상 DIP 위반

정책 변경

  • 중요: FixDiscountPolicyRateDiscountPolicy로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야 함 ⇒ OCP 위반

문제 해결

  • 문제 원인 : 클라이언트 코드인 OrderServiceImplDiscountPolicy의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존
    • 그래서 구체 클래스를 변경할 때 클라이언트 코드도 함께 변경해야 함
    • DIP 위반 ⇒ 추상(인터페이스)에만 의존하도록 변경
  • 해결 방안 : DIP를 위반하지 않도록 인터페이스만 의존하도록 의존관계를 변경하면 됨

인터페이스만 의존하도록 설계 변경

인터페이스만 의존하도록 코드 변경

// private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
private DiscountPolicy discountPolicy;
  • 인터페이스에만 의존하도록 설계와 코드를 변경함
  • 실행하면? ⇒ NPE 발생
    • 구현체를 지정하지 않고 코드를 실행했기 때문
  • 해결 방안
    • 누군가 클라이언트인 OrderSerivceImplDiscountPolicy의 구현 객체를 대신 생성하고 주입해주어야 함

 

※ 본 게시글은 인프런의 스프링 핵심 원리 - 기본편(김영한)을 수강하고 정리한 내용입니다.

+ Recent posts