[Spring] #3. 스프링 핵심 원리 이해2-객체 지향 원리 적용 (김영한_인프런_스프링핵심원리)

bien·2023년 4월 10일
0

Spring_basic

목록 보기
3/9

새로운 할인 정책 개발


할인정책을 정액할인에서 “정률할인”으로 변경하기로 함.
=> 애자일 소프트웨어 개발 선언. https://agilemanifesto.org/iso/ko/manifesto.html

공정과 도구보다 개인과 상호작용
포괄적인 문서보다 작동하는 소프트웨어
계약 협상보다 고객과의 협력
계획을 따르기보다 변화에 대응하기

RateDiscountPolicy

package hello.core.discount;

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

public class RateDiscountPolicy implements DiscountPolicy {

    private int discountPercent = 10;

    @Override
    public int discount(Member member, int price) {
        if(member.getGrade() == Grade.VIP) {
            return price * discountPercent / 100;
        } else {
            return 0;
        }
    }
}

RateDiscountPolicyTest

package hello.core.discount;

import hello.core.member.Grade;
import hello.core.member.Member;
import org.assertj.core.api.Assertions;
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.discount(member, 10000);
        // then
        assertThat(discount).isEqualTo(1000);
    }

    @Test
    @DisplayName("VIP가 아니면 10%할인이 적용되지 않아야 한다.")
    void vip_x() {
        // given
        Member member = new Member(1L, "memberBasic", Grade.BASIC);
        // when
        int discount = discountPolicy.discount(member, 10000);
        // then
        assertThat(discount).isEqualTo(0);
    }
}

alt+enter

Assertions는 add on-demand static import for'~'해주는게 좋다.
그럼 코드가 많이 단축됨.

영한님이 10% 할인하는 코드도 굉장히 많은 테스트가 필요하다고 하셨다. 지금 굉장히 코드가 잘 짜여있어 테스트도 쉬운 것이라고. 배우는 입장에서 실감이 안 될수도 있지만 지금 테스트가 굉장히 잘 이루어진 것이라고. 아직 잘 모르는 내 눈에도 어떤 느낌인지 막연히 와닿았다. 지금 코드가 단순히 학습을 위해 최소한으로 짜여졌음에도 꽤나 양이 된다. 코드가 방대해지고 복잡해지면 테스트가 얼마나 어려울지.. 바로바로 테스트하고 최소한의 단위로 테스트를 시험해보는 것이 너무 중요할 것 같다.


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

할인 정책을 애플리케이션에 적용해보자.

  • 할인 정책을 변경하기 위해 클라이언트인 OrderServiceImpl 코드를 수정했다.
public class OrderServiceImpl implements OrderService {

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

}

문제점 발견

  • 우리는 역할과 구현을 충실하게 분리했다. = Ok
  • 다형성도 활용하고, 인터페이스와 구현 객체를 분리했다 = Ok
  • OCP, DIP 같은 객체지향 설계 원칙을 충실히 준수했다. => No!!
    DIP: DisscountPolicy(추상 의존), FixDiscountPolicy, RateDiscountPolicy(구체 클래스 의존) 클래스와 인터페이스를 동시에 의존하고 있다. => 위반!
    OCP: 변경하지 않고 확장이 가능해야 한다. -> 기능을 확장해 변경하자 클라이언트 코드에 영향을 줘 변경해야 했다. => 위반!

기대했던 의존관계

실제 의존관계 & 정책 변경


관심사의 분리

📢 애플리케이션을 하나의 공연이라 생각해보자. 각각의 인터페이스를 배역(배우 역할)이라 생각하자. 그런데! 실제 배역을 맡을 배우를 선택하는 것은 누가 하는가?
✔️ 로미오와 줄리엣 공연을 하면 로미오 역할을 누가 할지 줄리엣 역할을 할지는 배우들이 정하는게 아니다. 이전 코드는 마치 로미오 역할(인터페이스)을 하는 레오나르도 데카프리오(구현체, 배우)가 줄리엣 역할(인터페이스)을 하는 여자 주인공(구현체, 배우)을 직접 초빙하는 것과 같다. 디카프리오는 공연도 해야하고 동시에 여자 주인공도 공연에 직접 초빙해야 하는 다양한 책임을 가지고 있다.

관심사를 분리하자.

  • 배우는 본인의 역할인 배역을 수행하는 것에만 집중해야 한다.
  • 디카프리오는 어떤 여자 주인공이 선택되더라도 똑같이 공연을 할 수 있어야 한다.
  • 공연을 구성하고, 담당 배우를 섭외하고, 역할에 맞는 배우를 지정하는 책임을 담당하는 별도의 공연 기획자가 나올 시점이다.
  • 공연 기획자를 만들고, 배우와 공연 기획자의 책임을 확실히 분리하자.

AppConfig 등장

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

AppConfig

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

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

MemberSerciceImpl - 생성자 주입

package hello.core.member;

import hello.core.order.OrderServiceImpl;

public class MemberServiceImpl implements MemberService{

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

클래스 다이어그램

  • DIP가 완성되었고, 관심사가 분리되었다.(객체 생성 및 연결 - 실행 역할 분리)

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

OrderServiceImpl - 생성자 주입

package hello.core.order;

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

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

OrderServiceImpl은 이제 실행에만 집중. 생성자를 통해 외부(AppConfing)에서 어떤 구현 객체를 주입할지 결정한다.

AppConfig 실행

사용클래스 - 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 = 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("findMember : " + findMember.getName());
    }
}

appConfig로부터 MemberService를 받아서 적용.

사용클래스 - 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);

    }
}

테스트 코드 수정

MemberServiceImplTest

class MemberServiceImplTest {
    MemberService memberService;

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

}    

OrderServiceImplTest

public class OrderServiceTest {

    MemberService memberService;
    OrderService orderService;

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

정리

  • AppConfig가 공연 기획자 역할을 수행하면서 관심사를 확실히 분리했다.
  • AppConfig는 구체 클래스를 선택한다. 배역에 맞는 담당 배우를 선택한다. 애플리케이션이 어떻게 동작해야 할지 전체 구성을 책임진다.
  • 이제 각각의 배우(OrderServiceImpl, MemberServiceImpl)들은 기능을 실행하는 책임만 지면 된다.

AppConfing 리팩터링

현재 AppConfig의 문제점:

  • 중복이 있음
  • 역할에 따른 구현이 분명하지 않음
    => 역할에 따른 구현이 한눈에 파악 가능한 것이 중요하다. 이렇게 수정해보자!

기대하는 그림

어떤 인터페이스들이 있고, 해당 인터페이스들에 대해 어떤 구현체를 사용하는지 한눈에 파악 가능해야 함

ctrl + alt + M: 메서드 추출 기능
return type: interface (구체 클래스로 선택 하지 않는다)
name: memberRepository()
중복값 알림창(intellij): replace 선택해 중복값 모두 바꾸도록.

리팩터링 후

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.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;

public class AppConfig {

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

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

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

    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }

}

메서드명 만으로도 역할이 다 드러남.
각각의 인터페이스(OrderService, discountPolicy ...)에 어떤 구현체를 사용하는지 한눈에 확인 가능.
설계에 대한 그림이 구성정보에 그대로 드러남. 역할들이 나오고, 역할들의 구현을 선명히 표기 가능.

  • 그냥 코드를 작성하는게 아니라, 역할을 분명히 세우고 그 안에 구현이 들어가도록 코드를 작성했음.
    • 중복제거 및 직관성 획득

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

이제 정액할인 정책을 “정률 할인 정책”으로 변경해보자.
어떤 부분만 변경하면 될까?

AppConfig의 등장으로 애플리케이션이 크게 사용영역과, 객체를 생성하고 구성(Configuration)하는 영역으로 분리 되었다.

할인 정책을 변경하는 경우

코드 변경 시 구성 영역만 영향을 받고, 사용 영역은 전혀 영향을 받지 않는다.

할인 정책 변경 코드

public class AppConfg {

    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

구성 영역은 당연히 변경된다. 구성 역할을 담당하는 AppConfig를 애플리케이션이라는 공연의 기획자로 생각하자. 공연 기획자는 공연 참여자인 구현 객체들을 모두 알아야 한다.


전체 흐름 정리

새로운 할인 정책 개발
: 다형성(인터페이스 - 클래스)으로 추가 코드 개발은 쉬웠음.

새로운 할인 정책 적용과 문제점
DIP위반(DiscountPolicy discountPolicy = new FixDiscountPolicy): 인터페이스와 클래스 동시 의존
=> 할인을 변경하니 클라이언트 코드도 함께 변경해야 함.

관심사의 분리
AppConfig의 등장: 구현 객체 생성 & 연결을 책임.
클라이언트 객체는 자신의 역할 실행에만 집중 + 권한 축소 (=책임이 명확해짐)

AppConig 리팩터링
"역할vs구현"이 명확히 표기되도록 코드 변경
“메서드명(인터페이스) : 반환값(클래스)” 형태. 직관적으로 인터페이스와 구현체의 관계를 확인가능

새로운 구조와 할인 정책 적용
AppConfig의 등장으로 애플리케이션이 크게 사용 영역과, 객체를 생성하고 구성하는 영역으로 분리
구현체가 변경되어도, 구성 영역만 변경하면 됨. 사용 영역은 변경할 필요가 없음.(클라이언트 코드 변경 필요하지 않음)


좋은 객체 지향 설계의 5가지 원칙의 적용

3가지가 적용됨: SRP, DIP, OCP

SRP 단일 책임 원칙

한 클래스는 하나의 책임만 가져야 한다.

  • 기존 코드에서 클라이언트 개체의 책임: 직접 구현 객체 생성 + 연결+ 실행+ 기능 수행..
    => 구현 객체 관리(생성, 연결 등)의 책임은 AppConfig에게 넘겨줌
    => 클라이언트 객체는 실행의 책임만 담당

DIP 의존관계 원칙

프로그래머는 “추상화에 의존해야지 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 장법 중 하나다.

  • DiscountPolicy discountPolicy = new FixDiscountPolicy();
    => 추상화 인터페이스에만 의존하도록 변경 + 생성자를 통해 인스턴스를 주입(의존관계 주입)
DiscountPolicy discountPolicy;

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

OCP 개방-폐쇄 원칙

소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.

  • 다형성 사용 + DIP준수
  • 애플리케이션을 사용 영역과 구성영역으로 구분
  • 소프트웨어 요소를 새롭게 확장해도 사용 영역의 변경은 닫혀있다!

IoC, DI, 그리고 컨테이너

IoC (Inversion of Control)

  • 기존에서는 클라이언트 구현 객체(OrderServiceImpl)가 직접 객체를 생성, 연결, 실행하며 프로그램의 ‘제어 흐름(Control)’을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다.
  • AppConfig 등장 이후, 객체 관리를 AppConfig가 맡게 되었다. 즉 프로그램의 ‘제어 흐름(Control)'을 외부에서 담당한다. 이제 클라이언트 구현 객체는, 자신이 호출하는 인터페이스가 어떤 구현체를 통해 실현될지 알 수 없으며, 심지어는 클라이언트 객체 스스로의 사용 유무도 알 수 없다.
  • 이 같은 프로그램에 대한 ‘제어 흐름’이 다른 이에게 빼앗겨 외부에서 관리하므로, '제어의 역전(Inversion of Control)' 이라고 한다.

프레임워트 vs 라이브러리

  • 프레임워크: 내가 작성한 코드를 대신 제어하고 실행 (JUnit)
  • 라이브러리: 내가 작성한 코드가 직접 제어의 흐름을 담당

의존관계 주입 DI (Dependency Injection)

  • OrderServiceImplDiscountPolicy 인터페이스에 의존한다. 실제 어떤 구현 객체가 사용될지는 모른다.
  • 의존관계는 정적인 클래싀 의존관계와, 실행시점에 결정되는 동적인 객체(인스턴스) 의존관계 둘을 분리해서 생각해야 한다.

정적인 클래스 의존관계

클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 정적인 의존관계는 애플리케이션을 실행하기도 전에, 클래스 다이어그램만으로 의존관계가 파악 가능하다.
그런데 이러한 클래스 의존관계만으로는 실제 어떤 객체가 OrderServiceImpl에 주입될지 알 수 없다.

(클래스 다이어그램)

동적인 객체 인스턴스의 의존관계

애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계다.

(객체 다이어그램)

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성해서 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결되는 것을 의존관게 주입이라 한다.
  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
  • DI장점: 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스의 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너, DI 컨테이너

  • AppConfig: IoC컨테이너, DI 컨테이너, 어셈블러(조합해준다고해서), 오브젝트 팩토리 etc..
  • IoC의 의미가 광범위해 DI로 축소. 주로 DI컨테이너라 부름

스프링으로 전환하기

지금까지 순수 자바 코드로 DI를 적용했다. 이제 스프링을 사용해보자!!

AppConfig 스프링 기반으로 변경

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.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

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

    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

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

    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }

}

-클래스명 위에 @Configuration: AppConfig에 설정을 구성한다는 뜻
-각 메서드 위에 @Bean: 스프링 컨테이너에 스프링 빈으로 등록함

MemberApp에 스프링 컨테이너 적용

package hello.core;

import hello.core.member.Grade;
import hello.core.member.Member;
import hello.core.member.MemberService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {

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

        // 스프링은 모두 이걸로 시작. 이게 스프링 컨테이너. 모든걸 관리해줌.
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        // (멤버 이름, 반환타입)

        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에 스프링 컨테이너 적용

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;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class OrderApp {

    public static void main(String[] args) {

//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();
//        OrderService orderService = appConfig.orderService();
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

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

    }
}

@Bean으로 등록해 놓은 것들이 스프링 컨테이너에 메서드 이름으로 등록됨.
이 이름으로 컨테이너에서 찾고, 해당 타입으로 돌려받음.
실제 스프링 코드에서 어떤 이름으로 빈들이 등록됐는지 확인 가능.

스프링 컨테이너

  • ApplicationContext : 스프링 컨테이너
  • 기존에는 개발자가 AppConfig를 사용해 직접 객체 생성하고 DI함. 그러나 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용함. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록함. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean이 붙은 메서드 명을 스프링 빈의 이름으로 사용한다.(memberService, OrderService)
  • 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회했으나, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean()메서드를 사용해서 찾을 수 있다.
  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.

코드가 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까??

요약하자면, 많은 장점을 가져갈 수 있다. 앞으로 알아보자!!

profile
Good Luck!

0개의 댓글