서버 예외 처리 가이드북
서버 예외 처리 가이드북 — #개발자의도구들 #서버예외 #서버예외처리 #ExceptionHandler 🚨📝 📖 📒✏️💡🔍 AI스쿨 ...
#개발자의도구들 #서버예외 #서버예외처리 #ExceptionHandler
🚨📝 📖 📒✏️💡🔍
AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다
\* 현재 Spring x 프로젝트를 진행하고 있습니다. 전체 목차를 보시려면 여기를 눌러주세요.
\* 해당 프로젝트 이전에 공부한 내용을 보시려면 여기를 눌러주세요.
예외 처리 종결내기
오늘 하루종일 Spring project 전체 서비스를 손봤습니다. 사실 생각보다 너무 오래걸려서 1/3 정도 밖에 못한건 비밀이지만요..
그동안 만들었던 서비스를 다시한번 손보는건 매우 험난한 작업인 것 같습니다. 뭔가 제자리를 걷는 것 같은 느낌도 들고, 시간낭비하나 싶습니다. 하지만, 계속해서 대충 보고 넘어간 곳에서 문제가 반복되니, 이 점들을 고친다면, 분명히 나중에는 시간을 오히려 버는 일이 될거라고 생각합니다.
서비스를 손본 메인 이유는 바로 예외처리 때문인데요. 이전글에서도 다뤘다 싶이. 최근에 서버 응답을 잘못 설계해서 응답에 대한 부분을 모두 변경하였는데요. 변경하면서 예외 처리를 안할 수 없었기 때문에 예외처리 공부도 모두 다시 진행하였습니다
REST 기능별 예외 종류
서버가 처리해야할 예외는 수없이 많아서 개발자가 어떤 예외를 처리해줘야 하는지에 대한 판단을 내리기가 쉽지 않습니다.
그래도 웹서버는 기본적으로 REST를 기반으로 동작하기 때문에, REST 동작시 발생하는예외 상황을 잘 정리해두면 개발하는데 있어서 매우 편리합니다.
| ✏️ Save: Unique(duplicated) 예외 ✏️ Delete: NotFound 예외 (기본적으로 찾고, 값을 제거함) ✏️ GET: NotFound 예외 |
|---|
Front vs MSA
Fonrt서버의 경우 클라이언트와 직접전인 연결이 있으므로, 최소한의 정보를 제공하는 것이 원칙입니다.
반면, MSA의 경우 서버간의 응답을 주고 받기 때문에 디버깅용으로 상세한 응답이 필요합니다.
MSA의 오류 코드
| ⛓️💥 CircuitBreaker ⭐⭐⭐- CircuitBreaker는 기본적으로 5xx 응답을 인식하여 fall count를 증가시킵니다.- 4xx 에러를 정상처리로 인식하게 할 수 있습니다. |
|---|
저는 MSA 구조에서 에러처리를 할 때 서버 전용 에러 코드를 만들어서 처리하고 있습니다. 이런 경우
package org.example.timelineapiserver.enum
enum class MSAServerErrorCode(val code: Int) {
SUCCESS(1),
UNKNOWN_ERROR(9999),
// DB integrity
DUPLICATED_ID(10000),
INVALID_ID(10001),
// Valid
NOT_NULL(20000),
NOT_BLANK(20001),
NOT_PATTERN(20002);
companion object {
/**
* 숫자 에러 코드로부터 MSAServerErrorCode를 반환합니다.
* 매칭되지 않는 경우 null을 반환합니다.
*/
fun fromCode(code: Int): MSAServerErrorCode? =
entries.find { it.code == code }
}
}
이런식으로 코드를 확장하여 서버간에 통신할 전용 코드를 만들었습니다.
ExceptionHandler
Client/Server
🤵 FrontServer
// ✏️ 프론트 서버
// 1. Business 에러 처리를 위해 ExceptionHandler러에 아래 코드를 등록합니다.
open class BusinessException(
override val message: String
): RuntimeException(message)
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(BusinessException::class)
fun handleBusinessException(ex: BusinessException): ResponseEntity<String> {
return ResponseEntity.badRequest().body(
ex.message
)
}
}
모든 에러를 하나 하나 handler에 등록시키는 일은 매우 번거롭기 때문에 공용으로 처리할 BusinessException을 등록하고나서, 원하는 에러에 상속 시켜 에러를 처리합니다. 여기서는 Business 예외를 모두 badRequest()로 처리하기로 결정하였습니다.
중요한건 메세지를 전달하여, 간략하게나마 어떤 오류인지 설명을 하고 있습니다.(디버깅용)
package com.example.frontServer.exception
class NotFoundEntityException(
override val message: String
): BusinessException(message)
// in BoardService
fun findSingleBoard(request: SingleBoardRequest): SingleBoardResult {
val boardOptional = boardRepository.findById(request.boardId)
return if (boardOptional.isPresent) {
val writerName = userRepository.findById(boardOptional.get().writerId)
.map { user -> user.username}
.orElse("")
val likeCount = fetchLikeCount(boardOptional.get().id!!)
SingleBoardResult.of(
board = boardOptional.get(),
writerName = writerName,
likeCount = likeCount
)
} else {
throw NotFoundEntityException("can't find this board: ${request.boardId}")
}
}
BoardService에서 하나의 Board를 못찾은 경우 해당 에러를 발생시켜 hanlder에서 지정한 응답을 반환하도록 합니다. 이렇게 하여 컨트롤러에서 처리해야하는 응답코드를 더 편리하게 관리할 수 있습니다.
🧑🍳 MSA Server
MSA 서버는 에러에 대한 상세한 정보를 기술해야합니다.
// MSA Server
class DuplicatedIdException(
errorDetails: MSAServerErrorDetails
): BusinessException(
errorCode = MSAServerErrorCode.DUPLICATED_ID,
errorDetails = errorDetails
) {}
@RestControllerAdvice
class GlobalExceptionHandler {
@ExceptionHandler(BusinessException::class)
fun handleBusinessException(e: BusinessException):ResponseEntity<MSAServerErrorResponse> {
return ResponseEntity.ok().body(
MSAServerErrorResponse(
errorCode = e.errorCode,
errorDetails = e.errorDetails
)
)
}
}
save로직에서 unique에러가난 경우를 예시로 아래와 같이 코드를 작성하였습니다.
class DuplicatedIdException(
errorDetails: MSAServerErrorDetails
): BusinessException(
errorCode = MSAServerErrorCode.DUPLICATED_ID,
errorDetails = errorDetails
) {}
@Transactional
fun save(request: List<TimelineSaveRequest>): Int { // 저장 건수만
return try {
timelineRepository.saveAll(
request.map {
Timeline(
id = null,
receiverId = it.receiverId,
boardId = it.boardId,
createdAt = null
)
}
).size
} catch (ex: DataIntegrityViolationException) {
// check duplicated error
val rootCause = ex.rootCause
if (rootCause is ConstraintViolationException) {
if (rootCause.message?.contains("duplicate") == true) {
throw DuplicatedIdException(
errorDetails = MSAServerErrorDetails(
url = "/saveTimeline",
status = HttpStatus.BAD_REQUEST,
title = "duplicated id inserted!!",
detail = "${rootCause.message}"
)
)
} else {
throw BusinessException(
errorCode = MSAServerErrorCode.UNKNOWN_ERROR,
errorDetails = MSAServerErrorDetails(
url = "/saveTimeline",
status = HttpStatus.BAD_REQUEST,
title = "unknown business error",
detail = "Data integrity violation error, but not defined!" +
"${rootCause.message}"
)
)
}
} else {
throw BusinessException(
errorCode = MSAServerErrorCode.UNKNOWN_ERROR,
errorDetails = MSAServerErrorDetails(
url = "/saveTimeline",
status = HttpStatus.BAD_REQUEST,
title = "unknown business error",
detail = "Data integrity violation error, but not defined!" +
"${ex.message}"
)
)
}
}
}
service에서 코드가 좀 길어지기는 하지만, 확실하게 어디에서 에러가 발생했는지 최대한 많은 정보를 제공하였습니다.
- 추가로 Exception의 코드에는 MSA전용 에러코드가 들어있어, 응답에서 확인이 가능합니다.
- 여기서의 핵심은 비즈니스 로직 에러를 CircuitBreaker가 정상처리로 인식하도록 하는 것입니다.
그 외에 처리해야할 오류들
개발자가 고려해야할 오류는 비즈니스 로직 오류입니다. 아래와 같은 에러들도 직접 처리하면 좋습니다.
1. 접근 권한 오류
: 클라이언트가 권한이 없는 url로 리소스를 요청할 때
2. 인증오류
: 클라이언가 로그인에 실패했을 때
3. validation 오류
: 클라이언트의 json 데이터가 서버 규칙과 유효하지 않음
...
// 기타 등 등 ...
Question
| 🤔 옳은 설계인가 ? |
|---|
Feedback
| 🙋♂️ self - 보완하기: Result, Response Dto 따로 나누기 - Error handling |
|---|
