봄싹 사이트에서는 여러 종류(이메일, 구글토크, 트위터, 미투데이 등등)의 알림 서비스를 제공할 계획입니다. 현재는 이 중에서 미투데이를 뺀 나머지 세 개의 서비스가 구현되어 있습니다.

이메일은 스프링의 JavaMailSender를 이용했고, 구글 토크는 Smack 라이브러를 이용했고 트위터는 twiiter4j 라이브러를 이용했습니다.

메일 기능을 만들 때는 인터페이스를 고려하지 않았었는데, 구글 토크 서비스를 만들 때는 인터페이스를 생가해서 미리 MessangerService라는 걸 만들었었습니다.

public interface MessagangerService {
   
    void sendMessage(SpringSproutMessage ssm);

}

SpringSproutMesage 타입의 객체를 받아서 어떤 메시지를 전송하는 인터페이스를 만들고, 그것을 구현한 JabberMessagangerService를 만들었습니다.

public class JabberMessangerService implements NotificationService {
   
...
   
    public void sendMessage(SpringSproutMessage ssm) {
      ...
    }

}

그런다음 메시지들을 몇 개 만들었습니다.

public abstract class SpringSproutMessage {
   
    protected StringBuilder msg;
    protected Collection<String> tos;
   
    public SpringSproutMessage() {
        msg = new StringBuilder();
    }
   
    public String getMessage() {
        return msg.toString();
    }

    public Collection<String> getTos() {
        return tos;
    }
   
}

메시지에서 기본으로 필요한 속성들을 가지고 있는 상위 클래스를 제공하기로 했습니다.

public class MeetingMessage extends SpringSproutMessage{

    public MeetingMessage(Study study, Meeting meeting, MeetingStatus status) {
      ...
    }

    public MeetingMessage(Meeting meeting, MeetingStatus status) {
        this(meeting.getStudy(), meeting, status);
    }

}

이런식으로 상속 받아서 필요한 메시지는 주로 생성자에서 만들어 채웁니다.

    @AfterReturning(pointcut = "addStudyPointcut() && args(study)", argNames="study")
    public void sendMailAfterAddStudy(Study study){
        sendMailService.sendMail(new StudyMail(study, StudyStatus.OPEN));
        messangerService.sendMessage(new StudyMessage(study, StudyStatus.OPEN););
    }

사용할 땐, 이렇게 어드바이스 내에서 넘겨받은 매개변수들을 이용해서 메시지를 마들고 만든 메시지를 messangerService에 넘겨주도록 말이죠.

이렇게.. 인터페이스를 설계하지 않고 그냥 만든 메일 서비스가 있는 상태에서 비슷하지만 조금 다른 새로운 알림 서비스.. 구글토크서비스를 추가해보니 거의 모든 부분을 새로 만들었습니다.

하지만 이번에는 인터페이스를 만들어 둔 상태에서 트위터 서비스를 추가해봤습니다. 추가하려니까 기본적인 골격이 이미 구글토크서비스와 상당히 비슷하더군요.

그래서 구글토크서비스의 인터페이스를 다음과 같이 바꿨습니다.

public interface NotificationService {
   
    void sendMessage(SpringSproutMessage ssm) throws NotificationException ;

}

노티로 바꾸고, 노티 예외를 하나 만들어서 그걸 던지도록 했습니다. smack이나 twitter4j가 checked exception을 던지는데.. 별로 뭐 예외를 잡아서 할 일이 없기 때문에 unchecked exception 계층 구조가 필요해서 저렇게 바꿨습니다.

그리고 트위터 서비스를 구현했습니다.

@Service
public class TwitterService implements NotificationService {

    @Autowired Twitter twitter;

    public void sendMessage(SpringSproutMessage ssm)
            throws NotificationException {
        try {
            twitter.updateStatus(ssm.getMessage());
        } catch (TwitterException e) {
            throw new TwitterServiceException("HTTP status code: "
                    + e.getStatusCode() + " 메시지: " + ssm.getMessage(), e);
        }
    }

}

초간단입니다. 이 클래스랑 NotificationExcpetion을 상속받은 예외 클래스를 하나 만들었습니다. SpringSproutMessage는 구글토크 서비스에서 사용하던 것을 그대로 재사용합니다.

애스팩트를 수정해 줍니다.

    @AfterReturning(pointcut = "addStudyPointcut() && args(study)", argNames="study")
    public void sendMailAfterAddStudy(Study study){
        sendMailService.sendMail(new StudyMail(study, StudyStatus.OPEN));
        StudyMessage msg = new StudyMessage(study, StudyStatus.OPEN);
        messangerService.sendMessage(msg);
        twitterService.sendMessage(msg);
    }

이렇게 됐습니다. 메일 서비스가 이 인터페이스를 따르지 않고 있는것이 좀 불만입니다. 게다가 인터페이스는 있지만 인터페이스 기반 프로그래밍이 제대로 되고 있지 않은 것 같습니다. 이건 그림으로 보면 보다 명확합니다.


이런 상태죠. MailService역시 NotificationService를 구현하도록 수정하고 MailService에서 사용하던 메시지들도 SpringSproutMessage를 상속받아 구현하게 하면 다음과 같은 구조가 될 겁니다. 그나마 다행인 것은 이메일 서비스를 만들 때 3차인가 4차 수정을 거쳐 Aspect를 빼놨다는 것입니다. 서비스 코드가 무한정 정신없어질뻔했는데 역시 Aspect로 빼내길 잘한것 같습니다.


자. 이렇게 말이죠. NotificationService 타입의 콜렉션을 가지고 해당 콜렉션을 순회하면서 메시지를 보내도록 코딩을 하면 새로운 NotifiacationService를 추가할 때마다 애스팩트를 고치지 않아도 됩니다.

간단한 빈 설정으로 콜렉션에 추가만 해주면 되죠. 이 얼마나 멋진 인터페이스 기반 프로그래밍인가요. 왜 인터페이스 기반 프로그래밍이 중요한지 몸소 체험할 수 있는 기회였습니다.

자.. 이제 설계가 끝났고 이게 훨씬 좋은 아키텍트라는 확신이 생겼으니... 비용은 좀 크겠지만, 코드를 뜯어고쳐야겠습니다.

뜯어고치는 일은.. 집에가서~~

ps: 여기서도 로컬 환경과 운영 환경 설정 분리와 구분이 필요한데, 로컬 서버에 띄우고 테스트 하느라 스터디/모임 등을 만들고 수정하면서 트위터에 잘 게시가 되나 확인했습니다. 그러나 실제로는 이 내용들이 실제 봄싹 트위터 계정이 아니라 테스트용 계정으로 만든 곳으로 올린다던지... 해야겠죠.