이 글은 인프런 김영한님의 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) : 요청 수락
클라이언트에서 먼저 SYN 메시지를 서버에게 접속을 허락해달라고 요청한다.
서버는 접속을 수락하고 ACK 메시지를 클라이언트 한테 보낼 때 서버도 접속을 허락해달라고 SYN 메시지와 함께 보낸다.
클라이언트가 접속을 수락하고 서버에서 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 번호를 찾아서 연결하면 된다.
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.
빈 스코프
스프링 빈은 기본적으로 싱글톤으로 생성되고, 싱글톤 스코프이기 때문에 스프링 컨테이너의 시작과 함께 스프링 컨테이너가 종료될 때 까지 유지된다.
스코프는 번역 그대로 빈이 존재할수 있는 범위를 뜻한다.
스프링은 다음과 같은 다양한 스코프를 지원한다.
싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
웹 관련 스코프
request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
빈 스코프는 다음과 같이 지정할 수 있다.
컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토타입 스코프
프로토타입 스코프의 개념
싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다.
프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
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
@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를 사용해보자.
이 글은 인프런 김영한님의 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 를 사용하자.
막상 개발을 해보면, 대부분이 다 불변이고, 그래서 다음과 같이 필드에 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 함께 사용하면 기능은 다 제공하면서, 코드는 깔끔하게 사용할 수 있다.
스프링 컨테이너에 스프링 빈이 같은 타입으로 2개가 등록되어 있고, 타입으로 조회하면 모호성이 생긴다.
@Autowired 는 타입(Type)으로 조회한다.
@Autowired
private DiscountPolicy discountPolicy
타입으로 조회하기 때문에, 마치 다음 코드와 유사하게 동작한다.
ac.getBean(DiscountPolicy.class)
타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
@Autowired 매칭
@Autowired 는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다.
예를 들어 discountPolicy 인터페이스의 구현 클래스가 fixDiscountPolicy와 rateDiscountPolicy 두 개있는데, 그 중에서 rateDiscountPolicy 를 사용하고 싶다면 이름만 필드나 파리미터의 이름을 해당 빈 이름으로 변경하면 자동으로 어떤 구현 클래스를 사용할지 정해준다.
@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가 우선적으로 적용된다.
코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자. 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary 를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier 를 지정해서 명시적으로 획득 하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다. 물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier 를 지정해주는 것은 상관없다.
우선순위
@Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 따라서 여기서도 @Qualifier 가 우선권이 높다.
어노테이션 직접 만들기
@Qualifier("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 가 주입된다.
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 같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데, 이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.
반면에 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다.
컴포넌트 스캔을 사용하려면 먼저 @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 옵션은 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.
참고 @Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters 는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다. 특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 옵션을 변경하면서 사용하기 보다 는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장한다.
중복 등록과 충돌
컴포넌트 스캔에서 동일한 빈 이름을 등록하면 충돌이 일어난다.
다음 두가지 상황이 있다.
자동 빈 등록 vs 자동 빈 등록
수동 빈 등록 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로 설정하면 테스트 코드의 결과 대로 수동 등록 빈이 자동 등록 빈을 오버라이딩한다.
스프링 컨테이너는 싱글톤 패턴의 문제점을 해결하면서, 객체 인스턴스를 싱글톤(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();
}
}
이 글은 인프런 김영한님의 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
}
그래서 모든 자바 객체의 최고 부모인 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을 읽어서 BeanDefinition을 만들면 된다. -자바 코드를 읽어서 BeanDefinition을 만들면 된다. -스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면 된다.
BeanDefinition 을 빈 설정 메타정보라 한다. -@Bean , <bean> 당 각각 하나씩 메타 정보가 생성된다.
스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.
GenericXmlApplicationContext 는 XmlBeanDefinitionReader 를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition 을 생성한다.
새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition 을 생성하면 된다.
BeanDefinition 정보
BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음)
factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig