Spring Exception Handler로 예외처리 재설계 하기
Spring Exception Handler로 예외처리 재설계 하기 — #SpringExceptionHandler #스프링코틀린 #스프링예외처리 #개발자의도구들 🚨📝 📖 📒✏️💡...
#SpringExceptionHandler #스프링코틀린 #스프링예외처리 #개발자의도구들
🚨📝 📖 📒✏️💡🔍
AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다
\* 현재 Spring x 프로젝트를 진행하고 있습니다. 전체 목차를 보시려면 여기를 눌러주세요.
\* 해당 프로젝트 이전에 공부한 내용을 보시려면 여기를 눌러주세요.
Exception Handler를 통한 예외처리
Spring을 구현하면서 제일 많이 고민이 되는 부분이 예외처리라고 생각됩니다. 매번 할 때마다 예외를 어떻게 처리해야하는지 고민하고 고민하며 몇번을 새로 뒤집어 엎었는지 모르겠네요...
매번 이런 고민이 들 때마다, "이런 고민할 시간에 핵심 로직 정리했으면 다 했겠다..."라는 생각이 머릿속을 계속 멤돌곤 합니다. 아주 간단한 일처럼 보이지만, 신경써야할 부분이 많기 때문에, api 생성 때마다 매번 시간을 많이 잡아먹습니다.
그래서 이번글의 목표는 예외처리 가이드를 제시하는 것입니다. 100% 동일하게 처리할 필요는 없지만, 와이어 프레임 처럼 최소한의 틀을 제공하여 예외처리 의사결정 시간을 줄이는게 목표입니다.
| ✅ 사용할 Tool: Spring - ExceptionHandler |
|---|
Index
- Purpose
- Exception Hanlder
- Def
- Why to use
- How to use
- Custom Exception 🫣
- Code
- Controller
- Service
- Others
- Question
- Answer
- Feedback
| 💡 Tips |
|---|
Purpose
Spring에서 발생하는 예외 처리를 위한 최소한의 가이드가 될 수 있도록 글을 정리하였습니다.
| 💡 모든 코드는 HTTP상태코드가 비현실적인 이유의 AllOk() 섹션의 표준을 따릅니다. |
|---|
Exception Handler
DEF
Spring에서 Exception을 효율적으로 처리하기 위해 제공되는 기능입니다. Spring server app 내에서 처리되지 않는 Exception은 최종적으로 Controller에 도달하는 것이 일반적인데, 이 Controller에서도 처리되지 않는 예외들을 Exception Handler가 대신 처리해줍니다.
Why to Use
이렇게 사용하는데 큰 이점은 아무래도 코드 가독성이 높아집니다. 이전글에서 다룬 MSA LikeServer의 LikeService코드를 통해 설명하겠습니다.
package com.example.liveApiServer.service
import com.example.liveApiServer.dto.save.LikeRequest
import com.example.liveApiServer.dto.save.LikeServerSaveResult
import com.example.liveApiServer.entity.Like
import com.example.liveApiServer.repository.LikeRepository
import org.springframework.stereotype.Service
@Service
class LikeService(
private val likeRepository: LikeRepository,
) {
fun save(request: LikeRequest): LikeServerSaveResult{
return try {
likeRepository.save(
Like(
id = null, // 이렇게 명시하는게 좋아.
boardId = request.boardId,
userId = request.userId,
likeType = request.likeType,
)
)
LikeServerSaveResult(
details = null,
error = null
)
} catch {
// error 별로 LikeServerSaveResult 관리
// boardId userId가 없는 경우 어떻게 처리?
// likeType이 없는 type인 경우도 있을 수 있다.
}
}
}
해당 서버에서는 Service에서 예외처리를 하도록 설계를 하였습니다. catch에 정확히 어떤 Exception을 처리해야할지 상세히 적진 않았지만, 만약 적는다면 코드 라인이 굉장히 늘어나게 될 것 입니다.
또한 매번 새로운 Exception이 생겨날 때 마다.Service로 돌아와 처리를 해줘야하는게 코드를 작성하는 입장에서 매우 피곤한 일이될 것 입니다.
위 상황은 Controller에서 예외처리를 할 때 역시 마찬가지일 것 입니다. 하지만 Exception Handler를 사용하여 예외를 처리하면, 예외 처리 부분을 따로 분리 시킬 수 있습니다.
How to use
@RestControllerAdvice
class GlobalExceptionHandler {
// validation error
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
val message = ex.bindingResult.allErrors.joinToString(", ") { error ->
val fieldError = error as FieldError
"${fieldError.field}: ${fieldError.defaultMessage}"
}
return ResponseEntity.ok().body(
ErrorResponse(
code = FrontServerError.VALIDATION_ERROR,
message = message
)
)
}
}
사용법은 매우 간단합니다. 전용 ExceptionHandle class를 만들고 @RestControllerAdvice를 붙여줍니다. 이후 원하는 특정 예외를 함수형태로 명시해주면 됩니다. 이때 @ExceptionHandler를 붙여줍니다.
해당 예외는 서버 전체에서 @Valid 검증시 발생하는 예외인데, 이때 Spring에서 자체적으로 생성해둔 MethodArgumentNotValidException을 발생시킵니다.
해당 예외를 Exception Handler에 적용 시켜주면, 어떤 API든지 공용으로 사용할 수 있게됩니다.
Custom Exception
🫣(주의)
\* 해당 섹션은 피드백이 필요합니다. (이후 표기 삭제)
위의 case에서는 Spring에서 제공되는 Exception에 관한 부분이고, 개발자가 직접 예외 코드를 만들어 핸들러에게 처리하게 하고 싶으면 다음 방법으로 구현할 수 있습니다.
그 이전에 아래 사항을 함께 고려해보죠.
💭 서버 어플리케이션 내에 발생할 수 있는 에러는 수 없이 많다.
🤔 에러가 발생하면 그 때마다 Exception Hanlder에 등록해야할까?
우리는 항상 효율적으로 처리할 수 있는 방법을 고안해야합니다. 매번 예외가 생성될때마다, 핸들러에 등록하는건 너무나 단순하면서도 번거로운 일이거든요.
이를 해결하기 위해 아래와 같은 방법을 고려할 수 있습니다.
- Parent Exception
package com.example.frontServer.exception
import com.example.frontServer.enum.FrontServerError
open class CustomErrorException(
val errorCode: FrontServerError,
override val message: String? = null
) : RuntimeException(message)
CustomError를 만들때 해당 클래스를 상속하여, ErrorCode가 전달되도록 설정합니다.
- Inherit
package com.example.frontServer.exception
import com.example.frontServer.enum.FrontServerError
class InValidIdException(
message: String? = "Invalid Id"
): CustomErrorException(FrontServerError.INVALID_ID, message)
Exception을 생성할때 CustomErrorException을 상속받습니다. 이때 code와 message를 등록합니다.
- Exception Handler
// Invalid Id Error
@ExceptionHandler(InValidIdException::class)
fun handleCustomErrorException(ex: CustomErrorException): ResponseEntity<ErrorResponse> {
return ResponseEntity.ok().body(
ErrorResponse(
code = ex.errorCode,
message = ex.message
)
)
}
Exception Handler 내에서 CustomErrorException을 처리하는 코드를 작성합니다. 자! 이제 어떤 Exception을 커스텀 하더라도, 더이상 Hanlder에 추가할 필요가 없습니다.
Code
그럼 마지막으로 Controller 및 Service 코드를 정리하며 마치도록 하겠습니다.
| 🧑💻 Service |
|---|
package com.example.frontServer.service
import com.example.frontServer.dto.*
import com.example.frontServer.dto.board.BoardResult
import com.example.frontServer.dto.board.BoardSaveRequest
import com.example.frontServer.entity.Board
import com.example.frontServer.repository.BoardRepository
import com.example.frontServer.repository.FollowRepository
import jakarta.transaction.Transactional
import org.springframework.stereotype.Service
import java.util.*
@Service
class BoardService(
val boardRepository: BoardRepository,
val notificationService: NotificationService,
val fileService: FileService,
val followRepository: FollowRepository,
val timelineService: TimelineService,
val likeService: LikeService
) {
@Transactional
fun findById(id: Long): BoardResult? {
val boardWithComment = boardRepository.findBoardWithCommentById(id)
return boardWithComment?.let {
BoardResult.of(it, it.board.writerId.toString(), 0)
}
}
}
현재 BoardResult?를 Service에서 return하고 있습니다.
| 🧑💻 Controller |
|---|
@RestController
class BoardController(
private val boardService: BoardService,
) {
private val logger = KotlinLogging.logger {}
@GetMapping("/board")
fun findById(@RequestBody id: Long): ResponseEntity<BoardResponse> {
return boardService.findById(id)?.let {
ResponseEntity.ok().body(
BoardResponse.of(it)
)
} ?: throw InValidIdException()
}
}
그걸 Controller가 받고 있습니다. 만약 Return 값이 null이라면, Controller는 여기서 직접 Response를 처리하는게 아닌, 그저 InValidIdException()을 던지면 됩니다.
| 🧑💻 Others |
|---|
ErrorResponse (ExceptionHandler -> Cleint)
package com.example.frontServer.dto
import com.example.frontServer.enum.FrontServerError
data class ErrorResponse(
val code: FrontServerError,
val message: String?
)
FrontServerErrorCode(Client-FrontServer Error 공통 코드)
package com.example.frontServer.enum
enum class FrontServerError {
NOT_FOUND_404, // Wrong URL
INVALID_ID, // invalid Id : boardId, userId, ....
DUPLICATED, // unique key
BAD_REQUEST, // HTTP BAD_REQUEST
UNEXPECTED_ERROR, // UnExpected case
FATAL_SERVER_ERROR, // fatal server error ex) db connection error
PARAMETER_ERROR, // Parameter missing
VALIDATION_ERROR, // Sign-up or param validation error
PARSING_ERROR, // can't read request body
CANNOT_ACCESS_RESOURCE, // can't access resources for network
CREDENTIALS_ERROR, // Login failed
ACCESS_DENIED, // Failed access to premium service
BODY_TYPE_ERROR,
FILE_NOT_EXIST,
}
기존 HTTP가 아닌 재정의한 에러 코드입니다.
Question
| 🤔 write Question 1. custom Exception 처리 맞는지2. |
|---|
Feedback
| 🙋♂️ |
|---|
