본문 바로가기
Spring Boot

SpringBoot를 활용한 Rest api 만들기(5) - API 인터페이스 및 결과 데이터 구조 설계

by ZIAHO 2023. 3. 4.

이번 포스팅에서는 API 서버 개발을 본격적으로 진행하기 위해 현재 API 인터페이스 및 결과 데이터의 구조를 살펴보고 확장 가능한 형태로 설계 해 보겠습니다.

API는 제공 대상이 클라이언트 App이나 web 개발자입니다. 따라서 한번 배포되고 공유한 API는 구조를 쉽게 바꿀 수 없기 때문에, 처음부터 효율적이고 확장 가능한 형태로 모델을 설계하고 시작하는것이 좋습니다.

 

그래서 다음과 같이 HttpMethd를 사용하고 Restful한 API를 만들기 위한 몇가지 규칙을 적용하도록 하겠습니다.


1. 리소스의 사용목적에 따라 Http Method를 구분해서 사용한다.

Http 프로토콜은 여러가지 사용목적에 따라 HttpMethod를 제공하고 있습니다.

그 중 아래의 4가지 HttpMethod를 상황에 맞게 API구현에 사용하도록 하겠습니다.

GET - 서버에 주어진 리소스의 정보를 요청한다.(읽기)

POST - 서버에 리소스를 제출한다.(쓰기, 입력)

PUT - 서버에 리소스를 제출한다. POST와 달리 리소스 갱신 시 사용한다.(수정, UPDATE)

DELETE - 서버에 주어진 리소스를 삭제 요청한다.(삭제)

 

2. 리소스에 Mapping된 주소 체계를 정형화 한다.

주소 체계는 아래와 같이 정형화된 구조로 구성하고 HttpMethod를 통해 리소스의 사용 목적을 판단하는 것이 핵심입니다.

GET/v1/user/{userId} - 회원 userId에 해당하는 정보를 조회한다.

GET/v1/users - 회원 리스트를 조회한다.

POST/v1/user - 신규 회원정보를 입력한다.

PUT/v1/user - 기존 회원의 정보를 수정한다.

DELETE/v1/user/{userId} - 파라미터로 입력한 userId와 일치하는 기존 회원의 정보를 삭제한다.

 

3. 결과 데이터의 구조를 표준화하여 정의한다.

결과 데이터는 아래의 샘플처럼 '결과 데이터 + api요청 결과 데이터'로 구성합니다.

// 기존 User 정보
{
    "msrl": 1,
    "uid": "test@test.com",
    "name": "테스트"
}

// 표준화한 USER 정보
{
    "data": {
        "msrl": 1,
        "uid": "test@test.com",
        "name": "테스트"
    },
    "success": true,
    "code": 0,
    "message": "성공하였습니다."
}

구현1. 결과 모델의 정의

org.ziaho.ziahorestapi 하위에 model.response package를 생성합니다.

생성한 package 하위에 결과를 담을 3개의 모델을 생성합니다.

 

api의 실행 결과를 담는 공통 모델

api의 처리 상태 및 메시지를 내려주는 데이터로 구성됩니다.

success는 api의 성공/실패 여부를 나타내고 code, msg는 해당 상황에서의 응답코드와 응답 메시지를 나타냅니다.

package org.ziaho.ziahorestapi.model.response;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class CommonResult {

    @Schema(name = "응답 성공여부 : true/false")
    private boolean success;

    @Schema(name = "응답 코드 번호 : >= 0 정상 / < 0 비정상")
    private int code;

    @Schema(name = "응답 메시지")
    private String msg;

}

결과가 단일건인 api를 담는 모델

Generic Interface에 <T>를 지정하여 어떤 형태의 값도 넣을 수 있도록 구현하였습니다. 또한 CommonResult를 상속받으므로 api요청 결과도 같이 출력됩니다.

package org.ziaho.ziahorestapi.model.response;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class SingleResult<T> extends CommonResult {

    private T data;

}

결과가 여러건인 api를 담는 모델

api 결과가 다중 건인 경우에 대한 데이터 모델입니다. 결과 필드를 List형태로 선언하고 Generic Interface에 <T>를 지정하여 어떤 형태의 List 값도 넣을 수 있도록 구현하였습니다.

또한 CommonResult를 상속받으므로 api요청 결과도 같이 출력됩니다.

package org.ziaho.ziahorestapi.model.response;

import java.util.List;

public class ListResult<T> extends CommonResult {

    private List<T> list;

}

 

구현2. 결과 모델을 처리할 Service의 정의

결과 모델에 데이터를 넣어주는 역할을 할 Service를 정의합니다.

org.ziaho.ziahorestapi 하위에 service package를 생성하고 아래의 ResponseService라는 이름의 Service Class를 생성합니다.

package org.ziaho.ziahorestapi.service;

import org.springframework.stereotype.Service;
import org.ziaho.ziahorestapi.model.response.CommonResult;
import org.ziaho.ziahorestapi.model.response.ListResult;
import org.ziaho.ziahorestapi.model.response.SingleResult;

import java.util.List;

@Service // 해당 Class가 Service임을 명시합니다.
public class ResponseService {

    // enumddmfh api 요청 결과에 대한 code, message를 정의합니다.
    public enum CommonResponse {
        SUCCESS(0, "성공하였습니다."),
        FAIL(-1, "실패하였습니다.");

        int code;
        String msg;

        CommonResponse(int code, String msg) {
            this.code = code;
            this.msg = msg;
        }

        public int getCode() {
            return code;
        }

        public String getMsg() {
            return msg;
        }
    }

    // 단일건 결과를 처리하는 메소드
    public <T> SingleResult<T> getSingleResult(T data) {
        SingleResult<T> result = new SingleResult<>();
        result.setData(data);
        setSuccessResult(result);

        return result;
    }

    // 다중건 결과를 처리하는 메소드
    public <T> ListResult<T> getListResult(List<T> list) {
        ListResult<T> result = new ListResult<>();
        setSuccessResult(result);

        return result;
    }

    // 성공 결과만 처리하는 메소드
    public CommonResult getSuccessResult() {
        CommonResult result = new CommonResult();
        setSuccessResult(result);

        return result;
    }

    // 실패 결과만 처리하는 메소드
    public CommonResult getFailResult() {
        CommonResult result = new CommonResult();
        result.setSuccess(false);
        result.setCode(CommonResponse.FAIL.getCode());
        result.setMsg(CommonResponse.FAIL.getMsg());

        return result;
    }

    // 결과 모델에 api 요청 성공 데이터를 세팅해주는 메소드
    private void setSuccessResult(CommonResult result) {
        result.setSuccess(true);
        result.setCode(CommonResponse.SUCCESS.getCode());
        result.setMsg(CommonResponse.SUCCESS.getMsg());
    }

}

구현3. HttpMethod와 정형화된 주소체계로 Controller 수정

리소스의 사용목적에 따라 GetMapping, PostMapping, PutMapping, DeleteMapping을 사용하였습니다. 결과 데이터의 형태에 따라 단일건 처리는 getSingleResult()를 다중 건 처리는 getListResult(),를 api 처리 성공 결과만 필요할 경우 getSuccessResult()를 사용합니다.

package org.ziaho.ziahorestapi.controller.v1;

//import io.swagger.annotations.ApiParam;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.ziaho.ziahorestapi.entity.User;
import org.ziaho.ziahorestapi.model.response.CommonResult;
import org.ziaho.ziahorestapi.model.response.ListResult;
import org.ziaho.ziahorestapi.model.response.SingleResult;
import org.ziaho.ziahorestapi.repo.UserJpaRepo;
import org.ziaho.ziahorestapi.service.ResponseService;

import java.util.List;

@Tag(name = "1. User", description = "UserController") // UserController를 대표하는 최상단 타이틀 영역에 표시될 값을 세팅합니다.
@RequiredArgsConstructor // class상단에 선언하면 class내부에 final로 선언된 객체에 대해서 Constructor Injection(의존성 주입)을 수행합니다.
                        // 해당 어노테이션을 사용하지 않고 선언된 객체에 @Autowired를 사용해도 됩니다.
@RestController // 결과 데이터를 JSON형식으로 내보냅니다.
@RequestMapping(value = "/v1") // api resource를 버전별로 관리하기 위해 /v1 을 모든 리소스 주소에 적용되도록 합니다.
public class UserController {

    private final UserJpaRepo userJpaRepo;
    private final ResponseService responseService; // 결과를 처리할 Service

    @Operation(summary = "회원 조회", description = "모든 회원을 조회한다.") // 각각의 리소스에 제목과 설명을 표시하기 위해 세팅합니다.
    @GetMapping(value = "/user")
    public ListResult<User> findAllUser() { // user테이블에 있는 데이터를 모두 읽어옵니다. 데이터가 한개 이상 일 수 있으므로 리턴타입은 List<User>로 선언합니다.
        // Jpa를 사용하면 기본적으로 CRUD에 대해서는 별다른 설정없이 쿼리를 질의할 수 있도록 메소드를 지원합니다.
        // select * from user와 같음

        // 결과 데이터가 여러건인경우 getListResult를 이용해서 결과를 출력한다.
        return responseService.getListResult(userJpaRepo.findAll());
    }

    @Operation(summary = "회원 입력", description = "회원을 입력한다.") // 각각의 리소스에 제목과 설명을 표시하기 위해 세팅합니다.
    @PostMapping(value = "/user")
    public SingleResult<User> save(@Parameter(name = "회원아이디", required = true) @RequestBody String uid,
                                  @Parameter(name = "이름", required = true) @RequestBody String name) { // 파라미터에 대한 설명을 보여주기 위해 세팅합니다.
        // user 테이블에 데이터 1건을 입력합니다.
        // Jpa를 사용하면 기본적으로 CRUD에 대해서는 별다른 설정없이 쿼리를 질의할 수 있도록 메소드를 지원합니다.
        // insert into user(msrl, name, uid) values(null, ?, ?)와 같음
        User user = User.builder()
                .uid(uid)
                .name(name)
                .build();
        // 결과 데이터가 단일건일경우 getSingleResult를 이용해서 결과를 출력한다.
        return responseService.getSingleResult(userJpaRepo.save(user));
    }

    @Operation(summary = "회원 수정", description = "회원정보를 수정한다.")
    @PutMapping(value = "/user")
    public SingleResult<User> modify(
            @Parameter(name = "회원번호", required = true) @RequestBody long msrl,
            @Parameter(name = "회원 아이디", required = true) @RequestBody String uid,
            @Parameter(name = "회원이름", required = true) @RequestBody String name) {
        User user = User.builder()
                .msrl(msrl)
                .uid(uid)
                .name(name)
                .build();

        return responseService.getSingleResult(userJpaRepo.save(user));
    }

    @Operation(summary = "회원 삭제", description = "userId로 회원정보를 삭제한다")
    @DeleteMapping(value = "/user/{msrl}")
    public CommonResult delete(
            @Parameter(name = "회원번호", required = true) @PathVariable long msrl) {
        userJpaRepo.deleteById(msrl);
        // 성공 결과 정보만 필요한 경우 getSuccessResult()를 이용하여 결과를 출력한다.
        return responseService.getSuccessResult();
    }

}

결과 확인

HttpMethod 및 정형화된 주소체계가 적용 되었습니다.


위와 같이 test2@test.com / 테스트 라는 회원정보를 입력하려고 했으나,

400에러가 발생하였다.

 

콘솔창에서도

2023-03-04T22:16:24.929+09:00  WARN 9748 --- [nio-8080-exec1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'uid' for method parameter type String is not present]

 

다음과 같은 에러가 발생하였는데

uid라는 String타입의 파라미터가 존재하지 않는다고 나온다....

 

Request URL을 보니 쿼리스트링으로 전송이 될때 자동으로 인코딩이 되어서 '%ED%9A%8C%EC%9B%90%EC%95%84%EC%9' 이런식으로 파라미터가 넘어가는것이 문제인것 같아서

UserController에서 validation 어노테이션을 사용하여 파라미터를 검증하거나 해보았지만 정상적으로 작동하지 않았다....

 

이거 아무리 찾아봐도 원인을 모르겠는데... 혹시 해결 방법 아는 분 계신다면 댓글로 남겨주시면 감사드리겠습니다.


위와같은 에러를 해결하지 못해서 결과 테스트는 Postman으로 진행하려고 한다.

이후 포스팅에서도 Swagger에 대한 오류를 해결하지 못하면 지속적으로 Postman을 사용할 예정입니다.

 

POST로 회원정보 입력이 성공하였고 표준화 된 결과 모델로 데이터가 출력되었습니다.
PUT으로 회원정보 수정이 성공하였고, 표준화된 결과 모델로 수정된 데이터가 출력되었습니다.
GET으로 전체 회원정보를 조회하였고, 수정된 회원과 새로 입력된 회원정보 모두 정상적으로 출력되었습니다.
GET으로 단건 조회를 실행하였고, msrl이 1인 회원의 정보가 정상적으로 출력되었습니다.
DELETE로 회원정보가 삭제 되었고, 표준화된 api 결과 모델만 출력되었습니다.


이상으로 api 서비스를 구축하기 위한 인터페이스 및 결과 구조 설계 방법을 살펴보았습니다.

이번 포스팅에서는 성공에 대한 내용만 다루어 보았는데, 다음 포스팅에서는 실패 시의 ExceptionHandling과 결과 Message 처리에 대한 내용을 살펴보도록 하겠습니다.


출처

SpringBoot2로 Rest api 만들기(5) – API 인터페이스 및 결과 데이터 구조 설계 (daddyprogrammer.org)

 

SpringBoot2로 Rest api 만들기(5) – API 인터페이스 및 결과 데이터 구조 설계

이번 시간엔 api 서버 개발을 본격적으로 진행해 보기 위해 현재 api 인터페이스 및 결과 데이터의 구조를 살펴보고 확장 가능한 형태로 설계해 보겠습니다. api는 제공 대상이 클라이언트 app이나

www.daddyprogrammer.org

codej99/SpringRestApi at feature/api-structure (github.com)

 

GitHub - codej99/SpringRestApi: SpringBoot2, SpringSecurity, JWT, Stateless Restful API

SpringBoot2, SpringSecurity, JWT, Stateless Restful API - GitHub - codej99/SpringRestApi: SpringBoot2, SpringSecurity, JWT, Stateless Restful API

github.com