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가 응답 본문을 직렬화하기 직전에 응답을 변형하거나 추가 작업을 수행할 수 있게 하는 인터페이스
- 컨트롤러는 BaseResponse<T>만 반환(상태 코드를 직접 지정하지 않는다)
- ResponseBodyAdvice가 BaseResponse 안의 상태 코드를 꺼내 HTTP 응답 상태 코드로 설정
- 클라이언트에게는 의도한 상태 코드와 본문이 전달
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 패턴이 좋은 경우
- 컨트롤러가 많고 보일러플레이트가 누적되는 프로젝트
- 응답 구조를 전사적으로 통일하고 싶은 경우
'Back-end > SpringBoot' 카테고리의 다른 글
| [Spring Boot] Swagger 적용 (0) | 2026.06.28 |
|---|---|
| [Spring Boot] 헬스 체크(Health Check) 구현 (0) | 2026.06.26 |
| [Spring Boot] 공통 예외처리 구현 - CustomException, GlobalExceptionHandler (0) | 2026.06.25 |
| [Spring Boot] JPA Auditing으로 BaseTimeEntity 구현 (0) | 2026.06.25 |
| [Spring Boot] Chap 4 - 도서 관리 서비스 JPA 사용 및 트랜잭션 (0) | 2026.01.22 |