PROJECT/Sunny Braille

[Sunny Braille] Swagger로 더러워진 내 컨트롤러 코드 인공호흡: 인터페이스로 스웨거 코드 분리하는 방법

깃짱 2024. 3. 4. 11:00
반응형
반응형

 

 

 

시각장애인을 위한 빠르고 정확한 수학교재 인공지능 점자 변환 서비스, Sunny Braille

어떤 교재든 텍스트 뿐만 아니라 수식도 점자로 변환해주는 교육용 AI 점역 소프트웨어

 

 

 

해바라기 팀 소개 영상 https://www.youtube.com/watch?v=cuYIEpQx2po&t=8s

 

Sunny Braille 소스 코드https://github.com/sunnybraille

 

시각장애인을 위한 빠르고 정확한 수학교재 인공지능 점자 변환 서비스, Sunny Braille

어떤 교재든 텍스트 뿐만 아니라 수식도 점자로 변환해주는 교육용 AI 점역 소프트웨어. 시각장애인을 위한 빠르고 정확한 수학교재 인공지능 점자 변환 서비스, Sunny Braille has 4 repositories available

github.com

 

 

관련 PR ⇒ https://github.com/Sunflower-yonsei/sunflower-server/pull/78

 

refactor: Swagger 코드를 인터페이스로 분리 by gitchannn · Pull Request #78 · Sunflower-yonsei/sunflower-server

주요 변경사항 관련 이슈 closes #63 ✔️ Squash & Merge 방식으로 병합해 주세요!

github.com

 

💋 Swagger 적용으로 복잡해진 Controller,,,

 

프로젝트 도중 클라이언트와 API를 문서화하기 위해서 스웨거를 적용했다. 그랬더니 컨트롤러가 너무 복잡해서 읽기 어려워졌다.

Swagger와 관련된 코드와 웹 endpoint, request parameter, path variable을 정의하는 코드가 모두 섞여있어서 정말 보기 안좋다

 

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import sunflower.server.api.response.BrfFileQueryResponse;
import sunflower.server.api.response.PdfRegisterResponse;
import sunflower.server.api.response.TranslationStatusResponse;
import sunflower.server.application.TranslationService;
import sunflower.server.application.dto.TranslationStatusDto;
import sunflower.server.exception.FileEmptyException;

import java.net.URI;

import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;

@RequiredArgsConstructor
@RestController
@RequestMapping("/translations")
@Tag(name = "점자 번역 API", description = "Sunny Braille 핵심 기능으로, 점역 작업을 담당합니다.")
public class TranslationApiController {

	private final TranslationService translationService;

	@Operation(summary = "PDF 점자 번역 요청 API", description = "점역 작업은 서버에서 비동기적으로 처리되며, id만 즉시 반환됩니다.")
	@ApiResponse(responseCode = "201", description = "PDF가 성공적으로 업로드되었습니다.",
			headers =
			@io.swagger.v3.oas.annotations.headers.Header(
					name = "Location",
					description = "Translations id(해바라기에서 제공하는 id)",
					schema = @Schema(type = "long"),
					required = true
			))
	@PostMapping(consumes = MULTIPART_FORM_DATA_VALUE)
	public ResponseEntity<PdfRegisterResponse> registerPdf(
			@Parameter(name = "file", description = "점역할 pdf 파일", required = true,
					example = "쎈_3124번.pdf", schema = @Schema(type = "file"))
			@RequestPart("file") MultipartFile file
	) {
		if (file.isEmpty()) {
			throw new FileEmptyException(1, "빈 파일입니다.");
		}

		final Long id = translationService.register(file);

		return ResponseEntity
				.created(URI.create("/translations/" + id))
				.body(PdfRegisterResponse.from(file.getOriginalFilename()));
	}

	@Operation(summary = "점역 상황 체크 API", description = "id와 함께 요청하면 점역 진행 상황을 반환하며, 클라이언트의 progress bar 표시를 위해 사용해 주세요.")
	@ApiResponse(responseCode = "200", description = "점역 진행 상황을 반환합니다.",
			content = {
					@Content(mediaType = "application/json", schema = @Schema(implementation = TranslationStatusResponse.class))
			})
	@GetMapping("/{id}/status")
	public ResponseEntity<TranslationStatusResponse> checkStatus(
			@Parameter(description = "Translations id", required = true) @PathVariable("id") Long id) {
		final TranslationStatusDto dto = translationService.status(id);
		return ResponseEntity.ok(TranslationStatusResponse.from(dto));
	}

	@Operation(summary = "점역 결과 BRF 파일 반환 API", description = "id와 함께 요청하면 점역 결과를 반환합니다.")
	@ApiResponse(responseCode = "200", description = "점역된 결과를 반환합니다..",
			content = {
					@Content(mediaType = "application/json", schema = @Schema(implementation = BrfFileQueryResponse.class))
			})
	@GetMapping("/{id}")
	public ResponseEntity<BrfFileQueryResponse> queryBrfFile(
			@Parameter(description = "Translations id", required = true) @PathVariable("id") Long id) {
		final String brfContent = translationService.findBrfFileById(id);
		final BrfFileQueryResponse response = BrfFileQueryResponse.from(id, brfContent);
		return ResponseEntity.ok(response);
	}
}

 

 

💋 Swagger 관련 코드를 인터페이스로 분리!

 

인터페이스로 분리해도 여전히 어노테이션은 유지하면서 컨트롤러의 코드를 깔끔하게 수정할 수 있다.

 

TranslationApiControllerDocs 인터페이스

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import sunflower.server.api.response.BrfFileQueryResponse;
import sunflower.server.api.response.PdfRegisterResponse;
import sunflower.server.api.response.TranslationStatusResponse;

@Tag(name = "점자 번역 API", description = "Sunny Braille 핵심 기능으로, 점역 작업을 담당합니다.")
public interface TranslationApiControllerDocs {

	@Operation(summary = "PDF 점자 번역 요청 API", description = "점역 작업은 서버에서 비동기적으로 처리되며, id만 즉시 반환됩니다.")
	@ApiResponse(responseCode = "201", description = "PDF가 성공적으로 업로드되었습니다.",
			headers =
			@io.swagger.v3.oas.annotations.headers.Header(
					name = "Location",
					description = "Translations id(해바라기에서 제공하는 id)",
					schema = @Schema(type = "long"),
					required = true
			))
	ResponseEntity<PdfRegisterResponse> registerPdf(
			@Parameter(name = "file", description = "점역할 pdf 파일", required = true,
					example = "쎈_3124번.pdf", schema = @Schema(type = "file"))
			@RequestPart("file") MultipartFile file
	);

	@Operation(summary = "점역 상황 체크 API", description = "id와 함께 요청하면 점역 진행 상황을 반환하며, 클라이언트의 progress bar 표시를 위해 사용해 주세요.")
	@ApiResponse(responseCode = "200", description = "점역 진행 상황을 반환합니다.",
			content = {
					@Content(mediaType = "application/json", schema = @Schema(implementation = TranslationStatusResponse.class))
			})
	ResponseEntity<TranslationStatusResponse> checkStatus(
			@Parameter(description = "Translations id", required = true) @PathVariable("id") Long id);

	@Operation(summary = "점역 결과 BRF 파일 반환 API", description = "id와 함께 요청하면 점역 결과를 반환합니다.")
	@ApiResponse(responseCode = "200", description = "점역된 결과를 반환합니다..",
			content = {
					@Content(mediaType = "application/json", schema = @Schema(implementation = BrfFileQueryResponse.class))
			})
	ResponseEntity<BrfFileQueryResponse> queryBrfFile(
			@Parameter(description = "Translations id", required = true) @PathVariable("id") Long id);
}

 

 

TranslationApiController

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import sunflower.server.api.response.BrfFileQueryResponse;
import sunflower.server.api.response.PdfRegisterResponse;
import sunflower.server.api.response.TranslationStatusResponse;
import sunflower.server.application.TranslationService;
import sunflower.server.application.dto.TranslationStatusDto;
import sunflower.server.exception.FileEmptyException;

import java.net.URI;

import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;

@RequiredArgsConstructor
@RestController
@RequestMapping("/translations")
public class TranslationApiController implements TranslationApiControllerDocs {

	private final TranslationService translationService;

	@PostMapping(consumes = MULTIPART_FORM_DATA_VALUE)
	public ResponseEntity<PdfRegisterResponse> registerPdf(@RequestPart("file") MultipartFile file) {
		if (file.isEmpty()) {
			throw new FileEmptyException(1, "빈 파일입니다.");
		}

		final Long id = translationService.register(file);

		return ResponseEntity
				.created(URI.create("/translations/" + id))
				.body(PdfRegisterResponse.from(file.getOriginalFilename()));
	}

	@GetMapping("/{id}/status")
	public ResponseEntity<TranslationStatusResponse> checkStatus(@PathVariable("id") Long id) {
		final TranslationStatusDto dto = translationService.status(id);
		return ResponseEntity.ok(TranslationStatusResponse.from(dto));
	}

	@GetMapping("/{id}")
	public ResponseEntity<BrfFileQueryResponse> queryBrfFile(@PathVariable("id") Long id) {
		final String brfContent = translationService.findBrfFileById(id);
		final BrfFileQueryResponse response = BrfFileQueryResponse.from(id, brfContent);
		return ResponseEntity.ok(response);
	}
}

 

💋 참고자료

 

반응형