[TIL] Spring Boot에서 표준화된 응답 구조와 예외 처리

알쓸코딩·2025년 4월 11일
1

TIL

목록 보기
1/1

공통 응답 포맷

  • 성공했을 때
    • 전달할 데이터가 있는 경우
      {
        "success": true,
        "data": {
          // 실제 반환하는 데이터 (객체, 리스트 등)
          "id": 1,
          "name": "사용자명",
          "email": "user@example.com"
        },
        "error": null
      }
    • 전달할 데이터가 없는 경우 (단순 성공 응답)
      {
        "success": true,
        "data": null,
        "error": null
      }
  • 에러났을 때
    {
      "success": false,
      "data": null,
      "error": {
        "code": "USER-001",
        "message": "사용자를 찾을 수 없습니다",
        "status": 404
      }
    }

우리는 ok(), ok(data), error(errorResponse) 이렇게 세 가지 정적 팩토리 메소드를 제공해서 성공/에러 응답을 전달함

정적 팩토리 메서드

  • 객체 생성을 담당하는 정적 메서드
    • 정적이란?
      • 객체에 속하지 않고 클래스에 속함
      • static 키워드가 붙은 메소드나 변수는 클래스당 딱 하나만 존재하고 모든 객체가 공유한다!
      • static 함수처럼 전역에 한 번 선언해두면 어디서든 불러와서 쓸 수 있는 것
      • 객체 안 만들고 클래스 이름으로 바로 호출할 수 있는 것
  • 생성자와 객체를 만드는 방식이 다름
  • from : 매개변수를 하나 받아서 리턴하는 형변환 메서드
    Data d = Date.from(instant);
  • of : 여러 매개변수를 받아 적합한 타입을 반환하는 집계 메서드
    Data d = List.of(1, 2, 3);
  • valueOf : from과 of의 더 자세한 버전
    Data d = BigInteger.valueOf(42);

image.png

image.png

image.png

코드 비교

객체 생성은 둘 다 일어나지만, 정적 팩토리 메서드는 그 생성 로직을 캡슐화해서 사용하는 쪽의 코드를 깔끔하게 해준다는 장점이 있음

  • // 매번 이렇게 생성해야 함
    CommonResponseDto<Object> response = new CommonResponseDto.CommonResponseDtoBuilder<Object>()
        .success(false)
        .data(null)
        .error(errorResponse)
        .build();
  • // 이렇게 간단하게!
    CommonResponseDto<Object> response = CommonResponseDto.error(errorResponse);

메서드 활용

// 기본 에러 메시지 사용
ErrorResponse response1 = ErrorResponse.of(ErrorCode.USER_NOT_FOUND);

// 커스텀 에러 메시지 사용
ErrorResponse response2 = ErrorResponse.of(
    ErrorCode.VALIDATION_ERROR, 
    "이메일 형식이 올바르지 않습니다."
);
  • error enum
// 인터페이스 기반 공통 에러 코드
public enum CommonErrorCode implements ErrorCode {
    INVALID_INPUT(400, ~~"COM-001",~~ "잘못된 입력입니다"),
    UNAUTHORIZED(401, ~~"COM-002",~~ "인증이 필요합니다"),
    SERVER_ERROR(500, ~~"COM-003",~~ "서버 오류가 발생했습니다");
    
    private final int status;
    private final String code;
    private final String message;
}

Lombok 활용

@RequiredArgsConstructor : 생성자를 직접 구현

  • public enum GlobalErrorCode implements ErrorCode {
        // 공통 에러
        INVALID_INPUT_VALUE(400, ~~"GLOBAL-001",~~ "잘못된 입력값입니다"),
        INTERNAL_SERVER_ERROR(500, ~~"GLOBAL-002",~~ "서버 오류가 발생했습니다"),
        // ... 다른 에러 코드들 ...
    
        private final int status;
        private final String code;
        private final String message;
    
        // 직접 생성자 구현
        GlobalErrorCode(int status, String code, String message) {
            this.status = status;
            this.code = code;
            this.message = message;
        }
    
        // 직접 getter 구현
        @Override
        public int getStatus() {
            return status;
        }
    
        @Override
        public String code() {
            return code;
        }
    
        @Override
        public String getMessage() {
            return message;
        }
    }
  • @Getter
    @RequiredArgsConstructor
    public enum GlobalErrorCode implements ErrorCode {
        // ... 에러 코드들 ...
    
        private final int status;
        private final String code;
        private final String message;
    
        @Override
        public String code() {
            return name(); 
            //String code 부분에 enum의 name을 넣으면 되므로 파라미터로 400이랑 message만 있으면 된다!
        }
    }

전역 예외 처리

공통 응답 코드를 만들었다면, 이제는 에러를 던질 수 있는 GlobalException을 만들어야 함.

// 이건 불가능해! - ErrorCode는 예외 클래스가 아니라서
throw GlobalErrorCode.USER_NOT_FOUND;  // 컴파일 에러!

// 이건 가능해! - GlobalException은 RuntimeException을 상속받은 예외 클래스니까
throw new GlobalException(GlobalErrorCode.USER_NOT_FOUND);  // OK!

비즈니스 로직에서 문제가 생겼을 때 throw new GlobalException(에러코드) 형태로 예외를 발생시키고, 이걸 GlobalExceptionHandler가 잡아서 처리하는 구조

처리 흐름

🔄 성공 시나리오 흐름

  1. 컨트롤러에서 비즈니스 로직 처리 (서비스 호출)
  2. 비즈니스 로직 정상 수행 완료
  3. CommonResponseDto.ok(데이터) 호출하여 성공 응답 객체 생성
  4. 클라이언트에게 다음과 같은 응답 반환
{
  "success": true,
  "data": { ... 실제 데이터 ... },
  "error": null
}

🚫 실패 시나리오 흐름

  1. 컨트롤러 또는 서비스에서 예외 상황 발견
  2. throw new GlobalException(에러코드) 실행
  3. 메서드 실행 중단 및 예외 발생
  4. GlobalExceptionHandler가 예외 캐치
  5. 적절한 ErrorResponse 생성 및 CommonResponseDto.error() 호출
  6. 클라이언트에게 다음과 같은 응답 반환:
{
  "success": false,
  "data": null,
  "error": {
    "code": "MISSING_REQUIRED_FIELD",
    "message": "필수 입력값이 누락되었습니다.",
    "status": 400
  }
}

return CommonResponseDto.ok() 라인은 실행되지 않음

GlobalExceptionHandler가 예외를 잡았을 때의 처리 과정

  1. 예외 감지: @ExceptionHandler 어노테이션을 통해 특정 타입의 예외 발생 시 해당 메서드 자동 호출
  2. 로깅: 발생한 예외 정보를 서버 로그에 기록 (log.error() 등을 통해)
  3. 응답 객체 생성:
    • ErrorResponse 객체 생성 - 에러 코드, 메시지, 상태 코드 등 포함
    • CommonResponseDto.error() 호출해 표준화된 응답 형식에 에러 정보 담기
  4. HTTP 응답 설정:
    • 적절한 HTTP 상태 코드 설정 (보통 에러 코드에서 가져옴)
    • 응답 본문에 에러 데이터 포함
  5. 응답 반환: 최종적으로 ResponseEntity 객체 반환해 클라이언트에게 응답 전송
profile
하루하루 배운 것들

0개의 댓글