[Twitter Clone] v0.0.0 Devlog - Response & Custom Exception

Sierra·2023년 1월 13일
0

Twitter Clone Project

목록 보기
3/4
post-thumbnail

Intro

원래 다음 포스팅은 클라이언트 사이드에서의 JWT 토큰 처리에 관한 내용이었지만, 그 전에 응답 메시지 처리에 대해 꼭 언급해야 할 것 같아서 해당 포스팅을 먼저 업로드 하게 되었다.

서버사이드에서 에러처리를 어떻게 해 주느냐의 문제는 서비스 품질에 중대한 영향을 끼친다. 현재 서버사이드에서 랜더링까지 따로 하고있는 상황이 아니므로(프론트엔드를 분리한 상황) 전달 받은 Request에 대한 Response에 균일한 양식이 필요한 상황이다. (서버사이드 랜더링을 하는 상황이라고 다른건 아니지만 어쨌든 데이터 처리의 주체가 다른 쪽으로 넘어간 상황이기에 훨씬 더 신경을 써야한다.)

이번 포스팅에서는 Spring에서 Response를 처리하는 법에 대하여 기록 해보려 한다.

Payload

HTTP를 통해 데이터를 전송할 때 Payload에 데이터를 전달하게 된다. 어렵게 생각할 것 없이 Request Body라고 생각해도 일맥상통한다.
Payload 상에 데이터는 전달 해 주는 주체에서 결정하게 된다. 어떤 형태로 보낼 지도 마찬가지다. 이 부분은 프로그래머의 몫이다.
이 Payload에 관련 된 객체를 개발 해 두는것을 권장한다. 왜냐하면 여러가지 응답들에 대해 재사용 할 수 있기 때문이다.

서버사이드에서는 다양한 요청들에 대한 응답을 처리하게 된다. 이 경우마다 Response 객체를 새로 지정해주는 것은 낭비다. 필자는 BaseResponse 객체를 통해 이 문제를 해결하였다.

import org.springframework.http.HttpStatus

data class BaseResponse<T> (
    val status : HttpStatus,
    val data : T,
    val message : String
    ){
    companion object {
        fun <T> success(data : T) = BaseResponse<T> (
                status = HttpStatus.OK,
                data = data,
                message = "SUCCESS"
        )

        fun failure(status : HttpStatus, message : String) = BaseResponse<String> (
            status = status,
            data = message,
            message = "FAILURE"
        )
    }
}

총 세가지의 데이터를 공통적으로 집어넣는다. HTTP Status, Data, Message.
HTTP Status 자체는 굳이 Response Body를 확인하지 않아도 알 수는 있지만 편의상 직접 전달하는 방향을 채택했다. 클라이언트 사이드에서 에러 핸들링 할 때 이 형태 그대로 Type을 지정해서 Response data를 가져올 수 있기 때문이다.

성공 한 경우와 실패한 경우에 대해 각 객체를 반환하는 static function을 만들어두었다. 중간에 Data 같은 경우는 제네릭으로 처리되어있기 때문에 어떠한 형태의 객체든 데이터로 전달 해 줄 수 있다.

즉 성공했다면 다음과 같은 데이터를 전달받을 수 있게 된다.

{
  "status":"OK",
  "data":"테스트입니다!",
  "message":"SUCCESS"
}

실패 했다면 오류코드와 함께 다음과 같은 데이터가 전달된다.

{
  "status":"INTERNAL_SERVER_ERROR",
  "data":"내부 서버 오류입니다. 관리자에게 문의하세요",
  "message":"FAILURE"
}

BaseResponse 객체는 아래와 같이 Controller 메소드의 리턴타입으로 지정해서 사용하면 된다. 처리 된 요청이 성공했다면 해당 결과값을 BaseResponse.success() 를 통해 생성해서 ResponseEntity 의 Body에 지정하면 된다.

 @GetMapping("/test")
    fun testApi() : ResponseEntity<BaseResponse<String>> {
        return ResponseEntity.ok().body(BaseResponse.success("테스트입니다!"))
    }

지금까지 Response body의 객체화에 대해 얘기 해 보았다. 이를 통해 에러 처리 또한 할 수 있다는 사실도 알게 되었다. 그럼 에러가 발생했을 때 어떻게 해야 위의 응답 결과를 전달해 줄 수 있을까?

Custom Exception

앞서 언급했듯, 전달받은 요청에 대한 응답의 균일한 양식이 있어야지만 클라이언트 사이드에서 처리하기 편하기도 하지만 (매번 응답 메시지 형태가 바뀐다 생각해보라...), 민감한 정보의 노출을 막기 위해서도 Custom Exception 처리는 반드시 필요하다.

특정 상황에서 NPE와 같은 에러가 발생했을 때 코드의 어떤 부분에서 어떻게 에러가 발생했는 지 추적할 수 있도록 에러코드가 장황하게 출력되는 경우가 많다. 중요한 건 이러한 에러 코드가 서버 로그 상에 출력되었을 때는 개발자 입장에서 트러블 슈팅이 용이해질 수 있지만, 배포 된 클라이언트에 전달된다면? 사용자 입장에서 불필요한 데이터를 전달 받는 상황이기도 하지만, 서버 상의 코드에 대한 정보가 노출되게 된다.

물론 이것 말고도 이유는 많다. 에러가 발생했을 때 빨리 예외처리하고 다른 작업을 할 수 있는 상태로 다시 돌려놓기 위해서도 있지만, 무차별적인 Try Catch 문을 줄이기 위해서도 Custom Exception 작업은 필요하다.

ControllerAdvice

Spring의 작동 과정은 다음과 같다. 요청이 전달된다면 Dispatcher Servlet이 제일 먼저 해당 요청을 받고 URL을 확인하여 HandlerMapping 객체에 넘긴다. 그 후 호출해야 할 Controller 정보를 확인하고 Handler Adapter객체의 메소드를 통해 Contoller 인터페이스를 실행시킨다. 그 후 Controller, Service 단에서 비즈니스 로직이 처리되고 결과값은 Handler Adapter에 전달되고 해당 데이터가 Dispatcher Servlet을 통해 응답 메시지로 전달된다.

이 과정에서 Controller 단 아래부터 전달 된 요청 데이터에 대해 생기는 에러에 대해서는 Controller Advice를 통해 처리할 수 있다. 그 전에 이러한 전역적인 예외 사항에 대해 처리해줄 수 있는 클래스를 하나 생성 해 두어야 한다. 또한 필자는 HTTP Status와 메시지를 처리해 줄 수 있는 Enum 클래스 또한 생성 해 두었다.

class BaseException(baseResponseCode: BaseResponseCode): RuntimeException() {
    val baseResponseCode: BaseResponseCode = baseResponseCode;
}
enum class BaseResponseCode(status: HttpStatus, message: String ) {
    //OK
    OK(HttpStatus.OK, "요청 성공"),

    //NOT_FOUND
    NOT_FOUND(HttpStatus.NOT_FOUND,  "리소스를 찾을 수 없습니다."),
    USER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 조건에 맞는 유저를 찾을 수 없습니다."),
    TWEET_NOT_FOUND(HttpStatus.NOT_FOUND, "조건에 맞는 트윗 데이터를 찾을 수 없습니다."),

    //UNAUTHORIZED
    UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "권한이 없습니다."),
    EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "Token Expired"),

    //BAD_REQUEST
    BAD_REQUEST(HttpStatus.BAD_REQUEST, "요청이 올바르지 않습니다."),
    INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "비밀번호가 올바르지 않습니다."),
    INVALID_TOKEN(HttpStatus.BAD_REQUEST, "올바르지 않은 토큰 요청입니다."),

    //FORBIDDEN
    FORBIDDEN(HttpStatus.FORBIDDEN,  "금지된 요청입니다."),
    REFRESH_TOKEN_EXPIRED(HttpStatus.FORBIDDEN, "리프레쉬 토큰이 만료되었습니다."),

    //CONFLICT
    DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 계정 이메일입니다."),
    DUPLICATE_USERNAME(HttpStatus.CONFLICT, "이미 존재하는 유저 닉네임입니다."),
    LOGIN_FAILED(HttpStatus.CONFLICT, "로그인에 실패하였습니다."),

    //INTERNAL SERVER ERROR
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR,  "내부 서버 오류입니다. 관리자에게 문의하세요"),
    FILE_UPLOAD_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드에 실패하였습니다.");

    val status: HttpStatus = status
    val message : String = message

}

이제 에러처리를 위해 필요한 객체와 데이터는 준비되었으니 ControllerAdvice 객체를 작성 해 보자.

@RestControllerAdvice
class GlobalExceptionHandler {
    private val logger = LoggerFactory.getLogger(javaClass)

    @ExceptionHandler(BaseException::class)
    protected fun handleBaseException(e: BaseException): ResponseEntity<BaseResponse<String>> {
        logger.error("Exception : " + e.message)
        return ResponseEntity.status(e.baseResponseCode.status)
            .body(BaseResponse.failure( e.baseResponseCode.status, e.baseResponseCode.message))
    }

    @ExceptionHandler(Exception::class)
    protected  fun handleException (e : Exception) : ResponseEntity<BaseResponse<String>> {
        logger.error("Exception : " + e.message)
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(BaseResponse.failure(BaseResponseCode.INTERNAL_SERVER_ERROR.status,
                BaseResponseCode.INTERNAL_SERVER_ERROR.message))
    }
}

BaseException 은 코드 상에서 직접 처리하는 에러, 그 외에 미쳐 발견하지 못한 에러 사항 총 두가지에 대해 처리할 수 있는 ControllerAdvice다. 에러가 발생했을 시 에러 메시지와 함께 BaseResponse.failure() 를 통해 실패 응답 메시지가 생성되어 반환된다.

{
  "status":"INTERNAL_SERVER_ERROR",
  "data":"내부 서버 오류입니다. 관리자에게 문의하세요",
  "message":"FAILURE"
}

이제 에러가 나더라도 외부에 에러로그가 노출 될 일은 없다.

Outro

클라이언트 사이드에서 JWT 토큰 및 에러 핸들링을 편리하게 하기 위해, 또한 보안 문제를 해결하기 위해, 그 외에도 응답 형태를 고정해서 클라이언트에서 처리하기 편하도록 Response 메시지를 처리하는 방법에 대해 알아보았다. 다음 포스팅에서는 이 균일화 된 응답 메시지를 클라이언트사이드에서 처리하는 방법에 대해 JWT 토큰 처리 과정을 분석해 보며 알아보도록 하겠다.

profile
블로그 이전합니다 : https://swj-techblog.vercel.app/

0개의 댓글