커넥션 풀 (Connection Pool)
- 애플리케이션에 요청이 들어올 때마다 데이터베이스 연결을 수립하고, 해제하는 일은 굉장히 비효율적이다.
- 이 문제를 해결하기 위해 미리 여러개의 데이터베이스 커넥션을 생성해놓고, 필요할 때마다 꺼내쓰는 것이 커넥션 풀이다.
IoC 컨테이너
- Inversion of Control의 약자로, 컨테이너가 대신 객체의 생성부터 소멸까지의 인스턴스 생명주기의 관리를 해주는 것을 말한다.
- 객체 관리 주체가 프레임워크가 되기 때문에 개발자는 비즈니스 로직에 집중할 수 있다.
- 스프링 컨테이너가 관리하는 객체를 Bean이라고 하고, 이 빈들을 관리한다는 의미로 컨테이너를 BeanFactory라고 부른다.
Bean LifeCycle
- 빈의 생명주기란 해당 객체가 언제,어떻게 생성되고 소멸 전까지 어떤 작업을 수행하고 언제,어떻게 소멸되는지의 과정을 말한다.
- 스프링 컨테이너는 빈 생명주기를 관리하고, 객체의 생성이나 소멸 시 호출될 수 있는 콜백 메서드를 제공한다.
스프링 컨테이너 생성 → 스프링 빈 생성 → 의존관계 주입 → 초기화 콜백 → 사용 → 소멸전 콜백 → 스프링 종료
빈 생명주기 콜백
- 커넥션 풀처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 하려면 객체의 초기화와 종료 작업이 필요하다.
- 스프링 빈은 간단하게 **객체 생성 → 의존관계 주입** 이라는 라이프사이클을 가진다.
- 스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.
- 스프링은 의존관계 주입이 완료되면, 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다.
- 스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.
- 인터페이스 (InitializingBean, DisposableBean)
- 설정 정보에 초기화 메서드, 종료 메서드 지정
- @PostConstruct, @PreDestory 애노테이션 지원 → 이걸 사용하자!
빈 스코프
- 지금까지는 스프링 빈이 스프링 컨테이너의 시작과 종료까지 유지된다고 배웠다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. (스코프: 빈이 존재할 수 있는 범위)
- 스프링은 다음과 같이 다양한 스코프를 지원한다.
- 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
- 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여, 더는 관리하지 않는 짧은 범위의 스코프
- 웹 관련 스코프: request / session / application
프로토타입 스코프
- 싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 스프링 빈을 반환하는 반면, 프로토타입 스코프를 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.
- 싱글톤 빈 요청)
- 싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
- 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
- 이후에 스프링 컨테이너는 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다.
- 프로토타입 빈요청1)
- 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
- 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
- 프로토타입 빈요청2)
- 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
- 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입을 생성해서 반환한다.
- 핵심은 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 처리한다는 것이다.
- 클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않기 때문에 @PreDestory 같은 종료 메서드 호출은 클라이언트의 책임이다.
프로토타입 빈 직접 요청
- 클라이언트 A와 B는 스프링 컨테이너에 프로토타입 빈을 요청한다.
- 스프링 컨테이너는 요청마다 새로운 프로토타입 빈을 생성해서 반환한다.
- 클라이언트 A와 B는 각각 조회한 프로토타입 빈에 addCount()를 호출하고, 각각의 프로토타입 빈의 count는 1이 된다.
싱글톤 빈에서 프로토타입 빈 사용
- 이번에는 clientBean이라는 싱글톤 빈이 의존관계 주입을 통해 프로토타입 빈을 주입받아서 사용하는 경우이다.
- clientBean은 싱글톤이므로, 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.
- 이때 clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
- 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환한다. 이때 프로토타입 빈의 count 필드는 0이다.
- 이제 clientBean은 프로토타입 빈을 내부 필드에 보관한다.
- 클라이언트 A는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
- 클라이언트 A는 clientBean.logic()을 호출한다.
- clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가시켜 count 값이 1이 된다.
- 클라이언트 B도 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
- 여기서 중요한 점은, clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 프로토타입 빈이 새로 생성이 된 것이지, 사용할 때마다 새로 생성되는 것이 아니다!
- 클라이언트 B도 clientBean.logic()을 호출한다.
- clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가시키고 count 값이 1에서 2가 된다.
싱글톤 빈과 사용시 Provider로 해결
- 위에서 프로토타입 빈을 주입 시점에만 새로 생성하기 때문에 원하지 않는 상황이 발생했다.
- 싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 사용할 때마다 항상 새로운 프로토타입 빈을 생성할 수 있는 방법이 있다.
가장 간단한 방법은 싱글톤 빈이 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- 실행해보면 ac.getBean()을 통해 항상 새로운 프로토타입 빈이 생성되는 것을 볼 수 있다.
- 의존관계를 외부에서 주입(Dependency Injection) 받는게 아니라, 이렇게 직접 필요한 의존관계를 찾는 방식(Dependency Lookup)이 있다.
- 그러나, 스프링의 ApplicationContext 전체를 주입받게 되면 스프링 컨테이너에 종속적인 코드가 되며 단위 테스트도 어려워진다.
- 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 기능만 제공하는 무언가가 있으면 된다.
ObjectFactory, ObjectProvider
- 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider이다.
- 과거에 있던 ObjectFactory에 편의 기능을 추가해서 ObjectProvider가 만들어졌다.
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- provider.getObject()를 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
- ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 반환한다. → DL
- 스프링의 제공 기능을 사용하지만, 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.
JSR-330 Provider
- 마지막 방법은 javax.inject.Provider라는 JSR-339 자바 표준을 사용하는 방법이다.
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 코드를 만들기 훨씬 쉽다.
웹 스코프
- 웹 스코프는 웹 환경에서만 동작한다.
- 웹 스코프는 프로토타입과는 다르게 해당 스코프의 종료시점까지 관리한다. (종료 메서드가 호출된다.)
- 웹 스코프 종류)
- request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 각각의 요청마다 별도의 빈 인스턴스가 생성, 관리됨
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: ServletContext와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
request 스코프 예제
- 동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
- 이럴때 사용하기 좋은 것이 바로 request 스코프이다. → @Scope(value = "request")
- 스프링 애플리케이션은 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 실제 고객의 요청이 와야 생성할 수 있기 때문에 ObjectProvider로 이 문제를 해결한다.
- ObjectProvider 덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
- ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로, request scope 빈의 생성이 정상 처리된다.
- ObjectProvider.getObject()를 LogDemoController, LogDemoService에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다!
@Component
@Scope(value = "request") // HTTP 요청당 하나씩 생성되고 소멸
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(); // 빈이 생성되는 시점에 uuid 생성해서 저장
System.out.println("[" + uuid + "] request scope bean create: " + this);
}
@PreDestroy // 고객요청이 끝나면 빈 소멸
public void close() {
System.out.println("[" + uuid + "] request scope bean close: " + this);
}
}
- 이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장해둔다.
- 이 빈은 HTTP 요청당 하나씩 생성되므로, uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.
- 이 빈이 소멸되는 시점에 @PreDestory를 사용해서 종료 메시지를 남긴다.
- requestURL은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
//private final MyLogger myLogger;
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
//myLogger.setRequestURL(requestURL);
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("id");
return "OK";
}
}
- 여기서 HttpServletRequest를 통해 요청 URL을 받았다. → http://localhost:8080/log-demo
- 이렇게 받은 requestURL 값을 myLogger에 저장해둔다. myLogger은 HTTP 요청당 각각 구분된다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
//private final MyLogger myLogger;
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
//myLogger.log("service id: " + id);
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id: " + id);
}
}
- 여기서 중요한 점은, request scope를 사용하지 않고 파라미터로 모든 정보를 서비스 계층에 넘긴다면 파라미터가 많아서 지저분해진다. 서비스 계층은 웹과 관련된 정보와 무관하게 유지하는 것이 좋기 때문에 requestURL와 같은 웹 정보는 필요없다.
- request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고 멤버변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다.
스코프와 프록시
@Component
@Scope(value = "request" , proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
- 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS, 인터페이스면 INTERFACES 선택
- 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해둘 수 있다.
- 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다. 즉, MyLogger 클래스는 MyLogger$EnhancerBySpringCGLIB라는 클래스로 만들어진 가짜 프록시 객체가 대신 등록되었다.
- 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 내부에 단순한 위임 로직만 있고, 싱글톤처럼 동작한다.
- 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다.
- Provider를 사용하든, 프록시를 사용하든 핵심은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
'Spring > 개념' 카테고리의 다른 글
Entity Manager (0) | 2024.01.09 |
---|---|
웹 애플리케이션 이해 (0) | 2023.12.26 |
컴포넌트 스캔과 의존관계 자동 주입 (0) | 2023.12.24 |
객체지향 설계와 스프링 (0) | 2023.05.09 |