[Spring Boot] 73. 스프링부트 블로그 v3 (RestAPI) (18) Swagger API 문서 자동화

김주희's avatar
May 15, 2025
[Spring Boot] 73. 스프링부트 블로그 v3 (RestAPI) (18) Swagger API 문서 자동화

API 문서의 목적

  • 프론트엔드에게 내 서버가 어떻게 생겼는지 알려주기 위해서
  • = 내 서버 사용법
 

API 문서에는 어떤 것들이 들어가야 할까?

게시글 작성의 경우를 예로 들어보자. 게시글 작성은 내가 입력한 값이 결국 db에 insert 되어야 한다. 그러나 프론트엔드에서는 db에 직접 접근 불가능하다. 즉 외부에서 봤을 때 Controller는 쓸 수 있는 메서드를 열어준다는 점에서 인터페이스와 유사하다.
  1. postmapping으로 /board 와 같이 api 요청을 위한 uri를 알아야 한다.
  1. localhost:8080과 같은 네트워크 주소도 필요하다.
  1. db에 들어가야할 컬럼값에 대한 정보도 필요하다.
  1. 마지막으로 데이터를 어떻게, 어떠한 방식으로 전달해야할지와 같은 content-type을 알아야 한다. (e.g. application/json)
 

Swagger를 사용해보자

Swagger란?

 
💡
단점 - 코드 가독성을 해친다

Swagger 사용하기

 
1.
notion image
2.
notion image
plugins { id 'java' id 'org.springframework.boot' version '3.2.2' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'shop.mtcoding' version = '1.0' java { sourceCompatibility = '21' } configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.springframework.boot:spring-boot-starter-aop' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation group: 'com.auth0', name: 'java-jwt', version: '4.3.0' implementation group: 'org.qlrm', name: 'qlrm', version: '4.0.1' implementation group: 'org.mindrot', name: 'jbcrypt', version: '0.4' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() } // plain 파일 생성하지 않기 jar { enabled = false }
3.
package shop.mtcoding.blog.user; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; import lombok.Data; public class UserRequest { @Data public static class UpdateDTO { @Schema(description = "비밀번호 (4~20자)", example = "1234") @Size(min = 4, max = 20) private String password; @Schema(description = "이메일 주소", example = "user@example.com") @Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요") private String email; } @Data public static class JoinDTO { @Schema(description = "유저네임 (2~20자, 특수문자/한글 불가)", example = "metacoding") @Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다") private String username; @Schema(description = "비밀번호 (4~20자)", example = "1234") @Size(min = 4, max = 20) private String password; @Schema(description = "이메일 주소", example = "user@example.com") @Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요") private String email; public User toEntity() { return User.builder() .username(username) .password(password) .email(email) .build(); } } @Data public static class LoginDTO { @Schema(description = "유저네임 (2~20자)", example = "metacoding") @Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다") private String username; @Schema(description = "비밀번호 (4~20자)", example = "1234") @Size(min = 4, max = 20) private String password; @Schema(description = "자동 로그인 여부 (체크시 'on')", example = "on", nullable = true) private String rememberMe; // check되면 on, 안되면 null } }
 
4.
package shop.mtcoding.blog.user; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.util.Resp; import java.util.Map; @Slf4j @Tag(name = "User API", description = "회원가입, 로그인, 회원정보 수정 등 사용자 관련 API") @RequiredArgsConstructor @RestController // json만 리턴!! public class UserController { private final UserService userService; private final HttpSession session; @Operation(summary = "회원정보 수정", description = "로그인한 사용자의 비밀번호와 이메일을 수정합니다.") @PutMapping("/s/api/user") public ResponseEntity<?> update(@Valid @RequestBody UserRequest.UpdateDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); UserResponse.DTO respDTO = userService.회원정보수정(reqDTO, sessionUser.getId()); return Resp.ok(respDTO); } @Operation(summary = "유저네임 중복체크", description = "해당 유저네임이 이미 사용 중인지 확인합니다.") @GetMapping("/api/check-username-available/{username}") public ResponseEntity<?> checkUsernameAvailable( @Parameter(description = "확인할 유저네임", example = "metacoding") @PathVariable("username") String username) { Map<String, Object> respDTO = userService.유저네임중복체크(username); return Resp.ok(respDTO); } @Operation(summary = "회원가입", description = "유저네임, 비밀번호, 이메일을 받아 회원가입을 진행합니다.") @PostMapping("/join") public ResponseEntity<?> join( @Valid @RequestBody UserRequest.JoinDTO reqDTO, Errors errors, HttpServletResponse response, HttpServletRequest request) { log.debug(reqDTO.toString()); log.trace("트레이스ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); log.debug("디버그---------"); log.info("인포ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); log.warn("워닝ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); log.error("에러ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); String hello = request.getHeader("X-Key"); System.out.println("X-good : " + hello); response.setHeader("Authorization", "jooho"); UserResponse.DTO respDTO = userService.회원가입(reqDTO); return Resp.ok(respDTO); } @Operation(summary = "로그인", description = "유저네임과 비밀번호를 이용하여 로그인합니다.") @PostMapping("/login") public ResponseEntity<?> login( @Valid @RequestBody UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) { UserResponse.TokenDTO respDTO = userService.로그인(loginDTO); return Resp.ok(respDTO); } // AccessToken만으로는 Logout 을 할 수 없다. }
 
 
5.
package shop.mtcoding.blog.user; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; import org.springframework.validation.Errors; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.util.Resp; import java.util.Map; @Slf4j @Tag(name = "User API", description = "회원가입, 로그인, 회원정보 수정 등 사용자 관련 API") @RequiredArgsConstructor @RestController public class UserController { private final UserService userService; private final HttpSession session; @Operation(summary = "회원정보 수정", description = "로그인한 사용자의 비밀번호와 이메일을 수정합니다.") @ApiResponse(responseCode = "200", description = "회원정보 수정 성공", content = @Content(schema = @Schema(implementation = UserResponse.DTO.class))) @PutMapping("/s/api/user") public ResponseEntity<?> update(@Valid @RequestBody UserRequest.UpdateDTO reqDTO, Errors errors) { User sessionUser = (User) session.getAttribute("sessionUser"); UserResponse.DTO respDTO = userService.회원정보수정(reqDTO, sessionUser.getId()); return Resp.ok(respDTO); } @Operation(summary = "유저네임 중복체크", description = "해당 유저네임이 이미 사용 중인지 확인합니다.") @ApiResponse(responseCode = "200", description = "중복 여부 반환", content = @Content(schema = @Schema(implementation = Map.class))) @GetMapping("/api/check-username-available/{username}") public ResponseEntity<?> checkUsernameAvailable( @Parameter(description = "확인할 유저네임", example = "metacoding") @PathVariable("username") String username) { Map<String, Object> respDTO = userService.유저네임중복체크(username); return Resp.ok(respDTO); } @Operation(summary = "회원가입", description = "유저네임, 비밀번호, 이메일을 받아 회원가입을 진행합니다.") @ApiResponse(responseCode = "200", description = "회원가입 성공", content = @Content(schema = @Schema(implementation = UserResponse.DTO.class))) @PostMapping("/join") public ResponseEntity<?> join( @Valid @RequestBody UserRequest.JoinDTO reqDTO, Errors errors, HttpServletResponse response, HttpServletRequest request) { log.debug(reqDTO.toString()); log.trace("트레이스ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); log.debug("디버그---------"); log.info("인포ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); log.warn("워닝ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); log.error("에러ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ"); String hello = request.getHeader("X-Key"); System.out.println("X-good : " + hello); response.setHeader("Authorization", "jooho"); UserResponse.DTO respDTO = userService.회원가입(reqDTO); return Resp.ok(respDTO); } @Operation(summary = "로그인", description = "유저네임과 비밀번호를 이용하여 로그인합니다.") @ApiResponse(responseCode = "200", description = "로그인 성공", content = @Content(schema = @Schema(implementation = UserResponse.TokenDTO.class))) @PostMapping("/login") public ResponseEntity<?> login( @Valid @RequestBody UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) { UserResponse.TokenDTO respDTO = userService.로그인(loginDTO); return Resp.ok(respDTO); } }
6.
 
package shop.mtcoding.blog.user; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Builder; import lombok.Data; public class UserResponse { @Data public static class TokenDTO { @Schema(description = "엑세스 토큰", example = "eyJhbGciOiJIUzI1NiIsInR5cCI...") private String accessToken; @Schema(description = "리프레시 토큰", example = "dGhpc0lzUmVmcmVzaFRva2Vu") private String refreshToken; @Builder public TokenDTO(String accessToken, String refreshToken) { this.accessToken = accessToken; this.refreshToken = refreshToken; } } @Data public static class DTO { @Schema(description = "유저 ID", example = "1") private Integer id; @Schema(description = "유저 이름", example = "cos") private String username; @Schema(description = "이메일 주소", example = "cos@nate.com") private String email; @Schema(description = "생성일시", example = "2024-05-16T10:00:00") private String createdAt; public DTO(User user) { this.id = user.getId(); this.username = user.getUsername(); this.email = user.getEmail(); this.createdAt = user.getCreatedAt().toString(); } } }
 
notion image
Share article

jay0628