brunch

You can make anything
by writing

C.S.Lewis

by 에디의 기술블로그 May 13. 2018

RESTful API 설계 및 Tips

-Rest API 설계 및 스프링 환경에서의 Tips 정리

최근에 회사 프로젝트로 RESTful API를 구축하게 되었다. 정말 오랜만에 진행하는 신규 API 개발이다. 그래서 이번에 프로젝트를 진행하면서 RESTful API 설계 방법에 대해서 개인적으로 가이드를 정리하였고 그 내용을 공유한다. 그리고 추가로 Spring 환경에서 RESTful API를 구축 시 각종 Tip을 추가로 정리하였다. 


필자의 개인적인 방법입니다. 일반적인 방법과 차이가 있거나 잘못된 부분이 있으면 피드백 부탁드립니다. 


RESTful API란?

REST(Representational State Transfer)는 웹서비스에서 통신을 위한  아키텍처이다. 이 용어는 로이 필딩(Roy Fielding)의 2000년 박사학위 논문에서 소개되었다. REST API는 리소스 중심으로 설계하고, 기능에 맞게 GET, POST, PUT, PATCH, DELETE 등의 메서드를 정의한다. 


GET : 지정된 URI에서 리소스의 표현을 조회한다.

POST : 지정된 URI에 신규 리소스를 생성한다.

PUT : 지정된 URI에 리소스를 생성하거나 업데이트한다.

PATCH : 리소스의 부분 업데이트한다.

DELETE : 지정된 URI의 리소스를 제거한다. 


Rest API 아키텍처에 적용되는 6가지 제한 조건은 아래와 같다.  참고로 이 내용은 위키피디아를 참고했다.

클라이언트/서버 구조: 일관적인 인터페이스로 분리되어야 한다

무상태(Stateless): 각 요청 간 클라이언트의 콘텍스트가 서버에 저장되어서는 안 된다

캐시 처리 가능(Cacheable): WWW에서와 같이 클라이언트는 응답을 캐싱할 수 있어야 한다. 잘 관리되는 캐싱은 클라이언트-서버 간 상호작용을 부분적으로 또는 완전하게 제거하여 scalability와 성능을 향상한다.

계층화(Layered System): 클라이언트는 보통 대상 서버에 직접 연결되었는지, 또는 중간 서버를 통해 연결되었는지를 알 수 없다. 중간 서버는 로드 밸런싱 기능이나 공유 캐시 기능을 제공함으로써 시스템 규모 확장성을 향상하는 데 유용하다.

Code on demand (optional): 자바 애플릿이나 자바스크립트의 제공을 통해 서버가 클라이언트가 실행시킬 수 있는 로직을 전송하여 기능을 확장시킬 수 있다.

인터페이스 일관성: 아키텍처를 단순화시키고 작은 단위로 분리(decouple)함으로써 클라이언트-서버의 각 파트가 독립적으로 개선될 수 있도록 해준다.


또한, REST 인터페이스의 원칙에 대한 가이드는 아래와 같다. 

자원의 식별

메시지를 통한 리소스의 조작

자기 서술적 메시지

애플리케이션의 상태에 대한 엔진으로써 하이미디어


https://ko.wikipedia.org/wiki/REST#REST_%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98%EC%97%90_%EC%A0%81%EC%9A%A9%EB%90%98%EB%8A%94_6%EA%B0%80%EC%A7%80_%EC%A0%9C%ED%95%9C_%EC%A1%B0%EA%B1%B4


리소스 중심 RESTful API URI(End Point) 설계

Rest API 개발 시 가장 중요한 것은 바로 URI 설계이다. 필자는 보통 엔드포인트(End-Point)라고 부른다. 일반적인 Rest API에서의 URI은 아래와 같은 규칙으로 설계한다.


동일한 URI(End Point)에 행위에 맞게 POST, GET, DELETE, PATCH 등의 메서드를 사용한다. 

명사를 사용한다. 리스트를 표현할 때는 복수형을 사용한다. 

URI Path 에 불필요한 파라미터를 넣지 말자. 즉, 단계를 심플하게 설계해야 한다. (3 Depth 이상 길어지면 안 된다.)


사실, 아주 기초적인 내용이라서.. 웬만한 개발자는 잘 알고 있는 내용인 것 같다. 필자는 개발을 오랫동안 안 해서 잊은 상태였다; 어쨌든 잘못된 내용은 꼭 피드백을 부탁한다.


동일한 URL(End Point)에 POST, GET, DELETE, PATCH 등 메서드를 적용

필자는 예전에 RestAPI 개념이 제대로 없을 때는 URI로 기능을 구분하였다. 물론, POST, GET 등의 메서드를 사용하기는 했다. 예로, 커피에 관련하여 API를 구축한다고 가정하자. 


//아주 예전(약.. 10년 전)에 RestAPI 개념을 제대로 모르던 시절 설계

//잘못된 방식의 예

카페에서 판매하는 커피의 리스트를 조회한다.  [GET 요청] http://도메인:포트/api/coffee/list

카페에서 판매하는 커피 중 특정 커피 하나를 조회한다.  

[GET 요청] http://도메인:포트/api/coffee/list/아메리카노

or 

[GET 요청] http://도메인:포트/api/coffee/list?name=아메리카노

카페에 신규 커피를 메뉴에 등록한다.  [POST 요청] http://도메인:포트/api/coffee/create-coffee

카페에 특정 커피를 메뉴에서 삭제한다.  [DELETE요청] http://도메인:포트/api/coffee/delete/latte


사실 위에 URL 적용된 list, create, delete 등은 불필요하다. RestAPI 철학을 잘 활용하기 위해서는 동일한 URL에 메서드를 활용해서 설계/개발해야 한다. 위에 예시는 아래와 같이 동일한 URL로 설계해야 한다. 


//RestAPI를 조금 알기 시작한 이후 설계

카페에서 판매하는 커피의 리스트를 조회한다.  [GET요청] http://도메인:포트/api/coffees


카페에서 판매하는 커피 중 특정 커피 하나를 조회한다.  

[GET요청] http://도메인:포트/api/coffees/아메리카노

카페에 신규 커피를 메뉴에 등록한다.  [POST요청] http://도메인:포트/api/coffees

카페에 특정 커피를 메뉴에서 삭제한다.  [DELETE요청] http://도메인:포트/api/coffees/아메리카노


불필요한 Path는 제거해서, http://도메인:포트/api/coffees 로 통일되었다. 


명사를 사용하자.

위 URI는 coffees라는 복수형 명사를 사용하였다. 동사 조합의 URI는 바람직하지 않다. 

잘못된 예)http://도메인:포트/api/getCoffees 이런 식으로 절대 사용하지 말자

명사를 사용하자!

불필요한 파라미터를 URI Path에 추가하지 말자. 

예를 들어서 커피 메뉴 리스트를 조회하는 API에서 우유가 추가된 메뉴만 조회한다고 하면 어떻게 할까? 만약 아래와 같이 API를 구성한다면 잘못된 예이다. 

잘못된 예) http://도메인:포트/api/coffees/milk 

불필요한 파라미터를 추가할 필요는 없다. 아래와 같이 Path 가 아닌 Query Param으로 적용하자. 

괜찮은 예) http://도메인:포트/api/coffees?type=milk


정리하면, 리소스 중심으로 API를 구성하는 것이다!! 필자의 설명은 별로니깐, 설명이 잘 되어있는 MSDN를 참고하자.

https://docs.microsoft.com/ko-kr/azure/architecture/best-practices/api-design#organize-the-api-around-resources


RestAPI는 심플하게 설계를 하는 것이 중요하지만, 불가피하게 API는 복잡해질 수 있다. 협업을 위해서, API는 반드시 문서화되어야 하는데, Swagger UI를 사용하면 좀 더 심플하게 커뮤니케이션을 할 수 있다. 


Swagger UI

Swagger는 API 문서화를 제공하며, 테스트를 쉽게 구성할 수 있다. 


스프링 부트 Swagger UI

스프링 부트 환경에서 간단하게 Swagger UI를 연동해보겠다. 쉽다. 


1. 디펜던시 추가를 한다. 


dependencies {
  생략...
   compile('io.springfox:springfox-swagger2:2.6.1')
   compile('io.springfox:springfox-swagger-ui:2.6.1')
생략...
}



2. Swagger Config 설정한다. 아래와 같이 패키지 경로를 설정해야 한다. 필자는 controller 패키지로 지정하였다. 


@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("spring.rest.api.swagger.controller"))
                .paths(PathSelectors.any())
                .build();
    }
}



3. @ApiOperation, @ApiImplicitParams 를 사용해서 API 설명 및 테스트 파라미터 지정한다.


@ApiOperation(value = "커피를 조회한다.")
@ApiImplicitParams({
        @ApiImplicitParam(name = "type", value = "커피타입", required = true, dataType = "string", paramType = "query", defaultValue = ""),
})
@GetMapping
public List<Coffee> ListCoffees(@RequestParam String name) {
    생략...
    return coffeeList;
}



이 글에서 모든 내용을 작성하기는 어려우니, 궁금하면 아래 참고 링크를 참고하자. 

https://github.com/swagger-api/swagger-core/wiki/annotations

http://www.baeldung.com/swagger-2-documentation-for-spring-rest-api



개인적인 의견 추가

Swagger 를 사용해본 결과, @ApiOperation, @ApiImplicitParams 를 사용하면, 처음에는 괜찮았지만 점점 사용할 수록 코드가 지저분해지는 상황을 만들었습니다. 스프링 기반으로 프로젝트를 진행한다면, 개인적으로는 Swagger 보다는 Spring Rest Docs 를 사용하는 것을 추천하고 싶습니다. 


JSONP

웹서비스 개발을 하다 보면, 도메인 간 웹서비스 통신의 경우에 보안상 이유로 API 호출이 안 되는 경우가 종종 있다. 보통 Ajax 통신하는 경우에 자주 발생하며 CORS 정책 때문이다. 일단, CORS에 대해서는 조금 이따 설명하기로 하고 CORS 의 대안으로 많이 사용하는 JSONP에 대해서 공부하자. JSONP는 서로 다른 도메인 간의 API 호출을 위한 방법으로, 클라이언트의 요청에 응답을 callback 함수로 매핑해서 전달하는 방식이다. 스프링에서 JSONP에 대해서는 아래 링크를 참고하자.

http://kingbbode.tistory.com/26

스프링에서는 JSONP Response에 callback 메서드를 간단하게 매핑해줄 수 있는데, 아래 소스만 적용하면 된다.


@ControllerAdvice

public class JsonpAdviceController extends AbstractJsonpResponseBodyAdvice {

    public JsonpAdviceController() {

        super("callback");

    }

}



@Bean

 public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {

 MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();

 ObjectMapper objectMapper = new ObjectMapper();

 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

 jsonConverter.setObjectMapper(objectMapper);

 return jsonConverter;

 }


근데, 사실 JSONP는 Get메서드만 가능하다. RestAPI를 정리하고 있는 이 글의 주제와는 약간 거리가 있으니 이 정도로만 정리하자. CORS에 대해서 알아보자.


CORS

위에 내용처럼 도메인 간 접근 시 GET 요청만 하겠다면, JSONP로 구현해도 된다. 하지만, RestAPI 답게 개발하기 위해서는 CORS 설정으로 통신을 해야 한다.  근데, 의외로 개발자들이 CORS에 대해서 자세히 모르는 경우가 많다. 일단 아래 링크를 참고하자. 

https://developer.mozilla.org/ko/docs/Web/HTTP/Access_control_CORS


스프링에서의 RESTful API 의 CORS 설정에 대해서 아래와 같이 정리하였다.


@CrossOrigin

서버에 접근 가능한 클라이언트를 지정한다. @CrossOrigin(origins="*") 로 설정한다면 모든 클라이언트 사용자의 접근을 허용할것이다. 특정 사용자만 허용한다면, @CrossOrigin(origins="도메인:포트") 로 설정하면 된다. 만약, 약속된 Header 정보가 외에 추가 Header 정보를 전송해야 한다면 allowedHeaders 를 사용해서 설정하면 된다. 


http://www.baeldung.com/spring-cors

https://spring.io/guides/gs/rest-service-cors/


응답 상태 코드 전달

@ResponseStatus(HttpStatus.OK) 어노테이션을 사용하면 된다. 상태 코드값에 대해서는 package org.springframework.http의 HttpStatus enum을 참고하자. 상태 코드에 대한 상세 설명은 생략한다.

1XX – Informational

2XX – Success

3XX – Redirection

4XX – Client Error

5XX – Server Error


공통 Exception 처리하기

RESTful API 의 공통 Exception 처리는 @RestControllerAdvice 으로 적용 가능하다. 


//공통 Exception 처리 예

@RestControllerAdvice
public class ApiExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(ApiExceptionHandler.class);

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ExceptionPojo> unExpectedExceptionHandler (Exception e) {
        logger.error("API Exception (UnExpected Exception) - {}", e);

        return new ResponseEntity<>(new ExceptionPojo("UnExpected Exception", e.getMessage()),HttpStatus.INTERNAL_SERVER_ERROR);
    }

생략...


그리고, 클라이언트에 Response Error 포맷을 지정하고 싶다면, 위와 같이 ResponseEntity  에 커스텀하게 구현한 Pojo 를 적용하면 된다. 


@JsonIgnoreProperties(ignoreUnknown = true)
public class ExceptionPojo {

    @JsonProperty(value = "status_code")
    private int status_code;
    생략...

    @JsonProperty(value = "message")
    private String message;

    public ExceptionPojo(String type, String message) {
        this.type = type;
        생략...
        this.message = message;
    }
}


참고로, 다른 회사에서는 Error 포맷을 어떻게 할까? 아래 링크를 참고하자. 참고로, 트위터, 페이스북, MS Bing 서비스에 대한 예시를 참고할 수 있다. 아주 재미있는 글이다. 

https://nordicapis.com/best-practices-api-error-handling/


정리

오늘도 이렇게 내용이 부실한 내용의 글이 되었다. 마음 같아서는 정말 자세하게, 강의하듯이 글을 쓰고 싶지만 주말에 잠깐 시간을 내서 작성하는 글이라서 완벽하게 글을 쓸 수는 없어서 아쉬운 마음이다. 어쨌든, RESTful API는 초기 디자인 설계가 정말 중요하다. 왜냐면, 한번 잘못 설계한 디자인은 추후에 변경하기도 쉽지 않고 커뮤니케이션하기 어렵기 때문이다. 필자의 설명이 완벽하지는 않지만 조금이라도 참고가 되었기를 바라며, 자세한 내용은 아래 MSDN의 레퍼런스를 통해서 참고하길 바란다. 


레퍼런스

https://docs.microsoft.com/ko-kr/azure/architecture/best-practices/api-design#organize-the-api-around-resources

https://github.com/Microsoft/api-guidelines/blob/master/Guidelines.md

매거진의 이전글 Bootiful Quartz
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari