목표

1. 애노테이션을 직접 만들어본다.
[참고 : 애노테이션을 모아서 사용하는 기능은 자바가 아니라 스프링이 제공하는 기능이다 ]

'의존관계 자동 주입' 목차

1. 다양한 의존관계 주입 방법 

2. 옵션 처리 

3. 생성자 주입을 선택해라!  

4. 롬복과 최신 트랜드

5. 조회 빈이 2개 이상 - 문제

6. @Autowired 필드 명, @Qualifier, @Primary

7. 애노테이션 직접 만들기 (이번 포스팅)

8. 조회한 빈이 모두 필요할 때, List, Map

9. 자동, 수동의 올바른 실무 운영 기준


 

7.  애노테이션 직접 만들기

지난 시간에 @Qualifier에 @Qualifier("mainDiscountPolicy") 이름을 지정해서 사용했다. 

그런데 컴파일러는 @Qualifier("mainnnDiscountPolicy")  라는 실수가 있는지 @Qualifier("nainDiscountPolicy")  인지 모른다.

이런 문자 실수는 컴파일 타임에 잡을 수 없다. 

 

@Qualifier 용도로 쓸 @MainDiscountPolicy 라는 애노테이션을 만들어서 해결하자. 

 

1) 애노테이션 파일 MainDiscountPolicy 를 생성한다. 

2) @Qualifier 에 있던 애노테이션들을 다 복사해서 애노테이션 파일 MainDiscountPolicy 에 붙여넣는다. 

Qualifier 용도의 애노테이션이니까 @Qualifier("mainDiscountPolicy") 도 넣어준다. 

3) rate할인정책에 main할인정책이라는 @Qualifier 애노테이션을 붙이고 싶다. 

4) 혹시 @MainnnDiscountPolicy 로 애노테이션 이름에 에러가 나면? 

바로 컴파일 에러가 난다. 

 

5) RateDiscountPolicy 를 의존관계로 주입하려는 클래스 OrderServiceImpl 코드를 열어보자. 

DiscountPolicy 에 @Qualifier 를 지정하되, 방금 만든 @MainDiscountPolicy를 쓰자. 

6) 로그를 보면 정상적으로 스프링 빈이 등록되고 의존관계도 주입됬음을 확인할 수 있다. 

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'

OrderServiceImpl 싱글톤 빈이 생성됬고,

생성자를 통해 memoryMemberRepository와 rateDiscountPolicy 빈이 의존관계 주입이 됬다.


다음 강의에서는 조회한 빈이 모두 필요할 때 List, Map 으로 처리하는 방법을 배운다. 

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

728x90

목표

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

지난 시간까지 순수 자바 코드로 만든 예제 프로젝트의 문제점을 개선하면서 스프링으로 전환해봤다.

그러면서 왜 스프링이 필요한지, 스프링의 DI 개념에 대해 배웠다. 이번 시간에는 스프링 그 자체에 대해 배운다. 

목표

1.  스프링 컨테이너의 생성 과정을 배운다. 
2. 스프링 빈을 찾는 기본 방법을 배운다. 

'스프링 컨테이너와 스프링 빈' 목차

1. 스프링 컨테이너 생성  (이번 포스팅)

2. 컨테이너에 등록된 모든 빈 조회

3. 스프링 빈 조회 - 기본

4. 스프링 빈 조회 - 동일한  타입이 둘 이상

5. 스프링 빈 조회 - 상속 관계

6. BeanFactory와 ApplicationContext 

7. 다양한 설정 형식 지원 - 자바 코드, XML

8. 스프링 빈 설정 메타 정보 - BeanDefinition


1. 스프링 컨테이너 생성 과정

이전 시간에 AppConfig.class를 넘겨서 생성한 스프링 컨테이너 코드를 떠올려보자. 

컨테이너라는게 '객체를 담고 있다'는 뜻이다. 

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

ApplicationContext는 스프링 컨테이너다. 

ApplicationContext 는 인터페이스인데, 그것을 구현한 클래스가 AnnotationConfigApplicationContext다. 

스프링 컨테이너를 생성하는 방법에는 XML 기반과 애노테이션 기반이 있다.

우리가 AppConfig.class 에 @Configuration 애노테이션을 달고, 메서드에 @Bean을 달았는데. 이 방법이 애노테이션 기반 자바 설정 클래스로 스프링 컨테이너를 생성한 것이다.

 

참고 : 더 정확히는 스프링 컨테이너를 부를 때 BeanFactory와 ApplicationContext를 구분한다. 뒤에서 더 자세히 배운다. 

 

1) 스프링 컨테이너 생성 과정 

스프링 컨테이너를 생성 할 때 구성 정보(설정 정보)를 정해줘야 한다. 여기서는 AppConfig.class 를 구성 정보로 지정했다. 

2) 스프링 빈 등록

스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 @Bean이 붙은 메서드의 반환 객체를 모두 스프링 빈으로 등록한다.

 

{ @Bean 이름 - @Bean 객체 } 쌍으로 컨테이너에 저장된다. 

아래에 코드를 보자. memberService() 메서드에 @Bean 애노테이션이 붙어있다. 

memberService 이름의 스프링빈은 아래의 쌍으로 저장된다. 

{ @Bean이름: memberService  - @Bean객체: new MemberServiceImpl(memberRepository())  }

빈 이름은 디폴트로 메서드 이름과 똑같이 정해지는데 직접 이름을 지을 수도 있다. 

주의: 빈 이름은 중복 불가

 

3) 스프링 빈 의존관계 설정 - 준비 

애노테이션 기반 자바 설정 클래스(AppConfig.class)를 기반으로 스프링 컨테이너를 생성한다. 

@Bean 을 달아놓은 메서드를 전부 호출하여 메서드 명 그대로 이름붙여서 컨테이너에 스프링 Bean으로 등록한다. 

4) 스프링 빈 의존관계 설정 - 완료 

스프링 컨테이너가 설정 정보를 참고해서 의존관계를 주입(DI) 한다. 

어떤 인터페이스에 어떤 구현체를 생성해서 인스턴스 레퍼런스를 넘길지 정보를 보고 의존관계를 주입한다. 

단순히 자바 코드를 호출하는 것 같아보이지만, 차이가 있다. 이 차이는 뒤에서 싱글톤 컨테이너에서 설명한다. 

[ 참고 ]

스프링 컨테이너는 빈을 생성하고 의존관계를 주입한다는 것이 핵심이다. 

스프링 빈을 생성하고 의존관계를 주입하는 단계를 나눠서 그림으로 그렸다. 사실 스프링에서는 이것이 한 번에 처리되는 것이고 이해를 위해 나눠 그린 것이다. 이제 스프링 컨테이너에서 데이터를 조회해보자. 


2. 컨테이너에 등록된 모든 빈 조회

테스트 코드를 작성해서 스프링 컨테이너에 스프링 빈이 등록되었는지 확인하자

테스트를 실행한 결과. 

파랗게 드래그한 부분은 appConfig.class를 포함해서 @Bean 을 달아놓은 스프링빈이 출력되었다. 

드래그한 윗 부분은 스프링이 내부적으로 스프링 자체를 확장하기 위해 필요한 스프링빈이다. 

스프링 내부적으로 필요한 것 말고, 내가 정의한 스프링 빈만 출력하자.  
getRole()== ROLE_APPLICATION : 개발을 위해 등록한 (일반적으로 사용자가 등록한) 빈만 출력된다. 

파랗게 드래그한 부분을 보면 appConfig.class를 포함해서 내가 정의한 빈 4개가 출력된다. 


3. 스프링 빈 조회 - 기본

1) getBean() 메서드로 빈 이름을 넘기면 스프링 빈을 조회할 수 있다. 

테스트 실행 결과, appConfig를 비롯한 스프링빈이 출력된다. 

2) 이름 없이 타입으로만 조회할 수 있다. 

memberService 빈 이름을 호출하지 않고, memberService.class 타입으로 빈을 조회할 수 있다. 

3) 구체 타입 으로 조회할 수 있다. 

memberService 빈을 호출하면 구체클래스 memberServiceImpl을 반환해준다. 

AppConfig.class 코드를 열어 보면 memberService 스프링 빈의 반환 타입을 확인할 수 있다. 

따라서 스프링 빈을 memberServiceImpl 구체 타입으로 조회 가능하다. 

좋은 코드는 아니다.

왜냐하면 프로그래머는 "추상화에 의존해야지, 구현체에 의존하면 안된다." 는 SOLID원칙을 다시 떠올리자! 

 

4) 존재하지 않는 빈을 조회해보자. 

테스트 작성 시, 항상 실패 케이스도 만들어야 한다. XXX 라는 이름의 빈을 조회하면, 없는 빈이니까. 아래의 예외가 터져야 한다. 

NoSuchBeanDefinitionException: No bean named 'XXX' available

XXX라는 빈 없다는 에러 내용을 확인할 수 있다.

빨간 글씨를 보긴 했지만 예쁘게 고쳐보자. 

5) 실패 케이스 "@@예외가 터지면 성공이다."라는 테스트를 작성하자. 

다음 시간에는 "동일한 타입의 빈이 2개 이상 있으면 어떻게 조회 하는지 " 알아보자. 


다음 강의에서는 '스프링 빈 조회와 BeanFactory'를 배운다. 

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

728x90

목표

1.  예제 프로젝트에 객체 지향 원리를 적용한다.
2. IoC, DI, 그리고 컨테이너를 배우고 순수 자바코드만 사용했던 프로젝트를 이제 스프링으로 전환한다. 

'객체 지향 원리 적용' 목차

1. 새로운 할인 정책 개발

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

3. 관심사의 분리

4. AppConfig 리팩터링 

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

6. 전체 흐름 정리 

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

8. IoC, DI, 그리고 컨테이너 (이번 포스팅)

9. 스프링으로 전환하기

 


8. IoC, DI, 그리고 컨테이너 

제어의 역전 IoC (Inversion of Control)

 

1) 기존 프로그램 : 구현 객체가 스스로 제어 흐름을 조종

기존 프로그램은 구현 객체가 스스로 필요한 객체를 생성하고, 연결하고, 실행했다. 

 

2) AppConfig가 외부에서 프로그램 동작 방식을 제어

반면에 AppConfig를 도입한 후에는 구현 객체는 자신의 로직을 실행하는 역할만 담당했다. 

프로그램을 제어하는건 전부 AppConfig가 맡는다. 어떤 인터페이스에 어떤 구현체를 선택할지 정해준다. 

예를 들어, OrderServiceImpl은 필요한 인터페이스를 호출할 때 어떤 구현 객체가 실행될지는 모른다. 몰라도 자신의 로직 실행에 아무런 영향을 받지 않는다. 

 

이렇게 프로그램의 제어 흐름을 구현체들이 직접 제어하는게 아니라 외부에서 관리하는 것을 제어의 역전이라고 한다. 

 

프레임워크 vs 라이브러리

프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit) 

JUnit 테스트 프레임워크는 자신만의 라이프사이클이 있다. 순서대로 테스트를 진행하되, 내가 작성한 테스트를 그 안에 집어넣어서 실행시켜주는 것이다. 

반면 내가 작성한 코드가 직접 제어의 흐름을 담당한다면, 그것은 라이브러리다.

 

의존관계 주입 DI(Dependency Injection)

구현체는 인터페이스에 의존한다. OrderServiceImpl은 DiscountPolicy 인터페이스에만 의존한다. 

OrderServiceImpl 구현체는 기능을 호출하면서도 인터페이스를 통해 실제 어떤 구현체가 사용될지는 모른다. 

의존관계는 정적인 클래스 의존관계와 실행 시점에 결정되는 동적인 객체(인스턴스)의존관계 2가지를 분리해서 생각해야 한다. 

 

정적인 클래스 의존관계

애플리케이션을 실행하지 않아도, 클래스가 사용하는 import 코드만 보고 의존관계를 분석할 수 있다. 

FixDiscountPolicy 와 RateDiscountPolicy 클래스는 DiscountPolicy 인터페이스에 의존한다. 

OrderServiceImpl 구현체는 MemberRepository 인터페이스와 DiscountPolicy 인터페이스에 의존한다. 

화살표 방향으로 -> 의존관계를 표현하고 있다.

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

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

외부에서 실제 구현체를 생성하고 연결해주는 것을 의존관계 주입이라 한다. (AppConfig가 해준다)

 

의존관계 주입의 효과 

클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 인스턴스를 변경할 수 있다. 외부에서 구현체를 정해주기 때문이다. 

정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스를 쉽게 변경할 수 있다. (중요하니까 반복한다)

IoC컨테이너, DI 컨테이너 

AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해주는 것을 IoC컨테이너 또는 DI컨테이너라 한다. 

 


9. 스프링으로 전환하기 

지금까지 순수 자바코드만으로 DI를 적용했다. 이제 스프링으로 전환해보자. 코드 부터 작성하자. 스프링으로 전환한 코드 private repo

 

1) 스프링 컨테이너에 등록하기

AppConfig에 '구성 정보'를 의미하는 @Configuration 애노테이션을 붙인다.

DI 하는 메서드에 @Bean애노테이션을 붙인다. 

2) ApplicationContext에 Bean을 등록 

테스트를 위해 작성했던 MemberApp를 열어보자. 

기존에는 AppConfig 에서 직접 필요한 객체를 꺼냈었다. 이제 스프링을 쓰자. 

스프링은 ApplicationContext 로 시작한다. 이것이 객체(Bean)을 관리해주는 스프링 컨테이너다. 

AppConfig.class 를 파라미터로 넘겨서 ApplicationContext를 생성하자. 

AppConfig에 있는 설정 정보를 가지고 Bean을 스프링 컨테이너에 넣고 관리해준다. 

객체(Bean)의 이름은 각각의 메서드 이름으로 붙여진다. ex) memberService 빈, memberRepository 빈

 

memberService 객체가 필요할 때 AppConfig에서 꺼내지 않고, 스프링 ApplicationContext에서 꺼낸다. 

스프링 컨테이너에 빈으로 등록된 인스턴스가 로그에 뜬다! 

3) OrderApp도 스프링으로 바꿔보자. 

AppConfig.class 를 파라미터로 넘겨서 ApplicationContext를 생성하자. 

ApplicationContext 에서 memberService와 orderService를 꺼낸다. 

4) 스프링 컨테이너 (== DI 컨테이너) 

  • ApplicationContext를 스프링 컨테이너라 한다. 
  • 기존에는 개발자가 AppConfig 에 객체를 생성하고 직접 DI를 구현했다. 
  • 이제 스프링 컨테이너가  @Configuration 애노테이션이 붙은 AppConfig를 설정(구성)정보로 사용한다. 여기서 @Bean이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 모두 등록한다. 이렇게 스프링 컨테이너에 등록된 메서드를 스프링 빈이라고 한다. 
  • 스프링 빈은  @Bean애노테이션이 붙은 메서드 이름을 스프링 빈의 이름으로 사용한다. 
  • 스프링 빈은 applicationContext.getBean() 메서드로 찾을 수 있다. 

 

코드가 더 복잡해진 것 같은데. 스프링 컨테이너를 사용하면 어떤 장점이 있을까 ? 다음 시간에 장점을 배우자.


다음 강의에서는 '스프링 컨테이너 생성 과정'을 배운다. 

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

728x90

목표

1.  예제 프로젝트에 객체 지향 원리를 적용한다.
2. 이전 시간에 만든 AppConfig를 리팩터링하고 공부한 내용을 정리한다.

'객체 지향 원리 적용' 목차

1. 새로운 할인 정책 개발

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

3. 관심사의 분리

4. AppConfig 리팩터링 (이번 포스팅)

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

6. 전체 흐름 정리 

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

8. IoC, DI, 그리고 컨테이너 

9. 스프링으로 전환하기


4. AppConfig 리팩터링

현재 AppConfig의 중복을 제거하자. 역할에 따른 구현을 분명하게 만들자. 

아래는 목표로 하는 도메인 협력관계다. AppConfig 에서 이 그림의 모든 요소를 드러나도록 바꾸자. 

리팩토링 전 AppConfig 

리팩토링 후 AppConfig 코드

메서드 명을 보면 역할 마다의 구현이 모두 드러나있다. 애플리케이션의 구성이 한 눈에 들어온다. 

AppConfig를 도입해서 애플리케이션이 사용 영역과 객체를 생성하고 구성하는 영역으로 분리됬다. 

 


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

기획자의 요구사항대로 정률% 할인 정책으로 변경하자. 

FixDiscountPolicy를 RateDiscountPolicy로 구현체가 변경되어야 하는데. 어디를 바꿔야 할까? 

 

클라이언트를 건드리지 않고 AppConfig만 수정하면 된다. 

DiscountPolicy 역할(인터페이스)의 구현체를 RateDiscountPolicy로 바꾸면 끝이다!

공연기획자 AppConfig 가 모든 역할과 구현을 선택한다. 

사용 영역의 변경이 필요없다!


6. 전체 흐름 정리 

지금까지 배운 것을 정리해보자. 

 

1. 새로운 할인 정책 개발

할인 정책 인터페이스와 할인 정책 구현체를 분리해뒀다.

역할과 구현이 분리되어 있으니 새로운 정률 할인 정책 코드를 추가 개발하는 것에 문제가 없다.

 

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

새로 개발한 정률 할인 정책을 적용하려고 하니까 클라이언트 코드를 변경해야 하는 문제를 발견했다. 

주문 서비스가 할인 정책 인터페이스 DiscountPolicy 뿐만 아니라 구현체 FixDiscountPolicy도 둘다 의존하고 있었기 때문이다. 

-> DIP 위반! 추상에만 의존해야 하는데. 구현체에도 의존하고 있는 문제.

 

3. 관심사의 분리

기존에는 구현체 다른 구현체를 직접 선택하는 다양한 책임을 가지고 있었다. 

구현체는 기능 구현에만 집중하게 두자.

역할 마다의 구현체를 지정하는 책임을 맡을 공연 기획자가 필요해졌다!

공연 기획자 AppConfig를 만들어서 전체 동작 방식을 구성하도록 했다. 

AppConfig는 구현 객체를 생성하고 연결하는 책임을 가진다. 

클라이언트는 기능 구현이라는 책임이 명확해졌다. 

 

4. AppConfig 리팩터링

중복을 제거했다. 구성 정보에서 역할과 구현을 명확하게 분리했다. 

 

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

새로 개발한 정률 할인 정책을 적용했다. 구성영역인 AppConfig만 수정하면 됬고 사용영역은 변경할 필요가 없었다. 


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

예제 프로젝트에서 SOLID원칙이 적용된 것을 확인해보자. 여기서 SRP, DIP, OCP가 적용되었다. 

 

SRP 단일 책임 원칙 

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

  • 클라이언트 객체는 객체 생성, 연결, 실행하는 다양한 책임을 가지고 있었다. 
  • 구현 객체를 생성하고 연결하는 책임을 AppConfig가 담당하도록 넘겼다. 클라이언트 객체는 기능 실행만 맡게 됬다. 

DIP 의존관계 역전 원칙  

"추상화에 의존해야지, 구체화에 의존하면 안된다."

  • 새로운 할인 정책을 개발하고 적용하려고 하니 클라이언트 코드를 변경해야 하는 문제를 발견했다. 
  • 왜냐하면 클라이언트 코드 OrderServiceImpl이 DiscountPolicy 인터페이스에만 의존하는 것 같았지만, FixDiscountPolicy 구현체에도 의존하고 있었기 때문이다. 
  • 클라이언트 코드 OrderServiceImpl이 DiscountPolicy 인터페이스에만 의존하도록 변경했다. 
  • AppConfig가 FixDiscountPolicy 인스턴스 생성하여 의존관계를 클라이언트 코드에 주입했다. 

OCP 개방 폐쇄 원칙

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

  • 구성영역인 AppConfig가 의존관계를 결정한다. 
  • 따라서 소프트웨어 요소를 새롭게 확장해도 사용영역(클라이언트 코드)을 변경하지 않아도 된다. 

다음 강의에서는 스프링의 IoC, DI 그리고 컨테이너를 배운다. 

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

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

김영한님의 스프링 핵심 원리 기본편 강의를 수강하고, 복습하기 위해 포스팅한다. 

 

전체 목차

1. 객체 지향 설계와 스프링
2. 스프링 핵심 원리 이해1 - 예제 만들기
3. 스프링 핵심 원리 이해2 - 객체 지향 원리 적용
4. 스프링 컨테이너와 스프링 빈
5. 싱글톤 컨테이너
6. 컴포넌트 스캔
7. 의존관계 자동 주입
8. 빈 생명주기 콜백
9. 빈 스코프

섹션 9개로 구성되어 있고, 섹션마다 하위 강의 6~10개가 들어있다. 

 

강의 목표

  • 스프링 핵심 원리, 기능, 실무 적용 예시를 학습
  • 스프링 본질에 대한 깊은 이해 
  • 객체 지향 설계를 고민하는 개발자로 성장

강의 대상 

  • 스프링을 처음 접하는 개발자 

김영한님의 추천 도서 

  • 객체지향 책 : 객체지향의 사실과 오해 
  • 토비의 스프링 : 무조건 사야 한다. 초심자에게는 어렵지만, 스프링을 한 번 뗀 다음에 펴보자. 
  • JPA 책 : 자바 ORM 표준 JPA 프로그래밍 

학습 기록

1. 객체 지향 설계와 스프링 - 역할과 구현을 분리하기 

2. 객체 지향 설계와 스프링 - 좋은 객체 지향 설계의 5가지 원칙

3. 예제 프로젝트 만들기 

4. 객체 지향 원리 적용 - 관심사의 분리 

5. 객체 지향 원리 적용 - AppConfig 리팩터링 

6. 객체 지향 원리 적용 - IoC, DI, 그리고 컨테이너 

7. 스프링 컨테이너 생성 과정

8. 스프링 빈 조회 - 상속관계
9. 스프링 컨테이너의 다양한 설정 형식 - 자바코드, XML 

10. 싱글톤 컨테이너와 싱글톤 방식의 주의점

11. @Configuration과 싱글톤

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

13. 컴포넌트 스캔의 필터와 빈 이름 중복

14. 다양한 의존관계 주입 방법

15. 생성자 주입을 선택해야하는 이유와 롬복

16. 조회할 빈이 2개 이상일 때의 문제 해결하기

17. 애노테이션 직접 만들기

18. 조회한 빈이 모두 필요할 때 List, Map에 담기

19. 빈 생명주기 콜백

20. 스프링 빈 생명주기 콜백 지원 방법 3가지 

21. 빈 스코프의 프로토타입 

 


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

728x90

+ Recent posts