개발 팁
SpringBoot 3 (Spring 6.x) Error Response Formatting
북치던노인
2024. 4. 2. 20:02
자체 요구 사항
- response의 http status를 원하는 코드로 응답할 수 있을 것
- 커스텀 프로퍼티를 추가할 수 있을 것
- Spring 기본 에러 응답과 동일한 형태의 response json 구조를 유지 할 것
- 별도 라이브러리 안쓸것
공통 - 커스텀 프로퍼티 없는 경우
ResponseStatusException 사용
throw ResponseStatusException(HttpStatus.LOCKED, "잠겨있는 리소스 입니다.")
방법 1 - spring.mvc.problemdetails.enabled: true
application.yml 에 아래 설정을 추가한 뒤,
spring.mvc.problemdetails.enabled: true
아래와 같은 형태로 커스텀 익셉션을 구성해서 throw.
import org.springframework.http.HttpStatus
import org.springframework.web.ErrorResponseException
class UserAlreadyExistException : ErrorResponseException(
HttpStatus.CONFLICT
) {
init {
this.body.setProperty("a", "2")
}
}
이 경우 에러 response는 아래와 같은 형태가 된다.
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"instance": "/users/123123",
"a": "2"
}
각 필드는 익셉션 안에서 ErrorResponseException을 override 하거나, init 블록에서 수정하는 식으로 변경 가능
단점 : stacktrace나 exception 종류를 확인 할 수 없음
방법 2 - spring.mvc.problemdetails.enabled: false (기본값)
response 필드가 조금 다름
{
"timestamp": "2024-04-02T10:21:46.812+00:00",
"status": 409,
"error": "Conflict",
"path": "/users/123123"
}
노출시킬 필드를 프로퍼티 파일로 설정 가능
server.error:
include-message: always
include-binding-errors: always
include-stacktrace: always
include-exception: true
모두 포함시키면 아래와 같이 출력됨
{
"timestamp": "2024-04-02T10:13:12.616+00:00",
"status": 409,
"error": "Conflict",
"exception": "***.service.user.UserAlreadyExistException",
"trace": "***.service.user.UserAlreadyExistException: 409 CONFLICT, ProblemDetail[type='about:blank', title='Conflict', status=409, detail='null', instance='null', properties='{a=2}']\n\tat ***.service.user.UserService.addUser(UserService.kt:16)\n\tat ***.controller.UserController.addUser(UserController.kt:23)\n(생략)",
"message": "409 CONFLICT, ProblemDetail[type='about:blank', title='Conflict', status=409, detail='null', instance='null', properties='{a=2}']",
"path": "/users/121231"
}
단점 1) 커스텀 프로퍼티가 출력되지 않는다.
CustomErrorAttributes 컴포넌트를 구현해줘야 한다.
import org.springframework.boot.web.error.ErrorAttributeOptions
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes
import org.springframework.stereotype.Component
import org.springframework.web.ErrorResponseException
import org.springframework.web.context.request.WebRequest
@Component
class CustomErrorAttributes: DefaultErrorAttributes() {
override fun getErrorAttributes(webRequest: WebRequest?, options: ErrorAttributeOptions?): MutableMap<String, Any> {
val attributes = super.getErrorAttributes(webRequest, options)
val error = getError(webRequest)
if (error is ErrorResponseException && error.body.properties != null) {
attributes.putAll(error.body.properties!!)
}
return attributes
}
}
단점 2) GET 메서드인 경우 브라우저에서 접근시 /error 매핑으로 이동하므로 아래와 같은 에러 화면이 보인다.
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Tue Apr 02 18:57:04 KST 2024
There was an unexpected error (type=Locked, status=423).
아래와 같이 비활성화 시킬 수 있지만, 그러면 톰캣 정보가 출력 됨 (ResponseStatusException을 반환한 경우)
server.error.whitelabel.enabled: false
싫으면 ErrorController를 구현해야 함 (https://sh970901.tistory.com/132 참고)
방법 3 - @ExceptionHander 사용
Exception에 따로 어노테이션을 달거나 상속받거나 하지 않고 처리하는 방법
Controller에 아래 내용 추가
@ExceptionHandler(UserAlreadyExistException::class)
fun handle(e: UserAlreadyExistException): ProblemDetail {
val problemDetail = ProblemDetail.forStatus(HttpStatus.CONFLICT)
problemDetail.setProperty("a", "1")
return problemDetail
}
response 형태는 spring.mvc.problemdetails.enabled = true 일 때와 동일함
{
"type": "about:blank",
"title": "Conflict",
"status": 409,
"instance": "/users/new",
"a": "1"
}
Controller 구분 없이 전역으로 적용 하고 싶을 땐
@RestControllerAdvice(annotations = RestController.class)
public class GlobalRestControllerAdvice {
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(UserAlreadyExistsException::class)
fun handle(e: UserAlreadyExistsException): ProblemDetail {
단점 : 1번 방법일때랑 동일..
결론
1. 제일 간단한 방법
throw ResponseStatusException(HttpStatus.LOCKED, "잠겨있는 리소스 입니다.")
2. throw 된 exception을 다른 로직에서 catch 해서 사용하기도 하는 경우
@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Data Not Found")
public class DataNotFoundException extends RuntimeException { }
3. custom property가 필요한 경우 (stack trace도 보고싶음)
import org.springframework.http.HttpStatus
import org.springframework.web.ErrorResponseException
class UserAlreadyExistException : ErrorResponseException(
HttpStatus.CONFLICT
) {
init {
this.body.setProperty("a", "2")
}
}
import org.springframework.boot.web.error.ErrorAttributeOptions
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes
import org.springframework.stereotype.Component
import org.springframework.web.ErrorResponseException
import org.springframework.web.context.request.WebRequest
@Component
class CustomErrorAttributes: DefaultErrorAttributes() {
override fun getErrorAttributes(webRequest: WebRequest?, options: ErrorAttributeOptions?): MutableMap<String, Any> {
val attributes = super.getErrorAttributes(webRequest, options)
val error = getError(webRequest)
if (error is ErrorResponseException && error.body.properties != null) {
attributes.putAll(error.body.properties!!)
}
return attributes
}
}
4. custom property가 필요한 경우 (단 특정 Controller에서만 사용, stack trace 안봄)
class DataNotFoundException(val type: String): RuntimeException() {
}
@ExceptionHandler(DataNotFoundException::class)
fun handle(e: DataNotFoundException): ProblemDetail {
val problemDetail = ProblemDetail.forStatus(HttpStatus.CONFLICT)
problemDetail.setProperty("type", e.type)
return problemDetail
}
5. custom property가 필요한 경우(모든 Controller에서 사용, stack trace 안봄)
spring.mvc.problemdetails.enabled: true
import org.springframework.http.HttpStatus
import org.springframework.web.ErrorResponseException
class UserAlreadyExistException : ErrorResponseException(
HttpStatus.CONFLICT
) {
init {
this.body.setProperty("a", "2")
}
}
또는
@RestControllerAdvice(annotations = RestController.class)
public class GlobalRestControllerAdvice {
@ResponseStatus(HttpStatus.NOT_FOUND)
@ExceptionHandler(UserAlreadyExistsException::class)
fun handle(e: UserAlreadyExistsException): ProblemDetail {