스프링 빈 라이프사이클

라이프사이클

스프링의 빈은 스프링이 빈 생성, 주입, 소멸까지를 책임진다. 빈 생성부터 소멸까지의 라이프사이클을 다뤄보고자 한다.

라이프사이클 콜백

컨테이너가 빈의 라이프사이클을 관리하는데, 라이프사이클의 특정 시점에 커스텀 로직을 넣을 수 있도록 한 것이 라이프사이클 콜백이다.

라이프사이클 살펴보기

1. XML이나 어노테이션을 탐색하여 빈 정의를 스캔

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

SpringApplication.run()에서 어플리케이션컨텍스트를 만들고 빈 정의를 탐색하기 시작한다.

2. 빈 클래스의 인스턴스를 생성 - 생성자 호출

정의된 빈 인스턴스를 생성한다. 이때 생성자를 호출한다.

3. 빈 안에 주입되야할 프로퍼티들을 셋팅한다.

@Component
public class HelloBean implements ApplicationContextAware, InitializingBean {
    @Autowired
    WorldBean worldBean;
}

빈 안에 프로퍼티를 주입해야 하는데 프로퍼티에 해당하는 빈이 아직 생성되지 않은 경우 그 빈부터 생성해준다.
빈 간에 순환참조가 있으면 빈 생성과정에 에러가 발생하게 된다.

3. ~Aware 관련 콜백 호출

예를 들면 다음과 같은 것들이 있다.

ApplicationContextAware.setApplicationContext - 어플리케이션컨텍스트를 받을 수 콜백,
BeanClassLoaderAware.setBeanClassLoader - 클래스로더를 받을 수 콜백

이걸 써서 컨텍스트나 클래스로더를 따로 저장해둬야하는 상황이 어떤게 있을까....

이런 콜백들을 호출하려면 각 인터페이스를 implement해야한다.

package sample;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class MyBean implements ApplicationContextAware, BeanClassLoaderAware {

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        // do something
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        // do something
    }
}

4. @PostContruct, InitializingBean.afterPropertiesSet(), init-method

셋은 프로퍼티 및 종속성 주입이 완료된 후 호출되는 점에서 동일하다.

@PostContruct는 JSR-250에서 정의한 어노테이션으로 스프링이 아닌 프레임워크에서도 사용할 수 있는 콜백이고, InitializingBean.afterPropertiesSet(), init-method는 스프링에서 제공하는 콜백이다.

InitializingBean.afterPropertiesSet()의 경우 InitializingBean 인터페이스를 implement하면 된다.

@Component
public class HelloBean implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        logger.info("afterPropertiesSet() called");
    }
}

init-method의 경우 @Bean(initmethod="..")으로 등록한 메서드가 실행되게 된다.

실행 순서는 @PostContruct -> InitializingBean.afterPropertiesSet() -> init-method 이다.

세 가지 콜백을 각각 언제 사용해야 하는지를 명확히 구분할 수는 없다.

참고로 BeanPostProcessor.postProcessBeforeInitialization() 안에서 @PostContruct 가 호출된다.

5. BeanPostProcessor.postPRocessAfterInitialization()

기본 스프링부트 설정을 사용하는 경우 BeanPostProcessor를 따로 설정하지 않는 한 이 단계에서 하는 일은 없다.

이렇게 되면 빈을 사용할 수 있는 상태가 된다.
빈이 제거 될 때 설정할 수 있는 콜백도 생성과 비슷하다.

'개발 > Spring' 카테고리의 다른 글

[Java, Spring] Filter와 Interceptor의 차이  (0) 2020.05.22
[SpringMVC] CORS 처리  (2) 2020.03.31

Filter와 Interceptor, 둘 다 전후처리 용도로 사용될 수 있다는 점에선 동일한 것 같다.
구체적으로 어떤 차이가 있는 지 정리해보았다.

Filter

Filter는 Java의 Servlet 스펙에서 정의된 개념이다.
Java Servlet의 스펙이므로 스프링 프레임워크가 없이도 사용 가능하다.
SpringMVC를 사용하는 경우 DispatcherServlet 전후로 호출된다.

Interceptor

Interceptor는 스프링의 스펙으로, Filter와 목적은 동일하나 좀 더 세분화된 컨트롤을 제공한다.

When to use Interceptor

세분화된 컨트롤을 하고자 하는 경우

Filter는 전, 후 처리만 가능하지만 Interceptor는 preHandle, postHandler, afterCompletion로 세분화된다.
그 외 WebContentInterceptor, LocaleChangeInterceptor 등의 상황에 맞게 활용할 수 있는 다양한 인터페이스를 제공한다.

ModelAndView를 컨트롤 해야하는 경우

Filter는 DispatcherServlet의 영역이 아니기 때문에 ModelAndView에 접근이 불가하다.
Interceptor는 DispatcherServlet 사이클 안에서 사용되므로 ModelAndView를 접근할 수 있다.

public interface HandlerInterceptor {
    // ... 생략
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            @Nullable ModelAndView modelAndView) throws Exception {
    }

When to use Filter

HttpServletRequest, HttpServletResponse를 유연하게 컨트롤하고자 하는 경우

Filter는 다음 Filter로 체이닝을 할때 새로운 HttpServletRequest, HttpServletResponse를 넘길 수 있다.
Interceptor는 이미 DispatcherServlet으로 넘어온 상태이기 때문에 HttpServletRequest, HttpServletResponse를 다른 인스턴스로 교체할 수 있는 방법이 없다.

예를 들어 다음 상황에서 Filter가 좀 더 유연하다.

  • ServletRequest의 Body를 여러번 읽어야 하는 경우 (ex: 로깅)
    ServletRequest의 Body는 한 번 밖에 읽을 수 없다. 여러번 읽을 수 있게 원본 Request에서 읽은 내용을 byte[] 버퍼에 저장해둔 새로운 ServeletRequest를 정의하여 넘길 수 있다.

이처럼 재정의한 인스턴스로 넘길 수 있기 때문에 유연하게 내가 원하는 기능을 추가할 수 있을 것이다.

참고로 SpringSecurity는 Filter 기반으로 동작한다. (링크)

Appendix

다음은 Filter에서 HttpServletRequest, HttpServletResponse를 수정하는 예시다.

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
      throws IOException, ServletException {
        HttpServletRequest currentRequest = (HttpServletRequest) servletRequest;
        ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(currentRequest);
        chain.doFilter(wrappedRequest, servletResponse);
    }

'개발 > Spring' 카테고리의 다른 글

[Spring] 스프링 빈(Bean) 라이프사이클  (0) 2020.05.23
[SpringMVC] CORS 처리  (2) 2020.03.31

CORS(Cross Origin Resource Sharing)

다른 오리진(Cross origin)으로의 요청은 보안상의 이유로 제한된다.
요청을 정상적으로 보내려면(CORS를 하려면) 별도 추가 작업이 필요하다.

CORS 과정

from.com에서 POST to.com/myApi라는 리소스를 요청한다고 하자.

Preflight 요청

본 요청을 보내기전에 서버(b.com)가 요청하는 리소스에 CORS를 허용하는 지를 확인하는 Preflight 요청을 보낸다.

  OPTIONS /myApi
  Access-Control-Request-Method: POST 
  Access-Control-Request-Headers: origin, x-requested-with
  Origin: https://from.com
  • OPTIONS라는 특별한 메서드를 사용한다.
  • Access-Control-Request-Method, Access-Control-Request-Headers, Origin 헤더가 있어야 한다.
  • 이 요청을 'Preflight'라 한다.
  • Preflight는 브라우저에 의해 자동으로 요청된다.(개발자는 관여하지 않는다.)

Preflight 응답 확인

CORS를 허용하는 경우 응답에 아래 헤더가 있어야 한다.

Access-Control-Allow-Origin: https://from.com
Access-Control-Allow-Methods: POST, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type (설정값에 따라 다름)
Access-Control-Max-Age: 86400 (설정값에 따라 다름)

a.com에서 해당 API를 POST로 호출해도 좋다는 응답이다. 이후 본요청을 전송하게 된다.

만약 허용하지 않는 서버라면 응답에 위 헤더들이 없을 것이고 브라우저는 다음과 같은 에러를 뱉을 것이다.

No 'Access-Control-Allow-Origin' header is present on the requested resource.

참고로 Preflight가 실패하면 본래의 요청 조차 보내지 않는다.

CORS 처리하기

보통 백엔드 서버만 작업하면 된다.
그러나 세션정보(쿠키)등을 같이 넘기려면 JS 작업이 필요할 수 있다.

SpringBoot에서 CORS 처리하기

옵션 1, @CrossOrigin 어노테이션 사용한다.

@CrossOrigin(origins = "https://from.com")
@PostMapping("/myApi")
public void myApi() {}

세션 정보(쿠키)를 같이 넘기려면 withCredentials를 활성화해야 한다. (js에서도 withCredentials 옵션을 줘야한다.)

@CrossOrigin(origins = "https://from.com, withCredentials = "true" )
@PostMapping("/myApi")
public void myApi() {}

withCredentials=true로 하려면 allow origin을 "*"으로 설정하면 안된다. {"a.com", "b.com"} 등으로 특정해야 한다.

옵션 2. 전역 설정

@Configuration
public WebConfig {
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/myApi").allowedOrigins("https://from.com");
                //.allowCredentials(true) // 필요한 경우
            }
        };
    }
 }

JS에서 CORS 처리하기

특별히 할건 없다.
세션정보(쿠키)를 넘기려면 withCredentials 옵션을 줘야한다.

기본

var xhr = new XMLHttpRequest();
xhr.open('POST', 'https://to.com/myApi', true);
xhr.withCredentials = true;
xhr.send(null);

axios

const myMethod = () => {
  axios.post(`https://to.com/myApi`, {
    ...
  },
  { withCredentials: true }
  ).then(response => {
    ...
  })
}

jQuery

$.ajax({
   url: 'https://to.com/myApi',
   xhrFields: {
      withCredentials: true
   }
   ...
});

'개발 > Spring' 카테고리의 다른 글

[Spring] 스프링 빈(Bean) 라이프사이클  (0) 2020.05.23
[Java, Spring] Filter와 Interceptor의 차이  (0) 2020.05.22

+ Recent posts