📋 문제 상황
증상
- 커스텀 예외 클래스의 생성자는 정상 실행됨
- 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);
}
}
🧪 해결 확인
테스트 방법
- 중복 이메일로 회원 등록 API 호출
- 콘솔 로그 확인
정상 동작 시 출력
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
의 위치가 컴포넌트 스캔 범위를 결정한다!
'Spring' 카테고리의 다른 글
백엔드에서 API 응답의 HTTP 상태코드를 설정하는 방법과 원칙 (0) | 2025.06.26 |
---|---|
스프링 부트 SQLite DB 연결 (0) | 2025.06.24 |
Spring Boot 설정 시스템 application.properties, application-dev.yml (0) | 2025.06.24 |
스프링에서 받을 데이터는 '명시적'이어야 한다. (0) | 2025.06.13 |
스프링 빈의 종류 : 스코프 (1) | 2025.06.10 |