출처 : http://slipp.net/questions/123

오늘 애플리케이션 개발할 때 retry는 어떤 식으로 구현하고 있나? (http://www.slipp.net/questions/122)에에) 대한 질문을 올렸다. 이 질문을 올린 후에 곰곰히 생각해 봤다. 지금까지 애플리케이션을 구현하면서 retry를 구현하는 경우가 많지 않았는데 유독 slipp.net 소스에 retry 구현이 많지 않은가? 분명 어딘가 문제가 있다는 생각이 들었다.

그 래서 retry를 이렇게 많이 사용하게된 원인을 파악해 봤다. 그랬더니 페이스북으로 글을 전송할 때 가끔씩 정상적으로 동작하지 않아 문제를 해결하려다보니 retry를 무분별하게 사용하게 되었다는 것이 떠올랐다. 그렇다면 왜 페이스북으로 글을 전송할 때 문제가 발생했을까? 원인을 찾아보니 페이스북 전송에 문제가 있었던 것이 아니라 Async로 처리하는 부분에서 데이터베이스에 저장된 데이터가 정상적으로 조회되지 않는다는 것이 가장 이슈였다. 원인은 다음 상황에서 발생하고 있었다.

@Transactional
public class QnaService {
    public Question createQuestion(SocialUser loginUser, QuestionDto questionDto) {
        [...]


        if (questionDto.isConnected()) {
             facebookService.sendToQuestionMessage(loginUser, savedQuestion.getQuestionId());
        }
        return savedQuestion;
    }
}

위 소스 코드와 같이 글을 쓰는 시점에 페이스북으로 전송 상태이면 facebookService.sendToQuestionMessage()를 호출해 페이스북으로 글을 전송한다. facebookService.sendToQuestionMessage() 내부를 살펴보면 다음과 같다.

@Service
@Transactional
public class FacebookService {
    @Async
    public void sendToQuestionMessage(SocialUser loginUser, Long questionId) {
        Question question = questionRepository.findOne(questionId);
        if (question == null) {
            question = retryFindQuestion(questionId);
        }


        [...]
    }


}

위 소스 코드를 보면 알 수 있듯이 sendToQuestionMessage()는 Async로 동작하도록 구현하고 있다. 이 때 문제가 발생하는 부분은 questionRepository.findOne(questionId)를 호출할 때 Question 데이터가 null이 되는 경우가 가끔 발생한다는 것이 문제의 원인이었다. 이 문제에 대한 해결책으로 retry를 선택한 것이다. 하지만 위와 같이 retry를 하더라도 문제는 완전히 해결되지 않았다. 왜 이와 같은 현상이 발생하는지 살펴보자.

먼저 위와 같이 구현할 경우의 데이터 처리 상태를 파악해 보자.

QnaService.createQuestion()에서 Transaction이 시작된다.

데이터베이스에 Question 데이터를 저장한다. 하지만 아직까지 commit이 완료되지 않았기 때문에 데이터베이스에 완전히 저장된 것은 아니다.

facebookService.sendToQuestionMessage() 메소드를 호출해 페이스북에 메시지를 전송한다.

QnaService.createQuestion() 메소드가 종료되면서 commit을 한다.

위 과정에서 문제의 원인이 된 부분은 4번 과정이 완료되지 않은 상태에서 3번 과정이 먼저 실행되면서 4번에서 commit 후에 저장해야 될 Question 데이터를 조회하기 때문에 조회할 수 없는 상황이 발생한 것이다. 3번과 4번의 실행 순서가 어떻게 되느냐에 따라 기능이 정상 동작하거나 동작하지 않는 상황이 발생한 것이다.

이에 대한 문제를 인지하고 자료를 찾아보니 몇 일 전에 올라온 따끈따끈한 자료가 있어 참고해서 문제를 해결할 수 있었다.

http://architects.dzone.com/articles/synchronizing-transactions : 이틀 전에 올라온 글이다. ㅋㅋ

이 글을 참고해서 다음과 같이 코드를 수정했다.

public class QnaService {
    public Question createQuestion(final SocialUser loginUser, QuestionDto questionDto){
        Set<Tag> tags = tagService.processTags(questionDto.getPlainTags());


        Question newQuestion = new Question(loginUser, questionDto.getTitle(), questionDto.getContents(), tags);
        final Question savedQuestion = questionRepository.saveAndFlush(newQuestion);


        if (questionDto.isConnected()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                public void afterCommit() {
                    facebookService.sendToQuestionMessage(loginUser, savedQuestion.getQuestionId());
                }
            });
        }
        return savedQuestion;
    }
}

위 소스 코드와 같이 TransactionSynchronizationManager.registerSynchronization()를 활용해 구현해 봤다. 아직까지 모든 문제가 완료된 것인지는 확인하지 못했다. 당분간 운영해 보면 문제가 정상적으로 해결되었는지 확인해 볼 수 있을 듯하다. 이 이슈를 경험하면서 Async 기능을 구현할 때는 고려해야할 부분이 많다는 것을 다시 한번 느꼈다. 한 가지 기능에 대해 여러 개의 Thread가 동작하는 경우 Transaction이 어떻게 동작하는지 정확하게 이해하고 사용할 필요가 있을 것으로 생각한다.


+ Recent posts