카테고리 없음

Spring 에서 ResponseEntity와 RestApiResponse로 RESTful API 응답 구조 이해하기 2탄

kingmusung 2024. 9. 6. 01:17

개요.

 

헤커톤 프로젝트로 진짜 남들이 보면 비웃을 수도 있는 Spring Boot로 기본적인 CRUD 구현을 했다,

헤커톤이 끝난 후 다른 사람들이 spring boot로 만든 프로젝트를 보면서 이건 왜 이렇게 했지 등을 연구하면서 공부를 하고 있었다!

 

공부를 하면서 이해하기 어려웠던 부분이나, 나의 회로구조로 이해한 경험을 적어보고 싶었다. 비록 미흡할 수는 있지만 추후 추가적인 공부가 필요하거나, 비슷한 내용에 관해서 새로운 지식을 얻는다면 이어서 써보고 싶다.

 

1편에서 ResponseEntity에 대해 설명을 했고,

ResponseEntity <RestApiResponse>에 RestApiResponse 부분에 대해서는 설명을 하지 않았다, 2편에서는 해보려고 한다.

 

https://kingmusung.tistory.com/61

 

Spring 에서 ResponseEntity와 RestApiResponse로 RESTful API 응답 구조 이해하기 1탄

개요.헤커톤 프로젝트로 진짜 남들이 보면 비웃을 수도 있는 Spring Boot로 기본적인 CRUD 구현을 했다,헤커톤이 끝난 후 다른 사람들이 spring boot로 만든 프로젝트를 보면서 이건 왜 이렇게 했지 등

kingmusung.tistory.com

 


RestApiResponse는 무엇이고 왜? 쓰는걸까

 

RestApiResponse는 사용자 정의 클래스이다.

 

즉 내가 만들어서 쓰는 것이다. 

 

왜? 만들어서 쓰는지가 가장 중요할 거 같다. 이유도 모르고 쓰면 안 하는 것만 못하니.

 

    @GetMapping("/{id}")
    public ResponseEntity<RestApiResponse> getWish(@PathVariable("id") Long id) {
        Wish wish = wishService.getWish(id);
        RestApiResponse response = new RestApiResponse(SuccessCode.FIND_WISH, wish);
        return ResponseEntity.status(HttpStatus.OK).body(response);
    }

 

1편에서 사용한 소원 ID에 대응하는 소원 하나를 GET 해오는 메서드이다.

 

ResponseEntity만 사용을 한다면 무슨 일이 벌어지는지 보여보겠다!

 

@GetMapping("/{id}")
@CrossOrigin(origins = "http://localhost:5173")
public ResponseEntity<Map<String, Object>> getWish(@PathVariable("id") Long id) {

    Wish wish = wishService.getWish(id);
    
    // 응답 본문에 들어갈 데이터를 Map으로 구성
    Map<String, Object> responseBody = new HashMap<>();
    responseBody.put("code", "W001");
    responseBody.put("status", "OK");
    responseBody.put("message", "소원 조회 성공");
    responseBody.put("data", wish); // 실제 데이터 (Wish 객체)

    return ResponseEntity
            .status(HttpStatus.OK)
            .body(responseBody);
}

 

대충 읽어 보아도, 무엇이 문제인지 보일 것이다. 단순히 생각하면 코드가 길어져서 귀찮아진다.

 

하지만 귀찮아서도 그럴 수 있겠지만, 중요한 이유는.

 

1. 모든 API응답에서 일관성 있는 구조를 사용함으로 추후에 유지보수에 용이하다.

2. 확장성, 중간에 혹여나 다른 데이터도 넣고 싶다거나 수정을 원한다면, 응답코드들이 정의되어 있는 부분만 수정하면 된다!

3. 간결함, 귀찮은 걸 넘어 일단 간결해지면 가독성도 높아지고 다른 사람이 코드를 보더라도 이해하기 쉬워진다.

 

오케이! 왜 사용하는지 알았으면 되었다!!

 


RestApiResponse 구조를 보자!

 

직독직해 느낌으로 끊어서 보는 게 편할 듯하다.

 

package com.hackaton.Good_Night_3rd_Hackathon_Backend.common;

import lombok.Getter;
import lombok.Setter;
import org.springframework.http.HttpStatus;

import java.util.Objects;

//enum 으로 오류코드를 정의(오류코드 및 메시지는 정해져있으므로 상수? 취급을 해서 열거형으로 고정) -> 사용자 응답을 할때
// 상태코드, Data도 포함이 되어야하기 때문에, 열거형 + data를 일관성 있게 입력 후 사용자에게 응답을 주기위해
//RestApiResponse 클레스를 따로 만들었음.
@Getter
@Setter
public class RestApiResponse {

    private HttpStatus status;
    private String code;
    private String message;
    private Object data;

    public RestApiResponse(SuccessCode successCode){
        this.status = successCode.getStatus();
        this.code = successCode.getCode();
        this.message = successCode.getMessage();

    }

    public RestApiResponse(SuccessCode successCode, Object data){
        this.status = successCode.getStatus();
        this.code = successCode.getCode();
        this.message = successCode.getMessage();
        this.data = data;
    }

    public RestApiResponse(ErrorCode errorCode){
        this.status = errorCode.getStatus();
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
    }
    public RestApiResponse(ErrorCode errorCode, Object data){
        this.status = errorCode.getStatus();
        this.code = errorCode.getCode();
        this.message = errorCode.getMessage();
        this.data = data;
    }
}

 

일단 생성자는 보지 말고! 멤버 변수만 주목!!

 

클래스의 멤버를 보면 상태, 상태코드, 메시지, 데이터가 있을 것이다. 오! 이거 어디서 본 거 같지 않아?

private HttpStatus status;
    private String code;
    private String message;
    private Object data;

 

 

응답 본문이랑 형식이 똑같쥬?


오케이 그럼, 생성자를 차근차근 보자.

 

뭐 어떤 건 Object Data를 생성자에 포함하는 경우도 있고 아닌 경우도 있다.

쉽게 생각해 보면,

 

1. "소원생성"을 하면 생성이 잘 되었다는 상태만 보여주면 되지 굳이 Data값까지 리턴해줄 필요는 없을 것이다 그리고,

2. "소원조회"를 한다면 소원이 잘 조회되었다는 상태와, Data(소원객체)도 보여줘야 한다,

조회를 했는데 1번처럼 상태만 보여줄 수는 없지 않습니까!

 

자 다시 돌아와서

public RestApiResponse(SuccessCode successCode){...}

 

첫 번째 생성자를 보면 SuccessCode객체를 생성자의 인자로 받고 있다, 아 그놈의 객체 ㅋㅋㅋㅋ

 

public RestApiResponse(SuccessCode successCode, Object data){...}

 

두 번째 생성자를 보면 SuccessCode객체 외에 Object형식의 data를 인자로 받고 있다, 위에 비유한 예시를 생각하면 될듯하다.

 

여기서 핵심

 

"HTTP의 상태, 상태코드, 메시지를 한 번에 관리하기 위해 RestApiResponse를 쓴다고 했잖아 아니 근데 클래스 보니까 개뿔 아무것도 없는데?"라고 난 처음에 생각했었다.

 

일단 SuccessCode라는 뭔지 모르는 객체를 받는구나라고 일단 이해하고 넘어가자.

 


SuccessCode (Class 아님 Enum임)

 

package com.hackaton.Good_Night_3rd_Hackathon_Backend.common;

import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
public enum SuccessCode {
    WISH_CREATE_SUCCESS(HttpStatus.CREATED,"W001","소원 등록 완료"),
    FIND_WISH(HttpStatus.OK, "W002", "소원 조회 성공"),
    FIND_WISH_LIST(HttpStatus.OK, "W003","소원 리스트 조회 성공"),
    DELETE_WISH(HttpStatus.OK,"W004","소원 삭제 성공"),
    CONFIRM_WISH(HttpStatus.OK,"W005","소원 승인 완료");



    private HttpStatus status;
    private String code;
    private String message;
    SuccessCode(HttpStatus status, String code, String message){
        this.status=status;
        this.code=code;
        this.message=message;
    }
}
 

Enum을 사용해서 미리 상태, 상태코드, 메시지를 정의를 해놓고 사용하는 것이다.

 

Enum을 사용함으로

 

혹여나 내가 HTTP응답을 작성할 때 일관성이 없게 작성을 한다거나, 오타가 난다거나, 할 이유도 없다.

 

그리고 무엇보다 코드 또한 간결해진다.

 

최종정리를 하기 전!


Enum

Enum에 대해 내가 이해가 부족했어서, 그 부분을 적어 보려고 한다.

 

일단 Enum 열거형이라고 한다. 사실 Enum이라고 하면

public enum Grade {
    Basic,
    Silver,
    Gold,
    VIP;
}

 

이런 식으로 정의를 한다음 " memner.grade = Grade.Gold " 이런식으로 사용하는 줄만 알았다.

 

비유가 맞는지 모르겠지만 TypeScript에 Interface 같은 느낌정도로만... 생각했었다.

    WISH_CREATE_SUCCESS(HttpStatus.CREATED,"W001","소원 등록 완료"),
    FIND_WISH(HttpStatus.OK, "W002", "소원 조회 성공"),
    //몇개 생략
    private HttpStatus status;
    private String code;
    private String message;
    
    SuccessCode(HttpStatus status, String code, String message){
        this.status=status;
        this.code=code;
        this.message=message;
    }

 

이런 식으로 생성자를 만들어서 객체처럼 사용가능한지를 이번에 처음 알았다. 진짜 많은 걸 배웠다.

 

단 Enum의 생성자는 외부에서 호출을 못한다.

 SuccessCode.WISH_CREATE_SUCESS

 

외부에서 위와 같이 호출을 한다면, 

(HttpStatus.CREATED,"W001","소원 등록 완료")

Enum에 정의했던 위와 같은 부분이 생성자에 들어가면서 하나의 객체로 나오게 된다는 것이다.

 


정리.

 

    @GetMapping("/{id}")
    public ResponseEntity<RestApiResponse> getWish(@PathVariable("id") Long id) {
        Wish wish = wishService.getWish(id);
        RestApiResponse response = new RestApiResponse(SuccessCode.FIND_WISH, wish);
        return ResponseEntity.status(HttpStatus.OK).body(response);
    }

 

1. 위에서 정의한 RestApiResponse 객체를 만든다

public RestApiResponse(SuccessCode successCode, Object data)

 

 

2. 생성자에 데이터가 들어가는 구조를 다시 한번 생각한다.

 

3. Enum으로 이미 정의를 했으므로 " SuccessCode. " 까지 치면 내가 정의한 열거형들이 나오므로 엔터치고 쓰면 편하다.