본문 바로가기
  • Let's study
BackEnd/Spring

REST API 응답 형식 통일하기 - HTTP 상태 코드와 RsData

by 코딩고수이고파 2026. 8. 12.

1. REST API의 응답을 JSON으로 통일하기

REST API에서는 프론트엔드와 서버가 데이터를 주고받기 때문에 일관된 응답 형식을 사용하는 것이 중요하다.

조회 API에서는 JSON을 반환하면서 추가, 수정, 삭제 API에서는 평문을 반환한다면 프론트엔드에서 각각 다른 방식으로 응답을 처리해야 한다.

예를 들어 다음과 같은 응답이 있다고 가정한다.

게시물이 삭제되었습니다.

문자열만 반환하기 때문에 프론트엔드에서는 이 메시지가 성공을 의미하는지, 실패를 의미하는지 별도로 판단해야 한다.

따라서 REST API에서는 응답을 일정한 JSON 구조로 통일하는 것이 좋다.

2. 응답에 결과 코드와 메시지를 사용하기

비즈니스 로직이 복잡해지면 단순히 HTTP 상태 코드만으로 모든 결과를 표현하기 어려워진다.

세부적인 상황을 표현하기 위해 결과 코드(ResultCode)를 별도로 정의할 수 있다.

예를 들어 프로젝트에서 다음과 같은 규칙을 정할 수 있다.

성공 → 200-1
성공 → 200-2
실패 → F-1
실패 → F-2

이러한 코드는 프로젝트의 요구사항에 따라 자유롭게 정할 수 있다.

다만 HTTP 상태 코드와 프로젝트 내부의 결과 코드는 역할이 다르므로 구분해서 사용하는 것이 좋다.

3. HTTP 주요 상태 코드

HTTP 상태 코드는 요청의 처리 결과를 나타내는 표준 규칙이다.

상태  코드의미 설명
200 OK 요청이 성공적으로 처리됨
201 Created 새로운 리소스가 생성됨
204 No Content 요청이 성공했지만 응답 본문이 없음
400 Bad Request 잘못된 요청
401 Unauthorized 인증이 필요함
403 Forbidden 접근 권한이 없음
404 Not Found 요청한 리소스를 찾을 수 없음
409 Conflict 현재 서버 상태와 요청이 충돌함
422 Unprocessable Entity 요청 형식은 맞지만 처리할 수 없음
500 Internal Server Error 서버 내부 오류
502 Bad Gateway 게이트웨이에서 잘못된 응답을 받음
503 Service Unavailable 서버를 일시적으로 사용할 수 없음

HTTP 상태 코드만 사용해도 충분한 프로젝트가 있지만, 비즈니스 규모가 커지면 더 세부적인 결과를 표현하기 위해 별도의 결과 코드 체계를 사용하는 경우도 있다.

4. RsData로 응답 형식 통일하기

응답을 다음과 같은 구조로 통일할 수 있다.

{
    "resultCode": "200-1",
    "msg": "댓글이 삭제되었습니다."
}

이를 Java 클래스로 만들면 다음과 같다.

@AllArgsConstructor
@Getter
public class RsData {

    private String resultCode;
    private String msg;
}

컨트롤러에서는 다음과 같이 반환할 수 있다.

return new RsData(
        "200-1",
        "%d번 댓글이 삭제되었습니다.".formatted(commentId)
);

이제 프론트엔드는 모든 API에서 동일한 형태로 응답을 처리할 수 있다.

5. 응답에 추가 데이터가 필요한 경우

요청을 처리한 후 프론트엔드에서 정보를 잠시 보관해야 하는 상황이 있을 수 있다.

이 경우 응답에 data를 추가하면 된다.

@AllArgsConstructor
@Getter
public class RsData<T> {

    private String resultCode;
    private String msg;
    private T data;
}

T를 사용했기 때문에 API마다 서로 다른 타입의 데이터를 전달할 수 있다.

예를 들어 삭제된 게시물의 정보를 PostDto로 전달할 수 있다.

@GetMapping("/{id}/delete")
public RsData<PostDto> delete(
        @PathVariable int id
) {
    Post post = postService.findById(id).get();

    postService.delete(id);

    return new RsData<>(
            "200-1",
            "게시물이 삭제되었습니다.",
            new PostDto(post)
    );
}

응답은 다음과 같은 형태가 된다.

{
    "resultCode": "200-1",
    "msg": "게시물이 삭제되었습니다.",
    "data": {
        "postDto": {
            "id": 1,
            "title": "게시물 제목",
            "body": "게시물 내용"
        }
    }
}

이렇게 하면 프론트엔드는 data에 들어 있는 삭제된 게시물 정보를 이용할 수 있다.

6. 전달할 데이터가 없다면 Void 사용

반대로 결과 메시지만 전달하고 별도의 데이터가 필요하지 않은 경우도 있다.

이때는 Void를 사용할 수 있다.

@GetMapping("/{id}/delete")
public RsData<Void> delete(
        @PathVariable int id
) {
    postService.delete(id);

    return new RsData<>(
            "200-1",
            "게시물이 삭제되었습니다."
    );
}

RsData에 생성자를 추가하면 된다.

public RsData(String resultCode, String msg) {
    this.resultCode = resultCode;
    this.msg = msg;
    this.data = null;
}

응답은 다음과 같이 data가 null이 된다.

{
    "resultCode": "200-1",
    "msg": "게시물이 삭제되었습니다.",
    "data": null
}
 

7. 응답 데이터가 여러 개라면

RsData의 data에는 하나의 객체만 담을 수 있다.

하지만 API를 구현하다 보면 여러 데이터를 한 번에 전달해야 하는 경우가 있다.

예를 들어 게시물 생성 후 생성된 게시물과 전체 게시물 개수를 함께 전달할 수 있다.

이때 별도의 응답 객체를 만들어 data에 담을 수 있다.

public record PostWriteResBody(
        PostDto postDto,
        long totalPostCount
) {
}

그리고 다음과 같이 사용한다.

return new RsData<>(
        "201-1",
        "%d번 글이 성공적으로 작성되었습니다.".formatted(post.getId()),
        new PostWriteResBody(
                new PostDto(post),
                postService.count()
        )
);

결과적으로 JSON은 다음과 같은 구조가 된다.

{
    "resultCode": "201-1",
    "msg": "1번 글이 성공적으로 작성되었습니다.",
    "data": {
        "postDto": {
            "id": 1,
            "title": "제목입니다.",
            "body": "내용입니다."
        },
        "totalPostCount": 10
    }
}

댓글