no image
[HTTP] URI와 웹 브라우저 요청 흐름
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. URI URI, URL, URN URI 내부에 URL, URN이 포함되어 있다. URI는 로케이터(Locator), 이름(Name) 또는 둘다 추가로 분류될 수 있다. URI (Uniform Resource Identifier) Uniform : 리소스 식별하는 통일된 방식이다. Resource : URI로 식별할 수 있는 모든 걸 자원이라고 한다. 웹 브라우저에 있는 HTML의 파일 것만 자원을 뜻하는 게 아니라 실시간 교통 정보 등등 이런것도 자원이라고 한다. Idenrifier : 다른 항목과 구분하는 데 필요한 정보이다. 사람을 식별할 때 주민등록번호를 식별 하는 것처럼 말한다. URL (Uniform ..
2024.02.06
no image
[HTTP] 인터넷 네트워크
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 인터넷 통신 인터넷망에서 컴퓨터들은 어떻게 통신할까? 클라이언트가 한국에 있고 서버가 먼 곳에 있다면 한국에 있는 클라이언트가 'Hello, world'라는 메시지를 보내야 하는데 데이터의 출발지와 도착지 사이에 수많은 중간 노드라고 하는 서버들을 걸쳐서 다른 곳에 있는 서버에게 안전하게 메시지가 도착해야 한다. 어떤 규칙으로 다른 곳에 있는 서버에게 안전하게 도착할 수 있는지 이해를 하려면 IP 프로토콜을 알아야 한다. IP 인터넷 프로토콜 IP 주소 부여 IP(인터넷 프로토콜) 역할 지정한 IP 주소(Address)에 데이터 전달 패킷(Packet)이라는 통신 단위로 데이터 전달 클라이언트에게 IP 주소, ..
2024.02.05
no image
[Spring 핵심원리 - 기본] Bean Scope
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 빈 스코프 스프링 빈은 기본적으로 싱글톤으로 생성되고, 싱글톤 스코프이기 때문에 스프링 컨테이너의 시작과 함께 스프링 컨테이너가 종료될 때 까지 유지된다. 스코프는 번역 그대로 빈이 존재할수 있는 범위를 뜻한다. 스프링은 다음과 같은 다양한 스코프를 지원한다. 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프 웹 관련 스코프 request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프 session: 웹 세션이 생성되고 종료될 때 까지 유지되..
2024.02.03
no image
[Spring 핵심원리 - 기본] Bean 생명주기 콜백
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. Bean 생명주기 콜백 데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다. 간단하게 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정해보자. 실제로 네트워크에 연결하는 것은 아니고, 단순히 문자만 출력하도록 한다. 이 NetworkClient 는 애플리케이션 시작 시점에 connect() 를 호출해서 연결을 맺어두어야 하고, 애플리케이션이 종료되면 disConnect() 를 호출해서 연결을 끊어야 한다. package hello.core.lifecyc..
2024.02.02
no image
[Spring 핵심원리 - 기본] 의존관계 자동 주입
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 다양한 의존관계 주입 방법 생성자 주입 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다. 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다. 불변, 필수 의존관계에 사용 @Component public class OrderServiceImpl implements OrderService { private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; @Autowired public OrderServiceImpl(MemberRepository memberRepository, DiscountPol..
2024.02.01
no image
[Spring 핵심원리 - 기본] 컴포넌트 스캔
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 이 글은 어노테이션 개념이 많이 포함하고 있습니다. 아래의 글을 먼저 읽고오면 도움이 됩니다. 2023.07.23 - [Java Category/Java] - [JAVA] 리플렉션(Reflection)과 어노테이션(Annotation) [JAVA] 리플렉션(Reflection)과 어노테이션(Annotation) 이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 리플렉션 리플렉션이란 실행 도중에 타입(클래스, 인터페이스 등)을 rebugs.tistory.com 컴포넌트 스캔과 의존관계 자동 주입 이전까지 스프링 빈을 등록할 때는 자바 코드의 ..
2024.01.31
no image
[Spring 핵심원리 - 기본] 싱글톤 컨테이너
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 웹 애플리케이션과 싱글톤 패턴 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다. 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있다. 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 만약 싱글톤 패턴이나 싱글톤 컨테이너를 사용하지 않는다면 아래의 그림과 같이 사용자가 요청을 할 때마다 객체가 생성될 것이다. 싱글톤 패턴에 대한 내용은 아래의 글을 참고해주세요. 2023.01.04 - [Java Category/Java] - [JAVA] 싱글톤(Singleton), final 필드와 상수 하지만 싱글톤 패턴의 문제점이 있다...
2024.01.30
no image
[Spring 핵심원리 - 기본] 스프링 컨테이너와 스프링 빈
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다. 스프링 컨테이너 생성 ApplicationContext 를 스프링 컨테이너라 한다. ApplicationContext 는 인터페이스이다. 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다. 이전에 AppConfig 를 사용했던 방식이 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다. 자바 설정 클래스를 기반으로 스프링 컨테이너( ApplicationContext )를 만들어보자. //스프링 컨테이너 생성 ApplicationContext applicationContext = new AnnotationConfigApplicationConte..
2024.01.29

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


URI

URI, URL, URN 

URI 내부에 URL, URN이 포함되어 있다.

URI는 로케이터(Locator), 이름(Name) 또는 둘다 추가로 분류될 수 있다. 

URI (Uniform Resource Identifier) 

  • Uniform : 리소스 식별하는 통일된 방식이다. 
  • Resource :  URI로 식별할 수 있는 모든 걸 자원이라고 한다. 웹 브라우저에 있는 HTML의 파일 것만 자원을 뜻하는 게 아니라 실시간 교통 정보 등등 이런것도 자원이라고 한다.
  • Idenrifier : 다른 항목과 구분하는 데 필요한 정보이다. 사람을 식별할 때 주민등록번호를 식별 하는 것처럼 말한다.

 

URL (Uniform Resource Locator) 

  • Locator : 리소스가 있는 위치를 지정한다. 

 

URN (Uniform Resource Name)

  • Name : 리소스에 이름을 부여한다. 

 

위치는 변할 수 있지만 이름은 변하지 않는다.

 

URN이 이름으로 실제 리소스가 결과 나올수 있어야 하는데, 이름만으로는 찾기가 어렵다.

 

그래서 URN은 보통 잘 사용되지 않는다. 따라서 URI = URL 으로 통용되기도 한다.

 

URI 전체 문법

scheme://[userinfo@]host[:port][/path][?query][#fragment]
https://www.google.com:443/search?q=hello&hl=ko
  • scheme

주로  프로토콜 사용한다. 어떤 방식으로 자원에 접근할 것인가 하는 클라이언트와 서버 간의 약속 규칙이라고 보면 된다. 

http : 80 / https : 443 / ftp : 20, 21 주로 사용한다.

https는 http에 강력한 보안 추가로 적용된거다.  (HTTP Secure)

 

  • userinfo

URL에 사용자정보를 포함해서 인증해야 할 때 있는데 거의 사용하지 않는다.

 

  • host

호스트명이라고 한다. 보통 도메인명이나 IP 주소를 직접 사용 가능하다.

 

  • port

접속 포트이다. 일반적은 웹 브라우저에서는 생략을 많이 하지만 특정 서버에 따로 접근 할 때는 port를 입력을 한다.

 

  • path
/home/file1.jpg  -> home 이라는 경로에 file1.jpg가 있다.
/members  -> 회원들에 대한 정보를 보여주는 경로이다.
/members/100, /items/iphone12  -> 100번의 회원의 정보, 아이템 중에 아이폰12 정보 경로이다.

리소스가 있는 경로이자 계층적 구조로 되어 있다. 

 

  • query

key와 value 형태로 데이터가 들어가 있다.

?keyA=valueA&keyB=valueB

query는 ?로 시작하고 &로 추가적으로 query string를 입력한다.

숫자를 적어도 다 문자 형태로 넘긴다 해서 query string이라고 부르기도 한다. 

 

  • fragment
https://docs.spring.io/spring-boot/docs/current/reference/html/getting-
started.html#getting-started-introducing-spring-boot

HTML 내부에서 중간에 이동하고 싶을 때 북마크 등에 사용한다. 잘 사용하지 않고 서버에 전송하는 정보 아니다.

 

웹 브라우저 요청 흐름

1. URL을 입력한다. 

2. DNS 서버로 IP를 찾아내고 생략된 PORT는 scheme로 찾아낸다. 

3. 웹 브라우저가 HTTP 요청 메시지를 생성한다.

 

4. SOCKET 라이브러리를 통해서 TCP/IP로 IP와 PORT 정보를 찾은 것을 3 way handshake 방식으로 서버와 연결을 한다.

5. HTTP 요청 메시지는 OS에 있는 TCP/IP 계층으로 전달한다.

6. TCP/IP 계층에서 HTTP 요청 메시지에 패킷으로 감싼다.

7. 웹 브라우저가 만든 요청 패킷을 서버에서 도착하면 패킷을 열어서 HTTP 요청 메시지를 확인해서 서버가 해석한다.

 

8. 서버가 HTTP 응답 메시지를 만들어서 TCP/IP 패킷을 감싸서 클라이언트에게 도착하면 클라이언트는 패킷을 열어서 HTTP 응답 메시지를 확인해서 해석한다. 

 

9. 웹 브라우저가 HTML 렌더링을 해서 클라이언트가 HTML 결과를 볼 수 있다.

'네트워크 > HTTP' 카테고리의 다른 글

[HTTP] 상태코드  (1) 2024.02.10
[HTTP] HTTP 메서드 활용  (1) 2024.02.09
[HTTP] HTTP 메서드  (1) 2024.02.08
[HTTP] HTTP 기본  (1) 2024.02.07
[HTTP] 인터넷 네트워크  (1) 2024.02.05

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


인터넷 통신

인터넷망에서 컴퓨터들은 어떻게 통신할까?

클라이언트가 한국에 있고 서버가 먼 곳에 있다면 한국에 있는 클라이언트가 'Hello, world'라는 메시지를 보내야 하는데 데이터의 출발지와 도착지 사이에 수많은 중간 노드라고 하는 서버들을 걸쳐서 다른 곳에 있는 서버에게 안전하게 메시지가 도착해야 한다.

어떤 규칙으로 다른 곳에 있는 서버에게 안전하게 도착할 수 있는지 이해를 하려면 IP 프로토콜을 알아야 한다.

 

IP 인터넷 프로토콜

IP  주소 부여

IP(인터넷 프로토콜) 역할

  • 지정한 IP 주소(Address)에 데이터 전달 
  • 패킷(Packet)이라는 통신 단위로 데이터 전달

클라이언트에게 IP 주소, 서버에게도 IP 주소가 있어야 하고 서버에게 'Hello, world!' 메시지를 보낼 때 최소한의 규칙이 있어야 보낼 수 있는데 패킷이라는 규칙으로 보내야 한다.

 

IP 패킷 정보

메시지에 패킷의 정보인 출발지 IP와 목적지 IP 등등을 넣고 보내는데 IP 프로토콜에 의해서 서버들이 규약을 따르고 있다. 

 

클라이언트 패킷 전달

인터넷망에서 내가 보낸 패킷을 서로 노드들끼리 출발지가 어디고 목적지가 어디인지 알고 서버 IP 주소인 200.200.200.2 까지 정확하게 도착한다.

 

서버 패킷 전달

 

서버도 마찬가지로 새로 메시지를 만들어서 메시지에 패킷의 정보인 출발지 IP와 목적지 IP 등등을 넣고 클라이언트에게 보내는데 클라이언트가 보낼 때랑 서버가 보낼 때랑 인터넷 망이 복잡하기 때문에 서로 다른 곳으로 전달 될 수 있다. 그래서 IP라는 패킷에 담는 방식으로는 한계가 있다. 

 

IP 프로토콜의 한계

비연결성

미국에 있는 서버가 PC가 꺼져 있는 것도 모르고 클라이언트는 계속 메시지를 보내게 된다.

 

그래서 패킷을 받을 대상이 없거나 서비스불능 상태여도 패킷은 계속 전송한다.

 

비신뢰성 

서버들이 전달하는 사이에서 한 서버가 문제가 생겨서 패킷이 유실 될 수 있다.

 

메시지를 한 번에 보낼 때 보통 1500byte가 넘어가게 되면 보통 나눠서 보낸다.


두 개의 패킷을 나눠서 보내는데 인터넷 망안에 있는 노드들을 통해서 패킷마다 노드를 각각 선택을 해서 따로 따로 보내게 되는 상황이 오다보니 클라이언트가 보낸 메시지 순서가 서버가 받은 메시지 순서와 상이하게 된다.

같은 IP 내에 프로그램 미 구분

  • 클라이언트에서 게임도 하고 음악도 듣고 여러가지 프로그램으로 같은 IP로 쓰고 있는데 어떤 프로그램에 필요한 데이터인지 구분되지 않아 한계가 있다.

 

TCP/ UDP

프로토콜 4계층

IP 프로토콜에서 중간에 패킷이 손실되고 순서가 상이한 문제들을 TCP 프로토콜이 해결해준다.

 

'Hello world!' 라는 메시지를 보낼 때 SOCKET 라이브러리를 통해서 OS계층에서 TCP 정보를 감싼다.

 

그 밑에 IP 패킷이 생성되서 IP와 관련된 정보도 있고 그 안에 TCP와 관련된 정보가 있다.

 

이 메시지를 랜카드를 통해서 나갈 때 Ethernet frame을 포함해서 나가게 된다. 

 

Ethernet frame : 램 카드에 등록된 맥주소의 물리적인 정보

 

TCP/IP 패킷 정보

TCP와 관련된 정보에는 출발지 PORT, 목적지 PORT, 전송 제어, 순서, 검증 정보 등이 들어 간다.

 

IP 프로토콜에서 해결이 안된 순서 제어 문제들이 TCP 프로토콜이 해결이 되고 전송 데이터를 넣게 된다.

 

TCP(Transmission Control Protocol)의 특징

TCP 3 way handshake (가상 연결)

  • SYN(synchronize) : 접속 요청
  • ACK(acknowledge) : 요청 수락

 

  1. 클라이언트에서 먼저 SYN 메시지를 서버에게 접속을 허락해달라고 요청한다.
  2. 서버는 접속을 수락하고 ACK 메시지를 클라이언트 한테 보낼 때 서버도 접속을 허락해달라고 SYN 메시지와 함께 보낸다.
  3. 클라이언트가 접속을 수락하고 서버에서 ACK 메시지를 보낸다.
    참고로 요즘 최적화가 잘 되서 마지막 ACK를 보낼 때 데이터도 전송한다.

 

이렇게 3단계를 거쳐 연결이 되고나면 데이터를 전송한다.  

 

위에 TCP 연결이 되었다고 연결이 된 게 아니라 논리적으로 연결되어 있을 뿐이다.

 

인터넷 망에 있는 수 많은 서버들은 이 둘이 연결되어 있는 건지 알 수 없다. 

 

데이터 전달 보증

메시지에 TCP 프로토콜이 포함되어 있으면 메시지를 전송할 때 서버에서 잘 받았다고 다시 보내는데 클라이언트가 메시지를 잘 전달 됐는지 안됐는지 알 수 있다. 

 

순서보장

메시지가 1500byte가 넘어서 패킷을 1번, 2번, 3번 순서로 나눠서 보냈다.

 

서버에서 1번, 3번, 2번 순서로 도착을 하면 내부적으로 최적화하는 로직에 따라서 2번부터 다시 보내라고 클라이언트에게 요청할 수 있다. 그렇기 때문에 순서가 보장이 된다.

 

또는 서버측에서 내부적으로 순서를 올바르게 정렬할 수도 있다.

 

순서대로 보낼 수 있는 이유는 TCP 정보 안에는 전송 제어, 순서, 검증 정보가 있어서 TCP 프로토콜이 신뢰할 수 있는 프로토콜이라고 한다.

 

UDP(User Datagram Protocol)의 특징

  • TCP 3 way handshake X
  • 데이터 전달 보증 X
  • 순서 보장 X
  • PORT 정보 O
  • 체크섬 정보 O

TCP는 데이터 양도 많고 3 way hands 때문에 전송 속도가 느린 반면에 UDP는 아무것도 없기 때문에 상대적으로 전송 속도가 빠르다.

 

클라이언트 PC에서 IP가 한 개만 할당되어 있어 있는데 게임용, 음악용 등 구분하기위해 PORT를 사용하고 메시지에 대해서 맞는지 검증해 주는 체크섬 특징이 있다.

 

PORT

클라이언트에서 게임도 하고 화상통화도 하고 웹 브라우저도 하고 있으면 여러 개의 서버랑 통신 해야 된다.

 

PORT 번호를 모르면, 클라이언트 IP에서 패킷이 올텐데 어떤 프로그램에게 온 패킷인지 알 수가 없다.

 

즉, IP 주소를 통해 아파트 앞까지는 왔는데 몇호인지 알 수 없는 것이다.

 

PORT 정보를 알면 어떤 프로그램에게 온지 알 수 있다.

 

즉, 몇호인지 알 수 있는 것이다.

 

TCP/IP 패킷 정보

위에 봤던 TCP/IP 패킷 정보에 TCP와 UDP에서 출발지 PORT와 목적지 PORT가 있다.

 

IP는 목적지 서버를 찾는 용도이고 서버 안에서 돌아가는 애플리케이션들을 구분하는게 PORT이다. 

 

같은 IP 내에서 프로세스 구분

같은 IP 내에서 프로세스를 구분하는 게 PORT 이다. 

  • [클라이언트] 게임 : 8090 ↔ [서버] 게임 : 11200
  • [클라이언트] 화상통화 : 21000 ↔ [서버] 화상통화 : 32202
  • [클라이언트] 웹 브라우저 : 10010 ↔ [서버] 웹 브라우저 : 80

 

위와 같이 예시처럼 각각 클라이언트와 서버 안에 맞는 PORT 번호를 찾아서 연결하면 된다.

여기서 패킷을 보낼 때 IP와 PORT를 포함해서 보낸다.

PORT 번호

  • 0 ~ 65535 : 할당 가능
  • 0 ~ 1023 : 잘 알려진 포트라 사용하지 않는 것이 좋음
  • FTP : 20, 21 / TELNET : 23 / HTTP : 80 / HTTPS : 443 

 

DNS

IP 주소의 문제점

IP를 가지고 서로 통신을 할 수 있지만 IP가 숫자로 되어 있어서 다 기억하기가 어렵다.

 

그리고 IP가 바뀌는 일이 많아져서 접속이 안되는 경우가 생긴다.

 

DNS (Domain Name System)

이러한 문제를 해결하기 위해 DNS 가 생겼다.

 

도메인을 사기 위해서 DNS 서버에 도메인을 등록을 할 수 있다.

 

클라이언트가 DNS 서버에다가 도메인에 맞는 IP를 달라고 요청을 하면 DNS 서버가 응답을 하고 클라이언트는 서버에 도메인으로 접속할 수 있다.

 

나중에 IP가 변경 되면 DNS 서버에 등록된 도메인에 IP를 변경을 할 수있다.

'네트워크 > HTTP' 카테고리의 다른 글

[HTTP] 상태코드  (1) 2024.02.10
[HTTP] HTTP 메서드 활용  (1) 2024.02.09
[HTTP] HTTP 메서드  (1) 2024.02.08
[HTTP] HTTP 기본  (1) 2024.02.07
[HTTP] URI와 웹 브라우저 요청 흐름  (1) 2024.02.06

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


빈 스코프

스프링 빈은 기본적으로 싱글톤으로 생성되고, 싱글톤 스코프이기 때문에 스프링 컨테이너의 시작과 함께 스프링 컨테이너가 종료될 때 까지 유지된다.

스코프는 번역 그대로 빈이 존재할수 있는 범위를 뜻한다.

스프링은 다음과 같은 다양한 스코프를 지원한다.

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프

 

웹 관련 스코프

  • request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
  • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
  • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

빈 스코프는 다음과 같이 지정할 수 있다.

컴포넌트 스캔 자동 등록

@Scope("prototype")
@Component
public class HelloBean {}

 

수동 등록

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
 	return new HelloBean();
}

 

프로토타입 스코프

프로토타입 스코프의 개념

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.

반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.

 

  1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
  2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
  3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다.

 

  1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
  2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.

 

3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.

정리

여기서 핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것이다.
클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다.

 

싱글톤 스코프와 차이점 테스트

싱글톤 스코프 빈 테스트

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import static org.assertj.core.api.Assertions.assertThat;

public class SingletonTest {
    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }
        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }

    @Test
    public void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);

        assertThat(singletonBean1).isSameAs(singletonBean2);
        ac.close(); //종료
    }
}

 

빈 초기화 메서드를 실행하고, 같은 인스턴스의 빈을 조회하고, 종료 메서드까지 정상 호출 된 것을 확인할 수 있다.

 

프로토타입 스코프 빈 테스트

package hello.core.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.*;
public class PrototypeTest {
    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }
        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
    
    @Test
    public void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);

        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close(); //종료
    }
}

싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드만 실행된다.

 

프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 확인할 수 있다.

싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만, 
프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하고, 더는 관리하지 않는
다.

따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestroy 같은 종료 메서드가 전혀 실행되지 않
는다.

 

프로토타입 빈의 특징 정리

- 스프링 컨테이너에 요청할 때 마다 새로 생성된다.
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
- 종료 메서드가 호출되지 않는다.
- 그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.

 

 

싱글톤 빈과 함께 사용시 문제점

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다.

하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.


프로토타입 빈 직접 요청

1. 클라이언트A는 스프링 컨테이너에 프로토타입 빈을 요청한다.
2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x01)한다. 해당 빈의 count 필드 값은 0이다.
3. 클라이언트는 조회한 프로토타입 빈에 addCount() 를 호출하면서 count 필드를 +1 한다.

결과적으로 프로토타입 빈(x01)의 count는 1이 된다.

 

4. 클라이언트B는 스프링 컨테이너에 프로토타입 빈을 요청한다.
5. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x02)한다. 해당 빈의 count 필드 값은 0이다.
6. 클라이언트는 조회한 프로토타입 빈에 addCount() 를 호출하면서 count 필드를 +1 한다.

결과적으로 프로토타입 빈(x02)의 count는 1이 된다

 

싱글톤 빈에서 프로토타입 빈 사용

이번에는 clientBean 이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예를 보자

clientBean 은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.

1. clientBean 은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 에 반환한다. 프로토타입 빈의 count 필드 값은 0이다.

이제 clientBean 은 프로토타입 빈을 내부 필드에 보관한다. (정확히는 참조값을 보관한다.)

 

클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean 이 반환된다.

3. 클라이언트 A는 clientBean.logic() 을 호출한다.
4. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.

count값이 1이 된다.

 

클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean 이 반환된다.

여기서 중요한 점이 있는데, clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.

주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다.

5. 클라이언트 B는 clientBean.logic() 을 호출한다.
6. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.

원래 count 값이 1이었으므로 2가 된다.

 

테스트 코드

package hello.core.scope;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Configuration
    static class ClientBean {
        private final PrototypeBean prototypeBean;

        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }
        public int logic() {
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;
        public void addCount() {
            count++;
        }
        public int getCount() {
            return count;
        }
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init " + this);
        }
        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);
        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();

        assertThat(count2).isEqualTo(2);
    }
}

테스트 코드가 문제없이 통과했으므로 count 2는 1이 아니라 2이다.

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.

그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.

아마 원하는 것이 이런 것은 아닐 것이다.

프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로생성해서 사용하는 것을 원할 것이다.

 

참고

여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.
예를 들어서 clientA, clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.
-clientA -> prototypeBean@x01
-clientB -> prototypeBean@x02
당연히 한번 할당받은 프로토타입 빈은 그대로 유지된다.

 

싱글톤 빈과 함께 사용시 문제 해결

 

가장 간단한 방법

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 프로토타입을 스프링 컨테이너에 새로 요청하는 것이다.

package hello.core.scope;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import static org.assertj.core.api.Assertions.assertThat;

public class PrototypeProviderTest {
    @Scope("prototype")
    static class PrototypeBean {

        private int count = 0;
        public void addCount() {
            count++;
        }
        public int getCount() {
            return count;
        }
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init " + this);
        }
        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }

    static class ClientBean {
        @Autowired private ApplicationContext ac;
        public int logic() {
            PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

    @Test
    void providerTest() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);
        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(1);
    }

}

 

핵심코드

@Autowired
private ApplicationContext ac;public int logic() {
 	PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
 	prototypeBean.addCount();
 	int count = prototypeBean.getCount();
 	return count;
}

실행해보면 ac.getBean() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다.

그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.

지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 된다. 

 

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.

과거에는 ObjectFactory 가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어졌다.

ObjectFactory 를 상속받은 것이 ObjectProvider

 

ClientBean을 아래처럼 바꾸면 된다.

static class ClientBean {
    @Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider;
    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)

스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬쉬워진다.

ObjectProvider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.

 

특징

  • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
    ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

 

 

JSR-330 Provider

마지막 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.

jakarta.inject:jakarta.inject-api:2.0.1 라이브러리를 gradle에 추가해야 한다.

import jakarta.inject.Provider;

...

static class ClientBean {
    @Autowired private Provider<PrototypeBean> provider;
    public int logic() {
        PrototypeBean prototypeBean = provider.get();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)

자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.

Provider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.

 

특징

  • get() 메서드 하나로 기능이 매우 단순하다.
  • 별도의 라이브러리가 필요하다.
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

 

정리

그러면 프로토타입 빈을 언제 사용하는가?
-> 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.

그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.

ObjectProvider , JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할수 있다

 

참고

실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지, 아니면 스프링이 제공하는 ObjectProvider 를 사용할 것인지 고민이 될 것이다.
ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별 도의 의존관계 추가가 필요 없기 때문에 편리하다.

만약(정말 그럴일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야한다.
스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많 이 있다.
대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없 다면, 스프링이 제공하는 기능을 사용하면 된다

 

웹 스코프

웹 스코프의 특징

  • 웹 스코프는 웹 환경에서만 동작한다.
  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.

 

웹 스코프 종류

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

 

이 글에서는 request 스코프를 예제로 설명한다. 나머지도 범위만 다르지 동작 방식은 비슷하다.

 

HTTP request 요청 당 각각 할당되는 request 스코프

클라이언트A 가 HTTPrequest를 하면 스프링 컨테이너에 request가 들어오고 스프링 컨테이너에 다시 나갈 때까지 생명주기를 갖는다.

마찬가지로, 클라이언트B 의 HTTPrequest도 마찬가지로 동작한다.

 

또한, A와 B가 동시에 요청을 하였더라도 각각 클라이언트에 다른 스프링 빈이 할당된다.

클라이언트 A가 Controller@x01에 요청을 하여도, Service@x02에서 요청을 하여도 똑같은 스프링 빈을 가리킨다.

클라이언트 B의 경우도 마찬가지다.

 

즉,  HTTP 요청 당 하나씩 생성된 빈이 해당 요청의 전용 빈이 되는 것이다.

이렇게 생성된 스프링 빈은 HTTPrequest 가 들어오고 스프링 컨테이너에서 나갈때까지의 스코프를 갖는다.

 

request 스코프 예제

웹 환경 추가

웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가해야 한다.


build.gradle에 추가

//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'


이제 hello.core.CoreApplication 의 main 메서드를 실행하면 웹 애플리케이션이 실행되는 것을 확인할 수 있다.

spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰켓 서버를 활용해서 웹 서버와 스프링을 함께 실행시킨다.

스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext 을 기반으로 애플리케이션을 구동한다.
웹 라이브러리가 추가되 면 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext 를 기반으로 애플리케이션을 구동한다

만약 기본 포트인 8080 포트를 다른곳에서 사용중이어서 오류가 발생하면 포트를 변경해야 한다. 9090 포트로 변경하 려면 다음 설정을 추가해야한다.
main/resources/application.properties 에서  server.port=9090

 

동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.

이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.

다음과 같이 로그가 남도록 request 스코프를 활용해서 추가 기능을 개발해보자.

[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
  • 기대하는 공통 포멧: UUIDrequestURL {message}
  • UUID를 사용해서 HTTP 요청을 구분하자.
  • requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인하자.

 

MyLogger

package hello.core.common;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL){
        this.requestURL = requestURL;
    }

    public void log(String message){
        System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
    }

    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }

    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}
  • 로그를 출력하기 위한 MyLogger 클래스이다.
  • @Scope(value = "request") 를 사용해서 request 스코프로 지정했다. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
  • 이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장해둔다. 
  • 이 빈은 HTTP 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.
  • 이 빈이 소멸되는 시점에 @PreDestroy 를 사용해서 종료 메시지를 남긴다.
  • requestURL 은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.

 

LogDemoController

package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class LogDemoController { 
    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
  • 로거가 잘 작동하는지 확인하는 테스트용 컨트롤러다.
  • 여기서 HttpServletRequest를 통해서 요청 URL을 받는다.
  • requestURL : http://localhost:8080/log-demo
  • 이렇게 받은 requestURL 값을 myLogger에 저장해둔다. myLogger는 HTTP 요청 당 각각 구분되므로 다른
  • HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.
  • 컨트롤러에서 controller test라는 로그를 남긴다.
참고
requestURL을 MyLogger에 저장하는 부분은 컨트롤러 보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다

 

LogDemoService 추가

package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;
    public void logic(String id) {
        myLogger.log("service id = " + id);
    }
}
  • 비즈니스 로직이 있는 서비스 계층에서도 로그를 출력해보자.
  • request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면,  파라미터가 많아서 지저분해진다. 더 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 된다.
  • 웹과 관련된 부분은 컨트롤러까지만 사용해야 한다. 서비스 계층은 웹 기술에 종속되지 않고, 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
  • request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고, MyLogger의 멤버변수에 저장해서
    코드와 계층을 깔끔하게 유지할 수 있다.

 

기대하는 출력

[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close

-> 실제는 기대와 다르게 애플리케이션 실행 시점에 오류 발생

스프링 애플리케이션을 실행하는 시점에 싱글톤 빈을 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되
지 않았다.

 

컨트롤러에서 MyLogger 의존성 주입을 받아야 하는데, MyLogger의 스코프는 request scope이기 때문에 아직 생성조차 되지 않았다. -> 스프링 빈 자체가 아직 스프링 컨테이너에 없다.

 

즉, 요청이 와야 생성된다.

 

첫번째 해결방안은 Provider를 사용하는 것이다.
ObjectProvider를 사용해보자.

package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

 

package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

main() 메서드로 스프링을 실행하고, 웹 브라우저에 http://localhost:8080/log-demo 를 입력하면 드디어 잘 작동하는 것을 확인할 수 있다.

 

ObjectProvider로 스프링 컨테이너가 의존성 주입을 할때 허수아비을 세워둔다.

 

나중에 ObjectProvider.getObject() 를 호출할 때는 요청(request)이 왔을테니, 그때는 request scope 빈을 활용할 수 있다.

ObjectProvider 덕분에 ObjectProvider.getObject() 를 호출하는 시점까지 request scope 빈을 실제가 꼭 필요한 시점을 지연할 수 있다.(설명하기가 참 어렵다)

->진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 점

 

ObjectProvider.getObject() 를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.

 

ObjectProvider.getObject() 를 LogDemoController , LogDemoService 에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다.

 

 

스코프와 프록시

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {}

여기가 핵심이다.

proxyMode = ScopedProxyMode.TARGET_CLASS 를 추가해주자. 

  • 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS 를 선택
  • 적용 대상이 인터페이스면 INTERFACES 를 선택

이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를
다른 빈에 미리 주입해 둘 수 있다.

 

이제 나머지 코드를 Provider 사용 이전으로 돌려두자.

 

package hello.core.web;
import hello.core.common.MyLogger;
import hello.core.logdemo.LogDemoService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

 

package hello.core.logdemo;
import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;
    
    public void logic(String id) {
        myLogger.log("service id = " + id);
    }
}

실행해보면 잘 동작하는 것을 확인할 수 있다.

 

스코프와 프록시 동작 원리

컨트롤러에 아래의 코드를 추가해서 확인해보자.

System.out.println("myLogger = " + myLogger.getClass());

 


출력결과 


CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.

@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.

결과를 확인해보면  순수한 MyLogger 클래스가 아니라 MyLogger$$EnhancerBySpringCGLIB 이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다.

그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.
ac.getBean("myLogger", MyLogger.class) 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다.

그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.

 

 

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.

  • 가짜 프록시 객체는 내부에 진짜 myLogger 를 찾는 방법을 알고 있다.
  • 클라이언트가 myLogger.log() 을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
  • 가짜 프록시 객체는 request 스코프의 진짜 myLogger.log() 를 호출한다.
  • 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다(다형성)

 

동작 정리

  • CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
  • 이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
  • 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.

 

특징 정리

  • 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
  • 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리한다는 점이다.
    단지 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
  • 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.

 

주의점

  • 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
  • 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


Bean 생명주기 콜백

데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.

 

 

간단하게 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정해보자.

실제로 네트워크에 연결하는 것은 아니고, 단순히 문자만 출력하도록 한다.

이 NetworkClient 는 애플리케이션 시작 시점에 connect() 를 호출해서 연결을 맺어두어야 하고, 애플리케이션이 종료되면 disConnect() 를 호출해서 연결을 끊어야 한다.

 

package hello.core.lifecycle;
public class NetworkClient {
    private String url;
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }
    public void setUrl(String url) {
        this.url = url;
    }
    //서비스 시작시 호출
    public void connect() { 
        System.out.println("connect: " + url);
    }
    public void call(String message) {
        System.out.println("call: " + url + " message = " + message);
    }
    //서비스 종료시 호출
    public void disconnect() {
        System.out.println("close: " + url);
    }
}

 

package hello.core.lifecycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanLifeCycleTest {

    @Configuration
    static class LifeCycleConfig {
        @Bean
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }

    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }
}

 

실행해보면  아래와 같은 결과가 나온다.

 

생성자 부분을 보면 url 정보 없이 connect가 호출되는 것을 확인할 수 있다.
너무 당연한 이야기이지만 객체를 생성하는 단계에는 url이 없고, 객체를 생성한 다음에 외부에서 수정자 주입을 통해서 setUrl() 이 호출되어야 url이 존재하게 된다.


스프링 빈은 간단하게 다음과 같은 라이프사이클을 가진다.
객체 생성 -> 의존관계 주입(생성자 주입은 객체 생성에서 의존관계 주입)

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다.

따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.

 

스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공
한다.

또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.

 

스프링 빈의 이벤트 라이프사이클

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

  • 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
  • 소멸전 콜백: 빈이 소멸되기 직전에 호출

 

스프링은 다양한 방식으로 생명주기 콜백을 지원한다.

참고
객체의 생성과 초기화를 분리하자.
생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 이렇
게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.

따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.
물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는게 더 나을수 있다.

 

참고
싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다.
싱글톤 처럼 컨테이너의 시작과 종료까지 생존하는 빈도 있지만, 생명주기가 짧은 빈들도 있는데 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸전 콜백이 일어난다.

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.
- 인터페이스(InitializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 종료 메서드 지정
- @PostConstruct, @PreDestroy 어노테이션 지원

 

 InitializingBean, DisposableBean 인터페이스

  • InitializingBean 은 afterPropertiesSet() 메서드로 초기화를 지원한다.
  • DisposableBean 은 destroy() 메서드로 소멸을 지원한다.

 

@Override
public void afterPropertiesSet() throws Exception {
    connect();
    call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception {
    disConnect();
}

이렇게 하면 초기화 메서드가 주입 완료 후에 적절하게 호출할 수 있다.

그리고 스프링 컨테이너의 종료가 호출되면 소멸 메서드가 호출할 수 있다.

 

초기화, 소멸 인터페이스 단점
이 인터페이스는 스프링 전용 인터페이스다.
해당 코드가 스프링 전용 인터페이스에 의존한다.초기화, 소멸 메서드의 이름을 변경할 수 없다.
내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.
이 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법 들이 있어서 거의 사용하지 않는다.

 

빈 등록 초기화, 소멸 메서드 지정

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.

 

public void init() {
 	System.out.println("NetworkClient.init");
 	connect();
	call("초기화 연결 메시지");
 }
 public void close() {
 	System.out.println("NetworkClient.close");
 	disConnect();
 }

 

public class BeanLifeCycleTest {

    @Configuration
    static class LifeCycleConfig {
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }
}

이렇게 해도 동일하게 의존성 주입이 된 후에 init() 을 호출하고, 빈이 소멸할 때 close() 메소드를 호출할 수 있다.

 

설정 정보 사용

  • 특징메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

 

종료 메서드 추론

  • @Bean의 destroyMethod 속성에는 아주 특별한 기능이 있다.
  • 라이브러리는 대부분 close , shutdown 이라는 이름의 종료 메서드를 사용한다. 
  • @Bean의 destroyMethod 는 기본값이 (inferred) (추론)으로 등록되어 있다.
    이 추론 기능은 close , shutdown 라는 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출해준다.
  • 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
    추론 기능을 사용하기 싫으면 destroyMethod="" 처럼 빈 공백을 지정하면 된다.

 

@PostConstruct, @PreDestroy 어노테이션

@PostConstruct , @PreDestroy 이 두 어노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.

 

package hello.core.lifecycle;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

public class NetworkClient {
    private String url;
    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }
    public void setUrl(String url) {
        this.url = url;
    }
    //서비스 시작시 호출
    public void connect() { System.out.println("connect: " + url);
    }
    public void call(String message) {
        System.out.println("call: " + url + " message = " + message);
    }
    //서비스 종료시 호출
    public void disConnect() {
        System.out.println("close + " + url);
    }
    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메시지");
    }
    @PreDestroy
    public void close() {
        System.out.println("NetworkClient.close");
        disConnect();
    }
}

 

package hello.core.lifecycle;
import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
public class BeanLifeCycleTest {

    @Configuration
    static class LifeCycleConfig {
        @Bean
        public NetworkClient networkClient() {
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close(); //스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }
}

 

@PostConstruct, @PreDestroy 애노테이션 특징

  • 최신 스프링에서 가장 권장하는 방법이다.
  • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 패키지를 잘 보면 javax.annotation.PostConstruct 이다. 스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

 

정리

@PostConstruct, @PreDestroy 애노테이션을 사용하자
코드를 고칠 수 없는 외부 라이브러리를 초기화, 종료해야 하면 @Bean 의 initMethod , destroyMethod 를 사용하자.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


다양한 의존관계 주입 방법

생성자 주입

이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법이다.

  • 생성자 호출시점에 딱 1번만 호출되는 것이 보장된다.
    불변, 필수 의존관계에 사용

 

@Component
public class OrderServiceImpl implements OrderService {
     private final MemberRepository memberRepository;
     private final DiscountPolicy discountPolicy;
     
     @Autowired
     public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
     }
}

생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. (스프링 빈에만 해당)

 

수정자 주입(setter)

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.

  • 선택, 변경 가능성이 있는 의존관계에 사용
  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다

 

@Component
public class OrderServiceImpl implements OrderService {
     private MemberRepository memberRepository;
     private DiscountPolicy discountPolicy;
     
     @Autowired
     public void setMemberRepository(MemberRepository memberRepository) {
     	this.memberRepository = memberRepository;
     }
     
     @Autowired
     public void setDiscountPolicy(DiscountPolicy discountPolicy) {
     	this.discountPolicy = discountPolicy;
     }
}
참고
@Autowired 의 기본 동작은 주입할 대상이 없으면 오류가 발생한다.
주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false) 로 지정하면 된다

 

필드 주입

이름 그대로 필드에 바로 주입하는 방법이다.

  • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 치명적인 단점이 있다.
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
  • 애플리케이션의 실제 코드와 관계 없는 테스트 코드나 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용
@Component
public class OrderServiceImpl implements OrderService {
     @Autowired private MemberRepository memberRepository;
     @Autowired private DiscountPolicy discountPolicy;
}
참고
순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다.
@SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다.

 

일반 메서드 주입

일반 메서드를 통해서 주입 받을 수 있다.

  • 한번에 여러 필드를 주입 받을 수 있다.
  • 일반적으로 잘 사용하지 않는다
@Component
public class OrderServiceImpl implements OrderService {
     private MemberRepository memberRepository;
     private DiscountPolicy discountPolicy;
     
     @Autowired
     public void init(MemberRepository memberRepository, DiscountPolicydiscountPolicy) {
     	this.memberRepository = memberRepository;
     	this.discountPolicy = discountPolicy;
     }
}
참고
의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다.
스프링 빈이 아닌 일반 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

 

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

그런데 @Autowired 만 사용하면 required 옵션의 기본값이 true 로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.

자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.
//호출 안됨
@Autowired(required = false)
public void setNoBean1(Member member) {
 	System.out.println("setNoBean1 = " + member);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
 	System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
	System.out.println("setNoBean3 = " + member);
}
  • Member는 스프링 빈이 아니다.
  • setNoBean1() 은 @Autowired(required=false) 이므로 호출 자체가 안된다.
참고
@Nullable, Optional은 스프링 전반에 걸쳐서 지원된다.
예를 들어서 생성자 자동 주입에서 특정 필드에 만 사용해도 된다.

 

생성자 주입을 선택해야 하는 이유

과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 그 이유는 다음과 같다.

 

불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.(불변해야 한다.)
  • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다.
    누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.

 

누락

프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우에 다음과 같이 수정자 의존관계인 경우

public class OrderServiceImpl implements OrderService {
     private MemberRepository memberRepository;
     private DiscountPolicy discountPolicy;
     
     @Autowired
     public void setMemberRepository(MemberRepository memberRepository) {
     	this.memberRepository = memberRepository;
     }
     
     @Autowired
     public void setDiscountPolicy(DiscountPolicy discountPolicy) {
     	this.discountPolicy = discountPolicy;
     }
 //...
}

 

@Test
void createOrder() {
 	OrderServiceImpl orderService = new OrderServiceImpl();
 	orderService.createOrder(1L, "itemA", 10000);
}

생성자에서 memberRepository 와 discountPolicy의 의존관계를 주입하지 않았기 때문에 createOrder() 를 호출하면 NPE가 발생한다.

 

반면에 생성자 호출을 사용하면 똑같은 코드를 하더라도 컴파일 오류가 발생하기 때문에 실행전에 오류를 잡을 수 있다.

 

final 키워드

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오
류를 컴파일 시점에 막아준다.

@Component
public class OrderServiceImpl implements OrderService {
 private final MemberRepository memberRepository;
 private final DiscountPolicy discountPolicy;
 
 @Autowired
 public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
 	this.memberRepository = memberRepository;
 }
 //...
}
참고
수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 호출 이후에 호출되므로, 필드에 final 키워드를 사용 할 수 없다.
오직 생성자 주입 방식만 final 키워드를 사용할 수 있다

 

정리
생성자 주입 방식을 선택하는 이유는 여러가지가 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징 을 잘 살리는 방법이기도 하다.

기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다.
이렇게 하면 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.

항상 생성자 주입을 선택하는 것이 좋다. 그리고 가끔 옵션이 필요하면 수정자 주입을 선택하고, 필드 주입은 사용하지 않는 게 좋다.

 

롬복과 최신 트렌드

2023.07.28 - [Java Category/Java] - [Java] 레코드, 롬복(lombok)

 

[Java] 레코드, 롬복(lombok)

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 레코드(record) 데이터 전달을 위한 DTO(Data Transfer Object)를 작성할 때 반

rebugs.tistory.com

 

간결한 코드 작성

막상 개발을 해보면, 대부분이 다 불변이고, 그래서 다음과 같이 필드에 final 키워드를 사용하게 된다.

롬복이 제공하는 여러 어노테이션을 사용하면, 코드를 적는 양이 최소화된다.

 

예를 들어서 아래와 같은 코드가 있다고 가정하면

@Component
public class OrderServiceImpl implements OrderService {
     private final MemberRepository memberRepository;
     private final DiscountPolicy discountPolicy;
     
     //@Autowired 생략 가능(생성자가 1개라서)
     public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
     }
}

롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.

 

따라서 아래의 코드처럼 간결해질 수 있다.

@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
 	private final MemberRepository memberRepository;
 	private final DiscountPolicy discountPolicy;
}

위 코드와 이전의 코드는 완전히 동일하다.

롬복이 자바의 어노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성해준다.

정리
최근에는 생성자를 딱 1개 두고, @Autowired 를 생략하는 방법을 주로 사용한다.
여기에 Lombok 라이브러리의 @RequiredArgsConstructor 함께 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.

 

롬복 라이브러리 적용 방법

 build.gradle에 아래의 내용을 추가해준다.

configurations {
 compileOnly {
 extendsFrom annotationProcessor
 }
}

 

또한 dependencies에 아래의 내용을 추가해준다.

dependencies {
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
}

 

gradle을 reload 해준다.

 

이후 아래의 과정을 진행한다.

  1. Preferences(윈도우 File Settings) -> plugin -> lombok 검색 설치 실행 (재시작)
  2. Preferences -> Annotation Processors 검색-> Enable annotation processing 체크 (재시작)
  3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인

 

 

조회 빈이 2개 이상

스프링 컨테이너에 스프링 빈이 같은 타입으로 2개가 등록되어 있고, 타입으로 조회하면 모호성이 생긴다.

 

@Autowired 는 타입(Type)으로 조회한다.

@Autowired
private DiscountPolicy discountPolicy

 

타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작한다.

ac.getBean(DiscountPolicy.class)

 

타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.

 

@Autowired 매칭

@Autowired 는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.

 

예를 들어 discountPolicy 인터페이스의 구현 클래스가 fixDiscountPolicy와 rateDiscountPolicy 두 개있는데, 그 중에서 rateDiscountPolicy 를 사용하고 싶다면 이름만 필드나 파리미터의 이름을 해당 빈 이름으로 변경하면 자동으로 어떤 구현 클래스를 사용할지 정해준다.

 

필드 이름

기존 이름

@Autowired
private DiscountPolicy discountPolicy

 

필드 명을 빈 이름으로 변경

@Autowired
private DiscountPolicy rateDiscountPolicy

 

파라미터 이름

기존 코드

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

파라미터 이름을 빈 이름으로 변경

public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }

 

필드 명 매칭은 먼저 타입 매칭을 시도 하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.

@Autowired 매칭 정리
1. 타입 매칭
2. 타입 매칭의 결과가 2개 이상일 때 필드 명, 파라미터 명으로 빈 이름 매칭

 

@Qualifier

@Qualifier 는 추가 구분자를 붙여주는 방법이다.

주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

 

빈 등록시 @Qualifier를 붙여 준다

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

 

주입시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.

 

생성자 자동 주입 예시

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
 	this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}

 

수정자 자동 주입 예시

@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
 	this.discountPolicy = discountPolicy;
}

 

@Qualifier 로 주입할 때 @Qualifier("mainDiscountPolicy") 를 못찾으면  mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.

@Qualifier 는 @Qualifier 를 찾는 용도로만 사용하는게 명확하고 좋다.

 

다음과 같이 직접 빈 등록시에도 @Qualifier를 동일하게 사용할 수 있다

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
 	return new ...
}
@Qualifier 정리
1. @Qualifier끼리 매칭
2. 빈 이름 매칭
3. NoSuchBeanDefinitionException 예외 발생

 

@Primary 사용

@Primary 는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을 가진다.

 

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

이렇게 @Primary로 설정하면 RateDiscountPolicy가 우선적으로 적용된다.

//생성자
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
     this.memberRepository = memberRepository;
     this.discountPolicy = discountPolicy;
}

//수정자
@Autowired
public DiscountPolicy setDiscountPolicy(DiscountPolicy discountPolicy) {
 	this.discountPolicy = discountPolicy;
}

코드를 실행해보면 문제 없이 @Primary 가 잘 동작하는 것을 확인할 수 있다.

 

여기까지 보면 @Primary 와 @Qualifier 중에 어떤 것을 사용하면 좋을지 고민이 될 것이다.

@Qualifier 의 단점은 주입 받을 때 다음과 같이 모든 코드에 @Qualifier 를 붙여주어야 한다는 점이다.

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
     this.memberRepository = memberRepository;
     this.discountPolicy = discountPolicy;
}

 

반면에 @Primary 를 사용하면 이렇게 @Qualifier 를 붙일 필요가 없다.

@Primary, @Qualifier 활용

코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자.
메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary 를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier 를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.
물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier 를 지정해주는 것은 상관없다.
우선순위

@Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다.
스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다.
따라서 여기서도 @Qualifier 가 우선권이 높다.

 

어노테이션 직접 만들기

@Qualifier("mainDiscountPolicy") 를 이용해 직접 문자를 적으면 컴파일시 타입 체크가 안된다.

즉 오타로 인해 오류가 발생할 수 있다는 것이다.

어노테이션을 직접 만들면 이러한 실수를 방지할 수 있다.

package hello.core.annotataion;

import org.springframework.beans.factory.annotation.Qualifier;
import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

이렇게 코드를 작성하면 @MainDiscountPolicy 어노테이션을 붙이면 @Qualifier("main DiscountPolicy ") 또한 적용된다.

 

@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}

RateDiscountPolicy에 만든 어노테이션을 적용하고

 

//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}

//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
 	this.discountPolicy = discountPolicy;
}

생성자 또는 수정자에 만든 어노테이션을 적용하면 DiscountPolicy 인터페이스의 구현체가 RateDiscountPolicy가 된다.

 

참고
어노테이션에는 상속이라는 개념이 없다.
이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
@Qualifier 뿐만 아니라 다른 애노테이션들도 함께 조합해서 사용할 수 있다. 단적으로 @Autowired 도 재정의 할 수 있다.
물론 스프링이 제공하는 기능을 뚜렷한 목적 없이 무분별하게 재정의 하는 것은 유지보수에 더 혼란만 가중할 수 있다.

 

조회한 빈이 모두 필요할 때

의도적으로 해당 타입의 스프링 빈이 다 필요한 경우도 있다.
예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자.

스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다

package hello.core.autowired;

import hello.core.AutoAppConfig;
import hello.core.discount.DiscountPolicy;
import hello.core.member.Grade;
import hello.core.member.Member;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;

import java.util.List;
import java.util.Map;import static org.assertj.core.api.Assertions.assertThat;

public class AllBeanTest {

    @Configuration
    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;

        // 생성자 의존성 주입
        @Autowired
        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }
        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode); //맵에서 파라미터로 전달받은 할인 정책을 찾음
            System.out.println("discountCode = " + discountCode); //할인 정책 이름 출력
            System.out.println("discountPolicy = " + discountPolicy); //할인 정책 객체 출력
            return discountPolicy.discount(member, price); //할인된 가격 출력
        }
    }

    @Test
    void findAllBean() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);
        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");
        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000); //할인 정책에 따라 예상되는 가격이 달라짐
    }
}

 

로직 분석

  • DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy , rateDiscountPolicy 가 주입된다.
  • discount () 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.

 

주입 분석

  • Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • List<DiscountPolicy> : DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

 

참고 - 스프링 컨테이너를 생성하면서 스프링 빈 등록하기

스프링 컨테이너는 생성자에 클래스 정보를 받는다. 여기에 클래스 정보를 넘기면 해당 클래스가 스프링 빈으로 자동 등록된다.
new  AnnotationConfigApplicationContext(AutoAppConfig.class,DiscountService.class);
이 코드는 2가지로 나누어 이해할 수 있다.

- new AnnotationConfigApplicationContext() 를 통해 스프링 컨테이너를 생성한다.
- AutoAppConfig.class , DiscountService.class 를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링 빈으로 등록한다.

정리하면 스프링 컨테이너를 생성하면서, 해당 컨테이너에 동시에 AutoAppConfig , DiscountService 를 스프링 빈으로 자동 등록한다.

 

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

편리한 자동 기능을 기본으로 사용하자

그러면 어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고, 어떤 경우에 설정 정보를 통해서 수동으로 빈을 등록하고,  의존관계도 수동으로 주입해야 하는지 --> 스프링이 나오고 시간이 갈 수록 점점 자동을 선호하는 추세

스프링은 @Component 뿐만 아니라 @Controller , @Service , @Repository 처럼 계층에 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다.

 

거기에 더해서 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.

 

설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이지만, 개
발자 입장에서 스프링 빈을 하나 등록할 때 @Component 만 넣어주면 끝나는 일을 @Configuration 설정 정보에 가서 @Bean 을 적고, 객체를 생성하고, 주입할 대상을 일일이 적어주는 과정은 상당히 번거롭다.

 

또 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다.
그리고 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

 

그러면 수동 빈 등록은 언제 사용하면 좋을까?

애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.

  • 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
  • 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

 

업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 있다. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽다.

 

기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 드러나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는
것이 유지보수 하기 좋다.

 

비즈니스 로직 중에서 다형성을 적극 활용할 때

의존관계 자동 주입 - 조회한 빈이 모두 필요할 때, List, Map을 다시 보자. DiscountService 가 의존관계 자동 주입으로 Map<String, DiscountPolicy> 에 주입을 받는 상황을 생각해보면

여기에 어떤 빈들이 주입될 지, 각 빈들의 이름은 무엇일지 코드만 보고 한번에 쉽게 파악할 수 없다.

자신이 개발한 것이라면 상관없지만, 다른 사람이 개발한 것이라면 처음 봤을 때 이해하기가 힘들 것이다. 

자동 등록을 사용하고 있기 때문에 파악하려면 여러 코드를 찾아봐야 한다.

이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 좋다.

핵심은 이해가 쉬워야 한다.

 

이 부분을 별도의 설정 정보로 만들고 수동으로 등록하면 다음과 같다

@Configuration
public class DiscountPolicyConfig {

     @Bean
     public DiscountPolicy rateDiscountPolicy() {
     	return new RateDiscountPolicy();
     }
     
     @Bean
     public DiscountPolicy fixDiscountPolicy() {
     	return new FixDiscountPolicy();
     }
}

 

설정 정보만 봐도 한눈에 빈의 이름은 물론이고, 어떤 빈들이 주입될지 파악할 수 있다.

그래도 빈 자동 등록을 사용하고 싶으면 파악하기 좋게 DiscountPolicy 의 구현 빈들만 따로 모아서 특정 패키지에 모아두자.

 

참고로 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외다.

 

이런 부분들은 스프링 자체를 잘 이해하고 스프링의 의도대로 잘 사용하는게 중요하다. 스프링 부트의 경우 DataSource 같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.

 

반면에 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


이 글은 어노테이션 개념이 많이 포함하고 있습니다.

아래의 글을 먼저 읽고오면 도움이 됩니다.

2023.07.23 - [Java Category/Java] - [JAVA] 리플렉션(Reflection)과 어노테이션(Annotation)

 

[JAVA] 리플렉션(Reflection)과 어노테이션(Annotation)

이 게시글은 이것이 자바다(저자 : 신용권, 임경균)의 책과 동영상 강의를 참고하여 개인적으로 정리하는 글임을 알립니다. 리플렉션 리플렉션이란 실행 도중에 타입(클래스, 인터페이스 등)을

rebugs.tistory.com

 

컴포넌트 스캔과 의존관계 자동 주입

이전까지 스프링 빈을 등록할 때는 자바 코드의 @Bean이나 XML의 <bean> 등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.

스프링 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생한다. 역시 개발자는 반복을 싫어한다.

그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔(@Component)이라는 기능을 제공한다.

또 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.

 

package hello.core;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

import static org.springframework.context.annotation.ComponentScan.*;

@Configuration
@ComponentScan(
    excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}

 

컴포넌트 스캔을 사용하려면 먼저 @ComponentScan 을 설정 정보에 붙여주면 된다.
기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없다.

참고
@SpringBootApplication 안에 @ComponentScan 이 들어있다.
그렇기 때문에 스프링 부트에서는 @ComponentScan 을 사용하지 않아도 된다.

 

참고
컴포넌트 스캔을 사용하면 @Configuration 이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다.
그래서 excludeFilters 를 이용해서 설정 정보는 컴포넌트 스캔 대상에서 제외했다.
보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않지만, 기존 예제 코드를 최대한 남기고 유지하기 위해서 이 방법을 선택했다.

 

컴포넌트 스캔은 이름 그대로 @Component 어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다. 

참고
@Configuration 또한 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.

 

MemoryMemberRepository @Component 추가

@Component
public class MemoryMemberRepository implements MemberRepository {}

 

RateDiscountPolicy @Component 추가

@Component
public class RateDiscountPolicy implements DiscountPolicy {}

 

MemberServiceImpl @Component, @Autowired 추가

@Component
public class MemberServiceImpl implements MemberService{
    //private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository){
        this.memberRepository = memberRepository;
    }
...

 

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

이제는 이런 설정 정보 자체가 없기 때문에, 의존관계 주입도 이 클래스 안에서 해결해야 한다

 

OrderServiceImpl @Component, @Autowired 추가

@Component
public class OrderServiceImpl implements OrderService{
    //private final MemberRepository memberRepository = new MemoryMemberRepository();

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

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
...

 

@Autowired 를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

@Autowired
의존성 주입(Dependency Injection, DI)을 자동화하기 위해 사용된다.
이는 필요한 의존성(다른 빈들)을 자동으로 주입해주는 역할을 한다.
스프링 컨테이너가 @Autowired가 붙은 필드나 메소드에 해당하는 타입의 빈을 자동으로 찾아서 연결해준다.
이를 통해 개발자는 의존성을 수동으로 연결할 필요 없이, 스프링이 알아서 처리해주는 편리함을 누릴 수 있다
@Component는 클래스를 스프링 빈으로 등록하는 데 사용되며, 클래스 레벨에서 사용된다.
@Autowired는 스프링 컨테이너에서 관리되는 빈들 사이의 의존성을 자동으로 주입하는 데 사용되며, 필드, 생성자, 메소드 레벨에서 사용된다.

 

테스트 코드는 아래와 같다.

package hello.core.scan;

import hello.core.AutoAppConfig;
import hello.core.member.MemberService;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class AutoAppConfigTest {

    @Test
    void basicScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class); //스프링 컨테이너에 등록된 MemberService 빈을 가져옴
        Assertions.assertThat(memberService).isInstanceOf(MemberService.class);  //MemberService 타입이 일치하는지 확인
    }
}
  • AnnotationConfigApplicationContext 를 사용하는 것은 기존과 동일하다.
  • 설정 정보로 AutoAppConfig 클래스를 넘겨준다.
  • 실행해보면 기존과 같이 잘 동작하는 것을 확인할 수 있다.

 

로그를 잘 보면 컴포넌트 스캔이 잘 동작하는 것을 확인할 수 있다

 

@ComponentScan

  • @ComponentScan 은 @Component 가 붙은 모든 클래스를 스프링 빈으로 등록한다.
  • 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
     -> 빈 이름 기본 전략: MemberServiceImpl 클래스 -> memberServiceImpl
     -> 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면 @Component("memberService2") 이런식으로 이름을 부여하면 된다.

 

@Autowired 의존관계 자동 주입

  • 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
     -> getBean(MemberRepository.class) 와 동일하다고 이해하면 된다.

 

  • 생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.

 

탐색 위치와 기본 스캔 대상

탐색할 패키지의 시작 위치 지정

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

@ComponentScan(
	basePackages = "hello.core",
}
  • basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
  • basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도있다.
  • basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
    만약 지정하지 않으면 @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
권장하는 방법
즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것 이다.
최근 스프링 부트도 이 방법을 기본으로 제공한다

 

프로젝트가 다음과 같이 구조가 되어 있으면

  • com.hello
  • com.hello.serivce
  • com.hello.repository

com.hello 프로젝트 시작 루트, 여기에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 어노테이
션을 붙이고, basePackages 지정은 생략한다.

 

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

그리고 프로젝트 메인 설정정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다 생각한다.

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

@SpringBootApplication 안에 @ComponentScan 이 들어있다.

그렇기 때문에 스프링 부트에서는 @ComponentScan 을 사용하지 않아도 된다.

 

컴포넌트 스캔 기본 대상

컴포넌트 스캔은 @Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.

  • @Component : 컴포넌트 스캔에서 사용
  • @Controller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용

위 어노테이션의 선언을 보면 @Component를 포함하고 있는 것을 알 수 있다.

@Component
public @interface Controller {
}
@Component
public @interface Service {
}
@Component
public @interface Configuration {
}
노테이션에는 상속관계라는 것이 없다.
그래서 이렇게 어노테이션이 특정 어노테이션을 포함하고 있는 것을 인식할 수 있는 것은 자바 언어가 지원하는 기능은 아니고, 스프링이 지원하는 기능이다.

 

컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.

  • @Controller : 스프링 MVC 컨트롤러로 인식
  • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Configuration : 앞서 보았듯이 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
  • @Service : @Service 는 특별한 처리를 하지 않는다. 대신 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다
참고
useDefaultFilters 옵션은 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.

 

필터

includeFilters 와 excludeFilters

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.

 

@ComponentScan(
 includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
 excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
  • includeFilters 에 MyIncludeComponent 어노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.
  • excludeFilters 에 MyExcludeComponent 어노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.

 

컴포넌트 스캔 대상에 추가할 어노테이션

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}

 

컴포넌트 스캔 대상에서 제외할 어노테이션

package hello.core.scan.filter;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}

 

컴포넌트 대상에 추가할 클래스

package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {
}
  • @MyIncludeComponent 적용

 

컴포넌트 대상에 제외할 클래스

package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {
}
  • @MyExcludeComponent 적용

 

설정 정보와 전체 테스트 코드

package hello.core.scan.filter;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.context.annotation.ComponentScan.*;

public class ComponentFilterAppConfigTest {
    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )

    static class ComponentFilterAppConfig {
    }

    @Test
    void filterScan(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);

        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull(); // BeanA 클래스는 컴포넌트 대상에 추가했으므로 오류 발생 X

        //ac.getBean("beanB", BeanB.class) 를 실행하면 NoSuchBeanDefinitionException 예외가 발생해야함
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean("beanB", BeanB.class));
    }

}

 

FilterType 옵션

FilterType은 5가지 옵션이 있다.

  • ANNOTATION : 기본값, 애노테이션을 인식해서 동작한다.
    ex) org.example.SomeAnnotation
  • ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작한다.
    ex) org.example.SomeClass
  • ASPECTJ : AspectJ 패턴 사용
    ex) org.example..*Service+
  • REGEX : 정규 표현식
    ex) org\.example\.Default.*
  • CUSTOM : TypeFilter 이라는 인터페이스를 구현해서 처리
    ex) org.example.MyTypeFilter

 

 BeanA도 빼고 싶으면 다음과 같이 추가하면 된다

@ComponentScan(
     includeFilters = {
     	@Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
     },
     excludeFilters = {
     	@Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
     	@Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
     }
)
참고
@Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다.
excludeFilters 는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다.
특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 옵션을 변경하면서 사용하기 보다 는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.

 

중복 등록과 충돌

컴포넌트 스캔에서 동일한 빈 이름을 등록하면 충돌이 일어난다.

 

다음 두가지 상황이 있다.

  1. 자동 빈 등록 vs 자동 빈 등록
  2. 수동 빈 등록 vs 자동 빈 등록

 

 

자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.
ConflictingBeanDefinitionException 예외 발생

이러한 경우는 임의로 자동으로 등록된 빈의 이름을 바꿨을 때 나타난다.

 

수동 빈 등록 vs 자동 빈 등록

테스트 코드에서 수동 빈 등록과 자동 빈 등록에서 빈 이름이 충돌되면 수동 빈 등록이 우선권을 가진다. (수동 빈이 자동 빈을 오버라이딩 해버린다.)

물론 개발자가 의도적으로 이런 결과를 기대했다면, 자동 보다는 수동이 우선권을 가지는 것이 좋다.

 

하지만 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는 여러 설정들이 꼬여서 이런 결과가 만들어지는 경우가 대부분이다.
그러면 정말 잡기 어려운 버그가 만들어진다.

항상 잡기 어려운 버그는 애매한 버그다.
그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다

 

수동 빈 등록, 자동 빈 등록 충돌시 스프링 부트 에러

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

 

이는 spring.main.allow-bean-definition-overriding 가 기본적으로 false로 설정되어 있기 때문이다.

이 파일에서 spring.main.allow-bean-definition-overriding=ture로 설정하면 테스트 코드의 결과 대로 수동 등록 빈이 자동 등록 빈을 오버라이딩한다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


웹 애플리케이션과 싱글톤 패턴

  • 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.
  • 대부분의 스프링 애플리케이션은 웹 애플리케이션이다. 물론 웹이 아닌 애플리케이션 개발도 얼마든지 개발할 수 있다.
  • 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다.

 

만약 싱글톤 패턴이나 싱글톤 컨테이너를 사용하지 않는다면 아래의 그림과 같이 사용자가 요청을 할 때마다 객체가 생성될 것이다.

 

싱글톤 패턴에 대한 내용은 아래의 글을 참고해주세요.

2023.01.04 - [Java Category/Java] - [JAVA] 싱글톤(Singleton), final 필드와 상수

 

하지만 싱글톤 패턴의 문제점이 있다.

문제점의 내용은 아래와 같다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다.

 

싱글톤 컨테이너

스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(1개만 생성)으로 관리한다.

스프링 빈이 바로 싱글톤으로 관리되는 빈이다.

 

싱글톤 컨테이너의 특징

  • 스프링 컨테이너는 싱글턴 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 컨테이너 생성 과정에서 컨테이너는 객체를 하나만 생성해서 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스
    트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
  • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
  • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다

 

스프링 컨테이너를 사용하는 테스트 코드

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    //1. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    //2. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);
    //참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);
    //memberService1 == memberService2
    assertThat(memberService1).isSameAs(memberService2);
}

싱글톤 패턴을 직접 작성하지 않아도 스프링 컨테이너에 객체가 싱글톤으로 관리되고 있는 것을 확인할 수 있다.

이는 싱글톤 컨테이너가 객체를 관리하기 때문이고, 싱글톤 컨테이너를 적용후 스프링 DI 컨테이너의 그림은 아래와 같다.

스프링 컨테이너 덕분에 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 재사용할 수 있다.

참고
스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다.
요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. -> 빈 스코프

 

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • 무상태(stateless)로 설계해야 한다.
  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 큰 장애가 발생할 수 있다.

 

상태를 유지할 경우 발생하는 문제점 예시

package hello.core.singleton;
public class StatefulService {
    private int price; //상태를 유지하는 필드
    public void order(String name, int price) {
        System.out.println("name = " + name + " price = " + price);
        this.price = price; //여기가 문제!
    }
    public int getPrice() {
        return price;
    }
}

package hello.core.singleton;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
public class StatefulServiceTest {
    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);
        
        //ThreadA: A사용자 10000원 주문
        statefulService1.order("userA", 10000);
        //ThreadB: B사용자 20000원 주문
        statefulService2.order("userB", 20000);
        //ThreadA: 사용자A 주문 금액 조회
        
        int price = statefulService1.getPrice();
        //ThreadA: 사용자A는 10000원을 기대했지만, 기대와 다르게 20000원 출력
        System.out.println("price = " + price);
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }
}

ThreadA가 사용자A 코드를 호출하고 ThreadB가 사용자B 코드를 호출한다 가정하자.

StatefulService 의 price 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.

사용자A의 주문금액은 10000원이 되어야 하는데, 20000원이라는 결과가 나왔다.

실무에서 이런 경우를 종종 보는데, 이로인해 정말 해결하기 어려운 큰 문제들이 터진다.

스프링 빈은 항상 무상태(stateless)로 설계해야 한다.

 

@Configuration과 싱글톤

package hello.core;

@Configuration
public class AppConfig {

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

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

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

 

설정 정보를 저장하는 AppConifg 파일의 코드를 보면

  • memberService 빈을 만드는 코드를 보면 memberRepository() 를 호출한다.
    -> 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.
  • orderService 빈을 만드는 코드도 동일하게 memberRepository() 를 호출한다.
    -> 이 메서드를 호출하면 new MemoryMemberRepository() 를 호출한다.

 

스프링 컨테이너에 스프링 빈을 등록할 때 설정 정보를 저장하는 파일을 확인하면서 @Bean 어노테이션이 있는 메서드를 모두 호출한다.

그렇기 때문에 자바 코드 상으로는 각각 다른 2개의 MemoryMemberRepository 가 생성되면서 싱글톤이 깨지는 것 처럼 보인다.

검증을 하기위해서 MemberServiceImpl 클래스와 OrderServiceImpl 클래스에 아래의 코드를 삽입한다.

//테스트 용도
 public MemberRepository getMemberRepository() {
 	return memberRepository;
 }

 

package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.*;
public class ConfigurationSingletonTest {
    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);
        
        //모두 같은 인스턴스를 참고하고 있다.
        System.out.println("memberService -> memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService -> memberRepository = " + orderService.getMemberRepository());
        System.out.println("memberRepository = " + memberRepository);
        
        //모두 같은 인스턴스를 참고하고 있다.
        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
}

자바 코드의 내용으로는 MemoryMemberRepository 객체가 두 개가 생성되어야 하는데, 테스트 코드의 결과는 모두 하나의 객체를 가리키고 있다.

 

그렇다면 AppConfig 파일의 @Bean 어노테이션이 있는 메서드가 두 번 호출이 되지 않는 것일까? 라고 의심해볼 수 있다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        //1번
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public OrderService orderService() {
        //1번
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public MemberRepository memberRepository() {
        //2번? 3번?
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

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

스프링 컨테이너가 각각 @Bean을 호출해서 스프링 빈을 생성한다.

그래서 memberRepository() 는 다음과 같이 총 3번이 호출되어야 한다.

1. 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출
2. memberService() 로직에서 memberRepository() 호출
3. orderService() 로직에서 memberRepository() 호출

하지만 결과는 아래와 같이 memberRepository()는 한 번만 호출된다.

이는 @Configuration 어노테이션과 관련이 있다.

 

@Configuration과 바이트코드 조작

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다.

그런데 스프링이 자바 코드까지 어떻게 하기는 어렵다.

저 자바 코드를 보면 분명 3번 호출되어야 하는 것이 맞다.

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

모든 비밀은 @Configuration 을 적용한 AppConfig 에 있다.

@Test
void configurationDeep() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    //AppConfig도 스프링 빈으로 등록된다.
    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
    //출력: bean = class hello.core.AppConfig$$SpringCGLIB$$0
}

AnnotationConfigApplicationContext 에 파라미터로 넘긴 값은 스프링 빈으로 등록된다.

그래서 AppConfig 도 스프링 빈이 된다.

AppConfig 스프링 빈을 조회해서 정보를 출력하면 아래와 같다.

 

순수한 클래스라면 class hello.core.AppConfig 로 출력되어야 한다.

그런데 예상과는 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다.

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

그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다.

아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다.(실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡하다.)

 

AppConfig@CGLIB 예상 코드

@Bean
public MemberRepository memberRepository() {
 
     if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
        return 스프링 컨테이너에서 찾아서 반환;
     } else { //스프링 컨테이너에 없으면
         기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록
         return 반환
     }
}

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
덕분에 싱글톤이 보장되는 것이다.

참고
AppConfig@CGLIB는 AppConfig의 자식 타입이므로, AppConfig 타입으로 조회 할 수 있다.
스프링 빈을 조회할 때 부모 타입으로 조회하면 자식 타입도 함께 조회하기 때문이다.

즉, 부모는 등록되지 않았고 자식만 스프링 컨테이너에 등록된 상태이기 때문에 부모 타입을 조사하면 자식 타입만 조회되는 것이다.

 

@Configuration 을 적용하지 않고, @Bean 만 적용하면 어떻게 될까?

@Configuration 을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만, 만약 @Bean만 적용하면 싱글톤이 보장되지 않는다.
이유는 CGLIB라는 바이트코드 조작 라이브러리를 사용하지 않기 때문이다.

@Bean 어노테이션으로 해당 객체들이 스프링 빈으로 등록되지만, 싱글톤은 보장하지 않는다.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


스프링 컨테이너 생성

  • ApplicationContext 를 스프링 컨테이너라 한다.
  • ApplicationContext 는 인터페이스이다.
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
  • 이전에 AppConfig 를 사용했던 방식이 어노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든 것이다.

 

자바 설정 클래스를 기반으로 스프링 컨테이너( ApplicationContext )를 만들어보자.

//스프링 컨테이너 생성
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • new AnnotationConfigApplicationContext(AppConfig.class);
    이 클래스는 ApplicationContext 인터페이스의 구현체이다. 즉, Appconfig를 구현 클래스로 설정한 것이다.
더 정확히는 스프링 컨테이너를 부를 때 BeanFactory , ApplicationContext 로 구분해서 이야기 한다.
BeanFactory가 최상위 인터페이스이고 ApplicationContext이 바로 하위 인터페이스이다.
BeanFactory 를 직접 사용하는 경우는 거의 없으므로 일반적으로 ApplicationContext 를 스프링 컨테이너라 한다.

 

스프링 컨테이너의 생성 과정

 

1. 스프링 컨테이너 생성

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

 

2. 스프링 빈 등록

  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
Bean 이름
빈 이름은 메서드 이름을 사용한다.
빈 이름을 직접 부여할 수 도 있다. -> @Bean(name="memberService2")
주의
빈 이름은 항상 다른 이름을 부여해야 한다.
같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버 리거나 설정에 따라 오류가 발생한다.

 

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

 

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

  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다.
  • 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다. 

 

참고
스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다.
그런데 이렇게 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.
여기서는 이해를 돕기 위해 개념적으로 나누어 설명한 것이다.

 

컨테이너에 등록된 빈(bean) 조회

모든 빈 출력하기

  • 실행하면 스프링에 등록된 모든 빈 정보를 출력할 수 있다.
  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회한다.
  • ac.getBean() : 빈 이름으로 빈 객체(인스턴스)를 조회한다.
public class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean(){
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }
}

 

spring이 사용하는 빈과 직접 등록한 빈이 모두 출력되었다.

 

AnnotationConfigApplicationContext.getbean()

getBean()메소드는 이 컨텍스트 안에서 관리되고 있는 빈(bean)을 검색하고 반환하는 데 사용된다.
빈은 Spring에서 관리하는 객체로, 의존성 주입을 통해 다른 빈이나 기능에 접근하고, Spring 프레임워크의 생명주기 관리를 받는다.


빈 이름으로 검색
public Object getBean(String name) throws BeansException​

name: 검색하고자 하는 빈의 이름이다.
이 이름은
@Bean 어노테이션을 사용하거나 XML 설정 파일에서 정의된다.

빈 타입으로 검색

public <T> T getBean(Class<T> requiredType) throws BeansException

requiredType: 검색하고자 하는 빈의 타입이다.
이 메소드는 지정된 타입과 호환되는 빈을 반환한다.


빈 이름과 타입으로 검색
public <T> T getBean(String name, Class<T> requiredType) throws BeansException​

name: 검색하고자 하는 빈의 이름이다.
requiredType: 검색하고자 하는 빈의 타입이다.
이 메소드는 지정된 이름과 타입 모두에 해당하는 빈을 반환한다.

 

애플리케이션 빈 출력하기

  • 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력해보자.
  • 스프링이 내부에서 사용하는 빈은 getRole() 로 구분할 수 있다.
  • ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
  • ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean(){
    String[] beanDefinitionNames = ac.getBeanDefinitionNames();
    for (String beanDefinitionName : beanDefinitionNames) {
        BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

        //Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
        //Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
        if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION){
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName + " object = " + bean);
        }
    }
}

 

스프링 빈 조회 

기본

package hello.core.beanfind;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import static org.assertj.core.api.Assertions.*;

public class ApplicationContextBasicFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름과 타입으로 조회")
    void findBeanByName(){
        MemberService memberService = ac.getBean("memberService", MemberService.class); //여기서 MemberService는 인터페이스
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름 없이 타입으로 조회")
    void findBeanByType() {
        MemberService memberService = ac.getBean(MemberService.class); //여기서 MemberService는 인터페이스
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class); //여기서 MemberServiceImpl은 인터페이스의 구현체
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    //실패 테스트 케이스
    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        //ac.getBean("xxxxx", MemberService.class);
        //ac.getBean("xxxxx", MemberService.class) 이 코드를 실행했을 때 NoSuchBeanDefinitionException 예외가 발생하는지 테스트
        Assertions.assertThrows(NoSuchBeanDefinitionException.class, () ->
                ac.getBean("xxxxx", MemberService.class));
    }
}
참고
구체 타입으로 조회하면 변경시 유연성이 떨어진다.

스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법

  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)

 

조회 대상 스프링 빈이 없으면 예외 발생
-> NoSuchBeanDefinitionException: No bean named 'xxxxx' available

 

동일한 타입이 둘 이상

  • 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 
  • 이때는 빈 이름을 지정하자. ac.getBeansOfType() 을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.
package hello.core.beanfind;

import hello.core.member.MemberRepository;
import hello.core.member.MemoryMemberRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

public class ApplicationContextSameBeanFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(sameBeanConfig.class);

    @Configuration
    static class sameBeanConfig{

        @Bean
        public MemberRepository memberRepository1(){
            return new MemoryMemberRepository();
        }

        @Bean
        public MemberRepository memberRepository2(){
            return new MemoryMemberRepository();
        }
    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByTypeDuplicate() {
        //MemberRepository bean = ac.getBean(MemberRepository.class); - MemberRepository에 리턴하는 값이 중복된 것이 있으므로 오류 발생

        //ac.getBean(MemberRepository.class) 코드를 실행하면 NoUniqueBeanDefinitionException 예외가 발생하는지 테스트
        Assertions.assertThrows(NoUniqueBeanDefinitionException.class, () ->
                ac.getBean(MemberRepository.class));
    }

    @Test
    @DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByName(){
        MemberRepository memberRepository = ac.getBean("memberRepository1", MemberRepository.class);
        assertThat(memberRepository).isInstanceOf(MemberRepository.class);
    }

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType(){
        Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + beansOfType + " value = " +beansOfType.get(key));
            assertThat(beansOfType.size()).isEqualTo(2);
        }
    }
    //key = {memberRepository1=hello.core.member.MemoryMemberRepository@7a356a0d, memberRepository2=hello.core.member.MemoryMemberRepository@c827db} value = hello.core.member.MemoryMemberRepository@7a356a0d
    //key = {memberRepository1=hello.core.member.MemoryMemberRepository@7a356a0d, memberRepository2=hello.core.member.MemoryMemberRepository@c827db} value = hello.core.member.MemoryMemberRepository@c827db
}
static class는 아래의 링크에서 정적 멤버 클래스 부분을 참고하면 된다.

2023.01.12 - [Java Category/Java] - [JAVA] 중첩 클래스(Nested Class)

 

스프링 빈 조회 - 상속 관계

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

package hello.core.beanfind;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ApplicationContextExtendsFindTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Configuration
    static class TestConfig {
        @Bean
        public DiscountPolicy rateDiscountPolicy() {
            return new RateDiscountPolicy();
        }
        @Bean
        public DiscountPolicy fixDiscountPolicy() {
            return new FixDiscountPolicy();
        }
    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        //DiscountPolicy bean = ac.getBean(DiscountPolicy.class); //DiscountPolicy를 구현하는 클래스가 2개임

        //ac.getBean(DiscountPolicy.class) 코드를 실행하면 NoUniqueBeanDefinitionException 예외가 발생하는지 테스트
        assertThrows(NoUniqueBeanDefinitionException.class, () ->
                ac.getBean(DiscountPolicy.class));
    }
    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class); //DiscountPolicy 구현하는 클래스 중 특정 클래스 선택
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }
    
    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }
    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value=" + beansOfType.get(key));
        }
    }
    //key = rateDiscountPolicy value=hello.core.discount.RateDiscountPolicy@12477988
    //key = fixDiscountPolicy value=hello.core.discount.FixDiscountPolicy@2caf6912

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value=" + beansOfType.get(key));
        }
    }
    //key = org.springframework.context.annotation.internalConfigurationAnnotationProcessor value=org.springframework.context.annotation.ConfigurationClassPostProcessor@377c68c6
    //key = org.springframework.context.annotation.internalAutowiredAnnotationProcessor value=org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor@538cd0f2
    //key = org.springframework.context.annotation.internalCommonAnnotationProcessor value=org.springframework.context.annotation.CommonAnnotationBeanPostProcessor@238ad8c
    //key = org.springframework.context.event.internalEventListenerProcessor value=org.springframework.context.event.EventListenerMethodProcessor@430fa4ef
    //key = org.springframework.context.event.internalEventListenerFactory value=org.springframework.context.event.DefaultEventListenerFactory@1761de10
    //....
}

 

BeanFactory와 ApplicationContext

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • getBean() 을 제공한다.
  • 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다.

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • BeanFactory 의 기본적인 기능 이외에 부가적인 기능을 제공

 

ApplicatonContext가 제공하는 부가기능

  • 메시지소스를 활용한 국제화 기능 : 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수 : 로컬, 개발, 운영등을 구분해서 처리
  • 애플리케이션 이벤트 : 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회 : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
정리
-ApplicationContext는 BeanFactory의 기능을 상속받는다.
-ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다.
-BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
-BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다

 

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

스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있게 유연하게 설계되어 있다.

자바 코드, XML, Groovy 등등

어노테이션 기반 자바 코드 설정 사용

  • new AnnotationConfigApplicationContext(AppConfig.class)
  • AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다.

 

XML 설정 사용

  • 최근에는 스프링 부트를 많이 사용하면서 XML기반의 설정은 잘 사용하지 않는다. 아직 많은 레거시 프로젝트 들이 XML로 되어 있고, 또 XML을 사용하면 컴파일 없이 빈 설정 정보를 변경할 수 있는 장점도 있으므로 한번쯤 배워두는 것도 괜찮다.
  • GenericXmlApplicationContext 를 사용하면서 xml 설정 파일을 넘기면 된다.

package hello.core.xml;

import hello.core.member.MemberService;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

import static org.assertj.core.api.Assertions.*;
public class XmlAppContext {
    @Test
    void xmlAppContext() {
    	//xml 파일을 기반으로 스프링 컨테이너에 자바빈을 등록
        ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");

        MemberService memberService = ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

xml 기반의 스프링 빈 설정 정보

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="memberService" class="hello.core.member.MemberServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
    </bean>
    
    <bean id="memberRepository" class="hello.core.member.MemoryMemberRepository" />
    
    <bean id="orderService" class="hello.core.order.OrderServiceImpl">
        <constructor-arg name="memberRepository" ref="memberRepository" />
        <constructor-arg name="discountPolicy" ref="discountPolicy" />
    </bean>
    
    <bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy" />
</beans>

xml 기반의 appConfig.xml 스프링 설정 정보와 자바 코드로 된 AppConfig.java 설정 정보를 비교해보면 거의 비슷하다는 것을 알 수 있다.

스프링 공식 레퍼런스 문서 주소는 아래와 같다.

https://spring.io/projects/spring-framework

 

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

스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까?

  • 그 중심에는 BeanDefinition 이라는 추상화가있다.
  • 쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것이다.
    -XML을 읽어서 BeanDefinition을 만들면 된다.
    -자바 코드를 읽어서 BeanDefinition을 만들면 된다.
    -스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다.
  • BeanDefinition 을 빈 설정 메타정보라 한다.
    -@Bean , <bean> 당 각각 하나씩 메타 정보가 생성된다.
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.

 

  • AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.
  • GenericXmlApplicationContext 는 XmlBeanDefinitionReader 를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition 을 생성한다.
  • 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition 을 생성하면 된다.

 

BeanDefinition 정보

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한 생성을 지연 처리 하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
package hello.core.beandefinition;
import hello.core.AppConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
public class BeanDefinitionTest {
    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); //팩토리 메서드를 통해 스프링 컨테이너에 등록
    //GenericXmlApplicationContext ac = newGenericXmlApplicationContext("appConfig.xml"); //직접 스프링 빈을 스프링 컨테이너에 등록

    @Test
    @DisplayName("빈 설정 메타정보 확인")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                System.out.println("beanDefinitionName" + beanDefinitionName + " beanDefinition = " + beanDefinition);
            }
        }
    }
}