목차
반응형

문제 개요
Spring ApplicationRunner 내 run 메서드에서 RuntimeException 발생 시
ExceptionHandler로 핸들링 설정을 했음에도 Spring Boot는 애플리케이션을 종료하는 현상이 있다.
원인 분석
Checked Exception과 Unchecked Exception
RuntimeException 을 상속하지 않은 클래스는 Checked Exception,
반대로 상속한 클래스는 Unchecked Exception으로 분류할 수 있다.
- Unchecked Exception : 명시적인 예외 처리를 강제하지 않는다.
- try ~ catch로 예외를 잡거나 throw 로 호출한 메서드에게 예외를 던진다.
- 트랜잭션 내에서 예외 발생 시 롤백된다.
- Checked Exception : 명시적인 예외 처리를 강제한다.
- try ~ catch로 예외를 잡거나 throw로 호출한 메소드에게 예외를 던질 필요가 없다.
- 트랜잭션 내에서 예외 발생 시 롤백하지 않는다.
- 예외 복구 전략이 별도 마련되어 있기 때문에 롤백을 진행하지 않는 것이다.
- 예외 복구 전략에는 예외 전환, 예외 회피, 예외 복구 등이 있다.
ApplicationRunner의 동작 원리
- ApplicationRunner는 Spring Boot Application이 실행될 때, 다양한 초기화 작업이 순차적으로 수행된다.
- 이러한 초기화 과정 중 하나로 ApplicationContext 가 로드되고 구성되며 애플리케이션의 빈들이 생성된다.
- 그 후에 ApplicationRunner 의 구현체들의 run 메서드가 호출되어 특정 작업을 수행한다.
- 즉, ApplicationRunner는 Spring Boot 애플리케이션 초기화 과정 중 일부로 실행된다.
GlobalException.java
java
닫기@Getter public class GlobalException extends RuntimeException { private final RsData<Empty> rsData; public GlobalException() { this("400-0", "에러"); } public GlobalException(String msg) { this("400-0", msg); } public GlobalException(String resultCode, String msg) { super("resultCode=" + resultCode + ",msg=" + msg); this.rsData = RsData.of(resultCode, msg); } public static class E404 extends GlobalException { public E404() { super("404-0", "데이터를 찾을 수 없습니다."); } } public static class E403 extends GlobalException { public E403() { super("403-0", "잘못된 접근입니다."); } } public static class STATION_NOT_FOUND extends GlobalException { public STATION_NOT_FOUND() { super("STATION_NOT_FOUND", "충전소가 존재하지 않습니다."); } } }
GlobalExceptionHandler.java
java
닫기@ControllerAdvice @RequiredArgsConstructor public class GlobalExceptionHandler { private final Rq rq; @ExceptionHandler(Exception.class) public ResponseEntity<Object> handleException(Exception ex) { // 아래 `throw ex;` 코드는 API 요청이 아닌 경우에만 실행 return handleApiException(ex); } // 자연스럽게 발생시킨 예외처리 private ResponseEntity<Object> handleApiException(Exception ex) { Map<String, Object> body = new LinkedHashMap<>(); body.put("resultCode", "500-1"); body.put("statusCode", 500); body.put("msg", ex.getLocalizedMessage()); LinkedHashMap<String, Object> data = new LinkedHashMap<>(); body.put("data", data); StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); ex.printStackTrace(pw); data.put("trace", sw.toString().replace("\t", " ").split("\\r\\n")); String path = rq.getCurrentUrlPath(); data.put("path", path); body.put("success", false); body.put("fail", true); return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR); } // 개발자가 명시적으로 발생시킨 예외처리 @ExceptionHandler(GlobalException.class) @ResponseStatus // error 내용의 스키마를 타입스크립트화 public ResponseEntity<RsData<Empty>> handle(GlobalException ex) { HttpStatus status = HttpStatus.valueOf(ex.getRsData().getStatusCode()); rq.setStatusCode(ex.getRsData().getStatusCode()); return new ResponseEntity<>(ex.getRsData(), status); } }
- 이와 같이 정상적인 ExceptionHandler 의 설계 의도대로라면 ApplicationRunner에서 발생하는 GlobalException의 경우에도 이렇게 Exception handling에 의해 ResponseEntity가 반환이 되어야 하지만 그렇지 않았다.
- Spring Boot Application이 Initialize, 즉 초기화된 이후 실행된 후에는 정상적으로 동작했다.
ReportService.java (문제의 RuntimeException을 ApplicationRunner에 던졌던 친구)
java
닫기@Transactional public ReportResponseDto complete( ReportCompleteRequestDto requestDto, Long reportId, String username ) { Report report = findById(reportId); TechnicalManager manager = technicalManagerService.findByName(username); if (!manager.getChargingStation().equals(report.getChargingStation())) { throw new GlobalException.E403(); } if (report.isCompleted()) { throw new GlobalException("이미 처리가 완료된 신고내용입니다."); } report.complete(requestDto, manager); return new ReportResponseDto(report); }
ApplicationRunner Exception 발생 시 Spring Boot가 종료되는 이유
결론은 Checked Exception이든 UncheckedException 이든 스프링부트가 실행되고 Bean을 초기화하는 과정에서 발생하는 예외는 Spring의 ExceptionHandler가 IOC 컨테이너의 Bean으로 등록되기 이전에 수행되는 과정이기 때문에,
ExceptionHandler 의 예외 처리 방식을 타는 것이 아닌, java의 예외 처리 정책을 수행하기 때문에 발생하는 문제였다.
즉, 정상적인 구현 의도와 발생한 문제는 아래와 같은 차이점이 있다.
[Spring Boot Initialize 이후]
- @GlobalExceptionHandler , @ControllerAdvice 가 정상적으로 Bean 객체로 등록된 시점
- 1. GlobalException 발생
- 2. GlobalExceptionHandler 를 거친 후 의도대로 ResponseEntity를 반환한다.
[Spring Boot Initialize 이전]
- ApplicationRunner 수행시점
- @GlobalExceptionHandler 와 같은 @ControllerAdvice 가 Bean 객체로 등록되기 이전 시점
- 1. GlobalException 발생
- 2. GlobalException 이 GlobalExceptionHandler 를 거치지 않는다.
- 3. @SpringBootApplcation의 run 메소드에서 해당 예외를 java의 main으로 던진다.
- 4. 적절한 예외 처리 로직이 main에 구비되지 않았으므로 JVM은 프로그램을 종료시킨다.
해결 과정
try-catch 사용
java
닫기@Bean @Order(3) @SneakyThrows(RuntimeException.class) public ApplicationRunner initNotProd() { return args -> { try { self.makeTestUser(); self.makeTestTechnicalManager(); self.makeTestReport(); self.makeTestReportResult(); } catch (Exception e) { e.printStackTrace(); log.error("샘플 데이터 생성 중 에러 발생"); } }; }
명시적으로 try-catch를 선언해서 ApplicationRunner에서 실행되는 run 메서드가 Exception을 발생시켜도 이를 catch 하고 로그를 반환하는 것으로 처리해 Spring Boot Initialize의 shutdown을 방지했다.
결론
ApplicationRunner는 Spring Boot application 실행 후 Bean Context를 Initialize하기 이전에 실행된다.
따라서, 이에 따른 Exception은 명시적으로 try-catch로 처리될 수 있게 하자!
반응형
'DEV > Spring' 카테고리의 다른 글
[Spring] SecurityConfig Spring Method Security 적용하기 (0) | 2024.02.20 |
---|---|
[트러블슈팅][Spring] 의존성 주입 간 순환참조 문제 해결 방법 (0) | 2024.02.20 |
[트러블슈팅][Spring] .sql 파일 init이 자동으로 실행되지 않는 문제 원인 / 해결방법 (0) | 2024.02.20 |
[트러블슈팅][Spring] JPA OneToOne 양방향 관계 시 의도하지 않은 N+1 문제 발생 원인과 해결 방법 (0) | 2024.02.20 |
[트러블슈팅] @ReqestParam, @PathVariable 사용 시 Name for argument of type [”type”] not specified 에러 해결 (0) | 2024.02.19 |