- Rest API의 응답값을 넘기는 형식 중 하나
- 데이터를 캡슐화하는 방법
왜 필요한가?
public class Member {
private Long id;
private String name;
private int age;
}
{
"id" : 1,
"username" : "홍길동",
"age" : 15
}
Member라는 객체를 JSON형태로 표현하면 다음과 같다.
[
{
"id":1,
"username":"홍길동",
"age":15
},{
"id":2,
"username":"아무개",
"age":24
},
...
{
"id":99,
"username":"개똥이",
"age":47
}
]
이 멤버를 여러 명 조회해야 하는 API를 개발해야 한다면 조회된 리스트를 JSON 형식으로 던져준다.
[
{
"id":1,
"username":"홍길동",
"age":15,
"memberCount": 99
},{
"id":2,
"username":"아무개",
"age":24,
"memberCount": 99
},
...
{
"id":99,
"username":"개똥이",
"age":47,
"memberCount": 99
}
]
이렇게 사용하던 API에 모든 멤버의 숫자를 추가해달라는 요청이 왔다면 데이터를 어떻게 넘겨줘야 할까?
따로 멤버 숫자에 해당하는 데이터를 안 넘겨주고 클라이언트가 JSON으로 전달 받은 member의 Id 개수를 세게 함
-> 누락된 멤버가 있을 수 있음
각 멤버 마다 memberCount 필드를 추가함
-> 멤버 각각 가지고 있는 값인지, 조회된 멤버의 카운트인지 알 수 없음
-> 불필요한 데이터 중복으로 인해 네트워크 트래픽이 더 많이 발생함
Envelop Pattern
Envelop Pattern으로 API 확장성을 유지해보자.
data부분을 한 번 감싸는 형태로 API를 바꾼다.
{
"code":"success",
"data":{
"members":[
{
"id":1,
"username":"홍길동",
"age":15
},{
"id":2,
"username":"아무개",
"age":24
},
...
{
"id":99,
"username":"개똥이",
"age":47
}
]
},
"message":null
}
보통 code, data, message를 보내준다.
- code : http status로 표현할 수 없는 상태값
- data : 실제 api 요청에 대한 데이터 값
- message : api 응답이 성공했을 때 null, 에러 발생 시 에러에 대한 정보
{
"code":"success",
"data":{
"members":[
{
"id":1,
"username":"홍길동",
"age":15
},{
"id":2,
"username":"아무개",
"age":24
},
...
{
"id":99,
"username":"개똥이",
"age":47
}
],
"memberCount" : 99
},
"message":null
}
그래서 memberCount를 추가해야 할 일이 생기면 멤버 마다 추가하는 게 아니라 데이터에 필드 하나를 추가해주면 된다.
-> 원하는 결과를 추가로 얻을 수 있게 되었으니 확장성이 늘어남
참고) http status 규격
REST API는 응답에 http status에 담아 보내야 함
-> 이미 정의되어있는 Http status 규격을 따르되
-> 개발 하다 보면 2xx만으로 표현하기 어려운 경우가 있는데 이 때 추가 설명 목적으로 미리 정의된 code값을 넘겨주면 조금 더 명확한 상태를 알 수 있음
https://developer.mozilla.org/ko/docs/Web/HTTP/Reference/Status
Java에서 구현 방법
프론트엔드에서 일관성 있는 API 응답 처리를 위해 Envelop Pattern을 다음과 같이 적용하여 공통 응답 형식을 만든다.
{
statusCode: number;
timestamp: Date;
content: object;
message: string;
}
- statusCode : 응답의 상태 코드
- timestamp : 응답이 생성된 시점
- content : 클라이언트의 요청 처리에 대한 응답 데이터
- message : api 응답 메시지
예시)
{
"statusCode": 200,
"timestamp": "2025-03-01H12:13:56.3514",
"content": {
"memberId": 3000,
"memberName": "John"
},
"message": "회원 정보 조회 성공"
}
공통 API 형식을 지정하는 공통 dto 클래스 정의
@RequiredArgsConstructor
@Getter
@ToString
public class ApiResponse<C> {
private final int statusCode;
private final LocalDateTime timestamp;
private final String message;
private final C content
}
static 메소드로만 ApiResponse 객체를 생성할 수 있게 한다. (오버로딩 활용해서 생성하게 함)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE) //final 필드에 생성자 자동 생성, 생성자의 접근 제어자를 Private으로 막아서 외부에서 직접 객체를 생성하지 못하게 함
@Builder(access = AccessLevel.PRIVATE) //Builder 패턴으로 객체 생성을 쉽게 만듦
@Getter
@ToString
public class ApiResponse<C> {
private final int statusCode;
private final LocalDateTime timestamp;
private final String message;
private final C content
public static <C> ApiResponse<C> of(C content, int statusCode, LocalDateTime timestamp, String message) {
return ApiResponse.builder()
.statusCode(statusCode),
.timestamp(timestamp)
.message(message)
.content(content)
.build();
}
public static <C> ApiResponse<C> of(int statusCode, LocalDateTime timestamp, String message) {
return of(null, statusCode, timestamp, message);
}
public static <C> ApiResponse<C> of(C content, int statusCode, LocalDateTime timestamp) {
return of(content, statusCode, timestamp, null);
}
public static <C> ApiResponse<C> of(int statusCode, LocalDateTime timestamp) {
return of(null, statusCode, timestamp);
}
}
- ApiResponse 클래스를 제네릭 형태로 만들어 객체가 다양한 타입의 content를 가질 수 있게 함
- 예) String, List<User>, Map<String, Object> 등
- 클래스의 필드를 모두 final(불변 객체)로 선언하여 값이 변경될 수 없게 만듦
- 정적 팩토리 메서드 (of)를 호출하여 객체를 생성할 수 있게 함.
- 생성자는 모두 Private으로 만들어 클래스 외부에서 객체를 직접 생성할 수 없게 함
- 빌더 패턴을 적용하여 객체를 메서드 체이닝 방식으로 설정할 수 있게 함
에러 응답
사용자 입력이 잘못됐거나 코드 로직상 문제가 발생한 경우 관련 내용을 클라이언트가 정형화된 형식으로 응답 받아야 한다.
이를 위해 공통 에러 응답 포맷을 정의하여 클라이언트에서 에러를 쉽게 처리할 수 있게 할 수 있다.
{
statusCode: number;
timestamp: Date;
errorCode: string;
errorName: string;
message: string;
detail: object;
}
- statusCode : 현재 에러 응답의 상태 코드
- timestamp : 에러의 응답이 반환되는 시점의 시간
- errorCode : 에러의 코드. 상태 코드만으로는 표현할 수 없는 서비스 고유의 에러 코드를 반환
- errorName : 에러의 이름
- message : 에러 메시지
- detail : 에러 응답에 대한 추가 정보
* 서버에서 클라이언트에게 성공적인 응답을 보낼 때의 timestamp는 응답이 생성된 시점이고 에러 응답을 보낼 때의 timestamp는 에러의 응답이 반환되는 시점의 시간이다. 이렇게 timestamp를 기록하는 기준이 다르게 설정된 이유는 각 응답의 의미와 목적이 다르기 때문이다. 성공적인 응답에서는 클라이언트가 요청한 데이터가 어느 시점의 응답인지 알 수 있도록 응답이 생성된 시점의 시간을 기록해야 한다. 에러 응답의 경우는 클라이언트 입장에서 에러가 언제 발생했는지 보다 언제 응답을 보냈는지가 더 중요하다. 특히 네트워크 지연이나 재시도 요청이 있는 경우 에러 응답이 생성된 시점과 보낸 시점이 다를 수 있기 때문에 응답이 반환되는 시점을 기록해서 언제 문제가 발생했는지를 추적할 수 있도록 한다.
예시)
회원 조회 시 해당 회원이 존재하지 않을 경우
3000번 회원을 조회하려고 했으나 해당 회원이 없으므로 알맞은 에러 응답을 클라이언트로 전송
이 때 몇 번 회원이 존재하지 않는지에 대한 정보 또한 에러 응답에 포함시킴
{
"statusCode": 404,
"timestamp": "2025-03-01H01:23:22.003",
"errorCode": "MEM001",
"errorName": "MEMBER_NOT_FOUND",
"message": "해당 회원이 존재하지 않습니다.",
"detail": {
"memberId": 3000
}
}
Java에서 구현 방법
에러 응답을 구현하기 위해 다음 항목들을 구현해야 함
- 공통 인터페이스
- 에러 코드 enum : 도메인 마다 각자 필요한 에러 코드를 정의하기 위해 사용
- 공통 ResponseDto : 일관된 응답 형식을 보내기 위해 사용
- 예외를 처리하는 RestControllerAdvice
공통 인터페이스
- 모든 에러가 공통된 형식을 가지도록 인터페이스를 정의
- 이 인터페이스를 구현하는 enum 클래스에는 statusCode, errorCode, message 필드가 있어야 하고 @Getter을 붙여야 한다는 것을 알 수 있음
public interface ErrorSpecifiable {
int getStatusCode();
String getErrorCode();
String getMessage();
String name();
}
위 인터페이스를 enum에서 구현한다.
에러 코드 enum
- http 표준 상태 코드 외에 우리 서비스의 고유한 에러를 표현하기 위한 에러 코드를 enum으로 정의
- ErrorSpecifiable 인터페이스를 각 도메인에 맞게 따로 enum으로 구현 -> 필요한 에러 코드를 정의
- enum으로 구현하면 허용된 에러 코드만 사용할 수 있게 강제할 수 있음
- 예) String errorCode = "MEM09090909" //존재하지 않는 에러 코드지만 사용 가능
- 예) MemberErrorCode errorCode = MemberErrorCode.MEMBER_NOT_FOUND; //정의된 에러 코드만 사용 가능
- enum의 각 상수 마다 특정 속성(상태 코드, 에러 코드, 메시지)를 저장할 수 있도록 final 필드를 지정함
- 이렇게 필드가 있는 enum을 사용하려면 생성자가 필요하므로 @RequiredArgsContructor을 붙여줌
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
enum MemberErrorCode implements ErrorSpecifiable {
MEMBER_NOT_FOUND(404, "MEM001", "회원이 존재하지 않습니다."),
INVALID_PASSWORD(401, "MEM002", "패스워드가 존재하지 않습니다.");
private final int statusCode;
private final String errorCode;
private final String message;
}
공통 ResponseDTO
- 클라이언트에게 반환할 공통 에러 응답 형식을 정의한 DTO
- 에러 코드들을 직접 String으로 선언하지 않고 enum을 참조하므로 일관성이 유지되고 실수를 방지할 수 있음
@RequiredArgsConstructor
@Builder
@Getter
@ToString
public class ErrorResponse {
private final int statusCode; // HTTP 상태 코드 (ex: 404, 401)
private final String errorCode; // 내부 시스템에서 정의한 에러 코드 (ex: "MEM001")
private final String errorName; // 에러 이름 (ex: "MEMBER_NOT_FOUND")
private final String message; // 사용자에게 보여줄 메시지 (ex: "회원이 존재하지 않습니다.")
private final LocalDateTime timestamp; // 에러 발생 시간
private final Object detail; // 추가적인 에러 정보 (선택적)
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(access = AccessLevel.PRIVATE)
@Getter
@ToString
public class ErrorResponse {
private final int statusCode;
private final String errorCode;
private final String errorName;
private final String message;
private final LocalDateTime timestamp;
private final Object detail;
public static ErrorResponse of(ErrorSpecifiable e) {
return ErrorResponse.builder()
.statusCode(e.getStatusCode())
.errorCode(e.getErrorCode())
.errorName(e.name())
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
}
public static ErrorResponse of(ErrorSpecifiable e, Object detail) {
return ErrorResponse.builder()
.statusCode(e.getStatusCode())
.errorCode(e.getErrorCode())
.errorName(e.name())
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.detail(detail)
.build();
}
}
https://junhyunny.github.io/information/class-diagram-in-uml/
'프로젝트' 카테고리의 다른 글
| (5)-2 SPA, MPA (0) | 2025.03.30 |
|---|---|
| (5)-1 Spring Boot3에서 Swagger 사용하기 : Swagger 설치, Swagger Config 설정 (0) | 2025.03.30 |
| (3) 알림 엔티티 생성 (0) | 2025.03.05 |
| (2) Issue, Feature Branch, PR 생성 방법 (0) | 2025.03.05 |
| (1) Jira/스크럼(Scrum)이란? 스크럼 진행 방식 / Sprint / Backlog (0) | 2025.02.03 |