[Spring] 비동기 처리
비동기 처리를 찾아본 이유
이전 시간에 파이썬 서버를 구성하고 api 통신으로 일부 처리는 파이썬에게 맡기도록 하였다. 잘 처리되고 됐다 생각하던 찰나, 다른 문제가 생겼다. 파이썬으로 request를 보내고 응답이 올때까지 클라이언트도 대기 상태가 된다. 키워드 추출 후 유의어, 반의어 등은 나중에 볼 것이라 요청만 보내면 되는데, 이것마저 기다려야 되는 것이다. 그렇기에 스프링에서 파이썬에게 요청을 보낼 때 비동기 요청을 하고, 아래 코드를 실행시키는 방법을 찾아 적용해보았다.
지금은 팀 회의를 거쳐서 다시 동기적으로 되돌려 놨다.
문제점 발견
팀 회의를 통해 동기적으로 돌리기로 했다. 그래서 비동기 작업임을 알려주는 @Aysnc 어노테이션만 제거하면 되겠다 싶어 주석 처리 후 다시 실행시켜보았는데 웬걸? 디비에는 자꾸 모든 데이터가 null로 지정되는 것이다. 문제를 찾아보다 짐작되는 코드는 다음 부분이었다.
@Transactional
public BasicResponseDto<KeywordResponseDto> makeKeyword(Long lectureId) {
List<Quiz> quizList = new ArrayList<>();
Optional<Lecture> optional = lectureRepository.findById(lectureId);
if (optional.isEmpty()) {
return null;
}
Lecture lecture = optional.get();
KeywordResponseDto keywordResponseDto = new KeywordResponseDto(lectureId);
if (lecture.getHasKeyword()) {
for (Quiz quiz : lecture.getQuizzes()) {
keywordResponseDto.getKeywordList().add(new KeywordDto(quiz.getWord(), quiz.getMeaning()));
}
return new BasicResponseDto<>(BasicResponseDto.SUCCESS, BasicResponseDto.KEYWORD, keywordResponseDto);
}
String content = lecture.getContent();
String[] keywords = keyword.separateWords(content);
for (String keyword : keywords) {
Quiz quiz = Quiz.builder().word(keyword).build();
quiz.setLecture(lecture);
quizList.add(quizRepository.save(quiz));
}
asyncService.setKeywordInfo(quizList);
quizList.forEach(quiz -> keywordResponseDto.getKeywordList().add(new KeywordDto(quiz.getWord(), quiz.getMeaning())));
lecture.updateHasKey(true);
return new BasicResponseDto<>(BasicResponseDto.SUCCESS, BasicResponseDto.KEYWORD, keywordResponseDto);
}
코드는 다음과 같고 간략히 설명하자면, 퀴즈 리스트를 만들어 asyncService.setKeywordInfo(quizList)로 보내서 비동기적으로 처리하는 로직이다. 그리고 메서드 위에 @Transactional 이 걸려있는 것을 볼 수 있는데, 메서드가 정상 작동 후 최종적으로 commit이 일어나는데 여기서 문제가 생기는 것이다.
1. 비동기적으로 처리할 때 - 비동기적으로 보내니 메서드는 종료되서 커밋이 일어난다. 이때 디비에는 저장이 되어있고, 이후 파이썬 서버에서 유의어, 반의어 처리하면서 디비에 저장하게 되어 문제 없이 조회가 되었던 것이다.
2. 동기적으로 처리할 때 - @Async만 제거했을 때, 순차적으로 처리하게되면서 파이썬 서버에서 먼저 디비 반영이 일어난다. 그런데 아직 commit 전이라 테이블엔 로우가 없고 없는 로우를 찾아 update를 실행시키니 아무것도 일어나지 않는다. 그러고 파이썬 처리가 끝나 다시 스프링으로 돌아와 디비를 저장하기 때문에 아무일도 일어나지 않았던 것이다.
해결책은?
흠.. 사실 나는 네트워크 왔다 갔다 하는 것을 줄이기 위해 리스트를 보내서 한 번에 처리되는 것을 원했다. 그럴려면 현재 구조를 바꾸어 서비스를 분리하면 되지 않을까 싶은데, 그러면 로직을 좀 바꿔야할거 같은데 깔끔한 해답이 생각나지 않는다. 그래서 바꾼 구조로는 퀴즈를 하나 만들 때마다 파이썬 서버로 요청을 보내어 처리값을 json으로 돌려받아 스프링에서 필드값을 바꾸게 해주었다.
@Transactional
public BasicResponseDto<KeywordResponseDto> makeKeyword(Long lectureId) {
List<Quiz> quizList = new ArrayList<>();
List<Quiz.QuizBuilder> quizBuilders = new ArrayList<>();
Optional<Lecture> optional = lectureRepository.findById(lectureId);
if (optional.isEmpty()) {
return null;
}
Lecture lecture = optional.get();
KeywordResponseDto keywordResponseDto = new KeywordResponseDto(lectureId);
if (lecture.getHasKeyword()) {
for (Quiz quiz : lecture.getQuizzes()) {
keywordResponseDto.getKeywordList().add(new KeywordDto(quiz.getWord(), quiz.getMeaning()));
}
return new BasicResponseDto<>(BasicResponseDto.SUCCESS, BasicResponseDto.KEYWORD, keywordResponseDto);
}
String content = lecture.getContent();
String[] keywords = keyword.separateWords(content);
for (String keyword : keywords) {
PythonKeywordResponseDto keywordInfo = pythonService.getKeywordInfo(keyword);
Quiz quiz;
Quiz.QuizBuilder quizBuilder = Quiz.builder().word(keyword);
if (keywordInfo != null) {
quizBuilder.definition(keywordInfo.getDefinition())
.antonym(keywordInfo.getAntonym())
.example(keywordInfo.getExample());
}
quiz = quizBuilder.build();
quiz.setLecture(lecture);
quizList.add(quizRepository.save(quiz));
}
quizList.forEach(quiz -> keywordResponseDto.getKeywordList().add(new KeywordDto(quiz.getWord(), quiz.getMeaning())));
lecture.updateHasKey(true);
return new BasicResponseDto<>(BasicResponseDto.SUCCESS, BasicResponseDto.KEYWORD, keywordResponseDto);
}
public PythonKeywordResponseDto getKeywordInfo(String keyword) {
return fastApi.webClient.get().
uri(uriBuilder -> uriBuilder
.path("/api/keyword-info")
.queryParam("keyword", keyword)
.build())
.retrieve()
.bodyToMono(PythonKeywordResponseDto.class)
.block();
}
method는 get으로 하고 쿼리파라미터로 변경하여 값을 받도록 했다. 반복문 마다 request를 보내게 되는 구조로 썩 마음에 들진 않지만 지금으로써 최선의 방법이라 일단은 이렇게도 적용해 보았다. 오늘도 느낀점은 정말로 구조를 잘 짜고 개발을 해야겠다는 것이다.