반응형

스웨거란?

Swagger : Open Api Specification(OAS)를 위한 프레임 워크

FrontEnd와 BackEnd간 API 명세를 위해 사용한다고 생각하면 되며, 사용 시 페이지 하나에 기록되어있다.

하나의 협업 툴이라고 생각하면 되며, 기존 프로젝트는 Notion을 사용했었는데, 이번에는 Swagger를 채택하여 사용해보기로 했다.

의존성 추가

build.gradle 파일에 해당 dependency를 추가한다.

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'

 

필자의 경우 Could not find org.springdoc:springdoc-openai-starter-webmvc-ui:2.2.0.Required by root project와 같은 에러메세지를 당도하여, 곤혹을 치뤘는데

 

build.gradle repositories에 해당 maven을 추가해준다.

maven { url 'https://repo.spring.io/snapshot' }

 

2018년도까지 업데이트 되었던 springfox를 사용하여 swagger를 작성했지만, 이제는 spring-openapi 라이브러리를 불러와 swagger를 작성한다. (2020년도에 springfox가 업데이트 되어 병행하는 경우도 있다.)

공식문서는 https://springdoc.org를 참조하자.

 

application.yml

springdoc:
  packages-to-scan: {스캔할 경로}
  default-consumes-media-type: application/json;charset=UTF-8
  default-produces-media-type: application/json;charset=UTF-8
  swagger-ui:
    path: /
    disable-swagger-default-url: true
    display-request-duration: true
    operations-sorter: alpha

application.yml에 해당 코드를 추가한다.

pakages-to-scan에 해당하는 경로를 넣게되면 해당 경로를 상위폴더로 두고 하위폴더를 전부 탐색하니 생각하여 경로를 설정하도록 하자. 필자는 프로젝트 전체를 경로로 넣었다.

 

API가 RESTFUL하게 통신을 주고받기 때문에 json타입으로 기본 설정한다.

나머지는 기본 url을 사용할건지 등의 기본 설정이기에 해당 설정을 따른다.

 

Config 파일 추가

Bean에 등록하여 서버 실행 시 참조에 오류가 없도록한다.

import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.models.OpenAPI;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@OpenAPIDefinition(
        info = @Info(
                title = "LLM Service API",
                version = "1.0",
                description = "LLM Service API")
)
@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new io.swagger.v3.oas.models.info.Info()
                        .title("LLM Service API")
                        .version("1.0")
                        .description("LLM Service API"));
    }
}

@OpenAPIDefinition 어노테이션과 OpenAPI를 정의하여 Swagger의 제목과 버젼 설명을 명세한다.

 

이후 각 Controller, DTO, Entity, ResponseEntity(디폴트 응답 틀)에 설명을 추가한다.

 

Controller

@RestController
@RequiredArgsConstructor
@RequestMapping("/llm")
@Tag(name = "Chat-GPT(LLM) 서비스", description = "LLM 관련 API")
public class LLMController {
    private final LLMService llmService;
    @Operation(summary = "코딩테스트 문제 생성 API", description = "난이도(1~5), 알고리즘, 추가 사항을 기반으로 코딩 테스트를 생성하는 API")
    @PostMapping("/code-generating")
    public ResponseEntity<LLMResponseDTO.CodeGenerateResponse> codeGenerator(@RequestBody LLMRequestDTO.codeGeneratingInfo request) {
        return llmService.codeGenerator(request);
    }

    @PostMapping("/chat")
    public ResponseEntity<LLMResponseDTO> chat(@RequestBody LLMRequestDTO.chatMessage request) {
        return llmService.chat(request);
    }
}

 

DTO

@Builder
@Getter
@AllArgsConstructor
@Schema(name = "LLMResponseDTO", description = "LLM 응답 양식", title = "LLMResponseDTO [LLM 응답 양식]")
public class LLMResponseDTO {
    @Builder
    @Getter
    @AllArgsConstructor
    @NoArgsConstructor
    @Schema(name = "CodeGenerateResponse", description = "코딩테스트 생성 응답", title = "CodeGenerateResponse [코딩테스트 생성 응답]")
    public static class CodeGenerateResponse{
        @Schema(name = "title", description = "문제 제목", example = "혼자 남지 않기 위한 최적의 경로 찾기")
        private String title;
        @Schema(name = "algorithm", description = "알고리즘", example = "DP")
        private String algorithm;
        @Schema(name = "content", description = "문제 내용", example = "<h3>혼자 남지 않기 위한 최적의 경로 찾기</h3>\n\n<p>이 문제는 '혼자 남지 않기 위한 최적의 경로 찾기'를 목표로 합니다. 주어진 2차원 격자에서 특정 조건을 만족하며 목적지에 도달하기 위한 행동을 결정해야 합니다. 주어진 조건을 만족하는 함수를 작성하고, 격자를 탐색하는 중 최대한의 안전성을 확보해야 합니다.</p>\n\n<h4>문제 설명:</h4>\n<p>이 문제는 특정 시작점에서 목적지까지 '혼자 남지 않기' 위한 최적의 경로를 찾아야 합니다. 2차원 격자에서는 특정 위치로 이동할 때 1의 스탭이 소요되며, 각 위치에서 움직일 수 있는 최대 경로 수를 제한하여 주어질 것입니다. 주어진 조건하에서 최단 경로를 결정해야 하며, 경로에 존재하는 위험 요소를 피하면서 격자를 탐색해야 합니다.</p>\n\n<h4>문제 제한 사항:</h4>\n<ul>\n<li>격자 크기: M x N (1 ≤ M, N ≤ 100)</li>\n<li>시작점: 출발 위치는 (0,0)으로 고정되어 있습니다.</li>\n<li>목적지: 목적지는 항상 오른쪽 아래 모서리 (M-1, N-1)입니다.</li>\n<li>위험 요소는 격자 내 임의의 위치에 존재할 수 있으며, 이를 피하는 것이 중요합니다.</li>\n</ul>\n\n<h4>입력 예시:</h4>\n<pre><code>\n5 5\n0 0 0 0 0\n0 -1 0 -1 0\n0 0 0 -1 0\n0 -1 -1 0 -1\n0 0 0 0 0\n</code></pre>\n\n<h4>출력 예시:</h4>\n<pre><code>\n7\n</code></pre>\n\n<h4>입출력 예 설명:</h4>\n<p>주어진 격자에서 (0,0)에서 (4,4)까지 가장 적은 스탭으로 이동하기 위한 경로는 7스탭 입니다. 중간에 -1은 이동할 수 없는 위치이며, 다른 안전한 경로를 찾아야 합니다.</p>")
        private String content;
        @Schema(name = "difficulty", description = "난이도", example = "3")
        private String difficulty;
        @Schema(name = "additionalNotes", description = "추가 사항", example = "문제의 조건을 만족하는 최소의 시간 복잡도를 갖는 알고리즘을 작성해줘.(can be null)")
        private String additionalNotes;
        @Schema(name = "testCases", description = "테스트 케이스 목록(front 필요 없을 확률이 높음.)")
        private List<TestCaseSpec> testCases;

        @Builder
        @Getter
        @AllArgsConstructor
        @NoArgsConstructor
        @Schema(name = "TestCaseSpec", description = "테스트 케이스 명세", title = "TestCaseSpec [테스트 케이스 명세]")
        public static class TestCaseSpec {
            @JsonProperty("input")
            @Schema(name = "input", description = "입력값", example = "5 5\n0 0 0 0 0\n0 -1 0 -1 0\n0 0 0 -1 0\n0 -1 -1 0 -1\n0 0 0 0 0")
            private String input;
            @Schema(name = "expectedOutput", description = "예상 출력값", example = "7\n")
            @JsonProperty("expected_output")
            private String expectedOutput;
        }


        public static LLMResponseDTO.CodeGenerateResponse of(String jsonResponse) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                JsonNode rootNode = objectMapper.readTree(jsonResponse);

                JsonNode problemInfoNode = rootNode.path("problem_info").get(0);

                List<TestCaseSpec> testCases = objectMapper.readValue(
                        rootNode.path("test_cases").toString(),
                        objectMapper.getTypeFactory().constructCollectionType(List.class, TestCaseSpec.class)
                );
                return LLMResponseDTO.CodeGenerateResponse.builder()
                        .title(rootNode.path("title").asText())
                        .content(rootNode.path("content").asText())
                        .algorithm(problemInfoNode.path("algorithm").asText())
                        .difficulty(problemInfoNode.path("difficulty").asText())
                        .additionalNotes(problemInfoNode.path("additional_notes").asText())
                        .testCases(testCases)
                        .build();
            } catch (Exception e) {
                throw new RuntimeException("Failed to parse JSON response"+ e.getMessage(), e);
            }
    }

 

ResponseEntity

@Data
@AllArgsConstructor
@Schema(name = "ResponseEntity", description = "전체 API 응답", title = "ResponseEntity [전체 API 응답]")
public class ResponseEntity<T> {
    @Schema(name = "statusCode", description = "응답 코드", example = "200")
    private Integer statusCode;
    @Schema(name = "message", description = "응답 메시지", example = "OK")
    private String message;
    @Schema(name = "data", description = "응답 데이터")
    private T data;

    public static <T> ResponseEntity<T> success(T data) {
        return new ResponseEntity<>(HttpStatus.OK.value(), "OK", data);
    }

    public static <T> ResponseEntity<T> error(Integer statusCode, String message, T data) {
        return new ResponseEntity<>(statusCode, message, data);
    }
}

 

이후 서버를 실행시킨 뒤, localhost:8080/swagger-ui/index.html# 로 접속한다.

 

메인 페이지

 

상세 페이지 Request
Response

 

이렇게 정의하여, 프론트에게 API 명세를 껴안아 주면 그때부터 완전한 프로젝트 협업의 시작이 되겠다.

 

반응형

+ Recent posts