목표

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. 스프링이 싱글톤을 보장하기 위해 @Configuration 애노테이션을 사용하는 이유를 배운다. 

'싱글톤 컨테이너' 목차

1. 웹 애플리케이션과 싱글톤

2. 싱글톤 패턴 

3. 싱글톤 컨테이너 

4. 싱글톤 방식의 주의점 

5. @Configuration과 싱글톤  (이번 포스팅)

6. @Configuration과 바이트코드 조작의 마법 


5. @Configuration과 싱글톤

아래는 스프링 컨테이너를 만들 때 쓰는 애노테이션 기반 자바 설정 정보 AppConfig.class 클래스 파일이다. 

이상한게 하나 있다. 정말 싱글톤을 보장하고 있을까? @Bean 을 붙여서 빈을 생성하는 코드를 차례로 보자. 

1) memberService 빈을 생성하는 코드를 보자.

-> MemberServiceImpl 을 생성하자 -> memberRepository()를 호출하게 된다.  

memberRepository() 빈을 만드려고 보니 MemoryMemberRepository() 호출하게 된다. 

정리하면,

@Bean memberService 빈 생성 -> memberRepository() 호출 -> MemoryMemberRepository() 호출

 

2) orderService 빈을 생성하는 코드를 보자.

-> orderServiceImpl 생성하려고 보니 memberRepository() 와 discountPolicy() 를 호출한다. 

memberRepository()를 호출하면  MemoryMemberRepository() 호출된다. 

? 이상한데. MemoryMemberRepository()가 2번 호출되서 객체가 2개 생성된 것 같다! 

 

정리하면,

@Bean orderService 빈 생성 -> memberRepository() 호출 -> MemoryMemberRepository() 호출

 

3) 싱글톤이 보장되는지 객체를 출력해서 동일한지 테스트 코드 configurationTest() 를 작성해보자 

MemberServiceImpl 에 생성된 memberRepository가  orderServiceImpl 에 생성된 memberRepository와 같은 객체일까? 

구현체 내부에 생성된 memberRepository 객체를 비교했다. 테스트 결과, 같은 인스턴스가 공유되어 사용됨을 확인했다. 

memberRepository() 생성 호출이 안 되고 있는건 아닐까? 

 

4) AppConfig.class에 println()출력을 찍어두고 테스트 코드 configurationTest() 를 실행해보자. 

memberRepository 가 생성되는 지점이 어디일까?

AppConfig.class 자바 코드를 보면,  "call AppConfig.memberRepository" 가 3번 출력될 것이다. 

  * memberService 빈 생성할 때 memberRepository가 1번 호출된다.  

  * MemberServiceImple 을 생성할 때 1번 호출된다. 

  * orderService 빈 생성할 때 1번 호출되기 때문에 총 3번 출력 될 것이다. 

테스트를 실행해보니 반전 결과다. 

"call AppConfig.memberRepository" 가 딱 1번만 출력된다. 그러니까 각 스프링빈이 1번만 생성된다. 싱글톤이 보장되고 있었다.

왜그럴까 ? 다음 강의에서 알아보자. 


6. @Configuration과 바이트코드 조작의 마법 

스프링 컨테이너는 싱글톤 레지스트리다. 그러니까 스프링 빈이 싱글톤이 되도록 보장해야 한다. 자바 코드만 봤을 때 분명 3번 호출되어야 맞는데. 어떻게 memberRepository 객체를 1번만 생성한걸까?

그 이유는 스프링은 클래스의 바이트코드 조작 라이브러리를 사용하기 때문이다. 

 

1) AppConfig.class 가 순수한 클래스로 만든 스프링 빈으로 등록된다? 

AppConfig 도 스프링 빈으로 등록되는데. 생성된 AppConfig 빈의 타입을 getClass() 메서드로 출력해보자. 

순수한 스프링 빈이라면, 내가 만든 AppConfig 클래스는 hello.core.AppConfig 라고 출력되어야 하는데. 

$$EnhancerBySpringCGLIB 라는 이름이 붙어있다. 

내가 만든 클래스가 아닌걸까? 이유를 알아보자. 

 

2) 스프링이 만든 AppConfig@BySpringCGLIB 클래스가 스프링 빈으로 등록된다!

 

스프링은 @Configuration이 붙은 클래스를 읽으면, 그 클래스를 그대로 스프링 클래스로 만들지 않는다.

바이트코드 조작 라이브러리를 사용해서 AppConfig.class 클래스를 상속받은 임의의 다른 클래스를 만든다. 그리고 그 클래스를 스프링 빈으로 등록한다. 

클래스 이름에는 BySpringCGLIB 이런 이름이 붙는다. 

스프링이 만든 임의의 클래스가 스프링 빈으로 등록된다!

그림은 AppConfig.class를 상속받아서 AppConfig@BySpringCGLIB라는 다른 클래스를 만든 것을 표현했다. 

 

3) AppConfig@CGLIB 클래스가 어떤 역할을 할까? 

 AppConfig@CGLIB 클래스가 스프링 빈의 싱글톤을 보장한다.

(CGLIB 내부 기술을 사용하는데 매우 복잡하다.)

 

아마도 다음과 같은 로직으로 AppConfig@CGLIB 클래스가 싱글톤을 보장할 것이다. 

1.  AppConfig.class를 쭈욱 읽으면서 @Bean이 붙은 메서드마다 빈을 생성한다

2. 이미 스프링 빈이 존재하면, 스프링 컨테이너에 존재하는 빈을 반환한다. 

3. 컨테이너에 스프링 빈이 없다면, AppConfig.class 의 기존 로직을 읽어서 스프링 빈을 등록하고 반환한다. 

 

[ 참고 ] 

내가 만든 AppConfig의 타입을 조회했는데. 왜 AppConfig@CGLIB 클래스 타입이 출력되었을까? 

AppConfig@CGLIB 클래스는 AppConfig를 상속받은 자식클래스이기 때문이다. 

빈 조회를 하면서 배운 내용을 떠올려보자. 부모 타입을 조회하면, 자식 타입도 전부 딸려서 조회된다. 

 

4) 만약, @Configuration 을 적용하지 않고, @Bean 만 붙여도 문제가 없을까? 

애노테이션을 주석처리 하고, 아까 작성한 configurationDeep() 테스트 코드를 실행하여 스프링 빈을 조회해보자.

에러가 나지는 않는다. 하지만 memberRepository 가 3번 호출되어 3개의 객체가 생성된다. 

memberRepository 처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤이 보장되지 않는다. 

5) 정리 

고민할 게 없다. @Configuration 애노테이션이 스프링 빈의 싱글톤을 보장해준다는 점이 핵심이다. 


다음 강의에서는 설정 정보 없이 자동으로 스프링빈을 등록하는 '컴포넌트 스캔'을 배운다. 

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

728x90

목표

1. 스프링 컨테이너가 객체 인스턴스를 싱글톤으로 관리하는 이유를 배운다.
2. 싱글톤 패턴의 문제점과 주의점을 배운다.

'싱글톤 컨테이너' 목차

1. 웹 애플리케이션과 싱글톤  (이번 포스팅)

2. 싱글톤 패턴 

3. 싱글톤 컨테이너 

4. [중요] 싱글톤 방식의 주의점  

5. @Configuration과 싱글톤

6. @Configuration과 바이트코드 조작의 마법 


1. 웹 애플리케이션과 싱글톤  

스프링은 태생이 온라인 서비스 기술을 지원하기 위해 탄생했다. 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 

웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 

고객 요청이 올 때마다 AppConfig가 객체를 새로 만들면? 요청이 올 때마다 인스턴스가 생성되는데.

1000개의 요청이 서버에 들어오면?? 1000개의 인스턴스가 메모리를 차지하게 된다. -> 메모리 낭비! 

해결 방안

객체를 딱 1개만 생성되도록 보장하고 이것을 공유하도록 설계하는 것이다. 

 


2. 싱글톤 패턴

1) 싱글톤 패턴으로 이 문제를 해결해보자.

싱글톤 패턴은 JVM 안에 클래스의 인스턴스가 딱 1개만 생성되는 방식이다. 

인스턴스를 2개 이상 생성하지 못하도록 막아야 한다! 코드로 확인해보자. 

핵심은 private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막는 것이다.

 

아래는 싱글톤 패턴 구현 코드다. 

public class SingletonService {

    // 1. 자기 자신을 내부에 private 으로 선언. final 이니까 딱 1번만 생성하고, 2번 생성 불가하게 만든다.
    private static final SingletonService instance = new SingletonService();

    // 2. 인스턴스 조회는 public 으로 열어둠
    public static SingletonService getInstance(){
        return instance;
    }

    // 3. 생성자를 private 으로 생성. 외부에서 new 키워드로 객체 생성 불가하도록 막음
    private SingletonService(){ 
    }

    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

2) 싱글톤을 어떻게 구현할까? : 객체를 미리 생성하는 안전하고 단순한 방법으로 구현해보자. 

 

1. static 영역에 객체 instance를 미리 1개 생성해서 올려둔다.

    자바가 시작할 때 객체를 하나 생성해서 정적 (클래스) 영역에 상수처럼 가지고 있는 것이다. 이미 1개 생성 끝.

2. 인스턴스 조회할 수 있는 메서드를 public으로 열어뒀다.

    호출할 때 마다 최초에 1개 만들어둔 같은 객체 인스턴스가 반환된다. 

3. 인스턴스 생성은 private 으로 막아뒀다. 외부에서 new 키워드로 객체 생성 불가하게 만들었다. 

    new 키워드로 생성하려는 순간 컴파일 오류남! 세상에서 제일 좋은 오류 컴파일 오류. 

 

3) 이제 AppConfig를 전부 싱글톤 패턴으로 바꾸면 되겠네요?

-> 스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리한다. 이미 만들어진 객체를 공유한다. 

 

4) 싱글톤 패턴의 여러 문제점 

객체 마다 싱글톤 패턴 구현하는 코드 자체가 많이 들어간다. 객체 선언, 생성자 등등 구현 코드 필요 

의존관계상 클라이언트가 구체 클래스에 의존한다 -> DIP를 위반한다.

테스트하기 어렵다.

내부 속성을 변경하거나 초기화하기 어렵다.

private 생성자를 쓰니까, 자식 클래스를 만들기 어렵다. 

결론적으로 유연성이 떨어져서 안티패턴으로 불리기도한다. 


3. 싱글톤 컨테이너 

스프링 컨테이너는 싱글톤 패턴의 문제점은 싹 해결하고, 객체 인스턴스를 싱글톤으로 관리한다. 

 

1) 싱글톤 컨테이너 

스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다. 

이렇게 싱글톤 객체를 생성하고 관리하는 컨테이너를 싱글톤 레지스트리라 한다. 

싱글톤 패턴을 위한 지저분한 코드를 안 넣어도 된다. 

private생성자, DIP 위반 등의 단점을 해결하고 싱글톤을 사용할 수 있게 된다. 

 

2) 싱글톤 객체를 사용하는지 코드로 확인하자. 

MemberService.class 타입 빈을 2개 꺼내자.  참조값이 똑같은지 확인하자. 참조값이 똑같다!

3) 스프링 컨테이너 덕분에 여러개의 요청이 오더라도 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다. 

스프링 컨테이너는 객체를 싱글톤으로 관리한다

스프링 컨테이너는 기본 빈 등록 방식이 싱글톤이다. 

하지만 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공하긴 한다. 하지만 99% 경우가 싱글톤을 쓴다.

자세한 내용은 뒤에 빈 스코프에서 배운다. 

 

4) 스프링 컨테이너의 싱글톤 패턴 확인하는 테스트 코드


[중요] 4. 싱글톤 방식의 주의점

실무에서 아래의 주의점을 안 지켜서 큰 장애가 발생하는 경우가 있다. 

스프링 컨테이너든, 싱글톤 패턴을 구현해서 사용하든
싱글톤 방식을 쓸 때 객체를 무상태로 설계해야 한다. 

 

1) 무상태(stateless)로 설계해야 한다는 의미가 뭘까? 

* 특정 클라이언트에 의존적인 필드가 있으면 안 된다. 

* 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다. 

* 가급적 읽기만 가능해야 한다. 

* 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다. 

 

여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 상태유지(stateful)하게 설계하면 안 된다. 

 

2) 실무에서 자주 발생하는 문제 코드를 알아보자 

StatefulsService 의 order() 메서드 : 사용자가 주문하면 금액을 저장한다.

A사용자는 10000원, B사용자는 20000원 어치를 주문한다고 가정한다.  

사용자 A의 주문금액이 20000원으로 나온다.

StatefulService는 싱글톤 빈이니까. statefulService1 과 statefulService2 는 같은 객체다.

하나의 price 변수에 ThreadA와 ThreadB가 접근해서 값을 변경한 것이다. 

이러면 서비스 망하는거에요. 몇 년에 한 번씩 실무에서 꼭 만나는 상태유지 문제!! 이런 건 잡기도 어렵습니다.

원래는 실무를 반영하려면 멀티스레드 넣고 상속 관계 넣고 하는 복잡한 예제가 더 적절한데, 간단한 예제를 든 것이다. 

결론: 공유 필드는 조심하면서 무상태로 만들자!

 

3) 무상태 (Stateless)로 바꿔보자! 공유되지 않는 지역변수를 쓰자. 

공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

금액을 리턴값으로 넘기자. 


다음 강의에서는 '@Configuration과 싱글톤'을 배운다. 

프로젝트 레포지토리

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

728x90

목표

1. 스프링 컨테이너가 지원하는 설정 형식을 배운다. - 자바코드 와 XML
2. 스프링 빈 설정 메타 정보를 배운다. 

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

1. 스프링 컨테이너 생성  

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

3. 스프링 빈 조회 - 기본

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

5. [ 중요 ] 스프링 빈 조회 - 상속 관계

6. BeanFactory와 ApplicationContext 

7. 다양한 설정 형식 지원 - 자바 코드, XML (이번 포스팅)

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


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

스프링 컨테이너는 다양한 설정 형식을 지원한다. 

자바 코드, XML, Groovy 등등.. 

애노테이션 기반 자바 코드로 설정

지금까지 애노테이션 기반 자바 코드로 설정(팩토리 빈으로 설정)하는 법을 배웠다.

AppConfig라는 팩토리 빈에서 @Bean 애노테이션이 달린 팩토리 메서드를 통해 스프링 빈을 스프링 컨테이너에 등록하는 방식이다. 

AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

XML로 설정

이번 시간에는 XML로 설정 해보자. 최근에는 스프링 부트를 많이 사용하면서 이 방식은 드물게 쓰고 있고, 레거시 프로젝트들에서 주로 사용한다. XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있다는 장점이 있다.

 

1) resource 디렉토리에 appConfig.xml 을 작성한다. 

스프링 빈의 id, class, constructor-arg 등 으로 구성되어 있다. 이 때, class의 전체 경로를 적어줘야 한다.

내용은 자바 코드와 똑같은데 형식이 약간 다를 뿐이다. 

xml 기반 방식은 직접 스프링 빈을 스프링 컨테이너에 등록하는 방식이다. 

2) GenericXmlApplicationContext 에 appConfig.xml 설정 정보를 넘겨서 스프링 컨테이너를 생성한다. 

테스트 코드를 작성해서 memberService 빈을 조회해보자. 

xml 기반으로 설정하는 것은 최근에 잘 안쓰니까 이정도로 마무리하고. 필요하면 레퍼런스 문서를 확인하자. 


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

조금 깊이 있는 내용으로 가보자.  

1) 스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까? 그 중심에는 BeanDefinition  이라는 추상화가 있다. 

  • 즉, 역할과 구현을 개념적으로 나눈 것이다. 
    • XML 을 읽어서 BeanDefinition을 만들면 된다. 
    • 자바 코드를 읽어서 BeanDefinition을 만들면 된다.
    • 스프링 컨테이너는 자바 코드인지 XML 인지(구현은) 몰라도 된다. 오직 BeanDefinition만(역할만) 알면 된다. 

역할과 구현을 개념적으로 나눈 것

  • BeanDefinition 을 빈 설정 메타정보라 한다. 
  • 이 메타정보를 기반으로 인스턴스를 생성한다. 
  • BeanDefinition 자체가 인터페이스다. 스프링 컨테이너는 인터페이스만 바라본다.
    • @Bean , <bean> 당 각각 하나씩 메타정보가 생성된다. 이 메타정보를 기반으로 스프링 빈을 생성한다. 

2) 코드 레벨로 들어가보자. 

아유 잘 모르겠다 싶으면 몰라도 된다. 개발에 아~무 지장없다. 

설정 형식 마다 Reader가 있다! 

  • ApplicationContext 인터페이스에는 설정 파일을 읽기 위한 여러 구현체들이 있다. 
  • 자바 코드 설정을 읽을 Reader == AnnotatedBeanDefinitionReader 가 있다. 얘가 AppConfig.class 파일을 읽어서 빈 메타정보를 생성한다. 
  • xml 형식의 설정을 읽을 XmlBeanDefinitionReader 가 있다. 얘가 appConfig.xml을 읽어서 빈 메타정보를 생성한다. 
  • 새로운 형식의 설정 정보가 추가되면, 또는 개발자가 새 형식의 설정 정보를 추가하면, XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성할 수 있다. 

[ 정리 ] 

스프링이 다양한 형태의 설정 정보를 BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다. 

스프링 관련 오픈 소스를 보다가 BeanDefinition 이 보일 것이다. 이 때 이러한 매커니즘을 떠올리면 된다. 


다음 강의에서는 객체가 JVM안에 딱 하나만 있어야되는 '싱글톤 컨테이너와 싱글톤 방식의 주의점'를 배운다. 

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

728x90

목표

1. 스프링 빈 조회 방법을 배운다.
2. BeanFactory와 ApplicationContext 의 차이를 배운다.

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

1. 스프링 컨테이너 생성  

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

3. 스프링 빈 조회 - 기본

4. 스프링 빈 조회 - 동일한  타입이 둘 이상 (이번 포스팅)

5. [ 중요 ] 스프링 빈 조회 - 상속 관계

6. BeanFactory와 ApplicationContext 

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

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


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

1) 타입으로 조회 시, 같은 타입이 2개 이상 있으면 중복 오류가 발생한다. 

임시로 MemberRepository 타입 빈이 2개 있는 SameBeanConfig.class 설정 파일을 만들었다. 

여기서 MemberRepository.class 타입 빈을 가져오면? 

아래와 같은 NoUniqueBeanDefinitionException 이 발생한다. 설명이 굉장히 친절하다.

싱글 매칭 빈을 기대했는데, 2개 였다는 내용. 

org.springframework.beans.factory.NoUniqueBeanDefinitionException
: No qualifying bean of type 'hello.corebasic.member.MemberRepository' available
: expected single matching bean but found 2: memberRepository1,memberRepository2

 

이렇게 특정 에러가 발생하는지 확인하려면 assertThrow와 Exception 클래스명을 써서 확인한다.

2) 타입으로 조회 시, 같은 타입이 2개 이상 있으면 빈 이름을 지정하면 된다!

빈 이름은 중복 불가다. 빈 이름을 안겹치게 지어서 조회하면 된다.

3) 특정 타입을 모두 조회하기

getBeansOfType() 메서드로 Map<이름, 타입> 특정 타입의 모든 빈을 모두 조회할 수 있다. 

나중에 @Autowired같이 자동으로 주입되는 기능을 배울 때 이런식으로 적용이 된다. 

ac.getBeansOfType(MemberRepository.class);


[ 중요 ] 5. 스프링 빈 조회 - 상속 관계 

1) 부모 타입으로 빈을 조회하면, 자식 타입도 함께 조회한다.

스프링 빈 대 원칙 하나는 "스프링 빈 하나 조회하면? 그 빈의 자식을 싹다 조회한다. " 

그래서 모든 자바 객체의 최고 부모인 Object 타입. Object 타입으로 조회하면, 모든 스프링 빈을 조회한다. 

테스트 코드로 확인하자. TestConfig 설정파일을 하나 만들자. DiscountPolicy 이름으로 생성될 빈이 2개다!  반환되는 생성자만 다르다. 

2) 부모 타입을 조회하면, 자식까지 다 딸려서 조회되니까 자식이 둘 이상인 경우 오류가 발생한다. 

이럴 땐 이름을 지어서 구분하면 된다. 

3) 부모 타입을 조회 시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다. 

rateDiscountPolicy 특정 빈 이름으로 조회하자. 

4) 특정 하위 타입으로 조회할 수 있다. 

구체적인 타입으로 딱 지정하면 조회할 수 있지만 안 좋은 방법이다. 

5) 부모 타입으로 모두 조회해보자. 

할인 정책 인터페이스를 구현한 구현체 클래스 2개가 조회 된다. 

rateDiscountPolicy, fixDiscountPolicy 자식 타입 스프링 빈 2개가 출력됨을 확인할 수 있다. 

[ 참고 ] 실무에서 테스트 코드를 작성할 때는 이렇게 출력물을 만들면 안된다

지금은 공부용이라서 system.out.print를 하지만, 실무에서 테스트 코드를 작성할 때는 이렇게 출력물을 만들면 안된다. 

테스트 통과/실패 여부만 판단하도록 작성하는게 좋다. 

 

6) 최고 부모 타입인 Object 로 조회해보자. 

모든 자바 객체의 부모는 Object 라서. 모든 스프링 빈이 조회된다. 

7) 부모 타입으로 조회하면, 자식 타입도 함께 조회한다

실무에서는 ApplicationContext를 통해 getBean() 빈을 조회할 일이 거의 없다. 스프링 컨테이너가 자동으로 의존관계를 주입해주기 때문이다.

굳이 이렇게 배운 이유는, 빈 조회는 스프링의 기본 기능이기도 하고 아주 가끔 순수한 자바 애플리케이션에서 스프링 컨테이너를 생성해서 사용할 일이 있기 때문이다. 
그리고 부모 타입으로 조회 시, 자식 타입 빈까지 조회됨을 알고 있어야 자동 의존관계 주입을 배울 때 잘 사용할 수 있다.


6. BeanFactory 와 ApplicationContext

BeanFactory 와 ApplicationContext에 대해서 알아보자. 아래 계층 구조를 보자.

  • 최상위에 있는 인터페이스 BeanFactory 가 있다.
  • 최상위에 있는 인터페이스 BeanFactory를 상속받은 인터페이스 ApplicationContext 가 있다. 
  • ApplicationContext 는 BeanFactory에 부가 기능을 더한 것이다. 

BeanFactory

  • BeanFactory 는 스프링 컨테이너의 최상위 인터페이스다. 
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다. 지금까지 빈 조회에 사용한 getBean() 을 BeanFactory이 제공한 것이다. 

ApplicationContext 

  • BeanFactory의 모든 기능을 상속 받아서 제공한다. 

BeanFactory와 ApplicationContext의 차이는 뭘까? 

  • 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고 수 많은 부가기능이 필요하다. 
  • ApplicationContext는 아래와 같이 여러 인터페이스를 상속받아서 국제화, 이벤트, 리소스 조회 등의 기능을 제공한다. 
  • 우리는 BeanFactory를 직접 사용할 일은 거의 없고, 빈 관리와 부가기능이 포함된 ApplicationContext를 사용한다. 

메시지 소스를 활용한 국제화 기능 

 : 예를 들어 한국에서 접속하면 한국어로, 영어권에서 접속하면 영어로 출력해준다 

환경 변수 

 : 로컬, 개발, 운영 등을 구분해서 환경 변수를 처리 

애플리케이션 이벤트

 : 이벤트를 발행하고 구독하는 모델을 편리하게 지원 

편리한 리소스 조회 

 : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

 

BeanFactory 나 ApplicationContext를 스프링 컨테이너라 한다. 


다음 강의에서는 '스프링 컨테이너가 지원하는 자바코드, XML 형식의 설정 정보'를 배운다. 

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

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

+ Recent posts