ahlight
개발 저장소
ahlight
전체 방문자
오늘
어제
  • 분류 전체보기 (28)
    • Java (5)
    • Spring (6)
    • JPA (2)
    • RDBMS (0)
    • Computer Science (0)
      • 디자인패턴, 프로그래밍 패러다임 (0)
      • 네트워크 (0)
      • 운영체제 (0)
      • 데이터베이스 (0)
    • 알고리즘 (0)
    • 프로그래머스 (0)
    • 백준 (0)
    • 서평 (3)
    • 회고 (1)
    • TIL (0)
    • 기타 (1)

블로그 메뉴

  • 홈

공지사항

인기 글

태그

  • 클린코드
  • TDD
  • 넥스트스텝
  • 라즈베리파이4 #홈서버 #포트포워딩 #dhcp
  • Java

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
ahlight

개발 저장소

Spring - Spring Boot만 해왔다면 순수 Spring에서 유의할 점
Spring

Spring - Spring Boot만 해왔다면 순수 Spring에서 유의할 점

2025. 2. 2. 17:42

최근 새로 담당하게 된 프로젝트의 프레임워크는 순수 Spring MVC 5.2.9 버전이다.

어노테이션 방식을 사용하는 Spring Boot 프로젝트만 익숙했기에, 많은 설정이 xml파일을 통해 이뤄져있는 순수 Spring은 굉장히 낯설었다. 이에 어려움을 겪었던 부분에 대해 기록해자.

1. 빈 등록 문제

배경

@RequiredArgsConstructor
@Configuration
public class JwtConfig {
	
    ...

    @Bean
    public JwtManager jwtManager() {
        return new JwtManager(...);
    } // 빈 등록 안됨

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(JwtManager manager) {
        return new JwtAuthenticationFilter(manager);
    }
}

JWT를 활용한 인가를 위해 필터와 관리 클래스를 빈으로 등록했고, 필터는 JWT 관리 클래스를 빈으로 주입받게 된다. 

위와 같은 Java class 설정으로 애플리케이션을 실행하면 다음과 같은 예외가 발생했다.

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'jwtAuthenticationFilter' available

 

root-context.xml에 직접 빈을 등록한 경우엔 정상적으로 등록되었다.

하지만 빈 등록 자체에 문제가 없다면 Java class를 통한 빈 등록 또한 정상 동작해야 했다.

문제 원인

결론부터 말하자면 Java class로 빈을 등록할 경우, Root WebApplicationContext에 빈 자체가 등록되지 않아 DelegatingFilterProxy가 위임 빈을 내부에 저장할 때 jwtAuthenticationFilter를 찾지 못해 발생한 문제였다.

이는 root-context.xml에 아래와 같은 컴포넌트 스캔 태그가 없었기 때문이다. 

<context:component-scan base-package="com.example.project.*" />

Spring Boot와 달리 xml 파일에 컴포넌트 스캔을 명시해줘야 Java class로 설정된 빈을 등록할 수 있었다. 

 

원인을 찾는 과정에서 다음 두 가지에 대한 개념이 도움되었다.

  • Spring Context의 로드 순서와 관계
  • DelegatingFilterProxy의 동작 시점

Spring Context의 로드 순서와 관계

Spring Context는 크게 두 가지로 구분할 수 있으며 관계는 다음 그림과 같다. 

https://linked2ev.github.io/spring/2019/09/15/Spring-5-%EC%84%9C%EB%B8%94%EB%A6%BF%EA%B3%BC-%EC%8A%A4%ED%94%84%EB%A7%81%EC%97%90%EC%84%9C-Context%28%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%29%EB%9E%80/

그림에서 볼 수 있듯 Servlet WebApplicationContext는 Root WebApplicationContext에 의존하게 된다. 

다시 말해, Root WebApplicationContext와 Servlet WebApplicationContext는 부모-자식 관계라고 볼 수 있다.

그러므로 Servlet WebApplicationContext에선 Root WebApplicationContext의 빈을 참조할 수 있지만 그 반대는 참조할 수 없다.

 

두 개의 컨텍스트는 다음 그림과 같은 순서로 로드된다.

https://yoo-hyeok.tistory.com/139

1. 톰캣은 web.xml을 읽어 프로젝트와 관련된 설정을 읽어들임

2 ~ 4. web.xml에 설정된 ContextLoaderListener를 통해 Root WebApplicationContext를 초기화

  • 이 때, 설정 값으로 web.xml에 <context-param> 태그에 명시된 xml 파일을 로드한다.
  • ContextLoaderListener는 부모 클래스인 ContextLoader의 initWebApplicationContext()를 호출해 초기화
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    ...
    try {
        // 스프링 컨테이너 생성
        if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        ...

        // 서블릿 컨테이너에 스프링 컨테이너(Root Context)를 설정
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        ...
    }
}

 

5 ~ 8. ServletWebApplicationContext 초기화

  •  그림 상에선 클라이언트 첫 요청이 들어오면 DispatcherServlet이 생성되고, Servlet WebApplicationContext가 생성되는 것으로 나와 있다.
  • 하지만 이는 web.xml에 서블릿 초기화 태그 내부에 <load-on-startup>1</load-on-startup> 태그가 존재하지 않는 경우에만 해당한다.
  • <load-on-startup>1</load-on-startup> 태그가 존재한다면 애플리케이션 시작 시점에 초기화되며, 이 방식이 권장된다.
  • ServletWebApplicationContext의 초기화는 DispatcherServlet의 부모 클래스인 FrameworkServlet이 initWebApplicationContext()를 호출해 초기화한다
protected WebApplicationContext initWebApplicationContext() {
    // 앞서 Servlet Context에 초기화된 Root Context를 가져옴
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    // 현재 서블릿에 Spring Context가 존재한다면 수행(마찬가지로 Root Context를 부모로 설정)
    // 생성자를 통해 주입된 컨텍스트인 경우
    if (this.webApplicationContext != null) {
        ...
    }

    // Servelt Context에 등록된 Spring Context가 존재한다면 등록
    if (wac == null) {
        wac = findWebApplicationContext();
    }

    // 모든 경우 없다면 새로 생성
    if (wac == null) {
        wac = createWebApplicationContext(rootContext);
    }

    ...

    // DispatcherServlet에 Servlet WebApplicationContext 저장
    if (this.publishContext) {
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    ...
    // 파라미터로 받은 Root Context를 부모로 설정
    wac.setParent(parent);

    ...

    return wac;
}

 

 

여기서 가장 중요한 점은 Root WebApplicationContext가 Servlet WebApplicationContext보다 먼저 생성되며, 부모 Context로 초기화된다는 것이다.

** Spring Context와 Servlet Context **

Root WebApplicationContext, Servlet WebApplicationContext 모두 Spirng Context라고 할 수 있다.

이름이 Servlet WebApplicationContext라고 Servlet Context인 것은 아니며, Spring Context는 단지 Servlet Context 위에서 동작하는 것 뿐이다. 

다만, Root WebApplicationContext는 Servlet Context의 속성으로 직접적으로 등록되는 것이고, 

ServletWebApplicationContext는 특정 서블릿(DispatcherServlet)에 종속되며, 이 Servlet을 통해 Servlet Context에 등록 되는 것이다.

 

DelegatingFilterProxy의 동작 시점

DelegatingFilterProxy의 존재 이유와 작동 방식은 Spring docs를 확인하자.

 

DelegatingFilterProxy가 Servlet Context의 필터 체인에 등록되는 시점은 톰캣이 web.xml 또는 @WebServlet을 통해 설정을 읽어들일 때다.

이후 Root WebApplicationContext가 초기화 되고, DelegatingFilterProxy에 실제로 위임할 빈이 내부에 저장된다. 

이 때, DelegatingFilterProxy가 빈을 찾기 위해 접근하는 Context가 바로 Root WebApplicationContext다.

@Override
protected void initFilterBean() throws ServletException {
    synchronized (this.delegateMonitor) {
        if (this.delegate == null) {
            if (this.targetBeanName == null) {
                this.targetBeanName = getFilterName();
            }

            WebApplicationContext wac = findWebApplicationContext();
            if (wac != null) {
                this.delegate = initDelegate(wac);
            }
        }
    }
}
    
/**
* findWebApplicationContext() 메서드를 따라가다보면 Root Context를 찾는 것을 알 수 있다.
**/
@Nullable
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
    return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}

 

** DelegatingFilterProxy는 Spring Security 의존성을 추가해야만 사용할 수 있다? **

아니다. 아래에서 볼 수 있듯 DelegatingFilterProxy는 Spring Framework 자체에 포함되어 있다.

package org.springframework.web.filter;

...

public class DelegatingFilterProxy extends GenericFilterBean {
	...
}

결론

  • Spring Context는 크게 두 가지로 구분할 수 있다.(Root WAC, Servlet WAC)
  • 각 Context는 일반적으로 root-context.xml, servlet-context.xml 파일에서 설정 값을 로드한다.
  • Root WAC와 Servlet WAC는 부모-자식 관계와 같으며, 1:N 관계를 가진다.
  • DelegatingFilterProxy가 필터 체인에 등록되는 시점은 톰캣이 web.xml을 통해 설정을 로드할 때다.
  • DelegatingFilterProxy가 위임할 빈을 저장하는 시점은 Root WAC 초기화 이후이다.
  • root-context.xml에서 컴포넌트 스캔을 하지 않아 Root WAC에 해당 빈이 등록될 수 없었다.
  • Servlet WAC에만 필터가 등록된다면 기본적으로 DelegatingFilterProxy는 위임할 빈(필터)을 찾을 수 없다. 

해결 방안

앞선 원인 분석 과정을 거치며, jwtAuthenticationFilter 빈이 Servlet WebApplicaitonContext에만 존재하고, Root WebApplicationContext엔 존재하지 않음을 확인했다. 이는 servlet-context.xml에만 컴포넌트 스캔 태그가 존재했고, root-context.xml엔 존재하지 않았기 때문이다.

 

해결을 위해 root-context.xml에 컴포넌트 스캔 태그를 추가했다.

이 과정에서 Root WebApplicationContext, Servlet WebApplicationContext에 빈이 이중으로 등록되며 같은 메서드가 두 번 호출되는 이슈가 발생했는데 servlet-context.xml의 스캔 범위를 조절하여 해결했다.

2. 환경 설정 파일 인식

앞선 빈 등록 문제와 함께 application.properties, yml과 같은 환경 설정 파일에 설정된 값이 @Value를 통해 변수에 주입되지 않는 문제가 존재했다.

이 문제도 마찬가지로 root-context.xml에 아래와 같은 환경 설정 파일 빈 등록 태그가 없었기 때문에 발생했던 문제였다.

<bean id="properties" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

// Spring 3.1부터 권장되는 방식
<bean id="propertyConfigurer" class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">

 

3. 선언적 트랜잭션 관리, AOP 프록시 방식의 차이

개발 단계에서 테스트 중, unchecked exception이 발생해도 롤백이 되지 않는 현상을 발견했다. 

Spring Boot에선 @Transactional만 붙이면 CGLIB를 사용해 선언적 트랜잭션 방식을 사용할 수 있었다. 

 

원인을 찾다보니 내가 진행 중인 프로젝트에 아래와 같은 태그가 xml 파일에 존재하지 않아 생겼던 문제였다. 

<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>

이 태그를 설정하니 손쉽게 해결했고, 태그의 역할에 대해 알아보자. 

<tx:annotation-driven>

이 태그는 @Transactional을 사용하여 선언적 트랜잭션 관리를 하겠다는 의미이다. 

즉, @Transactional이 정상 작동하려면 이 태그가 존재해야 한다.

transaction-manager="transactionManager"

이 속성은 말 그대로 TransactionManager를 설정하는 속성이다. 

여기에 등록된 TrasnactionManager가 기본 값으로 사용된다.

다중 DB 커넥션이 필요한 경우 별도로 TransactionManager를 생성하게 되는데, 이 경우 @Transactional의 value 설정에 해당 TransactionManager의 이름을 등록해주면된다.

proxy-target-class="true"

동적 프록시 생성 방식을 결정하는 속성이다.

기본 값은 false로 JDK 동적 프록시를 사용하게 된다. true일 경우 CGLIB를 사용하게 된다. 

두 프록시 생성 방식의 차이는 구체 클래스를 통해 프록시를 생성할 수 있냐 아니냐에 있다. 

'Spring' 카테고리의 다른 글

Spring - @Transactional 활용해 멀티 스레드 환경을 테스트할 때 주의점  (0) 2024.05.06
Spring - 요청 파라미터 LocalDate타입 필드에 바인딩 및 검증  (1) 2024.04.12
Spring - Springboot + Vue3 + MariaDB를 카페24에 배포할 때 Tip(주의사항)  (1) 2023.07.26
Spring - ArgumentResolver  (0) 2023.07.22
Spring - DispatcherServlet에 대해 알아보자  (0) 2023.07.15
    'Spring' 카테고리의 다른 글
    • Spring - @Transactional 활용해 멀티 스레드 환경을 테스트할 때 주의점
    • Spring - 요청 파라미터 LocalDate타입 필드에 바인딩 및 검증
    • Spring - Springboot + Vue3 + MariaDB를 카페24에 배포할 때 Tip(주의사항)
    • Spring - ArgumentResolver
    ahlight
    ahlight

    티스토리툴바