[Rest API] RESTful하게 URL 설계하기

YJ KIM·2022년 4월 26일
1

BackEnd

목록 보기
1/2

REST API에 대해 기존에도 알고는 있었지만 솔직히 제대로 알지는 못했다.
URL 규칙 정도로만 알고 있었다.

이번에 했던 Movie Review 프로젝트에서 API를 RESTful하게 변경했으면 좋겠다는 피드백을 받고 해당 포스팅을 작성하게 되었다.

최대한 REST에 대해 공부한 후, REST 지향적으로 설계했지만 안 맞는 부분이 있을 수도 있습니다. 지적 정말 감사하고 환영합니다! 😊

포스팅 구성은
1. REST API란? - 기본적인 REST의 개념
2. REST의 구성요소
3. REST API 설계 규칙
4. 프로젝트에서의 REST 적용 전/후

1. REST API란?

Representational State Transfer 🚌

REST API란 REST Architecture의 제약 조건을 준수하는 애플리케이션 프로그래밍 인터페이스를 뜻한다.

자원을 이름(자원의 표현)으로 구분해 해당 자원의 상태(정보)를 주고 받는 것을 의미한다.
= 자원(Resource)의 표현(Representation)에 의한 상태 전달

🐧 예를 들면?

어떤 URL를 사용할 때는 정보가 왔다갔다 한다고 생각하면 편할 것이다.

A의 블로그 중, 56번째 게시물에 접근할 때에는
https://a-blog/56 과 같은 URL로 접근할 수 있다.
이때의 자원은 a-blog/56인 URI로 A의 블로그의 56번째 게시물을 나타내는 고유 식별자이다.

=자원을 URI로 구분하여 https://a-blog/56의 URL로 해당 자원의 상태를 받을 수 있다.

2. REST의 구성요소

⭐ REST는 자원, 행위, 표현으로 구성된다.

자원(Resource)는 URI가 가리키는 리소스
행위(Verb)는 HTTP Method
표현(Representation)은 리소스의 특정 시점 상태를 반영하고 있는 정보

REST는 URI를 통해 자원을 표시하고 HTTP Method를 이용하여 해당 자원의 행위를 정해주며 그 결과를 받는 것을 말한다.

🌍 구성요소 살펴보기!

  1. 자원 - HTTP URI
    Ex) 회원 DB에서, 8번째 회원을 가리키고 싶은 경우 '/member/7'로 표현

  2. 행위 - HTTP Method
    REST API는 HTTP 프로토콜의 인프라를 그대로 사용하기 때문에, 행위에서도 HTTP Method를 그대로 사용한다.

    🚩Http Method 정리!
    GET		- 리소스에 대한 정보 검색
    POST	- 리소스 생성
    PUT		- 리소스 업데이트
    DELETE	- 리소스 또는 관련 구성 요소 삭제
  1. 표현 - HTTP Message Payload
    Client와 Server가 리소스를 주고받는 형태로 JSON, XML등의 형식으로 주고 받을 수 있다.

    🚩PAYLOAD란?
    전송되는 데이터를 의미한다. 

3. REST API 설계 규칙

  • URI는 동사❌ 명사⭕
    -> 행위를 사용하지 않는다. 행위는 HTTP Method로 표현된다.
  • URI는 소문자로만 구성
  • /로 계층 관계 표현
  • URI 마지막 문자로 /를 포함 ❌
  • _(언더바) 대신 -(하이픈)을 사용한다.
  • 파일 확장자는 URI에 포함하지 않는다.
  • HTTP 응답 코드를 사용하여 응답한다.

🚩HTTP 응답 코드란?

http 상태 코드 정리 사이트
위의 사이트로 들어가면 상태 코드에 대해 친절하게 설명이 되어있다!

4. 프로젝트의 REST 적용 전/후 비교

💻 적용 전)

   //리뷰 작성
    @ApiOperation(value="리뷰 작성",notes="입력받은 영화 객체가 등록되어 있지 않으면 등록하고, 리뷰를 저장한다.")
    @RequestMapping(value="/write",method= RequestMethod.POST)
    public ResponseEntity createReview(@RequestBody @Validated(ValidationGroups.createMovie.class) MovieWrapper movieWrapper) throws Exception {
        return new ResponseEntity(reviewService.createReview(movieWrapper), HttpStatus.OK);
    }

    //특정 영화 리뷰 조회 (review list를 body로 전달)
    @ApiOperation(value="특정 영화의 리뷰 조회",notes="영화의 id를 기준으로 해당 영화의 리뷰 list를 반환한다.")
    @RequestMapping(value="/movie/{mid}",method = RequestMethod.GET)
    public ResponseEntity getMovieReviews(@PathVariable Long mid){
        return new ResponseEntity(reviewService.getReviewsByMid(mid), HttpStatus.OK);
    }

    //특정 member의 리뷰 조회
    @ApiOperation(value="특정 사용자의 리뷰 조회",notes="사용자의 id를 기준으로 해당 사용자의 리뷰 list를 반환한다.")
    @RequestMapping(value="/member/{uid}",method =RequestMethod.GET)
    public ResponseEntity getMemberReviews(@PathVariable Long uid){
        return new ResponseEntity(reviewService.getReviewsByUid(uid),HttpStatus.OK);
    }

    //리뷰 삭제
    @ApiOperation(value="특정 리뷰 삭제",notes="리뷰의 id를 받아, 해당 리뷰를 삭제한다")
    @RequestMapping(value="/delete/{rid}",method=RequestMethod.GET)
    public ResponseEntity deleteReview(@PathVariable Long rid){
        return new ResponseEntity(reviewService.deleteReview(rid),HttpStatus.OK);
    }

    //리뷰에 좋아요 달기(자기 리뷰에 좋아요 달아도 됨)
    @ApiOperation(value="특정 리뷰 좋아요",notes="리뷰의 id를 받아, 현재 JWT 토큰으로 인증한 사용자가 좋아요 하도록 한다.(좋아요가 이미 되어있으면 취소하도록 한다.)")
    @RequestMapping(value="/like/{rid}",method=RequestMethod.GET)
    public ResponseEntity likeReview(@PathVariable Long rid) throws Exception {
        return new ResponseEntity(reviewService.likeReview(rid),HttpStatus.OK);
    }

    //리뷰 수정
    @ApiOperation(value="리뷰 수정", notes="리뷰의 id를 받아 해당 리뷰를 업데이트한다.")
    @RequestMapping(value="/update/{rid}",method=RequestMethod.POST)
    public ResponseEntity updateReview(@PathVariable Long rid, @RequestBody String review) throws Exception {
        return new ResponseEntity(reviewService.updateReview(rid,review),HttpStatus.OK);
    }

문제점으로는,

  1. 리뷰 삭제인데 Http Method를 GET으로(Http Method 문제)
    -> Http Method를 DELETE로 사용해야 맞음
    +추가적으로 UPDATE인데 POST를 쓴 것도 있다.
  2. URI에 '동사'가 들어간다.
    -> 행위가 URI에 들어간다. URI는 리소스를 가르켜야 하기 때문에 잘못되었다고 판단된다.

(사실 작성할 때 REST 생각 자체를 안 하고 작성하긴 했다...😅)

💻 REST 적용 후)

//리뷰 작성
    @ApiOperation(value="리뷰 작성",notes="입력받은 영화 객체가 등록되어 있지 않으면 등록하고, 리뷰를 저장한다.")
    @RequestMapping(value="/review",method= RequestMethod.POST)
    public ResponseEntity createReview(@RequestBody @Validated(ValidationGroups.createMovie.class) MovieWrapper movieWrapper) throws Exception {
        return new ResponseEntity(reviewService.createReview(movieWrapper), HttpStatus.OK);
    }

    //특정 영화 리뷰 조회 (review list를 body로 전달)
    @ApiOperation(value="특정 영화의 리뷰 조회",notes="영화의 id를 기준으로 해당 영화의 리뷰 list를 반환한다.")
    @RequestMapping(value="/review/movie/{mid}",method = RequestMethod.GET)
    public ResponseEntity getMovieReviews(@PathVariable Long mid){
        return new ResponseEntity(reviewService.getReviewsByMid(mid), HttpStatus.OK);
    }

    //특정 member의 리뷰 조회
    @ApiOperation(value="특정 사용자의 리뷰 조회",notes="사용자의 id를 기준으로 해당 사용자의 리뷰 list를 반환한다.")
    @RequestMapping(value="/review/member/{uid}",method =RequestMethod.GET)
    public ResponseEntity getMemberReviews(@PathVariable Long uid){
        return new ResponseEntity(reviewService.getReviewsByUid(uid),HttpStatus.OK);
    }

    //특정 rid의 리뷰 조회
    @ApiOperation(value="특정 리뷰 조회",notes="리뷰 id를 기준으로 해당 리뷰를 반환한다.")
    @RequestMapping(value="/review/{rid}",method=RequestMethod.GET)
    public ResponseEntity getReview(@PathVariable Long rid){
        return new ResponseEntity(reviewService.getReview(rid),HttpStatus.OK);
    }

    //리뷰 삭제
    @ApiOperation(value="특정 리뷰 삭제",notes="리뷰의 id를 받아, 해당 리뷰를 삭제한다")
    @RequestMapping(value="/review/{rid}",method=RequestMethod.DELETE)
    public ResponseEntity deleteReview(@PathVariable Long rid){
        return new ResponseEntity(reviewService.deleteReview(rid),HttpStatus.OK);
    }

    //리뷰에 좋아요 달기(자기 리뷰에 좋아요 달아도 됨)
    @ApiOperation(value="특정 리뷰 좋아요",notes="리뷰의 id를 받아, 현재 JWT 토큰으로 인증한 사용자가 좋아요 하도록 한다.(좋아요가 이미 되어있으면 취소하도록 한다.)")
    @RequestMapping(value="/review/likes/{rid}",method=RequestMethod.PUT)
    public ResponseEntity likeReview(@PathVariable Long rid) throws Exception {
        return new ResponseEntity(reviewService.likeReview(rid),HttpStatus.OK);
    }

    //리뷰 수정
    @ApiOperation(value="리뷰 수정", notes="리뷰의 id를 받아 해당 리뷰를 업데이트한다.")
    @RequestMapping(value="/review/contents/{rid}",method=RequestMethod.PUT)
    public ResponseEntity updateReview(@PathVariable Long rid, @RequestBody String review) throws Exception {
        return new ResponseEntity(reviewService.updateReview(rid,review),HttpStatus.OK);
    }

하다보니 특정 rid(리뷰 id)로 리뷰를 검색하는 것이 없어서 추가했다! front에서 특정 리뷰 제목을 누르거나 리뷰 미리보기 사진을 누를 때 필요할 것 같아서 추가했다.
(작년에 html과 자바스크립트로 k-pop 평론 사이트를 만들었을 때 이렇게 했었던 것 같아서)

변경 전 코드에서는, ReviewController 전체에 RequestMapping annotation을 사용하여

@RequestMapping("/reviews")

으로 모든 컨트롤러 메소드에 대하여 reviews로 계층 구조를 갖도록 했다. 포스팅 하려고 일단은 풀어서 작성했다.

  1. Http Method를 준수하여 작성하였다.
  2. URI에 동사를 안 쓰고 리소스를 가리키는 명사로 구성하였다.
    -> 행위를 나타내는 동사는 Http Method가 담당한다.

결과)

+추가 사항

ResponseEntity으로 Http Status를 반환하였다.
관련해서 추가 포스팅을 작성할 예정이니, 작성 후 첨부할 것이다.

🚑 참고

RESTful API 설계 가이드 - 이상학의 개발블로그
REST API 제대로 알고 사용하기

profile
모르면 쓰지 말고 쓸 거면 알고 쓰자

0개의 댓글