Bean은 스프링 프레임워크가 생성, 관리하는 클래스의 인스턴스, 즉, 객체입니다.
다른 객체와 다르게 Bean은 생성의 책임이 사용자에게 있지 않습니다.
이 책임은 Spring IoC 컨테이너에게 위임되고 사용자는 Bean에 대한 적절한 메타데이터를 제공해주면 됩니다.
Bean의 정의는 BeanDefinition 이라는 객체를 통해 표현됩니다.
BeanDefinition은 다음과 같은 정보들을 갖습니다.
더 자세하게는 다음과 같습니다.
(이미지 출처 :https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-definition)
이번 글에서는 이런 다양한 Bean의 메타 데이터 중 Scope
, 특히 RequestScope
에 대해 더 알아보겠습니다.
Scope 메타데이터를 통해 우리는 특정 Bean이 어느 범위에서 존재할 지 지정해줄 수 있습니다.
스프링에서는 기본적으로 6개의 Scope 설정을 지원하며 원한다면 custom scope를 만들 수도 있습니다.
스프링에서 제공하는 6개의 Scope는 다음과 같습니다.
singleton
별다른 설정을 해주지 않으면 지정되는 기본 Scope입니다.
이 Scope를 사용시 하나의 IoC container안에서 단 하나의 Bean만 존재하게 됩니다.
(클래스로더 전체에서 하나의 객체만 존재하도록 지정하는 GoF 패턴에서 소개된 singleton 패턴과는 약간의 차이가 있습니다.)
처음 생성된 Bean을 별도의 Cache에 저장하여 해당 Bean에 대한 다음 요청부터는 Cache에 있는 Bean을 제공해줍니다.
우리가 자주 사용하는 Service, Controller, Dao 같은 클래스들은 stateless 하기 때문에 singleton으로 사용해도 무방합니다.
prototype
빈에 대한 요청(생성, 의존성 주입 등..) 이 있을 때 마다 새로운 Bean을 만들어내는 Scope입니다.
이 Scope를 사용시 스프링에서는 새로운 Bean의 생성, 의존관계 주입 까지만 관여한 후 더 이상 관리하지 않습니다.
만약 Bean 소멸에 대한 callback이 설정되어있더라도 더 이상 스프링에서 관리하고 있지 않기 때문에 이가 호출되지 않습니다.
그렇기 때문에 일반 객체와 동일하게 사용자가 객체의 자원을 반환하는데에 신경써줘야합니다. (또는 bean post-processor 를 이용할 수도 있습니다.)
이 다음부터 4개의 Scope는 웹 환경에서만 동작하는 Scope입니다.
request
HTTP 요청 하나 당 하나의 Bean을 만들어내는 Scope입니다.
어떤 요청안에서 빈의 상태가 변경되어도 다른 요청에 영향이 가지 않습니다.
요청에 대한 응답이 끝이 나면 Bean이 소멸합니다.
이번 글에서 메인으로 살펴볼 스코프입니다.
session
HTTP 세션 한 번에 하나의 Bean을 만들어내는 Scope입니다.
request scope와 session과 request라는 단위의 차이가 있습니다.
application
ServeltContext와 동일한 생명주기를 가지는 Bean을 만들어내는 Scope입니다.
해당 Scope를 가진 Bean은 ServletContext의 속성으로 직접 등록됩니다.
singleton Scope와 비슷하게 보일 수 있지만, application Scope의 경우 ServeltContext당 하나의 Bean이 생성되고, singleton Scope의 경우 ApplicationContext(한 웹 어플리케이션에 여러개 존재 가능)당 하나의 Bean이 생성되는 차이가 있습니다.
websocket
websocket과 동일한 생명주기를 가지는 Bean을 만들어내는 Scope입니다.
이 Scope에 대한 더 자세한 정보는 여기서 확인하실 수 있습니다.
이 Scope를 가진 Bean은 HTTP 요청이 들어올 때 생성되고, 처리가 끝날 때 소멸됩니다.
다른 요청에 대해서는 다른 Bean을 생성하기에 Default(singleton) Scope에 비해 상태를 가지는 것에 대해 자유롭다고 할 수 있습니다.
그렇다면 이를 어디에 활용할 수 있을까요?
저는 이 Scope를 인증로직의 문제 해결에 활용할 수 있을 것 같습니다.
제가 구현한 인증 로직은 다음과 같습니다.
1. email과 password등 member의 정보를 가진 요청이 들어온다. →
2. email을 통해 memberDB에 접근해 존재하는 사용자인지 확인하고, password등의 필요한 정보를 가져온다. (중요) →
3. DB에서 가져온 정보와 요청의 정보를 비교한다. 정보가 일치하면 인증이 성공한다.
해당 로직은 Interceptor에서 실행됩니다.
문제는 이 다음에 발생합니다.
요청을 보낸 member의 카트에 담긴 상품을 조회하는 showCart와 물건을 추가하는 addProduct 메서드를 살펴봅시다.
인증 로직의 2번째 과정,
email을 통해 memberDB에 접근해 존재하는 사용자인지 확인하고, password등의 필요한 정보를 가져온다.
에서 memberDB에 이미 한 번 접근했는데도 불구하고, ID를 얻어오기 위해 한 번 더 DB에 접근하게 됩니다.
인증 로직에서 조회한 정보를 인증이 끝난 후 컨트롤러의 각 메서드에 넘겨 줄 방법은 없을까요? 🤔
여기서 request Scope를 활용할 수 있습니다.
먼저 각 요청의 멤버 정보를 저장하고 있을 클래스를 하나 생성합니다.
@Component
@RequestScope
public class AuthContext {
private Credential credential;
public void setCredential(Credential credential) {
this.credential = credential;
}
public Credential getCredential() {
Objects.requireNonNull(credential, "유저 정보가 초기화되지 않았습니다.");
return credential;
}
}
@RequestScope
어노테이션을 통해 간편하게 Scope를 변경해줄 수 있습니다.
이후 인증로직을 수행하는 Interceptor에서 DB에서 가져온 정보를 해당 클래스에 저장하게 해줍니다.
public class AuthInterceptor implements HandlerInterceptor {
...
private final AuthDao authDao;
private final AuthContext authContext;
...
@Override
public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) {
...
final Credential registeredMember = authDao.findMemberByEmail(requestMember.getEmail())
.orElseThrow(() -> new AuthorizationException("해당 이메일로 등록된 사용자가 없습니다."));
...
if (유효한_멤버_정보라면) {
authContext.setCredential(registeredMember);
return true;
}
throw new AuthorizationException("인증 정보가 잘못되었습니다.");
}
}
마지막으로 ArugmentResolver에서 이 정보를 각 메서드에 넘겨주도록 설정합니다.
위 과정을 통해 Interceptor에서 DB에 접근해 조회한 정보를 ArgumentResolver를 거쳐 controller의 인자로 전달해줄 수 있습니다.
사진에서 볼 수 있듯, controller에서는 DB에 재접근하는 코드를 삭제할 수 있습니다. 😎
요청이 처리되면 Bean이 자동적으로 소멸되므로 ThreadLocal 처럼 정보를 삭제해주는 일도 사용자가 신경쓰지 않아도 됩니다.