인터페이스 InitializingBean, DisposableBean 방법
NetworkClient 수정
public class NetworkClient implements InitializingBean, DisposableBean { //InitializingBean 구현 -> 초기화 콜백관련 DisposableBean 소멸 콜백관련
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);
}
@Override //오버라이드
public void afterPropertiesSet() throws Exception { //의존관계 주입이 끝나면 실행
connect(); //초기화가 다되고나서 연결을 시도한다.
call("초기화 연결 메시지");
}
@Override
public void destroy() throws Exception { //소멸전 콜백 (빈이 소멸할때 실행)
disconnect(); //빈 소멸전 연결을 끊어준다.
}
}
InitializingBean, DisposableBean 인터페이스를 이용해서
afterPropertiesSet을 오버라이드해서 초기화 후 할 행동들을 정의하고
destory()를 오버라이드해서 소멸전 할 행동들을 정의한다.
테스트
생성자가 호출될때까지는 아무런 url이 등록안되어있고, 초기화가 완료된후 afterPropertiesSet에 있는 내용이 실행된다.
그리고 빈이 소멸될때(컨테이너가 종료되면서 빈이 하나씩 종료된다.) destory안의 내용이 실행된다.
하지만 인터페이스를 사용하는 초기화,종료방법은 거의 사용하지않는다.
빈 등록 초기화, 소멸 메소드 지정
NetoworkClient 수정
인터페이스 구현했던거 지우고, init()이랑 close()라는 초기화 메소드, 소멸 메소드를 만들어준다.
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);
}
public void init() {
System.out.println("init 실행");
connect();
call("초기화 연결 메시지");
}
public void close() {
System.out.println("close 실행");
disconnect();
}
}
@Configuration
static class LifeCycleConfig {
@Bean(initMethod = "init", destroyMethod = "close")
public NetworkClient networkClient() {
NetworkClient networkClient = new NetworkClient();
networkClient.setUrl("http://hell-spring.dev"); //스프링빈으로 등록하기전에 url 설정해준다.
return networkClient;
}
}
}
위와 같이 @Bean 등록할때 초기화메소드, 소멸메소드를 지정해준다.
종료 메소드 추론
어노테이션을 이용 ( @PostConstruct, @PreDestory)
이름부터 생성된후에, 소멸되기이전에 로 네이밍 되어있어서 빈 생명주기 콜백용 어노테이션임을 알 수 있다.
@PostConstruct
public void init() {
System.out.println("init 실행");
connect();
call("초기화 연결 메시지");
}
@PreDestroy
public void close() {
System.out.println("close 실행");
disconnect();
}
아까 만들었던 메소드들에 각각 어노테이션을 붙이기만 하면 끝이다.
빈 스코프
빈 스코프 : 빈이 존재 할 수 있는 범위
빈 스코프는 다음과 같이 지정 할 수 있다.
자동 빈 등록시
@Scope("prototype")
@Component
public class HelloBean {}
수동 빈 등록시
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토타입 스코프
싱글톤의 빈 요청의 경우
1. 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 본인이 관리하는 스프링빈을 반환한다.
3. 이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링빈을 반환한다. ( 싱글톤이니까)
만약 프로토타입 스코프의 빈을 스프링컨테이너에 요청했을경우
1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고 ,필요한 의존관계를 주입한다(생성 및 DI, 초기화 메소드가 있다면 그거까지 진행)
3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환해준다.
정리
여기서 핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것이다.
클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에게 있다.
그래서 @PreDestroy같은 종료(소멸)메소드가 동작하지 않는다 [해당 빈이 스프링 컨테이너에 없으니까, 스프링 컨테이너가 종료될때 종료되는 빈 리스트에 없다 => 소멸메소드가 동작 x]
싱글톤 빈 테스트
public class SingletonTest {
@Test
void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
SingletonBean.class); //스프링컨테이너 생성할때, 빈 직접등록
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close(); // Annotation~에만 있는거라 Annotatino으로 객체를 생성. ApplicationContext 인터페이스변수로 받으려고하니 close가 없었다.
//ac.close()를 통해 스프링컨테이너를 닫고, 스프링컨테이너가 닫히면서 빈이 소멸되는걸 확인하기 위해
}
@Scope("singleton")
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("init 실행");
}
@PreDestroy
public void destory() {
System.out.println("detroy 실행");
}
}
}
close()메소드로 스프링컨테이너를 닫아, 소멸메소드를 실행테스트 할 수 있었다.
프로토타입 스코프 빈 테스트
public class PrototypeTest {
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(
PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
ac.close();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("init 실행");
}
@PreDestroy
public void destroy() {
System.out.println("destory 실행");
}
}
}
init이 두번 실행됬다는것은 -> ac.getBean할때 즉 조회할때 생성되었다는것을 의미한다.
그리고 ac.close()로 인해 스프링컨테이너가 종료되었는데도 불구하고, 소멸메소드가 실행되지않았다.
프로토타입 스코프의 빈은 스프링컨테이너가 초기화까지만 진행하고 클라이언트에게 객체 인스턴스를 반환해주기 때문이다.
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
스프링컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체를 생성해서 반환해준다.
하지만 싱글톤 빈과 함께 사용할때는 의도한대로 잘 동작하지않으므로 주의해야 한다.
먼저 스프링컨테이너에 프로토타입 빈을 직접 요청하는 예제를 보자..
당연한 이야기이다.
싱글톤 빈에서 프로토타입 빈 사용
싱글톤 빈안에서 의존관계 주입으로 프로토타입 빈을 사용할때 어떻게 될까.
이번에는 clientBean 이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는
예를 보자.
clientBean을 생성할때 프로토타입빈이 생성되서 주입된것이다.
그래서 clientBean에 있는 프로토타입빈도 같은 객체이다.
static class ClientBean {
private final PrototypeBean prototypeBean; //클라이언트빈이 생성될때, "그때" 프로토타입빈을 생성해서 주입시켜준다.
@Autowired
public ClientBean(PrototypeBean prototypeBean) { //클라이언트빈이 생성될때, "그때" 프로토타입빈을 생성해서 주입시켜준다.
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
public class SingletonWithPrototypeTest1 {
@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(); // 같은 프로토타입의 count를 사용하는것이다.
assertThat(count2).isEqualTo(2);
}
}
우리의 의도가 만약 호출할때마다
프로토타입 빈을 새로 만들어서 사용하고 싶다면 어떻게 해야할까?
가장 단순한 방법 : 싱글톤빈이 프로토타입빈을 사용할때마다 스프링컨테이너에 새로 요청한다.
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
ObjectFactory, ObjectProvider
static class ClientBean {
private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider;
//ObjectProvider는 스프링컨테이너에 있는 빈을 가져다줄때 쓰는 객체인거같다.
@Autowired
public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanObjectProvider) {
this.prototypeBeanObjectProvider = prototypeBeanObjectProvider;
}
public void logic() {
PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject(); //getObject()를 통해 스프링컨테이너에서 해당 빈을 찾아 반환하는데
//프로토타입 스코프의 빈은 반환할때 새로운 객체 인스턴스를 생성해서 반환해주니까 매번 새로운 프로로타입빈이 반환될것이다.
}
}
특징
JSR-330 Provider
스프링을 사용하지않는 자바표준.
implementation 'javax.inject:javax.inject:1'
static class ClientBean {
private Provider<PrototypeBean> prototypeBeanProvider;
public ClientBean(Provider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public void logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.get(); //Provider의 메소드 get을 이용해서 받아올 수있다. Provider를 사용하기 위해서 라이브러리를
//추가해줘야하는걸 잊지말자.
}
}
특징
웹 스코프
request : 각각의 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다
"요청 하나가 들어오고 나갈때까지 유지되는 스코프"
클라이언트A와 B가 동시에 request했다고 했을때,
컨트롤러는 각각 request scope를 가진 빈 인스턴스객체를 생성해주고 , 서비스 또한 처리를 각 request scope에다 해준다.
즉 request가 들어올때 request scope를 가진 빈 인스턴스를 생성해주고 , 요청에 대한 응답이 나가면 destory(소멸)된다.
request scope 예제 만들기
웹 환경추가
웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하게끔 라이브러리를 추가해주자.
build.gradle에 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
이제 @SpringbootApplication 어노테이션이 있는, 스프링 부트인 DemoApplication.java를 실행해보면 스프링과 함께 웹어플리케이션이 실행되는것을 확인할 수 있다.
웹 기술을 이용하여 서버가 띄워진것을 확인가능하고 http://127.0.0.1:8080/ 또는 localhost:8080으로 서버가 띄어진걸 확인 가능하다.
request 스코프 예제 개발
동시에 여러 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
UUID -> Universally unique identifier , 고유성이 보장되는 id를 만들기위한 표준 규약 ( 범용 고유 식별자)
아무튼 request 별로 구별하기 위한 식별자라고 생각하면 된다. ( 같은 request는 같은 uuid를 가진다)
스코프는 약간 빈이 관리되는 생명주기를 나타내는, 관리기간을 나타내는 느낌인거같다. ( 스코프는 빈이 존재할 수 있는 범위라는 뜻을 가지니까)
MyLogger.java
@Component //자동빈등록(컴포넌트스캔)을 위함
@Scope(value = "request") //request 스코프를 가지는 빈, vale = ~ 이 부분은 빼고 "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 //이 빈이 초기화 되고 난후 아래의 메소드가 실행된다. -> request스코프니까 http요청이 들어와서 빈 인스턴스가 생성되고 난후 실행될것이다.
public void init() {
uuid = UUID.randomUUID().toString(); //UUID.randomUUID().toString()를 이용해서 전세계적으로 유니크한 아이디를 받게된다.
System.out.println(
"[" + uuid + "]" + "request scope bean created" + this);
}
@PreDestroy //request 스코프이니까 요청이 끝나고,빈이 소멸될때 실행될것이다.
public void close() {
System.out.println(
"[" + uuid + "]" + "request scope bean close" + this);
}
}
로그 찍을때 사용할 MyLogger 클래스
request 스코프를 가지므로 이 빈은 http요청당 "하나씩"생성되고 http요청이 끝나는 시점에 "소멸"된다.
이 빈은 http요청당 하나씩 생성되므로, uuid를 저장해두면 다른 http요청과 구분할 수 있다.
LogDemoController
@Controller //@Component가 들어가있는, 자동빈등록을 위한 어노테이션
@RequiredArgsConstructor // 모든 필드값이 매개변수로 들어간 생성자를 만들어주는 어노테이션, 생성자가 하나일때 사용하면 자동으로 생성자주입까지 되니까 유용하다.
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo") //웹을 이용하니까, 다음과 같은 요청이 오면
@ResponseBody //반환하는게 뷰가 아닌 , 값이므로 responseBody로
public String logDemo(HttpServletRequest httpServletRequest) { //서블릿으로 httpRequest 요청정보를 받을 수 있다.
String requestUrl = httpServletRequest.getRequestURL().toString(); //이렇게 하면 어떤 url로 요청했는지 알 수 있다.
myLogger.setRequestURL(requestUrl); //request 스코프를 가지는 myLogger빈에다가 url를 저장해준다.
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
하지만 실행시
Error creating bean with name 'myLogger': Scope 'request' is not active for the
current thread; consider defining a scoped proxy for this bean if you intend to
refer to it from a singleton;
스프링 실행하려고 할때 오류나는이유
MyLogger는 request 스코프를 가지는데 , 스프링컨테이너가 LogDemoController나 LogDemoService를 생성하면서 의존관계 주입을 하려고 할때 MyLogger빈은 존재하지않는다 [request가 발생해야 생기는 빈이기 때문에]
해결방법
첫번째 해결방법은 앞서 배운 Provider를 사용하는것이다.
여기서는 ObjectProvider를 사용해본다.
컨트롤러 수정
@Controller //@Component가 들어가있는, 자동빈등록을 위한 어노테이션
@RequiredArgsConstructor // 모든 필드값이 매개변수로 들어간 생성자를 만들어주는 어노테이션, 생성자가 하나일때 사용하면 자동으로 생성자주입까지 되니까 유용하다.
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider; //이렇게하면 의존성주입때 myLogger가 아닌, dipendency lookup을 할 수 있는 Provider가 주입이 된다.
//그래서 싱글톤인 컨트롤러가 생성되고 의존관계 주입할떄 주입이 가능하다.
@RequestMapping("log-demo") //웹을 이용하니까, 다음과 같은 요청이 오면
@ResponseBody //반환하는게 뷰가 아닌 , 값이므로 responseBody로
public String logDemo(HttpServletRequest httpServletRequest) { //서블릿으로 httpRequest 요청정보를 받을 수 있다.
MyLogger myLogger = myLoggerProvider.getObject(); //RequestMapping으로 인해 이 메소드가 실행됬다는것은 http요청이 있다는것이므로,
// requset 스코프를 가지는 myLogger는 생성되서 스프링컨테이너에 존재할 것이다.
//그 myLogger를 Provider를 통해 가져와서 사용한다.
String requestUrl = httpServletRequest.getRequestURL().toString(); //이렇게 하면 어떤 url로 요청했는지 알 수 있다.
myLogger.setRequestURL(requestUrl); //request 스코프를 가지는 myLogger빈에다가 url를 저장해준다.
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
MyLogger myLogger = myLoggerProvider.getObject(); //RequestMapping으로 인해 이 메소드가 실행됬다는것은 http요청이 있다는것이므로,
// requset 스코프를 가지는 myLogger는 생성되서 스프링컨테이너에 존재할 것이다.
//그 myLogger를 Provider를 통해 가져와서 사용한다.
서비스도 수정
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerObjectProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerObjectProvider.getObject();
myLogger.log("service id = " + id);
}
}
그리고 서버를 킨후 http://localhost:8080/log-demo를 입력해서 요청해보면 RequestMapping때문에 logDemo메소드가 실행될것이다.
ObejctProvider 덕분에 싱글톤 패턴인 컨트롤러,서비스가 생성될때 의존관계주입이 되어야하는 myLogger 대신 Provider를 주입하고, http Request가 들어와 request 스코프 빈인 mylogger가 생성됬을때 .getObject()를 이용하여 request 스코프 빈인 mylogger를 가져온다.
이때 같은 http 요청이면 같은 requset 스코프의 스프링빈이 반환된다. (직접 구분할 필요없이 잘 찾아 반환해준다.)
[http요청으로 인해 진행되는 과정속에서는 같은 request스코프의 스프링빈이 반환]
스코프와 프록시
이번에는 Provider를 사용하는 방법이 아닌, 프록시를 사용하는 방법을 알아보자.
@Component //자동빈등록(컴포넌트스캔)을 위함
@Scope(value = "request",proxyMode = ScopedProxyMode.TARGET_CLASS) //request 스코프를 가지는 빈, vale = ~ 이 부분은 빼고 "request"라고 적어도 되긴하지만 proxyMode를 쓰려면 필요
public class MyLogger {
private String uuid;
private String requestURL;
MyLogger 클래스에서 @Scope 어노테이션 부분에 proxyMode = ScopedProxyMode.TARGET_CLASS 부분을
추가해준다.
이렇게하면 가짜 프록시 클래스를 만들어두고 http request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해둘 수 있다. ( 프록시의 뜻이 대리,대리인이니까 MyLogger의 대리인을 이용해서 의존관계 주입을 해둔 느낌)
그리고 컨트롤러랑 서비스는 Provider를 사용하기전으로 바꿔둔다.
@Controller //@Component가 들어가있는, 자동빈등록을 위한 어노테이션
@RequiredArgsConstructor // 모든 필드값이 매개변수로 들어간 생성자를 만들어주는 어노테이션, 생성자가 하나일때 사용하면 자동으로 생성자주입까지 되니까 유용하다.
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
이렇게
그 후 실행해보면 Provider를 사용했던것처럼 잘 동작하는걸 확인가능하다.
System.out.println("myLogger = " + myLogger.getClass()); //주입된 myLogger가 어떤것일지 로그로 찍어본다.
예전에 봤던 CGLIB를 이용해서 스프링이 조작한 가짜 myLogger가 주입되어있고 이게 마치 Provider처럼 동작하는것이다.(일단 껍데기를 주입시켜놓고, myLogger를 사용할때 스프링컨테이너에서 찾아서 사용하게끔)
댓글