[Spring] Chapter 3. 스프링 핵심 원리 이해 2 - 객체 지향 원리 적용 ①

joyful·2021년 7월 8일
0

Java/Spring

목록 보기
6/28
post-thumbnail

들어가기 앞서

이 글은 김영한 님의 스프링 핵심 원리 - 기본편(https://www.inflearn.com/스프링-핵심-원리-기본편/dashboard)을 수강하며 학습한 내용을 정리한 글입니다. 모든 출처는 해당 강의에 있습니다.


📖 새로운 할인 정책 개발

✅ 새로운 할인 정책 확장

할인 정책을 기존의 고정 금액 할인에서 정률 할인으로 변경

📝 RateDiscountPolicy 추가

📝 RateDiscountPolicy 코드 추가


[src/main/java/hello/core/discount/RateDiscountPolicy.java]

package hello.core.discount;

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

public class RateDiscountPolicy implements DiscountPolicy {

    private int discountPercent = 10;  //10% 할인

    @Override
    public int discountMember(Member member, int price) {
    
        if(member.getGrade() == Grade.VIP)  //회원 등급이 VIP인 경우
            return price * discountPercent / 100;
        else
            return 0;
    }
}

📝 테스트 작성

[src/test/java/hello/core/discount/RateDiscountPolicyTest.java]

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

class RateDiscountPolicyTest {

    RateDiscountPolicy discountPolicy = new RateDiscountPolicy();

    @Test
    @DisplayName("VIP는 10% 할인이 적용되어야 한다")
    void vip_o() {
        //given
        Member member = new Member(1L, "memberVIP", Grade.VIP);

        //when
        int discount = discountPolicy.discountMember(member, 10000);

        //then
        assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 할인이 적용되지 않아야 한다")
    void vip_x() {
        //given
        Member member = new Member(2L, "memberBASIC", Grade.BASIC);

        //when
        int discount = discountPolicy.discountMember(member, 10000);

        //then
        assertThat(discount).isEqualTo(0);
    }
    
}

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

✅ 할인 정책 적용

📝 OrderServiceImpl 코드 수정

[src/main/java/hello/core/order/OrderServiceImpl.java]

public class OrderServiceImpl implements OrderService {

//  private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
}

📝 판별

항목준수 여부
역할과 구현을 충실하게 분리했는가?O
다형성을 활용했는가?O
인터페이스와 구현 객체를 분리했는가?O
객체지향 설계 원칙을 충실히 준수했는가?
ex) OCP, DIP 등
X

📝 문제점

  • 추상(인터페이스) 뿐만 아니라 구체(구현) 클래스에도 의존
    DIP 위반
    • 추상(인터페이스) 의존 : DiscountPolicy
    • 구체(구현) 클래스 : FixDiscountPolicy, RateDiscountPolicy
  • 현재 코드는 기능확장하여 변경하면 클라이언트 코드영향을 줌
    OCP 위반
    • 기대했던 의존관계
    • 실제 의존관계
    • 정책 변경

      FixDiscountPolicyRateDiscountPolicy로 변경하는 순간 OrderServiceImpl의 소스 코드도 함께 변경해야 함 → OCP 위반


✅ 문제 해결 방법

📝 개요

  • 문제점
    클라이언트 코드인 OrderServiceImplDiscountPolicy의 인터페이스 뿐만 아니라 구체 클래스도 함께 의존
    → 구체 클래스 변경시 클라이언트 코드도 함께 변경해야 함
    DIP 위반

  • 해결 방법
    추상(인터페이스)에만 의존하도록 의존관계를 변경

📝 설계 변경

📝 코드 변경

[src/main/java/hello/core/order/OrderServiceImpl.java]

public class OrderServiceImpl implements OrderService {
  //private final DiscountPolicy discountPolicy = new RateDiscountPolicy();
    private DiscountPolicy discountPolicy;  //현재 값 → null
}
  • 문제점 : 구현체 x → NPE(Null Pointer Exception) 발생
  • 해결방안 : 클라이언트인 OrderServiceImplDiscountPolicy의 구현 객체를 대신 생성 및 주입해 줄 존재 필요


📖 관심사의 분리 ★★★

  • 이전 코드는 로미오 역할(인터페이스)을 하는 레오나르도 디카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같음
    다양한 책임
  • 공연 구성, 담당 배우 섭외, 배역 결정 → 공연 기획자의 역할
  • 배우는 본인의 역할(배역)을 수행하는 것에만 집중해야 하며, 상대가 누구냐에 상관 없이 공연을 할 수 있어야 함
  • 즉, 배우와 공연 기획자의 책임을 확실히 분리해야 함

✅ AppConfig의 등장

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

📝 AppConfig - 애플리케이션 전체 설정 및 구성

[src/main/java/hello/core/AppConfig.java]

package hello.core;

import hello.core.discount.FixDiscountPolicy;
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(new MemoryMemberRepository());
    }
    
    public OrderService orderSerice() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(),
                new FixDiscountPolicy());
    }
    
}
  • 애플리케이션의 실제 동작에 필요한 구현 객체 생성
    • MemberServiceImpl
    • MemoryMemberRepository
    • OrderServiceImpl
    • FixDiscountPolicy
  • 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해 주입(연결)
    • MemberServiceImplMemoryMemberRepository
    • OrderServiceImplMemoryMemberRepository, FixDiscountPolicy

현재, 각 클래스에 생성자가 없는 상태이므로 컴파일 오류가 발생한다.

📝 MemberServiceImpl - 생성자 주입

[src/main/java/hello/core/member/MemberServiceImpl.java]

package hello.core.member;

public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;
    
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
    public void join(Member member) {
        memberRepository.save(member);
    }
    
    public Member findMember(Long memberId) {
        return memberRepository.findById(memberId);
    }
    
}
  • 설계 변경으로 MemberServiceImplMemoryMemberRepository를 의존 x
    • MemoryMemberRepository 관련 코드가 전혀 존재 x
    • MemberRepository 인터페이스만 의존 → DIP 충족
  • 의존관계에 대한 고민은 외부(AppConfig)에 맡기고 실행에만 집중하면 됨
    • 생성자를 통해 어떤 구현 객체가 주입될 지 알 수 x
    • 생성자를 통해 주입할 구현 객체는 외부(Appconfig)에서 결정

📝 클래스 다이어그램

  • AppConfig : 객체의 생성 및 연결 담당
  • DIP 완성 : MemberServiceImpl은 구체 클래스를 알 필요 없이, 추상(MemberRepository)에만 의존하면 됨
  • 관심사의 분리 : 명확한 객체 관련 작업 역할의 분리
    • 생성 및 연결 → AppConfig
    • 실행 → MemberServiceImpl

📝 회원 객체 인스턴스 다이어그램


memberServiceImpl 입장에서 보면 의존관계를 마치 외부에서 주입받고 있는 것처럼 보임
=> 의존성 주입(DI, Dependency Injection) = 의존관계 주입

📝 OrderServiceImpl - 생성자 주입

[src/main/java/hello/core/order/OrderServiceImpl.java]

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);
        int discountPrice = discountPolicy.discount(member, itemPrice);

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

}
  • 설계 변경으로 OrderServiceImplFixDiscountRepository를 의존 x
  • DiscountPolicy 인터페이스만 의존 → DIP 충족
  • 실행에만 집중하면 됨
    • 생성자를 통해 어떤 구현 객체가 주입될 지 알 수 x
    • 생성자를 통해 주입할 구현 객체는 외부(Appconfig)에서 결정

✅ AppConfig 실행

📝 사용 클래스 - MemberApp

[src/main/java/hello/core/MemberApp.java]

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 = appConfig.memberService();

        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("find Member = " + findMember.getName());
    }
}

📝 사용 클래스 - OrderApp

[src/main/java/hello/core/OrderApp.java]

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

📝 테스트 코드 오류 수정

[src/test/java/hello/core/member/MemberServiceTest.java]

class MemberServiceTest {

    MemberService memberService;

    @BeforeEach  //테스트 실행하기 전 호출
    public void beforeEach() {
        AppConfig appConfig = new AppConfig();
        memberService = appConfig.memberService();
    }
    
}

[src/test/java/hello/core/order/OrderServiceTest.java]

class OrderServiceTest {

    MemberService memberService;
    OrderService orderService;

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


📖 AppConfig 리팩터링

✅ 기대하는 그림

✅ 리팩터링 전

[src/main/java/hello/core/AppConfig.java]

package hello.core;

import hello.core.discount.FixDiscountPolicy;
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(new MemoryMemberRepository());  //중복
    }
    
    public OrderService orderService() {
        return new OrderServiceImpl(
                new MemoryMemberRepository(), //중복
                new FixDiscountPolicy());
    }
}
  • new MemoryMemberRepository() 중복
  • 역할에 따른 구현이 잘 안보임

✅ 리팩터링 후

[src/main/java/hello/core/AppConfig.java]

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemberRepository;
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());  
    }
    
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
  • 중복(new MemoryMemberRepository()) 제거
    → 구현체 변경시 memberRepository()만 변경하면 됨
  • 역할과 구현 클래스가 한 눈에 들어옴
    → 애플리케이션 전체 구성 빠르게 파악 가능


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

AppConfig의 등장으로 애플리케이션이 크게 사용 영역구성(Configuration) 영역으로 분리됨

✅ 사용 - 구성의 분리

✅ 할인 정책의 변경


FixDiscountPolicy에서 RateDiscountPolicy로 변경하는 경우

  • 구성 영역 → 영향 O
  • 사용 영역 → 영향 X

✅ 할인 정책 변경 구성 코드

[src/main/java/hello/core/AppConfig.java]

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemberRepository;
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());  
    }
    
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy());
    }
    
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    
    public DiscountPolicy discountPolicy() {
      //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}
  • 할인 정책 역할 담당 구현 변경
    FixDiscountPolicyRateDiscountPolicy
  • 정책 변경시 AppConfig만 변경하면 됨
    • 사용 영역 → 어떤 코드도 변경할 필요 x
    • 구성 영역(=AppConfig) → 변경 필요


📖 전체 흐름 정리

✅ 새로운 할인 정책 개발

다형성 → 새로운 할인 정책 코드를 추가로 개발하는 것 자체는 문제 x

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

  • 새로 개발한 할인 정책 적용을 위해서는 클라이언트 코드인 구현체도 함께 변경해야 함
    OCP 위반
  • 클라이언트가 인터페이스 뿐만 아니라 구체 클래스도 의존 → DIP 위반

✅ 관심사의 분리

  • 기존에는 클라이언트가 의존하는 서버 구현 객체를 직접 생성 및 실행
  • 구현 객체 생성 및 연결 책임을 가지는 AppConfig(공연 기획자) 등장
    → 애플리케이션의 전체 동작 방식 구성(config)
  • 클라이언트 객체의 책임이 명확해짐 → SRP 충족
    • 자신의 역할 실행에만 집중
    • 권한 감소

✅ AppConfig 리팩터링

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

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

  • 할인 정책 변경 : 정액 → 정률
  • AppConfig의 등장으로 애플리케이션 영역 분리
    • 객체 사용 영역
    • 객체 생성 및 구성(Configuration) 영역
  • 정책 변경시
    • AppConfig가 있는 구성 영역변경하면 됨
    • 사용 영역(클라이언트 코드)은 변경 x
profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글