Restful API 설계, REST 하게 만들기

JungWooLee·2022년 9월 1일
0

SpringBoot ToyProject

목록 보기
12/14

이어서하기

  • 기존의 기능 중심의 가독성을 중시한 url 스타일을 REST 설계 방식에 맞추어 정규화 합니다
  • REST 설계 원칙에 대하여

REST 설계 원칙

1. URI는 정보의 리소스를 표현해야 한다

  • 기존 코드

  • 행위에 대한 표현이 아닌 리소스 표현하는데 중점을 두어야 한다
    즉, GET /restaurant/addNearest → POST /restaurant

  • 리소스명은 동사보다는 명사를 사용한다
    add와 같은 동사 사용 X

2. 리소스에 대한 행위는 HTTP Method 로 표현한다

  • GET, POST, PUT, DELETE 등
// REST 적용 예시 1
GET /members/delete/1 (X)
DELETE /members/1 (O)

// REST 적용 예시 2
GET /members/show/1 (X)
GET /members/1 (O)

// REST 적용 예시 3
GET /members/insert/3 (X)
POST /members/3 (O)

HTTP Method 의 경우, Service 에서 진행되는 비즈니스 로직에 맞추어 일관성을 유지해주는 것이 좋다
ex) service.updateSomething() → Controller (@PustMaaping(/something)

3. 슬래시 구분자(/)는 계층 관계를 나타내는데 사용한다

  • ex) restaurant 에서 상세 레스토랑 정보로 가는 url → restaurant/{restaurantID}

4. URI는 마지막 문자로 슬래시(/)를 포함하지 않는다

  • /user/ : X
    /user : O

5. 하이폰(-)은 URI 가독성을 높이는데 사용할 수 있다

  • URI를 쉽게 읽고 해석하기 위해, 불가피하게 긴 URI경로를 사용하게 된다면 하이픈을 사용해 가독성을 높일 수 있다

6. 언더바("_") 는 URI에 사용하지 않는다

  • 보기 어렵거나 밑줄 때문에 문자가 가려지는 가독성 문제가 발생, 대신 하이픈을 사용
  • user/saved_restaurant : X
    user/saved-restaurant : O

7. URI 경로에는 소문자를 사용한다

  • 대소문자에 따라 다른 리소스로 인식하기 때문
  • RFC3986(URI 문법형식)은 URI 스키마와 호스트를 제외하고는 대소문자를 구별하도록 규정

8. 파일 확장자는 URI에 포함시키지 않는다

  • REST API 에서는 메시지 바디 내용의 포맷을 나타내기 위한 파일 확장자를 URI안에 포함시키지 않는다. 대신 Accept header를 사용한다
// REST 적용 예시
http://restapi.example.com/members/soccer/345/pthoto.jpg (X)
GET /members/soccer/345/photo HTTP/1.1 (O)
	Host: restapi.example.com
	Accept: image/jpg

HTTP 메소드와 REST API 설계

1. 리소스 간의 관계를 표현하는 방법

  • 일반적인 소유 관계를 표현할 때
    GET /users/{userid}/devices
  • 관계명이 애매하거나 구체적 표현이 필요할 때 예시
    GET /users/{userid}/likes/devices

2. URI 설계 개념

  1. 문서(Document)
  • 하나의 문서 혹은 객체라고 볼 수 있다.
  • 리소스라고 표현할 수 있으며 URI에 표현된다.
  • /members/100, /files/star.jpg 등이 예시이다.
  1. 컬렉션(Collenction)
  • 문서, 객체들의 집합이라고 볼 수 있다.
  • 복수를 의미한다.
  • 리소스라고 표현할 수 있으며 URI에 표현된다.
  • /members 등이 예시이다.

3.스토어(Store)

  • 클라이언트가 관리하는 자원 저장소를 의미한다.
  • 클라이언트가 리소스의 URI를 알고 관리한다.
  • /files 등이 예시이다.
  1. 컨트롤러(Controller), 컨트롤 URI
  • 문서, 컬렉션, 스토어로 해결하기 어려운 추가 프로세스를 실행한다.
  • 동사를 직접 사용한다.
  • /members/{id}/delete 등이 예시이다.

3. HTTP 응답 상태 코드

  • 1xx(Informational): 요청이 수신되어 처리 중이다. 거의 사용하지 않는다.
  • 2xx(Successful): 요청이 정상 처리되었다.
  • 3xx(Redirection): 요청을 완료하려면 추가 행동이 필요하다.
  • 4xx(Client Error): 클라이언트 오류, 잘못된 문법 등으로 서버가 요청을 수행할 수 없다.
  • 5xx(Server Error): 서버 오류, 서버가 정상 요청을 처리하지 못한다.

REFERENCE


프로젝트 내 REST 하지 않는 부분 수정

1. API Controller

	// 위경도를 통해 주소 반환
    @PostMapping("/getFullAddress")
    public String getFullAddress(AddressDTO request){
        String result = userService.getAddress(request.getX(), request.getY());
        log.info(request.toString());
        return result;
    }

    // 주소를 통해 위경도 반환
    @PostMapping("/getLonLat")
    public AddressDTO getLonLat(@RequestParam(value ="fullAddress") String fullAddress){
        log.info("full address : "+fullAddress);
        return userService.getXY(fullAddress);
    }
  • 위배 사항 : 대문자가 포함, 동사 포함, 일관성 X

개선 후

  • api controller 에서 현재 사용중인 것은 kakao map 에서 주로 사용되는 api 들을 control 합니다
  • 이들은 geocoding 과 관련있기에 일관성을 유지 할 수 있도록 합니다
	@GetMapping("/search/reverse-geo")
    public String getFullAddress(AddressDTO request){
        String result = userService.getAddress(request.getX(), request.getY());
        log.info(request.toString());
        return result;
    }

    // 주소를 통해 위경도 반환
    @GetMapping("/search/geo")
    public AddressDTO getLonLat(@RequestParam(value ="fullAddress") String fullAddress){
        log.info("full address : "+fullAddress);
        return userService.getXY(fullAddress);
    }
  • 기존 POST Method 를 GET 으로 바꿔줍니다
  • 역지오 코딩의 경우 : reverse-geo, 지오코딩의 경우 : geo로 GET 할 수 있도록 합니다
  • 카카오 API 에서 조회 하는 것이기 때문에 /search 로 일관성을 유지합니다

2. Restaurant Controller

	@SneakyThrows
    @GetMapping("/addNearest")
    public String addNearestRestaurant(@LoginUser SessionUser user){
        // 신규 주변 음식점 정보가 있다면 insert, 이후 restaurant view 로 이동
        
        GetRestaurantRequest request = GetRestaurantRequest.builder()
                                        .x(String.valueOf(user.getX()))
                                        .y(String.valueOf(user.getY()))
                                        .build();
        restaurantService.getRestaurantData(request);
        return "redirect:/restaurant/main"; // 메인 페이지로 이동
    }

    @GetMapping("/main")
    public String main(@LoginUser SessionUser user
                        ,Model model){
        // 현재 위치 기준 가까운 순으로 정렬
        AddressDTO request = AddressDTO.builder()
                .x(user.getX())
                .y(user.getY())
                .build();
        // 가까운 음식점 정보를 가져옴
        List<RestaurantDTO> datas = restaurantService.getRestaurantDTO(request);
        model.addAttribute("restaurants", datas);

        return "restaurant";
    }@SneakyThrows
    @GetMapping("/addNearest")
    public String addNearestRestaurant(@LoginUser SessionUser user){
        // 신규 주변 음식점 정보가 있다면 insert, 이후 restaurant view 로 이동
        
        GetRestaurantRequest request = GetRestaurantRequest.builder()
                                        .x(String.valueOf(user.getX()))
                                        .y(String.valueOf(user.getY()))
                                        .build();
        restaurantService.getRestaurantData(request);
        return "redirect:/restaurant/main"; // 메인 페이지로 이동
    }

    @GetMapping("/main")
    public String main(@LoginUser SessionUser user
                        ,Model model){
        // 현재 위치 기준 가까운 순으로 정렬
        AddressDTO request = AddressDTO.builder()
                .x(user.getX())
                .y(user.getY())
                .build();
        // 가까운 음식점 정보를 가져옴
        List<RestaurantDTO> datas = restaurantService.getRestaurantDTO(request);
        model.addAttribute("restaurants", datas);

        return "restaurant";
    }
  • 위배 사항 : 동사 포함, 올바르지 않은 Http Method 사용, 대문자 포함

개선 후

	@SneakyThrows
    @PostMapping("/near")
    public String addNearestRestaurant(@LoginUser SessionUser user){
        // 신규 주변 음식점 정보가 있다면 insert, 이후 restaurant view 로 이동
        GetRestaurantRequest request = GetRestaurantRequest.builder()
                                        .x(String.valueOf(user.getX()))
                                        .y(String.valueOf(user.getY()))
                                        .build();
        restaurantService.getRestaurantData(request);
        return "redirect:/restaurant/main"; // 메인 페이지로 이동
    }
  • 행위 Create 에 맞추어 Http Method 를 POST 로 변경
  • 동사를 제거하고 /near 로 동사 사용 X

3. User Controller

	@PostMapping("/postXY")
    public String postXY(AddressDTO dto
            , @LoginUser SessionUser user){

        user.setX(dto.getX());
        user.setY(dto.getY());
        User newUser = userService.saveOrUpdateXY(user);
        httpSession.setAttribute("user", new SessionUser(newUser));

        return "redirect:/restaurant/addNearest";
    }
  • 위배 사항 : 동사 포함, 대문자 포함, 올바르지 않은 Http Method 사용

개선 후

	@PutMapping("/lon-lat")
    public String postXY(AddressDTO dto
            , @LoginUser SessionUser user){

        user.setX(dto.getX());
        user.setY(dto.getY());
        User newUser = userService.saveOrUpdateXY(user);
        httpSession.setAttribute("user", new SessionUser(newUser));

        return "redirect:/restaurant/addNearest";
    }
  • DB 에서 유저 x,y 를 Update 하기 때문에 Put 으로 Method 변경
  • 하이픈을 사용하여 lon, lat 에 대한 가독성 증가
  • 동사 사용 X, 명사 lon, lat 으로 URL 표현

테스트

REST 원칙에 맞게 바꾼 url 로 호출 시 잘 작동하는지 확인합니다

1. /user/lon-lat

<form th:action="@{/user/lon-lat}"  method="POST">
    <input type="hidden" name="_method" value="PUT">
    위도
    <input type="text"  id="x" name="x" readonly/>
    <br>
    경도
    <input type="text"  id="y" name="y" readonly/>
    <button type="submit">등록</button>
</form>
  • main view 에서 위도, 경도를 받아와 유저의 위도 경도 값을 update 해주는 곳이다
  • HTML form 태그는 GET, POST만 지원한다. 그렇다고 POST Method 로 처리하는가 ? XX : 이는 명백히 REST 원칙에 어긋나는 행동

    해결법

    • <input type="hidden" name="_method" value="PUT"> 다음과 같이 html 을 form 태그안에 추가한다
    • 스프링 부트 2.2버전 이상은 자동으로 구성되지 않는다, 그래서 프로퍼티에 다음 설정을 추가합니다. spring.mvc.hiddenmethod.filter.enabled=true
    • 추가적으로 왜 html 에서 지원하지 않는지 form 에서 put, delete

2. API Controller

  • GET reverse-geo
  • GET geo

3. Restaurant Controller

  • redirect 시에 PostMapping 하는 법을 서칭해보았으나 이거다 하는 방법이 없어 GET 으로 메서드를 바꾸었습니다
  • 이게 관점적으로 보았을 때 유저 세션에 있는 정보를 통해 GET 으로 pcmap-api.place.naver 이후 저장하는 것이기 때문에 GET 으로 하여도 무관하겠다 판단하였습니다

0개의 댓글