Whiteship's Note

'2009/09'에 해당되는 글 38건

  1. 2009.09.29 [봄싹] 새 기능 소개 (4)
  2. 2009.09.29 [테스트 데이터] 테스트에 필요한 데이터 만들기
  3. 2009.09.29 [테스트 코드 리팩토링] extract method
  4. 2009.09.28 [스프링 3.0] 애노테이션 기반 스케줄링 (2)
  5. 2009.09.26 [JSP 리팩토링] 태그 파일로 중복 코드 제거하기
  6. 2009.09.26 드이어 스프링 3.0 RC1이 나왔습니다. (3)
  7. 2009.09.25 정말 멋진 플러그 디자인 (6)
  8. 2009.09.24 비행기 갈아타야 할 때는... 시간을 좀 넉넉하게 (10)
  9. 2009.09.23 역시 개발자는 모니터가 빵빵한듯... (14)
  10. 2009.09.23 역시 개발자들은 파이어폭스인가.. (12)
  11. 2009.09.23 [NotificationService] NotificationService 인터페이스 기반 프로그래밍 (2)
  12. 2009.09.23 테스트 환경와 운영 환경용 설정 분리/구분하기 (4)
  13. 2009.09.22 [제이쿼리] 매개변수가 있는 function을 HTML 엘리먼트에서 분리하기 (12)
  14. 2009.09.22 [Expert One-on-One J2EE Design and Development] J2EE 프로젝트를 위한 설계 기술과 코딩 표준 2
  15. 2009.09.21 [봄싹] 매주 토요일 (12)
  16. 2009.09.18 [Expert One-on-One J2EE Design and Development] J2EE 프로젝트를 위한 설계 기술과 코딩 표준 1
  17. 2009.09.18 [봄싹] 스프링 레퍼런스 3.0 번역 시작 (4)
  18. 2009.09.15 초고속 로보트 손
  19. 2009.09.15 [Expert One-on-One J2EE Design and Development] 실용적인 데이터 접근 2
  20. 2009.09.15 [Expert One-on-One J2EE Design and Development] 실용적인 데이터 접근 1 (4)
  21. 2009.09.15 봄싹 9월 특강 Completed!! 후기랄까나... (8)
  22. 2009.09.14 스프링 이메일 확장하기 3
  23. 2009.09.12 스프링 이메일 확장하기 2
  24. 2009.09.10 [하이버네이트] 쿼리를 수정할 것이냐 모델을 수정할 것이냐...
  25. 2009.09.09 완전 부럽다... (22)
  26. 2009.09.08 [하이버네이트] 1 + 2N select 문제 해결하기 (4)
  27. 2009.09.07 [하이버네이트] 2차 캐싱 적용하기
  28. 2009.09.07 [hamcrest] Matcher 만들기
  29. 2009.09.04 [GenericDao] 하이버네이트 GenericDao (6)
  30. 2009.09.04 [Generic] 자바 Generic 타입 알아내기

[봄싹] 새 기능 소개

모하니?/Coding : 2009.09.29 18:41


1. 트위터 서비스!

http://twitter.com/springsprout

봄싹 트위터를 통해서 스터디와 모임 개설/변경 소식을 실시간으로 전해드립니다. 트위터 RSS를 구족하시면 RSS 리더기를 통해서도 스터디와 모임 정보를 쉽게 받아 보실 수 있겠죠.

2. 구글 토크 알리미!

s2cmailer@gmail.com

이 주소를 구글토크에 친구로 추가해두고, 구글 토크에서 사용하는 이메일로 봄싹에 가입했다면,
스터디와 모임 메시지를 바로 바로 받을 수 있습니다.

그것 뿐 아니라, 대화형 기능을 제공하여 study? 라고 입력하면 현재 운영중인 스터디 목록을 보여주고, meeting? 이라고 입력하면 현재 개설되어 있는 모임 목록을 보여줍니다.


3. 통계 기능 추가!

스터디에 얼마나 열성적으로 참석하는지..
스터디 별로 참석률은 어떤지..
모임 참가 신청을 하고나서 자주 불참하는 회원은 아닌지..
스터디 별로 신청을 해놓고 얼마나 약속을 잘 지키는지..

한눈에 알 수 있는 기능을 추가했습니다. (테스트 데이터가 엉망이라 통계가 좀 이상합니다.)


나중에는 참석률과 신뢰도를 게이지 형태로 디자인해서 마치.. 게임 캐릭터의 체력과 마력을 나타내듯이 표시할 생각입니다. ㅋㅋ

셋 다 제가 코딩했고 만들면서 많이 고민하고 재미를 느꼈던 기능들입니다.

- 트위터 알리미를 만들 때는 인터페이스 사용의 혜택을 만끽해 보았으며,

- 구글 토크 서비스를 만들 때는 smack API를 이용하는 JabberService를 만들면서 스프링 DI를 어떻게 하면 잘 활용할 수 있을까 고민할 수 있는 계기가 되었습니다.

- 마지막으로 통계 기능을 만들 때는 별로 안 해봤엇던 하이버네이트에 Map<Entity, Primitive Type> 형태의 맵핑도 해보고, 어떤 콜렉션 타입이 좋을지 고민도 해보고, EL로 map안에 있는 데이터 꺼내오기도 해봤네요. 하지만 뭐니뭐니해도 테스트를 통해서 얻을 수 있는 안정감.. 그걸 느껴볼 수 있었습니다.

현재 봄싹은 제가 만든 새로운 기능 말고도 다른 분들이 만들고 있는 새 기능도 많이 있습니다. 본격적인 운영은 추석이 지나고나서 될 것 같네요. 그전까지 지금 상태 그대로 갑니다.

모두 명절 잘 보내세요~
신고
top


[테스트 데이터] 테스트에 필요한 데이터 만들기

모하니?/Coding : 2009.09.29 15:24


    @Test
    public void calcTotalAttendanceRate() throws Exception {
        Member member = new Member();
        Study study1 = new Study();
        Meeting meeting1 = new Meeting();
        Meeting meeting2 = new Meeting();
        Meeting meeting3 = new Meeting();
        Study study2 = new Study();

        study1.addMeeting(meeting1);
        study1.addMeeting(meeting2);
        study2.addMeeting(meeting3);
        member.addJoinedStudy(study1);
        member.addJoinedStudy(study2);
        member.applyAttendance(meeting1);
        member.applyAttendance(meeting2);

        int attendanceSize = member.getAttendances().size();
        assertThat(attendanceSize, is(2));
        assertThat(member.getStudies().size(), is(2));
        assertThat(study1.getMeetingCount(), is(2));
        assertThat(study2.getMeetingCount(), is(1));

        when(mockStudyRepository.getConfirmedAttendanceCountOf(member)).thenReturn(1);
        when(mockStudyRepository.getTotalAttandanceCountOf(member)).thenReturn(attendanceSize);
        memberService.calcRatesOf(member);
       
        assertThat(member.getTotalAttendanceRate(), is(33));
        assertThat(member.getTotalTrustRate(), is(50));
    }

이 테스트에서 절반 이상이 테스트 데이터를 만들고 그 부분을 검증하는 코드입니다. 이 부분을 다음과 같이 바꿀 수 있다면.. 좋을까요? 안 좋을까요?

        Meeting meeting1 = Builder.Create(Meeting.class).Build();
        Meeting meeting2 = Builder.Create(Meeting.class).Build();
        Meeting meeting3 = Builder.Create(Meeting.class).Build();
       
        Builder.Create(Member.class)
            .addStudy(Builder.Create(Study.class)
                    .addMeeting(meeting1)
                    .addMeeting(meeting2)
                    .Build())
            .addStudy(Builder.Create(Study.class)
                    .addMeeting(meeting3)
                    .Build())
            .addAttendance(Builder.Create(Attendance.class)
                    .addMeeting(meeting1)
                    .Build())
            .addAttendance(Builder.Create(Attendance.class)
                    .addMeeting(meeting2)
                    .Build())
            .Build();

Object Mother에 대한 글을 찾아 보다가 Test Data Builder에 대한 글과 거기에 달린 댓글을 통해 닷넷에서 사용하는 NBuilder라는 것까지 대충 살펴봤는데.. 이거 뭐.. 해보지 않고서는 어떨지 잘 상상이 안 되네요.

http://martinfowler.com/bliki/ObjectMother.html
http://c2.com/cgi/wiki?ObjectMother
http://geekswithblogs.net/Podwysocki/archive/2008/01/08/118362.aspx
http://www.nbuilder.org/

그래서 일단은 위와 같이 상상 코딩을 해보았는데.. 어떨런지요.. 흠..
신고
top


[테스트 코드 리팩토링] extract method

모하니?/Coding : 2009.09.29 15:01


    @Test
    public void getAttendanceCountOf() throws Exception {
        insertXmlData("testData.xml");
        Member member = new Member();
        Study study = new Study();
        member.setId(1);
        study.setId(1);
        assertThat(sr.getAttendanceCountOf(member, study), is(2));
        member.setId(1);
        study.setId(2);
        assertThat(sr.getAttendanceCountOf(member, study), is(1));
        member.setId(2);
        study.setId(2);
        assertThat(sr.getAttendanceCountOf(member, study), is(1));
    }

다음과 같이 코드를 리팩토링할 수 있습니다.

    @Test
    public void getAttendanceCountOf() throws Exception {
        insertXmlData("testData.xml");
        checkAttendanceCountOf(1, 1, 2);
        checkAttendanceCountOf(1, 2, 1);
        checkAttendanceCountOf(2, 2, 1);
    }
   
    private void checkAttendanceCountOf(int memberId, int studyId, int count){
        Member member = new Member();
        Study study = new Study();
        member.setId(memberId);
        study.setId(studyId);
        assertThat(sr.getAttendanceCountOf(member, study), is(count));
    }

이번 경우에는 코드 라인수 차이가 얼마 나지 않지만, 대부분의 경우 훨씬 깔끔해집니다.
신고
top


[스프링 3.0] 애노테이션 기반 스케줄링

Spring/3.0 : 2009.09.28 18:17


참조: http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/ch25s05.html

웹 애플리케이션을 띄울 때 구글 토크 봇을 로그인 시켜두려고 스케줄링을 이용하려 했습니다. 찾아보니까 애노테이션 기반으로 설정할 수 있는 기능이 추가됐더군요.

<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>

<task:executor id="myExecutor" pool-size="5"/>

<task:scheduler id="myScheduler" pool-size="10"/>}

이렇게 task:annotation-driven 엘리먼트를 XML에 추가해주면 빈에 설정되어 있는 @Schedule과 @Async 애노테이션을 활성화 시켜줍니다.

@Schedule 애노테이션은 cron, fixedDelay, fixedRate 세 가지 속성 중 하나를 이용해서 설정해야 합니다. 반드시 이 셋 중에 하나는 설정되어 있어야 합니다.

@Async 애노테이션은 해당 메서드 호출을 비동기로 처리해주고 싶을 때 사용할 수 있습니다. 즉 이 애노테이션으로 스케줄링이 적용된 메서드를 호출하면 결과는 바로 리턴되고 실제 실행은 스프링의 TaskExecutor에 의해 별도의 Task 내부(이 녀석이 별도의 쓰레드겠죠)에서 실행됩니다.

막상 해보니 라이브러리 때문에 에러가 나더군요.

        <dependency>
            <groupId>edu.emory.mathcs.backport</groupId>
            <artifactId>com.springsource.edu.emory.mathcs.backport</artifactId>
            <version>3.1.0</version>
        </dependency>

그래서 필요한 라이브러리를 추가해주고 돌려보니까 잘 돌아갑니다.

그런데 해보고 나니까 굳이 반복 실행할 필요가 없는 메서드라;;; -_-;; @PostConstruct 애노테이션 붙여서 끝냈습니다.

이 간단한것을... 스캐쥴링은 머하러 찾아봤담;;
신고
top


[JSP 리팩토링] 태그 파일로 중복 코드 제거하기

모하니?/Coding : 2009.09.26 16:15


<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="s" tagdir="/WEB-INF/tags/study"%>

<page:studypage>
<s:defaultpage>
    <h1>스터디 추가</h1>
    <form:form commandName="study" method="post">
    <p>
        <label>스터디명</label>
        <form:input path="studyName" cssClass="text" />
        <form:errors path="studyName" />
    </p>
    <p>
        <label>최대인원수</label>
        <form:input path="maximum" cssClass="text" />
        <form:errors path="maximum" />
    </p>
    <p>
        <label>시작일</label>
        <form:input path="startDay" cssClass="text"/>
        <form:errors path="startDay" />
    </p>
    <p>
        <label>종료일</label>
        <form:input path="endDay" cssClass="text"/>
        <form:errors path="endDay" />
    </p>
    <p>
        <label>설명</label>
        <form:textarea path="descr" rows="4" cols="60" cssClass="text"/>
        <form:errors path="descr" />
    </p>
    <br/><hr/><br/>
    <a href="<c:url value="/study/list.do"/>">취소</a>
    <input type="submit" value="저장" class="s_waitblock" />
    </form:form>
</s:defaultpage>
<script type="text/javascript">
  $(document).ready(function(){
    $("#startDay").datepicker({ dateFormat: 'yy/mm/dd' });
    $("#endDay").datepicker({ dateFormat: 'yy/mm/dd' });
  });
</script>
</page:studypage>

이미 태그 파일로 <html> </html>과 js, css 임포트 하는 부분을 제거 해 두었습니다. 태그 파일을 여러 추상화 계층으로 세분화 해서 로우 레벨 태그파일과 하이 레벨 태그파일로 나눌 수도 있겠습니다. 저 위에 보이는 page 태그는 하이 레벨 태그 파일로 볼 수 있고, s 태그는 로우 레벨로 볼 수 있습니다. 하이 레벨이라고 해서 뭔가 더 여러운 태그라는게 아니라, 로우 레벨 태그를 조합하여 한 단계 더 추상화시킨 태그파일 입니다. 이런 구분이 원래 있는 것이 아니라 제가 생각하는 걸 정리한 것 뿐이오니,,, 괜히 "하이 레벨 태그 파일" 이런식으로 구글링을 하는 사태가 없기를 바랍니다.

사설을 좀 길었네요, 일단락하기로 하고, 위 코드를 태그파일로 리팩토링하면 다음과 같이 됩니다.

<page:studypage>
<s:defaultpage>
    <h1>스터디 추가</h1>
    <form:form commandName="study" method="post">
    <s:ftext title="스터디명" path="studyName" />
    <s:ftext title="최대인원수" path="maximum" />
    <s:fdate title="시작일" path="startDay" />
    <s:fdate title="종료일" path="endDay" />
    <s:ftextarea title="설명" path="descr" rows="4" cols="60" />
    <hr/>
    <s:back-button url="/study/list.do" />
    <input type="submit" value="저장" class="s_waitblock" />
    </form:form>
</s:defaultpage>
<script type="text/javascript">
  $(document).ready(function(){
    $(".fdate").datepicker({ dateFormat: 'yy/mm/dd' });
  });
</script>
</page:studypage>

이렇게 했을 때 좋은 점은 소스 코드에서 중복을 제거 했을 때 얻을 수 있는 장점과 같습니다.

그러나,,, 단점도 있는데 태그 파일에 정의해준 속성만 받아서 사용하기 때문에 그만큼 사용할 수 있는 기능이 제한 될달까.. 그런게 좀 있습니다. 해결책은 있습니다. 태그 파일에 거의 모든 속성을 다 정의해 놓고 정말 필요한 것만 required로 하고 사용해도 될테지만.. 태그 파일을 만드는 비용이 꽤 많이 들겠지요. 결국 선택의 기로에 서게 되는데, 저는 귀찮아서;; 그냥 최소한의 속성만 정의해서 쓰는 편입니다.

신고
top


드이어 스프링 3.0 RC1이 나왔습니다.

Spring/3.0 : 2009.09.26 08:14


http://www.springsource.org/node/2004

주요 변경 사항은 다음과 같습니다.

Changes in version 3.0.0.RC1 (2009-09-25)
-----------------------------------------

* upgraded to CGLIB 2.2, AspectJ 1.6.5, Groovy 1.6.3, EHCache 1.6.2, JUnit 4.7, TestNG 5.10
* introduced early support for JSR-330 "javax.inject" annotations (for autowiring)
* introduced early support for JSR-303 Bean Validation (setup and MVC integration)
* added default editors for "java.util.Currency" and "java.util.TimeZone"
* refined PathMatchingResourcePatternResolver's treatment of non-readable directories
* PathMatchingResourcePatternResolver understands VFS resources (i.e. works on JBoss 5.x)
* revised AccessControlContext access from BeanFactory
* AbstractBeanDefinitionParser can deal with null return value as well
* PropertyOverrideConfigurer's "ignoreInvalidKeys" ignores invalid property names as well
* PropertyPlaceholderConfigurer supports "${myKey:myDefaultValue}" defaulting syntax
* BeanFactory's default type conversion falls back to String constructor on target type
* BeanFactory tries to create unknown collection implementation types via default constructor
* BeanFactory supports ObjectFactory as a dependency type for @Autowired and @Value
* BeanFactory supports JSR-330 Provider interface as a dependency type for @Inject
* BeanFactory prefers local primary bean to primary bean in parent factory
* protected @Autowired method can be overridden with non-annotated method to suppress injection
* private @Autowired methods with same signature will be called individually across a hierarchy
* @PostConstruct processed top-down (base class first); @PreDestroy bottom-up (subclass first)
* ConfigurationClassPostProcessor detect @Bean methods on registered plain bean classes as well
* support for default "conversionService" bean in an ApplicationContext
* MBeanServerFactoryBean returns JDK 1.5 platform MBeanServer for agent id "" (empty String)
* changed NamedParameter/SimpleJdbcOperations parameter signatures to accept any Map value type
* refined logging in JMS SingleConnectionFactory and DefaultMessageListenerContainer
* introduced "ui.format" package as an alternative to PropertyEditors for data binding
* @RequestMapping annotation now supported for annotated interfaces (and JDK proxies) as well
* @RequestParam and co support placeholders and expressions in their defaultValue attributes
* @Value expressions supported as MVC handler method arguments as well (against request scope)
* JSR-303 support for validation of @MVC handler method arguments driven by @Valid annotations
* refined response handling for @ExceptionHandler methods
* @ResponseStatus usage in handler methods detected by RedirectView
* all @SessionAttributes get exposed to the model before handler method execution
* @Event/ResourceMapping uniquely mapped to through event/resource id, even across controllers
* MultipartRequest is available as a mixin interface on (Native)WebRequest as well
* removed outdated "cacheJspExpressions" feature from ExpressionEvaluationUtils
* introduced common ErrorHandler strategy, supported by message listener container
* Jpa/JdoTransactionManager passes resolved timeout into Jpa/JdoDialect's beginTransaction
* HibernateJpaDialect applies timeout onto native Hibernate Transaction before begin call
* Spring's Hibernate support is now compatible with Hibernate 3.5 beta 1 as well
* Spring's JPA support is now fully compatible with JPA 2.0 as in EclipseLink 2.0.0.M7
* SpringJUnit4ClassRunner is now compatible with JUnit 4.5, 4.6, and 4.7
* SpringJUnit4ClassRunner once again supports collective timeouts for repeated tests
* deprecated @NotTransactional annotation for test classes in favor of @BeforeTransaction

봄싹 사이트에 어서 적용해 봐야겠네요.
신고
top


정말 멋진 플러그 디자인

Good Tools : 2009.09.25 14:11


멋져요;;



자세한 건 요기서;;
신고
top


비행기 갈아타야 할 때는... 시간을 좀 넉넉하게

모하니?/Thinking : 2009.09.24 23:44


잡았어야 하는데... 큰일이로군요.

11시 25분에 떨어지는데 1시 뱅기로 갈아타야 합니다.

어떻게 보면 가능할 것 같기도 하지만.. 사실 굉장히 힘든 시간입니다.

원래는 1시반으로 예약을 해뒀었는데. 시간이 계속 바뀌더니 현재는 1시로 바뀌었습니다.

헐... -_-;; 부랴부랴 연락을 해뒀지만 부정적입니다. 아무런 대처도 안해줍니다.

프로모션 티켓이라 변경도 안되고 그냥 버리고 새로 사라는데 어이가 없더군요.

그럼 빨리 갈아탈 수 있게 가이드라도 해달라고 했는데 그것도 안 된답니다.

씨에어 이 개떡같은 것들아 니네꺼 다시는 안 탈꺼야!!!

이건 뭐 가기도 전에 스트레스 받아서 쓰러질 것 같아요.

흑흑흑...

최악의 경우 뱅기를 놓치고 당일 오후 늦은 뱅기표를 사서 아주 개고생을 하게 생겼네요.

에휴.. 이게다..  변수를 생각지 못하고 예약을 해버린 제 불찰입니다.

일정이 변경될 여지를 생각해서 최소한 3시간 텀은 두는게 안전할 것 같습니다.

아니면 아예 하루 자고 나서 갈아타던지 말이죠.

암튼.. 코딩도 못하고 공부도 못해서 재미도 없고 피곤하고 짜증나는 하루였네요.

뭐.. 이런 날도 있는거겠죠.
신고
top


역시 개발자는 모니터가 빵빵한듯...



진짜 집에 가려고 했는데;; 아주 특이한 분석자료가 있어서요..ㅋㅋ


1위인 1280이 요즘은 대세가 되서 그런걸지도 모르겠지만,
2위인 1680*1050 해상도로 거의 절반 가까이가 접속하셨네요..
세상에...

다들.. 그정도는 쓰시는군요. ^^
1680*1050 안쓰시는 분들만 조금 답답한 거죠 뭐.ㅋㅋㅋ


신고
top


역시 개발자들은 파이어폭스인가..




봄싹 사이트에 접속한 사용자들(대부분이 개발자시겠죠?)이 사용하는 브라우저 분석을 봤더니 IE 보다 파이어폭스 유저가 더 많네요.

참고로 봄싹사이트에 IE6로 접근하시면 재미난 걸 보실 수 있습니다. ㅋㅋ

ps: 이젠 진짜 집에가야지;

신고
top


[NotificationService] NotificationService 인터페이스 기반 프로그래밍

모하니?/Coding : 2009.09.23 20:08


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

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


신고
top


테스트 환경와 운영 환경용 설정 분리/구분하기

모하니?/Coding : 2009.09.23 19:06


public class SpringSprout2System {
    public static final String ENCODING = "UTF-8";
    public static final String S2C_HOME = "http://www.springsprout.org/";
    public static final String AVATAR_URL = "http://www.gravatar.com/avatar/";
...
    public static final String JSON_VIEW = "mappingJacksonJsonView";
}


이런식으로 시스템에서 자주 사용하는 상수 값들을 모아두었습니다. 그런데 문제는 저 중에서 S2C_HOME의 값이 로컬 서버에서 돌 때랑 실제 운영 서버에서 돌 때랑 달라져야 하는데 그렇지 않고 있다는 것이었죠.

그럼 일단 프로퍼티 파일로 빼고 싶어질 겁니다. 그런데.. 프로퍼티 파일로 빼면 다인가요? 프로퍼티 파일로 빼고 그걸 빈으로 등록해서 참조해서 쓰면 끝인가요? 프로퍼티 파일이 일단 로털 테스트용/운영 서버용 이렇게 두 개로 나뉠텐데.. 그럼 로컬에서 작업할 땐 로컬용 프로퍼티 파일 쓰다가 버전 관리 올릴 때는 빼고 운영 서버용을 쓸건가요 아니면 프로퍼티 파일 하나에 모두 넣어 놓고 일부는 로컬용으로 일부는 서버용으로 해놓고 주석처리를 바꿔가며 쓸건가요??

도무지 깔끔한 방법이 아니라는 생각이 들어서 고민하다가 봄싹에서 토론도 하고 사부님이랑 전화통화 하다 생각나서 물어보기도 했습니다. 결국은 다음 세 가지 방법으로 정리가 됐습니다.

1. 프로그래밍을 이용한 설정(스마트 설정)

빈을 하나 등록해서 해당 빈이 현재 애플리케이션이 배포된 서버 환경에 대한 정보를 분석해서 이게 운영 서버인지 아닌지 판단해서 거기에 맞게 환경 변수를 샤샥 등록하는 방법입니다. 자바의 System.getProperty()에 os.name os.arch os.version 키 값을 이용해서 지금 돌고 있는 운영체제의 정보를 알 수 있습니다.

2. 설정파일을 버전관리에서 제외시키기(설정파일 외부화)

별도의 프로퍼티 파일로 배포 환경에 따라 달라지는 속성들을 빼놓고, 이 파일을 버전관리에서 제외시켜서 개별적으로 관리하는 방법입니다. 공유되지 않기 때문에 운영 서버에 초기 세팅할 때나 로컬 개발환경에서 처음 세팅할 때 설정 파일을 만들어 주거나 기본 템플릿 파일을 수정해줘야겠죠.

3. 빌드 툴을 사용하는 방법(빌드 다양화)

앤트라면 간단하게 타겟 하나를 만들어서 특정 프로퍼티 파일은 지우고 다른 걸 추가하는 식으로 운영용 타겟과 빌드용 타겟을 만들어서 쓸 수 있을 것이고, 메이븐이라면 메이븐 프로파일을 사용할 수 있겠습니다.

이 세 가지 방법 이외에도 어떤 것이 있고 어떤 방법이 좋을지 논의 중입니다.

'스마트 설정' 방식과 '빌드 다양화'는 딱 한번만 설정 해두면 계속해서 쓸 수 있기 때문에 비슷한데, 프로그래밍을 할 것이냐 빌드 설정을 할 것이냐 그것이 고민이고, '설정파일 외부화'는 프로젝트를 새로 받을 때 마다 한번씩 수고해야 하기 때문에 다른 두개 보다는 좀 더 귀찮은 것 같습니다. 하지만... 로컬 서버 환경이라고 해서 전부 다 같은 설정을 사용하진 않겠죠. 누구는 localhost:8080에 설치하고 누구는 loclahost:80을 사용할 수도 있을테니까요. 그런면에서 보면 누구나 하나씩 프로퍼티 파일을 별도로 만들게 하는 '설정파일 외부화' 방법이 가장 적절해 보입니다.

뭘 선택할지 고민이네요. 아~~~~~~ 고민이로세. 고민이야.. @_@

이것들 말고 또 다른 방법은 뭐 없을까요. 획기적으로.. 스크립트 언어를 활용한다던가, 스프링 EL을 쓴다던가...

이럴 땐 찍어서 하나 써보면 될까요? 써보다 뭔가 불편하면 다른 방법으로 갈아타고..
그렇다면, 나중에 갈아타도 시스템에 별로 지장이 없어보이는 녀석을 선택하는게 좋겠네요.

그렇다면... '설정파일 외부화' --> '빌드 다양화' --> '스마트 설정' 순으로 시도해보는건 어떨까 싶습니다.

신고
top


[제이쿼리] 매개변수가 있는 function을 HTML 엘리먼트에서 분리하기

View/JavaScript : 2009.09.22 17:35


매개변수가 없는 함수는 HTML에서 분리가 쉽습니다.

$(document).ready( function(){

    $("#comment_add").click(function() {
        $("form").submit();
    });

}

이런식으로 제이쿼리를 이용해서 click 이벤트가 발생했을 때 특정 폼을 서브밋하라고 이벤트를 등록할 수 있죠. 그러나;; 매개변수가 들어간 녀석들은 어떻게 할지 좀 난감합니다.

<img id="btn_list" class="action" src="<c:url value="/images/study/back.png"/>" onclick="javascript:fn_click_study(${meeting.study.id});"/>

예를 들어 위와 같이 특정 함수에 어떤 변수 하나를 넘겨줘야하는데, 그 값이 매번 달라지고, 목록 속에 들어있는 모든 요소들에 대한 것이라.. 페이지 로딩시에 위와 같이 등록하기가 좀...

그래서 같이 스터디 하는 형 중에 제이쿼리를 유리갤라만큼 하시는 분이 계셔서 물어봤더니 다음과 같이 해결해 두셨더군요.

<img id="btn_edit" class="action" src="<c:url value="/images/study/edit.png"/>" member="${meeting.study.id}" meeting="${meeting.id})" />

이렇게 엘리먼트에 임의이 속성을 추가합니다. 이제 페이지 로딩시에 등록할 수 있습니다.

$(document).ready( function(){

    $("#btn_edit").click(function(){
        var self = $(this);
        var url = '<c:url value="/study/"/>' + self.attr("study") + "/meeting/update/" + self.attr("meeting") + ".do";
        $(document).attr("location", url);

    });

}

아.. 그나저나 c:url 정말 악몽 같네요. 저거 없으면 context path가 루트여야만 제대로 동작하고.. @_@
아휴 귀차나.. ㅠ.ㅠ
신고
top


[Expert One-on-One J2EE Design and Development] J2EE 프로젝트를 위한 설계 기술과 코딩 표준 2

Spring/J2EE D&D : 2009.09.22 11:04


참조: Expert One-on-One J2EE Design and Development 4장

Template Method 디자인 패턴

알고리즘 단계와 그 순서를 알고 있지만, 그 각각의 단계가 구체적으로 어떻게 수행될지는 예측할 수 없을 때 사용할 수 있는 디자인 패턴이다. 템플릿 메서드 패턴은 어떻게 수행될지 모르는 부분을 추상 메서드로 캡슐화 하고, 알고리즘을 순서대로 지니고 있는 추상 클래스를 제공한다. 중요한 개념은 그 추상 클래스가 워크 플로우를 관리한다는 것이다. public 상위 클래스의 메서드는 보통 final이고, 추상 메서드는 protected다.

이렇게 워크 플로우 로직을 추상 상위 클래스로 모아둔 것은 IoC의 예에 해당한다. 할리우드 원칙("Don't call me I'll call you")이라고도 하는 이 원칙은 일반적인 클래스 라이브러리처럼, 사용자 코드가 라이브러리를 사용하는 것이 아니라, 프레임워크 코드가 사용자 코드를 호출한다. 바로 이런 IoC가 프레임워크의 기본이며, 프레임워크는 보통 템플릿 메서드 패턴을 아주 많이 활용한다.

...

템플리세 메서드 패턴은 훌륭한 SoC(Separation of concern)를 제공한다. 상위 클래스는 비즈니스 로직에 집중하고 하위 클래스는 세부 기능 구현에 집중한다.

추상 상위 클래스가 인터페이스를 구현하도록 하는 것이 좋다.

Stratege 디자인 패턴

전략 패턴은 행위를 인터페이스로 빼낸다. 따라서 알고리즘을 알고 있는 클래스는 더이상 추상 클래스가 아니며 구체 클래스가 해당 인터페이스를 구현한 헬퍼를 사용한다. 전략 패턴은 템플릿 메서드 패턴 보다 작업할 것이 많지만, 더 유연하다.

저자(로드 존슨)는 다음과 같은 경우 템플릿 메서드 패턴보다 전략 패턴을 선호한다.

- 모든 스탭이 변하는 경우
- 스탭을 구현하는 클래스가 독립적인 인터페이스 계층 구조를 필요로 할 때.
- 스탭 구현이 다른 클래스와 연관이 있을 경우.
- 스탭 구현체가 실행시에 바뀌어야 하는 경우
- 스탭 구현체가 매우 다양하며 계속 늘어날 여지가 있는 경우.

확장성을 위해 콜백 사용하기

또다른 "IoC"를 사용하여 단일 작업을 매개변수화 해보자. 엄밀히 말하자면, 이것은 일종의 Strategy 디자인 패턴이다.

이 패턴은 메서드에서 호출할 여러 콜백 메서드로 구성된다.

이 패턴은 특히 JDBC같은 저수준 API를 사용할 때 유용하다.

*예제코드는 JdbcTemplate.query() 에서 사용하는  RowCallbackHandler*

다음과 같은 장점과 단점이 있다.

장점
- 프레임워크 클래스가 에러 처리, 자원 가져오기/반납하기를 할 수 있다. 즉 JDBC를 사용할 때 필요한 복잡한 에러 처리를 한 번만 작성하고 그것을 호출하여 사용할 수 있다. 에러 처리와 자원 반납이 복잡할수록 이 방법이 돋보인다.
- 이것을 사용하는(호출하는) 코드는 저수준 API를 신경 쓸 필요가 없다.
- JDBC 같은 저수준 API를 사용할 때 코드 재사용성을 높여준다.

단점
- 실행 플로우 자체를 다루는 코드를 실행하는 것 보다 덜 직관적이다. 코드를 이해하기 어려울 수 있고 관리가 어려워질 수 있다.
- 콜백 핸들러 객체를 만들어야 한다.
- 드문 경우, 인터페이스를 통해 콜백을 호출하니까 성능이.. 문제가..  (이건 좀.. )

이 패턴은 콜백 인터페이스가 매우 가단한 경우에 최대의 가치를 지닌다.

Observer 디자이니 패턴

인터페이스를 사용하는 것 처럼, 옵져서 디자인 패턴도 변경 없이 확장을 가능케하며(Open Closed Principal) 컴포넌트 간의 의존성을 낮춘다. 또한 관심사 분리(Separation of concerns)에도 기여한다.

주의할 것. 리스너는 작업을 빨리 마쳐야한다. 악당같은 리스너는 애플리케이션을 대기시킬 수 있다. 최대한 빨리 리턴하도록 하고 오래 걸리는 작업은 별도의 쓰레드로 분리한다. 리스너는 또한 동기화와 공유 객체 문제도 피해야하고 반드시 멀티 쓰레드에 안전해야 한다.

옵저버 디자인 패턴은 단일 서버보다 클러스터 배포시에 덜 유용하다. 오직 단일 서버에서만 이벤트를 발생시켜주기 떄문이다. 예를 들어 데이터 캐시를 갱신하는데 옵저버 패턴을 썼다면 그런 갱신은 오직 단일 서버에서만 발생할 것이다. 하지만, JMS를 사용한다면 클러스터 환경에서도 그러한 것이 가능하다. 하지만 API 복잡도와 성능 부담은 증가할 것이다.

저자의 경험상 옵저버 패턴은 EJB 단 보다는 웹 단에서 유용하다.

11장에서 옵저버 디자인 패턴을 애플리케이션 프레임워크에서 구현하는 방법을 살펴보겠다.

메서드 매개변수 뭉치기

public void setOptions(Font f, int lineSpacing, int linesPerPage,
                       int tabSize);

이런 것을

public void setOptions(Options options);

이렇게. (이건 리팩토링 기술에 있던것 같은데. 흠..)

주요 장점은 유연성이다. 더 많은 매개변수를 추가해도 메서드 시그너쳐를 변경하지 않아도 된다.

Command 디자인 패턴이 이런 접근 방법을 사용한다.

단점은 객체 생성이 많아질 수 있다는 것이다. 사용하는 메모리가 늘어나고 가비지 컬렉션을 필요로 하게 될 것이다. 객체는 힙 사이즈를 소비하지만, 기초타입은 그렇지 않는다. 이게 문제가 될지는 메서드가 얼마나 자주 호출되느냐에 달려있다.

 
신고
top


[봄싹] 매주 토요일




이러고 지낸답니다. 스터디도 하고 개발도 하다보면 주말이 정말 금방 끝나버립니다. 주 6일 근무 하는 것과 비슷한 기분이지요. 그래도 즐겁답니다. 매번 새로 오시는 분들과 매주 조금씩 발전하는 봄싹 사이트를 보면 정말 뿌듯합니다.

매주 스터디에 나가도 불평 한마디 없이 오히려 배려해주고 신경써주는 와이프와
봄싹에 참여하여 자신의 생각과 코드를 공유해주시는 분들이 있어서 정말 즐겁습니다.

모두 즐거운 한 주 되시고 토요일에 또 뵈요.ㅋ


신고
top


[Expert One-on-One J2EE Design and Development] J2EE 프로젝트를 위한 설계 기술과 코딩 표준 1

Spring/J2EE D&D : 2009.09.18 11:37


참조: Expert One-on-One J2EE Design and Development 4장

좋은 코드란 무엇인가?

- 좋은 코드는 엄청난 변경이 필요없이도 확장이 용이하다.
- 좋은 코드는 읽기 쉽고 유지보수가 편하다.
- 좋은 코드는 문서화가 잘 되어있다.
- 좋은 코드는 나쁜 코드가 발생할 여지가 적다.
- 좋은 코드는 테스트하기 편하다.
- 좋은 코드는 디버깅하기 편하다.
- 좋은 코드는 중복 코드가 없다.
- 좋은 코드는 재사용된다.

J2EE 애플리케이션을 위한 객체지향 설계 권고사항

많은 개발자들이 J2EE API를 익히는데 시간을 많이 소비하지, 좋은 코딩 습관을 기르는데는 너무 시간을 투자하지 않는다. SUN의 J2EE 예제 애플리케이션만 봐도 그런 걸 알 수 있다.

저자(로드존슨)의 경험을 바탕으로 볼 때 좋은 객체지향 원칙을 따르느 것은 탁상공론에 그치는 것이 아니라 실용적인 가치를 전해준다.

객체지향 설계는 (J2EE나 심지어 자바 같은) 특정 구현 기술 보다도 더 중요하다. 좋은 프로그래밍 습관과 적절한 OO 설계는 좋은 J2EE 애플리케이션의 기반이다. 나쁜 자바 코드는 나쁜 J2EE 코드가 된다.

인터페이스를 통해 의존성 낮추기(Loose coupling)

"구현체가 아닌 인터페이스를 사용하여 프로그래밍하라."
Progeam to an interface, not an implementation.

인터페이스 기반 접근방법의 장점

- 호출하는 쪽 코드를 변경하지 않고도 구현 내용을 변경할 수 있다.
- 인터페이스 구현의 자유. 오직 하나의 클래스 상속에 모든 걸 맡길 필요는 없다.
- 간단하게 테스트 구현체와 스텁 구현체를 만들어 제공할 수 있다.

딱딱한 상속 보다는 객체 조합을 선호하라(Composition)

"클래스 상속보다 객체 컴포지션을 선호하라"
Favor object composition over class inheritance

C++과 달리 자바의 클래스 상속은 한개의 클래스만 상속할 수 있다. 클래스 계층 구조는 매우 엄격하다. 클래스의 구현체 일부만 바꾸는 것이 불가능하다.(상위 클래스를 바꾸면 나머지도 다 바뀐다는 뜻인듯.) 하지만, 인터페이스로 그 부분을 (Strategy 패턴과 위임을 사용하여)캡슐화하면 문제는 해결된다.

객체 컴포지션은 클래스 상속보다 훨씬 유연하다. 자바 인터페이스는 위임을 자연스럽게 만들어준다. 객체 컴포지션은 -호출하는 쪽에서 행위를 표현하고 있는 인터페이스의 구현체를 주입하여- 객체의 행위를 실행중(런타임)에 변경할 수 있게 해준다. Strategy와 State 패턴이 이런 접근방법을 사용한다.

클래스 상속이 잘못 사용되는 예

- 간단한 인터페이스 구현이 필요한 상황인데도, 사용자가 추상 또는 구체 클래스를 상속받아 쓰도록 강제한다.  이렇게 하면 사용자 코드가 자신만의 상속 구조를 만들 수 있는 권한을 빼앗는 것이다.

- 하위 클래스가 사용할 헬퍼 메서드들을 상위 클래스에 놓고 클래스 상속을 사용한다. 만약 그 클래스 계층구조 밖에서 헬퍼 메서드를 호출할 필요가 있다면 어떻게 되는가? 객체 컴포지션이 낫다.

- 인터페이스 대신 추상 클래스를 사용하는 경우. Template Method 패턴처럼 추상 클래스가 매우 유용할 떄도 있다. 하지만 추상 클래스는 인터페이스의 대체제가 아니다. 인터페이스를 구현하는 유용한 과정일 뿐이다. 타입을 정의하고자 추상 클래스를 쓰지는 말아라.  다중 상속이 막혀있는 자바에게는 문제가 될 수 있다.

인터페이스는 간단하게 유지할수록 그 가치가 극대화 된다. 인터페이스가 복잡해지면 많은 양의 코드 구현을 해야하기 때문에 추상 클래스 또는 구체 클래스 상속을 강요하게 될 것이다. 따라서 그 가치가 떨어진다. 인터페이스의 적덜한 세밀도가 중요한 경우이다.
 
인터페이스 상속(클래스로 부터 기능을 상속받는 것이 아니라 인터페이스를 구현하는 것)은 클래스 상속보다 훨씬 유연하다.

그럼 클래스 상속이 나쁘다는 걸까? 전혀 아니다. 클래스 상속은 객체 지향 언어에서 코드를 재사용하는 강력한 방법을 제공한다. 하지만 높은 수준의 설계 접근 방법 보다는 구현 방식으로 생각해야 한다. 애플리케이션의 전반적인 설계로 강요하기 보다는 그 사용 여부를 우리가 선택할 수 있어야한다.


신고
top


[봄싹] 스프링 레퍼런스 3.0 번역 시작



http://springsprout.org/wiki/464.do

3.0 레퍼런스 번역을 꾸준히 하겠습니다. 그동안 블로그에 조금씩 번역해서 올려두기도 했었는데 아무래도 레퍼런스 글은 블로그에서 찾아보는게 불편해서 봄싹 위키에 정리하기로 했습니다.

일단은 저 혼자 시작합니다. 하지만 봄싹 회원이라면 누구나 위키 페이지를 추가/수정/이동 시킬 수 있기 때문에 자유롭게 참여하실 수 있습니다.

정해진 틀도 없고 파트를 나누지도 않았지만, 봄싹 사이트도 처음에는 이런 방법으로 개발을 시작했습니다. 지금은 제법 틀도 갖춰져 있고, 특정 모듈 또는 기능 담당자(? 라기 보단 스스로 책임을 느끼시는 분들)도 있습니다. 수직 구조로 누가 누구에게 지시하거나 일을 나눠주지 않고 수평구조로 서로 토론하며 자신이 만들고 싶은 기능을 마음대로 구현해 넣고 있습니다. 레퍼런스도 이런 방법으로 번역을 완성할 겁니다.

기여(?).. 흠. 참여하고 싶으신 분들은 언제나 대환영입니다.

파이팅!

ps: 위키 수정/추가시 포인트를 계산해서 위키 기여도를 측정해야겠군요. 가장 많이 기여한 분에게 봄싹 티셔츠라도...

ps: 위키 미리 보기 화면과 실제 화면이 좀 다른데;; 아마도 조만간 소내기형이 수정해주지 않을까 싶네요... 형 수정해주세요.. ㅠ.ㅠ


신고
top


초고속 로보트 손

모하니?/Watching : 2009.09.15 22:56




오오... 굉장하군요... 굉장해. 마지막에 핸드폰 잡는게 예술입니다.

신고
top

TAG 로보트

[Expert One-on-One J2EE Design and Development] 실용적인 데이터 접근 2

Spring/J2EE D&D : 2009.09.15 21:05


참고: Expert One-on-One J2EE Design and Development 9장

일반적인 JDBC 추상 프레임워크

JDBC API와 그 이슈를 이해하는 것으로 충분하지 않다.

JDBC API를 사용할 때는 항상 도무이 클래스를 사용하여 애플리케이션 코드를 간편화하라. 하지만, O/R 맵핑 계층을 손수 작성하지는 말자. O/R 맵핑이 필요하다면 기존의 솔루션을 사용하라.

동기

저수준 JDBC 코딩을 하는 것은 고통스럽다. 문제는 SQL이 아니라, 코드량이다. JDBC API는 일부 엘레강트하지 않으며 애플리케이션 코드에서 사용하기에는 너무 저수준이다.

SQL 자체는 간단하지만 이 쿼리를 조회하는데 30줄의 JDBC 코드가 필요하다. 거기에 중첩 try-catch까지 보니까 프레임워크 클래스로 리팩토링하고 싶은 강한 욕구를 느낀다.

*고수준 추상화 API*

간단한 추상화 프레임워크를 만들어서 JDBC API 사용을 훨씬 간편하게 할 수 있다.

AvailabilityQuery  availabilityQuery  =  new AvailabilityQuery (ds); 
List 1= availabilityQuery.execute(1, 1) ;

이렇게 할 수 있다. 이 쿼리 객체를 재사용할 수 있으며, execute() 메서드는 런타임 예외를 던진다. 즉 복구가 가능할 경우 필요에 따라 예외를 잡아서 처리할 수도 있다. JDO의 Query 인터페이스와 비슷하다.

목표

파레토 원칙을 기억하는가(80:20) 최선의 결과는 JDBC 추상화 계층으로부터 얻을 수 있다. 추상화 계층은 다음 요소들에 초점을 맞춘다.

- 너무 많은 코드
- 에러 발생시 깔끔한 클린업 - 대부분이 깨진 try-catch를 사용하는데 이를 방지한다.
- SQLException 다루리 - 검증형 예외일 필요가 없다.
- 사용자 코드를 작성하기 쉽게한다.

놀랍게도 이 문제를 다루는 프레임워크와 라이브러리가 많지 않다. 따라서 내가(로드 존슨) 예제 애플리케이션에서 사용할 용도로 프레임워클 개발했다.

이게 유일한 방법은 아니지만, 간단하고 매우 효율적이다.

예외 처리

JDBC API는 예외를 어떻게 사용하지 말하야하는지에 대한 교훈이다.

JDBC는 단일 예외 클래스를 사용한다. "뭔가 잘못됐다" 라는 것 빼고는 알 수가 없다. 이미 살펴봤다시피, 벤더 관련 코드와 SQL 예외를 구분할 수 있다.

다음은 모든 RDBMS에서 의미가 있는 에러들이다.
- 문법 에러
- 데이터 무결성 제약 위반
- SQL의 값을 부적절한 타입의 변수에 바인딩 시도

이런 문제는 java.lang.SQLException의 하위 클래스로 만들어져야 한다. 우리가 만들 추상화 프레임워크에서 풍부한 에러 계층 구조를 제공할 것이다.

JDBC 에러 처리를 할 때 다음의 이슈들도 다룬다.

- 우리가 만들 추상화 계층 코드가 JDBC에 묶이지 않기를 원한다. DAO 패턴 구현체에서 사용할 수 있도록 의도하고 있다. 만약 DAO를 사용하는 코드가 SQLException 같은 특정 리소스에 국한된 것을 처리해야 한다면 비즈니스 로직과 (DAO 패턴을 구현한)데이터 접근 구현체 간의 디커플링(decoupling)을 할 수가 없다. (메서드 시그너처에 들어가기 떄문에..) 따라서 JDBC API는 JDBC 관련 예외를 사용하더라도 우리가 만들 예외 계층 구조는 JDBC에 묶이지 않게 만들겠다.

- 4자에서의 검증형 예외와 비검증형 예외 논의에 따라 모든 예외를 런타임 예외로 만들겠다. JDO가 이런 접근 방법을 통해 좋은 결고를 냈다. JDBC 예외는 대부분 복구가 불가능하기 때문에 이런 방법이 적절하다.

- EJB를 쓴다면 블라 블라. 생략.

일반적인 데이터 접근 예외 계층구조

일반적인 데이터 접근 예외 계층구조를 만들어서 위와 같은 요구사항을 만족시킬 수 있다. JDBC 사용에만 국한 되지 않고 데이터베이스를 사용하는 DAO에서 모두 사용할 수 있다.

계층 구조의 최상위는 DataAccessException 클래스다. NestedRunitmeException을 상속받았다. 이 상위 클래스는 감쌓아야 할 예외를 스택 트레이스에 유지할 수 있게 해준다. 자세한건 4장에..

DataAccessException은 구체적인 데이터 접근 문제를 나타내는 하위 클래스를 가지고 있다.
- DataAccessResourceFailureException
자원을 가져오지 못한 경우에 발생. JDBC용 구현에서는 데이터베이스에서 connectino을 가져오지 못했을 때 발생한다.
- CleanupFailureDataAccessException
작업 수행을 잘 마치고 깨끗히 정리를 못했을 때 발생. JDBC용 구현에서는 Connection을 닫지 못했을 때 발생.
- Datalntegr ityViolationExcept ion
- InvalidDataAccessApiUsageException
- InvalidDataAccessResourceUsageException
부적절한 SQL을 날릴 때 발생
- OptimisticLockingViolationException
- DeadlockLoserDataAccessException
- UncategorizedDataAccessException
차마 분류하지 못한 것들...

JDBC 예외를 일반적인 예외로 변환하기

지금까지는 SQL 예외를 일반적인 예외로 변환하는 방법을 생각하지 않았다. 그렇게 하려면 SQLState 코드와 벤더 코드를 분석해야 한다. 하지만 SQLState로는 모든 문제를 분석하기에 적절치 않다. RDBMS 구현체 특화된 옵션들을 분석해야 한다.

변환 기능을 제공하는 인터페이스를 만든다. (보통 인터페이스를 사용하는 설계가 이식성을 얻을 수 있는 최선책이다.) 이 책에서는 SQLExceptionTranslater 인터페이스를 구현할 것이다.

public interface SQLExceptionTranslater {
    DataAccessException translate(String task, String sql, SQLException  sqlex);
}

*SQLState 코드를 사용하는 SQLExceptionTranslater 기본 구현체 코드*

정적인 데이터 구조(HasSet)를 만들어서 if/else 문을 줄였다.(묶을 수 있는 에러덩어리들을 하나의 맵에 넣어두고 map.contains()를 사용하여 if문 사용을 줄였군요.. 오호...)

*오라클에 특화된 구현체 OracleSQLExceptionTranslater*

벤더 코드가 SQLState 코드 보다 훨씬 많다.

2 단계 추상화 계층

강력하면서도 데이터베이스에 종속적이지 않은 예외 처리 계층 구조를 가지게 되었다. JDBC 사용을 간편하게하는 추상화 프레임워크를 구현해보자.

JDBC 프레임워크를 2 단계 추상화 계층으로 나눌 수 있다.

낮은 수준의 추상화는 com.interface21.jdbc.core 패키지에 있다. JDBC 워크프롤우와 예외 처리를 담당한다. 콜백 접근 방법을 취하였으며 애플리케이션 코드가 콜백 인터페이스를 구현토록 한다.

높은 수준의 추상화는 com.interface21.jdbc.core.object 패키지에 있다. JDO와 비슷하게 보다 객체 지향적인 방법을 제공한다. RDBMS 작업을 자바 객체로 표현할 수 있다.

JDBC 워크프롤우와 에러 처리를 담당하는 프레임워크

낮은 수준의 추상화는 JDBC 쿼리를 보내고 SQLException을 SQLExceptionTranslater를 사용하여 일반적인 예외 계층 구조로 변환한다.

다시 보는 "Inversion of Control"

일반적인 클래스 라이브러리 있는것처럼 애플리케이션 코드가 인프라 코드를 호출하는 것이 아니라.. 인프라 코드가 애플리케이션 코드를 사용하도록(이 부분이 "IoC"라 불리는 접근방법) 복잡한 예외 처리 코드를 그(인프라 코드) 안으로 넣어서 해결하는 방법을 보았다. 이런 접근 방법을 사용한 패키지를 "프레임워크"라 하고 그렇지 않은 것을 라이브러리라 한다.

com.interface21.jdbc.core 패키지

이 패키지에서 가장 중요한 클래스는 JdbcTemplate이다(오오.. 드디어.. 등장이다.) 핵심 워크 프롤우와 애플리케이션 코드 호출을 담당한다. jdbcTemplate에 있는 코드는 PreparedStatements 생성을 위임하여 쿼리를 실행하고 그 결과를 JDBC ResultSet에서 뽑아내는 콜백을 사용한다. 그 두 인터페이스가 바로 PreparedStatementCreator와 RowCallbackHandler 인터페이스다. 애플리케이션 개발자는 이 두 인터페이스 구현체만 만들면 된다. JDBC statement를 직접 실행하거나 예외를 처리할 필요가 없다.

preparedStatementCreator 인터페이스와 관련 클래스

preparedStatementCreator 인터페이스는 java.sql.PreparedStatement를 만들 애플리케이션 클래스가 반드시 구현해야 한다. 즉 SQL과 매개변수만 바인딩 시키면 jdbcTemplate이 실행해줄 것이다. 이것을 구현할 때 Connection을 가져오는 것이나 SQLException 처리는 신경쓰지 않아도 된다.

public interface PreparedStatementCreator {
PreparedStatement createPreparedStatement(Connection conn)
throws   SQLException; }

PreparedStatementCreatorFactory는 동일한 SQL에 매번 다른 매개변수로 PreparedStatementCreator 객체를 만들 때 도와주는 클래스다. 이 클래스는 높은 수준 추상화 프레임워크에서 빈번하게 사용한다.

RowCallbackHandler 인터페이스와 관련 클래스

RowCallbackHandler는 쿼리가 반환하는 ResultSet의 행에서 컬럼 값을 축출하는 클래스가 구현해야하는 인터페이스다. JdbcTemplate이 모든 ResultSet을 순회할 때 사용한다. 이 인터페이스는 SQLException을 그대로 나뒀다. JdbcTemplate이 처리한다.

public  interface RowCallbackHandler  {
void processRow(ResultSet  rs)   throws  SQLException; }

구현체는 컬럼의 수와 데이터 타입을 알고 있어야 한다.

RowCountCallbackHandler 클래스는 이 인터페이스의 구현체로 컬럼 이름과 타입 그리고 ResultSet의 행 갯수에 대한 정보를 가지고 있다. 비록 구체적인 클래스이지만, 애플리케이션 클래스가 이것을 상속받아서 사용한다.

ResultReader 인터페이스는 RowCallbackHandler를 확장하여 가져온 결과를 java.util.List에 저장하는 인터페이스다.

그밖의 클래스

JdbcTemplate 클래스는 SQLExceptionTranslator 객체 하나를 사용하여 SQLException을 일반화된 예외 계층구조로 변환한다. 중요한 건 JdbcTemplate의 동작을 매개변수화 했다는 것이다.

DataSourceUtils 클래는 javax.sql.DataSrouce 에서 Connection을 가져오는 static 메서드, SQLException을 일반화된 계층 구조로 변환하는 메서드, Connection을 닫는 메서드, JNDI에서 DataSource를 가져오는 메서드를 담고 있다.

JdbcTemplate은 DataSourceUtils 클래스를 사용한다.

JdbcTemplate 클래스의 핵심 워크 플로우

이 API가 전반적으로 java.sql.Connection 객체가 아니라 DataSource를 사용하는 이유는 다음과 같다.
- 그렇게 하지 않으면 connection을 어디에선가 얻어와야 하는데 그럼 애플리케이션 코드가 복잡해지고 DatsSrource.getConnection() 메서드를 사용한다면, 애플리케이션 코드에서 SQLException을 처리해야 할 것이다.

- JdbcTemplate이 사용한 connection을 닫는 것이 중요한데 connection을 닫을 때 예외가 발생할 수 있기 때문에 모든 JDBC 예외를 처리를 우리 프레임워크에 맡기고 싶다. 그러나 connection을 사요하면 connection을 외부에서 가져오고 그것을 JdbcTemplate에서 닫는 건 이상하고 이미 닫혀버린 connectino을 사용할 수 있는 여지를 만들게 된다.

JdbcTemplate 클래스 사용하기

조회하기

*JdbcTemplate 사용하여 조회(query)하는 예제 코드*

익명 내부 클래스르 사용하여 RowCallbackHandler와 PreparedStatementCreator 인터페이스를 구현했다.

가장 중요한 것은 JdbcTemplate을 사용함으로써 주요 에러 발생을 제거했다는 것이다. Connection이 닫히지 않을 위험이 없어졌다.

갱신하기

*JdbcTemplate 사용하여 갱신(update)하는 예제 코드*

PreparedStatementCreator 구현체를 만들고 그 객체를 jdbcTemplate.update()에 넘겨주면 끝. 단 한줄이다.


신고
top


[Expert One-on-One J2EE Design and Development] 실용적인 데이터 접근 1

Spring/J2EE D&D : 2009.09.15 20:28


참고: Expert One-on-One J2EE Design and Development 9장

데이터 접근 기술 선택

J2EE 애플리케이션에서 사용할 수 있는 데이터 접근 기술 들을 두 개의 카테고리로 분류할 수 있다. SQL 기반과 O/R 맵핑 기반이다.

SQL 기반 기술

JDBC

JDBC는 SQL을 기반으로 한다. 저장 프로시저, 커스텀 쿼리, RDBMS에 특화된 기능들을 사용할 때 적절하다.

중 요한 것은 우리가 JDBC를 어떻게 사용하느냐이다. 나이브한(안일한) 접근 방법은 애플리케이션 코드에서 JDBC 코드와 SQL문을 사용하는 것인데 이것은 큰 재앙을 가져올 것이다. 전체 애플리케이션을 특정 영속화 전략에 묶어버려서, 데이터 스키마가 바뀔 때 문제가 생길 것이다. 하지만 다음의 가이드라인만 지킨다면 애플리케이션 코드에서 JDBC를 효율적으로 사용할 수 있다.

- JDBC 접근 코드를 비즈니스 로직에서 최대한 분리하라. JDBC 코드는 오직 DAO에만 있어야 한다.
- JDBC API를 직접 사용하는 저수준(쌩짜) JDBC 코드를 기피하라. JDBC 에러 처리는 생산성을 심각하게 저하할 정도로 난잡하다. 에러 처리같은 저수준의 구체적인 내용들은 도우미 클래스로 빼버리고 고수준 API를 사용하라. SQL 제어권에 지장없이 그렇게 하는 것이 가능하다.

JDBC를 쓰는 것에 대해서는 아무 문제가 없다 다면, 세션 EJB같은 비즈니스 객체나 DAO에서 조차 JDBC를 직접 사용하지는 말자. 비즈니스 컴포넌트를 저수준 JDBC에서 분리할 수 있는 추상화 계층을 사용하라.

SQLJ

생략

O/R 맵핑 기술

O/R 맵핑 기술은 JDBC나 SQLJ 같은 API와는 전혀 다른 프로그래밍 모델이다. 이것을 사용해서도 J2EE 애플리케이션에서 DAO를 구현할 수 있다.

기업 수준에서 사용할만한 O/R 맵핑 솔루션을 제공하는 오픈 소스 제품이 없다.(로드 존슨이 이 책을 쓸 당시 하이버네이트가 없었거나.. 그리 알려지지 않았었나 봅니다.), 

상용 제품

생략

TopLink

생략

CoCoBase

생략

JDO

JDO는 J2EE에 중요한 기술이지만, 미션-크리티컬한 엔터프라이즈 애플리케이션에서 사용하기에는 아직 성숙도가 증명되지 않았다.(역시 책이 쓰여진 시점을 신경쓰며 읽으셔야겠죠)

샘플 애플리케이션에서 사용할 데이터 접근 기술 선택하기

(어찌저찌해서 JDBC 선택! 캐싱할 껀덕지도 별로 없고 저장 프로시저도 효율적으로 사용해야 하기 때문이라고 하는군요.)

JDBC 자세히 보기

그간의 경험을 통해 심각한 문제를 야기할 수 있는 J2EE 애플리케이션에서 sloppy(지져분한, 더러운, 진흙물이 튄것 같은) JDBC 코드를 보았다.

*에러 처리를 제대로 못한 예제 코드*
(대부분의 JDBC 코드와 con.close(); 까지 하나의 try-catch 문으로 묶여있습니다.)

이렇게 코딩을 하면 전체 애플리케이션이 깨지거나 데이터베이스가 뻗을 수 있다. con.close()까지 가기 전에 SQLException이 발생하면, 커넥션을 반환하지 못할 것이기 때문이다.

*에러 처리를 제대로 한 예제 코드*
(con.close();를 finally 블럭에서 null 체크를 한 다음에 수행하면서 try-catch로 묶어줬습니다.)

이렇게 하는것이 좀 더 견고하지만 너무 장황하다. 가장 간단한 JDBC 기능을 하나 수행하는데 39줄의 코드가 필요하다. 자.. 이제 JDBC API를 직접 쓰는것 말고 더 좋은 어떤 방법이 필요하다는 것을 분명히 알 수 있다.

SQLException에서 정보 축출하기

JDBC 예외 처리에서 또 다른 중요한 것은 SQLExcpetion에서 최대한 정보를 뽑아내는 것이다. 불행히도 JDBC는 이 걸 매우 복잡하게 만든다.

java.sql.SQLException은 getNextException() 메서드로 다음 SQLException을 받아올 수 있다. JDBC는 모든 예외 상황에 대해 오직 한 개의 예외 클래스만 사용하고 있다. 또한 벤더 코드와 SQLState 코드를 포함하고 있다.

벤더코드는 getErrorCode() 메서드가 반환하는 int 값이다. 그 이름이 암시하듯이 벤더 코드는 벤더마다 다를 수 있다. 따라서 이 코드에 의존하는 것은 이식성을 저해한다.

getSQLState() 메서드는 5자리 문자열을 반환한다. 이론적으로는 이식이 가능한 에러 코드다. 처음 두 자리는 고수준의 정보를 담고 있고 다음 세자리는 보다 상세한 정보를 담고 있다.

불행히도 SQLState 코드는 모든 데이터베이스에서 지원하지 않으며 문제에 대해 충분한 정보를 제공해주지도 못한다. 때에따라 벤더 코드가 구체적인 정보를 얻을 수 있는 유일한 방법이 된다. 대부분의 데이터베이스는 SQLState 코드보다 훨씬 많은 벤더 코드를 제공하며 더 잘 정리된 문서를 제공하고 있다.

java.sql.SQLWarning 예외도 SQLException 만큼이나 잘 이해할 필요가 있다. SQLException처럼 검증형 예외(checked exception)지만, JDBC API가 실제로 던지지는 않는다. 치명적이지 않은 SQL 에러로 여기고 그 내용을 ResultSet, Statement, Connection에 덭 붙여준다. 이런 JDBC API에 getWarnings() 메서드를 호출해서 가져올 수 있다.

이런 Warning이 JDBC를 직접사용하면 좋치않은 이유 중 하나다. JDBC를 직접사용하는 코드에서 Warging을 확인하는 과정을 추가하면 111줄의 코드가 더 추가된다. 실제로 하기엔 너무 많다.

PreparedStatement 질문

java.sql.PreparedStatement 인터페이스와 java.sql.Statement 인터페이스의 차이를 이해하는 것이 중요하다. prepared statent는 변수들을 바인딩할 위치와 각각의 위치에 매개변수를 설정할 수 있는 기능을 제공한다.

SQL에 적합한 문자 형태로 바꿔주는 역할을 해준다.

문자열 보다 더 효율적이다. 처음 prepared statement를 실행하면 파싱되고 데이터베이스에 의해 컴파일 되며 statement 캐시에 저장될 수 있다. 반면, 일반적인 SQL 문자열은 매번 변수 값이 달라질 경우 이렇게 캐시를 할 수가 없다.

JDBC 2.0은 prepared statement 캐싱을 필수로 하지 않았지만, 3.0에서는 표준으로 정했다.
신고
top


봄싹 9월 특강 Completed!! 후기랄까나...

모하니?/Thinking : 2009.09.15 01:48



켄트벡 세미나와 KSUG 번개에 나갔었다면, 볼 수 있었겠지만 개인 사정으로 그동안 토비님을 못 뵈었다가 드디어 봄싹 스터디에 초대하여 3시간짜리 스터디 진행을 부탁드렸습니다. 커피 한 잔과 감자탕 한 끼로 너무 많이 부려먹은듯(?)해서 죄송스럽지만, 뭐.. 제자에게 이정도쯤은.. 해주셔야.. ㅎㅎㅎ 그저 감사 할 따름입니다. (__)/

이번에 사부님을 만나 느낀점은 많지만 그 중에서 개발자로서의 고민이 좀 더 심화되었습니다. 사실 다음 DebDay를 다녀왔을 때부터 느끼던 것인데 털어놓진 않고 속으로 앓고 있었지요.

문제의 핵심은 제가 작성한 코드가 개떡같다는 거였습니다.

그 개떡같은 코드는 현재 제 노트북에만 있고 그 어디에도 공개하지 않았습니다. 저번 달인가 이번 달 초에 다음 DevDay 때문에 제주도에  갔었을 때 작성한 코드가 정말 최악이었습니다. 스프링 코드를 거의 사용하지 않고 Smack과 java.net 패키지를 주로 이용하여 코드를 작성했었는데 정말 끔찍했습니다. 조금씩 계속 지져분해지기 시작하더니 어느 순간 걷잡을 수 없는 형태의 코드가 되어버렸고... 그 뒤는... 동작하긴 하지만 속은 다 썪어서 도무지 남에게 보여줄 수 없는 코드가 되었습니다.

그러던 중... 봄싹 스터디에서 스프링의 가장 기초이자 핵심은 DI가 어떤 것인지 보여주는 명쾌한 코드와 설명을 보면서 다시 한 번 자극을 받을 수 있었습니다. 스프링의 핵심인 DI가 어떤 과정으로 탄생되는 것인지 살펴보았는데, 그 과정이 굉장히 논리적이고 깔끔했습니다.

거기에다, 비밀리에 베타리딩 중인 책에서도 스프링이 어떤 문제를 어떤 방법으로 해결해나가는지 순차적으로 보여주는 내용이 저에게 많은 도움이 되고 있습니다.

스프링의 가장 초기 모습을 볼 수 있는 빨간책 1권과 코드를 중심으로 살펴보면서 스프링 DI 감각을 익히는 것이 중요하고 재밌겠다고 생각했습니다. 그래서 갑작스래 봄싹 스터디에 '오리지널 스프링' 스터디를 만들었는데, 생각보다 많은 분들이 참여하고 계십니다. 슬슬 스터디 홍보로 전환되는 듯 한데 어서 마무리하고 좀 더 공부하다 자야겠습니다.

밤늦게 글을 써서 그런지 두서가 없는데, 결론은 토비님 덕분에 스프링을 좀 더 진지하게 공부하게 되었다는 것입니다. 부디.. 개떡같은 코드가 찰떡같은 코드로 거듭나길 바라며~~ 열공!!
신고
top


스프링 이메일 확장하기 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


완전 부럽다...

모하니?/Thinking : 2009.09.09 06:59


연봉 5천. 제네시스 지급. 몇 년뒤 벤츠로 바꿔줌. 상여금은 연간 두 번 800%. 매달 기름값 100만원 지급. 기타 등등등...

매달 순수 월급만 세금떼고 350은 넘게 받으실 텐데, 그거의 800%를 일년에 두 번이나 받으시면 제가 1년에 버는 돈보다 많은 돈을 보너스로 받으시는 겁니다. 거기에 매달 기름값을 하라고 100만원씩 받으신다면.. 와우... *_*

가까우신 분의 최근 입사 조건입니다. 저것보다 더 좋은 조건으로 일하고 계신 분들도 많이 있겠죠? 저는 개발자로서, 어찌하면 저 만큼의 대우를 받을 수 있을까요? 어떻게 하면 저만큼 회사에 필요한 사람이 될 수 있을까요? 제가 요즘 활동하며 공부하는 것들이 미래에 저런 대우를 받을 수 있는 행위일까요? 아니면 나 자신을 위안하려는 행위들에 불과한 걸까요?

저는 어떤 길을 가고 있는 걸까요? 무슨 길이든 좋으니... 지금처럼 하고 싶은 공부 마음껏 할 수 있고, 공부한 걸 적용해볼 수 있고, 주말이면 다른 개발들과 만나서 즐겁게 공부도 하고 개발도 할 수 있고, 늦지않게 집에가서 와이프랑 놀 수 있으면서도... 미래에 가질 자식들 육아비용 걱정없이, 공기 좋고 물 좋은 곳에서, 대 여섯 식구가 오손도손 살 수 있으면 좋겠습니다.

나한텐 언제 해가 뜰까?? 내일? 매일 같이 '내일' 해가 뜨는건 아닐가?
신고
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


[하이버네이트] 2차 캐싱 적용하기

Hibernate/etc : 2009.09.07 18:43


참조
Java Persistence With Hibernate
http://whiteship.tistory.com/1256
Improving Hibernate's Performance
19.2. The Second Level Cache
Hibernate: Truly Understanding the Second-Level and Query Caches
Hibernate Caching

이론적으로 정리할 것 들

- 2차 캐싱이라고 하는 이유? 1차 캐시(persistence context cache)는 있으니깐.
- 하이버네이트 캐싱 종류(트랜잭션 스콥 캐싱, 프로세스 스콥 캐싱, 클러스터 스콥 캐싱)
- 캐싱에 적당한 클래스(데이터 변경이 드물고, (금전적인 데이터 처럼)핵심적인 데이터가 아니며, 공유하는 데이터가 아니고 애플리케이션에 국한된 데이터)
- 동시성 전략(Transactional, Read-write, Nonstrict-read-write, Read-only)
- 캐싱 공급자 선택하기(EHCache, OSCache, SwarmCacahe, JBoss Cache)

적용하기

1. 캐싱 동시성 전략 선택하기

read-wirte 모드를 선택함. read committed isolation 수준을 유지하려고.

@Entity
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
public class Study implements Serializable{

2. SessionFactory 설정에 2차 캐싱 관련 설정 추가.

    <bean id="sessionFactory"
        class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="packagesToScan" value="springsprout.domain" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
                <prop key="hibernate.generate_statistics">true</prop>
                <prop key="hibernate.cache.use_second_level_cache">true</prop>
                <prop key="hibernate.cache.use_query_cache">true</prop>
                <prop key="hibernate.cache.provider_class">net.sf.ehcache.hibernate.EhCacheProvider</prop>
            </props>
        </property>
    </bean>

2차 캐시와 함께 쿼리 캐시도 켜주고, 사용할 캐싱 공급자를 설정해 줍니다.

3. Query 또는 Criteria API에 cacheable 설정 true로 변경.

기본값이 false기 때문에 이렇게 해주지 않으면 캐시가 적용되지 않습니다.

    public List<Study> getStudyList() {
        Criteria c = getCurrentSession().createCriteria(Study.class);
        c.setCacheable(true);
        return c.list();
    }

4. 테스트

    @Test
    public void getAll() throws Exception {
        insertXmlData("testData.xml");
        assertThat(sr.getStudyList().size(), is(2));
        assertThat(sr.getStudyList().size(), is(2));
    }

콘솔에 select 쿼리가 한 번만 찍히는지 확인합니다.

5. (optional) ehcache.xml 설정 파일 추가.

<ehcache>
    <diskStore path="java.io.tmpdir"/>
    <defaultCache maxElementsInMemory="500" eternal="true"
        overflowToDisk="false" memoryStoreEvictionPolicy="LFU" />
    <cache name="springsprout.domain.study.Study"
        maxElementsInMemory="500" eternal="true" overflowToDisk="false"
        memoryStoreEvictionPolicy="LFU" />
    <cache name="springsprout.domain.study.Meeting"
        maxElementsInMemory="500" eternal="true" overflowToDisk="false"
        memoryStoreEvictionPolicy="LFU" />
    <cache name="org.hibernate.cache.StandardQueryCache"
        maxElementsInMemory="5" eternal="false" timeToLiveSeconds="120"
        overflowToDisk="true" />
    <cache name="org.hibernate.cache.UpdateTimestampsCache"
        maxElementsInMemory="5000" eternal="true" overflowToDisk="true" />
</ehcache>

이런식으로 만든다음 src 폴더 바로 밑에 추가해주면 끝.

6. 고민하기

- 콘솔을 눈으로 확인하는;; 수동 테스트인데 이걸 어떻게 하면 자동화 테스트로 바꿀 수 있을까요. 흠..
- SessionFactory 자체에 아예 모든 Qeury와 Criteria가 캐시를 사용하도록 설정할 수는 없을까요?
- 스프링 AOP를 이용한 애플리케이션 차원의 캐시와 지금 사용한 시스템적인 캐시 중에 어떤것을 어떤 경우에 사용해야 적당한 것일까?

신고
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







티스토리 툴바