시각장애인을 위한 빠르고 정확한 수학교재 인공지능 점자 변환 서비스, Sunny Braille
어떤 교재든 텍스트 뿐만 아니라 수식도 점자로 변환해주는 교육용 AI 점역 소프트웨어
해바라기 팀 소개 영상 ⇒ https://www.youtube.com/watch?v=cuYIEpQx2po&t=8s
Sunny Braille 소스 코드 ⇒ https://github.com/sunnybraille
💋 인트로
지난 포스팅에 이어서 Sunny Braille 서비스의 가장 핵심이자 (사실 모든 것이라고도 할 수 있는) 점자번역 API를 개발중에 있다.
팀은 현재 1년 가량 유지되고 있었고, 1년 정도 즈음 내가 합류하게 되었다.
기존 팀에서 사용하던 API가 장고 코드로 이루어져 있는데, 점역 알고리즘과 섞여 있어서 유지보수가 극도로 어려워졌고, 나는 본질적인 기능은 같지만 언어를 자바+스프링으로 바꾸어 재구현을 하고 있다.
그중 받았던 피드백을 바탕으로 API 구조를 개선한 내용에 대해 포스팅하려고 한다.
💋 Before
✔️ 상황
우리팀에서는 점자 번역 처리를 위해서 4번 이상의 외부 API를 호출하고 있는데, 그 과정이 동기적으로 이루어져 클라이언트가 무한 대기해야 하는 상황이었다.
✔️ 피드백
문제가 될 것 같다고는 생각했는데,
역시나 서울맹학교 선생님과 User Test에서, 점자 번역할 수학문제의 Pdf를 업로드하고 기다리는 약 1분의 시간동안 아무런 progress bar도 없고, 어떻게 진행되고 있는지 깜깜무소식이라 답답하다는 피드백을 받았다.
💋 After
✔️ 개선안
새롭게 API를 짜서 제안했다.
그냥 외부 api 4번 호출을 4번의 비동기적인 동작으로 분리하고, 클라이언트에게는 즉시 id 값을 반환한다.
3번의 작업이 진행되는 상황을 체크할 수 있는 status 확인 API만을 제공하고,
작업이 완료되면 클라이언트가 알아서 서버에 결과물을 호출하게 해서 사용자를 무한 대기로부터 해방시켜주기로 함.
✔️ 구현 방법
자바에서 비동기로 처리하는 방식에 롱폴링, 소켓, 이벤트 발행 등등이 있다고 하는데,
지난 우아콘에서 접해서 가장 익숙한 스프링 이벤트 발행으로 구현하기로 결심했다. 나머지는 좀더 학습비용이 필요할 것 같아서!
아래는 서비스 계층 코드인데, 저렇게 최대한 간단히 받은 파일만을 어딘가에 저장해 놓고, 데이터베이스에 간단한 정보만을 저장한 뒤 (외부 API 호출은 하나도 안하고) 클라이언트에게 곧바로 id를 반환한다.
@Transactional
public Long register(final MultipartFile file) {
final String originalFileName = file.getOriginalFilename();
final String fileName = FileUtil.createRandomFileName(file);
final String pdfPath = FileUtil.saveFile(file, fileName, Paths.get("src", "main", "pdf"));
final Translations translations = translationsRepository.save(Translations.of(pdfPath, originalFileName));
log.info("Saved pdf File in Server. File URI: {}", pdfPath);
eventPublisher.publishEvent(new OcrRegisterEvent(this, translations));
log.info("pdf file 저장 이벤트를 발행했습니다!");
log.info("현재 스레드: {}", Thread.currentThread().getName());
return translations.getId();
}
그리고 보면 OcrRegisterEvent를 발행한다.
그러면 OcrRegisterEventListener가 깨어나서 첫번째 외부 API를 호출하게 된다.
@Slf4j
@NoArgsConstructor
@Component
public class OcrRegisterEventListener {
private TranslationsRepository translationsRepository;
private OcrRegisterClient ocrRegisterClient;
private ApplicationEventPublisher eventPublisher;
@Autowired
public OcrRegisterEventListener(
final TranslationsRepository translationsRepository,
final OcrRegisterClient ocrRegisterClient,
final ApplicationEventPublisher eventPublisher
) {
this.translationsRepository = translationsRepository;
this.ocrRegisterClient = ocrRegisterClient;
this.eventPublisher = eventPublisher;
}
@Async
@TransactionalEventListener
@Transactional(propagation = REQUIRES_NEW)
public void registerOcr(final OcrRegisterEvent event) {
log.info("현재 스레드: {}", Thread.currentThread().getName());
final Long id = event.getTranslations().getId();
final Translations translations = translationsRepository.getById(id);
final File file = FileUtil.findFile(translations.getPdfPath());
translations.startOcr();
final String pdfId = ocrRegisterClient.requestPdfId(file);
translations.registerPdfId(pdfId);
eventPublisher.publishEvent(new OcrStatusEvent(this, id));
}
}
앞선 방법과 유사하게 마지막에 또 이벤트를 발행한다.
저렇게 해서 계속해서 이벤트가 연쇄적으로 발행되어 클라이언트는 즉시 반환받은 아이디 값을 가지고 있다가,
진행 상태가 어떤지 확인하거나, 언젠가 점역 과정이 완료되었다면 파일을 다운로드 받을 수 있다.
✔️ 외부 API 호출 시 트랜잭션을 어떻게 해야할까,,?
개선된 코드에서 각각 작업을 아래와 같은 트랜잭션으로 묶고 있다.
- 트랜잭션 1: 클라이언트가 파일을 보내자마자, 그냥 일단 데이터베이스에 저장하고 id 즉시 반환
- 트랜잭션 2: 1번 외부 API 호출 후 데이터베이스에 상태 update
- 트랜잭션 3: 2번 외부 API 호출 후 데이터베이스에 상태 update
- 트랜잭션 4: 3번 외부 API 호출 후 데이터베이스에 상태 update
- 트랜잭션 5: 4번 외부 API 호출 후 데이터베이스에 상태 update
고민이,, 트랜잭션 2~5에서 외부 API 호출에 대한 부분도 트랜잭션에 포함되고 있다는 것이다.
물론 아직 서비스 규모가 작아서 문제가 되지는 않겠지만, 트랜잭션 유지에는 비용이 들기 때문에 필수가 아니면 제거하는게 좋다.
고려해봤을 때, 각각의 API에 대해서 하나를 호출한 후에 그 결과에 따라서 우리 데이터베이스에 상황을 업데이트한다. (클라이언트가 현재 진행도에 대해서 조회하는 API를 호출했을 때는 자체 데이터베이스만 찔러봐도 반환할 수 있도록)
진행 상황이 업데이트가 교착될 일은 딱히 없을 것 같다.
트랜잭션을 제거하려고 했는데 그렇게 하니, 업데이트가 반영이 되지 않는데 이건 아마도 코드 상의 이슈일 것 같아서 JPA를 암전히 좀더 공부해봐야겠다. (충분히 공부했다 생각했는데 아직도 섣부른거였니...!)
값을 업데이트 하는데 트랜잭션이 필수인건 아니어 보이니, 현재 흐름에서 트랜잭션은 중요한 부분이 아니라 제거하게 된다면 아예 없게 만들 수 있을 것 같다는 생각이 들며,,, 이건 성능개선이니 또 우선순위에서 밀리지 않을까 싶다,,킼키
💋 참고자료
- https://tecoble.techcourse.co.kr/post/2022-11-14-spring-event/
- https://cheese10yun.github.io/event-transaction
- https://velog.io/@heoseungyeon/REST-API-서버에서-비동기가-필요할-때-1
- https://www.youtube.com/watch?v=b65zIH7sDug&t=523s
도움이 되었다면, 공감/댓글을 달아주면 깃짱에게 큰 힘이 됩니다!🌟
비밀댓글과 메일을 통해 오는 개인적인 질문은 받지 않고 있습니다. 꼭 공개댓글로 남겨주세요!