
껄껄쓴.. 오늘은 스웨거로 api 명세서를 문서화하는 방법에 대해 정리해보겠습니다.!!!!
기본적으로 나는 postman을 별로 선호하지 않기에..,.. 스웨거를 늘 활용하는데 지난 학기에 백엔드가 아닌 프론트로 팀플에 참여해보게 된 경험 후로 느낀 것이 참 많다...
api 명세서와 같이 문서화가 정말정말 중요하고 프론트분들에게 폐를 끼치지 않기 위해 노력하고 노력해야겠다는 것을 배웠다. 그래서 스웨거로 api 명세서를 평소에도 활용하고 있었지만 에러 응답과 같이 세부적인 것도 보여줄 수는 없을까?? 하는 생각이 들었다.
그때!! 몇 달 전에 선배가 자기가 정리했다면서 보여준게 생각나서 나도 에러 코드도 스웨거에서 확인할 수 있도록 추가해보기로 결심하였다.
우선 필요한 dependency를 적용해준다
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
참고로 낮은 버전을 사용하면 springdoc-openapi 라이브러리와 spring-web의 버전 충돌이 일어나서 jakarta.servlet.ServletException: Handler dispatch failed: java.lang.NoSuchMethodError 가 발생한다. 꼭 확인하고 최근 버전으로 해주자!!
그리고 SwaggerConfig를 설정한다.
@OpenAPIDefinition(
info = @Info(
title = "API 명세서",
description = "BlockGuard API 명세서",
version = "v2"
)
)
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
Server prodServer = new Server();
prodServer.setUrl("https://www.blockguard.shop");
prodServer.setDescription("AWS EC2 서버");
Server localServer = new Server();
localServer.setUrl("http://localhost:8080");
localServer.setDescription("Local server for testing");
return new OpenAPI()
.components(new Components()
.addSecurityItem(securityRequirement) .servers(List.of(localServer, prodServer));
}
@OpenAPIDefinition : 전체 API 문서의 이름, 설명, 버전 등을 정의public OpenAPI openAPI() : Swagger 문서의 서버 정보 및 JWT 인증 헤더 정보 등을 설정해준다.이렇게만 해줘도 기본 Swagger를 사용할 준비가 끝난다. 하지만 여기서 더 자세한 문서화를 위해 몇 가지를 추가해주도록 하겠다!!
JWT 토큰을 이용해서 회원가입, 로그인 로직을 사용하는 경우가 많을 것이다. 이때 헤더에 jwt 토큰을 포함하는 경우가 많은데, 스웨거에서 api 테스트를 할 때

사진에 보이는 'Authorize' 버튼을 누르면

이런 식으로 로그인 시 발급받았던 토큰을 입력할 수 있다! 그러면 jwt 토큰이 필요한 api도 편하게 테스트 가능~
@Bean
public OpenAPI openAPI() {
SecurityScheme apiKey = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.in(SecurityScheme.In.HEADER)
.name("Authorization")
.scheme("bearer")
.bearerFormat("JWT");
SecurityRequirement securityRequirement = new SecurityRequirement()
.addList("Bearer Token");
Server prodServer = new Server();
prodServer.setUrl("https://www.blockguard.shop");
prodServer.setDescription("AWS EC2 서버");
Server localServer = new Server();
localServer.setUrl("http://localhost:8080");
localServer.setDescription("Local server for testing");
return new OpenAPI()
.components(new Components().addSecuritySchemes("Bearer Token", apiKey))
.addSecurityItem(securityRequirement)
.servers(List.of(localServer, prodServer));
}
아까 코드에서 SecuritySchema 부분을 추가해주면 간단하게 설정 가능하다.
여기까지는 프로젝트에서 늘 사용하던 부분이다. 이제는 더 나아가보자~

이런식으로 성공 시 예시 응답뿐만 아니라 관련 에러 응답까지 표시되도록 하는 코드를 추가하는 방법을 작성해보자!
우선 ErrorCode와 같이 예외 시 발생하는 에러 코드들을 관리하는 클래스가 있을 것이다.!
나 같은 경우
@Getter
@AllArgsConstructor
public enum ErrorCode {
/// 4000 ~ : client error
DUPLICATED_EMAIL(HttpStatus.BAD_REQUEST, 4001, "이미 가입된 이메일입니다."),
INVALID_EMAIL(HttpStatus.BAD_REQUEST, 4002, "존재하지 않는 이메일입니다."),
INVALID_PASSWORD(HttpStatus.BAD_REQUEST, 4003, "비밀번호가 일치하지 않습니다."),
INVALID_TOKEN(HttpStatus.UNAUTHORIZED, 4004, "유효하지 않은 토큰입니다."),
// 5000~ : server error
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 5000, "서버 오류가 발생했습니다.");
private final HttpStatus status;
private final int code;
private final String msg;
}
이런식으로 에러 코드를 관리하고 있는데 현재 위에 코드는 로그인 관련 에러, 회원가입 관련 에러 그리고 공통 에러가 존재한다.
이 에러들을 적절한 API에 맞춰서 스웨거에 표시하기 위해
package com.blockguard.server.global.config.swagger;
import com.blockguard.server.global.common.codes.ErrorCode;
import lombok.Getter;
import java.util.LinkedHashSet;
import java.util.Set;
@Getter
public enum SwaggerResponseDescription {
REGISTER_FAIL(new LinkedHashSet<>(Set.of(
ErrorCode.DUPLICATED_EMAIL
))),
LOGIN_FAIL(new LinkedHashSet<>(Set.of(
ErrorCode.INVALID_EMAIL,
ErrorCode.INVALID_PASSWORD
)));
private final Set<ErrorCode> errorCodeList;
SwaggerResponseDescription(Set<ErrorCode> errorCodes) {
// 공통 에러 추가
errorCodes.addAll(Set.of(
ErrorCode.INVALID_TOKEN,
ErrorCode.INTERNAL_SERVER_ERROR
));
this.errorCodeList = errorCodes;
}
}
위와 같이 SwaggerResponseDescription 라는 클래스에 회원가입 관련 에러 해시셋, 로그인 관련 해시셋으로 묶어주었다.
즉, 각 api기능 별로 관련된 ErrorCode 집합을 정의한다.
그리고 API 명세 문서에 요청이나 응답 예시 데이터를 명시할 때 사용하는 Example 객체를 사용해서 ExampleHolder를 만들어준다.
@Getter
@Builder
public class ExampleHolder {
private Example holder;
private String name;
private int code;
}
위와 같이 사용하게 되면 스웨거 문서에 이름과 응답코드가 뜨게된다. 예시를 담는 DTO 라고 생각하면된다.
그리고 메서드별로 스웨거 예외 응답 예시를 주석처럼 지정해주기 위해서
CustomExceptionDescription 클래스를 만들어준다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomExceptionDescription {
SwaggerResponseDescription value();
}
api에
@Operation(summary = "회원가입")
@CustomExceptionDescription(SwaggerResponseDescription.REGISTER_FAIL)
@PostMapping("/register")
public ResponseEntity<BaseResponse<RegisterResponse>> register(@RequestBody RegisterRequest registerRequest) {
...
}
이런식으로 어노테이션이 달려있다.
그러면 최종으로 SwaggerConfig 를 아래와 같이 완성해주면된다.
@OpenAPIDefinition(
info = @Info(
title = "API 명세서",
description = "BlockGuard API 명세서",
version = "v2"
)
)
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
SecurityScheme apiKey = new SecurityScheme()
.type(SecurityScheme.Type.HTTP)
.in(SecurityScheme.In.HEADER)
.name("Authorization")
.scheme("bearer")
.bearerFormat("JWT");
SecurityRequirement securityRequirement = new SecurityRequirement()
.addList("Bearer Token");
Server prodServer = new Server();
prodServer.setUrl("https://www.blockguard.shop");
prodServer.setDescription("AWS EC2 서버");
Server localServer = new Server();
localServer.setUrl("http://localhost:8080");
localServer.setDescription("Local server for testing");
return new OpenAPI()
.components(new Components().addSecuritySchemes("Bearer Token", apiKey))
.addSecurityItem(securityRequirement)
.servers(List.of(localServer, prodServer));
}
@Bean
public OperationCustomizer customize() {
return (operation, handlerMethod) -> {
CustomExceptionDescription customExceptionDescription = handlerMethod.
getMethodAnnotation(CustomExceptionDescription.class);
if (customExceptionDescription != null) {
generateErrorCodeResponseExample(operation, customExceptionDescription.value());
}
return operation;
};
}
// SwaggerResponseDescription에서 ErrorCode에 대해 ExampleHodler 생성
private void generateErrorCodeResponseExample(Operation operation, SwaggerResponseDescription type) {
ApiResponses responses = operation.getResponses();
Set<ErrorCode> errorCodeList = type.getErrorCodeList();
Map<Integer, List<ExampleHolder>> statusWithExampleHolders =
errorCodeList.stream()
.map(
errorCode -> {
return ExampleHolder.builder()
.holder(
getSwaggerExample(errorCode))
.code(errorCode.getCode())
.name(errorCode.toString())
.build();
}
).collect(groupingBy(ExampleHolder::getCode));
addExamplesToResponses(responses, statusWithExampleHolders);
}
// 주어진 ErrorCode로부터 Swagger에 넣을 예시 객체 생성
private Example getSwaggerExample(ErrorCode errorCode) {
ErrorResponse errorResponse = ErrorResponse.of(errorCode);
Example example = new Example();
example.description(errorCode.getMsg());
example.setValue(errorResponse);
return example;
}
// 상태코드에 대해 Swagger의 ApiResponse 생성 & application/json 타입에 여라 개의 예시 추가
private void addExamplesToResponses(ApiResponses responses, Map<Integer, List<ExampleHolder>> statusWithExampleHolders) {
statusWithExampleHolders.forEach((status, holders) -> {
Content content = new Content();
MediaType mediaType = new MediaType();
ApiResponse apiResponse = new ApiResponse();
holders.forEach(holder -> mediaType.addExamples(holder.getName(), holder.getHolder()));
content.addMediaType("application/json", mediaType);
apiResponse.setContent(content);
responses.addApiResponse(status.toString(), apiResponse);
});
}
customize(): API 메서드에 @CustomExceptionDescription 어노테이션을 감지하여 예외 응답 예시를 스웨거 문서에 동적으로 추가
generateErrorCodeResponseExample(): SwaggerResponseDescription에 등록된 ErrorCode들을 기반으로 예시를 만들고 operation 객체에 상태코드별로 응답 추가
getSwaggerExample(): 하나의 ErrorCode를 Swagger에 넣을 수 있는 Example 객체로 변환하여 Json 형태로 표시해줌
addExamplesToResponses(): 같은 상태코드에 여러 개의 예시를 ApiResponse로 묶어서 스웨거 문서에 추가
즉, SpringDoc이 API 명세를 스캔할 때, OperationCustomizer가 작동 -> customize()에서 메서드에 붙은 @CustomExceptionDescription 어노테이션을 감지 -> generateErrorCodeResponseExample()에서 등록된 에러 코드를 기준으로 예시 목록 생성 -> getSwaggerExample()에서 각 상태 코드 별로 Example 객체 생성 -> 최종적으로 addExamplesToResponses()이 operation.getResponse()에 예시 추가
스웨거 자동화 관련 코드들은 !
이런 식으로 구성되어있다.