
백엔드에서 API를 만들다 보면 단순히 “값을 잘 반환하는가”만 생각하기 쉽습니다.
저도 처음에는 요청이 오면 필요한 데이터를 꺼내서 JSON으로 내려주면 된다고 생각했습니다.
그런데 API가 하나둘 늘어나기 시작하면 생각보다 금방 문제가 보입니다.
어떤 API는 문자열만 반환하고, 어떤 API는 JSON 객체를 반환하고, 또 어떤 API는 성공했을 때와 실패했을 때 응답 형식이 완전히 다르게 내려오는 식입니다.
처음에는 큰 문제가 아닌 것처럼 보여도, 프론트엔드와 함께 작업하거나 API 개수가 많아질수록 이런 차이는 점점 불편해집니다.
특히 프론트엔드 입장에서는 API마다 응답 형식을 따로 해석해야 하고, 어떤 경우에는 성공 여부를 판단하는 방식조차 달라질 수 있습니다.
그래서 이번 글에서는 워크북 내용을 바탕으로 왜 API 응답을 통일해야 하는지, 공통 응답 객체는 어떤 구조로 만들 수 있는지, 그리고 응답 DTO와 함께 관리하는 이유는 무엇인지를 정리해보려고 합니다.
이번 글에서 다룰 내용은 다음과 같습니다.
- API 응답을 왜 통일해야 하는가
- 공통 응답 객체는 어떤 형태로 구성되는가
- code, message, result는 각각 어떤 역할을 하는가
- 성공 응답 코드를 왜 따로 관리하는가
- 응답 DTO를 함께 사용하는 이유는 무엇인가
- Controller에서 공통 응답 객체를 어떻게 사용하는가
왜 API 응답을 통일해야 할까?

API를 처음 몇 개만 만들 때는 응답 형식이 조금씩 달라도 크게 불편하지 않을 수 있습니다.
예를 들어 어떤 API는 그냥 "성공"이라는 문자열을 반환하고, 어떤 API는 { "name": "홍길동" } 같은 객체를 반환해도 일단 동작은 합니다.
하지만 프로젝트가 커지면 이런 방식은 금방 한계를 드러냅니다.
예를 들어 프론트엔드가 여러 API를 호출한다고 해보겠습니다.
- 어떤 API는 성공 여부를 HTTP 상태 코드로만 판단하고
- 어떤 API는 message 문자열을 보고 판단하고
- 어떤 API는 result가 있으면 성공이라고 가정하고
- 어떤 API는 실패했을 때 아예 다른 구조를 내려준다면
프론트엔드 입장에서는 API마다 대응 로직을 따로 작성해야 합니다.
즉, 백엔드가 응답을 제각각 보내기 시작하면 결국 클라이언트가 그 복잡함을 대신 감당하게 되는 구조가 됩니다.
그래서 API 응답 통일은 단순히 보기 좋게 만들기 위한 작업이 아니라, 클라이언트와의 약속을 명확하게 맞추는 과정이라고 생각했습니다.
보통 어떤 형식으로 통일할까?
프로젝트마다 세부 구조는 다를 수 있지만, 보통은 다음과 같은 형태를 많이 사용합니다.
{
"isSuccess": true,
"code": "COMMON200",
"message": "요청에 성공했습니다.",
"result": {
"name": "홍길동"
}
}
이 구조를 보면 응답이 크게 네 부분으로 나뉘어 있습니다.
- isSuccess
- code
- message
- result
이 형식의 장점은 응답을 받는 쪽에서 항상 같은 틀로 결과를 해석할 수 있다는 점입니다.
즉, 어떤 API든 먼저 isSuccess를 보고 성공 여부를 확인하고, code와 message를 통해 결과의 의미를 파악하고, 실제 데이터는 result에서 꺼내 쓰면 됩니다.
이렇게 기준이 정해져 있으면 API를 추가하더라도 응답 구조 자체는 흔들리지 않게 됩니다.
isSuccess는 왜 필요할까?
처음에는 “어차피 HTTP 상태 코드가 있는데 굳이 성공 여부를 또 넣어야 하나?”라는 생각이 들 수 있습니다.
그런데 실제로는 응답 본문 안에 성공 여부가 명확하게 들어가 있으면 클라이언트에서 처리하기가 훨씬 편합니다.
특히 공통 응답 객체를 기준으로 파싱할 때는, 응답 구조를 열자마자 성공인지 실패인지 바로 확인할 수 있다는 점이 꽤 직관적입니다.
물론 프로젝트에 따라 isSuccess 없이 설계할 수도 있습니다. 하지만 응답을 일관되게 다루고 싶다면 이런 식의 명시적인 필드는 꽤 유용하다고 느꼈습니다.
code는 왜 따로 둘까?
code는 HTTP 상태 코드와는 조금 다른 역할을 합니다.
HTTP 상태 코드는 요청이 전체적으로 성공했는지, 잘못되었는지, 서버 에러가 났는지를 나타내는 큰 범주의 신호라면, code는 그 안에서 조금 더 구체적인 결과를 표현하는 값에 가깝습니다.
예를 들어 같은 400번대 에러라고 해도
- 잘못된 입력값인지
- 존재하지 않는 회원인지
- 중복된 요청인지
는 서로 다른 의미를 가질 수 있습니다.
이런 상황에서 code를 두면 프론트엔드나 다른 팀원이 결과를 훨씬 명확하게 이해할 수 있습니다.
즉, code는 단순한 문자열이 아니라 프로젝트 안에서 결과를 식별하는 공통 언어라고 볼 수 있습니다.
message는 어떤 역할을 할까?
message는 code를 사람이 읽기 쉬운 문장으로 풀어주는 역할을 합니다.
예를 들어 MEMBER200_1 같은 코드만 보면 개발자끼리는 약속된 의미를 알 수 있을지 몰라도, 바로 해석하기는 어렵습니다.
하지만 여기에
- “성공적으로 유저를 조회했습니다.”
- “존재하지 않는 회원입니다.”
- “잘못된 요청입니다.”
같은 메시지가 함께 오면 응답 내용을 이해하기 훨씬 쉬워집니다.
개인적으로는 이 필드가 디버깅할 때도 꽤 유용하다고 느꼈습니다.
Swagger나 Postman으로 테스트할 때도 결과를 한눈에 확인하기 편하고, 예외 상황이 생겼을 때 어떤 문제가 발생했는지 빠르게 파악할 수 있기 때문입니다.
result에는 무엇이 들어갈까?
result는 실제로 클라이언트가 필요로 하는 데이터를 담는 영역입니다.
예를 들어 회원 조회 API라면 이름, 이메일, 포인트 같은 데이터가 들어갈 수 있고, 목록 조회 API라면 리스트 형태의 데이터가 들어갈 수 있습니다.
즉, 공통 응답 객체에서 isSuccess, code, message는 공통 메타 정보에 가깝고, result가 실제 비즈니스 데이터라고 볼 수 있습니다.
이렇게 나누면 응답의 바깥 구조는 항상 유지하면서도, API마다 필요한 데이터만 result에 유연하게 넣을 수 있습니다.
이 점이 공통 응답 객체의 가장 큰 장점 중 하나라고 생각했습니다.
왜 공통 응답 객체를 따로 만들어야 할까?
응답을 통일하려면 결국 모든 API가 공통적으로 사용할 수 있는 객체가 필요합니다.
예를 들면 이런 식의 구조를 생각할 수 있습니다.
public class ApiResponse<T> {
private final Boolean isSuccess;
private final String code;
private final String message;
private final T result;
// 생성자, 정적 팩토리 메서드 등
}
여기서 중요한 점은 result가 제네릭 타입이라는 것입니다.
왜냐하면 어떤 API는 문자열을 반환할 수도 있고, 어떤 API는 DTO를 반환할 수도 있고, 어떤 API는 리스트를 반환할 수도 있기 때문입니다.
즉, ApiResponse<T>로 만들면 공통 구조는 유지하면서도 내부 데이터 타입은 유연하게 바꿀 수 있습니다.
정리해보면 공통 응답 객체는 단순히 클래스를 하나 더 만드는 작업이 아니라, 모든 API가 같은 언어로 응답하도록 기준을 세우는 작업이라고 볼 수 있습니다.
성공 코드와 실패 코드를 왜 enum으로 관리할까?
응답 통일을 하다 보면 자연스럽게 코드와 메시지를 어디에서 관리할지도 고민하게 됩니다.
처음에는 Controller나 Service 안에 문자열을 직접 적어도 동작은 합니다.
하지만 이런 방식은 API가 많아질수록 관리가 어려워집니다.
예를 들어 여러 곳에서 "성공적으로 조회했습니다" 같은 문장을 직접 작성하다 보면
- 오타가 생길 수 있고
- 같은 의미인데 표현이 달라질 수 있고
- 나중에 문구를 바꿔야 할 때 수정 범위가 커질 수 있습니다
그래서 보통은 성공 코드와 실패 코드를 enum으로 분리해서 관리합니다.
예를 들어
- GeneralSuccessCode
- MemberSuccessCode
- GeneralErrorCode
- MemberErrorCode
처럼 나누면 도메인별로 관리하기도 쉽고, 코드 체계도 더 명확해집니다.
특히 도메인형 구조를 사용하는 프로젝트에서는 각 도메인 담당자가 자기 영역의 코드만 관리할 수 있다는 점도 장점이라고 느꼈습니다.
응답 DTO는 왜 따로 둘까?
공통 응답 객체가 있으면 모든 API 응답을 한 번에 통일할 수 있습니다. 그런데 여기서 한 가지 더 중요한 포인트가 있습니다.
바로 result 안에 들어가는 데이터도 DTO로 따로 관리하는 것이 좋다는 점입니다.
예를 들어 회원 조회 API가 있다고 할 때, 그냥 엔티티를 그대로 result에 넣어버릴 수도 있습니다.
하지만 이렇게 하면 문제가 생길 수 있습니다.
- 클라이언트에게 필요 없는 값까지 노출될 수 있고
- 엔티티 구조가 바뀌면 응답 구조도 함께 흔들릴 수 있고
- API 명세와 실제 응답이 어긋날 수 있습니다
반대로 응답 DTO를 따로 만들면 클라이언트에게 보여줄 데이터만 선택해서 응답할 수 있습니다.
예를 들면 이런 식입니다.
public class MemberResDTO {
@Builder
public record MyPageResponse(
String name,
String profileUrl,
String email,
String phoneNumber,
Integer point
) {}
}
이렇게 해두면 result에 어떤 데이터가 들어가는지 더 명확해지고, 응답 스펙도 훨씬 읽기 쉬워집니다.
그래서 공통 응답 객체와 응답 DTO는 서로 대체 관계가 아니라, 함께 써야 더 안정적인 구조라고 생각했습니다.
Controller에서는 어떻게 사용할까?
공통 응답 객체를 만들었다면 Controller에서는 이를 감싸서 반환하면 됩니다.
예를 들어 회원 조회 API라면 대략 이런 형태가 됩니다.
@PostMapping("/v1/users/me")
public ApiResponse<MemberResDTO.MyPageResponse> getMyPage(
@RequestBody MemberReqDTO.MyPageRequest request
) {
MemberResDTO.MyPageResponse result = memberService.getMyPage(request);
return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_FOUND, result);
}
이 구조를 보면 흐름이 다음과 같습니다.
- Controller는 요청 DTO를 받는다
- Service는 응답 DTO를 만든다
- Controller는 그 DTO를 ApiResponse로 감싸서 반환한다
즉, 실제 데이터는 DTO로 관리하고, 전체 응답 형식은 ApiResponse가 담당하는 구조입니다.
이 방식의 장점은 API마다 result 내용은 달라도, 바깥 응답 틀은 항상 같다는 점입니다.
응답을 통일하면 무엇이 좋아질까?
직접 정리해보면서 느낀 장점은 크게 세 가지였습니다.
1. 프론트엔드와의 약속이 명확해진다
프론트엔드는 모든 API 응답을 같은 방식으로 해석할 수 있습니다.
성공 여부, 코드, 메시지, 실제 데이터를 어디서 꺼내야 하는지가 항상 같기 때문입니다.
2. API 문서와 테스트가 더 명확해진다
Swagger에서 응답을 확인할 때도 구조가 통일되어 있으면 훨씬 읽기 쉽습니다.
어떤 API가 어떤 형태로 데이터를 내려주는지 한눈에 파악하기 편합니다.
3. 유지보수가 쉬워진다
응답 형식을 수정해야 할 일이 생겼을 때도 공통 응답 객체를 중심으로 관리할 수 있습니다.
API마다 제각각 수정하는 것보다 훨씬 안정적입니다.
결국 API 응답 통일은 단순한 형식 맞추기가 아니라, 협업과 유지보수를 위한 기본 설계라고 느꼈습니다.
이번 내용을 정리하며 느낀 점
이번 내용을 정리하면서 가장 크게 느낀 점은, API 응답은 단순히 데이터를 보내는 문제가 아니라는 점이었습니다.
응답을 어떻게 설계하느냐에 따라 프론트엔드가 데이터를 해석하는 방식이 달라지고, 협업의 난이도도 꽤 많이 달라집니다.
특히 인상 깊었던 부분은 공통 응답 객체와 응답 DTO의 역할이 생각보다 분명하다는 점이었습니다.
- 공통 응답 객체는 전체 형식을 통일하고
- 응답 DTO는 실제 비즈니스 데이터를 명확하게 표현합니다
이 둘을 함께 사용하면 응답 구조가 훨씬 안정적으로 느껴졌습니다.
이전에는 그냥 JSON만 잘 내려주면 된다고 생각했는데, 이번에 정리하면서 보니 결국 중요한 것은 클라이언트가 예측 가능한 방식으로 응답을 받을 수 있게 만드는 것이라는 생각이 들었습니다.
마무리
이번 글에서는 API 응답을 왜 통일해야 하는지와 공통 응답 객체를 어떤 방식으로 설계할 수 있는지 정리해보았습니다.
프로젝트 초반에는 응답 형식이 조금씩 달라도 큰 문제가 없어 보일 수 있지만, API가 늘어나고 협업이 시작되면 통일된 응답 구조의 중요성이 훨씬 크게 느껴집니다.
특히 isSuccess, code, message, result처럼 공통된 틀을 두고, 실제 데이터는 DTO로 분리해서 관리하면 응답을 훨씬 더 명확하고 안정적으로 다룰 수 있습니다.
결국 API 응답 통일은 보기 좋은 형식을 맞추는 작업이 아니라, 클라이언트와 서버가 같은 기준으로 데이터를 주고받기 위한 약속을 만드는 과정이라고 생각합니다.
다음 글에서는 이번 흐름에서 이어서, 예외가 발생했을 때도 같은 응답 형식을 유지할 수 있도록 돕는 전역 에러 핸들러에 대해 정리해보려고 합니다.