[spring] swagger와 관련된 설정들
프로젝트에서 Swagger을 이용하면서 알게 된 설정들을 정리한다.
https://github.com/blaxsior/Team6-AwesomeOrange-BE
GitHub - blaxsior/Team6-AwesomeOrange-BE: 현대자동차그룹 소프티어 부트캠프 6조 어썸오렌지 팀 프로젝트
현대자동차그룹 소프티어 부트캠프 6조 어썸오렌지 팀 프로젝트 (fork). Contribute to blaxsior/Team6-AwesomeOrange-BE development by creating an account on GitHub.
github.com
Javadoc 함께 사용하기
https://springdoc.org/#javadoc-support
나는 여러 사람과 협업하는데 있어서 주석의 역할이 상당히 중요하다고 생각한다. 다른 사람들은 나의 생각을 그대로 읽을 수 없기 때문에 코드의 의도를 문서화해두지 않으면 10년 후의 나, 또는 내 코드를 유지보수하게 될 다른 팀원들이 나의 코드를 이해하는 과정에서 오해가 발생할 여지가 있으며, 이는 팀 전체 생산성 저하로 이어질 수 있기 때문이다.
swagger 주석은 컨트롤러 또는 dto 등 사용자에게 노출하는 부분에서 많이 사용한다. 그런데, swagger 방식의 주석은 어노테이션 내부에 작성되기 때문에 주석의 내용을 보려면 일반적인 주석과는 달리 해당 클래스 정의를 찾아가야 한다. 개발 중 이와 같은 불편함을 방지하기 위해 일반 주석과 Swagger 어노테이션을 동시에 작성할 수도 있겠지만, 동일한 메시지가 중복되므로 코드도 지저분해지고 둘 사이의 동기화도 귀찮아진다.
/**
* 오늘의 인터렉션에 참여한다. 서버 시간 기준으로 참여를 정하며, 당일 중복 참여는 불가능하다.
* @param eventId 이벤트의 id
*/
@Operation(summary = "오늘의 인터렉션 이벤트에 참여한다.", description = "오늘의 인터렉션 이벤트에 참여한다. 서버 시간 기준으로 참여를 정하며, 당일 중복 참여는 불가능하다.", responses = {
@ApiResponse(responseCode = "200", description = "이벤트 참여 기록 획득"),
@ApiResponse(responseCode = "404", description = "이벤트를 찾을 수 없음", content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
@ApiResponse(responseCode = "409", description = "이미 이벤트에 참여함", content = @Content(schema = @Schema(implementation = ErrorResponse.class))),
})
@PostMapping("/{eventId}/participation")
public ResponseEntity<Void> participateDailyEvent(
@PathVariable ("eventId") String eventId,
@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo
) {
epService.participateDaily(eventId, userInfo.getUserId());
return ResponseEntity.ok().build();
}
이를 방지하기 위해 간단한 Javadoc 주석과 Swagger을 함께 사용할 수는 없을까? 해답은 공식 문서에 나와 있다.
springdoc-openapi can introspect Javadoc annotations and comments:
|
위 내용을 요약하면, description, return, dto 필드에 대한 설명을 주석으로 대체할 수 있다. javadoc 지원을 위해서는 아래 의존성을 설치한다. 버전은 공식 문서에 언급된 최신 버전을 설치하자.
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'
annotationProcessor 'com.github.therapi:therapi-runtime-javadoc-scribe:0.15.0'
implementation 'com.github.therapi:therapi-runtime-javadoc:0.15.0'
의존성 설치 이후에는 주석에 작성한 내용이 swagger에 반영된다.
public class EventDto {
/**
* HD000000_000 형식으로 구성된 id 값. 직접 설정할 필요 없음.
*/
private String eventId;
/**
* 이벤트의 이름
*/
private String name;
/**
* 이벤트에 대한 설명
*/
private String description;
/**
* 이벤트 시작 시간
*/
@NotNull
private Instant startTime;
/**
* 이벤트 종료 시간
*/
private Instant endTime;
}
태그 이름 순서로 정렬
프론트엔드 개발자 분들과 API를 연동하던 중 태그 / API들이 너무 난잡하게 분포되어 있어 swagger 문서에 어떤 API가 있는지 한 번에 알기 어렵다는 피드백이 나왔다. Admin 관련 기능들을 AdminAuthController, AdminCommentController 등으로 나눠 각 도메인 별로 나눴는데, swagger 문서상에서 API가 뒤죽박죽 분포하고 있는 것이 문제였다.
공식 문서를 찾아보니 알파벳 순서, 메서드 순서 등으로 정렬할 수 있는 옵션이 있다는걸 알게 되었다. 아래 옵션을 application.yml에 추가하면 된다.
springdoc:
swagger-ui:
operations-sorter: alpha
tags-sorter: alpha
위 설정을 통해 API 목록을 알파벳 순서로 정렬, 프론트엔드 개발자들이 보다 편하게 API 목록을 볼 수 있게 했다.
특정 파라미터 노출 제외하기
custom ArgumentResolver나 Request / Response 객체 등 프론트엔드 개발자 입장에서 API를 사용하는데 필요하지 않은 파라미터가 존재할 수 있다. 이들을 숨길 때는 @Parameter(hidden = true)을 붙인다.
public ResponseEntity<Void> participateDailyEvent(
@PathVariable ("eventId") String eventId,
@Parameter(hidden = true) @EventUserAnnotation EventUserInfo userInfo
) {
epService.participateDaily(eventId, userInfo.getUserId());
return ResponseEntity.ok().build();
}
과거에는 @ApiIgnore 어노테이션을 사용했다고 하지만, Spring Doc 버전 업데이트에 따라 swagger 상에서 숨김처리하는 옵션이 @Operation, @Parameter으로 이동했고, 기존 어노테이션은 최신 버전에서는 사용할 수 없다. ( 아예 없음 )
Admin, User 인증 구분하기
프로젝트에서 대부분의 API에는 JWT 인증이 필요했다. 이때 API을 사용하는 사용자는 (1) 이벤트 유저, (2) 관리자 유저로 총 2개로 구성되어 있으므로, swagger 상에서 이 둘에 대한 인증을 구분하여 표현함으로써 팀 내 개발자들이 보다 편리하게 각 사용자를 테스트할 수 있도록 구성하고 싶었다.
인터넷 상에 swagger v3에 대한 여러 문서가 있었는데, spring doc 라이브러리를 이용하다 보니 addSecuritySchemes에 등록한 인증 필요 정보가 Available authorization에 전달되며, 메서드 이름처럼 여러 인증 필요 정보를 등록할 수 있다는 것을 알게 되었다.
코드는 다음과 같다.
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.components(
new Components()
// 일반화 할 수 있는 방법?
.addSecuritySchemes(AuthConst.ADMIN_AUTH, createAPIKeyScheme())
.addSecuritySchemes(AuthConst.EVENT_USER_AUTH, createAPIKeyScheme())
)
.info(apiInfo());
}
private Info apiInfo() {
return new Info()
.title("Orange BE")
.description("Awesome Orange Back-End REST API")
.version("1.0.0");
}
private SecurityScheme createAPIKeyScheme() {
return new SecurityScheme().type(SecurityScheme.Type.HTTP)
.bearerFormat("JWT")
.scheme("bearer");
}
}
addSecuritySchemes을 2번 호출, 어드민과 이벤트 유저에 대한 인증을 분리했다.
@SecurityRequirement(name = AuthConst.ADMIN_AUTH)
@PostMapping("{eventId}/draw")
public ResponseEntity<Void> drawEvent(@PathVariable("eventId") String eventId) {
drawEventService.draw(eventId);
return ResponseEntity.ok().build();
}
인증이 필요한 API를 매핑할 때는 @SecurityRequirement을 대상 메서드 또는 클래스에 붙인다. name 속성과 addSecuritySchemes에서 전달한 key 값을 동일하게 매칭시키면 해당 API에 대해 인증이 동작하게 된다.
결과적으로, 두 종류 사용자에 대한 인증을 별개로 처리할 수 있게 되었다. 이를 통해 두 종류의 유저를 번갈아가며 테스트하기 위해 인증 정보를 매번 갱신할 필요가 없어졌다.
참고로 @SecurityRequirement은 다른 어노테이션 위에 붙어있어도 인식된다. 나는 이런 특징을 이용해서 어노테이션 기반 커스텀 인증 시스템과 결합하여 다음과 같은 어노테이션을 만들어 사용했었다.
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@SecurityRequirement(name = AuthConst.ADMIN_AUTH)
@Auth({AuthRole.admin})
public @interface AdminAuth {
}
swagger 도입 초기에는 요청에 필요한 데이터와 응답, 적당한 설명이 있다면 충분할 것이라고 생각했지만, 다른 사람들은 나의 의도를 알 수 없기 때문에 생략된 정보에 의해 고통을 받을 수 있다. 예외 코드는 뭐고 예외 메시지의 타입은 또 어떤지 구체적으로 명시해두지 않으면 팀원들은 알 수 없기 때문이다. 구현과 직접적으로는 관계가 없더라도, API 문서의 순서로 인해 팀원의 오해가 생길 수도 있다는 것도 경험했다.
내가 구현한 API라도 오랜 기간이 지난 후 특정 파라미터가 필요한지, 인증은 필요한지 등 세부적인 의도와 생각을 이해하는 것은 매우 어렵다. 나 자신도 어려운데, 하물며 다른 팀원들은 내 의도를 이해하는게 얼마나 어려울까? 이런 생각을 시작으로, 미래의 누군가가 나의 코드를 이해하려면 내 생각을 정리된 문서 형태로 관리해야 한다는 사실을 다시금 깨닫게 되었다. 앞으로도 팀 내 의도를 명확하게 표현함으로써, 미묘한 생각 차이로 인한 생산성 저하와 갈등을 사전에 방지할 수 있도록 노력해야겠다.