JUINTINATION
Swagger를 활용한 API Specification 본문
Open API Document 명세 제안
스프링부트를 활용한 todo-api-server의 내용을 깃허브에 올리고 얼마 지나지 않았을 때 갑자기 알림이 왔다. 준호 형이 API 명세를 하면 좋겠다고 Issue를 남긴 것이었다. 그래서 나는 지난 스퍼트 프로젝트에서 사용한 적이 있는 Swagger를 사용하기로 했다. (샤라웃 투 준호형)
Swagger
Swagger란 Restful API를 문서화하고, 사용자가 쉽게 테스트하고 호출할 수 있도록 하는 도구이다. 위에서 준호 형이 언급했듯이 실제로 프론트엔드 개발자와의 협업에는 정보 공유가 중요하기 때문에 API 명세는 매우 중요한 역할을 한다. 어떤 url에서 어떤 기능을 하는지, 해당 기능이 성공 혹은 실패를 했을 때 어떤 값을 리턴 받는지 등을 알아야 다른 개발자들도 불필요한 시간 낭비 없이 일처리를 할 수가 있는 것이다.
build.gradle
buildscript {
ext {
queryDslVersion = "5.0.0"
}
}
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.4'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'org.zerock'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
annotationProcessor 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation "com.querydsl:querydsl-jpa:${queryDslVersion}:jakarta"
annotationProcessor(
"jakarta.persistence:jakarta.persistence-api",
"jakarta.annotation:jakarta.annotation-api",
"com.querydsl:querydsl-apt:${queryDslVersion}:jakarta"
)
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
}
tasks.named('test') {
useJUnitPlatform()
}
compileJava.dependsOn('clean')
Swagger를 스프링부트에 적용하기 위해서는 위와 같이 기존의 build.gradle에서 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
부분을 추가해야 한다.
열심히 찾아본 것이 아니라서 각 스프링부트의 버전별 맞는 Swagger의 버전은 다르겠지만, 일단 스프링부트 3.2.4 버전에서 2.2.0 버전의 Swagger는 잘 작동하는 것을 볼 수 있다.
아래의 과정을 모두 거친 후에 application을 실행한 후 http://localhost:8080/swagger-ui/index.html에 접속하게 되면 위의 화면을 볼 수 있다.
예시로 GET 요청에 대해 창을 열어보면 위와 같이 Try it out 버튼을 눌러 tno을 입력하여 원하는 값을 요청할 수 있고, 실제 출력값과 정상적인 입력이 들어왔을 때의 출력되는 예시도 같이 볼 수 있다.
또한 아래쪽에 있는 Schemas 부분에서의 TodoDTO의 예시를 봤을 때 각 dto의 필드값이 어떤 역할을 하는지, 어떤 값이 들어와야 하는지, 기본값은 어떻게 되어있는지 등을 볼 수 있다.
이와 같은 API 명세를 보고 다른 개발자들은 내가 작성한 코드를 어떻게 활용해야 올바르게 사용하는 것인지, 어떤 값을 어떻게 사용해야 하는지 등을 알 수 있다.
이제 작성한 내용에 대해 공유할 것인데 아직 공부를 깊게 하지는 않은 내용이므로 아래에 작성된 내용이 정답은 아니다..
더 좋은 방법이 있거나 틀린 방법으로 작성한 부분이 있다면 따끔하게 알려주세요..!!
controller/TodoController.java
package org.zerock.todoapi.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.web.bind.annotation.*;
import org.zerock.todoapi.dto.PageRequestDTO;
import org.zerock.todoapi.dto.PageResponseDTO;
import org.zerock.todoapi.dto.TodoDTO;
import org.zerock.todoapi.service.TodoService;
import java.util.Map;
@RestController
@RequiredArgsConstructor
@Log4j2
@RequestMapping("/api/todo")
@Tag(name = "TODO API", description = "TodoController")
public class TodoController {
private final TodoService todoService;
@GetMapping("/{tno}")
@Operation(summary = "TODO 조회", description = "TODO 하나를 조회합니다.")
public TodoDTO get(@PathVariable("tno") Long tno) {
return todoService.get(tno);
}
@GetMapping("/list")
@Operation(summary = "TODO 리스트 조회", description = "페이지 번호와 사이즈에 맞는 TODO 리스트를 조회합니다.")
@Parameters({
@Parameter(name = "page", description = "페이지 번호, 1부터 시작", example = "1"),
@Parameter(name = "size", description = "페이지 사이즈", example = "10"),
})
public PageResponseDTO<TodoDTO> list(PageRequestDTO pageRequestDTO) {
return todoService.getList(pageRequestDTO);
}
@PostMapping("/")
@Operation(summary = "TODO 생성", description = "TODO 하나를 생성합니다.")
@Parameters({
@Parameter(name = "title", description = "TODO 제목", example = "Title.."),
@Parameter(name = "content", description = "TODO 내용, 500자 이내", example = "Content.."),
@Parameter(name = "dueDate", description = "TODO 마감 날짜", example = "2024-03-12"),
})
@ApiResponse(
responseCode = "200",
description = """
{
"TNO": {tno}
}
"""
)
public Map<String, Long> register(@RequestBody TodoDTO dto) {
long tno = todoService.register(dto);
return Map.of("TNO", tno);
}
@PutMapping("/{tno}")
@Operation(summary = "TODO 수정", description = "TODO 하나를 수정합니다.")
@Parameters({
@Parameter(name = "tno", description = "수정할 TODO 번호", example = "1"),
@Parameter(name = "title", description = "수정할 TODO 제목", example = "Modified Title.."),
@Parameter(name = "content", description = "수정할 TODO 내용, 500자 이내", example = "Modified Content.."),
@Parameter(name = "complete", description = "수정할 TODO의 마감 여부", example = "true"),
@Parameter(name = "dueDate", description = "수정할 TODO 마감 날짜", example = "2024-03-12"),
})
@ApiResponse(
responseCode = "200",
description = """
{
"RESULT": "SUCCESS"
}
"""
)
public Map<String, String> modify(@PathVariable("tno") Long tno,
@RequestBody TodoDTO dto) {
dto.setTno(tno);
todoService.modify(dto);
return Map.of("RESULT", "SUCCESS");
}
@DeleteMapping("/{tno}")
@ApiResponse(
responseCode = "200",
description = """
{
"RESULT": "SUCCESS"
}
"""
)
@Operation(summary = "TODO 삭제", description = "TODO 하나를 삭제합니다.")
public Map<String, String> remove(@PathVariable("tno") Long tno) {
todoService.remove(tno);
return Map.of("RESULT", "SUCCESS");
}
}
먼저 TodoController 부분부터 내용을 추가해볼 것이다.
@Tag
어노테이션은 API의 그룹을 나타낸다.- name 속성은 그룹의 이름을, description 속성은 그룹에 대한 설명을 제공한다.
- 여기에서는 TODO API라는 이름의 그룹으로, TodoController에 대한 설명을 제공한다.
@Operation
어노테이션은 특정 엔드포인트의 동작을 설명한다.- summary는 간단한 설명을 제공하고, description은 보다 자세한 설명을 제공한다.
- 예시로 get 메서드에 대해 'TODO 조회'라는 간단한 설명과 'TODO 하나를 조회합니다.'라는 상세 설명을 추가했다.
@Parameters
어노테이션은 여러 개의@Parameter
어노테이션을 그룹화하여 사용할 수 있다.- 각
@Parameter
어노테이션은 API 엔드포인트의 매개변수를 설명한다. - name 속성은 매개변수의 이름을, description 속성은 매개변수에 대한 설명을, example 속성은 매개변수의 예제 값을 제공한다.
- 예시로 list 메서드에 대해 페이지 번호와 페이지 사이즈를 설명하는 매개변수들이 정의되어 있다.
- 각
@ApiResponse
어노테이션은 특정 상태 코드에 대한 응답을 설명한다.- responseCode는 HTTP 상태 코드를 나타내고, description은 응답의 예제를 설명한다.
- 예시로 register 메서드에 대해 200 상태 코드의 응답 예제를 정의했다.
여기까지 작성을 마쳤으면 다른 개발자들이 어떤 url에 요청을 해야 원하는 리턴값을 받을 수 있는지 이해할 수 있을 것이다.
다음은 Schemas 부분에 해당하는 dto에 관한 내용들이다.
dto/TodoDTO.java
package org.zerock.todoapi.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "TODO DTO")
public class TodoDTO {
@Schema(description = "TODO 식별 번호", defaultValue = "0")
private Long tno;
@Schema(description = "TODO 생성 일자")
private LocalDateTime regDate;
@Schema(description = "TODO 수정 일자")
private LocalDateTime modDate;
@Schema(description = "TODO 제목", example = "Title..")
private String title;
@Schema(description = "TODO 내용", example = "Content..")
private String content;
@Schema(description = "TODO 완료 여부", defaultValue = "false")
private boolean complete;
@Schema(description = "TODO 마감 일자", example = "2024-03-12")
private LocalDate dueDate;
}
dto/PageRequestDTO.java
package org.zerock.todoapi.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
@Data
@SuperBuilder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "페이지 요청 DTO")
public class PageRequestDTO {
@Builder.Default
@Schema(description = "요청 페이지 번호", defaultValue = "1")
private int page = 1;
@Builder.Default
@Schema(description = "한 페이지에 들어가는 TODO의 개수", defaultValue = "10")
private int size = 10;
}
dto/PageResponseDTO.java
package org.zerock.todoapi.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@Data
@Schema(description = "페이지 응답 DTO")
public class PageResponseDTO<E> {
@Schema(description = "현재 페이지의 TODO DTO 리스트")
private List<E> dtoList;
@Schema(description = "현재 페이지에서 볼 수 있는 페이지 번호 리스트")
private List<Integer> pageNumList;
@Schema(description = "페이지 요청 DTO")
private PageRequestDTO pageRequestDTO;
@Schema(description = "이전 페이지 리스트 존재 여부")
private boolean prev;
@Schema(description = "다음 페이지 리스트 존재 여부")
private boolean next;
@Schema(description = "전체 TODO 개수")
private int totalCount;
@Schema(description = "이전 페이지 리스트의 시작 번호")
private int prevPage;
@Schema(description = "다음 페이지 리스트의 시작 번호")
private int nextPage;
@Schema(description = "현재 페이지에서 볼 수 있는 페이지 개수")
private int totalPage;
@Schema(description = "현재 페이지 번호")
private int current;
@Builder(builderMethodName = "withAll")
public PageResponseDTO(List<E> dtoList, PageRequestDTO pageRequestDTO, long totalCount) {
this.dtoList = dtoList;
this.pageRequestDTO = pageRequestDTO;
this.totalCount = (int) totalCount;
int end = (int) (Math.ceil(pageRequestDTO.getPage() / 10.0)) * 10;
int start = end - 9;
int last = (int) (Math.ceil((totalCount / (double) pageRequestDTO.getSize())));
end = Math.min(end, last);
this.prev = start > 1;
this.next = totalCount > (long) end * pageRequestDTO.getSize();
this.pageNumList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
if (prev) {
this.prevPage = start -1;
}
if (next) {
this.nextPage = end + 1;
}
this.totalPage = this.pageNumList.size();
this.current = pageRequestDTO.getPage();
}
}
위의 dto 관련 코드들에서 공통적으로 사용된 것은 @Schema
어노테이션이다.
@Schema
어노테이션은 dto의 메타데이터를 제공하여 dto 클래스들의 각 필드를 설명하는 데 도움을 준다.- OpenAPI 표준에 따라 작성해야 한다고 하는데 이에 관련된 내용은 추후에 더 찾아보면 좋을 것 같다.
- description 속성은 필드에 대한 설명을 제공하고, defaultValue 속성은 해당 필드의 기본값을 정의한다.
결론
API 명세와 Swagger에 대해 정리해 봤다. 추후에 다른 사람들과 협업을 할 때 굉장히 중요한 내용이며, 표준에 대해 더 연구해야 하는 부분이기도 하다. 그래서 그런지 이 글을 쓰기까지 굉장히 망설여졌는데, "틀리면 어때? 나중에 발전하고 고치면 되지"라는 말을 입버릇처럼 하는 내가 부끄러워져서 바로 이 글을 작성하게 되었다.
아직도 가야할 길이 멀다. 정확히 알지도 못하면서 아는척하는 그런 사람은 되지 말아야지. 더 열심히 해야겠다는 그런 생각이 들었다.
'StudyNote' 카테고리의 다른 글
2024 SW 융합클러스터 2.0 세종 DX 해커톤 후기 (0) | 2024.08.04 |
---|---|
Postman을 활용한 API 문서 만들기 (2) | 2024.07.07 |
비지터(Visitor) 패턴 (1) | 2024.06.26 |
빌더(Builder) 패턴 (2) | 2024.04.30 |
스프링부트 + MySQL 프로젝트 도커라이징하기 (1) | 2024.03.11 |