스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
본 포스팅은 <스프링 핵심 원리 - 기본편> 강의 내용을 복습하기 위해 간략히 작성된 글이므로 전체 내용은 위 강의에서 확인 가능합니다.
2. 객체지향의 5가지 원칙(SOLID), 관심사의 분리
7. @Autowired 필드명 매칭, @Qualifier, @Primary
빈 스코프
앞선 포스팅들을 봤을 때 우리는 스프링 빈이 스프링 컨테이너 시작과 함께 생성되고 종료될 때까지 유지된다고 알고 있는데요. 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문입니다. 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻하는데요.
스프링 빈은 싱글톤 뿐만 아니라 다양한 스코프를 지원합니다.
- 싱글톤 : 기본 스코프, 가장 넓은 범위의 스코프
- 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
- 웹 관련 스코프
- reqeust : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
- session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
적용 방법은 다음과 같습니다.
// 컴포넌트 스캔 자동 등록
@Scope("prototype")
@Component
public class HelloBean {}
// 수동 등록
@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
return new HelloBean();
}
프로토타입 스코프
프로토타입 스코프는 항상 같은 인스턴스의 스프링 빈을 반환했던 싱글톤과는 다르게 항상 새로운 인스턴스를 생성해서 반환합니다. 핵심은 스프링 컨테이너가 프로토타입 빈을 생성하고 의존관계 주입 그리고 초기화까지만 처리한다는 것입니다. 클라이언트에 빈을 반환하고 난 다음부터는 아~무 신경도 쓰지 않습니다. 그 빈을 책임질 주체는 클라이언트가 되는 것이죠. 그래서 우리가 싱글톤 스코프 빈에 사용했던 종료 메서드는 호출되지 않습니다.
이런 프로토타입 스코프를 싱글톤 빈과 같이 사용하면 문제점이 생기는데요. 싱글톤 빈이 내부에 가지고 있는 프로토타입 빈은 주입 시점에 생성된 것이기 때문에 만약 이 싱글톤 빈을 사용한 클라이언트 A가 프로토타입 빈의 로직을 사용했으면 그 결과가 클라이언트 B에게도 적용되게 됩니다. 프로토타입 빈이 아무리 새로 생성되어도 싱글톤 빈은 한번 생성하고 주입받으면 그걸로 끝이기 때문이죠.
그렇다면 싱글톤 빈과 프로토 타입을 같이 사용할 때 사용할 때마다 항상 새로운 프로토타입 빈을 생성하려면 어떻게 해야 할까요?
- 스프링 컨테이너에 요청
- ObjectFactory, ObjectProvider
- JSR-330 Provider
1. 스프링 컨테이너에 요청
싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청을 해봅시다.
예전 코드
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;
}
}
수정 코드
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
의존 관계를 외부에서 주입(DI) 받는 게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL), 의존관계 조회(탐색)이라고 합니다. 그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면 스프링 컨테이너에 종속적인 코드가 되고 단위테스트도 어려워집니다.
그러면 이렇게 하는 방식 말고 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 스프링 기능은 무엇일까요?
2. ObjectFactory, ObjectProvider
바로 이 DL 서비스를 제공하는 것이 ObjectProvider입니다. 과거에는 ObjectFactory가 있었는데 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌습니다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
...
return count;
}
ObjectProvider의 getBean()을 호출하면 내부에서는 스프링 컨테이너를 통해서 해당 빈을 찾아 반환한다.
간단하게 특징을 살펴보겠습니다.
- ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
- ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
3. JSR-330 Provider
마지막 방법은 JSR-330 자바 표준을 사용하는 것이고 따로 라이브러리를 추가하면 됩니다.(javax.inject:javax.inject:1)
public interface Provider<T> {
T get();
}
@Autowired
private Provide<PrototypeBean> provider;
public int log() {
PrototypeBean prototypeBean = provider.get();
...
int count;
}
get() 메서드 하나만 사용하면 되기 때문에 기능이 단순하고 자바 표준이기 때문에 다른 컨테이너에서도 사용이 가능합니다.
실무에서는 자바 표준인 JSR-330 Provider과 스프링이 제공하는 ObjectProvider 중에 어떤 걸 선택해야 할까요?
다른 컨테이너를 사용할 일이 없는 이상에는 스프링이 제공하는 기능을 사용하는 것이 좋다고 합니다.
이렇게 프로토타입 빈에 대해서 알아봤는데 그러면 이 프로토타입 빈은 언제 사용하는 것일까요?
매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용하면 됩니다. 실무에서는 대부분 싱글톤 빈으로 문제를 해결하기 때문에 직접적으로 사용하는 일은 드뭅니다.
🍪 참고로 스프링이 제공하는 메서드에 @Lookup 애노테이션을 사용하는 방법도 있지만 이전 방법들로 충분히 사용이 가능하기 때문에 개인적으로는 추후 필요할 때 방법을 찾아보려고 합니다.
웹 스코프
웹 스코프는 웹 환경에서만 동작하는 스코프로 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리하기 때문에 종료 메서드가 호출됩니다.
웹 스포크의 종류
- request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
이 포스팅에서는 request 스코프에 대해 예제로 배워보도록 하겠습니다. request스코프는 언제 사용할까요?
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기가 어렵기 때문에 구분하기 위해 사용합니다.
원하는 로그 포맷은 다음과 같습니다.
[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][requestURL]{message}
- UUId로 HTTP 요청을 구분하고 requestURL로 어떤 URL을 요청해서 남은 로그인지 확인
MyLogger
로그를 출력하기 위한 클래스 예제 코드입니다.
@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);
}
}
@Scope(value = "request")를 사용해서 스코프 범위를 지정했고 requestURL을 setter메서드로 입력받는 이유는 빈이 생성되는 지점에는 알 수 없기 때문입니다.
LogDemoController
로거가 잘 작동하는지 확인하는 테스트용 컨트롤러입니다.
@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 받음
LogDemoService
비즈니스 로직이 있는 서비스 계층에서 로그를 출력합니다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
위의 코드들을 쭉 살펴보면 웹과 관련된 부분은 컨트롤러까지만 사용하고 서비스 계층에서는 웹 기술에 종속되지 않는 것을 알 수 있는데 그렇게 코드를 분리(중요!!)해야 유지보수를 할 때 용이합니다.
리퀘스트 스코프 빈은 실제 고객의 요청이 와야 생성되기 때문에 지금까지 작성한 코드로는 실행할 수가 없는데요. 실행하기 위해서 Provider를 활용해서 예제 코드를 완성하겠습니다.
@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";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
ObjectProvider를 사용했기 때문에 ObjectProvider.getObject()를 보출하는 시점까지 request 스코프 빈의 생성을 지연할 수 있습니다. 앞에서 HTTP 요청 당 하나의 인스턴스가 생성된다고 했었는데 컨트롤러와 서비스를 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환되는 것을 확인할 수 있습니다.
Provider를 사용한 위의 코드를 더 줄일 수 있는 방법은 없을까요?
스코프와 프록시
이번에는 프록시 방법을 사용해보겠습니다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {}
적용 대상이 인터페이스가 아닌 클래스이면 TAGET_CLASS를 선택하고 인터페이스라면 INTERFACES를 선택하면 됩니다. 이렇게 추가하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있습니다. Provider를 사용하기 이전의 코드에 적용해도 그대로 실행됩니다.
전에 싱글톤 컨테이너에 대해 배울 때 CGLIB이라는 바이트코드 조작 라이브러리로 설정 정보를 갖고 있는 객체를 상속 받는 프록시 객체를 만든다는 이야기를 했었는데요. 이번에도 저렇게 proxyMode를 추가해서 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입하는 겁니다.
프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request 스코프를 사용할 수 있었습니다. 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리했다는 점입니다. 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다는 게 신기하고 흥미롭지만 이런 특별한 스코프는 꼭 필요한 곳에서만 사용해야 나중에 유지보수할 때 걱정할 필요가 없다고 해요!
'Spring' 카테고리의 다른 글
MultipartFile이 있는 DTO 요청하기 (404 Bad Request 해결) (0) | 2022.11.28 |
---|---|
DTO와 MultipartFile 요청하기 (415 Unsupported Media Type 해결) (0) | 2022.11.28 |
[Spring] 빈 생명주기 콜백 (1) | 2022.09.17 |
[Spring] @Autowired 필드명 매칭, @Qualifier, @Primary (0) | 2022.09.17 |
[Spring] 컴포넌트 스캔 (0) | 2022.09.17 |