JavaScript

웹 개발의 필수 언어

동적인 웹 페이지 구현을 위한 핵심 프로그래밍 언어.

Java

객체지향 프로그래밍

안정적이고 확장성 있는 백엔드 개발의 대표 언어.

HTML

웹의 기초

웹 페이지의 구조를 정의하는 마크업 언어.

React

현대적 UI 라이브러리

효율적인 사용자 인터페이스 구축을 위한 JavaScript 라이브러리.

CSS

웹 디자인의 핵심

웹 페이지의 시각적 표현을 담당하는 스타일 언어.

Spring

자바 웹 프레임워크

기업급 애플리케이션 개발을 위한 강력한 프레임워크.

Spring

🚨 Spring Boot 컴포넌트 스캔 범위 문제 트러블 슈팅

lamarcK 2025. 6. 26. 08:56

📋 문제 상황

증상

  • 커스텀 예외 클래스의 생성자는 정상 실행됨
  • GlobalExceptionHandler의 메서드가 실행되지 않음
  • 예외 발생 시 Spring의 기본 에러 응답만 반환됨

환경

  • Spring Boot 프로젝트
  • 커스텀 예외: DuplicateMemberException
  • 전역 예외 처리: @RestControllerAdvice 사용

증상 재현 코드

// Service에서 예외 발생
throw new DuplicateMemberException("이미 존재하는 이메일입니다");

// Exception 클래스 (정상 동작)
public class DuplicateMemberException extends RuntimeException {
    public DuplicateMemberException(String message) {
        super(message);
        System.out.println("1단계: 오류 감지"); // ✅ 출력됨
    }
}

// GlobalExceptionHandler (동작하지 않음)
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(DuplicateMemberException.class)
    public ResponseEntity<ErrorResponse> handleDuplicateMemberException(DuplicateMemberException e) {
        System.out.println("🛠️ 2단계: ExceptionHandler 실행"); // ❌ 출력 안됨
        // ...
    }
}

🔍 문제 원인 분석

패키지 구조 문제

src/main/java/
└── com/example/exp/
    ├── member/
    │   ├── MemberApplication.java      // @SpringBootApplication 위치
    │   ├── controller/
    │   └── service/
    └── exception/
        └── GlobalExceptionHandler.java  // 스캔 범위 밖!

Spring Boot 컴포넌트 스캔 규칙

  • @SpringBootApplication이 있는 패키지를 기준점으로 설정
  • 현재 패키지 + 하위 패키지만 스캔
  • 형제 패키지는 스캔하지 않음

스캔 범위 분석

@SpringBootApplication  // com.example.exp.member
├── ✅ com.example.exp.member.*        (스캔됨)
├── ✅ com.example.exp.member.controller.*  (스캔됨)  
├── ✅ com.example.exp.member.service.*     (스캔됨)
└── ❌ com.example.exp.exception.*     (스캔 안됨!)

해결방법

방법 1: Application 클래스를 루트 패키지로 이동 (권장)

Before:

src/main/java/
└── com/example/exp/
    ├── member/
    │   └── MemberApplication.java  // 여기에 있었음
    └── exception/
        └── GlobalExceptionHandler.java

After:

src/main/java/
└── com/example/exp/
    ├── MemberApplication.java      // 루트로 이동!
    ├── member/
    │   ├── controller/
    │   └── service/
    └── exception/
        └── GlobalExceptionHandler.java

변경할 코드:

// 패키지 경로 변경
package com.example.exp;  // 이전: com.example.exp.member

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

방법 2: ComponentScan 범위 명시적 설정

package com.example.exp.member;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = "com.example.exp")  // 전체 exp 패키지 스캔
public class MemberApplication {
    public static void main(String[] args) {
        SpringApplication.run(MemberApplication.class, args);
    }
}

🧪 해결 확인

테스트 방법

  1. 중복 이메일로 회원 등록 API 호출
  2. 콘솔 로그 확인

정상 동작 시 출력

1단계: 오류 감지
🛠️ 2단계: ExceptionHandler 실행

HTTP 응답 확인

{
  "code": "DUPLICATE_MEMBER",
  "message": "이미 존재하는 이메일입니다: test@example.com",
  "timestamp": "2024-01-01T10:00:00"
}

⚠️ 추가 체크포인트

1. try-catch 블록 확인

// ❌ Controller에서 예외를 잡으면 GlobalExceptionHandler로 전달되지 않음
@PostMapping
public ResponseEntity<MemberResponseDto> createMember(@RequestBody MemberRequestDto requestDto) {
    try {
        return memberService.createMember(requestDto);
    } catch (RuntimeException e) {  // 이렇게 하면 GlobalExceptionHandler 우회
        return ResponseEntity.badRequest().build();
    }
}

// ✅ try-catch 제거하여 GlobalExceptionHandler로 전달
@PostMapping  
public ResponseEntity<MemberResponseDto> createMember(@RequestBody MemberRequestDto requestDto) {
    return memberService.createMember(requestDto);
}

2. @RestControllerAdvice 어노테이션 확인

@RestControllerAdvice  // ✅ 필수!
public class GlobalExceptionHandler {
    // ...
}

📚 예방법

1. 표준 패키지 구조 사용

src/main/java/
└── com/company/project/
    ├── Application.java        // 루트에 배치
    ├── controller/
    ├── service/  
    ├── repository/
    ├── dto/
    ├── entity/
    ├── exception/             // 전역 예외 처리
    └── config/               // 설정 클래스

2. 컴포넌트 스캔 로그 활성화

# application.yml
logging:
  level:
    org.springframework.context.annotation: DEBUG

3. 애플리케이션 시작 시 빈 등록 확인

@SpringBootApplication
public class MemberApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(MemberApplication.class, args);

        // GlobalExceptionHandler가 빈으로 등록되었는지 확인
        try {
            GlobalExceptionHandler handler = context.getBean(GlobalExceptionHandler.class);
            System.out.println("✅ GlobalExceptionHandler 빈 등록 성공");
        } catch (NoSuchBeanDefinitionException e) {
            System.out.println("❌ GlobalExceptionHandler 빈 등록 실패");
        }
    }
}

🎯 핵심 요약

구분 내용
문제 GlobalExceptionHandler가 동작하지 않음
원인 Spring Boot 컴포넌트 스캔 범위 밖에 위치
해결 @SpringBootApplication을 루트 패키지로 이동
검증 예외 발생 시 두 단계 로그 모두 출력 확인

핵심: @SpringBootApplication의 위치가 컴포넌트 스캔 범위를 결정한다!