이 연재글은 SpringBoot2로 Rest api 만들기의 5번째 글입니다.

이번 시간엔 api 서버 개발을 본격적으로 진행해 보기 위해 현재 api 인터페이스 및 결과 데이터의 구조를 살펴보고 확장 가능한 형태로 설계해 보겠습니다. api는 제공 대상이 클라이언트 app이나 web 개발자입니다. 한번 배포되고 공유한 api는 구조를 쉽게 바꿀 수 없기 때문에, 처음부터 효율적이고 확장 가능한 형태로 모델을 설계하고 시작하는 것이 좋습니다. 그래서 다음과 같이 HttpMethod를 사용하고 Restful 한 api를 만들기 위해 몇 가지 규칙을 적용하도록 하겠습니다.

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

Http 프로토콜은 여러 가지 사용목적에 따라 HttpMethod를 제공하고 있는데요. 여기서는 그중 아래의 4가지 HttpMethod를 상황에 맞게 api 구현에 사용하도록 하겠습니다.
GET – 서버에 주어진 리소스의 정보를 요청한다.(읽기)
POST – 서버에 리소스를 제출한다(쓰기)
PUT – 서버에 리소스를 제출한다. POST와 달리 리소스 갱신 시 사용한다.(수정)
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": "yumi@naver.com",
    "name": "정유미"
}
// 표준화한 USER 정보
{
  "data": {
    "msrl": 1,
    "uid": "yumi@naver.com",
    "name": "정유미"
  },
  "success": true
  "code": 0,
  "message": "성공하였습니다."
}

구현1. 결과 모델의 정의

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

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

api의 처리 상태 및 메시지를 내려주는 데이터로 구성됩니다. success는 api의 성공 실패 여부를 나타내고 code, msg는 해당 상황에서의 응답 코드와 응답 메시지를 나타냅니다.

@Getter
@Setter
public class CommonResult {

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

    @ApiModelProperty(value = "응답 코드 번호 : >= 0 정상, < 0 비정상")
    private int code;

    @ApiModelProperty(value = "응답 메시지")
    private String msg;
}

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

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

@Getter
@Setter
public class SingleResult<T> extends CommonResult {
    private T data;
}

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

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

@Getter
@Setter
public class ListResult<T> extends CommonResult {
    private List<T> list;
}

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

결과 모델에 데이터를 넣어주는 역할을 할 Service를 정의합니다. com.rest.api 하위에 service package를 생성하고 아래의 Service Class를 생성합니다.

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

    // enum으로 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<>();
        result.setList(list);
        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을 사용하였습니다. 결과 데이터의 형태에 따라 단일건 처리는 getBasicResult()를 다중 건 처리는 getListResult()를, api 처리 성공 결과만 필요한 경우 getSuccessResult()를 사용합니다.

@Api(tags = {"1. User"})
@RequiredArgsConstructor
@RestController
@RequestMapping(value = "/v1")
public class UserController {

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

    @ApiOperation(value = "회원 리스트 조회", notes = "모든 회원을 조회한다")
    @GetMapping(value = "/users")
    public ListResult<User> findAllUser() {
        // 결과데이터가 여러건인경우 getListResult를 이용해서 결과를 출력한다.
        return responseService.getListResult(userJpaRepo.findAll());
    }

    @ApiOperation(value = "회원 단건 조회", notes = "userId로 회원을 조회한다")
    @GetMapping(value = "/user/{msrl}")
    public SingleResult<User> findUserById(@ApiParam(value = "회원ID", required = true) @PathVariable long msrl) {
        // 결과데이터가 단일건인경우 getBasicResult를 이용해서 결과를 출력한다.
        return responseService.getSingleResult(userJpaRepo.findById(msrl).orElse(null));
    }

    @ApiOperation(value = "회원 입력", notes = "회원을 입력한다")
    @PostMapping(value = "/user")
    public SingleResult<User> save(@ApiParam(value = "회원아이디", required = true) @RequestParam String uid,
                                   @ApiParam(value = "회원이름", required = true) @RequestParam String name) {
        User user = User.builder()
                .uid(uid)
                .name(name)
                .build();
        return responseService.getSingleResult(userJpaRepo.save(user));
    }

    @ApiOperation(value = "회원 수정", notes = "회원정보를 수정한다")
    @PutMapping(value = "/user")
    public SingleResult<User> modify(
            @ApiParam(value = "회원번호", required = true) @RequestParam long msrl,
            @ApiParam(value = "회원아이디", required = true) @RequestParam String uid,
            @ApiParam(value = "회원이름", required = true) @RequestParam String name) {
        User user = User.builder()
                .msrl(msrl)
                .uid(uid)
                .name(name)
                .build();
        return responseService.getSingleResult(userJpaRepo.save(user));
    }

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

Swagger Test

HttpMethod 및 정형화된 주소체계가 적용되었습니다.
POST로 회원정보 입력이 성공하였고 표준화된 결과 모델로 데이터가 출력되었습니다.
PUT으로 회원정보 수정이 성공하였고 표준화된 결과 모델로 수정된 데이터가 출력되었습니다.
DELETE로 회원정보 삭제가 성공하였고 표준화된 api 결과 모델만 출력되었습니다.
GET으로 회원정보를 조회하였고 표준화된 리스트 결과 모델로 데이터가 출력되었습니다.
GET으로 단일 회원정보를 조회하였고 표준화된 단일 결과 모델로 데이터가 출력되었습니다.

이상으로 본격적으로 api 서비스를 구축하기 위한 인터페이스 및 결과 구조 설계 방법을 살펴보았습니다. 이번 내용에는 api 성공에 대한 내용만 살펴보았는데요. 다음 시간에는 실패 시의 ExceptionHandling과 결과 Message 처리에 대한 내용을 살펴보도록 하겠습니다.

최신 소스는 GitHub 사이트를 참고해 주세요.
https://github.com/codej99/SpringRestApi/tree/feature/api-structure

GitHub Repository를 import하여 Intellij 프로젝트를 구성하는 방법은 다음 포스팅을 참고해주세요.

Docker로 개발 환경을 빠르게 구축하는 것도 가능합니다. 다음 블로그 내용을 읽어보세요!

스프링 api 서버를 이용하여 웹사이트를 만들어보고 싶으시면 아래 포스팅을 참고해 주세요.

연재글 이동[이전글] SpringBoot2로 Rest api 만들기(4) – Swagger API 문서 자동화
[다음글] SpringBoot2로 Rest api 만들기(6) – ControllerAdvice를 이용한 Exception처리