이 글에서 다루는 내용

JWT 기반 인증을 사용하는 Spring Boot 프로젝트에서 소스 코드에 InMemoryUserDetailsManager 관련 구현이 전혀 없음에도 불구하고 prod 환경 로그에 Using generated security passwordGlobal AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager 메시지가 출력되는 현상을 다룬다. 원인이 되는 Spring Boot의 자동 구성 메커니즘을 분석하고, JWT 아키텍처에서 왜 전통적인 인증 빈들이 정의되지 않는지 설명한 뒤, 세 가지 해결 방법을 비교한다.

문제 상황

Prod 환경에서 애플리케이션 시작 시 다음과 같은 로그가 출력되었다.

Using generated security password: 80d71e52-107f-4061-8946-4fb1e3a0de62
Global AuthenticationManager configured with UserDetailsService bean with name inMemoryUserDetailsManager

이상한 점은 소스 코드 어디에도 InMemoryUserDetailsManager를 선언한 부분이 없다는 것이었다. grep으로 확인해봐도 관련 구현이 전혀 발견되지 않았다.

grep -r "InMemoryUserDetailsManager\|inMemoryUserDetailsManager" src/ --include="*.kt" --include="*.java" -l
# 결과 없음

grep -r "UserDetailsService\|loadUserByUsername" src/ --include="*.kt" --include="*.java" -l
# 결과 없음

유일한 Security 관련 파일은 SecurityConfig.kt 하나뿐이었다.

원인 분석

원인은 외부 라이브러리나 에이전트가 아니라 Spring Boot의 UserDetailsServiceAutoConfiguration이었다. 이 자동 구성은 다음 두 조건이 모두 충족될 때 활성화된다.

첫째, spring-boot-starter-security가 클래스패스에 존재해야 한다. build.gradle.kts에 의존성으로 포함되어 있으면 조건이 충족된다.

둘째, AuthenticationManager, AuthenticationProvider, UserDetailsService 빈이 하나도 정의되지 않아야 한다.

현재 프로젝트는 JWT 기반 인증을 사용하며, JwtAuthenticationFilter에서 SecurityContextHolderAuthentication을 직접 주입하는 방식이다. 따라서 위 세 빈 중 어떤 것도 등록되어 있지 않아 자동 구성이 작동했고, 그 결과 기본 계정과 InMemoryUserDetailsManager가 생성되어 prod 로그까지 남게 된 것이다.

왜 세 빈이 하나도 없는가

이것은 버그가 아니라 JWT 아키텍처의 자연스러운 결과다.

전통적인 Form Login 방식에서는 요청이 들어올 때마다 AuthenticationManagerAuthenticationProvider를 호출하고, UserDetailsService.loadUserByUsername()으로 DB에서 사용자를 조회한 뒤 비밀번호를 검증하는 흐름을 거친다. 이 과정 전체가 Session에 기반하여 동작한다.

반면 JWT 방식에서는 토큰 자체에 사용자 정보가 담겨 있고 서명으로 위변조를 검증하므로 DB에서 사용자를 매번 조회할 필요가 없고, 요청마다 비밀번호를 보내지도 않는다. 따라서 UserDetailsService, AuthenticationProvider, AuthenticationManager 모두 불필요하다.

최초 로그인 시 ID/PW 검증 로직은 일반적으로 AuthService 레이어에서 PasswordEncoder.matches()로 직접 처리하고 JWT를 발급하는 패턴을 사용한다. Spring Security의 인증 체계를 우회하므로 역시 세 빈이 필요하지 않다.

항목 전통 방식 JWT 방식
요청마다 인증 Session 조회 토큰 검증
UserDetailsService 필요 불필요
AuthenticationProvider 필요 불필요
AuthenticationManager 필요 불필요
자동 구성 비활성 활성 → 기본 계정 생성

해결 방법

세 가지 접근 방법이 있으며, 프로젝트 상황에 따라 선택할 수 있다.

1. Application 클래스에서 자동 구성 제외 (권장)

가장 의도가 명확한 방법이다. UserDetailsService를 전혀 사용하지 않는다는 사실을 코드로 드러낸다.

@SpringBootApplication(exclude = [UserDetailsServiceAutoConfiguration::class])
class BlogSpringServerApplication

2. application.yml에서 제외

설정 파일로 분리하고 싶을 때 유용하다.

spring:
  autoconfigure:
    exclude:
      - org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration

3. AuthenticationManager 빈 등록

세 빈 중 하나라도 등록되면 자동 구성이 비활성화된다. 향후 AuthenticationManager를 실제로 사용할 계획이 있다면 이 방법이 적합하다.

@Bean
fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
    config.authenticationManager

현재 프로젝트처럼 UserDetailsService를 전혀 사용하지 않는 JWT 기반 구조에서는 1번이 가장 명확하다.