Whiteship's Note

'모하니?/Coding'에 해당되는 글 299건

  1. 2009.09.14 스프링 이메일 확장하기 3
  2. 2009.09.12 스프링 이메일 확장하기 2
  3. 2009.09.10 [하이버네이트] 쿼리를 수정할 것이냐 모델을 수정할 것이냐...
  4. 2009.09.08 [하이버네이트] 1 + 2N select 문제 해결하기 (4)
  5. 2009.09.07 [hamcrest] Matcher 만들기
  6. 2009.09.04 [GenericDao] 하이버네이트 GenericDao (6)
  7. 2009.09.04 [Generic] 자바 Generic 타입 알아내기
  8. 2009.08.27 [봄싹 오픈] springsprout.org (20)
  9. 2009.08.27 [봄싹] D-day 오후 10시에 오픈하겠습니다. (4)
  10. 2009.08.26 [봄싹] 시즌 2 오픈 D-1 (2)
  11. 2009.08.17 [봄싹] 시즌2 다음 주 오픈 예정 (2)
  12. 2009.08.14 [테스트] 커스텀 MimeMessageHelper 테스트하기
  13. 2009.08.14 [스프링 이메일] MimeMessageHelper 초간단히 사용하기
  14. 2009.08.11 [봄싹] 간단하지 않은 회원 관리 (4)
  15. 2009.08.06 [봄싹]기트 도입 실패 사례 (6)
  16. 2009.07.31 [SES 프로젝트] 베타 버전 완성~~!! (4)
  17. 2009.07.31 [SES 프로젝트] Self English Study 프로젝트를 시작합니다. (2)
  18. 2009.07.30 [smack] 라이브러리 추가 및 기초 코드 (4)
  19. 2009.07.30 [smack] 구글 토크 봇 - 파일럿 프로그래밍 (2)
  20. 2009.07.29 스프링 이메일 확장하기 (4)
  21. 2009.07.28 [하이버네이트] OneToMany에 FetchType.EAGER 사용시 어떤 일이 생길까?
  22. 2009.07.27 [Mockito] mock 객체 쉽게 만들기 (2)
  23. 2009.07.23 [하이버네이트]롹킹과 성능 사이에 서다. (1)
  24. 2009.07.22 JUnit 4.7 새 기능 @Rule (2)
  25. 2009.07.22 윈도우에서 모나코 폰트 이쁘게 쓰기 (6)
  26. 2009.07.21 에러 잡기 7단계
  27. 2009.07.20 내일은 꼭 Monaco 폰트 설치해야지 (8)
  28. 2009.07.17 스프링 시큐리티 2.X -> 스프링 시큐리티 3.X (2)
  29. 2009.07.15 모르겠네.. @_@ (2)
  30. 2009.07.14 스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 4

스프링 이메일 확장하기 3

모하니?/Coding : 2009.09.14 14:33


아마도 이번 글이 스프링 이메일 확장하기 시리즈의 마지막이 될 것 같습니다. 이전까지 작업한 내용들을 간략히 간추리자면 다음과 같습니다.

스프링 이메일 확장하기 1 - SimpleMailMessage를 확장하는 방법.
스프링 이메일 확장하기 2 - MimeMessage, JavaMailSender, 별도의 메일 클래스 계층 구조, 이 셋을 가지고 메일을 전송하는 SendMailService를 사용하는 방법.

이번에는 SendMailService를 StudyService와 MemberService 같은 곳에 혼재되어 나타나는 것을 Aspect로 분리한 것에 대해 이야기하렵니다. (보여드리는 편이 서로 편하겠지만..)

우선 메일 서비스 때문에 불편한 점들이 속속 생기기 시작했습니다. 테스트 하기가 불편했습니다. 단위테스트야 목킹한 다음에 대충 무시해주면 그만이지만, 통합테스트일 경우 스터디나 모임 추가/변경 테스트를 할 때 마다 테스트성 메일이 전송되는데, 이 때 생기는 오버헤드와 불필요한 메일 메시지가 상당히 거슬렸습니다. 그래서 대부분 단위테스트로 바꿨습니다.

그러나 단위테스트를 한다고 해서 편해지는 것도 아니었습니다. StudyService에서 SendMailService를 사용하고 있는 이상 목킹을 하고 해당 목이 잘 동작하는지 테스트를 해야했죠. 결국 테스트 할 분량이 늘어나서 테스트를 작성할 때 귀찮았습니다. 비스무리하게 메일 전송하는 부분을 스터디, 모임 추가/변경 때마다 테스트를 목킹해줘야 했으니 말이죠.

다음은 코딩하기가 번거로웠습니다. 지금까지 메일 서비스의 큼직한 설계 변경 두 번으로 StudyService와 MemberService는 계속 고쳐져야했습니다. 아니.. 메일 전송 관련 부분을 고치는데 얘네들이 이렇게 고생을 많이 해도 되는걸까? 뭔가 잘못된거 아닌가.. 하는 생각이 들었습니다. (OCP 원칙에 위배된 코드였던게죠.)

결론은.. 그래.. 빼자.. 빼..

어떻게?

AOP!!

그래서 SendMailAspect라는 것을 하나 만들었습니다. 그리고 StudyService와 MemberService에서 참조하고 있던 SendMailService를 없애고, SendMailAspect 안으로 넣어줬습니다.

SendMailAspect에 포인트컷을 정의하고, 어드바이스를 정의해서 간단하게 메일 전송 서비스를 AOP로 구현할 수 있었습니다. 그러나 곧 또다른 요구사항이 생기기 시작했습니다.

스터디, 모임 추가/변경 할 때 트위터에 메시지를 올리자는 것이었습니다. 그리고 재버 애플리케이션을 이용해서 구글 토크로 메시지도 전송하자고 했습니다.

캬.. 멋지구나!!

아.. 이름을 바꿔야겠다. NotificationAspect로 바꾸고 그 안에 TwitterService와 GoogleTalkService를 추가하면 되겠구나!! 그래서 이름을 NotificationAspect로 수정했습니다.

@Aspect
public class NotificationAspect {
   
    @Autowired SendMailService sendMailService;
   
    @Pointcut("execution(* *..*.StudyService.addStudy(..))") void addStudyPointcut(){}
    @Pointcut("execution(* *..*.StudyService.updateStudy(..))") void updateStudyPointcut(){}
    @Pointcut("execution(* *..*.StudyService.addMeeting(..))") void addMeetingPointcut(){}
    @Pointcut("execution(* *..*.StudyService.updateMeeting(..))") void updateMeetingPointcut(){}
    @Pointcut("execution(* *..*.MemberService.add(..))") void addMemberPointcut(){}

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

    @AfterReturning(pointcut = "updateStudyPointcut() && args(study)", argNames="study")
    public void sendMailAfterUpdateStudy(Study study){
        if(study.getStatus() != StudyStatus.ENDED)
            sendMailService.sendMail(new StudyMail(study, StudyStatus.UPDATED));
    }
   
    @AfterReturning(pointcut = "addMeetingPointcut() && args(study, meeting)", argNames="study, meeting")
    public void sendMailAfterAddMeeting(Study study, Meeting meeting){
        sendMailService.sendMail(new MeetingMail(study, meeting, MeetingStatus.OPEN));
    }

    @AfterReturning(pointcut = "updateMeetingPointcut() && args(meeting)", argNames="meeting")
    public void sendMailAfterUpdateMeeting(Meeting meeting){
        if(meeting.getStatus() == MeetingStatus.OPEN)
            sendMailService.sendMail(new MeetingMail(meeting, MeetingStatus.UPDATED));
    }

    @AfterReturning(pointcut = "addMemberPointcut() && args(member)", argNames="member")
    public void sendMailAfterAddMember(Member member){
        sendMailService.sendMail(new ConfirmMail(member));
    }
   
}


이제는 TwitterService와 GoogleTalkService만 위 애스팩트에 추가해서 넣으면 됩니다.

차후에 세미나 정보가 추가됐을 때 메시지를 보내달라는 요구사항이 생길 수도 있는데, 그 때도 위에 있는 애스팩트만 고치면 되지 세미나 서비스는 건드릴 필요가 없어졌습니다.

이렇게 좋은 점만 있었던 것은 아닙니다. 개인적으로 애스팩트를 개발할 때 가장 필요하다고 생각하는 테스트가 바로 포인트컷 테스트였는데, 그걸 못했었습니다. 어떻게 해야할지 떠오르지가 않더군요. 매번 애플리케이션을 돌려가며 확인하는것은 정말 너무 노가다이고, STS의 AspectJ 툴이 보여주는 위빙 포인트로는 만족하지 못하겠고 말이죠.

그런데.. 오늘.. 그동안 비밀리에 베타리딩 중이던 책을 읽다가 딱.. 제가 원하던 코드를 볼 수 있었습니다. 그 부분을 참조하여 포인트컷이 내가 원하는 메서드에 걸리는지 확인하는 테스트를 작성했습니다.

    @Test
    public void pointcuts() throws Exception {
        checkPointcutMatches(NotificationAspect.class, "addStudyPointcut",
                StudyService.class, "addStudy", Study.class);
        checkPointcutMatches(NotificationAspect.class, "updateStudyPointcut",
                StudyService.class, "updateStudy", Study.class);
        checkPointcutMatches(NotificationAspect.class, "addMeetingPointcut",
                StudyService.class, "addMeeting", Study.class, Meeting.class);
        checkPointcutMatches(NotificationAspect.class, "updateMeetingPointcut",
                StudyService.class, "updateMeeting", Meeting.class);
        checkPointcutMatches(NotificationAspect.class, "addMemberPointcut",
                MemberService.class, "add", Member.class);
    }

    private void checkPointcutMatches(Class<?> aspectClass,
            String pointcutMethodName, Class<?> targetClass,
            String targetMethodName, Class<?>... args)
            throws SecurityException, NoSuchMethodException {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression((String) AnnotationUtils
                .getValue(AnnotationUtils.findAnnotation(ReflectionUtils
                        .findMethod(aspectClass, pointcutMethodName),
                        Pointcut.class)));

        assertThat(pointcut.matches(targetClass), is(true));
        assertThat(pointcut.matches(targetClass.getMethod(targetMethodName,
                args), targetClass), is(true));
    }

인라인 리팩토링을 너무 많이 해서 그런지... 다소 복잡해 보이는 코드지만, 방법은 무척 간단했습니다. 단.. 테스트를 조금 편하게 하기 위해 불필요하게(여러 어드바이스에서 공용으로 사용할 포인트컷이 아님에도 불구하고) pointcut을 별도로 정의한 것이 조금 맘에 걸리기는 하지만, 그 정도는 뭐.. 애교로..ㅎㅎ 사실 더 정확하게 포인트컷을 테스트 하려면 애스팩트의 코드를 다음과 같이 바꿔야 합니다.

@Aspect
public class NotificationAspect {
   
    @Autowired SendMailService sendMailService;
   
    @Pointcut("execution(* *..*.StudyService.addStudy(springsprout.domain.study.Study))") void addStudyPointcut(){}
    @Pointcut("execution(* *..*.StudyService.updateStudy(springsprout.domain.study.Study))") void updateStudyPointcut(){}
    @Pointcut("execution(* *..*.StudyService.addMeeting(springsprout.domain.study.Study, springsprout.domain.study.Meeting))") void addMeetingPointcut(){}
    @Pointcut("execution(* *..*.StudyService.updateMeeting(springsprout.domain.study.Meeting))") void updateMeetingPointcut(){}
    @Pointcut("execution(* *..*.MemberService.add(springsprout.domain.Member))") void addMemberPointcut(){}

    @AfterReturning(pointcut = "addStudyPointcut()", argNames="springsprout.domain.study.Study")
    public void sendMailAfterAddStudy(Study study){
        sendMailService.sendMail(new StudyMail(study, StudyStatus.OPEN));
    }

    @AfterReturning(pointcut = "updateStudyPointcut()", argNames="springsprout.domain.study.Study")
    public void sendMailAfterUpdateStudy(Study study){
        if(study.getStatus() != StudyStatus.ENDED)
            sendMailService.sendMail(new StudyMail(study, StudyStatus.UPDATED));
    }
   
    @AfterReturning(pointcut = "addMeetingPointcut()", argNames="springsprout.domain.study.Study, springsprout.domain.study.Meeting")
    public void sendMailAfterAddMeeting(Study study, Meeting meeting){
        sendMailService.sendMail(new MeetingMail(study, meeting, MeetingStatus.OPEN));
    }

    @AfterReturning(pointcut = "updateMeetingPointcut())", argNames="springsprout.domain.study.Meeting")
    public void sendMailAfterUpdateMeeting(Meeting meeting){
        if(meeting.getStatus() == MeetingStatus.OPEN)
            sendMailService.sendMail(new MeetingMail(meeting, MeetingStatus.UPDATED));
    }

    @AfterReturning(pointcut = "addMemberPointcut()", argNames="springsprout.domain.Member")
    public void sendMailAfterAddMember(Member member){
        sendMailService.sendMail(new ConfirmMail(member));
    }

이전 애스팩트에서는 Advice에서 포인트컷을 다시 한 번 조합하기 때문에 실제로 어드아비스가 엉뚱한 곳에 적용된다던지.. 원하는 곳에 적용이 되지 않는 등의 문제가 발생할 수도 있습니다.

따라서, 애스팩트를 만들 때 테스트 가이드로

1. 포인트컷은 항상 @Pointcut을 이용하여 별도의 메서드로 정의하고 어드바이스에서는 포인트컷 메서드 이름만 참조하고, 조합은 하지 않는다. 조합이 필요할 때는 또 다른 @Pointcut을 정의하여 사용한다.

또는

2. 포인트컷 테스트는 어드바이스에 정의된 것을 가지고 테스트한다.

라고 정하던지 해야할 것 같습니다. 후자로 한다면 조금 복잡해지는 부분(여러 애노테이션 중에서 포인트컷 표현식을 가져오는 부분)이있어서 저라면 1번을 택할 겁니다.

Anyway!.. 이상으로 스프링 이메일 확장하기는 종료합니다. 다음에는 Twitter 서비스 만들기나 구글 토크 메시징 서비스 만들기 등의 글을 연재할지도... 바쁘면 안 할지도...


ps: 비밀리에 베타리딩 중인 책의 저자님 감사합니다. (__)/
top


스프링 이메일 확장하기 2

모하니?/Coding : 2009.09.12 23:52


예전에 작성했던 글에 이어지는 내용입니다. 처음부터 계획했던 글은 아니지만, 봄싹 코드가 개선될 수록 전해드리고픈 이야기도 늘어나네요.

지난 번까지의 스프링 이메일 확장에서는 한 가지 단점이 있었습니다. HTML 메시지, 첨부 파일이 있는 메일 등을 작성할 수 없다는 것이었습니다. SimpleMailMessage만을 확장한 구조기 때문이죠. 이와 같은 기능을 사용하려면 MimeMessage를 사용해야 합니다.

그런데 이 MimeMessage는 SimpleMailMessage처럼 간단하게 만들 수 있는 것이 아니라 스프링의 JavaMailSender로부터 가져와야 하는 객체입니다.

처음에는 다음과 같은 구조로 설계를 했었습니다.

MemberService {
    @Autowired JavaMailSender mailSender;
    ...
    public void addMember(Member member) {
        dao.add(member);
        sendMail(member);
    }
    ...
    private void sendMail(Member member){
        MimeMessage message = mailSender.createMimeMessage();
        ConfirmMail mail = new ConfirmMail(message, member);
        mailSender.send(mail.getMessage());
    }
}

이와 비슷한 구조들이 비밀번호를 재전송할 때도 나타났고, 스터디나 모임이 추가/변경 될 때에도 나타났습니다. 무언가 마음 한켠이 불편해지더군요.

(1) MemberService에서 메일 메시지를 만들고 보내는 것까지 관리할 필요가 없다는 판단이 들었습니다. 메일 메시지를 확장한 방법도 맘에 들지 않았습니다. (2) ConfirmMail 내부에서 MimeMessageHelper를 이용해서 MimeMessage에 필요한 정보들을 채워넣었는데 Mail에서는 Mail과 관련된 정보가 담겨야지 Mail 정보를 MimeMessage에 채우는 일은 메일을 보내는 사람의 담당이 아닐까 하는 생각이 들었습니다.

코드가 있어야 할 위치로 옮겨주는 것이 좋겠다는 생각에 설계를 바꾸게 되었고, 그결과 Mail과 관련된 모든 코드(클라이언트와 서비스까지)를 손봐야했습니다.

우선, ConfirmMail은 POJO로 to, from, content, title, isHTML 같은 속성들을 관리하도록 했습니다. MimeMessage를 꾸미는 일은 별도의 서비스 클래스를 하나 만들어서 그쪽으로 옮겼습니다. SendMailService라는 녀석이지요. 위에 표시해둔 (2)번 문제를 해결했습니다. 그리고 MemberService에서 JavaMailSender를 참조하는 대신 SendMailService를 참조하도록 했고, SendMailService.send(new ConfirmMail(member)); 이런식으로 사용할 수 있게하여 (1)번 문제를 해결했습니다. 즉, MimeMessage를 만들고, 그것을 꾸미고, 전송하는 기능을 다른곳(MemberService와 ConfirmMail)에서 가져다가 한 곳(SendMailService)로 모아뒀습니다.

그 결과 스프링 이메일을 확장한 설계도는 다음과 같이 변했습니다.


이제 별도의 메일 전송 서비스가 필요하다면, 그에 해당하는 메일관련 내용만 SpringSproutMail을 확장해서 만들면 되고, 모듈 서비스에서는 다음의 SendMailService를 이용해서 해당 메일을 send()에 넣어주기만 하면 됩니다. HTML 메시지를 보내는 SendMailService 코드는 다음과 같습니다. 이 클래스는 차후에 첨부파일 등을 추가한 메시지를 보낼 때 다시 수정 될 듯 합니다.

@Service
public class SendMailService {
   
    @Autowired JavaMailSender mailSender;

    public void sendMail(SpringSproutMail mail) {
        MimeMessage message = mailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, SpringSprout2System.ENCODING);
        try {
            helper.setTo(mail.getTos());
            helper.setFrom(mail.getFrom());
            helper.setSubject(mail.getSubject());
            helper.setText(mail.getContent(), mail.isHTML());
        } catch (MessagingException e) {
            throw new MailPreparationException(e);
        }
        mailSender.send(message);
    }

}

한가지 의문은 MimeMessageHelper에서는 왜 MailException이라는 RuntimeException 계층 구조를 만들어 놓고도.. 그걸 사용하지 않았느냐 입니다. 코드도 유겐이 작성한거라 뭔가 이유가 있을법한데 말이죠. 뭔가를 복구할 만한 일을 해야 한다고 생각한걸까요? 흠.. 귀찮습니다. 그냥 MailException 계층구조를 사용해서 던져줬으면 try-catch 안해도 될텐데. 일단은 잡아서 MailException의 하위 클래스 중 하나인 MailPreparationException으로 감싸서 다시 던져줬습니다.

이후에도.. 한가지 더 개선한 점이 있는데 그것은 다음에~

top


[하이버네이트] 쿼리를 수정할 것이냐 모델을 수정할 것이냐...

모하니?/Coding : 2009.09.10 18:17


이전에 올렸던 글에서 고민했던 것을 오늘에서야 처리했습니다.

SQL로 원하는 데이터를 가져올 것이냐(성윤이 제안).. 모델을 수정해서 로직으로 모델의 값들을 변경할 것이냐(내 생각) 중에서 결국은 제 생각을 따르기로 했습니다.

일단 시도는 SQL을 시도했습니다. 그러나 바로 단점이 생기더군요.
- 비슷한 쿼리가 있었는데 그것도 고쳐야 합니다. 비슷한 문장이 중복되는 쿼리가 여러 개 생기겠더군요.

이 문제는 DetachedCriteria를 사용해서 어느 정도 손불 수 있는 문제입니다.
- 쿼리 중에 비슷한 부분을 DetachedCriteria로 분리해두고 재사용하면 되거든요.

하지만 기술적인 문제가 기다리고 있었습니다.
- Criteria.setProject()에 넘겨주는 Proejction과 Criteria.setResultTransformer()가 예상한대로 동작하질 않았습니다.

        c.setProjection(Projections.projectionList()
            .add(Projections.property("id").as("id"))
        );
        c.setResultTransformer(new AliasToBeanResultTransformer(Study.class));

이건 잘 맵핑 해주는데..

        c.setProjection(Projections.projectionList()
            .add(Projections.property("id").as("id"))
            .add(Projections.property("name").as("name"))
        );
        c.setResultTransformer(new AliasToBeanResultTransformer(Study.class));

이렇게 하나 이상의 Projection이 추가되면 c.list()를 호출할 때 타입 캐스팅 에러가 발생합니다. 이걸 결국은 해결하지 못해서;;

마음 편하게 모델을 수정하기로 했습니다.
- 모델에 필요한 필드를 추가하고..
- xxxService에서 모임이나 멤버가 추가/삭제 될 때 로직을 추가하고.
- 테스트로 호가인하고.
끝~

이렇게 간단한 것을 쿼리 길게 작성하고, 쿼리 분리하고, DTO 만들고, 컨트롤러, 서비스, DAO 다 고치고;; 하는 것 보단 편한 것 같습니다.

top


[하이버네이트] 1 + 2N select 문제 해결하기

모하니?/Coding : 2009.09.08 14:30


죄송합니다. 낚시입니다. 1 + 2N select 문제 같은건 없습니다. ;-)

1+n select 문제라는 것이 있고 이것을 해결하는 여러 fetching 방법(batch, subselect , join)을 하이버네이트가 제공해줍니다.

1+n select 문제가 무엇이냐면, Study에서 1 대 다 관계로 Meeting을 참조한다고 했을 때, Study가 가지고 있는 Meeting 컬렉션은 기본값으로 Lazy 로딩이 적용됩니다. 즉, Study를 가져올 때 Meeting 컬렉션을 가져오지 않았다가.. 나중에 필요해지는 순간에 Meeting 목록을 가져옵니다.

이때, Study 전체 목록을 가져온 다음 각각의 Study에 들어있는 Meeting 목록도 가져와서 화면에 보여준다고 해보죠. OSIV 필터를 적용해뒀기 때문에 컨트롤러에서는 단순히 Study 목록만 넘겨줬지만, 화면에서는 c:foreach 구문으로 study.getMeetings()를 호출할 때 lazy 로딩을 하게 되어있습니다.

어떻게 될까요?

스터디 목록이 2개라고 해보죠.
- 전체 스터디 목록을 가져오는 쿼리를 날립니다.(컨트롤러에서)
- 첫번째 스터디의 전체 모임 목록을 가져오는 쿼리를 날립니다.(뷰에서)
- 두번째 스터디의 전체 모임 목록을 가져오는 쿼리를 날립니다.(뷰에서)

스터디 목록이 3개라고 해보죠.
- 전체 스터디 목록을 가져오는 쿼리를 날립니다.(컨트롤러에서)
- 첫번째 스터디의 전체 모임 목록을 가져오는 쿼리를 날립니다.(뷰에서)
- 두번째 스터디의 전체 모임 목록을 가져오는 쿼리를 날립니다.(뷰에서)
- 세번째 스터디의 전체 모임 목록을 가져오는 쿼리를 날립니다.(뷰에서)

이래서 1+n 문제라고 하는 겁니다. 그럼 이걸 어떻게 해결할 수 있을까요?
1. 처음으로 어떤 스터디의 모임 목록을 가져올 때, 특정 갯수 만큼의 스터디와 연관되어 있는 목록을 다 가져옵니다. => prefatching data in batches, @BatchSize
2. 처음으로 어떤 스터디의 모임 목록을 가져올 때, 로딩되어 있는 모든 스터디와 연관되어 있는 모든 모임 목록을 다 가져옵니다. => subselect fatchting, @Fetch(SUBSELECT)
3. 스터디를 가져올 때, 해당 스터디와 관련된 모임 목록도 미리 전부 가져옵니다. => eager fetching, @OneToMany(fetch=FetchType.EAGER)

이정도까지가 기본적인 하이버네이트 패칭 이야기이고, 제가 지금 겪고 있는 문제는 다음과 같습니다. 사실 이제부터가 본론이죠.

1+2N 문제가 어떻게 발생했냐면...

모든 스터디 목록을 가져오는데, 그 때 각 스터디에 참여한 회원수와 총 모임수를 가져와야 합니다.
- 모든 스터디 목록 select
- 모든 회원 수 or 목록 select
- 모든 모임 수 or 목록 select

스터디 모델에 memeberCount나 meetingCount 같은 속성은 없습니다. 스터디 목록 갯수가 20개가 된다면 select 문은 41개가 됩니다. 끔찍한 상황이죠. 갈 수록 성능이 안 좋아질 겁니다. 대책이 필요합니다. 위에서 살펴봤던 패칭 전략 중 어떤 것을 적용해 볼까요?

1. subselect fetching

어차피 모든 스터디가 가지고 있는 모든 멤버와 모임 목록을 가져와야 한다면, 굳이 배치 사이즈를 줘서 일부만 가져올 필요가 없어보입니다. 이럴 바엔 그냥 subselect fetching을 하는게 좋겠습니다.

...
    @ManyToMany
    @Fetch(FetchMode.SUBSELECT)
    private Set<Member> members;
    @OneToMany(cascade={CascadeType.ALL}, mappedBy="study")
    @Fetch(FetchMode.SUBSELECT)
    private Set<Meeting> meetings;
...

스터디 도메인에 위와같이 subselect fetching을 적용했습니다. 제가 원하는 결과는 다음과 같습니다.
1. 모든 스터디 가져오는 select
2. 모든 스터디에 대한 모든 사용자 select
3. 모든 스터디에 대한 모든 모임 select
이렇게 세 줄만 나오는 겁니다. 1+2n 에서 3으로 쿼리가 줄어들어야 합니다.

하지만, 무슨 이유에선지 제대로 동작하지 않습니다.

구글링을 해보니 subselect가 되지 않는다는 글들이 검색되는데 해결책은 마땅히 보이지가 않습니다. JPWH책을 다시 뒤젹여 봐도 설정은 위에서 추가한 애노테이션 하나 밖에 없습니다.

이게 뭔가.. @_@.. 흠 그렇다면 batch fetching을 해보지뭐..

2. batch fetching

... 
    @ManyToMany
    @BatchSize(size=10)
    private Set<Member> members;
    @OneToMany(cascade={CascadeType.ALL}, mappedBy="study")
    @BatchSize(size=10)
    private Set<Meeting> meetings;
...

자 이렇게 설정해뒀습니다. 정확한 쿼리 갯수는 BatchSize와 전체 row수와 하이버네이트의 batch-fetching d알고리즘에 따라 달라지겠지만 쿼리 갯수는 위와 비슷하거나 조금 더 많아 질 겁니다.
1. 모든 스터디 가져오는 select
2. 일부 스터디에 대한 모든 사용자 select
3. 일부 스터디에 대한 모든 모임 select
4. 일부 스터디에 대한 모든 사용자 select
5. 일부 스터디에 대한 모든 모임 select

대략 3 + 2n/10 정도로 줄어들 것으로 예상됩니다.

오예! 잘 동작합니다. 배치 사이즈 때문인지 딱 세 줄의 select로 이전에 보여주던 화면을 그대로 보여줬습니다. 다시 한 번 클릭했을 때는 어제 Study에 적용해둔 2차 캐쉬 때문에 두 번의 select가 날아갔습니다. 그 두 녀석에도 read-write로 캐쉬를 적용하면 이제 두 번째로 스터디 화면을 보여줄 때 커밋된 것이 없다면 아무런 쿼리도 날아가지 않을 겁니다. 일단은 논외기 때문에 패칭 정리를 끝낸 다음에 해보도록 하죠.

3. eager fetching

쿼리 세 줄도 아깝다!! 애초에 모든 스터디 목록으르 가져올 때, 멤버과 모임 목록도 같이 가져오도록 하고 싶다면 eager fetching을 써야겠죠.

1. 모든 스터디 목록을 가져올 때 모든 모임과 멤버 목록까지 select

1+2n이 1로 줄어듭니다. 최고네요.

...
    @ManyToMany(fetch=FetchType.EAGER)
    private Set<Member> members;
    @OneToMany(cascade={CascadeType.ALL}, mappedBy="study", fetch=FetchType.EAGER)
    private Set<Meeting> meetings;
...

오.. 원하던대로 쿼리가 하나만 날아갔습니다. 그런데!!! left outer join으로 인해서 study 목록이 원하던 것 보다 훨씬 많아졌습니다. study만 보자면 중복 데이터입니다.

못쓰겠네요. 지금까지 해본결과 두 번째에 시도한 batch fetching이 제일 적당히 잘 동작했습니다. subselect fetching이 제대로 동작해 줬다면 더 좋았을텐데 조금 아쉽네요.

4. 모델 고치기

저는 사실 패칭을 적용해보기 전에 날아가는 쿼리를 보고서 스터디 목록을 뿌리는데 모임하고 멤버는 왜 가져오는걸까;; 하면서 컨트롤러를 봤더니 스터디 목록만 줍니다. 뭐지? 그럼 어디서 쿼리가 날아가는거야??;; 뷰인가? 하고 봤더니 빙고.. OSIV 때문에 잘 보이지 않는곳(뷰)에서 쿼리가 날아가고 있었던 겁니다. 왜그런가 봤더니 바로 모임 총 갯수와 멤버 총 수를..

study.getMembers().size();
study.getMeetings().size();

이런식으로 가져오고 있었습니다. getM~~s()를 할 때 마다 뷰에서 쿼리가 날아가고 있었던 거죠. 필요한건 size인데 굳이 저렇게 멤버와 모임 목록을 가져올 필요가 있을까 싶었습니다. 그렇다고 member와 meeting의 count를 가져오자니.. 그것도 역시 SQL 한줄씩이니까 1+2n이 쿼리만 바뀔 뿐이지 여전히 1+2n이구나.. @_@..
모델을 고치자.

Study에 memberCount와 meetingCount를 추가하고
각각 스터디에 참가신청/탈퇴할 때 memberCount를 증가/감소 시키고
모임이 추가/삭제 될 때 마다 meetingCount을 증가/감소 시키자.

그러면 화면에서는

study.getMemeberCount();
study.getMeetingCount();

이렇게 호출하면 되니까 연관관계 탈 것도 없고, Lazy 로딩할 것도 없고.. 쿼리도 안날아가고..

1+2n에서 1로 줄일 수 있겠구나.. 하고 생각했었습니다. 그런데....

5. 생짜 SQL

봄싹에서 화면 디자이너겸 SQL 튜닝 전문가이자 스프링 개발자로 활약중인 성윤군이 이를 보다 못해 SQL 하나를 직접 작성해 주었습니다.

1. 모든 스터디 정보와 모임 갯수, 멤버 수를 같이 서머리 해옵니다.

select
study.id, study.studyname, study.status,
(select count(*) from study_member as sm where sm.studies_id = study.id) as member_cnt,
study.maximum,
(select count(*) from meeting as meeting where meeting.study_id = study.id) as meeting_cnt,
study.startday,study.endday
from study as study

오호.. 무척 간단했군요. 이것도 역시 1+2n을 1로 줄여주는 방법이고, 모델을 수정하지 않아도 됩니다.
대신 DTO가 하나 필요합니다. 단순히 Study 정보만 담고 있는것이 아니라 Study 도메인 객체 리스트로 화면에 넘겨줄수가 없습니다.

결국은 "DTO가 하나 늘어난다 VS 모델을 수정한다" 이 것이 고민입니다. 패칭이 적절한 경우였다면 패칭으로 해결했을 텐데 지금 여기서는 count만 하면 되지 실제로 모임과 멤버 목록을 다 가져올 필요는 없거든요.

일단은 SQL도 짜준 성윤이를 생각해서 마지막 방법을 적용해봐야겠습니다. 대신 이 쿼리를 그대로 하이버네이트로 날려도 되지만 좀 더 객체지향 적인 형태로 Criteria나 HQL을 써서 표현해볼까 합니다.

과연~ 도메인 모델에 속성 두 개 추가하고 모임 추가/삭제, 멤버 가입/탈퇴 할 때 코드를 조금 수정하는 것보다 편할 것인가~~


top


[hamcrest] Matcher 만들기

모하니?/Coding : 2009.09.07 10:59


hamcrest는 Mockito와 JUnit 등 테스트 관련 프레임워크에서 참조하는 라이브러리로, Matcher라는 개념을 제공해줍니다. 이것을 사용해서 테스트를 좀 더 간편하게 만들 수 있습니다.

예를 들어, 다음과 같은 테스트가 있습니다.

ConfirmMailTest

...
        Member member = new Member();
        member.setEmail("keesun@mail.com");
        member.setName("keesun");
        ConfirmMail mail = new ConfirmMail(member);
        assertThat(Arrays.asList(mail.getTos()), hasItem("keesun@mail.com"));
        assertThat(mail.makeMessage(), containsString("회원 인증 메일 입니다"));
        assertThat(mail.getFrom(), is("s2cmailer@gmail.com"));
        assertThat(mail.getSubject(), containsString("keesun"));
...

이 테스트에서 필요한 Matcher로 isAbout(Membe member)라는 것을 하나 만들고 다음과 같은 클래스를 정의합니다. 아. 그전에 위 테스트 코드를 미리 다음과 같이 수정해 두는 것이 좋겠습니다.

        Member member = new Member();
        member.setEmail("keesun@mail.com");
        member.setName("keesun");
        ConfirmMail mail = new ConfirmMail(member);
        assertThat(mail, isAbout(member));

그런다음 클래스를 정의합니다. 이 클래스는 Mockito의 ArgumentMatcher 클래스를 상속 받고, isAbout이라는 static 메서드를 제공해줍니다.

public class MailMatcher extends ArgumentMatcher<SpringSproutMail> {

    private String from;
    private String to;
    private String subject;
    private String message;

    public MailMatcher(String from, String to, String subject, String message) {
        super();
        this.from = from;
        this.to = to;
        this.subject = subject;
        this.message = message;
    }

    @Override
    public boolean matches(Object mail) {
        SpringSproutMail smail = (SpringSproutMail) mail;
        if (from != null && !from.equals(smail.getFrom()))
            return false;
        if (!Arrays.asList(smail.getTos()).contains(to))
            return false;
        if (!smail.getSubject().contains(subject))
            return false;
        if (!smail.makeMessage().contains(message))
            return false;
        return true;
    }

    public static Matcher<SpringSproutMail> isAbout(Member member) {
        return new MailMatcher(SpringSproutMail.SENDER_MAIL, member.getEmail(),
                member.getName(), "회원 인증 메일");
    }

}

배보다 배꼽이 큰거 아닌가 하는 생각이 들지만, 이 매처 클래스를 여러 메일 테스트에서 사용할 수 있도록 조금 손을 본다면 쓸만할 겁니다.

top


[GenericDao] 하이버네이트 GenericDao

모하니?/Coding : 2009.09.04 15:23


먼저, GenericDao를 만들어 쓰면 좋은 이유를 생각해보겠습니다.
- 모든 DAO에서 중복되거나 반복되는 코드를 상당량 줄일 수 있습니다.
- 테스트도 그만큼 줄일 수 있습니다.
- 개발이 좀 더 빨라집니다.
- 비슷한 기능을 하는 메서드 이름을 통일할 수 있습니다.

Entity 당 DAO를 쓰면 좋은 점과 타입-안정성을 제공하는 DAO 패턴을 사용하면 좋은 점 등은 이 글에 정리되어 있으니 궁금하신 분들은 참고하세요

TDD로 다 만든 다음, 맨 마지막에 이클립스 리팩터링 기능 중에 extract interface로 뽑아 낸 인터페이스는 다음과 같습니다.

public interface GneericDao<E> {

    void add(E entity);

    List<E> getAll();

    E getById(Serializable id);

    void delete(E entity);

    void update(E entity);

    void flush();

    E merge(E entity);

}


이것을 구현한 실제 DAO 구현체는 이렇게 생겼습니다.

public class HibernateGenericDao<E> implements GneericDao<E> {

    protected Class<E> entityClass;

    @SuppressWarnings("unchecked")
    public HibernateGenericDao() {
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
       if (type instanceof ParameterizedType) {
         this.entityClass = (Class) ((ParameterizedType) type).getRawType();
       } else {
         this.entityClass = (Class) type;
       }
    }

    @Autowired
    protected SessionFactory sessionFactory;

    public void add(E entity) {
        getCurrentSession().save(entity);
    }

    private Session getCurrentSession() {
        return sessionFactory.getCurrentSession();
    }

    @SuppressWarnings("unchecked")
    public List<E> getAll() {
        return getCurrentSession().createCriteria(entityClass)
                .list();
    }
   
    @SuppressWarnings("unchecked")
    public E getById(Serializable id){
        return (E) getCurrentSession().get(entityClass, id);
    }
   
    public void delete(E entity){
        getCurrentSession().delete(entity);
    }
   
    public void update(E entity){
        getCurrentSession().update(entity);
    }
   
    public void flush(){
        getCurrentSession().flush();
    }
   
    @SuppressWarnings("unchecked")
    public E merge(E entity){
        return (E) getCurrentSession().merge(entity);
    }

}

특징이라고 할 수 있는 걸 꼽자면..
- 하이버네이트 SessionFactory를 사용하는 GenericDAO 라는 것.
- 별도로 엔티티 타입을 인자로 넘겨줄 필요가 없다는 것.
- 타입-안전성을 보장하기 때문에 별도의 캐스팅 등이 필요없고, 컴파일 시점에 체크 가능하다는 것.

이 클래스는 다음과 같은 테스트 클래스를 이용해서 TDD로 만들었습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testContext.xml")
@Transactional
public class HibernateGenericDaoTest extends DBUnitSupport{

    @Autowired TestDao dao;
   
    @Test
    public void add() throws Exception {
        TestDomain entity = new TestDomain();
        dao.add(entity);
        assertThat(dao.getAll().size(), is(1));
    }
   
    @Test
    public void getAll() throws Exception {
        insertXmlData("testData.xml");
        assertThat(dao.getAll().size(), is(2));
    }
   
    @Test
    public void getById() throws Exception {
        insertXmlData("testData.xml");
        assertThat(dao.getById(1).getName(), is("keesun"));
    }
   
    @Test
    public void delete() throws Exception {
        insertXmlData("testData.xml");
        TestDomain entity = dao.getById(1);
        dao.delete(entity);
        assertThat(dao.getAll().size(), is(1));
    }
   
    @Test
    public void update() throws Exception {
        insertXmlData("testData.xml");
        // entity is (similar)detached object
        TestDomain entity = new TestDomain();
        entity.setId(1);
        entity.setName("whiteship");
       
        dao.update(entity);
        // now, entity has been persistent object
        entity.setName("helols");
        dao.flush();
        assertThat(dao.getById(1).getName(), is("helols"));
    }
   
    @Test
    public void merge() throws Exception {
        insertXmlData("testData.xml");
        // entity is detached object
        TestDomain entity = new TestDomain();
        entity.setId(1);
        entity.setName("whiteship");
       
        TestDomain newEntity = dao.merge(entity);
        // newEntity is persistent object, but entity is still detached object
        newEntity.setName("helols");
        entity.setName("nije");
        dao.flush();
        assertThat(dao.getById(1).getName(), is("helols"));
    }
   
}

이 테스트의 특징은 다음과 같습니다.
- 하이버네이트의 update()와 merge()의 특징과 그 차이점을 이해할 수 있도록 작성했습니다.
- 스프링 테스트 컨텍스트를 사용했습니다.
- DBUnit과 그것을 확장한 클래스를 이용했습니다.


생각해볼 것
- GenericDao에 있는 update(), merge(), flush()는 Generic하지 않다는 생각이 듭니다.
- (위에는 보여드리지 않았지만)테스트에 사용된 TestDomain 클래스와 TestDao를 GenericDaoTest 내부에 포함 시키는 것이 좋치 않을까?
- 어떤 기능을 더 추가할 수 있을까?


top


[Generic] 자바 Generic 타입 알아내기

모하니?/Coding : 2009.09.04 14:35


참조: http://blog.xebia.com/2009/03/09/jpa-implementation-patterns-data-access-objects/

GenericDao를 만들다 보면, entity 클래스 타입이 필요하게 되는데, 이 타입을 구하기 위해 GenericDAO를 상속받을 때 마다 생성자에서 넘겨주거나, abstract 메서드로 entity 클래스를 세팅하도록 강제하기도 하기도 하고, 저 같은 경우는 DAO 이름 컨벤션에 따라 도메인 이름을 추측하여 Class.forName()으로 자동으로 인식할 수 있게 한적도 있습니다.

일단, 전자의 방법들은 GenericDAO를 상속받는 클래스를 만들 때 귀찮다는 단점이 있습니다. 맨 마지막 방법은 컨벤션을 따르지 않았을 경우 난감해진다는 문제가 있습니다.

그러나... 이 방법들 보다 더 좋은 방법이 있었습니다.

자바 Generic 타입 정보는 흔히들 런타임에 활용할 수 없다고 알고 계실겁니다. 그건 객체들 얘기이고, 모든 객체가 지니고 있는 클래스(.class)에는 해당 정보가 남아있습니다. 따라서 런타임 때도 리플렉션을 이용해서 Generic 정보를 활용할 수 있습니다.

http://whiteship.me/1614

    @PersistenceContext
    protected EntityManager entityManager;
 
    public JpaDao() {
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        this.entityClass = (Class<E>) genericSuperclass.getActualTypeArguments()[1];
    }

참조 링크에 있는 JPA 패턴을보다가 발견한 이 코드를 응용해서 사용하시면 되겠습니다.

이로써, 보다 깔끔한 GenericDao를 만들 수 있겠네요. 하나 덤으로 위 링크에 첨언을 하자면, 위 링크에서는 주키 타입도 Generic 타입으로 지정해서 일일히 지정받고 있는데, 저는 그것보다 Serializable을 사용하면 타입 인자를 하나 더 줄일 수 있는데다, 키 타입에 대해 별다른 제약도 없기 때문에 더 깔끔하지 않나 생각하빈다.

ps: 오랜만에 HibernateGenericDao를 TDD로 만들고 있는데 재밌네요. 별거 아니지만;; 만들면 공개해볼까 합니다.
top


[봄싹 오픈] springsprout.org

모하니?/Coding : 2009.08.27 22:08


www.springsprout.org

드디어 봄싹 스터디 새로운 시즌을 시작합니다.


스터디에 가입 하지 않으셔도 어느 정도 구경은 하실 수 있습니다. 어느 정도만~
아직 미흡한 부분이 많지만, 분명히 점차 개선될 겁니다.

1차 오픈을 할 수 있었던 건 전부 봄싹 개발에 참여해주신 열 분의 개발자들 덕분입니다.

이것 저것 프로젝트 하느랴 바빴을텐데 가장 열정적으로 참여해준 성윤이.
스터디쪽 미흡하다고 한 마디 했더니 몇 일만에 모든 기능을 다 만들어버린 재진이.
깔끔한 개인화면 만들어준 막내.
다소 늦게 개발에 참여했지만, 그래도 저와 함께 회원 모듈을 당당하고 파일 업로드를 담당하고 계신 종봉이형
Ajax, 시큐리티, 제이쿼리, 맵 오픈 API 등 두루 두루 담당하고 개발해주신 정우형.
오프라인에서는 못 봤지만, 어느새 온라인에서 혼자 세미나를 전부 만들어준 명수형.
오프라인에도 자주 참석해주신 소내기형. 역시 혼자 위키를 전부 만들어주셨습니다. 아주 든든해요. 캬~
개발에는 직접 참여해주시지 않았지만, 메일링 통해서 여러 조언을 해주신 성철형님
그리고 마지막으로 약속을 지켜낸 저까지.

현재는 이렇게 10명이 만들고 가꾸고 있습니다. 앞으로 더 여러 개발자들과 함께 가꿔나가고 싶네요.
top


[봄싹] D-day 오후 10시에 오픈하겠습니다.

모하니?/Coding : 2009.08.27 15:58


현재 피튀기게 개발 중입니다. 몸이 10개였으면 좋겠어요. 안 믿으실까봐.. 증거자료 재출합니다.


보세요.. 불과 1시간전에 올린 글인데.. 댓글이 장난 아니죠. @_@ 결국은 명수형이 태그파일 만드는 걸로 결론이 났군요. 캬...

1차로 css로 간단하게 적용하고
2차로 스프링이 제공하는 HTMLUtils를 써보고
3차로 그것을 태그파일로 다듬고
4차는 다시 1~3차를 엎어버리고, 에디터를 적용하는 것..

이 중에서 3차까지는 한 다음 오픈을 할 것 같습니다. 따끈따근한 봄싹 개발 현장이었습니다. ㅎㅎ

불량사원으로 찍히실까봐.. 닉넴은 대충 모자이크 처리했습니다.
봄싹의 에이스 개발자들이 불량 사원으로 찍히는 이것이 바로 "봄싹 딜레마"

top


[봄싹] 시즌 2 오픈 D-1

모하니?/Coding : 2009.08.26 07:18


내일 오픈 합니다. 이번 주 월요일부터 예비군 동미참훈련? 때문에 월.화,수를 교장으로 출퇴근하고 있습니다. 막바지에 소중한 시간을 뺏겨서 아쉽긴하지만, 다른 봄싹 에이스분들께서 많이 신경써 주셔서 무사히 원했던 일정에 맞춰서 오픈할 수 있게 됐습니다.

봄싹 프로젝트와 관련해서는 하고 싶은 이야기들이 상당히 많은데, 그 중 몇 가지 주제들만 뽑아 보면 다음과 같습니다.

- 각종 프레임워크 및 개발환경 도입 성공 사례(하이버네이트, 시큐리티 3.0, 메이븐, CI 툴, 테스트 등)
- 기트 도입 실패 사례(이전에 공유한바 있습니다.)
- 봄싹 개발 프로세스
- 자율적이고 자연스러웠던 작업 세분화
- 데드라인
- 메일링 리스트 활용

이 것들은 제가 느낀점들이고, 다른 봄싹 에이스 분들께서 느낀 주제들도 잘 정리해서 공유할 날이 있을 것 같습니다.

드디어.. 마음대로 주무를 수 있는 홈 페이지가 생겨서 정말 기쁩니다. 비록 몇 달간은 베타 버전으로 공개하겠지만, 차차 스터디에 필요한 기능들을 더 다듬고 추가하면서 꾸준히 발전할 겁니다. 봄싹 파이팅~!!
top


[봄싹] 시즌2 다음 주 오픈 예정

모하니?/Coding : 2009.08.17 11:08



날짜를 보세요. 이 많은 이메일이 대부분 토요일과 일요일 그리고 월요일에 토론 중인 메일 목록입니다. 캬... 멋지죠.

열심히 달리고 있는 봄싹 개발자는 현재 총 10명입니다.
기선, 재진, 성윤, 정우, 재일, 수진, 대웅, 종봉, 명수, 성철.
모두 정말 감사합니다.

다음주 주말 전에 1차 오픈 준비가 끝나면, 급하게라도 개발자 페이지를 추가하겠습니다.
일주일만 더 불량 사원으로 지내봅시다!! 봄싹 베스트 회원이 되실겁니다!! 파이팅!!!!

top


[테스트] 커스텀 MimeMessageHelper 테스트하기

모하니?/Coding : 2009.08.14 14:07


MimeMessage를 사용해서 실제로 메일을 보내보고, 메일이 깨지지는 않는지 확인해보고 싶어서 다음과 같은 테스트를 작성했습니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
public class SignupConfirmMimeMessageHelperTest {

    @Autowired
    JavaMailSender mailSender;

    @Test
    public void sendInteface() {
        MimeMessage mimeMessage = mailSender.createMimeMessage();
        Member member = new Member();
        member.setEmail("whiteship2000@gmail.com");
        assertNotNull(mimeMessage);
        SignupConfirmMimeMessageHelper helper = new SignupConfirmMimeMessageHelper(mimeMessage);
        helper.makeMessage(member);
        mailSender.send(mimeMessage);
    }

}

테스트를 만들면서 JavaMailSender와 MimeMessage, SignupConfirmMimeMessageHelper 클래스를 어떻게 사용할지 고민을 했죠. 생성자에 member도 같이 줘봤다가.. 뺏다가.. 했습니다. 결국은 빼냈습니다. 중요했던 건 SignupConfirmMimeMessageHelper 클래스의 생성 방법과 사용방법 이었습니다.

저런식으로 실제 메일을 보내 본 뒤.. 그대로 두면.. 테스트가 돌 때 마다 저한테 메일을 보내줄 겁니다. 상당히 귀찮은 테스트입니다. 그래서 생각을 했습니다. 안 되겠다. 어차피 메일 보내는 것도 확인했고.. 인코딩도 확인했고.. 내가 이 테스트에서 정하고자 했던 건 SignupConfirmMimeMessageHelper 클래스의 생성자 구조랑 사용법이니깐... 단위테스트로 고치자~

@RunWith(MockitoJUnitRunner.class)
public class SignupConfirmMimeMessageHelperTest {

    @Mock JavaMailSender mockSender;
    @Mock MimeMessage mockMessage;

    @Test
    public void sendInteface() {
        assertNotNull(mockSender);
        assertNotNull(mockMessage);
        when(mockSender.createMimeMessage()).thenReturn(mockMessage);
       
        MimeMessage mimeMessage = mockSender.createMimeMessage();
        Member member = new Member();
        member.setEmail("whiteship2000@gmail.com");
        SignupConfirmMimeMessageHelper helper =
            new SignupConfirmMimeMessageHelper(mimeMessage);
        helper.makeMessage(member);
        mockSender.send(mimeMessage);
    }

}

그래서 이렇게 고쳤습니다. 고치고 나니까... 이런 생각이 드네요.


맨 마지막 줄만 지워버릴껄 그랬나.... 허허헐...
top


[스프링 이메일] MimeMessageHelper 초간단히 사용하기

모하니?/Coding : 2009.08.14 12:09


    private MimeMessage makeTestMimeMessage() throws MessagingException,
            AddressException {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message);
        helper.setTo("whiteship2000@gmail.com");
        helper.setFrom("s2cmailer@gmail.com");
        helper.setSubject("This is the Subject Line!");
        helper.setText("<h1>This is actual message</h1><a href=\"ads\">hi</a>", true);
        return message;
    }

    private MimeMessage makeTestConfirmMimeMessage() throws MessagingException,
            AddressException {
        MimeMessage message = javaMailSender.createMimeMessage();
        message.setFrom(new InternetAddress("s2cmailer@gmail.com"));
        message.addRecipient(Message.RecipientType.TO,
                                 new InternetAddress("whiteship2000@gmail.com"));
        message.setSubject("This is the Subject Line!");
        message.setContent("<h1>This is actual message</h1><a href=\"ads\">hi</a>",
                           "text/html" );
        return message;
    }

둘 중에 어떤 코드를 쓰느냐는 코딩하는 사람 맘이겠지만, 위에 있는 것이 훨씬 깔끔해 보이지 않나요. Helper를 이용해서 좀 더 직관적인 메서드 이름과 편리한 인터페이스로 MimeMessage를 작성할 수 있습니다.

애용하세요~
top


[봄싹] 간단하지 않은 회원 관리

모하니?/Coding : 2009.08.11 12:37


봄싹 시즌 2 개발 중에 자연스래 담당하는 모듈이 나뉘어져서 요즘은 주로 회원 관리 모듈만 개발하고 있습니다. 처음엔 뭐 간단하겠지.. 별거 있나.. CRUD만 하면 되겠지.. 라고 생각했었는데 그게 아니더군요.

일단 회원 관리와 더불어 인증/권한까지 같이 담당하게 되었는데, 사용자가 회원 가입과 로그인을 하고 그 사용자에게 권한을 부여하는 관리자 기능까지 생각해보면 할 일이 심심치는 않았습니다.

1. 회원 가입

로그인과 더불어 사용자가 가장 빨리 접하는 기능 중 하나 일 겁니다. 그래서 디자인에도 조금 신경을 써야했습니다. 그리고 스팸 사용자도 막고 싶었습니다. 캡차를 쓸 수도 있겠지만, 이메일 인증 방법을 사용하기로 결정했습니다. 회원 가입을 서브밋 하면 인증 메일을 전송하고, 사용자가 인증 메일의 링크를 클릭하면 사용자 상태를 인증 된 상태로 바꿔주는 겁니다. 그 전에는 로그인이 되지 않습니다.

검증은 2단계로 화면단에서 한 번, 서버 단에서 한 번 그렇게 두 번 걸러집니다. 이때, 클라이언트 쪽 검증은 제이쿼리의 validation 플러긴을 사용했으며, 중요한 검증 작업 중 하나로 email 중복 체크가 있습니다. 이 부분이 제대로 동작하지 않더라도 최종적으로는 DB단에서 unique 제약 위반으로 에러가 나고 해당 회원 정보는 추가되지 않을 겁니다. 하지만, 그 에러 메시지를 만나게 하기 전에 사용자가 email만 입력한 상태에서 검증을 해주고 싶습니다. Ajax가 필요한 시점입니다. 하지만 요청을 너무 자주 보내고 싶진 않기 때문에, 기본적인 email 형태를 갖췄는지 validation을 한 뒤에 해당 검증을 잘 통과 했을 경우에만 ajax로 요청을 보내서 중복 된 email이 아닌지 확인해 줍니다.

이렇게 화면단/서버단 검증이 잘 끝나서 회원 가입을 진행하게 되면 이메일을 전송하느라 3~5초 정도 시간이 걸리는데 이 시간 동안 사용자에게 뭔가 진행중이라는 사실을 알려주고 화면을 블러킹해줍니다. 이 때 아이튠즈의 블러킹 화면과 비슷한 화면을 보여주도록 했습니다.ㅋㅋ 단, IE에서는 모서리가 각져서 보인다는 거... FF나 Safari에서는 잘 보였습니다.

회원 가입 폼 처리가 끝난 뒤, 회원 가입 인증 대기 화면을 보여줍니다.

2. 가입 승인 대기

회원 가입 인증 메일을 전송했기 때문에, 이제 사용자는 해당 메일을 확인하고 그 메일에 들어있는 링크를 클릭하여 회원 가입 인증을 거칠 수 있습니다.

하지만 만약 사용자가 메일을 한 번 받아놓고 다른 일을 하다가 메일이 쌓여서 봄싹이 보낸 메일이 어딨는지 못 찾았다면? 혹은 메일을 실수로 지웠다면? 해당 사용자는 답답해 할 겁니다. 그래서 인증 메일 재전송 기능이 필요했습니다.

또한 현재 페이지에서 회원이 사용중인 메일 서비스로 바로 이동할 수 있는 링크도 유용할 겁니다.

마지막으로 승인을 받은 사용자에게 기본 권한을 설정해줍니다. 기본으로 모든 사용자는 USER라는 role을 가지도록 구현해 줬습니다. 그래야 익명 사용자와 가입한 사용자를 구분할 수 있을테니 말이죠.

스팸 사용자가 발생할 수 있습니다. 인증을 거치지 않고 계속 가입만 한 사용자들이 DB에 쌓일 수 있죠. 그럴 경우에 대비해서 주기적으로 배치를 실행해서 인증을 거치지 않은 사용자 정보를 삭제해 줍니다.

2. 로그인

여차 저차해서 회원 가입 인증까지 끝낸 사용자는 로그인 화면을 보게 될 겁니다. 회원 가입 인증 메일에 들어있던 링크가 로그인 화면으로 보내주면서 내부에서는 인증 작업을 해주는 것이죠.

이제부터는 스프링 시큐리티와 관련이 있는데, 그다지 어렵진 않았습니다. 스프링 시큐리티를 공부하는게 아니라 그냥 간략하게 사용만 하는 것이니까요. 그다지 어려울 것은 없습니다. 인증 방법은 여러가지가 있는데 일단은 가장 흔한 폼 인증 방식을 선택했습니다. 다음에는 OpenID로 지원해볼 생각이지만, 당장은 아닙니다.

아직 가입을 하지 않은 사용자가 로그인 화면에 들어온 경우가 있을 수 있기 때문에 회원 가입 화면으로 가능 링크가 필요합니다.

또한 가입은 했지만 아직 인증을 거치지 않은 사용자가 있을 수 있는데, 그런 경우 가입 승인 대기 화면으로 포워딩을 하고, 이메일 인증을 받도록 합니다.

비밀 번호를 잊어버렸을 수도 있는데, 이런 경우 가입시 입력한 이메일을 다시 입력받아서 해당 이메일로 비밀번호를 전송해 줍니다. 이 떄 입력하는 이메일도 검증 작업이 필요하겠죠.

3. 관리자

다행히(?) 이 부분은 제 담당이 아닙니다. 성윤군이 담당하고 있는데 제이쿼리를 이용해서 드래그앤드랍으로 권한을 변경할 수 있게 해준다니.. 기대중입니다.

회원 전체 목록 조회.

회원 정보/권한 조회.

회원 정보/권한 변경.

회원 삭제.

모든 목록을 뽑아낸 것이 아니라, 지금까지 완료한 작업이나 오늘 내일 처리할 작업까지만 정리한 것입니다. 아마도 생각하지 못한 작업들이 더 있겠지요. 간단해 보였던 회원 관리가 이렇게도 할 일이 많았다니.. 다른 모듈들은 어떨지 걱정입니다. 이번 주 내로 회원 관리를 정리하고 일손이 모자른 스터디 모듈이나 관리자 모듈쪽에 붙어야겠습니다. 그쪽도 아주 재밌을텐데 말이죠.
top


[봄싹]기트 도입 실패 사례

모하니?/Coding : 2009.08.06 18:59


봄싹 스터디에서 Git라는 분산 VCS를 사용해 보기로 결정하고, 사전 조사를 거친다음, 간단한 사용법을 공유하고, 개발을 시작했다. 그러나 개발은 더뎠다. 얼마전 더디다 못해 거의 진척이 없다시피 하는 모습을 보고 Git에서 SVN으로 버전 관리 시스템을 바꿨다. 그리고 프로젝트의 데드라인도 설정했다. 그러자... 이게 왠일인가..


불과 2주 만에 총 9명의 개발자가 온/오프라인에서 뜨겁게 개발에 참여하고 있다. 현재 이 모습은 내가 봄싹 구글 그룹스를 처음 만들 때 상상하던 모습이다. 이대로만 간다면, 8월 29일 데드라인 안에 사이트 1차 개발을 마칠 수 있을 것 같다.

바뀐 요인은 딱 두 가지. 1. 데드라인 설정. 2. 개발자에게 보다 편리한 개발 환경으로 전환. 어쩌면 2번은 1번으로 인해 불가피 했을지도 모르겠다. 처음에는 느긋하게 기트에 적응해가자면서 개발을 하자고 생각했었다. 하지만 그것은 오산이었다.

기트를 사용하고 있지만, 기트를 사용하는 시나리오는 예전 SVN을 사용하던 때와 별반 다르지 않았다. 수시로 branching/merging을 하지 않을 꺼라면 굳이 Git를 사용할 필요가 없다는 것을 몸소 체험했으며, 중간 관리자를 거쳐야만 하는 대규모 개발도 아니기 때문에 한방에 서버로 바로 커밋/업데이트하는 SVN이 그립기도 했다. 또한 이클립스 툴 지원이 아직도 미약했다. 마지막으로 별도의 기트 서버를 운영하지 않고 GitHub를 이용했는데, 나중에는 사용자가 많아져서 계정 관리하는 것이 어려웠다. 이 부분은 아마도 봄싹에서 GitHub를 잘못이용한 것이 아닌가 하는 생각이 든다. 애초에 내 계정에 다른 개발자들의 공개키를 등록하는것이 아니라, 프로젝트에서 별도의 브랜치를 따다가 자기 계정에서 관리하는 형태로 프로젝트를 진행했어야 하는 듯 하다.


그래서 모두에게 익숙한 SVN으로 넘어왔다. 결국은 기트 도입이 실패했지만, 프로젝트는 실패하지 않았다. 어쩌면 그로인해 프로젝트 성공의 길로 한 걸음 더 다가간 것 같이 느껴진다. 비록 이번에는 기트 도입이 실패했지만, 다음에 적절한 상황이 오면 다시 시도해 볼 생각이다. 다음에는 기트허브에서 새로운 방식으로 개발을 진행하던지, 별도의 기트 서버 환경을 구축한 뒤에 해볼 생각이다. 그때가 되면 이클립스 툴도 조금은 진전이 있겠지...??

ps: 예상외로 메이븐 도입에 대해서는 다들 잘 수긍하는 편이었다. 처음부터 메이븐 리파지토리, 플러긴, 페이스, 골 등의 개념 설명을 한 적이 없고, 필요한 명령어만 몇개 알려드리고, pom.xml에 의존성 추가하는 것만 알려드렸다. 기트가 워낙 충격적이어서 그랬나...? 아무튼 메이븐은 기트에 비하면 도입이 쉬운편이었다. 봄싹에선 말이다.



top


[SES 프로젝트] 베타 버전 완성~~!!

모하니?/Coding : 2009.07.31 16:36


아... 이런 감격적인 순간에는 스크린캐스팅을 찍어야 하는데... 스크린캐스팅은 나중에 집에가서 찍기로 하죠.

이번에는 사용법만 간략히 설명드리죠.

1. SES 베타 버전 친구 추가하기.

구글토크에 SES(s2cmailer@gmail.com)를 추가해 줍니다.


2. 말을 걸어 봅니다. 채팅창에 보이지 않는다면, 검색해서 찾아줍니다.


3. 이제 이걸 가지고 즐겁게 영어 공부를 합니다.

3-1. 일단은 문제를 몇 개 넣어보세요.

q:질문 a:답변

또는

q:질문 a:답변 r:o

형태로 넣어주세요. 이때 주의할 것이 있는데
- q: 왼쪽으로 빈공간이 있으면 안 됩니다.
- q: 와 질문 사이에 빈 공간이 생기면 안 됩니다
- 질문과 a: 사이의 공간은 딱.. 스페이스 하나 입니다. 더도 덜도 안 됩니다.
- a:와 답변 사이에도 빈공간이 들어가지 않습니다.
- r:o를 입력하지 않을 때는 답변 맨 뒤에 빈공간이 오면 안 됩니다.
- 답변과 r:o 사이의 공간도 딱... 스페이스 하나 입니다. 더도 덜도 안 됩니다.
- r:o 뒤에 빈공간이 있으면 안 됩니다.
- r:o에서 o는 영어 소문자 입니다.
- r:o를 입력하면, 질문과 답변을 맞 바꾼 문제도 추가됩니다.
(q:apple a:사과 입력시 q:사과 a:apple 문제도 추가됩니다.)

3-2. 입력한 문제들을 확인해 보세요.

list

간단하죠. 그러면 문제 목록이 쫙.. 나올껍니다.

3-3. 질문을 받으세요.

q

그럼 SES가 질문을 던집니다.

3-4. 답변하세요.

질문에 대한 답을 입력하세요.

끝입니다!. 참~ 쉽죠~잉


꺄오~~~ 만들었어 만들어~~~ 예에~~

ps: TDD로 만들었어요~(러너 뺴고)
top


[SES 프로젝트] Self English Study 프로젝트를 시작합니다.

모하니?/Coding : 2009.07.31 13:30


영어 공부도 스스로 하는거죠. 스스로 할 때 도움이 되는 프로그램을 만들까 합니다. 제가 주로 사용하고 있는 구글토크에 SES를 친구로 등록해주시면 다음과 같은 일을 할 수 있습니다.

1. 문제 추가하기

예제 1)
q:apple a:사과 r:o
=> OK q:apple a:사과 r:o

예제 2)

q:I ____(cycle) home yearterday when a man ____ (step) out into the road in front of me. a:was cycling, stepped
=> OK q:I ____(cycle) home yearterday when a man ____ (step) out into the road in front of me. a:was cycling, stepped

예제 1은 질문과 답변이 바껴도 상관없는 문제를 추가한 것이고, 예제 2는 질문 답변이 바뀌면 안 되는 문제를 추가한 것입니다. r:o로 질문과 답변이 바껴도 되는지 결정하는데, 기본값이 r:x 인거죠.

2. 문제 요청하기

q
=>q:apple

대화창에 q를 입력하면 입력한 문제 중에 랜덤으로 선택해서 질문을 합니다.

3. 답변하기

예제 1)
=>q:apple
사과
=> OK

예제 2)
=>q:apple
몰라...
=> (바보 멍청이 똥개 멍게 말미잘 해삼) a:사과

그냥 답을 입력하면 되며, 맞으면 OK, 틀리면 정답을 알려줍니다.

4. 문제 목록 보기

list
=>
q:apple a:사과 r:o
q:I ____(cycle) home yearterday when a man ____ (step) out into the road in front of me. a:was cycling, stepped

채팅창에 list를 입력하면 화면에 위와 같이 질문/답변 목록을 뿌려줍니다.

5. 초기화하기

clear
=> clear!

clear를 입력하면 저장해두었던, 문제들이 모두 날아가고, 응답으로 clear!를 출력합니다.

--------------------------
오늘은 일단 여기까지 만들어서 구글토크에 올려두는 것이 목표고 현재까지 생각난 추가로 할 일들은 다음과 같습니다.

1. 메시지 바인더를 추가할 것.
2. 특정 문제 수정.
3. 특정 문제 삭제

top


[smack] 라이브러리 추가 및 기초 코드

모하니?/Coding : 2009.07.30 16:40



교묘하게 비번을 가렸습니다. 후훗. 저 계정은 저희 스터디에서 쓸 계정이기 때문에 유충되면... 다른 아이디를 만들면 됩니다.ㅋㅋ

참조: http://www.andrejkoelewijn.com/wp/2008/12/30/using-google-talk-from-java-example/

위 코드를 참조했고, 라이브러리는 일단 메이븐 리파지토리를 추가해줍니다.

        <repository>
            <id>m2-repository-smack</id>
            <url>http://maven.reucon.com/public</url>
            <releases>
                <enabled>true</enabled>
            </releases>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>

다음으로 의존성을 추가해 줍니다.

        <dependency>
            <groupId>org.igniterealtime.smack</groupId>
            <artifactId>smack</artifactId>
            <version>3.0.4</version>
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.igniterealtime.smack</groupId>
            <artifactId>smackx</artifactId>
            <version>3.0.4</version>
            <exclusions>
                <exclusion>
                    <groupId>javax.servlet</groupId>
                    <artifactId>servlet-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

끝~ 3.0.4 버전 말고 더 최신 버전도 나온 듯 한데.. 그건 아직 안 받아봤습니다.
top

TAG smack

[smack] 구글 토크 봇 - 파일럿 프로그래밍

모하니?/Coding : 2009.07.30 16:04


구글 토크 봇을 만들고 싶어서 이것 저것 찾아보다가 결국엔 가장 중요한 사이트인 http://www.igniterealtime.org/ 이곳이 사이트 점검 중이라 막막해졌습니다. 이 사이트에서 제공하는 Smack(자바로 XMPP 프로토콜을 사용할 수 있게 해주는 프레임워크)을 사용해야 조금이라도 더 편하게 봇을 만들 수 있을텐데 사이트가 막혔으니..

'XMPP와 Jabber 책을 보면서 코딩해야되는건가... 아이 귀찮아... 하지 말까...' 하는 고민에 빠졌습니다.

그러나~~ 저 사이트에는 못 들어가더라도 누군가 메이븐 jar를 어느 리파지토리엔가는 올려놨겠지.. 그래! 그냥 라이브러리를 받고 관련 아티클 찾아서 코딩해보자~. 마음 먹고.. 구글신을 통해 이것 저것 찾아본 결과..


자바 코드로 구글토크에 접속해서 제 계정으로 메시지 하나(hello 기선)를 보내놓고 나가버리는 봇을 코딩 해볼 수 있었습니다. 음하하핫 재밌어요 재밌어~~ 이걸로 구글 토크 봇만들기 파일럿은 성공했다고 봐도 되겠습니다.

보아하니.. 서버 프로그래밍을 해야 할 것 같은데, 동시성 제어도 하고 이것 저것 많은 공부가 될 듯 하네요. 요즘 한참 인기가 상승하고 있는 아이폰 프로그래밍을 해볼까 생각했었는데, 그럴 여유가 없겠군요.

만들고 싶은건 요즘 공부중인 영어 문장들을 한글로 물어보면 저는 영어로 답변을 해주고 그럼 서버에서 그 영어가 맞았는지 틀렸는지 판별해주는.. 그런 영어 학습 봇입니다. 오늘 외울 문장들을 저장할 수 있는 기능도 있어야겠고, 전체 목록을 보여주는 기능도 있어야 하고.. 뭐 기타 등등 이건 한 번 정리를 해봐야겠네요.

또 스터디용 봇. study? 라고 치면 봇이 이번주 몇시부터 몇시까지 어디서 무슨 스터디를 한다고 정보를 주는거죠. 스터디 공지도 메시지로 쫙 뿌려주구요.

캬~~

소스코드와 Smack 라이브러리를 메이븐으로 추가하는 방법은 비밀입니다. 영원히~ 어쩔 수 없어요. 이제 세상의 이치에요.
top


스프링 이메일 확장하기

모하니?/Coding : 2009.07.29 21:49


Gmail의 SMTP를 이용하여 회원 가입시 메일을 보내서 회원 인증 링크를 보낸다. 그리고 해당 링크를 클릭할 시 회원 가입을 승인한다.

이러한 요구사항이 있습니다. 여기서 앞부분에 스프링 이메일을 사용하여 기능 구현을 하려고합니다.

먼저 GmailSender라는 녀석을 만들었습니다. 그러나 그런 클래스는 필요가 없었습니다. host, port, username, password만 설정하면 스프링이 제공하는 JavaMailSender가 GmailSender와 같은 녀석이 되기 때문이죠. 혹여나 그런 설정 이외에도 send()의 결과를 확인하고 싶어서 boolen 값을 반환해주도록 JavaMailSender의  send 메서드를 try-catch로 랩핑한 클래스를 만들었습니다. 하지만, 생각해보면 어차피 MailException은 런타임 예외기 때문에 false를 반환한다는 건 불가능한 일입니다. 오직 true 값을 반환하거나/MailExcpetion을 던지는 메서드가 되고 마는데... 이런 녀석이 존재할 필요가 있을까 싶어서 없애기로 결정했습니다.

없앨 코드 1.

    public boolean send(SpringSproutSimpleMailMessage mailMessage) {
        try {
            mailSender.send(mailMessage);
        } catch (MailException e) {
            throw e;
        }
        return true;
    }

또 하나 떠오르는 녀석은 SignupMailSender입니다. 이 Sender는 특정 Member를 인자로 받으면 해당 Member의 email로 Signup 관련 URL을 만들어서 보내주는 녀석입니다. 하지만 이 녀석도 잘 생각해보면, Sender의 책임이 두 가지가 되고 말았습니다. SRP를 위반하게 된거죠. 메시지를 만드는 일과 메일을 보내는 일이 합쳐졌습니다. 따라서 아래 코드의 대부분은  MailMessage 쪽으로 넘기는게 좋겠습니다.

리팩토링할 코드 2

    public boolean sendConfirmMail(Member receveMember) {
        List<Member> receives = new ArrayList<Member>();
        receives.add(receveMember);
        String authUrl;
        try {
            authUrl =
                    "http://localhost:8080/springsprout2/signupconfirm.do?email="+
                    URLEncoder.encode(receveMember.getEmail(), "UTF-8") +
                    "&authCode="+
                    URLEncoder.encode(receveMember.getAuthCode(), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("can not encoding url", e);
        }

        SpringSproutSimpleMailMessage message = new SpringSproutSimpleMailMessage(
                "[봄싹 SpringSprout] 사용자 인증 메일",
                "사용자 계정을 활성화 하려면 다음 링크를 클릭하세요. <a href=\""+authUrl+"\">"+authUrl+"</a>",
                receives);
        return mailSendService.send(message);
    }

마지막으로 한 개의 클래스가 더 떠오르는데, 바로 위에 보이는 MailMessage를 확장한 S3M2(SpringSproutSimpleMailMessage) 클래스입니다. 하지만 이 클래스의 이름에서 클래스가 무슨 일을 하는지 의도를 파악하기가 힘듭니다. 이 클래스는 생성자로 subjer, text, member list를 받아서 from, subject, text, to 등의 속성을 세팅해주는 클래스 입니다. 아쉬운 건 아직 이 녀석 생성자에 단일 member 객체를 받는 생성자가 없다는 것이죠. 그래서 위의 코드를 보면 Member List를 만들어서 보내는 모습을 볼 수 있었죠.ㅎㅎ 이 MailMessage는 봄싹에서 사용할 기본 메일 메시지를 설정하기 때문에, Abstract 클래스로 만들고, 좀 더 구체적인 메시지로 가입승인메시지(SignupConfirmMailMessage) 같은 녀석이 그것을 확장하는 것이 좋아보입니다. 그러면 클래스의 이름에서 의도도 알 수 있고, S3M2에 있던 과중한 업무도 분담이 될테니 말이죠.

결국 아래와 같은 구조가 될 듯 합니다. 오늘 밤... 코드를 조금 뜯어 고쳐야겠습니다. 새기능 만들것도 많은데 이번 주는 뜯어 고치는데 시간을 다 쓰네요~ 유후~


ps: 참..오늘의 영어과제부터 하고나서
top


[하이버네이트] OneToMany에 FetchType.EAGER 사용시 어떤 일이 생길까?

모하니?/Coding : 2009.07.28 20:28


Plan -> PlanDetail 관계에서 Plan 쪽에서 PlanDetail로 OneToMany 관계를 설정하고, fetch 모드를 EAGER로 설정하면,, 엄청난 문제가 생길 수 있습니다. @_@

P 1 <-- PD 1
P 1 <-- PD 2
P 2 <-- PD 3
P 3

이렇게 PD 두 개가 같은 P에 속해있을 경우, P 목록을 뿌리고자, createQuery("from P").list(); HQL로 이렇게 작성하면.. 쿼리는 다음과 같이 날아가게 됩니다.

(양방향 관계에서 mappedby 설정했다고 가정하면..)
~~ from P p left outer join PD pd on p.id = pd.p_id ~~

(양방향 관계에서 mappedby를 설정하지 않았다고 가정하면.. 이건 거의 최악)
~~ from P p left outer join P_PD p_pd on p.id = p_pd.p_id ~~

LEFT OUTER JOIN 인거죠...ㄷㄷㄷ..

결과는 아래와 같은 모습일 겁니다.

P 1 - PD 1
P 1 - PD 2
P 2 - PD 3
P 3 - null

그래서 DB에서는 레코드가 한 줄인데, 화면에는 두 줄이 나타납니다. 크하하하..  그런데.. 이게 .. 이상한 일일까요? 글쎄요. 그런 것 같진 않습니다. P가 가지고 있는 컬렉션을 EAGER 패치로 가져오란 얘기가 곧 DB 관점에서는 P를 왼쪽에 두고 LEFT OUTER JOIN해서 P와 연관 맺고 있는 PD도 가져오란 얘기가 될 테니.. 하이버는 그저 시킨대로 한 죄 밖에는 없습니다. 결국 자연스러운 일입니다.

그렇다면 애초에 원했던 결과는 무엇이었을까요? 바로 P 목록만 가져오는 것이었습니다. 그러려면 P를 가져올 때 PD는 내비두고 오로지 P만 가져오게 해야겠죠. 어차피 P 목록을 보려고 하는데 PD 까지 가져올 필요는 없자나요. 패치모드를 LAZY로 바꾸면 from Plan 같은 HQL을 보내면 아예 join을 하지 않습니다.

~~ from P ~~

아마도 이런 SQL을 보시게 될 겁니다.

논외로  하이버 HQL, Criteria로 발생하는 SQL 쿼리를 이해하는 개발자가 되는 길은 멀고도 험한듯 합니다.

예를 들어 이번 이슈(P->PD)에서 패치모드, 방향성, mappedby의 변화로 생기는 쿼리 형태를 조사하려면 몇 가지 경우의 수를 고려해야 할까요?

- 방향성: 총 2가지(P->PD, P<->PD)
- 패치모드: 총 2가지(P->PD Lazy, P->PD Eager)
- mappedby: 총 2가지(P의 pd에 붙인 @OneToMany에서 mappedby="p")

정답은 그렇다면 2 * 2 * 2 = 8 가지? 글쎄요.

몇 번 해보시면 MappedBy 설정은 거의 영향이 없다는 걸 아실 수 있습니다. mappedby를 하면 좋은 점은 연관 테이블 수를 줄일 수 있다는 것. 하지만 결과에 영향이 없는 이유는 연관 테이블(P_PD)과 PD의 row 수가 같기 때문이죠. P가 PD와 left outer join을 하나, P가 P_PD와 left outer join을 하나 결과는 같으니까요.

따라서 2 * 2 = 4 가지 일까요? 그런데 만약 전제로 했던 P -> PD로의 방향성이 PD -> P 방향성 이라면?? ManyToOne이 되는데, 이때는 어떤 변화가 있을까요? @OneToMany의 fetch 속성 기본값은 LAZY 입니다. 별다른 설정을 하지 않으면 위와 같이 원하지 않았던 결과는 발생하지 않겠죠. 하지만 @ManyToOne의 fetch 속성 기본값은 EAGER입니다. 어떻게 될까요? 무슨 일이라도 생길까요? 앞선 경우처럼 DB에 들어있는 PD의 갯수보다 더 많은 PD의 갯수가 출력될까요?

그렇진 않습니다. ManyToOne 관계니까 그럴리는 없습니다.

PD 1 <- P 1
PD 2 <- P 1
PD 3 <- P 2
            P 3

PD를 왼쪽에 두고 left outer join을 해봤자. 이런 관계라고 할 때 결과는 다음과 같겠죠.

PD 1 - P 1
PD 2 - P 1
PD 3 - P 2

결과 row과 PD row와 동일한 상태가 됩니다. 따라서 ManyToOne에서도 fetch 모드를 별도로 설정하지 않더라도 HQL로 from PD를 날리면 예상하던(?) 결과를 얻을 수 있습니다. ㅎㅎ 재밌지요.
top


[Mockito] mock 객체 쉽게 만들기

모하니?/Coding : 2009.07.27 21:15


이지목(easymock)을 사용할 때는 인터페이스의 목만 만들 수 있어서 불편했습니다. 물론, 이지목 확장팩(?)을 이용하면 클래스의 목객체도 만들 수 있었는데, 별도의 라이브러리를 추가해야 하는것이.. 좀 귀찮았죠. 요즘은 어떤지 몰겠습니다. 라이브러리 버전 올리면서 통합 할 법도 한데 말이죠.

암튼.. 저는 이지목보다 조금 더 간편하고 직관적인 라이브러리인 목킷투를 사용하고 있었는데, 예전에 살짝 공부한 상태에서 다시 별로 안 썼더니 그새에 많은 변화가 있었더군요. 그 중 가장 큰 변화가 애노테이션인 듯 한데요. 애노테이션을 이용해서 목객체를 아주 편리하게 만들 수 있었습니다.

@RunWith(MockitoJUnitRunner.class)
public class MemberServiceTest {

    MemberService memberService;
   
    @Mock MemberRepository mockMemberRepository;
    @Mock SignupSendService mockSignupSendService;

    @Before
    public void make() throws Exception {
        memberService = new MemberService();
        memberService.repository = mockMemberRepository;
        memberService.signupSendService = mockSignupSendService;
        assertNotNull(memberService.repository);
        assertNotNull(memberService.signupSendService);
    }

...

}

끝입니다. mock(MemberReposiroty.class); 같은 static 메서드 호출(이클립스에서 junit은 static import를 지원하지만 mockto는 지원하지 않아서 수동으로 static import 문을 적어줘야 하는 수고가 있죠.) 없이도 목 객체를 만들어(?) 사용할 수 있습니다. 그 비밀은 바로 @Runwith의 MockitoJUnitRunner에 있지요. 저 러너 설정하는 것이 귀찮다면 JUnit @Before 메서드에서 MockitoAnnotations.initMocks(this); 를 직접 호출해도 되지만, 개인적으로는 이걸 없애고 @RunWith를 사용하는 편이 코드가 더 깔끔한 듯 합니다.

목킷투~ 괜찮다~~
top

TAG @Mock, Mockito

[하이버네이트]롹킹과 성능 사이에 서다.

모하니?/Coding : 2009.07.23 18:09


하이버네이트에 Optimistic 롹킹을 지원해주는 기능으로 버저닝이 라는게 있습니다. version 필드를 하나 만들어 주면 해당 엔티티에 변화가 일어날 때 마다 version 값을 증가시키는데, flush 하기 직전에 해당 객체의 version이 DB에 있는 version 값과 같은지 확인하는 작업입니다. 버전이 같지 않으면 StaleObjectStateException을 던져줍니다.

하이버네이트의 특징 중 하나로 연관 객체의 id만 알고 있다면, 굳이 DB에서 해당 엔티티를 가져오지 않고, id 만 가지고 있는 (가짜) 객체를 만들어서 세팅해주면, 나중에 flush() 할 때 FK로 잘 들어간다는 겁니다. 이런 특징을 이용해서 다대다, 1대다 관계에 있는 것들을 연결 시켜줄 때, DB에 다녀와야 할 쿼리를 상당 수 줄일 수 있습니다.

예를 들어, Member 정보를 추가할 때 Member가 속한 Group 정보를 선택한다고 했을 때, Group 목록을 가져오는 쿼리를 보내서 Group 목록을 가져오고.. 그 중에 하나를 선택해서 해당 Group의 id를 알아냈다면, 이제 이 id를 가지고 실제 Group을 가져오는 쿼리를 보내서 Group 객체를 꺼내온 다음에 사용해야겠지만...그러지 않고 그냥 새 Group 객체를 만든 다음 id값만 세팅한 상태로 Member에 추가해주면 되는거죠.

Fake Object 참조
  1. 2008/11/27 PropertyEditor 활용 예제 (8)
  2. 2008/11/19 하이버네이트 사용시 Association Fake Object라는 기술을 사용해 보세요. (2)

하지만, 방금 말한 이런 작업은 versioning을 하지 않을 때에나 가능한 이야기 입니다. DB에 flush()가 되는 순간 버전 확인이 이루어지고 version 정보가 없기 때문에 하이버네이트는 이 객체가 transient 객체라는 걸 눈치챕니다. 그리고선 transient를 flush하기 전에 저장하라는 에러를 뱉어내게 되죠.

@Entity
@Configurable
@DomainInfo("권한")
public class Role {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    Integer id;
    @Version
    Integer version;

예를 들어 위와 같은 엔티티가 있는데 이 녀석을 어딘가에서 Fake 객체로 new Role(1); 이런식으로 만들어서 User.addRole(new Role(1)); 이렇게 했다면... 문제가 생기게 됩니다.

그렇다면, versioning을 하지 않는다면 어떻게 될까요? 위에서 얘기한 것처럼 아무런 에러 없이 잘 동작합니다. 즉, 그냥 DB에 들어가버리죠. 해당 id 값은 FK 필드에 고대로 들어간체 말이죠. 하이버는 이 객체가 transient라는 사실을 모릅니다. id를 가지고 있기 때문에 persistent 상태인 줄 알겠죠.

결론은.. 그래서 고민입니다.

version을 그래도 두고 매번 DB에서 가져오도록 할까.. 하지만 version을 빼고 지금처럼 Fake를 사용할지 말이죠.

하이버네이트를 속여서 미안하지만, DB 쿼리를 줄이고 last commit wins를 선택하고 싶어집니다. 굳이 동시 접근을 막고 싶다면, 애초에 접근 자체를 막아버리고 팝업창으로 현재 다른 사람이 작업 중이라는 메시지 정도를 띄우는 pessimistic locking을 해버리고 말지.. 실컷 입력 다 해놓고 확인.. 누르자마자 에러.. 이건 좀.. 사용자가 많이 짜증날 것 같아서 말이죠.

update 할 때만 Session.load(Class class, Serialiazle id, LockMode lockMode)  이용해서,  LockMode.WRITE)로 가져오게 하면 되지 않을런지.. 흠.. 어찌할까나~

ps: 하이버네이트 낙관적인 롹팅을 사용하면서 OSIV 쓸 때는 주의할 것이 있더군요. 그건 이따 집에서..
top


JUnit 4.7 새 기능 @Rule

모하니?/Coding : 2009.07.22 23:21


http://www.infoq.com/news/2009/07/junit-4.7-rules

Rule은 기본적으로 각각의 테스트에 부가적인 기능을 추가할 수 있는 확장 매커니즘이랍니다. 뭔지 잘 감이 오지 않으신다면, 일종의 인터셉터라고 생각하시면 될 듯 합니다. 테스트 메서드에 인터셉터를 적용할 수 있다는 거겠죠. 하지만, 만든 사람이 Rule이라고 하니까 그냥 룰이라고 부르고, 익숙해지는게 좋을 듯 합니다.

기본으로 제공할 룰이 몇 개 있는 듯 합니다.

    *  TemporaryFolder: 테스트 할 때 필요한 파일과 폴더를 만들 수 있게 해주고, 테스트가 끝날 때 만든 것들을 전부 지워준다는군요. 아주.. 아주 아주.. 원하던 기능입니다. 배포되면 바로 써먹어 봐야겠습니다. 최근 만들었던 테스트 중 일부가 파일이랑 폴더를 만들거든요.

    * ExternalResource: 소켓, 내장 서버 같은 외부 자원을 미리 설정할 필요가 있을 때 사용하고 테스트 실행 뒤에 제거 해준다는군요. 주로 이런 테스트를 작성해보질 않았는데, 이 녀석 떄문에 좀 더 쉽게 테스트를 만들 수 있다면 좋겠습니다.

    * ErrorCollector: 테스트가 하나 실패해도 나머지를 계속 테스트 하고 테스트 끝나면 모든 에러를 보고 하도록 해주는 룰

    * ExpectedException: 예측한 예외와 에러 메시지가 발생하는지 확인할 때 사용. 이 녀석도 보통 하는 테스트니까 자주 사용하게 될 듯 하네요.

    * Timeout: 클래스에 있는 모든 테스트에 동일한 타임아웃 적용. 타임아웃은 거의 사용해본 적이 없네요. 흠..


public class DigitalAssetManagerTest {

    @Rule
    public TemporaryFolder tempFolder = new TemporaryFolder();

    @Rule
    public ExpectedException exception = ExpectedException.none();

    @Test
    public void countsAssets() throws IOException {
        File icon = tempFolder.newFile("icon.png");
        File assets = tempFolder.newFolder("assets");
        createAssets(assets, 3);

        DigitalAssetManager dam = new DigitalAssetManager(icon, assets);
        assertEquals(3, dam.getAssetCount());
    }

    private void createAssets(File assets, int numberOfAssets) throws IOException {
        for (int index = 0; index < numberOfAssets; index++) {
            File asset = new File(assets, String.format("asset-%d.mpg", index));
            Assert.assertTrue("Asset couldn't be created.", asset.createNewFile());
        }
    }

    @Test
    public void throwsIllegalArgumentExceptionIfIconIsNull() {
        exception.expect(IllegalArgumentException.class);
        exception.expectMessage("Icon is null, not a file, or doesn't exist.");
        new DigitalAssetManager(null, null);
    }
}

테스트 코드를 보면 countsAssets 테스트에서 TemporaryFolder를 이용해서 파일과 폴더를 만드는 걸 볼 수 있습니다. 저렇게 만든 것들이 테스트가 끝나면 자동으로 없어진다니... 조금 더 테스트가 편해지는 것 같습니다.

두 번째 테스트는 일단 예상하는 예외와 에러 메시지를 ExpectedException한테 알려준 뒤에, 예외가 발생하는 코드를 작성했네요.

음... 뭐.. 그렇군요.

Rule을 잘 써서 Runner들을 대체 할 수 있겠군요. SpringJUnit4~~Runner도 잘하면 Rule로 대체 할 수 있겠지요.

덤으로..  @Configurable 클래스를 테스트 할 때 javaagent 옵션을 설정해줘야 되서 매우 불편한데, 테스트를 실행할 때 특정 자바 아규먼트를 가지고 실행하도록 코드에서 설정이 가능하면 좋겠습니다. 흠.. 이슈에 올려볼까요. 켄트 벡 아저씨가 만들어 주실려나.. @_@
top


윈도우에서 모나코 폰트 이쁘게 쓰기

모하니?/Coding : 2009.07.22 14:58



디스플레이 설정 -> 화면배색 -> 효과를 클릭하고, 위에 보이는 것처럼 화면 가장자리를 ClearTpe으로 다듬어 주시면 이쁘답니다. 톱님이 알려주셨어요. 땡큐~


캬.. 맥북에서 쓰는 이클립스랑 거의 같아 보이네요. 폰트 10은 좀 커서 9로 줄였습니다. 이제 좀 이쁘게 개발해 보아요.ㅋㅋ
top


에러 잡기 7단계

모하니?/Coding : 2009.07.21 16:09


참조: http://www.makinggoodsoftware.com/2009/06/14/7-steps-to-fix-an-error/

디버깅 왜 중요한가?

디버깅이란 소프트웨어 개발자가 버그를 고치는 행위를 말한다. 훌륭한 디버거가 되는 것은 훌륭한 개발자가 되기 위해 중요한 부분이다.

에러 잡기 7단계

스탭 1: 에러 식별하기

버그가 뭔지 알아야겠죠. "앗 버그다" 이러고 그냥.. 멍~~ 이러고 있으면 안 되겠죠. 무슨 버그인지 에러 메시지를 잘 읽어보거나, 에러를 다시 재현해 본다던가 말이죠.

스탭 2: 에러 찾기

정확히 에러가 어느지점에서 발생하는지 찾아야 합니다. 보통 이클립스 콘솔을 보면 메서드 호출 스택 중에서 프레임워크 패키지로 시작하는 것들 말고, 제가 작성한 패키지로 시작하는 부분을 보면 해당 클래스의 몇 번째 줄에서 예외가 발생했는지도 알 수 있죠. 대부분 그 지점에서 뭔가가 잘못됐을테죠. 아니면 ClassNotFound 같은 건 라이브러리 문제니까 특정 지점을 찾을 필요는 없곘네요. 쉽게 찾을 수 없는 경우 로깅, 디버깅, 작성했던 코드 빼기. 등을 사용해서 버그가 생긴 지점을 알 수 있겠습니다. 테스트를 작성해서 CI를 하는 이유 중 하나가 바로 이 에러가 발생한 지점을 찾을 때 생각해 볼 범위가 테스트에 반비례하기 때문인듯 합니다.

스탭 3: 에러 분석

해당 부분에서 무엇이 잘못됐는지 알아내는 겁니다. 확실하지 않다면 가설을 세워도 좋겠죠.

스탭 4: 분석 증명

이전 단계에서 분석을 했거나, 가설을 세웠다면 그것이 맞는지 테스트 코드를 이용해서 증명하는 단계입니다. 버그를 재현하는 테스트를 작성해서, 실패하는 테스트를 만듭니다. 테스트의 소중함을 알면서도 저는 보통 이 단계를 빼먹곤 하지요.

스탭 5: 연쇄 데미지 방지

분석 증명까지 끝났다면 바로 버그를 수정하는게 아니라, 수정하기 전 상태를 확인합니다. 이전 테스트들을 모두 실행해서 무사히 패스 하는지 확인하는 거죠. 이로써, 지금 수행할 버그 수정 작업이 이전 상태에 또 다른 버그를 만들지는 않는지 확인할 수 있습니다.

스탭 6: 버그 수정

수정하는 거죠~

스탭 7: 검증

모든 테스트를 실행하고 패스하는지 확인.

몇 가지 추천사항

버그와 그 수정 사항을 문서화 하라.
로깅을 하라.
툴을 사용하라.(이슈 트래커, 소스 관리, 디버깅 툴)
top


내일은 꼭 Monaco 폰트 설치해야지

모하니?/Coding : 2009.07.20 23:13


모나코 폰트는 맥용 이클립스에서 기본으로 사용하는 폰트입니다. 평소 아주 이쁘고 깔끔하다고 생각만 하고 윈도에 설치할 생각을 못했었는데, 몇 일전 톱님께서 알려주신게 생각났습니다.

http://digg.com/programming/Monaco_Font_Free_for_Windows

여기서 어떻게 타고 들어가면 구할 수 있을 것 같으니.. 일단 링크 하나 걸어둡니다. 오늘도 맥북으로 봄싹 사이트 개발 때문에 끄적 거리다가.. 괄호를 쳤는데 한 눈에 딱.. 구분이 되니 아주 좋습니다.


위 코드는 제이쿼리 탭이며, 탭 내의 링크들이 컨테이너 밖으로 나가지 않고 그 안에 뿌려지게 해주는 코드랍니다. 출처는 제이쿼리 UI 홈입니다.ㅋ

top


스프링 시큐리티 2.X -> 스프링 시큐리티 3.X

모하니?/Coding : 2009.07.17 19:52


하루동안 시큐리티를 두 번 각각 다른 버전으로 적용해보니 차이점이 보입니다.

시큐리티를 확장할 수 있는 포인트가 하두 여러 가지라 그만큼 확장하는 방법도 다양하겠지만, 저는 톱님 코드를 보고 그대로 적용해 봤습니다.

적용하는 방법은 별도의 UserDetailsService와 UserDetails를 구현하는 방법입니다. 그리고 여기서 구현한 UserDetailsService를 빈으로 등록해 주는 거죠.

    <http>
        <intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
        <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
        <form-login login-page="/login.do"
            authentication-failure-url="/login.do?login_error=t"
            default-target-url="/main.do" />
        <logout logout-success-url="/main.do" />
        <anonymous/>
        <remember-me />
    </http>

    <authentication-provider user-service-ref="customUserDetailsService" />

    <global-method-security secured-annotations="enabled"
        jsr250-annotations="enabled" />

설정은 이렇게 간단해졌지만, 내부에서 해주는 일은 여전히 필터체인프록시 와 여러 개의 필터, 프로바이더, 엔트리포인트 등이 수고 해 줍니다.

일단 위 설정은 2.X 대의 설정인데, 왜냐면, <anonymuos />가 있기 때문입니다. 이 녀석은 익명사용자를 나타내는  IS_AUTHENTICATED_ANONYMOUSLY를 쓸 때 필요한데, 3.X에서는 이 엘리먼트를 사용하지 못합니다. 하지만 IS_AUTHENTICATED_ANONYMOUSLY는 기본으로 쓸 수 있습니다. 클래스가 없어졌거나 패키지 이동을 했을 겁니다.

패키지가 바꼈습니다. 확장해야 할 인터페이스 2개

import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;

이 녀석들이 3.X에서는

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

이쪽으로 옮겨갔습니다. 이밖에도

import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;

이 녀석들도 같이 이동했습니다. 다행히 클래스 이름이 같기 때문에 마이그레이션 할 때 별 어려움은 없을 것입니다.

마지막으로 인터페이스가 바뀐 녀석이 있습니다. UserDetails는 2.X에서

public GrantedAuthority[] getAuthorities()

이런 인터페이스를 가지고 있었는데. 3.X에서는 List 타입을 반환하도록 바꼈습니다. 배열을 채워줄때는 배열 사이즈를 미리 알아야 하기 때문에 불편한 코딩이 조금 추가되는데, 3.X에서는 사이즈 상관없이 그냥 add만 해주면 되니까 조금 더 간편해 졌다고 느껴지네요.

(톱님 따라 저도) 오늘의 결론
- 시큐리티 2.X에서 3.X로 넘어가는 길이 아주 편한건 아니지만, 그리 불편하지도 않네요.







top


모르겠네.. @_@

모하니?/Coding : 2009.07.15 19:07


도메인 객체에 @Configurable을 사용해서 DAO 객체를 주입하는데 하이버네이트 SessionFactory를 만들다가 에러가 납니다... 허헐.. 이것 참..

@Configurable은 그대로 두고, 도메인 객체에 주입했던 DAO들을 주석처리하면 SessionFactory도 잘 만들어 주고 그 뒤엔 나머지 모든 빈들도 다 잘 만들어지고 테스트도 잘 돌아갑니다.

하지만...도메인 객체에 DAO만 주입했다... 하면... 아래처럼 결국은 DAO를 주입한 클래스의 인스턴스를 만들지 못해서 SessionFactory를 만들지 못하고 그로 인해 ApplicationContext도 제대로 만들지 못해서 아무런 테스트가 돌아가질 않습니다.

Caused by: org.hibernate.InstantiationException: could not instantiate test objectkoma.domain.User

이것이 핵심으로 보이는데. 왜 그런지 잘 모르겠네요. 희한한건 스프링 3.0.0.M3을 쓰면 아무 문제 없이 잘 돌아간다는 겁니다. 톱님께서 제공하시는 최신 버전의 스프링으로 버전을 바꾸면 이런 현상이 벌어진다는 겁니다.

몇 가지 추측을 해볼 수 있는데...

1. 스프링 최신버전과 하이버네이트 버전이 맞지 않음.

2. 스프링 최신버전과 AspectJ 버전이 맞지 않음.

3. 최신 스프링 코드에 문제가 있음.

다행스러운건... 정식배포버전은 스프링 3.0.0.M3에서는 무사히 잘 돌아간다는 겁니다. 현재 스프링 의존성 매트릭스를 참조해서 하이버와 Aspectj 버전을 맞춰봤지만.. 여전히 아래와 같은 에러 로그가 떨어집니다.

더보기


또하나는 위버와의 궁합이 안맞을지 모른다는 생각인데...

3.0 M 버전들은 배포할 때 with-doc 만 배포하면서 그 안에 weaver들을 포함시켜주지 않았습니다. with-dependencies를 보면 dist/weaver 폴더에 들어있는데 말이죠.

최신 버전에서 위버와 뭔가 궁합이 안맞을 수 있겠다는 생각도 해봤었지만, 최신 위버를 찾을 수가 없어서 뒤로 재쳐두고 있었는데..

문제 실마리가 안 보이네요. @_@
top


스프링 JSON view와 jQuery 이용하여 자동완성 기능 만들기 4

모하니?/Coding : 2009.07.14 18:35


마무리로 태그 파일화 작업을 했습니다.

             <o:textwithac label="이름" id="name" size="20" maxlength="30"
                 url="/ajax/user/names.do" model="users" width="200" selectFirst="true" />

이런 태그와..

<script type="text/javascript">
function formatItem(row, i, total){
    return row[1] + " " + row[2];
}
function formatResult(row){
    return row[1];
}
function result(event, row, formatted){
    if (row){
        $("#namedetail").text(row[0] + " " + row[2]);
    }
}
</script>

자바스크립트 함수 세 개로 자동완성 기능을 사용할 수 있습니다.

속성 중에서

url="/ajax/user/names.do" model="users" width="200" selectFirst="true"

이 녀석들이 자동완성과 관련된 속성들이며..

formatItem은 자동완성 목록을 어떤 형태로 보여줄지 코드를 작성해주면 되는데, 이 때 row는 현재 데이터 row이고, i는 인덱스, total은 총 갯수 입니다.

formatResult는 자동완성 목록에서 선택했을 때 text input 박스에 최종적으로 입력할 값을 return하도록 합니다.

마지막으로 result는 자동완성 목록을 선택했을 때 후 처리 이벤트 같은 것으로, result 타입의 이벤트와, 선택한 데이터 row, 그리고 formatResult에서 input 필드에 입력되도록 포맷팅한 값입니다. 이 녀석들을 이용해서 적당하게 코딩해주면 되겠습니다. 예제에서는 namedetila이라는 엘리먼트에 부가정보를 기입해주었습니다.



top




: 1 : 2 : 3 : 4 : 5 : 6 : 7 : ··· : 10 :