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

서버 예외 처리 가이드북

서버 예외 처리 가이드북 — #개발자의도구들 #서버예외 #서버예외처리 #ExceptionHandler 🚨📝 📖 📒✏️💡🔍 AI스쿨 ...

#Spring#Naver Blog

#개발자의도구들 #서버예외 #서버예외처리 #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 구조에서 에러처리를 할 때 서버 전용 에러 코드를 만들어서 처리하고 있습니다. 이런 경우

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

javascript 코드 예제
                                    🤵 FrontServer
javascript 코드 예제
                                    // ✏️ 프론트 서버
// 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()로 처리하기로 결정하였습니다.

​

중요한건 메세지를 전달하여, 간략하게나마 어떤 오류인지 설명을 하고 있습니다.(디버깅용)

javascript 코드 예제
                                    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에서 지정한 응답을 반환하도록 합니다. 이렇게 하여 컨트롤러에서 처리해야하는 응답코드를 더 편리하게 관리할 수 있습니다.

javascript 코드 예제
                                    🧑‍🍳 MSA Server

MSA 서버는 에러에 대한 상세한 정보를 기술해야합니다.

javascript 코드 예제
                                    // 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에러가난 경우를 예시로 아래와 같이 코드를 작성하였습니다.

javascript 코드 예제
                                    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가 정상처리로 인식하도록 하는 것입니다.

그 외에 처리해야할 오류들

개발자가 고려해야할 오류는 비즈니스 로직 오류입니다. 아래와 같은 에러들도 직접 처리하면 좋습니다.

javascript 코드 예제
                                    1. 접근 권한 오류
: 클라이언트가 권한이 없는 url로 리소스를 요청할 때

2. 인증오류
: 클라이언가 로그인에 실패했을 때

3. validation 오류
: 클라이언트의 json 데이터가 서버 규칙과 유효하지 않음

...
// 기타 등 등 ...

Question

🤔 옳은 설계인가 ?

Feedback

🙋‍♂️ self ​- 보완하기: Result, Response Dto 따로 나누기 - Error handling

​