Chapter 7. API 응답 통일 & 에러 핸들러

김지민·2024년 12월 27일

UMC springboot

목록 보기
6/9

이번 시간에는 API 응답 통일과 에러 핸들링을 스프링 부트에서 어떻게 구현하는지 알아봅시다! 😊 이를 제대로 배우면 스프링부트 프로젝트의 상당 부분을 구현할 수 있게 됩니다.


🏁 이번 주차 목표

  • API 응답의 일관성을 유지하는 방법을 학습합니다.
  • 에러 핸들링의 원리를 이해하고, 효율적인 구현 방식을 알아봅니다.

🌱 1. API 응답 통일이란?

API란 애플리케이션들 간의 대화하는 통로 역할을 하는 것이었는데요.

그렇다면, 왜 API 응답을 통일해야 할까요?

API 개발 시 응답 형식이 통일되지 않으면 협업과 유지보수에 큰 문제가 생깁니다. 특히 프론트엔드와 대화를 해야하는 수단인 API에서 응답의 형태가 제각각이면, 이를 파악하기가 어렵겠죠?

따라서 API의 응답을 통일 하는 것은 프로젝트 진행에 있어 매우 필요한 작업입니다.

1-1. 통일된 API 응답의 구조

API의 형태는 프로젝트마다 다르지만, 응답 형식은 대개 다음과 같은 형태를 가집니다:

{
  "isSuccess": true,
  "code": "200",
  "message": "Request Successful",
  "result": {
    "data": "Your Data Here"
  }
}

각 필드의 의미

  • isSuccess: 요청 성공 여부 (true 또는 false)
  • code: 상태 코드를 나타내는 값. HTTP 상태코드로는 너무 제한적인 정보만 줄 수 있어서 조금 더 세부적인 응답 상황을 알려주기 위한 필드
  • message: 상태 코드(code)에 추가적으로 내용 설명
  • result: 요청 결과 데이터를 포함 (null로 설정 가능, DTO와 같은 것) 실제로 클라이언트에게 필요한 데이터가 담깁니다. 보통 에러 상황에는 null을 담지만, 아닌 경우도 있음
🪄 HTTP 상태 코드?

HTTP 상태 코드는 여러가지가 있지만 200번 대, 400번 대, 500번 대만 알아봅시다.
아래에 작성한 상태 코드 정도만 알아도 충분합니다만, 궁금하시면 더 찾아보세요!

  1. 200번 대 : 문제 없음
    1. 200 : OK 성공임
    2. 201 : Created: 네가 준 데이터를 가지고 적절한 과정을 거쳐 새로운 리소스를 만들었어
  2. 400번 대 : 클라이언트 측 잘못으로 인한 에러
    1. 400 : Bad Request : 요청 이상하게 함, 필요한 정보 누락됨..
    2. 401 : Unauthorized : 인증이 안됨 (로그인이 되어야 하는데 안된 상황)
    3. 403 : Forbidden : 권한 없음 (로그인은 되었으나 접근이 안됨, 관리자 페이지 등등)
    4. 404 : NotFound : 요청한 정보가 그냥 없음
  3. 500번 대 : 서버 측 잘못으로 인한 에러(안돼…….😱)
    1. 500 : Internal Server Error : 서버 터짐…….
    2. 504 : Gateway Timeout : 서버가 응답을 안 줌 (그냥 터진거긴 함..)

🌟 1-2. API 응답 통일하는 방법 알아보기

이제 실제로 차근차근 API 응답을 어떻게 통일하는지 살펴봅시다. 응답의 경우 enum으로 그 형태를 관리합니다.

이 때, 성공 응답과 실패 응답을 하나의 enum으로 관리할 수도 있고, 분리할 수도 있습니다. 이번에는 분리해서 관리하는 걸 같이 봐봅시다.

1. 디렉토리 구조 생성

먼저, apiPayload라는 디렉토리를 만들고 그 아래에 API 응답 통일을 위한 ApiResponse 클래스를 만들어주세요. 그리고 code라는 패키지도 디렉토리 내에 만들어주세요

2. ApiResponse 기본 틀 생성

이 클래스는 모든 API 응답의 기본 구조를 제공합니다.

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"isSuccess", "code", "message", "result"})
public class ApiResponse<T> {
    private final Boolean isSuccess; // 성공 여부
    private final String code;       // 상태 코드
    private final String message;    // 응답 메시지
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private final T result;          // 실제 데이터

    // 성공 응답 생성 메서드
    public static <T> ApiResponse<T> onSuccess(T result) {
        return new ApiResponse<>(true, "2000", "Request Successful", result);
    }

    // 실패 응답 생성 메서드
    public static <T> ApiResponse<T> onFailure(String code, String message) {
        return new ApiResponse<>(false, code, message, null);
    }
}

result는 어떤 형태의 값이 올지 모르기에 Generic으로 만들어 줍니다.

3. API 응답에 들어갈 code와 message의 형식 생성

아까 만든 폴더 아래에 아래와 같은 폴더 형태를 만들어주세요

3-1) 응답을 해주는 ErrorReasonDTO와 ReasonDTO의 코드를 적어주기

이 코드는 단순히 데이터를 담는 컨테이너 역할으로, 프로젝트가 확장될수록 DTO는 응답 구조를 구체화하는 데 활용될 것입니다.

@Getter
@Builder
public class ErrorReasonDTO {

    private HttpStatus httpStatus;

    private final boolean isSuccess;
    private final String code;
    private final String message;

    public boolean getIsSuccess(){return isSuccess;}
}
@Getter
@Builder
public class ReasonDTO {

    private HttpStatus httpStatus;

    private final boolean isSuccess;
    private final String code;
    private final String message;

    public boolean getIsSuccess(){return isSuccess;}
}

둘의 코드가 같은데 왜 따로 만들지?
두 클래스가 현재는 동일한 코드를 가지고 있지만, 구체적인 용도와 의미를 구분하기 위해 따로 정의된 것
=> 이러한 구조는 확장성유지보수성을 고려한 설계입니다!

3-2) BaseCode와 BaseErrorCode 작성

두 코드는 code 내용을 구체화 하는 Status에서 두 개의 메소드를 반드시 Override할 것을 강제하는 역할을 합니다. 상태 코드, 메시지 등의 구조를 정의하고, 이를 Enum과 결합하여 일관된 상태 코드 관리를 가능하게 하는 것이죠.

public interface BaseCode {

    Reason getReason();

    Reason getReasonHttpStatus();
}
public interface BaseErrorCode {

    ErrorReason getReason();

    ErrorReason getReasonHttpStatus();
}

Q. DTO와 BaseCode는 어떻게 활용되는 건가요?
사용 흐름 예시
1. BaseCode / BaseErrorCode 에서 상태 코드와 메시지를 정의.
2. 컨트롤러에서 상태 코드와 메시지를 가져와 ReasonDTO/ReasonErrorDTO를 생성.
3. DTO를 클라이언트에게 응답으로 전달.

3-3) 성공 응답을 나타내는 SuccessStatus 작성

@Getter
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {

    // 일반적인 응답
    _OK(HttpStatus.OK, "COMMON200", "성공입니다.");

    private final HttpStatus httpStatus;
    private final String code;
    private final String message;

    @Override
    public ReasonDTO getReason() {
        return ReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(true)
                .build();
    }

    @Override
    public ReasonDTO getReasonHttpStatus() {
        return ReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(true)
                .httpStatus(httpStatus)
                .build()
                ;
    }
}

코드를 자세히 보면, enum 자신의 값으로 가지고 있던 message, code, httpStatus값을 interface의 메소드 오버라이딩을 통하여 DTO를 만드는 것을 확인할 수 있습니다.


1-3. Spring Boot로 임시 API 만들어보기

1. API 설계

API 정보

  • HTTP 메서드: GET
  • URL: /temp/test
  • Query String, Request Body, Request Header: 없음
  • Response: 아래와 같은 JSON 형식 반환
    {
        "isSuccess": true,
        "code": "2000",
        "message": "OK",
        "result": {
            "testString": "This is test!"
        }
    }

2. DTO(Data Transfer Object) 작성

DTO는 데이터를 전달하기 위한 객체입니다.
이번 API에서는 TempResponse 클래스를 작성하여 응답 데이터 구조를 정의합니다. RequestBody에 담겨오는 값은 없으므로, 응답용 DTO만 작성합니다.

TempResponse 코드

public class TempResponse {

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TempTestDTO {
        String testString;
    }
}

코드 분석
1. @Builder

  • 객체를 빌더 패턴으로 생성하도록 지원.
  • 가독성유연성을 제공하며, 생성자 대신 직관적으로 객체를 생성할 수 있음.
  1. @Getter
  • DTO 클래스의 모든 필드에 대해 getter 메서드를 자동 생성.
  • 코드 간결화와 캡슐화 유지에 도움.
  1. @NoArgsConstructor

    • 매개변수가 없는 기본 생성자를 자동 생성.
  2. @AllArgsConstructor

    • 모든 필드를 초기화하는 생성자를 제공.
    • 테스트 코드 작성 시나 특정 상황에서 객체를 한 번에 초기화할 때 유용.
  3. Static Class 사용

    • DTO를 큰 묶음으로 관리할 수 있도록 설계.
    • 여러 관련 DTO를 하나의 클래스 내부에서 관리하여 코드 구조를 간소화하고 재사용성을 높임.

    추가 설명
    빌더 패턴이란?
    빌더 패턴(Builder Pattern)은 객체 생성 패턴 중 하나로, 복잡한 객체를 단계별로 구성할 수 있도록 설계된 디자인 패턴입니다.

    • 주로 객체 생성 과정이 복잡하거나, 동일한 생성 프로세스를 통해 다양한 객체 구성을 제공해야 할 때 사용.
    • 객체 생성 시 생성자 대신 @Builder를 활용하여 직관적이고 가독성이 높은 코드 작성이 가능.
    • 필드가 많거나 선택적 필드가 있는 객체를 생성할 때 유리.
      빌더 패턴 사용 예시
      TempResponse.TempTestDTO dto = TempResponse.TempTestDTO.builder()
          .testString("This is Test!")
          .build();
      빌더 패턴의 장점
    • 필드 순서에 상관없이 원하는 값만 설정 가능.
    • 코드 가독성과 유지보수성 향상.

    public static class를 사용할까?

    • DTO는 여러 곳에서 사용될 수 있으므로 범용적인 사용을 위해 내부 static 클래스로 정의.
    • 장점:
      • 별도의 DTO 파일을 만들지 않아도 됨.
      • 관련된 DTO들을 하나의 클래스로 묶어 관리 가능.

3. Converter 작성

Converter는 데이터 변환을 담당하는 클래스입니다.
여기서는 TempConverter 클래스를 작성해 TempResponse.TempTestDTO 객체를 생성합니다.

TempConverter 코드

public class TempConverter {

    public static TempResponse.TempTestDTO toTempTestDTO() {
        return TempResponse.TempTestDTO.builder()
                .testString("This is Test!")
                .build();
    }
}

코드 분석
1. toTempTestDTO(): TempTestDTO 객체를 생성하고 반환하는 정적 메서드.
2. 빌더 패턴을 활용하여 가독성과 유연성을 높임.

4. Controller 작성

Controller는 클라이언트 요청을 처리하고 응답을 반환하는 역할을 담당합니다.
이번 API는 비즈니스 로직이나 DB 연동이 없으므로 Service와 Repository 계층을 생략하고 작성합니다.

TempRestController 코드

@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {

    @GetMapping("/test")
    public ApiResponse<TempResponse.TempTestDTO> testAPI() {
        return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
    }
}

코드 분석
1. @RestController: RESTful 웹 서비스 컨트롤러를 나타냄. JSON 응답 자동 반환.
2. @RequestMapping("/temp"): 모든 엔드포인트에 /temp URL prefix를 설정.
3. @GetMapping("/test"): /temp/test 경로의 GET 요청을 처리.
4. ApiResponse.onSuccess(): 성공적인 응답 구조를 표준화하여 생성.

5. API 실행 결과

로컬 환경에서 API 호출
localhost:8080/temp/test로 GET 요청을 보내면 다음과 같은 JSON 응답을 반환합니다.

{
    "isSuccess": true,
    "code": "2000",
    "message": "OK",
    "result": {
        "testString": "This is test!"
    }
}

지금은 로컬호스트이지만, 원격 서버 Nginx에 Spring Boot을 연동했다면...
1. 클라이언트 요청은 Nginx에 도달.
2. Nginx는 요청을 Spring Boot 애플리케이션으로 전달(리버스 프록시).
3. Spring Boot에서 처리한 결과를 Nginx가 클라이언트에 반환.
이 순서로 작동되게 됩니다.

로컬서버 vs Ngnix

  • 로컬 서버:
    내 컴퓨터 안에서만 작동.
    예를 들어, localhost:3000 같은 주소를 브라우저에 입력하면, 내 컴퓨터가 서버처럼 작동해서 결과를 보여줌.
  • Nginx:
    인터넷에 연결된 컴퓨터(서버)에 설치됨.
    다른 사람들이 내 웹사이트 주소(www.mywebsite.com)에 접속하면, Nginx가 "어! 여기에 파일 있네!" 하고 데이터를 전달해줌.
    추가로, 내가 만든 프로그램(예: Django, Spring 같은 백엔드)도 연결해서 사용자 요청을 처리할 수 있게 도와줌.

🌱 2.에러 핸들링의 중요성

에러를 관리해야 하는 이유

  • 사용자 경험 개선: 명확한 에러 메시지를 제공하면 사용자가 문제를 이해하고 해결하기 쉬워집니다.
  • 디버깅 용이성: 통일된 에러 구조는 개발자가 문제를 빠르게 파악하고 수정하는 데 도움을 줍니다.

Enum으로 에러 코드 관리하기 - 추천 방식
1. common 에러는 COMMON000 으로 둔다. <- 잘 안쓰지만 마땅하지 않을 때 사용
2. 관련된 경우마다 code에 명시적으로 표현한다.
- 예를 들어 멤버 관련이면 MEMBER001 이런 식으로
3. 2번에 이어서 4000번대를 붙인다. 서버측 잘못은 그냥 COMMON 에러의 서버 에러를 쓰면 됨.
- MEMBER400_1 아니면 MEMBER4001 이런 식으로


2-2. Spring Boot 에러 핸들링 구현 과정

Spring Boot에서는 @RestControllerAdvice를 사용하여 전역적으로 발생하는 예외를 처리하고, 표준화된 응답을 반환할 수 있습니다.

1) ErrorStatus 작성하기

ErrorStatus는 애플리케이션에서 발생하는 다양한 에러를 관리하는 에러 상태 코드 클래스입니다.

@Getter
@AllArgsConstructor
public enum ErrorStatus implements BaseErrorCode {

    // 공통 에러
    _INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "COMMON500", "서버 에러, 관리자에게 문의 바랍니다."),
    _BAD_REQUEST(HttpStatus.BAD_REQUEST, "COMMON400", "잘못된 요청입니다."),
    _UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "COMMON401", "인증이 필요합니다."),
    _FORBIDDEN(HttpStatus.FORBIDDEN, "COMMON403", "금지된 요청입니다."),

    // 사용자 관련 에러
    MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, "MEMBER4001", "사용자가 없습니다."),
    NICKNAME_NOT_EXIST(HttpStatus.BAD_REQUEST, "MEMBER4002", "닉네임은 필수입니다."),

    // 기타 에러
    ARTICLE_NOT_FOUND(HttpStatus.NOT_FOUND, "ARTICLE4001", "게시글이 없습니다.");

    private final HttpStatus httpStatus;
    private final String code;
    private final String message;

    @Override
    public ErrorReasonDTO getReason() {
        return ErrorReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(false)
                .build();
    }

    @Override
    public ErrorReasonDTO getReasonHttpStatus() {
        return ErrorReasonDTO.builder()
                .message(message)
                .code(code)
                .isSuccess(false)
                .httpStatus(httpStatus)
                .build();
    }
}

ErrorStatus가 필요한 이유

  • 에러 코드를 중앙에서 관리할 수 있어, 일관성 있고 읽기 쉬운 에러 처리가 가능합니다.
  • 클라이언트에게 명확한 에러 메시지와 상태 코드를 제공할 수 있습니다.

2) GeneralException 추가하기

GeneralException은 특정 조건에서 발생하는 커스텀 에러를 처리하기 위해 작성된 사용자 정의 예외 클래스입니다.

@Getter
@AllArgsConstructor
public class GeneralException extends RuntimeException {

    private BaseErrorCode code;

    public ErrorReasonDTO getErrorReason() {
        return this.code.getReason();
    }

    public ErrorReasonDTO getErrorReasonHttpStatus() {
        return this.code.getReasonHttpStatus();
    }
}

GeneralException이 필요한 이유

  • 개발자가 원하는 대로 커스텀 예외를 생성하고 처리할 수 있습니다.
  • 비즈니스 로직에 따라 발생하는 특정 에러를 명확히 처리할 수 있습니다.

3) 에러 핸들러 만들기 (ExceptionAdvice)

ExceptionAdvice 클래스는 애플리케이션의 모든 예외를 처리하고 표준화된 JSON 응답을 반환합니다.

build.gradle에 의존성 추가

유효성 검사 예외를 처리하려면 validation 관련 라이브러리를 추가해야 합니다.

implementation 'org.springframework.boot:spring-boot-starter-validation'

ExceptionAdvice 코드

@Slf4j
@RestControllerAdvice(annotations = {RestController.class})
public class ExceptionAdvice extends ResponseEntityExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<Object> validation(ConstraintViolationException e, WebRequest request) {
        String errorMessage = e.getConstraintViolations().stream()
                .map(constraintViolation -> constraintViolation.getMessage())
                .findFirst()
                .orElseThrow(() -> new RuntimeException("ConstraintViolationException 추출 도중 에러 발생"));

        return handleExceptionInternalConstraint(e, ErrorStatus.valueOf(errorMessage), HttpHeaders.EMPTY, request);
    }

    @Override
    public ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        Map<String, String> errors = new LinkedHashMap<>();

        e.getBindingResult().getFieldErrors().forEach(fieldError -> {
            String fieldName = fieldError.getField();
            String errorMessage = Optional.ofNullable(fieldError.getDefaultMessage()).orElse("");
            errors.merge(fieldName, errorMessage, (existing, newMsg) -> existing + ", " + newMsg);
        });

        return handleExceptionInternalArgs(e, HttpHeaders.EMPTY, ErrorStatus._BAD_REQUEST, request, errors);
    }

    @ExceptionHandler
    public ResponseEntity<Object> exception(Exception e, WebRequest request) {
        e.printStackTrace();
        return handleExceptionInternalFalse(e, ErrorStatus._INTERNAL_SERVER_ERROR, HttpHeaders.EMPTY, ErrorStatus._INTERNAL_SERVER_ERROR.getHttpStatus(), request, e.getMessage());
    }

    @ExceptionHandler(value = GeneralException.class)
    public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
        ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
        return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request);
    }

    private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) {
        ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null);
        WebRequest webRequest = new ServletWebRequest(request);
        return super.handleExceptionInternal(e, body, headers, reason.getHttpStatus(), webRequest);
    }

    private ResponseEntity<Object> handleExceptionInternalFalse(Exception e, ErrorStatus errorCommonStatus, HttpHeaders headers, HttpStatus status, WebRequest request, String errorPoint) {
        ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), errorPoint);
        return super.handleExceptionInternal(e, body, headers, status, request);
    }

    private ResponseEntity<Object> handleExceptionInternalArgs(Exception e, HttpHeaders headers, ErrorStatus errorCommonStatus, WebRequest request, Map<String, String> errorArgs) {
        ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), errorArgs);
        return super.handleExceptionInternal(e, body, headers, errorCommonStatus.getHttpStatus(), request);
    }

    private ResponseEntity<Object> handleExceptionInternalConstraint(Exception e, ErrorStatus errorCommonStatus, HttpHeaders headers, WebRequest request) {
        ApiResponse<Object> body = ApiResponse.onFailure(errorCommonStatus.getCode(), errorCommonStatus.getMessage(), null);
        return super.handleExceptionInternal(e, body, headers, errorCommonStatus.getHttpStatus(), request);
    }
}

주요 메서드 설명

  1. validation(): ConstraintViolationException 처리

    • 유효성 검사 실패 시 발생하는 예외를 처리하고, 표준화된 응답을 반환합니다.
  2. handleMethodArgumentNotValid(): MethodArgumentNotValidException 처리

    • 메서드 인자의 유효성 검사가 실패할 경우, 필드별 에러 메시지를 수집해 반환합니다.
  3. exception(): 일반 예외 처리

    • 예상치 못한 예외를 포괄적으로 처리하여 클라이언트에게 서버 에러 메시지를 반환합니다.
  4. onThrowException(): 사용자 정의 예외 처리

    • GeneralException을 처리하여, 비즈니스 로직에 따라 커스텀 응답을 생성합니다.
  5. handleExceptionInternal(): 기본 예외 응답 생성

    • 표준화된 JSON 응답을 생성합니다.

예시 JSON 응답

{
  "success": false,
  "code": "MEMBER4001",
  "message": "사용자가 없습니다.",
  "data": null
}

2-3. 실제로 에러가 처리를 구현하는 과정 예시

GET /temp/exception 요청에 대한 예외 처리 구현 과정을 예시로 살펴봅시다. 이 API는 Query String으로 flag를 받아오며, flag 값이 2일 경우 Exception을 발생시킵니다.

1. ErrorStatus에 새로운 경우 추가하기

ErrorStatus에 테스트용 에러 상태를 추가합니다.

// For test
TEMP_EXCEPTION(HttpStatus.BAD_REQUEST, "TEMP4001", "이거는 테스트");

2. Temp 핸들러 추가하기

handler 패키지TempHandler 클래스를 생성합니다. 이 클래스는 GeneralException을 상속받습니다.

public class TempHandler extends GeneralException {

    public TempHandler(BaseErrorCode errorCode) {
        super(errorCode);
    }
}

TempHandler는 테스트용 예외를 처리하기 위해 작성되었고, 부모 클래스의 생성자를 호출해 에러 코드를 전달합니다.

3. TempResponse DTO 추가하기

TempResponse 클래스는 예외와 테스트 응답 데이터를 포함합니다.

public class TempResponse {

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TempTestDTO {
        String testString;
    }

    @Builder
    @Getter
    @NoArgsConstructor
    @AllArgsConstructor
    public static class TempExceptionDTO {
        Integer flag;
    }
}
  • TempTestDTO: 테스트 API 응답 데이터.
  • TempExceptionDTO: 예외 발생 시 전달되는 데이터.

4. TempConverter 작성하기

TempConverter는 DTO를 생성하는 정적 메서드를 제공합니다. 이를 통해 TempResponse DTO를 생성해서 반환합니다.

public class TempConverter {

    public static TempResponse.TempTestDTO toTempTestDTO() {
        return TempResponse.TempTestDTO.builder()
                .testString("This is Test!")
                .build();
    }

    public static TempResponse.TempExceptionDTO toTempExceptionDTO(Integer flag) {
        return TempResponse.TempExceptionDTO.builder()
                .flag(flag)
                .build();
    }
}
  • toTempTestDTO: 테스트 응답 객체 생성.
  • toTempExceptionDTO: 예외 발생 시 전달할 DTO 생성.

5. Controller 작성하기

TempRestController@RestController를 사용하여 API 요청을 처리합니다.

@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {

    @GetMapping("/test")
    public ApiResponse<TempResponse.TempTestDTO> testAPI() {
        return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
    }

    @GetMapping("/exception")
    public ApiResponse<TempResponse.TempExceptionDTO> exceptionAPI(@RequestParam Integer flag) {
        return null;
    }
}
  • @RequestParam을 통해 Query String에서 값을 받아옵니다.
  • /test API는 테스트 데이터를 반환합니다.
  • /exception API는 서비스 로직과 연동됩니다.

이때, RestControllerAdvice를 통해 @RestController가 붙은 대상에서 Exception이 발생하는 것을 감지하게 됩니다.

6. Service 작성하기

🥕서비스 설계 원칙
1. GET 요청과 나머지 요청의 로직을 분리합니다.
- GET 요청: TempQueryService.
- 기타 요청: TempCommandService.
2. 인터페이스를 먼저 정의한 뒤 구체화 클래스를 작성합니다.
- 인터페이스: TempQueryService.
- 구현체: TempQueryServiceImpl.
3. 컨트롤러는 인터페이스에 의존하며, Spring의 의존성 주입을 통해 구현체를 주입받습니다.

TempQueryService 인터페이스

public interface TempQueryService {

    void checkFlag(Integer flag);
}

TempQueryServiceImpl 구현체

@Service
@RequiredArgsConstructor
public class TempQueryServiceImpl implements TempQueryService {

    @Override
    public void checkFlag(Integer flag) {
        if (flag == 1)
            throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
    }
}
  • checkFlag: flag 값이 1인 경우 TempHandler 예외를 던집니다.

7. Controller 완성하기

TempRestController/exception API를 완성합니다.

@RestController
@RequestMapping("/temp")
@RequiredArgsConstructor
public class TempRestController {

    private final TempQueryService tempQueryService;

    @GetMapping("/test")
    public ApiResponse<TempResponse.TempTestDTO> testAPI() {
        return ApiResponse.onSuccess(TempConverter.toTempTestDTO());
    }

    @GetMapping("/exception")
    public ApiResponse<TempResponse.TempExceptionDTO> exceptionAPI(@RequestParam Integer flag) {
        tempQueryService.checkFlag(flag);
        return ApiResponse.onSuccess(TempConverter.toTempExceptionDTO(flag));
    }
}

tempQueryService.checkFlag(flag)를 호출하여 예외 발생 여부를 확인하고, 예외가 발생하지 않으면 DTO를 반환합니다.


위 과정을 따라 에러가 처리되는 과정

  1. Service에서 예외 발생
    TempQueryServiceImplcheckFlag 메서드에서 flag 값이 1인 경우 TempHandler를 통해 예외를 발생시킵니다.

    @Override
    public void checkFlag(Integer flag) {
        if (flag == 1)
            throw new TempHandler(ErrorStatus.TEMP_EXCEPTION);
    }
  2. ExceptionAdvice에서 예외 처리
    TempHandlerGeneralException을 상속받기 때문에 @RestControllerAdvice에 등록된 ExceptionAdvice가 예외를 처리합니다.

    @ExceptionHandler(value = GeneralException.class)
    public ResponseEntity onThrowException(GeneralException generalException, HttpServletRequest request) {
        ErrorReasonDTO errorReasonHttpStatus = generalException.getErrorReasonHttpStatus();
        return handleExceptionInternal(generalException, errorReasonHttpStatus, null, request);
    }
  3. 표준화된 응답 반환
    handleExceptionInternal 메서드를 통해 표준화된 JSON 응답이 클라이언트로 반환됩니다.

    private ResponseEntity<Object> handleExceptionInternal(Exception e, ErrorReasonDTO reason, HttpHeaders headers, HttpServletRequest request) {
        ApiResponse<Object> body = ApiResponse.onFailure(reason.getCode(), reason.getMessage(), null);
        WebRequest webRequest = new ServletWebRequest(request);
        return super.handleExceptionInternal(e, body, headers, reason.getHttpStatus(), webRequest);
    }
  4. 응답 결과 확인
    클라이언트는 에러 코드와 메시지를 포함한 표준화된 응답을 받습니다.

예외 응답 예시

{
  "success": false,
  "code": "TEMP4001",
  "message": "이거는 테스트"
}

📝 정리하며

  • API 응답 통일은 협업과 유지보수성을 높이는 핵심입니다.
  • 에러 핸들링은 명확하고 일관된 방식으로 사용자와 개발자 모두에게 유용한 정보를 제공합니다.

🎯 다음 시간에는 오늘 배운 내용을 더 구체적인 과정으로 하나하나씩 살펴보고 실제 예제를 같이 보겠습니다!

profile
열혈개발자~!!

0개의 댓글