애플리케이션 개발할 때 retry는 어떤 식으로 구현하시나요?

2013-04-02 09:42

slipp.net의 기능을 보면 Facebook과 연동하는 부분이 있다. Facebook과 연동하는 부분은 성능 상의 이슈도 있고, 실패하더라도 핵심 비지니스 로직이 실패하면 안되기 때문에 Async로 분리해서 구현을 진행했다. 그런데 Async로 처리하다보니 몇 가지 이슈들이 발생해 retry를 해야 하는 상황이 발생했다.

정확한 원인은 Async 처리로 인해 Transaction이 분리되다보니 데이터를 정상적으로 가져오지 못하는 이슈가 있어서 다음과 같이 구현했다. 또한 Facebook과 연동할 때도 가끔씩 Facebook 연동이 실패하는 경우가 있어 retry 기능을 써야했다. retry가 몇 군데 나뉘어서 구현되다보니 중복되는 부분도 발생고 있어 리팩토링을 해야겠다는 생각이 든다. 현재 Facebook과의 연동을 담당하고 있는 클래스는 다음과 같다.

@Service
@Transactional
public class FacebookService {
    [...]


    @Async
    public void sendToQuestionMessage(SocialUser loginUser, Long questionId) {
        log.info("questionId : {}", questionId);
        Question question = questionRepository.findOne(questionId);
        if (question == null) {
            question = retryFindQuestion(questionId);
        }


        if (question == null) {
            log.info("Question of {} is null", questionId);
            return;
        }


        String message = createFacebookMessage(question.getContents());
        String postId = sendMessageToFacebook(loginUser, createLink(question.getQuestionId()), message);
        if (postId != null) {
            question.connected(postId);
        }
    }


    private Question retryFindQuestion(Long questionId) {
        Question question = null;


        int i = 0;
        do {
            if (i > 2) {
                break;
            }


            sleep(500);
            question = questionRepository.findOne(questionId);


            i++;
        } while (question == null);


        return question;
    }


    private void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
        }
    }


    private String sendMessageToFacebook(SocialUser loginUser, String link, String message) {
        String postId = null;
        try {
            FacebookClient facebookClient = new DefaultFacebookClient(loginUser.getAccessToken());
            int i = 0;
            do {
                if (i > 2) {
                    break;
                }


                FacebookType response = facebookClient.publish("me/feed", FacebookType.class,
                        Parameter.with("link", link), Parameter.with("message", message));
                postId = response.getId();


                i++;
            } while (postId == null);
            log.info("connect post id : {}", postId);
        } catch (Throwable e) {
            log.error("Facebook Connection Failed : {}", e.getMessage());
        }
        return postId;
    }


    @Async
    public void sendToAnswerMessage(SocialUser loginUser, Long answerId) {
        log.info("answerId : {}", answerId);
        Answer answer = answerRepository.findOne(answerId);


        if (answer == null) {
            answer = retryFindAnswer(answerId);
        }


        if (answer == null) {
            log.info("Answer of {} is null", answerId);
            return;
        }


        Question question = answer.getQuestion();
        String message = createFacebookMessage(answer.getContents());


        String postId = sendMessageToFacebook(loginUser, createLink(question.getQuestionId(), answerId), message);
        if (postId != null) {
            answer.connected(postId);
        }
    }


    private Answer retryFindAnswer(Long answerId) {
        Answer answer = null;


        int i = 0;
        do {
            if (i > 2) {
                break;
            }


            sleep(500);
            answer = answerRepository.findOne(answerId);


            i++;
        } while (answer == null);


        return answer;
    }


    [...]
}

위 코드를 보면 모두 3군데에서 retry를 하고 있다. 이 코드를 리팩토링한다면 어떻게 하면 좋을까? 이 코드를 리팩토링하지 않고 이미 존재하는 라이브러리를 쓴다면 추천할만한 라이브러리가 있을까?

5개의 의견 from SLiPP

2013-04-02 17:51

@자바지기 우선 중복을 없애고 재사용 가능한 상태로 만들어야 할 것 같네요. 만만해보이는 건 retry 패턴이니까 이 부분을 뜯어내고 나머지 로직들을 모듈화하면 되지 않을까요?

class Retry {
    Retry(Retryable retryable, int retryCount) {
        this.retryable = retryable;
        this.retryCount = retryCount;
    }
    void execute() {
        for (int i = 0; i < this.retryCount; i++) {
            if (true == retryable.execute()) {
                return ;
            }
            Thread.sleep(500);
        }
        logger.error("..."); // or throw
    }
}
interface Retryable {
    boolean execute();
}
class SlippMessageToFacebook implements Retryable {
    SlippMessageToFacebook(SocialUser user, String link, String message) {
        ...
    }
    @Override boolean execute() {
         this.send();    
    }
    public boolean send() { 
         FacebookType response = facebookClient.publish(...);
         this.postId = response.getId();
         if (null == this.postId) return false; return true;
    }
}


사용하는 방식은 아래처럼 ..

     if (false == messageToFacebook.send()) 
         new Retry(messageToFacebook, Retry.DEFAULT_RETRY_COUNT).execute();

혹은 Retry를 Failover로 이름을 변경해도 되겠네요. 그리고 failover 방식에 retry()가 있도록 하고요.

     if (false == messageToFacebook.send()) 
         new Failover(messageToFacebook).retry(Failover.DEFAULT_RETRY_COUNT);
     sysout(messageToFacebook.postId);
2013-04-02 18:54

@benghun 코드 리팩토링과 좋은 의견 감사요. 이 곳의 장점이 답변글이 원글보다 더 긴 경우들이 있다는 것인데 이 부분은 개선사항으로 두어서 이전 상태와 같이 넖힐 수 있도록 할께요. 코드 리팩토링하는 과정들을 보면서 저도 많이 배웁니다.

2013-04-02 20:37

모발이라 다를 깊게 읽지는 않았지만~ 다른 팀에서 했던 코드가 제쪽에 있어서 그 내용으로 의견 드리면~~^^

어짜피 트랜잭션 단위로 기능을 적절히 나누고 retry 할 부분에 대해서 aop를 적용하면 좋을 듯 합니다. 몇 번을 재 시도 할 것인가는 애노테이션에 명세를 이용하면 좋구요. retry를 해야하는 예외를 던지게 하여 처리하면 좋을 듯 합니다

의견 추가하기

연관태그

← 목록으로