[Spring Boot] 공통 응답 처리 구현

01_공통 응답 형식 구현


📍 BaseCode

응답 코드의 공통 규격을 인터페이스로 정의

package com.example.global.response.base;

import org.springframework.http.HttpStatus;

public interface BaseCode {
    HttpStatus getHttpStatus();
    String getMessage();
    String name();
}

 

 

 

📍 SuccessCode

상태 코드별로 HTTP 상태 코드와 메시지 관리

package com.example.global.response.code;

import com.example.global.response.base.BaseCode;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;

@Getter
@RequiredArgsConstructor
public enum SuccessCode implements BaseCode {
    // 200 OK
    OK(HttpStatus.OK, "요청이 성공적으로 처리되었습니다."),

    // 201 CREATED
    CREATED(HttpStatus.CREATED, "리소스가 성공적으로 생성되었습니다."),

    // 204 NO CONTENT
    NO_CONTENT(HttpStatus.NO_CONTENT, "요청이 처리되었으며 반환할 콘텐츠가 없습니다.");

    private final HttpStatus httpStatus;
    private final String message;
}

 

 

 

📍 SuccessResponse

package com.example.global.response.dto;

import com.example.global.response.base.BaseCode;
import com.fasterxml.jackson.annotation.JsonInclude;

@JsonInclude(JsonInclude.Include.NON_NULL)
public record SuccessResponse<T>(
        int status,
        String code,
        String message,
        T data
) {

    // 데이터 없음
    public static <T> SuccessResponse<T> of(BaseCode baseCode) {
        return new SuccessResponse<>(
                baseCode.getHttpStatus().value(),
                baseCode.name(),
                baseCode.getMessage(),
                null
        );
    }

    // 데이터 있음
    public static <T> SuccessResponse<T> of(BaseCode baseCode, T data) {
        return new SuccessResponse<>(
                baseCode.getHttpStatus().value(),
                baseCode.name(),
                baseCode.getMessage(),
                data
        );
    }

    // 데이터 없음, 메시지 커스텀
    public static <T> SuccessResponse<T> of(BaseCode baseCode, String message) {
        return new SuccessResponse<>(
                baseCode.getHttpStatus().value(),
                baseCode.name(),
                message,
                null
        );
    }

    // 데이터 있음, 메시지 커스텀
    public static <T> SuccessResponse<T> of(BaseCode baseCode, String message, T data) {
        return new SuccessResponse<>(
                baseCode.getHttpStatus().value(),
                baseCode.name(),
                message,
                data
        );
    }
}

 

🔍 코드 설명

  • 제네릭 <T>: 반환 데이터 타입을 컴파일 타임에 보장. UserDto, List<UserDto> 등 어떤 타입이든 담을 수 있다.

 

 

컨트롤러에서 사용

@GetMapping("/{id}")
public ResponseEntity<SuccessResponse<UserDto>> getUser(@PathVariable Long id) {
	UserDto user = userService.findById(id);
  return ResponseEntity
            .status(SuccessCode.USER_FOUND.getHttpStatus())
            .body(SuccessResponse.of(SuccessCode.USER_FOUND, user));
}

 

 

 

 

02_BaseResponse


📍 ResponseEntity 방식의 문제점

상태 코드를 두 번 지정한다.(ResponseEntity.status()에 한 번, SuccessResponse 안에 한 번)

반환 타입 ResponseEntity<SuccessResponse<UserDto>>가 길어 한눈에 들어오지 않는다.

모든 컨트롤러에서 ResponseEntity.status(...).body(...) 패턴이 반복되는 보일러플레이트가 발생한다.

public ResponseEntity<SuccessResponse<UserDto>> getUser(@PathVariable Long id) { ... }

 

 

 

📍 BaseResponse

package com.example.global.response.dto;

import com.example.global.response.base.BaseCode;
import com.example.global.response.code.SuccessCode;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BaseResponse<T> {

    private int status;
    private String code;
    private String message;
    private T data;

    // 데이터 있음
    public static <T> BaseResponse<T> of(BaseCode baseCode, T data) {
        return new BaseResponse<>(
                baseCode.getHttpStatus().value(),
                baseCode.name(),
                baseCode.getMessage(),
                data
        );
    }

    // 데이터 없음
    public static <T> BaseResponse<T> of(BaseCode baseCode) {
        return of(baseCode, null);
    }

    public static <T> BaseResponse<T> ok(T data) {
        return of(SuccessCode.OK, data);
    }

    public static <T> BaseResponse<T> ok() {
        return of(SuccessCode.OK, null);
    }
    
    public static <T> BaseResponse<T> created() {
        return of(SuccessCode.CREATED);
    }

    public static <T> BaseResponse<T> noContent() {
        return of(SuccessCode.NO_CONTENT);
    }
}

 

🔍 코드 설명

  • of(): BaseCode를 받는 제네릭 팩토리 메서드. 새로운 SuccessCode가 추가되어도 수정 필요 없음

 

 

 

📍 ResponseBodyAdvice

Spring MVC가 응답 본문을 직렬화하기 직전에 응답을 변형하거나 추가 작업을 수행할 수 있게 하는 인터페이스

  1. 컨트롤러는 BaseResponse<T>만 반환(상태 코드를 직접 지정하지 않는다)
  2. ResponseBodyAdvice가 BaseResponse 안의 상태 코드를 꺼내 HTTP 응답 상태 코드로 설정
  3. 클라이언트에게는 의도한 상태 코드와 본문이 전달
package com.example.global.response;

import com.example.global.response.dto.BaseResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice
public class ApiResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType,
                            Class<? extends HttpMessageConverter<?>> converterType) {
        return BaseResponse.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public Object beforeBodyWrite(Object body,
                                  MethodParameter returnType,
                                  MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                  ServerHttpRequest request,
                                  ServerHttpResponse response) {
        if (body instanceof BaseResponse<?> baseResponse) {
            response.setStatusCode(HttpStatusCode.valueOf(baseResponse.getStatus()));
        }
        return body;
    }
}

 

🔍 코드 설명

  • supports(): 어떤 컨트롤러 메서드에 이 Advice를 적용할지 결정. 반환 타입이 BaseResponse인 경우에만 적용.
    • isAssignableFrom: BaseResponse를 상속한 클래스까지 함께 처리
  • beforeBodyWrite(): 응답 본문이 직렬화되기 직전에 호출됨.

 

 

컨트롤러에서 사용

@GetMapping("/{id}")
public BaseResponse<UserDto> getUser(@PathVariable Long id) {
    return BaseResponse.ok(userService.findById(id));
}

@PostMapping("/{id}")
public BaseResponse<Void> signUp(@PathVariable SignUpRequest request) {
    authService.signUp(request);
    return BaseResponse.created();
}

 

 

 

📍 방식 비교

  • ResponseEntity<SuccessResponse<T>> 방식이 좋은 경우
    • 명시성이 중요한 프로젝트 (코드만 봐도 응답 동작이 완전히 보여야 하는 경우)
    • 헤더, 쿠키, 캐시 제어 등을 자주 다루는 경우

 

  • BaseResponse<T> + ResponseBodyAdvice 패턴이 좋은 경우
    • 컨트롤러가 많고 보일러플레이트가 누적되는 프로젝트
    • 응답 구조를 전사적으로 통일하고 싶은 경우