목표

1. 필터를 이용해 컴포넌트 스캔에서 대상을 제외하거나 추가하는 것을 배운다. 
2. 빈 이름이 중복 되었을 때 처리를 배운다. 

'컴포넌트 스캔' 목차

1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

2. 탐색 위치와 기본 스캔 대상

3. 필터 (이번 포스팅)

4. 중복 등록과 충돌 


3. 필터

1) 컴포넌트 스캔 필터에 쓸 애노테이션을 만들어보자. 

애노테이션 2개를 만드는데, '포함'한다는 의미로 MyIncludeComponent 라고 이름붙인다.

애노테이션 MyExcludeComponent 는 제외의 용도로 만든다. 

클래스 2개를 만들고 각각 다른 애노테이션을 붙인다.

BeanA 클래스에는 @MyIncludeComponent 를 붙인다. BeanB 에는 @MyExcludeComponent 를 붙인다. 

2) 컴포넌트 스캔에 필터를 적용한 테스트 코드를 작성한다.

@ComponentScan 에 includeFilters와 excludeFilters를 적용할 수 있다. 

includeFilters 에는 포함할 애노테이션인 MyIncludeComponent.class를 써주고,

excludeFilters 에는 제외하는 필터 애노테이션이니까 MyExcludeComponent.class를 써주자. 

3) 필터가 적용된 컴포넌트 스캔을 하되, 스프링 빈이 생성됬는지 확인하자.

BeanA만 컴포넌트 스캔에 포함되어야 한다.  BeanA 스프링 빈으로 등록되고,  BeanB 라는 빈은 찾을 수 없어야 테스트 성공이다. 

3) FilterType 옵션 5가지를 알아보자. 

 

* ANNOTATION  : 기본값. 애노테이션을 인식해서 동작하는 것이 기본이다. 

" type = FilterType.ANNOTATION "을 지워도 똑같이 동작한다. 

* ASSIONABLE_TYPE  : 지정한 타입과 자식 타입을 인식해서 동작한다 

    org.example.SomeClass

ASPECTJ  : AspectJ 패턴을 사용한다 

    org.example..*Service+

* REGEX  : 정규표현식을 줄 수 있다 

    org\.example\.Default.*

* CUSTOM  : TypeFilter 라는 인터페이스를 구현해서 처리한다 

    org.example.MyTypeFilter

 

[ 참고 ]

@Component 애노테이션으로 충분하기 때문에 includeFilters 를 사용할 일은 드물다. excludeFilters는 아주 가끔 쓴다. 

스프링 부트가 기본으로 제공하는 컴포넌트 스캔을 사용하는 것을 권장한다. 


4. 중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까? 

다음 두 가지 상황이 있다.

 

1) 자동 빈 등록 vs 자동 빈 등록 

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록될 때 이름이 같으면 오류가 발생한다. 

 

예를 들어, 2개의 빈에 service라고 이름 붙여보자. 

MemberServiceImpl 과 OrderServiceImpl에 둘 다 @Component("service")라고 붙이자. 

컴포넌트 스캔을 해보면, 빈이 충돌난다고  ConflictingBeanDefinitionException  에러가 난다. 

org.springframework.beans.factory.BeanDefinitionStoreException: 
Failed to parse configuration class [hello.corebasic.AutoAppConfig]; 
nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: 

Annotation-specified bean name 'service' for bean class [hello.corebasic.order.OrderServiceImpl] conflicts with existing, 
non-compatible bean definition of same name and class [hello.corebasic.member.MemberServiceImpl]

에러 내용이 굉장히 친절하다. 애노테이션으로 이름지어진 'service'라는 빈 이름이 있는데,

MemberServiceImpl 에도 정의되어 있고, OrderServiceImpl 에도 같은 이름으로 정의되어 있다는 내용이다. 

 

2) 수동 빈 등록 vs 자동 빈 등록 

이 경우, 수동 빈 등록이 우선권을 가진다. 수동 빈이 자동 빈을 오버라이딩한다. 

 

예를 들어, MemoryMemberRepository@Component 를 달아놔서 컴포넌트 스캔의 대상이되고

빈 이름은 기본으로 클래스 명 앞글자 하나만 소문자로 바꿔서 memoryMemberRepository이름지어진다. 

@ComponentScan 하는 설정에서 memoryMemberRepository라는 똑같은 이름으로 MemoryMemberRepository 빈 등록을 시도해보자. 

컴포넌트 스캔 하는 테스트를 실행하면? 성공하긴 한다. 

수동 빈이 자동 빈을 오버라이딩 한다고 로그 메시지가 출력된다. 

Overriding bean definition for bean 'memoryMemberRepository' with a different definition: 
replacing [Generic bean: class [hello.corebasic.member.MemoryMemberRepository];

 

하지만 현실은 개발자가 의도적으로 설정해서 이런 결과를 낳기보다는 여러 설정들이 꼬여서 이렇게 되는 경우가 다반사다!

이러면 정말 잡기 어려운 버그가 만들어진다. 항상 잡기 어려운 버그는 애매한 버그다. 

 

3) 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 이름 충돌이 나면 아래 내용의 오류를 내도록 바뀌었다. 

스프링 부트를 실행해보자. 

"이미 AutoAppConfig.class 에 정의된(수동으로 등록한) 'memoryMemberRepository' 빈이 등록되지 못했다. 

MemoryMemberRepository 에 정의된 이름으로 빈이 등록되었기 때문이다. 그리고 오버라이딩이 불가능하다. "

The bean 'memoryMemberRepository', 
defined in class path resource [hello/corebasic/AutoAppConfig.class], 
could not be registered. 
A bean with that name has already been defined in file [/Users/.../hello/corebasic
/member/MemoryMemberRepository.class] and overriding is disabled.

스프링 부트의 기본 설정으로는 이렇게 오류를 내지만, 

application.yml 애플리케이션 프로퍼티 파일에 옵션을 아래처럼 바꾸면 수동 등록 빈 오버라이딩을 허용해준다. 

Consider renaming one of the beans or enabling overriding by setting 
spring.main.allow-bean-definition-overriding=true

 

[ 참고 ] 

어설픈 추상화를 하지 말자. 코드 양이 좀더 나오더라도 명확하게 코딩하는 것이 낫다.

애매한 상황과 코드는 피해야 한다. 애매한 버그가 제일 잡기 어렵기 때문이다. 그리고 개발은 혼자하는 것이 아니기 때문이다. 


다음 강의에서는 다양한 의존관계 주입 방법을 배운다. 

공부 내용 출처 :  스프링 핵심 원리 기본편 

728x90

목표

1. 자바 설정 정보 없이 스프링 빈을 등록할 수 있는 '컴포넌트 스캔'을 배운다. 
2. 의존관계를 자동으로 주입하는 @Autowired 기능을 배운다. 

'컴포넌트 스캔' 목차

1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기 (이번 포스팅)

2. 탐색 위치와 기본 스캔 대상 

3. 필터 

4. 중복 등록과 충돌 


1. 컴포넌트 스캔과 의존관계 자동 주입 시작하기

지금까지 애노테이션 기반 자바 설정 정보로 스프링 빈을 생성했다. 

실무에서는 스프링 빈이 수십 수백개가 필요한데, 관리할 것이 많으면 실수가 나오게 된다. 좀 더 편리한 방법이 있을까? 

스프링은 설정 정보 파일이 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다. 

 

컴포넌트 스캔으로 자바 설정 정보를 대체해보자. 

AppConfig.class 는 그대로 두고, 새롭게 AutoAppConfig.class 를 만들자. 이제 컴포넌트 스캔으로 스프링 빈을 등록해보자. 

 

1) 자바 설정 정보 AutoAppConfig.class 만들고, @Configuration 과 @ComponentScan 를 붙인다. 

기존의 AppConfig.class와는 다르게 @Bean으로 등록한 클래스가 하나도 없다. 

@ComponentScan 애노테이션은 @Component가 붙은 클래스를 찾아서 전부 스프링 빈으로 등록한다. 

 

2) [ 참고 ]

excludeFilters = @ComponentScan.Filter() 는 기존 예제 코드들을 유지하기 위한 작업이다.

AppConfig.class와 여러 테스트 코드 에 @Configuration이 붙어있다. 이것들을 컴포넌트 스캔에서 제외하기 위한 코드를 입력하자. 

@Configuration 를 열어보면 @Component 애노테이션이 붙어있다. 그래서 @Configuration 도 컴포넌트 스캔의 대상이다. 

 

3) 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component를 붙여준다. 

MemoryMemberRepository, RateDiscountPolicy, MemberServiceImpl 에 붙인다. 

4) '컴포넌트 스캔'에서 의존관계 주입은 어떻게 할까?

이전 자바 설정 파일에서는  MemberServiceImpl 구현체에 MemberRepository 의존관계를 생성자를 통해 주입했다.

컴포넌트 스캔에서는 생성자에 @Autowired 를 붙여서 자동으로 의존관계를 주입한다. 

이전에 설정 파일 AppConfig.class 에서는 @Bean 으로 직접 설정 정보를 작성했고 의존관계도 직접 명시했다. 

이제는 설정 정보 파일 자체가 없기 때문에 의존관계 주입도 클래스 안에서 애노테이션으로 해결해야 한다. 

 

OrderServiceImple 클래스에도 @Component를 붙여서 스프링 빈으로 등록하고 @Autowired 로 의존관계를 주입하자. 

5) 컴포넌스 스캔으로 스프링 빈을 등록하고 Autowired로 의존관계가 주입됬는지 테스트 코드를 작성해보자.

* 로그에서 컴포넌트 스캔의 내용을 확인할 수 있다. 

org.springframework.context.annotation.ClassPathBeanDefinitionScanner 
- Identified candidate component class: 
file [/Users../corebasic/discount/RateDiscountPolicy.class]

org.springframework.context.annotation.ClassPathBeanDefinitionScanner 
- Identified candidate component class: 
file [/Users../corebasic/member/MemberServiceImpl.class]

* 로그에서 의존관계 주입을 확인할 수 있다. memoryMemberRepository 이름의 빈이 생성자를 통해 자동주입됨. 

Creating shared instance of singleton bean 'orderServiceImpl'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'memoryMemberRepository'
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'

6) 컴포넌트 스캔과 자동 의존관계 주입이 어떻게 동작하는지 그림으로 알아보자. 

 

@ComponentScan @Component가 붙은 클래스를 찾아서 전부 스프링 빈으로 등록한다. 

스프링 빈의 기본 이름은 클래스명을 쓰는데 앞글자만 소문자로 바꿔서 쓴다. 직접 빈 이름을 지정할 수 도 있다. 

MemberServiceImpl - > memberServiceImpl

생성자에 @Autowired 를 붙이면, 스프링 컨테이너가 자동으로 해당 빈을 찾아서 주입한다. 

기본 조회 전략은 타입이 같은 빈을 찾는 것이다. 

아래 코드를 보면 MemberRepository 빈을 의존관계로 주입해야 하는 상황이다.

MemberRepository와 타입이 같은 것을 찾는다. 

memoryMemberRepository 가 MemberRepository의 구현체니까 타입이 맞다.

그래서 memoryMemberRepository를 꺼내서 주입한다. 

getBean(MemberRepository.class)와 동일하다고 이해하면 된다. 


2. 탐색 위치와 기본 스캔 대상

컴포넌트 스캔을 시작하는 위치와 대상에 대해 알아보자. 

 

1) 탐색할 패키지의 시작 위치 지정 : 탐색 위치를 지정 할 수 있다!

AutoAppConfig.class 파일을 열어보자. 어떤 패키지부터 탐색할 지 basePackages 를 지정할 수 있다. 

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸리기 때문이다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다. 

현재 프로젝트 네비게이션은 아래와 같이 여러개의 패키지로 구성되어 있다. 

member 패키지를 basePackages 로 지정하고 컴포넌트 스캔하면 어떻게 될까? 

basePackages = member 패키지를 포함해서 하위 패키지를 모두 탐색한다.

그래서 memberServiceImpl, memoryMemberRepository 만 스프링 빈으로 등록됬다.

orderServiceImpl같은 다른 패키지의 빈은 제외됬다. 

2) 탐색 위치를 지정하지 않으면 어떻게 될까? 

@ComponentScan 이 붙은 설정 정보 클래스의 위치 부터 하위 패키지를 모두 탐색한다. 

hello.corebasic 패키지 하위의 모든 자바 파일에 @Component가 붙었는지 검사한다. 

3) 권장하는 방법 : @ComponentScan 이 붙은 설정 정보 클래스의 위치를 최상단에 두자 ! 

최근 스프링부트도 이 방법을 기본으로 제공한다. 

 

예를 들어, 패키지가 아래와 같은 구조라면, hello.corebasic 여기가 프로젝트 시작 루트가 된다. (지금 AutoAppConfig.class위치)

프로젝트 시작 루트에 @ComponentScan 를 붙인 메인 설정 정보 클래스를 두고 basePackage 지정은 생략한다.

이렇게 하면 hello.corebasic  를 포함한 하위는 모두 컴포넌트 스캔의 대상이 된다. 

 

[ 참고 ] 스프링 부트를 사용하는 경우

스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 이 프로젝트 시작 루트 위치에 두는 것이 관례이다. 

 

스프링 부트 프로젝트를 만들면 main() 메서드가 있는 프로젝트명으로 된 클래스가 자동 생성되어 있다. 

이 클래스에 이미 @SpringBootApplication 가 붙어있다. 

 @SpringBootApplication  애노테이션이 뭔지 싶어서 ctrl 을 누르고 들어가보면,  @ComponentScan 이 붙어있음을 확인할 수 있다.

그래서 스프링 부트를 실행하면 hello.corebasic 위치 부터 하위까지 다 스프링 빈이 등록된다.

스프링 부트를 쓰면 @ComponentScan 을 따로 달아줄 필요가 없다. 

 


4) 컴포넌트 스캔의 기본 대상

@ComponentScan뿐만 아니라 아래 내용도 컴포넌트 스캔 대상에 포함된다. 

  *  @Component 

  *  @Controller 

  *   @Service

  *   @Repository

  *   @Configuration

스프링 MVC를 다뤄봤다면 몇개는 익숙한 애노테이션일 것이다. 해당 클래스의 소스코드를 열어보면 @Component를 포함하고 있음을 확인할 수 있다. 

 

[ 참고 ] 애노테이션에는 상속관계 라는 것이 없다. 

애노테이션이 특정 애노테이션을 인식할 수 있는 것인 자바 언어가 지원하는게 아니라, 스프링이 지원하는 기능임을 인지하고 넘어가자. 

 

컴포넌트 스캔의 대상이 되면서도 아래의 애노테이션이 있으면 스프링은 부가 기능을 수행한다. 

예를 들어, @Controller 라면, MVC 컨트롤러로 인식한다. @Repository라면 데이터 계층의 예외를 스프링 예외로 변환해준다. 

 


다음 강의에서는 '컴포넌트 스캔의 필터와 빈 이름 중복'에 대해  배운다. 

공부 내용 출처 :  스프링 핵심 원리 기본편 

728x90

예제 프로젝트 목표

1. 회원 도메인, 주문과 할인 도메인을 중심으로 예제를 만든다.
2. 역할과 구현을 나눈다.
3. 실제 요구사항이 변경되었을 때 다형성, OCP, DIP가 지켜지는지 확인한다. 
4. 예제를 만들고 나서 객체지향 원리를 적용해보자.

목차

1. 프로젝트 생성

2. 비즈니스 요구사항과 설계

3. 회원 도메인 설계

4. 회원 도메인 개발

5. 회원 도메인 실행과 테스트

6. 주문과 할인 도메인 설계

7. 주문과 할인 도메인 개발

8. 주문과 할인 도메인 실행과 테스트


1. 프로젝트 생성

1) 사전 준비 : java 11 설치, intelliJ 또는 Eclipse 설치 

2) 스프링부트 스타터 사이트에서 프로젝트 생성 

Project: Gradle Project

Spring Boot: 2.3.x 이상 (2021년 12월 기준, 2.6.1선택함)

Language: Java

Packaging: Jar

Java: 11

 

Project Metadata

groupId: hello, artifactId: core (core-basic으로 생성함)

 

Dependencies: 선택하지 않는다.

지금은 스프링 없는 순수한 자바로만 개발을 진행한다는 점을 꼭 기억하자! 스프링 관련은 한참 뒤에 등장한다.
스프링부트는 프로젝트 생성이 편하니까 이용하는 것이다. 

 

3) IntelliJ Gradle 대신에 자바 직접 실행 

최근 IntelliJ 버전은 Gradle을 통해서 실행 하는 것이 기본 설정이다. 이렇게 하면 실행속도가 느리다.

다음과 같이 변경하면, IntelliJ가 자바를 바로 실행해서 실행속도가 더 빠르다.

Preferences -> Build, Execution, Deployment -> Build Tools -> Gradle 열고 아래 내용으로 선택.

Build and run using:  IntelliJ IDEA
Run tests using:    IntelliJ IDEA

4) Gradle Dependency에서 spring-boot-starter를 보면, 스프링 core 라이브러리를 확인할 수 있다. 


2. 비즈니스 요구사항과 설계

기획자로부터 아래의 요구사항을 듣게 되는 상황이다. 

요구사항은 크게 회원, 주문과 할인 2 가지로 구성된다. 

1) 회원

  • 회원을 가입하고 조회할 수 있다.
  • 회원은 일반과 VIP 두 가지 등급이 있다.
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

2) 주문과 할인 정책

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
  • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. (미확정)

확정되지 않은 부분이 있더라도, 인터페이스를 만들어 두면 구현체는 언제든 갈아 끼울 수 있도록 설계하면 된다!

도메인 설계 부터 시작해보자. 


3. 회원 도메인 설계

1) 회원 도메인 요구사항 

  • 회원을 가입하고 조회할 수 있다. 
  • 회원은 일반과 VIP 두 가지 등급이 있다. 
  • 회원 데이터는 자체 DB를 구축할 수 있고, 외부 시스템과 연동할 수 있다. (미확정)

2) 도메인 협력 관계

기획자도 보는 그림이다. 도메인 협력 관계를 기반으로 클래스 다이어그램을 그린다. 

  • 회원 서비스 : 회원 데이터에 접근할 수 있는 계층을 따로 만든다. 
  • 회원 저장소 : 저장소 인터페이스를 먼저 만든다. 자체 DB를 쓸 지, 외부 시스템 연동을 할지 미정이기 때문이다.
    일단 메모리 저장소를 사용하자.
  • (역할과 구현을 분리한다!)

3) 클래스 다이어그램

실제 구현 레벨로 내려오면 클래스, 인터페이스 명세를 작성한 클래스 다이어그램이 필요하다.

클래스 간의 의존 관계, 연관 관계, 제약 조건 등을 기반으로 개발한다. 백문이 불여일타! 곧 코드로 옮겨보자. 

  • 회원 서비스 인터페이스 : MemberService 
  • 회원 서비스 구현체 : MemberServiceImpl
  • 저장소 인터페이스 :  MemberRepository 
  • 저장소 구현체 : MemoryMemberRepository 

4) 객체 다이어그램 : 런타임에서 객체가 생성됬을 때의 시나리오를 다이어그램으로 표현

저장소가 어떤 것이 생성되는지는 런타임에서 객체가 생성 됬을때 정해진다. 

특정 순간에 객체 간의 관계 및 흐름을 표현한다. 


4. 회원 도메인 개발

1) 회원 엔티티

class Member, enum Grade 

 

2) 회원 저장소

인터페이스: interface MemberRepository

구현체: class MemoryMemberRepository

 

인터페이스와 구현체는 다른 패키지에 두면 좋다. 지금은 작은 예제니까 member 패키지에 함께 뒀다.  

오류처리 같은 예외처리는 제쳐두고 가입, 조회 기능에 집중한다. 

메모리 회원 저장소는 동시성 이슈 때문에 실무에서는 ConcurrentHashMap을 써야 하지만, 간단한 하게 HashMap을 쓴다. 

private static Map<Long, Member> store = new HashMap<>(); // 메모리 저장소

3) 회원 서비스

인터페이스 interface MemberService

구현체 class MemberServiceImpl

public class MemberServiceImpl implements MemberService{

    // 구현 객체를 MemoryMemberRepository 로 선택해주자
    private final MemberRepository memberRepository = new MemoryMemberRepository();

5. 회원 도메인 실행과 테스트

1) JUnit Test Framework를 이용한다.  

main메서드에서 시험하는 것에는 한계가 있기 때문이다. 테스트 코드를 제대로 짜야 좋은 코드가 나온다. 

패키지 레벨을 맞추고 Test파일 만들기

Tip ) 아래와 같은 구조로 테스트 메서드를 작성하면 좋다. 

2) 회원 서비스에서 저장 및 조회 JUnit 테스트 코드 

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

 

3) 주의 ! Assertions 는 assertj.core.api 를 쓰자. 헷갈리지 말기.

import org.assertj.core.api.Assertions;

4) 회원 서비스 구현체를 보면 구조적 문제가 있다? 

의존 관계가 인터페이스 뿐만 아니라 구현까지 모두 의존하는 문제가 있다. 

아래 코드를 보자.

MemberServiceImpl 구현체가 MemberRepository 인터페이스 뿐만 아니라, MemoryMemberRepository 구현체 까지 모두 의존하고 있다. 

-> DIP위반

public class MemberServiceImpl implements MemberService{

    // 구현 객체를 MemoryMemberRepository 로 선택해주자
    private final MemberRepository memberRepository = new MemoryMemberRepository();

6. 주문과 할인 도메인 설계

1) 주문과 할인 정책 요구사항

  • 회원은 상품을 주문할 수 있다.
  • 회원 등급에 따라 할인 정책을 적용할 수 있다.
  • 할인 정책은 모든 VIP는 1000원을 할인해주는 고정 금액 할인을 적용해달라. (나중에 변경 될 수 있다.)
  • 할인 정책은 변경 가능성이 높다. 회사의 기본 할인 정책을 아직 정하지 못했고, 오픈 직전까지 고민을 미루고 싶다. (미확정)

2) 주문 도메인 협력, 역할, 책임 : 기획자와 개발자가 구상하는 설계도 

  • 주문 생성 : 파라미터 3개를 넘겨서 주문. 회원id, 상품명, 상품 가격 -> 주문 서비스 인터페이스
  • 회원 조회 : 할인 받을 수 있는 등급인지 회원id로 저장소에 조회 -> 레포지토리 인터페이스 
  • 할인 적용 : 할인 정책은 회원 등급에 따라 할인을 적용해줌  -> 할인 정책 인터페이스 
  • 주문 결과 반환 : 실무에서는 데이터를 DB에 저장하지만, 예제를 단순화 하기 위해 주문 결과만 반환. -> 주문 서비스 인터페이스

3) 주문 도메인 전체 : 역할과 구현의 분리 

앞서 배운대로 역할은 인터페이스, 구현은 구현클래스다. 

역할과 구현을 분리해서 자유롭게 구현 객체를 조립할 수 있게 설계했다. 

구현 객체가 점선 화살표로 인터페이스를 바라보고 있다. 

회원 저장소는 물론이고 할인 정책도 유연하게 변경할 수 있다. 

4) 클래스 다이어그램 : 정적 정보. 클래스, 인터페이스의 명세 

주문 서비스는 인터페이스와 구현체로 분리되어 있다. 

주문 서비스 구현체는 회원 저장소 인터페이스에 의존한다. 

주문 서비스 구현체는 할인 정책 인터페이스에 의존한다. 

구현체가 인터페이스(역할)에만 의존한다

5) 객체 다이어그램 : 동적 정보. 런타임에서 특정 순간에 객체 간의 관계 및 상황을 표현

회원을 메모리에서 조회하고, 정액 할인 정책을 지원하는 경우. 

역할들의 협력관계를 그대로 재사용 할 수 있다.

즉, 저장소의 구현체가 바뀌어도, 주문 서비스 구현체를 변경할 필요가 없다.

객체 다이어그램1

회원을 DB에서 조회하고, 정률 할인 정책을 지원하는 경우. 

이 경우에도 역할들의 협력관계를 그대로 재사용 할 수 있다.

객체 다이어그램2

이제 개발하자.


7. 주문과 할인 도메인 개발

구현내용 : 주문 생성 요청이 오면, 회원 정보를 조회하고, 할인 정책을 조회 한 다음 주문 객체를 생성해서 반환한다. 

 

주문 생성 요청 받기 -> 주문 서비스 인터페이스에 의존

회원 정보를 조회 -> 멤버 리포지토리 인터페이스에 의존 

할인 정책 조회 -> 할인 정책 인터페이스에 의존 

 

1) 할인 정책 인터페이스 : interface DiscountPolicy

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

2) 고정 할인 정책 구현체 :  class FixDiscountPolicy

3) 주문 : class Order

4) 주문 서비스 인터페이스 : interface OrderService 

Order createOrder(Long memberId, String itemName, int itemPrice);

5) 주문 서비스 구현체 : class OrderServiceImpl


8. 주문과 할인 도메인 실행과 테스트

1) main() 메서드에서 테스트 코드

  JUnit 테스트에 익숙치 않은 사람을 위해 임시로.. 

2) JUnit 테스트 코드 "JUnit 단위 테스트 정말 중요합니다."

public class OrderServiceTest {

    MemberService memberService = new MemberServiceImpl();
    OrderService orderService = new OrderServiceImpl();

    @Test
    void createOrder(){
        Long memberId = 1L; // null 이 들어갈 수도 없어서 wrapper type 썼음
        Member member = new Member(memberId, "itemA", Grade.VIP);
        memberService.join(member);

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

주문 도메인에서 최대한 다형성을 활용하여 인터페이스를 의존하도록 설계했다. 

다음 시간에는 갑자기 악덕 기획자가 나타나서 요구사항을 바꿀 것이다.

할인 정책이 바뀌어도 큰 문제가 없을지 '객체 지향 원리'를 적용해서 해결하자. 


다음 강의에서는 '객체 지향 원리'를 적용하여 예제 프로젝트를 개선한다. 

공부 내용 출처 :  스프링 핵심 원리 기본편 

728x90

+ Recent posts