스프링 핵심원리-기본편 (김영한님) 강의를 듣고 정리한 글입니다.
스프링 지원 스코프
1. 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
2. 프로토타입: 스프링 컨테이너가 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지않는 매우 짧은 범위의 스코프
웹 관련 스코프
1. request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프
2. session: 웹 세션이 생성되고 종료될때까지 유지되는 스코프
3. applictaion: 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프
지금까지는 싱글톤 스코프를 사용했으므로 프로토타입 스코프 사용방법을 알아보자
컴포넌트 스캔 자동 등록
@Scope("Prototype")
@Component
public class HelloBean {}
싱글톤 스코프 빈 요청
싱글톤 스코프의 빈을 조회하면 항상 같은 인스턴스의 스프링 빈을 반환한다.
프로토타입 빈 요청
스프링 컨테이너에 빈 요청이 오면 프로토타입의 빈을 생성하고 필요한 의존관계를 주입한 후 반환한다.
같은 요청이 또 와도 항상 새로운 프로토타입 빈을 생성하여 반환한다.
스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다.
이후에는 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지않는다.
프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다...
@PreDestroy같은 종료 메서드가 호출되지않는다.
싱글톤 예시
시작과 종료 메서드 잘 호출됨
프로토타입 예시
*참고: PrototypeBean 클래스에 @Component 없어도 스캔이 되는 이유는 AnnotationConfigApplictionContext에 등록해주었기때문이다.
결과
싱글톤 빈: 스프링 컨테이너 생성시점에 초기화 메서드 실행, 스프링 컨테이너가 종료될때 빈의 종료 메서드 실행
프로토타입 스코프 빈: 스프링 컨테이너에서 빈을 조회할때 생성(ac.getBean()), 초기화 메서드도 실행, 스프링 컨테이너가 종료될때 종료 메서드 실행 안됨(destroy() 실행안됨)
프로토타입 빈의 종료 메서드 실행 방법
프로토타입 빈 정리
- 스프링 컨테이너에 요청할때마다 새로 생성됨
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입, 초기화까지만 관여한다.
- 종료 메서드가 호출되지않는다.
- 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.
프로토타입은 가끔 사용하는데, 싱글톤 빈과 같이 사용하다보면 문제가 발생하기도 한다!
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점
만들고 싶은 상황
코드
결과(잘 만들어짐)
싱글톤 안에 프로토타입 빈 만들기
clientBean이 생성되고 프로토타입 빈도 생성된다.
클라이언트 A가 로직 호출시 생성된 프로토타입 빈을 이용하여 count를 1 증가시킨다.
클라이언트 B가 clientBean에서 로직 실행시 기존 생성된 prototypeBean을 사용하는 문제가 발생한다.
그러면 count는 1이 아닌 2가 된다.
프로토타입 빈은 요청시 새로 생성해 주는것인데, 싱글톤 빈 안의 프로토타입 빈은 이미 처음에 싱글톤 빈이 프로토타입 빈을 생성하고, 주입했기 때문이다.
코드로 이 상황을 살펴보자
프로토타입 빈과 클라이언트 빈 생성
테스트 생성
사용할때마다 새로운 프로토타입 빈이 생성되기를 원한다.
해결방법
어떻게 하면 사용할때마다 항상 새로운 프로토타입 빈을 생성할 수 있을까?
1. 스프링 컨테이너에 요청(비추)
주입받지 않고 그냥 로직 실행시 새로 프로토타입 빈을 생성한다.
테스트를 실행해보면
1,1로 정상적으로 프로토타입이 생성되어 처리됨을 볼 수 있다.
단점
- 의존관계를 외부에서 주입받는 것이 아닌(DI) 필요한 의존관계를 찾는것을 DL(Dependency Lookup): 의존관계 조회라고 한다.
- 스프링 애플리케이션 컨텍스트 전체를 주입받게되면 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.
2. ObjectFactory, ObjectProvider 사용
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 ObjectProvider 이다.
과거: ObjectFactory -> +편의기능 추가 : ObjectProvider
ObjectProvider가 빈을 컨테이너에서 대신 찾아준다.
테스트 수행
결과
빈이 각각 만들어졌다.
- ObjectProvider는 getObject()를 통해 빈을 호출한다. => 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(프로토타입 빈이므로 호출시마다 각각 생성됨)
- ObjectProvider는 주입 안해도 자동으로 주입해준다.
- ObjectProvider는 별도의 라이브러리가 필요 없지만 스프링에 의존적이다.
스프링에 의존하지않는 방법?
3. JSR-330 Provider
javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법이다.
스프링 부트 3.0 : jakarta.inject.Provider 사용
이 방법을 사용하려면 라이브러리를 gradle에 추가해야한다.
스프링 부트 3.0이상이면 jakarta.inject:jakarta.inject-api:2.0.1 를 추가하면 된다.
스프링 버전 확인 방법: https://yjh5369.tistory.com/entry/Spring-%EB%B2%84%EC%A0%84-%ED%99%95%EC%9D%B8%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95
코드 작성
Provider, get() 함수 명이 바뀌었다.
오류가 발생했는데 해결하지 못했다 ... 코드는 맞다.. 해결방법을 찾자..
장점
- 스프링에 의존적이지 않고 DL기능만 제공한다.
- 별도의 라이브러리가 필요하다.
하지만 ,, 프로토타입 빈은 거의 사용하지않는다. 싱글톤 빈만 대부분 사용한다.
ObjectProvider vs JSR330 Provider
이 두가지 모두 DL이 필요한 경우 언제든지 사용이 가능하다.
ObjectProvider는 DL을 위한 편의 기능을 많이 제공해준다. 그리고 스프링외에 별도의 의존관계 추가가 필요없다.
JSR-330 Providers는 기능은 간단하지만 스프링에 의존적이지않아(자바 표준) 다른 컨테이너에서도 사용할 수 있다.
자바 표준 vs 스프링 무슨 기능을 쓸까?
서로 겹치는 기능이 존재할때가 있다. 기능의 차이를 보고 구별하자. 하지만 대부분 스프링이 더 다양하고 편리한 기능을 제공하므로 스프링을 쓰자. (ex)Autowired)
그러나 스프링 내에서도 자바 표준을 사용하기를 권장하는 것은 자바 표준을 쓰자. (ex)PreDestroy)
웹 스코프
- 웹 환경에서 동작하는 스코프
- 스프링이 해당 스코프의 종료 시점까지 관리한다(종료 메서드 호출)
웹 스코프 종류
- request : HTTP 요청 하나가 들어오고 나갈때까지 유지되는 스코프 - 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
2. session : HTTP 세션과 동일한 생명주기를 가지는 스코프
3. application : 서블릿(Servlet) 컨텍스트 동일한 생명주기를 가지는 스코프
4. websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
request 스코프 예제 만들기
준비단계
웹 환경 라이브러리 추가하기
implementation 'org.springframework.boot:spring-boot-starter-web'
CoreApplication.java 를 실행해보면 웹 사이트가 뜬다.
아직 사이트에 들어가면 오류가 뜬다.
만약 포트 오류 발생시에(8080이 이미 사용중일때) main/resources/application.properties 에서 포트를 변경하자
server.port=9090
시작단계
HTTP 요청을 구분하기위해 [UUID][requestURL][message] 형식으로 로그 찍히도록 하자.
URL은 빈 생성시에는 모르므로 외부에서 주입될 수 있도록 하였다.
Controller 만들기
@RequiredArgsConstructor : 생성자로 의존관계 자동 주입
http://localhost:8080/log-demo 로 URL 요청시에 실행된다.
requestURL 을 MyLogger에 저장한느 부분은 (setRequestURL~) 컨트롤러보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터등을 활용하는것이 좋다.(지금은 간단하게 하기위해 컨트롤러에서 처리)
option+Enter로 서비스 만들기
Service 만들기
logic 함수에 uuid와 id등을 다 넘기는 것보다 myLogger.log 함수를 이용하여 웹 정보가 서비스계층까지 넘어가지않는것이 좋다.
오류 발생
request 빈인 MyLogger는 고객 요청시에(HTTP) 만들어진다.
지금은 스프링 컨테이너가 띄워질때 request 빈인 MyLogger 빈을 생성하려고 하고있으므로 오류가 발생한다. (request 빈은 HTTP 요청시부터 만들어지고 요청이 끝나면 destroy되도록 만들어졌기때문!)
MyLogger 생성 시점을 고객 요청 시점으로 미루어야 오류가 발생하지 않는다.
=> 해결 방법 : Provider (사용 시점에 생성하므로 미리 request 빈을 생성하지않아 오류가 발생하지 않는다. 생성 시점을 고객 요청 시점으로 미룰 수 있다. )
스코프와 Provider
Provider를 사용하여 요청 들어올때 request 빈(MyLogger)이 생성되도록 하였다.
service 부분도 변경
실행결과
그런데 더 간단하게 사용할 수 있는 방법은 없을까? => 프록시 사용
스코프와 프록시
프록시로 한줄만 추가하면 오류가 발생하지않고 Provider를 사용한 것과 똑같이 동작한다.
Provider를 사용하지 않아 오류가 발생했던 코드로 돌아가자.
LogDemoController
LogDemoService
MyLogger
적용 대상이 클래스이면 TARGET_CLASS를 추가해주고, 인터페이스이면 INTERFACES를 선택하자.
잘 동작한다.
프록시
MyLogger 생성시에 가짜 껍데기 MyLogger를 생성하여 등록해두고, 만약 MyLogger 기능을 실제로 호출할때 진짜 MyLogger를 생성하여 동작시킨다.
애노테이션 변경만으로 프록시 객체로 대체될 수 있다. 👍