🐛이슈
- 발생한 오류 copy & paste
- 오류가 발생한 상황 간략히 설명
각자 API를 구현하던 중 팀원분이 [응답 통일 및 에러 핸들러]를 구현해서 PR을 올려주셨다. API 역할 분배할 때 되도록이면 엔티티별로 나눠놔서 내가 맡은 Transmission 부분을 끝내고 다음날 아침에 pull을 받았다. 충돌 좀 해결하고 Swagger로 테스트하려고 했는데, Swagger에 접속하면 Failed to load API definition.라는 문구와 함께 아래 화면이 뜨는 오류가 발생했다.
설명을 보면 API 정의해 둔 걸 로드하는 데 실패했다는 뜻이고, 응답 상태가 500이면 Server Error인 거라 localhost:8080/v3/api-docs 경로로 접속해 봤지만 에러에 대한 자세한 설명이 없었다. IntelliJ에서 확인한 에러는 아래와 같다.
jakarta.servlet.ServletException: Handler dispatch failed: java.lang.NoSuchMethodError: 'void org.springframework.web.method.ControllerAdviceBean.<init>(java.lang.Object)'
😓원인과 해결방안
- 오류가 발생한 원인 간략히 설명
- 해결방안 설명
위의 에러 메세지를 보면 아마 ControllerAdviceBean의 init 메서드가 존재하지 않는다는 뜻인 것 같아서 ControllerAdviceBean이나 위의 에러 메세지를 검색해 봤지만 참고할만한 자료는 찾지 못했다.
해결방안1
PR 올리신 걸 pull 받기 전까진 Swagger가 정상적으로 작동했어서 내 코드가 문제인지 확인하려고 팀원분께 여쭤봤는데, 에러 핸들러 코드를 추가했을 때부터 Swagger가 작동하지 않았다고 알려주셨다. 계속 고민하고 있었는데, 팀원분이 ExceptionAdvice class의 @RestControllerAdvice 애노테이션을 주석 처리하니까 잘 돌아간다고 알려주셨다.
@Slf4j
// @RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
...
}
@RestControllerAdvice
- 여기서 @RestControllerAdvice는 @RestController가 붙은 대상에서 Exception이 발생하는 것을 감지하는 역할을 한다.
- 하나의 class로 여러 개의 RestController에 대해 전역적으로 ExceptionHandler를 적용할 수 있게 해준다.
- 참고로 @RestController가 붙은 메서드에서 호출한 다른 class의 모든 메서드에 대해서도 동작한다. 예를 들어 Service에서 예외가 발생하더라도 그 메서드를 호출한 Controller에서 예외가 발생한 것으로 판단한다.
ExceptionAdvice class
- @RestControllerAdvice 애노테이션이 붙어있고, @RestController가 붙은 모든 Controller의 에러를 처리하는 핸들러 역할을 한다.
- Exception이 발생했을 때 모든 필드를 검사한 뒤, 필드마다 필드 이름과 에러 메시지를 통일해 둔 API 응답 값에 맞춰서 보여준다.
- 메서드마다 적힌 애노테이션이나 class에 맞는 Exception이 발생하면, 해당 메서드가 캐치해 부모인 ResponseEntityExceptionHandler의 handleExceptionInternal 메서드로 Exception을 넘기는 걸 확인할 수 있다.
물론 Swagger가 정상적으로 작동하게 된 건 다행이지만... 저 애노테이션을 주석 처리하면 에러 핸들러가 작동하지 않게 돼서, 에러가 발생했을 때 API 응답 값이 지저분하게 나오거나 에러와 맞지 않는 응답(400 에러가 500 에러로 나오는 등)이 나오게 된다.
- 프론트엔드랑 빠르게 연동하는 게 우선이라고 생각해서 Swagger를 살려두고 에러 핸들러를 포기하기로 했다.
해결방안2
이전에 7월, 8월에 개발한 프로젝트에서도 [응답 통일 및 에러 핸들러]에 동일한 코드를 사용했었다는 게 생각나서 그 프로젝트랑 HY-THON 프로젝트가 어떤 차이가 있는지 살펴봤다. 우선 에러 핸들러에 영향을 받는 service, controller, converter 같은 건 다 넘기고, 에러 핸들러에 영향을 주는 것들을 먼저 찾아봤다.
SwaggerConfig class는 코드가 거의 동일해서 이것도 넘기고, 라이브러리와 관련된 build.gradle을 비교해 봤다. 여기서 Spring Boot 버전이나 의존성 관리 버전, Swagger를 쓰기 위해 추가해야 하는 라이브러리의 버전이 다른 걸 확인했다.
- 버전을 아래처럼 변경하고 애노테이션 주석을 삭제한 뒤 서버를 돌려봤는데, Swagger가 정상적으로 돌아갔다.
plugins {
...
id 'org.springframework.boot' version '3.3.1' // 기존 3.4.0
id 'org.spring.dependency-management' version '1.1.5' // 기존 1.1.6
}
...
dependencies {
...
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' // 기존 2.0.3
...
}
다시 확인해 본 결과, 나머지는 그대로 두고 Spring Boot 버전만 3.3.1 또는 3.3.6으로 낮춰도 제대로 돌아가는 걸 확인했다. 앞으로 Spring Boot 최신 버전은 되도록이면 사용하지 않는 게 좋을 것 같다.
해결방안3
아래 OpenAPI 3 공식 문서에서 'Error Handling for REST using @ControllerAdvice'가 있길래 해당 내용에 대해 더 찾아봤고, stackoverflow에서 똑같은 오류를 만난 사람이 올린 글을 찾았다. 답변을 요약해 보면,
@ControllerAdvice 애노테이션과 Swagger 사이에 일어난 충돌이다.
@ControllerAdvice가 없다면 Swagger는 controller class를 가져와 API 정의를 분석한다.
@ControllerAdvice가 있다면 Swagger 응답이 내가 응답을 통일해 둔 방식으로 변환되고, Swagger 웹 페이지는 응답 구조를 인식할 수 없게 된다.
프로젝트의 패키지에서 @ControllerAdvice가 적용될 곳을 명시하면 Swagger 응답이 변환되지 않아 충돌을 해결할 수 있다.
따라서 주석 처리했던 코드를 아래와 같이 수정하면 된다고 한다.
@Slf4j
@RestControllerAdvice(annotations = {RestController.class}, basePackages = {"com.example.HyThon.web.controller"})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
...
}
나는 위에 있는 코드로 수정해도 돌아가지 않아서, 아래처럼 작성했더니 돌아가는 걸 확인할 수 있었다.
@Slf4j
@RestControllerAdvice(annotations = {RestController.class}, basePackageClasses = {DiaryController.class, MemberController.class, TransmissionController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
...
}
🪽참고자료
- 링크 기록