Common Response Structure

GEONNY·2024년 7월 29일
0

Building-API

목록 보기
9/28
post-thumbnail

공통 응답 구조체에 대해서 생각해 봅시다. API 에서 Operaion 마다 응답하는 구조체가 다르다면, 클라이언트 입장에서는 이에 맞춰 다른 처리 로직을 구현해야 합니다. 응답하는 데이터는 각기 다를 수 있지만, 이를 한번 더 감싸 공통의 구조체를 생성하면 클라이언트 입장에서도 공통의 처리 로직을 구현할 것이고 개발 효율성이 증가할 것 입니다.

그럼 공통 응답 구조체에는 어떤 데이터가 필요할까요? 우선 응답 코드와 메시지가 필요할 것이고 실제 처리된 데이터도 필요합니다. 응답 코드와 메시지의 타입은 확정할 수 있지만, 데이터의 타입은 사용 리소스에 따라 달라질 것이기 때문에 제네릭으로 구성해야 할 것입니다.
아래의 Operation 을 보도록 하죠.

@GetMapping(value = "/members/{memberId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MemberSearchResponse> getMemberById(
		@PathVariable("memberId") String memberId) {
    return ResponseEntity.ok()
            .body(memberService.getMemberById(memberId));
}

조회된 단일 객체를 리턴하는 Operation 입니다. 단일 객체에 대한 공통 응답이 필요할 것 같네요.

@GetMapping(value = "/members", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<MemberSearchResponse>> getMembers() {
    return ResponseEntity.ok()
            .body(memberService.getMembers());
}

위 코드는 Collection 을 리턴하니까 복수의 객체를 리턴할 구조체도 필요할 것 으로 보입니다.
그럼 지금까지 작성한 Operation 에 대한 공통 응답 구조체를 생성해 보도록 하죠.

📌공통 응답 구조체 생성


common/response 패키지를 생성합니다. 그리고 단일 객체에 대한 공통 구조체인 ItemResponse 와 복수 객체 구조체인 ItemsResponse record 를 생성합니다.
이름은 Item,Items 로 구분하긴 했지만 원하는 명칭을 사용하셔도 됩니다. (DataResponse, ListResponse 등등..) 그리고 가독성과 편의성을 위한 Builder pattern 을 사용하기 위해 lombok annotaion 인 @Builder 를 추가하겠습니다.

@Builder
public record ItemResponse<T>(
        String status,
        String message,
        T item
) {
}
@Builder
public record ItemsResponse<T>(
        String status,
        String message,
        List<T> items
) {
}

📌공통 응답 구조체로 리턴

자, 그럼 기존의 Controller 에서 공통 응답 구조체로 리턴하도록 변경해 봅시다.

@GetMapping(value = "/members/{memberId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ItemResponse<MemberSearchResponse>> getMemberById(
 		@PathVariable("memberId") String memberId) {
    return ResponseEntity.ok()
            .body(ItemResponse.<MemberSearchResponse>builder()
                    .status("API_OK")
                    .message("데이터를 조회하는데 성공하였습니다.")
                    .item(memberService.getMemberById(memberId))
                    .build());
}

@GetMapping(value = "/members", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ItemsResponse<MemberSearchResponse>> getMembers() {
    return ResponseEntity.ok()
            .body(ItemsResponse.<MemberSearchResponse>builder()
                    .status("API_OK")
                    .message("데이터를 조회하는데 성공하였습니다.")
                    .items(memberService.getMembers())
                    .build());
}

Request /my-api/members/member1

{
    status: "API_OK",
    message: "데이터를 조회하는데 성공하였습니다.",
    item: {
        memberId: "member1",
        memberName: "회원 명"
    }
}

Request /my-api/members

{
    status: "API_OK",
    message: "데이터를 조회하는데 성공하였습니다.",
    items: [
          {
            memberId: "member1",
            memberName: "회원 명"
          }
      ]
}

📌 공통 메시지 처리

위 처럼 status(200).message("데이터를 조회하는데 성공하였습니다.") 와 같이 사용하게 되면 메시지나 코드가 변경되면 모든 부분을 찾아서 바꿔야 합니다. 개발자마다 작성해야 한다면 오타가 발생할 확율도 높겠죠. IDE 에서 전체 변경 기능을 제공하긴 하지만 동일한 명칭을 사용한 다른 로직이 있다면 문제가 발생할 수도 있습니다. 그렇기 때문에 공통 메시지를 관리할 필요가 있는 것이죠. 타입 안전성과 코드 내 문서화가 중요한 경우 Enum 을 사용해도 되긴 하지만 다국어 처리등의 어려움이 있기 때문에 보통 properties 파일을 선호합니다.

📍message.properties 생성

공통 메시지 처리를 위한 message.properties 파일을 생성합니다.
resources/messages 에서 마우스 오른쪽 버튼을 클릭하여 New > Resource Bundle 을 선택합니다.

Create Resource Bundle 팝업이 뜨면 Resource bundle base namemessage 를 입력하고 OK 버튼을 클릭합니다.

message.properties 파일이 생성되면 아래의 내용을 입력합니다.

SUCCESS.SEARCH.CODE=API_OK
SUCCESS.SEARCH.MESSAGE=데이터를 조회하는데 성공하였습니다.

application.yml 파일에 message 관련 설정을 추가해줍니다.

spring:
  messages:
    basename: messages/message # message basename 설정, 
    encoding: UTF-8 # 인코딩 방식
    cache-duration: 30 # 캐시 주기(분)
    always-use-message-format: true # messageFormat을 전체 메시지에 적용 여부
    use-code-as-default-message: true # Message 파일이 없을 때 key를 그대로 전송할지 여부
    fallback-to-system-locale: true # 감지도 Locale 파일이 없는 경우 System Locale 설정 여부

이런 설정을 통해 MessageSource 가 bean 에 등록 되게 됩니다.

Controller 응답 부분도 message.properties 에 등록된 메시지로 리턴하도록 수정해줍니다.

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
    private final MessageSource messageSource;

    @GetMapping(value = "/members/{memberId}", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ItemResponse<MemberSearchResponse>> getMemberById(
    		@PathVariable("memberId") String memberId) {
        return ResponseEntity.ok()
                .body(ItemResponse.<MemberSearchResponse>builder()
                        .status(messageSource
                        	.getMessage("SUCCESS.SEARCH.CODE", null, Locale.getDefault()))
                        .message(messageSource
                        	.getMessage("SUCCESS.SEARCH.MESSAGE", null, Locale.getDefault()))
                        .item(memberService.getMemberById(memberId))
                        .build());
    }

    @GetMapping(value = "/members", produces = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<ItemsResponse<MemberSearchResponse>> getMembers() {
        return ResponseEntity.ok()
                .body(ItemsResponse.<MemberSearchResponse>builder()
                        .status(messageSource
                        	.getMessage("SUCCESS.SEARCH.CODE", null, Locale.getDefault()))
                        .message(messageSource
                        	.getMessage("SUCCESS.SEARCH.MESSAGE", null, Locale.getDefault()))
                        .items(memberService.getMembers())
                        .build());
    }

수정을 완료하고 응답을 확인해보겠습니다. 아래와 같이 Encoding 이 잘못된 경우 IDE의 encoding 방식을 변경해 줍니다.

{
    status: 200,
    message: "???? ????? ???????.",
    item: {
        memberId: "member1",
        memberName: "회원 명"
    }
}

IntelliJ의 경우 settings > File encoding 에서 UTF-8 로 변경 합니다.

{
    status: 200,
    message: "데이터를 조회하는데 성공하였습니다.",
    item: {
        memberId: "member1",
        memberName: "회원 명"
    }
}

📚참고

📕공통 응답 구조체 사용 시 장점

  1. 일관성 있는 응답 구조로 클라이언트의 개발 복잡성을 줄임.
  2. 새로운 필드 추가나 기존 필드를 제거 해야 될 시 공통 응답 구조체만 수정하면 되기 때문에 유지보수가 용이하고 확장성이 높음.
  3. 공통 구조체만 이해하면 다른 부분에서 응답을 읽고 이해하는 데 드는 시간이 줄어들어 코드 가독성을 높임.

📙Java Generic

Generic은 와 같이 사용되며 코드의 타입 안정성을 높이고, 코드 재사용성을 개선하기 위해 도입된 기능입니다. Generic을 사용하면 클래스, 인터페이스, 메서드를 정의할 때 구체적인 타입을 지정하지 않고, 나중에 실제 사용될 때 구체적인 타입을 지정할 수 있습니다. 이로 인해 컴파일 시 타입 체크를 수행할 수 있으며, 형변환(casting)을 줄여 코드의 안전성과 가독성을 높일 수 있습니다.

📖Wildcard Types

Wildcard 는 <?> 와 같이 사용되며 Generic의 유연성 높여주는 중요한 기능입니다. 와일드카드 타입을 사용하면 특정 타입에 한정되지 않고 다양한 타입을 인자로 받을 수 있어 메서드의 유연성이 높아집니다.

📗HTTP Status Code

HTTP Status codes 를 사용하여 API 상태를 전달 할 수 있습니다. 하지만 구체적인 상태 코드를 전달함으로써 내부 로직이나 작동 방식을 유추 할 단서를 제공하여 보안 이슈를 발생시킬 수 있습니다. 그래서 대부분의 프로젝트에서는 응답코드는 OK(200) 으로 전송 하고 응답 구조체 내부의 코드를 유추하기 어렵게 작성하여 전달합니다.

profile
Back-end developer

0개의 댓글