티스토리 뷰
현대 개발자들은 Swagger나 Spring docs를 사용해 RestFul API를 문서화 한다. 그렇다면 왜 문서화를 할까?
API 문서화가 필요한 경우
혼자 개발하는 경우엔 필요없는 기능일지도 모르지만, 같이 개발한다고 가정해보자 프론트엔드 개발자는 RestAPI 호출되는 방식을 알아야 한다. 하지만 문서 없이 말로만 설명 한다면 계속 까먹을 때마다 설명해야하는 번거로움도 존재할 수 있고 의사전달이 잘못될 수도 있는 문제가 발생한다.
어찌보면 이는 당연한 일이다. 누군가와 같이 일을 하는데 문서 없이 그저 말로만 하는 경우는 있을 수 없는 일이다!
그렇다면 대표적인 문서 자동화 도구인 Swagger와 RestDocs에 대해서 살펴보도록 하자
Spring REST Docs
Restful 서비스의 문서화를 도와주는 도구로서 Spring REST Docs는 문서 작성 도구로 기본적으로 Asciidoctor를 사용하며 이것을 사용해 HTML을 생성한다. 필요한 경우 Markdown을 사용하도록 변경할 수도 있다.
Spring REST Docs는 MVC의 테스트 프레임워크로 작성된 테스트 코드에서 생성된 Snippet을 사용한다. 테스트 접근 방식은 서비스 문서의 정확성을 보장해준다. Snippet이 올바르지 않을 경우 테스트가 실패하기 때문이다.
그렇기에 REST Docs는 정확성을 보장해 준다. 테스트가 성공해야 문서를 만들 수 있기 때문이다.
RESTful 서비스를 문서화 하는 것은 해당 리소스를 설명하는 것이다. 리소스 설명의 핵심은 HTTP 요청과 HTTP 응답의 세부정보이다. Spring REST Docs를 사용하면 리소스와 HTTP 요청 및 응답을 사용하여 서비스의 내부의 세부 정보로부터 문서를 보호할 수 있다. 즉, 서비스 내부 코드에 추가 또는 수정과 같이 영향을 주지 않고도 별도의 문서화 작업을 진행할 수 있게된다.
결론적으로 Spring REST Docs의 장점을 정리하자면
1. 테스트 기반 접근 방식이기에 서비스 문서의 정확성을 보장해준다.(정확하지 않으면 테스트 실패)
2. 자신의 비즈니스 로직 내에 코드의 추가 수정이 필요하지 않다. (코드가 깔끔해진다.)
자 그러면 한번 적용해보자
적용 시작!!
우선 간단한 책을 저장하는 구현 애플리케이션에 데이터를 저장할 엔티티 클래스를 만들어주었다.
@Entity
public class Book {
@Id @GeneratedValue
private Long id;
private String title;
private int count;
private String isbn;
// .... 이후 코드
}
그 후에 외부와 통신을 할 Controller를 구현 했다.
@RestController
@RequestMapping("/books")
@RequiredArgsConstructor
public class BookController {
private final BookService bookService;
// create
@PostMapping("/save")
public ResponseEntity<BookDTO> register(@RequestBody BookDTO requestBook) {
Book book = requestBook.toEntity();
return new ResponseEntity<>(bookService.create(book), HttpStatus.CREATED);
}
// read
@GetMapping("/{id}")
public ResponseEntity<BookDTO> findBook(@PathVariable Long id) {
return new ResponseEntity<>(bookService.findById(id), HttpStatus.OK);
}
// update
@PutMapping("/{id}")
public ResponseEntity<Void> modify(@PathVariable Long id, @RequestBody BookDTO bookDTO) {
bookService.update(id, bookDTO.toEntity());
return ResponseEntity.ok().build();
}
// delete
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
bookService.deleteById(id);
return ResponseEntity.noContent().build();
}
}
간단한 CRUD REST API에 대하여 구현해 보았습니다. 서비스로직과 외에 DTO 클래스는 주로 다루는 내용이 아니니 생략했습니다.
그렇다면 핵심 로직이 될 수 있는 테스트 코드를 살펴보자
@ExtendWith(RestDocumentationExtension.class) // When using JUnit5
@SpringBootTest
public class PostControllerTest {
private MockMvc mockMvc;
@BeforeEach
public void setUp(WebApplicationContext webApplicationContext,
RestDocumentationContextProvider restDocumentation) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
.apply(documentationConfiguration(restDocumentation))
.build();
}
}
먼저 기존 셋팅의 구조는 이러하다. webAppContextSetup()만 쓴다면 Mock을 사용하는 일반 적인 setting이겠지만 apply(documentationConfiguration(restDocumentation))를 추가함으로써 문서화를 할 수 있다.
@AutoConfigureMockMvc
@AutoConfigureRestDocs
@SpringBootTest
class BookControllerTest {
@Autowired MockMvc mockMvc;
@MockBean BookService bookService;
위 코드는 전 코드를 단순하게 만든 코드이다. @AutoConfigureRestDocs로 apply(documentationConfiguration(restDocumentation))를 간결하게 표현해 주었다. 이제 한번 테스트 전체적인 코드를 보자.
@Test
void create() throws Exception {
final BookDTO bookDTO = BookDTO.builder()
.title("designPattern")
.count(1234)
.isbn("4353")
.build();
when(bookService.create(any())).thenReturn(bookDTO);
mockMvc.perform(post("/books/save")
.content("{" +
"\"title\": \"designPattern\", " +
"\n\"count\": \"1234\", " +
"\n\"isbn\": \"4353\"" +
"}")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andDo(document("Book created",
requestFields(
fieldWithPath("title").description("designPattern"),
fieldWithPath("count").description(1234),
fieldWithPath("isbn").description("4353")
)));
}
요청 방식을 post를 선택하고 /books/save를 호출하였습니다. 그 후에 RequestBody를 받기 때문에 content안에 데이터가 있어야 한다. 그래서 Json 형태로 데이터를 보냈다.
{
"title": "designPattern",
"count": "1234",
"isbn": "4353"
}
그리고 application/json 형식으로 요청을 받고 정상적으로 동작 시 201 created를 던지게 설계하였다.
requestFields를 받아 description에 fieldWithPath에 대한 설명을 썼다.
정상적으로 테스트에 성공했다. 그러면 이제 build를 살펴보자
정상적으로 build/generated-snipets에 등록이 되었다. 이후 src/docs/asciidoc과 같이 디렉토리를 만들고 임시이름.adoc 파일을 적성해주자
ifndef::snippets[]
:snippets: ../../../build/generated-snippets
endif::[]
= RESTful Notes API Guide
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
include::{snippets}/Book created/curl-request.adoc[]
include::{snippets}/Book created/http-request.adoc[]
include::{snippets}/Book created/http-response.adoc[]
include::{snippets}/Book created/httpie-request.adoc[]
include::{snippets}/Book created/request-body.adoc[]
include::{snippets}/Book created/request-fields.adoc[]
include::{snippets}/Book created/response-body.adoc[]
코드 작성을 완료 후
빨간 펜으로 강조한 부분을 클릭하면 html 파일이 생성된다. 그걸 resource/static/html으로 디렉토리를 변경해준다.
마지막으로 서버를 실행시키고 http://localhost8080/html/파일명.html로 접속하면 다음과 같이 문서화된 HTML 페이지가 나올 것이다.
그렇다면 이제 REST docs에 대한 얘기가 끝났다. 이제 Swagger에 대해 알아보자
Swagger 2.0
Swagger도 REST Docs와 마찬가지로 문서 자동화를 위해 많은 개발자들이 사용하는 프레임워크이다. 기능을 살펴보면
- API 디자인
- API 빌드
- API 문서화
- API 테스팅
- API 표준화
Swagger에 가장 큰 장점은 적용이 쉬운 점 실제 사용되는 Parameter로 테스트가 가능하다는 점이다.
그렇다면 한번 직접 Spring Boot에 적용해보자
적용 시작!!
SwaggerConfig 파일 부터 만들어 보자
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.select()
.apis(RequestHandlerSelectors.basePackage("me.golf.swagger"))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("swaggerTest")
.description("Swagger2.0")
.version("1.0")
.build();
}
}
먼저 useDefaultResponseMessage(false)는 200이나 400등 상태코드를 자동으로 만들어주는 기능이다. 오류가 날 수 있고 이미 본 필자는 코드에 코드를 정의해 뒀기에 꺼준다.
두 번째로 select()메서드로 ApiSelectorBuilder를 생성하고 apis안에 본인이 적용시킬 패키지 범위를 정의한다. 후에 연결되는 path를 apis에 있는 API 중 특정 path를 선택하고 apiInfo로 Swagger UI에 노출할 정보를 정한다.
자 그러면 일단 기본적인 Swagger에 대한 셋팅이 끝났다. 정말 간단하게 끝났다.
그렇다면 Controller와 DTO에 Swagger를 적용할 차례이다.
@RestController
@RequestMapping("/books")
@RequiredArgsConstructor
public class BookController {
private final BookService bookService;
// create
@PostMapping("/save")
@ApiOperation(value = "책 정보 저장", notes = "책의 정보를 받아 저장한다.")
@ApiParam(name = "BookRequest", value = "요청 들어온 책 데이터")
public ResponseEntity<BookDTO> register(@RequestBody BookDTO requestBook) {
Book book = requestBook.toEntity();
return new ResponseEntity<>(bookService.create(book), HttpStatus.CREATED);
}
// read
@GetMapping("/{id}")
@ApiOperation(value = "책 정보 조회", notes = "책의 정보를 조회한다.")
@ApiParam(name = "id", value = "책 고유 식별 번호")
public ResponseEntity<BookDTO> findBook(@PathVariable Long id) {
return new ResponseEntity<>(bookService.findById(id), HttpStatus.OK);
}
}
간단하게 어노테이션을 이용하여 적용해 보았다.
@ApiOperation으로 해당 API에 대한 간단한 설명을 적어보고
@APiParam으로 요청으로 들어와야 하는 값에 대해 설명을 적어 놓았다. 그렇다면 해당 문서를 본 프론트 개발자는 더욱 쉽게 작업에 들어갈 수 있을 거라 생각한다.
아무튼 이어서 DTO 클래스를 살펴보자
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class BookDTO {
@ApiModelProperty(example = "책 제목")
private String title;
@ApiModelProperty(example = "책 수량")
private int count;
@ApiModelProperty(example = "책 번호")
private String isbn;
public Book toEntity() {
return Book.builder()
.title(title)
.count(count)
.isbn(isbn)
.build();
}
}
@ApiModelProPerty를 이용하여 요청과 응답이 어떤 구조로 주고 받게 되며 key가 어떤 의미인지에 대해 적어놓을 수 있다. 정말 편한기능이 아닐 수 없다.
자, 그러면 이제 모든 준비는 끝났다 결과 화면을 보아야 한다.
화면을 보는 방법 또한 간단하다.
localhost:8080/swagger-ui.html 페이지로 들어가면 swagger 페이지를 볼 수 있다.
보기 좋은 UI가 보여지는 것을 볼 수 있다. 결코 필자가 Swagger를 사용해서 그런 것은 아니다.
자 그러면 직접 내부를 살펴보자
다음은 조회 API에 대한 문서이다. 필수 값이 무엇인지 응답 코드는 무엇인지 Example value까지 친절하게 나와 있다. 이걸 이용해 테스트도 가능하다. Postman처럼 테스트를 해보며 검증이 가능하니 한 번 해보시는 것을 추천한다.
마무으리
우리는 두 가지의 방식의 API 자동 문서화 프레임 워크에 대해 알아보았다. 두 가지 모두 좋은 프레임 워크라고 생각한다.
먼저 REST Docs는 테스트 기반의 문서화 프레임 워크로 테스트 성공 시 문서가 생성되어 정확한 문서가 생성된다는 장점과 실제 코드엔 추가 코드가 필요하지 않아 내부 코드를 깔끔하게 유지할 수 있다는 장점이 있다.
하지만 REST Docs는 진입 난이도가 어렵고 mockMvc라는 개념도 도입을 해야하기 때문에 적용하기 까다롭다는 단점이 있다.
두 번째로 Swagger는 REST Docs에 비해 적용하기 쉽고 UI가 보기 좋은 장점과 테스트도 해볼 수 있는 장점이 존재하지만 REST Docs는 테스트 기반이기 때문에 크게 장점으로 부각될 순 없다고 생각한다. 또한 내부에 많은 어노테이션이 들어가 지저분한 내부 코드를 볼 수 도 있다.
필자는 이 두 프레임 워크 각자 장단점이 존재한다고 생각한다. 그렇기에 둘 다 써보고 입맛에 맞는 프레임 워크를 쓰는 것을 추천한다. 또한 내부의 코드 상태도 두 프레임워크를 선택할 때 큰 요인으로 작용할 수 있다고 생각한다. 본인 코드에 이미 많은 어노테이션이 붙어있고 코드 가독성이 떨어지는데 Swagger를 사용하면 안좋게 보일 수 있다고 생각한다.
상황에 따라 적절한 프레임 워크를 사용하는 것도 본인 능력이니 우리에게 그런 힘을 기르기 위한 공부만이 정답이다!!
이상 포스팅을 마치겠다.
Reference.
https://subji.github.io/posts/2021/01/06/springrestdocsexample
https://tecoble.techcourse.co.kr/post/2020-08-18-spring-rest-docs/
https://bcp0109.tistory.com/326
https://kim-jong-hyun.tistory.com/49
'Spring Framwork & JPA' 카테고리의 다른 글
많은 외부 API를 호출해야 하는 상황에서 성능 개선 (with. webClient) (0) | 2023.01.02 |
---|---|
Pagination 전략에 대해서 (Page 인터페이스 커스텀하기) (0) | 2022.08.04 |
Redis 직렬화/역직렬화시 생기는 Issue (0) | 2022.05.23 |
[Thymleaf - 1부] 타임리프의 주요 문법 (0) | 2022.02.21 |
SpringBootApplication과 Spring AutoConfigure란? (0) | 2022.02.17 |
- Total
- Today
- Yesterday
- 동시성
- java
- Spring
- 백엔드
- 개발자
- 자바
- DevOps
- swarm
- IT
- Kotlin
- 취업
- 취준
- 면접 준비
- MySQL
- 개발
- 인터뷰
- 코드
- Redis
- 코딩
- DB
- thread
- docker
- 면접
- 프로젝트
- 면접준비
- 취업준비
- JPA
- 프로그래밍
- CS
- 게시판
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |