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

이 연재글은 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로 프로젝트 구성은 다음을 참고해주세요.
https://daddyprogrammer.org/post/1215/intellij-github-spring-gradle-project-import

연재글 이동<< SpringBoot2로 Rest api 만들기(4) – Swagger API 문서 자동화
SpringBoot2로 Rest api 만들기(6) – ControllerAdvice를 이용한 Exception처리 >>
공유

This Post Has 5 Comments

  1. Spring boot랑 JPA 처음 사용하느라 애먹고 있는데 잘 따라가면서 배우고 있습니다! 감사합니다

  2. 단계별로 따라하며 즐겁게 만들고 있습니다^^
    정성스런 포스팅에 고마운 마음 뿐입니다!!!
    주인없이 헤매는 모든 복들 다 받으시고 항상 건강하시길 바래요~!

    1. 칭찬 감사합니다~ 제글이 도움이 된것같아 기쁘네요~ 항상 즐거운 프로그래밍하시길 바랍니다. 건강하시고 행복하세요~

  3. 회원단건 조회시 msrl을 long타입으로 하셨는데 이유가있나요?
    userJpaRepo.findById(msrl) 에서 파라메터 msrl은 인티져인듯 해요.

    관련해서 UserJpaReop 인터페이스 에서 상속받을때 JpaRepository으로
    수정하면, 에러는 안나나 조회시 에러가 나네요.

    1. User Entity안에 msrl도 long으로 선언했고…
      UserJpaRepo도 extends JpaRepository<User, Long> {}
      으로 선언해서 pk(msrl)가 형식이 Long일텐데요? Integer로 선언을 한 부분이 없는데…어느부분이 Integer로 되어있는지 알수 있을까요?
      Long으로 변경한건 회원번호가 들어갈수 있는 Max값을 좀더 늘릴려고 한거에요. entity에 int로 선언하면 mysql에서 컬럼 타입이 INT로 생성되는데 최대값이 약 2147483647까지 들어갈 수 있습니다..뭐 충분한 값이긴 합니다만, long으로 하면 mysql에 컬럼이 BIGINT로 생성되기 때문에 거의 무한으로 넣을수 있죠..해당 컬럼의 타입은 상황에 따라 적절히 적용하시면 됩니다.

댓글 남기기

Close Menu