한줄 요약
- API 서버 같은 멀티 스레드 환경에서 OpenAPI Generator로 생성한 구현 코드에 사용자 토큰을 릴레이하고 싶다면 ApiClient를 요청마다 생성해야한다
문제 상황
- OpenAPI Specification 3는 HTTP API를 정의 하기 위한 명세이다. yaml 파일과 같은 형태로 작성하기 때문에 특정 언어에 종속되지 않는 API 스펙을 작성할 수 있다
- OpenAPI Generator는 이런 OAS로 작성된 문서로부터 API를 사용하는 클라이언트/서버 코드를 다양한 언어로 생성하기 위한 프로젝트이다
- 운영 중인 서버 하나에서 API를 관리하기 위해 OAS를 작성하고는 있었으나, 이를 통해 코드 생성은 하고 있지 않았다
- 이 서버에서는 불편한 점이 없었지만, 서버를 호출하는 Webflux API 게이트 웨이에서 매번 DTO를 또 만들어주고 하려니 여간 귀찮은게 아니었다
- 다행이도 OpenAPI Generatorrk Java WebClient 클라이언트 코드 생성을 지원하기 때문에 사용해보기로 했다.
- 대충 설정해서 코드 생성을 하면 아래와 같은 클라이언트가 생성된다
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", date = "2023-03-21T20:39:48.722754+09:00[Asia/Seoul]")
public class OrderApi {
private ApiClient apiClient;
public OrderApi() {
this(new ApiClient());
}
@Autowired
public OrderApi(ApiClient apiClient) {
this.apiClient = apiClient;
}
public ApiClient getApiClient() {
return apiClient;
}
public void setApiClient(ApiClient apiClient) {
this.apiClient = apiClient;
}
public Flux<Response> apiName(Request req) throws WebClientResponseException {
}
}
- 이때
apiName()
같은 메소드들 하나하나가 api에 대응된다
- 인증/인가를 위해 사용자의 jwt 토큰이 필요한 경우에 이를 어떻게 전달해야하는지 난감했다
인증/인가
- 약간의 뻘짓을 하다가 찾아보니 클라 구현체에서 api를 호출하기 위해 사용되는
ApiClient
객체에 토큰을 설정 해야한다는 것을 찾아냈다
- ApiClient는 2019년도부터 bearer 토큰을 아래 이슈에서 볼 수 있듯 지원한다
public class ApiClient extends JavaTimeFormatter {
...
private Map<String, Authentication> authentications;
...
public void setBearerToken(String bearerToken) {
for (Authentication auth : authentications.values()) {
if (auth instanceof HttpBearerAuth) {
((HttpBearerAuth) auth).setBearerToken(bearerToken);
return;
}
}
throw new RuntimeException("No Bearer authentication configured!");
}
...
}
- 간단하게 ApiClient를 빈으로 등록해서 사용하면 되나?라고 했다가…
authentications
가 쓰레드 세이프하지 않아 아차!했다. 여러 요청이 동시에 들어오는 웹서버, 더군다나 이벤트 루프 모델인 Webflux에서 어떻게 처리해야할지 고민이 됐다.
멀티 쓰레드 환경에서 OpenAPI Generator 클라이언트
- OpenAPI Generator로 생성된 Api 클래스들은 다음과 같은
ApiClient
객체 설정 방법을 제공한다
public OrderApi() {
this(new ApiClient());
}
@Autowired
public OrderApi(ApiClient apiClient) {
this.apiClient = apiClient;
}
public ApiClient getApiClient() {
return apiClient;
}
public void setApiClient(ApiClient apiClient) {
this.apiClient = apiClient;
}
- 뭔가 멀티 쓰레드 환경에서는 계속 ApiClient를 새로 생성하기를 원하는거 같은데… 확신이 서지 않았다. 그리고
ApiClient
를 매번 생성했을때 성능 문제가 우려되었다
- 다행이도
ApiClient
객체의 생성에서 가장 무거운 동작은 WebClient
객체를 생성하는 것이고, ApiClient
는 WebClient 객체를 주입 받는 생성자가 있다. WebClient
생성 이외에 생성자에서 하는 나머지 작업은 빈 해시맵 만들기 같은 가벼운 연산이었다.
public ApiClient(WebClient webClient, ObjectMapper mapper, DateFormat format) {
this(Optional.ofNullable(webClient).orElseGet(() ->buildWebClient(mapper.copy())), format);
}
private ApiClient(WebClient webClient, DateFormat format) {
this.webClient = webClient;
this.dateFormat = format;
this.init();
}
protected void init() {
authentications = new HashMap<String, Authentication>();
authentications.put("bearer", new HttpBearerAuth("bearer"));
authentications = Collections.unmodifiableMap(authentications);
}
- 이제
ApiClient
를 매번 만드는게 Best Practice라는 확신만 있으면 되는데... 이게 생각보다 찾기 힘들었다. 인터넷을 한참 뒤지다가… 결국 ApiClient
를 매번 생성하는 것이 추천하는 방법이라는 문구를 OpenAPI Generator 레포의 예시 readme 저 아래 구석에서 찾았다 ㅠㅠㅠ
- 서블릿 환경이었다면 ApiClient를 요청 스코프 빈으로 등록한다던가 해서 직접 객체화하는 코드 없이 처리할 수 있었겠으나, 리액티브 스택에서는 그게 어렵기 때문에 약간 고민이 되었다. 정 하려면
Reactor Context
를 사용해서 뭔가 해볼 수도 있을거 같긴한데, 괜히 복잡해지고 할거 같아서 그냥 생성자 호출해서 하기로 했다