Spring발행일 2024. 11. 12.원본 https://blog.naver.com/jword_/223657463933 ↗

Spring Exception Handler로 예외처리 재설계 하기

Spring Exception Handler로 예외처리 재설계 하기 — #SpringExceptionHandler #스프링코틀린 #스프링예외처리 #개발자의도구들 🚨📝 📖 📒✏️💡...

#Spring#Naver Blog

#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코드를 통해 설명하겠습니다.

kotlin 코드 예제
                                    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

kotlin 코드 예제
                                    @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에 관한 부분이고, 개발자가 직접 예외 코드를 만들어 핸들러에게 처리하게 하고 싶으면 다음 방법으로 구현할 수 있습니다.

​

그 이전에 아래 사항을 함께 고려해보죠.

text 코드 예제
                                    💭 서버 어플리케이션 내에 발생할 수 있는 에러는 수 없이 많다.
🤔 에러가 발생하면 그 때마다 Exception Hanlder에 등록해야할까?

우리는 항상 효율적으로 처리할 수 있는 방법을 고안해야합니다. 매번 예외가 생성될때마다, 핸들러에 등록하는건 너무나 단순하면서도 번거로운 일이거든요.

​

이를 해결하기 위해 아래와 같은 방법을 고려할 수 있습니다.

​

  1. Parent Exception
kotlin 코드 예제
                                    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가 전달되도록 설정합니다.

​

  1. Inherit
python 코드 예제
                                    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를 등록합니다.

​

  1. Exception Handler
kotlin 코드 예제
                                    // 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
kotlin 코드 예제
                                    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
kotlin 코드 예제
                                    @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)

kotlin 코드 예제
                                    package com.example.frontServer.dto

import com.example.frontServer.enum.FrontServerError

data class ErrorResponse(
    val code: FrontServerError,
    val message: String?
)

FrontServerErrorCode(Client-FrontServer Error 공통 코드)

kotlin 코드 예제
                                    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

🙋‍♂️

​