해당 글은 Notion에서 작성되어진 글 입니다.

https://hyeon0j0.notion.site/Swagger-6506f70d58ba46a79f8841a9e413b7bf

 

Swagger 사용법 그리고 커스터마이징

🗒️ 노트 주제

hyeon0j0.notion.site

🗒️ 노트 주제


  • Swagger란 무엇인가??
  • Swagger 사용방법
  • Swagger 커스터마이징 하는 방법

🗝️ 핵심


Swagger란 무엇인가?


서버는 클라이언트와 통신을 하기 위해서 인터페이스 문서를 작성을 하고, 이를 공유하면서 API 규격을 맞춰 나간다.

기존에 팀에서는 Excel을 공유하여 문서로 관리하고, 테스트는 Postman을 통해서 테스트하고 클라이언트에게 테스트 방법을 안내할 때에는 Postman의 Export 기능을 통해 JSON파일로 테스트 명세를 전달하였다.

그러나 Swagger의 도입으로 인해 서버가 정상 기동만 되어있으면 웹상에서 바로 확인 및 테스트가 가능하게 제공할 수 있다.

그리고 Swagger에서 제공하는 api-docs의 JSON 파일을 이용하면 클라이언트 측에서는 Swagger Build를 통해 객체 생성 및 호출 함수까지 자동으로 완성시켜주어 개발 속도가 향상된다.

그럼 Swagger란 정확하게 무엇인가?

Swagger 는 REST API를 설계, 구축, 문서화 및 사용하는 데 도움이 될 수 있는 OpenAPI 사양을 기반으로 구축된 오픈 소스 도구 세트입니다.

About Swagger Specification | Documentation | Swagger

기존의 명칭은 springfox swagger 라는 명칭으로 되어 있다가, 어느부터 springdoc-openapi로 변경이 되었고, springfox swagger의 경우에는 특정 버전만 springboot에서 정상 동작하는 오류가 있었는데 springdoc-openapi로 변경됨에 따라서는 호환성 이슈는 사라진 것 같다. 프로세스가 실행중일때 excute 버튼이 비활성화 되는 기능도 추가됨

Swagger가 제공해주는 도구 중에서 Swagger-ui 툴을 통해서 웹상에서 표출하는 방법에 대해서 작성해보겠다.

Swagger 사용 방법


Swagger의 사용방법은 Java Spring Boot + Gradle을 기준으로 설명하겠다.

    1. Gradle 설정

Swagger-ui 버전정보는 확인을 통해 설정 할 것

  1.  
  2. implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web:2.7.0' testImplementation 'org.springframework.boot:spring-boot-starter-test' /* SWAGGER */ implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.9'
  3. API 설명 작성
    • 애노테이션 방법
    • @OpenAPIDefinition(info = @Info(title = "Hustar API 명세서", description = "API 명세서", version = "v1", contact = @Contact(name = "taehyeonkim", email = "taehyeonkim@kakao.com") ) )
    • Java Config 파일 방법

두가지중 한가지 방법으로 선택

  •  
  • @Configuration public class SwaggerConfig { @Bean public OpenAPI openAPI() { Info info = new Info().title("Hustar API 명세서").version("v1") .description("API 명세서") .contact(new Contact().name("taehyeonkim").email("taehyeonkim@kakao.com")) .license(new License().name("Apache License Version 2.0").url("http://www.apache.org/licenses/LICENSE-2.0.html")); return new OpenAPI() .components(new Components()) .info(info); } }
    1. Controller, Vo 에 설명 작성
       @Getter
       @Setter
       @ToString
       public class TestParmVo {
           @Schema(description = "이름", example = "김태현")
           String name;
      
           @Schema(description = "인사말", example = "안녕하세요")
           String message;
       }

Swagger Annotaion 추가 설명은 공식 wiki에서 확인

    1.  

Annotations · swagger-api/swagger-core Wiki

  1.  
  2. // Controller @RestController public class TestController { @Operation(summary = "이름을 출력하는 Controller", description = "입력 파라미터 이름 출력") @PostMapping("/test") public TestResultVo test(@RequestBody TestParmVo testParmVo){ return TestResultVo.builder() .result(testParmVo.getName()) .build(); } @Operation(summary = "메세지를 출력하는 Controller", description = "입력 파라미터 메세지 출력") @PostMapping("/test2") public TestResultVo test2(@RequestBody TestParmVo testParmVo){ return TestResultVo.builder() .result(testParmVo.getMessage()) .build(); } }
  3. 로컬 서버 기동 후 localhost:8080/swagger-ui.html 접속
  4. 스크린샷 2022-07-02 시간: 10.44.51.png
  5. 확인설명 작성한 부분 위치결과 확인
  6. 결과 확인
  7. 설명 작성한 부분 위치

Swagger 커스터마이징


OpenAPI이므로 Swagger에 로고도 바꾸고 색상도 바꾸고 사용자의 입맛에 바꿀수가 있는데 그중 로고와 헤더 색상 바꾸는 것만 설명을 하고자 한다.

  • GIt Issue에 있는 css path를 통한 커스터마이징

https://github.com/springdoc/springdoc-openapi/issues/737

해당 방식으로 하게 되면 Swagger Path에 해당 Controller를 예외 처리 해줘야 되며, 내가 바꾸고 싶은 부분 마다 Endpoint를 만드는 느낌이라 개인적으로는 비추

  • Swagger UI Resource를 통한 직접 커스터마이징
  1. swagger-ui resource 다운또는 Gradle에서 Import된 Resource를 사용
  2. Untitled
  3. https://github.com/swagger-api/swagger-ui 에서 dist 폴더 다운
  1. 인터넷 탭 아이콘 , 이름 변경
  2. <!-- index.html --> <!-- HTML for static distribution bundle build --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> **<!-- 해당 부분 이름 변경 --> <title>Hustar Swagger UI</title> <!-- 해당 부분 이름 변경 끝 -->** <link rel="stylesheet" type="text/css" href="./swagger-ui.css" /> <link rel="stylesheet" type="text/css" href="index.css" /> **<!-- 해당 부분 아이콘 리소스 변경 --> <link rel="icon" type="image/png" href="./hustar_32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="./hustar_32x32.png" sizes="16x16" /> <!-- 해당 부분 아이콘 리소스 변경 끝 -->** </head> <body> <div id="swagger-ui"></div> <script src="./swagger-ui-bundle.js" charset="UTF-8"> </script> <script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script> <script src="./swagger-initializer.js" charset="UTF-8"> </script> </body> </html>
  1. Swagger-ui 왼쪽 상단 로고 변경
    1. 적당한 이미지 로고(배경이 없는 png 파일 추천) 준비해서 Resource폴더 안에 넣어놓기
    2. Untitled
    3. swagger-ui-standalone-preset.js 수정 (배너 이미지 변경)
       // 변경
      
       return p.createElement("div", {className: "topbar"}, p.createElement("div", {className: "wrapper"}, p.createElement("div", {className: "topbar-wrapper"}, p.createElement(o, null, p.createElement("img", {
                                   height: "40",
                                   // TODO :: 바꿈
                                   src: "./Hustar.png",
                                   // TODO :: 바꿈
                                   alt: "Hustar - Swagger UI"
                               })), p.createElement("form", {
                                   className: "download-url-wrapper",
                                   onSubmit: f
                               }, b()(c).call(c, (function (t, e) {
                                   return (0, p.cloneElement)(t, {key: e})
                               }))))))
    4. // 기존 return p.createElement("div", {className: "topbar"}, p.createElement("div", {className: "wrapper"}, p.createElement("div", {className: "topbar-wrapper"}, p.createElement(o, null, p.createElement("img", { height: "40", src: j, alt: "Swagger UI" })), p.createElement("form", { className: "download-url-wrapper", onSubmit: f }, b()(c).call(c, (function (t, e) { return (0, p.cloneElement)(t, {key: e}) }))))))
4. 상단 배너 Back-ground 색상 변경

Index.css 파일 수정

```css
/** index.css **/

.swagger-ui .topbar {
    background-color: lightgray;
}
```

1. 결과 확인

    ![Untitled](Swagger%20%E1%84%89%E1%85%A1%E1%84%8B%E1%85%AD%E1%86%BC%E1%84%87%E1%85%A5%E1%86%B8%20%E1%84%80%E1%85%B3%E1%84%85%E1%85%B5%E1%84%80%E1%85%A9%20%E1%84%8F%E1%85%A5%E1%84%89%E1%85%B3%E1%84%90%E1%85%A5%E1%84%86%E1%85%A1%E1%84%8B%E1%85%B5%E1%84%8C%E1%85%B5%E1%86%BC%20839b56b3485a41b489336dd45b2c9321/Untitled%202.png)

 

소스코드

https://github.com/taehyeon3549/SwaggerCustomizing

 

GitHub - taehyeon3549/SwaggerCustomizing: Swagger 문서를 개인의 입맛에 맞게 수정 변경

Swagger 문서를 개인의 입맛에 맞게 수정 변경. Contribute to taehyeon3549/SwaggerCustomizing development by creating an account on GitHub.

github.com

 

🔗 참고 노트


Custom RestTemplateBuilder

  1. Builder 패턴을 활용한 RestTemplateBuilder
  2. Spring의 IoC를 활용한 싱글톤 RestTemplate
  3. RestTemplate의 Customizing
  4. RestTemplateBuilder 상속을 통한 기능 확장
[ 구조 ] 

TaehyeonRestTemple = RestTemplateBuilder + Taehyeon's Function    // Builder-pattern
                              |
                              |- HttpClient
                              |     ㄴ Connection Pool
                              |- HttpComponentsClientHttpRequestFactory
                              |     ㄴ Time Out
                              ㄴ CustomRestTemplateCustomizer
                                    ㄴ Logging                                    

[클래스 구조] TaehyeonRestTemplateBuilder 구조

Example

@Autowired
private TaehyeonRestTempleBuilder taehyeonRestTempleBuilder;


/** GET 요청 예시(String return) **/
String result = taehyeonRestTempleBuilder
                .requestUriAndPath(DATA_BASE_URL
                        , "/VilageFcstInfoService_2.0/getVilageFcst"
                        , new LinkedMultiValueMap<>() {{
                            add("serviceKey", SERVICE_KEY);
                            add("pageNo", "1");
                            add("dataType", "JSON");
                            add("base_date", "20220207");
                            add("base_time", "1200");
                            add("nx", "57");
                            add("ny", "127");
                        }})
                .getForObject();

/** GET 요청 예시 **/
ResponseEntity<Object> result = taehyeonRestTempleBuilder
                .headers(new HashMap<>(){
                    {
                        put("X-Auth-Token", authKey);
                    }
                })
                .requestUriAndPath(crowdworksDefaultUrl
                        , "/project/output"
                        , new LinkedMultiValueMap<>() {{
                            add("queryingField", "");
                            add("queryingWord", "");
                            add("dataStatus", "");
                            add("edited", "");
                            add("rangingField", "");
                            add("page", String.valueOf(page));
                        }})
                .get();


/** POST 요청 예시 **/
ResponseEntity<Object> result = taehyeonRestTempleBuilder
                .contentType(MediaType.APPLICATION_JSON)
                .body(loginInfo)
                .requestUriAndPath(apiUrl, loginpath, null)
                .post();


/** PATCH 요청 예시 **/
ResponseEntity<Object> result = taehyeonRestTempleBuilder
                .headers(new HashMap<>(){{
                    put("X-Auth-Token", authKey);
                }})
                .requestUriAndPath(apiUrl, authPath, null)
                .patch();

개발 목표

  • 비즈니스 로직에서 구현하던 RestTemplate 생성과 처리 부분을 분리
  • 추후 연계 시스템 개발에서도 해당 모듈을 사용하여 단순히 호출만을 통한 처리를 할 수 있게끔 개발
  • Spring과 디자인 패턴을 공부하고 있는 것을 복습하며 실적용을 노림
  • RestTemplate의 ConnectionPool 설정을 통해 3-Hankshake의 지연을 줄임
    (연계 서버이므로 API 호출 서버가 keep-alive 상태임을 가정 )
  • 기존의 Resttemplate 과 Custom한 TaehyeonRestTemplate을 공존하여 Client의 선택에 따라 사용할 수 있도록 개발
  • jar 라이브러리화 하여 추후 연계 서버 개발시 개발 시간을 단축
  • 회사 연계 공통 모듈로 사용할 수 있게??? :D


[개발 Note]

  • 2022-02-22

    • 프로젝트 Init
    • Builder patter 초기 동작 코드 작성
    • Get 요청시의 parameter를 명시적으로 주입하기 위해 MultiValueMap 과 UriComponentsBuilder를 활용하여 요청 URI를 생성
    • 모듈 자체 Exception 생성 (수정 보완필요)
  • 2022-02-23

    • RestTemplate이 Connection Pool이 없이 요청할때마다 3-HandShack를 진행한다는 것을 공부, 이에 따라 Connection Pool을 설정하여 오버헤드를 줄임
    • application.properties 를 통한 Connection Pool과 TimeOut 설정을 할 수 있게 설정
    • 초기의 TaehyeonRestTemplateBuilder의 코드에 Connection Pool과 TimeOut 설정을 하여 Bean 생성을 하려고 하였으나 ' RestTemplateBuilder를 사용한 RestTemplate 설정 방법 : https://www.baeldung.com/spring-rest-template-builder ' 을 보고 Customizing Logging을 추가 및 각 기능을 분리 시킨 Bean으로 생성하여 주입되어 Application Context가 자동 관리 되도록 함
    • RestTempleBuilder의 생성자에 CustomRestTemplateCustomizer가 parm으로 주입되는데 이에 따른 TaehyeonRestTemplateBuilder를 RestTemplateBuilder를 상속 받아 기능을 확장 시키는 방향으로 전환
    • (중요!!) 상속받은 Builder를 통한 build를 시키면 return이 super의 RestTemplate 이고, sub의 TaehyeonRestTempleBuilder로 casting이 안됨.
      해당 문제는 Builder 형태로 생성하고 마지막에 Casting을 하는 것이 아닌, Casting 한 Builder를 생성하고 생성한 객체에서 추가적인 작업을 진행
      (때문에 알고있던 Upcasting과 DownCasting 다시 공부....)
    • 새롭게 만들어낸 TaehyeonRestTemplate과 기존의 RestTemplate을 공용으로 사용할 수 있도록 2가지 type의 Bean 생성하는 방향으로 생각중
    • 테스트 코드 추가
  • 2022-02-25

    • Default RestTemplate Bean 추가 하여 Client가 선택적으로 사용할 수 있게 수정
    • TestCode에 기존 방식과 TaehyeonRestTemplate 방식 비교 코드 추가


[개발 이슈]

  1. RestTemplate Get 요청시 String 으로 parameter를 주입시 자동 Encoding 처리 되어 요청 [해결]

parmeter 를 URI로 주입하여 Encoding 되지 않게 처리

restTemplate.getForObject(new URI(URLDecoder.decode(requestUri.toString(), "UTF-8")), String.class);
  1. 상속받은 Builder를 통한 build를 시키면 return이 super의 RestTemplate 이고, sub의 TaehyeonRestTempleBuilder로 casting이 안됨 [해결]

Casting 한 Builder를 생성하고 생성한 객체에서 추가적인 작업을 진행

//불가능
TaehyeonRestTempleBuilder taehyeonRestTempleBuilder = (TaehyeonRestTempleBuilder)new TaehyeonRestTempleBuilder(customRestTemplateCustomizer())
        .requestFactory(() -> new BufferingClientHttpRequestFactory(factory))
        .additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")));

// 가능
TaehyeonRestTempleBuilder taehyeonRestTempleBuilder = new TaehyeonRestTempleBuilder(customRestTemplateCustomizer());

taehyeonRestTempleBuilder.requestFactory(() -> new BufferingClientHttpRequestFactory(factory))
        .additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")));
  1. TaehyeonRestTempleBuilder 는 Builder인가 RestTemplate 인가.....?

  2. RestTemple를 통해 Get 요청시 MessageConverter 오류**[해결]**

  • TaehyeonRestTemplateBuilder은 "additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")));" 만으로 Response Message Converting이 정상 이루어 졌지만, 기본 RestTemplateBuilder에도 똑같이 하였으나 Converting 오류 발생

혹시 몰라 TaehyeonRestTemplate 에도 MessageCoverter 설정 추가

// 오류 메세지
org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class java.lang.Object] and content type [application/json;charset=UTF-8]

// 해결 코드

RestTemplate restTemplate1 =restTemplateBuilder
.additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")))
.build(); // restemplate 생성

List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));  //converter 설정
messageConverters.add(converter);  

restTemplate1.setMessageConverters(messageConverters);   // messageConverter 직접 추가

 

* 원문과 예제 코드는 Git에 있습니다.

https://github.com/taehyeon3549/RestTemplateBuilder

 

GitHub - taehyeon3549/RestTemplateBuilder: Builder 패턴을 활용한 RestTemplateBuilder

Builder 패턴을 활용한 RestTemplateBuilder. Contribute to taehyeon3549/RestTemplateBuilder development by creating an account on GitHub.

github.com

 

+ Recent posts