DEV/Spring

[트러블슈팅][Spring] ApplicationRunner RuntimeException 발생 시 shutdown되는 오류 해결

Bi3a 2024. 2. 20. 10:34

목차
반응형

트러블슈팅
안되는 이유는 모르겠고 되는 이유는 더더욱 모르겠을 때


 

 

문제 개요

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로 처리될 수 있게 하자!

 


 

반응형