LostCatBox

SpringCore CH02~03

Word count: 1.8kReading time: 11 min
2022/12/24 Share

스프링 핵심 원리 (기본편) CH02~03

단축키 정리(인텔리J)

1
2
3
4
5
6
7
command+shift + T // 테스트 케이스 만들기
command +shift+enter //자동완성
command +D // 선택영역 복사
command + option +V // 변수로 바로 생성
command + E //최근에 봣던 목록
command + option + m //extract method
command + option + n //inline으로 바꿔줌

Core 프로젝트

  • start.io를 통해 java 11버전 gradle로 인텔리제이로 세팅완료
  • 인텔리제이에서 preference에서 gradle에서 test환경과 실행환경 둘다 인텔리제이로 하는것이 훨씬 원활한 실행가능..(변경권장)

요구사항

Untitled

스크린샷 2022-06-14 오후 1.11.20.png

  • 회원 데이터, 할인 정책같은 부분은 인터페이스를 만들어서 연동해야함>> 추후 구현체 갈아끼우기가능해야함

스크린샷 2022-06-14 오후 1.11.34.png

스크린샷 2022-06-14 오후 1.11.48.png

스크린샷 2022-06-14 오후 1.11.55.png

설계방식(!!!)

  1. 도메인
    (역할별로나누기==기획자)
  2. 클래스 다이어그램
    (인터페이스 및 클래스 의존관계 나눠보기)(정적)
  3. 객체 다이어그램
    (인터페이스 및 클래스별로 나눠보기==동적결정이므로, 실제로 쓰는 객체는 객체 다이어그램보기)(메모리간의 참조를 그려보기)(동적)

클래스대로 세팅완료

  • 입문과정과 거의 비슷하여 생략

테스트시도

  • java로만
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package hello.core;

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

public class MemberApp {
public static void main(String[] args) {
MemberService memberService = new MemberServiceImpl();
Member member = new Member(1L, "memberA", Grade.VIP);
memberService.join(member);

Member findMember = memberService.findMember(1L);
System.out.println("meM"+findMember);
System.out.println("member = " + member);
}
}
  • jtest사용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package hello.core.member;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;

public class MemberServiceTest {

MemberService memberService = new MemberServiceImpl();
@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);
}
}

주문과 할인 도메인 설계

  • 기능으로 일단 도메인 분리
  • 역할과 구현 분리해보기(클래스, 객체 다이그램그리기)

스크린샷 2022-06-14 오후 2.30.05.png

스크린샷 2022-06-14 오후 2.30.13.png

스크린샷 2022-06-14 오후 2.30.28.png

스크린샷 2022-06-14 오후 2.30.34.png

스크린샷 2022-06-14 오후 2.30.40.png

스크린샷 2022-06-14 오후 2.31.06.png

구현

테스트

  • java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package hello.core;

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

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

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);
System.out.println("order.calculatePrice() = " + order.calculatePrice());
}
}
  • j test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package hello.core.order;

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

public class OrderServiceTest {
MemberService memberService = new MemberServiceImpl();
OrderService orderService = new OrderServiceImpl();

@Test
void createOrder() {
long memberId = 1L;
Member member = new Member(memberId, "memberA", Grade.VIP);
memberService.join(member);

Order order = orderService.createOrder(memberId, "itemA", 10000);
Assertions.assertThat(order.getDiscountPrice()).isEqualTo(1000);
}
}

새로운 할인 정책 구현(OCP, DIP규칙)

요구 조건

스크린샷 2022-06-14 오후 5.22.32.png

  • 현재 OCP와 DIP를 지킬수가 없다. 지키지 못함
  • 따라서 스프링이 나타나기까지 이유 알 수 있다

구현

  • DiscountPolicy 인터페이스 구현한 파일 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}
}
}

테스트

  • VIP일때, 아닐떄 구별하여 TEST둘다 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;

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
Assertions.assertThat(discount).isEqualTo(1000);
}

@Test
@DisplayName("VIP가 아닌경우, 할인이 적용되지않아야한다")
void vip_x(){
//given
Member member = new Member(2L, "memberBASIC", Grade.BASIC);
//when
int discount = discountPolicy.discount(member, 10000);

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

}

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

  • 할인 정책을 변경하려면 클라이언트인 OrderServiceImpl 코드를 고쳐야 한다.
  • 방금 추가한 할인 정책은 분명, 인터페이스를 따르고있지만, 의존하는 곳들에서는 인터페이스 = new 구현체 를 따르고있기에 new 구현체 부분을 결국 전부다 바꿔줘야만 하는 한계를 가진다.

문제점

  • DIP(항상추상화에만 의존해라), OCP(코드변경X하면서 확장O) 규칙 위반

스크린샷 2022-06-14 오후 10.24.08.png

스크린샷 2022-06-14 오후 10.24.45.png

스크린샷 2022-06-14 오후 10.29.07.png

스크린샷 2022-06-14 오후 10.28.52.png

해결해야할 부분

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package hello.core.order;

public class OrderServiceImpl implements OrderService{

private final MemberRepository memberRepository = new MemoryMemberRepository();
// private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
private DiscountPolicy discountPolicy; //추상화만 남김.. 참조변수값은 null값인 상태

@Override
public Order createOrder(Long memberId, String itemName, int itemPrice) {
Member member = memberRepository.findById(memberId);
int discountPrice = discountPolicy.discount(member,itemPrice); //discountPolicy만 단일책임!!할인만고치면됨

return new Order(memberId,itemName,itemPrice,discountPrice);
}
  • DIP문제를 해결하려면, 구현체를 모두 제거하고 추상화에만 남긴다.. 근데 구현체가없는데? 실행은 불가(NullPointEx)
  • 이문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl에 DiscountPolicy의 구현 객체를 대신 생성하고 주입해줘야함!!!!

관심사의 분리

  • 애플리케이션을 하나의 공연이라 생각해보자
  • 배우가 직접 상대방 배우를 직접선택하는것이 아닌 상대방 배역을 의존하고있어야한다!
  • 그리고 모든 배역에 대해서 공연 기획자를 따로 만들어서 공연 기획자에게 책임지게함

AppConfig등장(!!!)

  • 애플리케이션의 전체 동작 방식을 구성(config)하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는
    별도의 설정 클래스를 만들자.
  • AppConfig.java
1
2
3
4
5
6
7
8
9
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(),new RateDiscountPolicy());
}

}

스크린샷 2022-06-14 오후 10.59.03.png

  • 의존하는 쪽에서는 반드시 생성자로 설정해줘야한다.(의존하는쪽에서는 어떤 구현 객체가 들어올지 알수없다
  • 어떤 객체 주입되는것은 오직 외부 AppConfig에서 결정된다
  • 클래스다이어그램

스크린샷 2022-06-14 오후 11.18.08.png

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

스크린샷 2022-06-14 오후 11.19.00.png

  • 마지막줄 중요!!! 클라이언트로써 의존관계있던것을 주입해줌
  • test 코드시 앞으로는 AppConfig를 사용하여, 통일하고, 초기화
1
2
3
4
5
6
@BeforeEach
public void beforeEach(){
AppConfig appConfig = new AppConfig();
memberService = appConfig.memberService();
orderService = appConfig.orderService();
}

정리

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

AppConfig 리팩터링

  • 현재 AppConfig를 보면 중복이 있고, 역할에 따른 구현이 잘 안보인다.

  • 한눈에 역할이 보이는게 중요

    스크린샷 2022-06-15 오전 12.16.29.png

Before

  • 중복제거, 역할에 따른 구현 보이도록 리팩터링하자
1
2
3
4
5
6
7
8
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(new MemoryMemberRepository());
}
public OrderService orderService(){
return new OrderServiceImpl(new MemoryMemberRepository(),new RateDiscountPolicy());
}
}

After

  • 역할과 구현이 정확히 구별되어있음
  • 구성이보이며, 중복도제거, 구현체 교체시 리턴값만 교체 편리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AppConfig {
public MemberService memberService(){

return new MemberServiceImpl(memberRepository());
}

public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}

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

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

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

  • 사용영역과 구성영역이 구별됨.

    스크린샷 2022-06-15 오전 12.31.11.png

  • 실제로 할인 정책 적용시 AppConfig만 고치면 끝이다..

    스크린샷 2022-06-15 오전 12.36.12.png

  • 사용영역에 있는 코드는 변경없다!!!

  • 구성 영역에서만 변경하면됨

정리

  • 새로운 정률 할인 정책으로 적용시 DIP, OCP모두 위반
  • 관심사 분리하여 구성 기획자를 만듬
  • 클라이언트 객체는 자신의 역할을 실행하는 것만 집중, 권한줄어듬(책임명확)
  • 구성 정보에서 역할 구현 명확하게 분리!!
  • 새로운 구조 다시 적용할때는 구성 영역만 하나만 고치면 모든것이 적용완료됨(실제 사용 영역은 코드변경X)

좋은 객체 지향 설계의 5가지 원칙적용여부판단

SRP 단일 책임 원칙

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 클라이언트에서 다양한 책임을 없애고, 관심사 분리>> AppConfig가 구현 객체 생성 연결담당시킴
  • 클라이언트 객체는 실행 책임만 담당

DIP 의존관계 역전 원칙

  • 프로그래머는 추상화에 의존해야지, 구체화에 의존하면 안된다
  • 기존코드는 추상화와 구현체를 모두 갖고 의존하고있었다.
  • AppConfig로 클라이언트 코드에 의존관계를 주입했다. 추상화로만 의존되니, DIP원칙 따르게됨

OCP

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야한다.
  • 다형성 사용하고 클라이언트가 DIP가 지켜야한다
  • 사용영역과 구성 영역으로 나눠서, 클라이언트코드는 어떤 코드도 변경안해도 주입해서 확장가능
  • 소프트웨어 요소는 확장가능해도 사용 영역의 변경은 닫혀있다

IoC, DI, 컨테이터

제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
    한마디로 구현 객체가 프로그램의 제어 흐름을 스스로 조종했다. 개발자 입장에서는 자연스러운 흐름이다.
  • 반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의
    제어 흐름은 이제 AppConfig가 가져간다. 예를 들어서 OrderServiceImpl 은 필요한 인터페이스들을 호출하지만 어떤 구현 객체들이 실행될지 모른다.(제어권없음)
  • 프로그램에 대한 제어 흐름에 대한 권한은 모두 AppConfig가 가지고 있다. 심지어 OrderServiceImpl도 AppConfig가 생성한다. 그리고 AppConfig는 OrderServiceImpl 이 아닌 OrderService인터페이스의 다른 구현 객체를 생성하고 실행할 수 도 있다. 그런 사실도 모른체 OrderServiceImpl 은 묵묵히 자신의 로직을 실행할 뿐이다.
  • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라한다.

프레임워크 vs 라이브러리

  • 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit)(제어의 역전일어남)(프레임워크가 내가짠코드를 실행해줌)
  • 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.(제어의 역전X)

의존 관계 주입 DI(Dependency Injection)

스크린샷 2022-06-15 오전 1.11.19.png

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

정적인 클래스 의존관계

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

동적인 객체 인스턴스 의존 관계(!!!)

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

스크린샷 2022-06-15 오전 1.15.34.png

DI 컨테이너(=IoC 컨테이너)

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI 컨테이너라 한다. 또는 어샘블러, 오브젝트 팩토리 등으로 불리기도 한다.

스프링으로 전환하기

  • 지금까지 순수한 자바코드만으로 DI를 적용했다.
  • AppConfig에 설정을 구성한다는 뜻으로 @Configuration
  • 각 메서드앞에는 @Bean 사용하여, 스프링 빈으로 등록

스프링 컨테이너

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

테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MemberApp {
public static void main(String[] args) {
// AppConfig appConfig = new AppConfig();
// MemberService memberService = appConfig.memberService();
// MemberService memberService = new MemberServiceImpl();
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("meM"+findMember);
System.out.println("member = " + member);
}
}
CATALOG
  1. 1. 스프링 핵심 원리 (기본편) CH02~03
  2. 2. 단축키 정리(인텔리J)
  3. 3. Core 프로젝트
    1. 3.1. 요구사항
    2. 3.2. 설계방식(!!!)
    3. 3.3. 클래스대로 세팅완료
    4. 3.4. 테스트시도
    5. 3.5. 주문과 할인 도메인 설계
      1. 3.5.1. 구현
      2. 3.5.2. 테스트
  4. 4. 새로운 할인 정책 구현(OCP, DIP규칙)
    1. 4.1. 요구 조건
    2. 4.2. 구현
    3. 4.3. 테스트
    4. 4.4. 새로운 할인 정책 적용과 문제점
      1. 4.4.1. 문제점
      2. 4.4.2. 해결해야할 부분
  5. 5. 관심사의 분리
    1. 5.0.1. AppConfig등장(!!!)
  6. 5.1. 정리
  • 6. AppConfig 리팩터링
    1. 6.1. Before
    2. 6.2. After
  • 7. 새로운 구조와 할인 정책 적용
  • 8. 정리
  • 9. 좋은 객체 지향 설계의 5가지 원칙적용여부판단
    1. 9.1. SRP 단일 책임 원칙
    2. 9.2. DIP 의존관계 역전 원칙
    3. 9.3. OCP
  • 10. IoC, DI, 컨테이터
    1. 10.1. 제어의 역전 IoC(Inversion of Control)
      1. 10.1.1. 프레임워크 vs 라이브러리
    2. 10.2. 의존 관계 주입 DI(Dependency Injection)
      1. 10.2.1. 정적인 클래스 의존관계
      2. 10.2.2. 동적인 객체 인스턴스 의존 관계(!!!)
    3. 10.3. DI 컨테이너(=IoC 컨테이너)
  • 11. 스프링으로 전환하기
    1. 11.1. 스프링 컨테이너
    2. 11.2. 테스트