Whiteship's Note

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

  1. 2009.11.18 [웹 사이트 속도 향상 베스트 프랙티스 3] Expires 또는 Cache-Control 헤더 추가하기
  2. 2009.11.17 [웹 사이트 속도 향상 베스트 프랙티스 2] CDN(Content Delivery Network) 사용하기 (2)
  3. 2009.11.16 [웹 사이트 속도 향상 베스트 프랙티스 1] HTTP 요청 최소화 하기 (4)
  4. 2009.11.12 [코딩] 데이터를 요청하지 말고 작업을 요청하라. (2)
  5. 2009.11.10 [하이버네이트] @BatchSize로 쿼리 갯수 대폭 줄이기
  6. 2009.10.31 [하이버네이트] 컬럼 타입은 어떻게 명시하는게 좋을까?
  7. 2009.10.29 [봄싹] XP 적용 시나리오 3. 개발하기
  8. 2009.10.29 [봄싹] XP 적용 시나리오 2. 배포 계획하기
  9. 2009.10.29 [봄싹] XP 적용 시나리오 1. 스토리 만들기
  10. 2009.10.23 [봄싹] 모임 추가 시나리오 - web flow (구현)
  11. 2009.10.23 [봄싹] 모임 추가 시나리오 - web flow
  12. 2009.10.22 [Spring Wev Flow(2.0.8)] SecurityFlowExecutionListener 패치 for Spring Security 3.X
  13. 2009.10.21 [코드값] 애플리케이션에 DB id값이... (6)
  14. 2009.10.20 [봄싹] 메일 서비스를 알림 서비스로 통합하기
  15. 2009.10.20 [리팩토링] 메일을 메시지로 통합하기
  16. 2009.10.16 [JUnit] @Rule TemporaryFolder 사용하기 (2)
  17. 2009.10.16 [DBUnit] 테이블 데이터를 엑셀로 export
  18. 2009.10.15 [기초 데이터] 백업/복구 방안
  19. 2009.10.15 [봄싹 버그]] JSON 뷰와 하이버가 가져온 Proxy 객체
  20. 2009.10.14 [권한] 3단 구조
  21. 2009.10.13 [자바스크립트] confirm() 주의할 것
  22. 2009.10.13 [테스트] 스프링의 MappingJacksonJsonView 초간단 학습 테스트
  23. 2009.10.12 [제이쿼리] 마우스 오버/아웃 이벤트 사용하기 (6)
  24. 2009.10.05 [작명 고민] 하이버네이트 get/find류 작명 규약 1 (2)
  25. 2009.09.29 [봄싹] 새 기능 소개 (4)
  26. 2009.09.29 [테스트 데이터] 테스트에 필요한 데이터 만들기
  27. 2009.09.29 [테스트 코드 리팩토링] extract method
  28. 2009.09.26 [JSP 리팩토링] 태그 파일로 중복 코드 제거하기
  29. 2009.09.23 [NotificationService] NotificationService 인터페이스 기반 프로그래밍 (2)
  30. 2009.09.23 테스트 환경와 운영 환경용 설정 분리/구분하기 (4)

[웹 사이트 속도 향상 베스트 프랙티스 3] Expires 또는 Cache-Control 헤더 추가하기

모하니?/Coding : 2009.11.18 16:34


http://developer.yahoo.com/performance/rules.html#expires

Add an Expires or a Cache-Control Header

tag: server

이 규칙에는 두 가지가 있다.

* 정적 컴포넌트인 경우: 아주 긴 시간으로 Expires 헤더를 설정하여 "Neber expire" 정책을 구현한다.
* 동적 컴포넌트인 경우: 적절한 Cache-Control 헤더를 사용하여 브라우저를 도와준다.

웹 페이지 디자인은 점점 더 풍부해지고 있다. 즉 보다 많은 스크립트, 스타일시트, 이미지, 플래시가 페이지에 들어간다는 것이다. 여러분 페이지를 처음 방문하는 사용자는 아마도 몇 개의 HTTP 요청을 보내야 할 것이다. 하지만 Expires 헤더를 사용하여 그런 컴포넌트들을 캐시가 가능하도록 설정할 수 있다. 이렇게 하면 연속하여 페이지를 참조할 때 불필요한 HTTP 요청을 줄일 수 있다. Expires 헤더는 대부분 이미지에 자주 사용하지만, 스크립트, 스타일시트, 플래시 컴포넌트를 포함한 모든 컴포넌트에도 활용할 수 있다.

브라우저(와 프록시)는 캐시를 사용하여 HTTP 요청과 사이즈를 줄이고, 웹 페이지 로딩 속도를 높인다. 웹 서버는 Expires 헤더를 HTTP 응답에 사용하여 클라이언트가 얼마나 오래 컴포넌트를 캐시할지 알려준다. 다음은 아주 오래동안 캐시를 보관하도록 설정한 Expires 헤더이다. 브라우저에게 이 응답은 2010년 4월 15일까지 변하지 않을꺼라고 알려준다.

      Expires: Thu, 15 Apr 2010 20:00:00 GMT

만약 여러분의 서버가 아파치라면, ExpiresDefault 지시자를 사용하여 현재 시간 기준으로 상대적인 만료 날자를 설정하라. 다음 예제는 만료 날짜를 요청을 받은 시점 부터 10년 뒤로 설정하는 ExpiresDefault 지시어다.

      ExpiresDefault "access plus 10 years"

명심해야 할 것이 있다. 만약 아주 오래 살도록 Expried 헤더를 사용한 경우에는 컴포넌트를 변경할 때 마다 반드시 컴포넌트의 파일이름을 변경해 주어야 한다. 야후에서는 이런 작업을 빌드 과정 중 하나로 만들어 사용한다. 컴포넌트의 파일 이름에 버전 넘버를 명시해주는 것이다. 예를 들어 yahoo_2.0.6.js 처럼.

아주 오래 캐시를 유지하도록 설정한 Expires 헤더는 이미 여러분의 사이트를 방문했던 사용자가 보는 페이지에만 적용된다. 사이트를 처음 방문하는 사용자의 HTTP 요청이거나 브라우저의 캐시가 비어있는 경우에는 아무런 영향이 없다. 따라서 이런 성능 개선의 효과는 사용자가 어느정도 '준비된 캐시'를 가지고 페이지를 얼마나 자주 방문하느냐에 달려있다. ('준비된 캐시'에는 페이지의 모든 컴포넌트를 이미 가지고 있다.) 우리는 야후에서 이것을 측정했고 그 결과 준비된 캐시를 가지고 방문하는 페이지 뷰의 수가 75~85%에 달한다는 것을 발견했다. 아주 왜 캐시를 유지하는 Expires 헤더를 사용함으로써, 브라우저가 캐시할 컴포넌트 수를 늘리고 연쇄적으로 페이지를 볼 때 사용자 인터넷 연결에 한 바이트로 보내지 않을 수 있다.

참조: http://www.mnot.net/cache_docs/
top


[웹 사이트 속도 향상 베스트 프랙티스 2] CDN(Content Delivery Network) 사용하기

모하니?/Coding : 2009.11.17 21:39


http://developer.yahoo.com/performance/rules.html#cdn

Use a Content Delivery Network

tag: server

여러분 웹 서버에 대한 사용자 접근은 응답 시간에 영향을 준다. 컨텐츠를 여러개로 분산되어 있는 서버에 배포한다면 사용자 시점에서 페이지 로딩을 빠르게 할 수 있다. 하지만 무엇부터 시작해야 할까?

지리학적으로 분산되어 있는 컨텐트(geographically dispersed content)를 구현할 때 제일 먼저 할 것은, 여러분의 웹 애플리케이션을 분산 아키텍처로 다시 설계하는 짓을 하지 않는 것이다. 애플리케이션에따라, 아키텍처의 변경은 여러 곳에 위치한 서버를 넘나드는 '세션 상태 동기'와 '데이터베이스 트랜잭션 복사' 작업의 압박을 받을 수 있다. 사용자와 컨텐츠 사이의 거리를 좁히려는 시도는 그러한 애플리케이션 아키텍처 변경 단계로 말미암아 연기 되거나 그 단계를 못 벗어날지도 모른다.

기억할 것은 80-90%의 최종 사용자 응답 시간은 페이지의 컴포넌트들(이미지, 스타일시트, 스크립트, 플래시 등)을 다운로드 하는데 소비 된다는 것이다. 바로 이것이 성능에 있어서 황금 법칙이다. 여러분 애플리케이션 아키텍처를 다시 설계하는 어려운 작업부터 시작하지 말고, 정적인 컨텐츠들을 분리하는 것부터 먼저 하는 것이 더 좋다. 응답 시간을 크게 줄여줄 뿐 아니라, 고마운 CDN 덕분에 그 작업도 쉽다.

CDN은 보다 효율적으로 사용자에게 컨텐츠를 제공하기 위해 여러 곳으로 분산되어 있는 웹 서버 집합이다. 특정 사용자에게 컨텐츠를 제공할 서버는 기본적으로 네트워크 접근성 측정을 기반으로 선택된다. 예를 들어, 가장 낮은 네트워크 홉(hop)을 가지고 있거나 가장 빠른 응답 시간을 가지고 있는 것이 선택된다.

몇몇 대규모 인터넷 회사들은 자신들의 CDN을 가지고 있지만, Akamai Technologies, Mirror Image Internet, Limeight Networks 같은 CDN 서비스 제공자를 사용하는 것이 비용적으로 효율적이다. 신생 회사나 개인적인 웹 사이트의 경우 CDN 서비스 비용이 비쌀수도 있지만, 여러분의 대상 고객이 점점 증가하고 점점 글로벌화 됨에 따라, 첫 응답 시간을 높이기 위해 CDN이 필요해질 것이다. 야후에서 정적인 컨텐츠를 웹 서버에서 CDN으로 옮김으로써 사용자 응답 시간을 20% 이상 향상 시켰다. CDN으로 바꾸는데 필요한 것은 여러분의 웹 사이트 속도를 극적으로 향상시키는것에 비해 상대적으로 간단하게 코드를 변경하기만 하면 된다.
top


[웹 사이트 속도 향상 베스트 프랙티스 1] HTTP 요청 최소화 하기

모하니?/Coding : 2009.11.16 18:46


참조: http://developer.yahoo.com/performance/rules.html#num_http

HTTP 요청 최소화하기(Minimize HTTP Requests)

tag: content

사용자 응답 시간 중 80%가 브라우저 단(front-end)에서 소요된다. 이 시간 중 대부분이 페이지의 모든 컴포넌트들(이미지, 스타일시트, 스크립트, 플래시 등)을 다운로드 하는데 소요된다. 컴포넌트의 갯수를 줄이면 페이지를 랜더링하는데 필요한 HTTP 요청의 수도 줄어들게 된다. 바로 이것이 페이지 로딩 속도를 높이는 핵심이다.

화면에 있는 컴포넌트의 수를 줄이는 한가지 방법은 페이지의 디자인을 단순하게 만드는 것이다. 하지만 꽤 복잡한 컨텐츠로 구성되어 있는 페이지를 만들 때도 빠른 응답 속도를 보장할 수 있는 방법은 없을까? 이제부터 풍요롭게 디자인된 페이지를 지원하면서도 HTTP 요청을 줄이는 기술을 몇 가지 살펴보자.

묶음 파일(Combined files)이란 모든 스크립트를 하나의 스크립트로 묶고, 그와 비슷하게 모든 CSS를 하나의 스타일시트로 묶어서 HTTP 요청을 요청을 줄이는 방법이다. 묶음 파일은 페이지 마다 필요한 스크립트와 스타일시트가 다를 때 좀 더 도전적인 과제가 될 수 있지만, 이러한 것을 배포 절차 중 하나로 만든다면 응답 시간을 줄일 수 있다.

CSS Sprites는 이미지 요청 수를 줄일 때 선호하는 방법이다. 배경 이미지들을 하나의 단일 이미지로 묶고 background-image와 background-position 속성을 사용해서 이미지의 일부를 보여주는 것이다.

Image map은 여러 개의 이미지 파일을 하나의 이미지로 만든다. 전체 사이즈는 거의 같겠지만, HTTP 요청의 수가 줄어들어 페이지 로딩 속도가 빨라진다. 이미지 맵은 네비게이션 바와 같이 연속적으로 나타나는 이미지 일 때 적합하다. 이미지 맵의 좌표를 정의하는 것은 매우 지루하고 오류를 범하기 쉬울 것이다. 이미지 맵을 사용하여 네비게이션을 하는 것 또한 적용하기 쉽지 않다. 따라서 권장하는 방법은 아니다.

인라인 이미지(inline image)는 data: URL 스키마를 사용하여 이미지 데이타를 실제 페이지에 첨부한다. 이렇게 하면 HTML 문서의 크기가 늘어난다. 인라인 이미지를 (캐싱을 사용하는) 스타일시트에 넣는 방법으로 HTTP 요청을 줄이고 문서 크기가 늘어나는 것을 방지할 수 있다. 하지만 아직 인라인 이미지를 모든 주요 브라우저에서 지원하고 있지는 않다.

페이지에 필요한 HTTP 요청을 줄이는 것부터가 시작이다. 특히 이것은 첫 방문자를 위한 성능 향상에서 가장 중요한 가이드라인이다.  Tenni Theurer의 블로그 글 Browser Cache Usage - Exposed!에 따르면, 매일 여러분의 사이트 방문자 중 40~60%는 캐시가 비어있는 상태에서 찾아온다. 처음 방문하는 유저를 위해 페이지 로딩 속도를 높이는 것은 더 나은 사용자 경험에 있어서 핵심이다.
top


[코딩] 데이터를 요청하지 말고 작업을 요청하라.

모하니?/Coding : 2009.11.12 18:45


어떤 컨트롤러 안에 다음과 같은 코드가 있습니다.

    @RequestMapping("/study/{studyId}/meeting/delete/{meetingId}")
    public String deleteMeeting(@PathVariable int studyId,
            @PathVariable int meetingId) {
        Meeting meeting = meetingService.getById(meetingId);
        meetingService.deleteMeeting(meeting);
        return redirectStudyView(studyId);
    }

안그래도 컨트롤러 코드를 보면 항상 무언가 미적지근한 것이 있었는데, 그 원인을 이제야 알겠네요. 이 글의 제목에 적혀있는 객체지향 원리(?)를 생각해볼때 위와 같은 코드의 문제점은 무엇일까요?

이상하죠.. meeting을 model에 주어 담을 것도 아닌데, 뭐하러 가져와서 다시 service쪽에 넘겨주는 걸까요. 저런 식으로 코딩한 부분이 한 두군데가 아닙니다. 죄다 고쳐야겠어요. 크헉!!!!;;;;;




top

TAG 코딩

[하이버네이트] @BatchSize로 쿼리 갯수 대폭 줄이기

모하니?/Coding : 2009.11.10 01:04


참조:
- http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html
- http://docs.jboss.org/hibernate/stable/annotations/api/org/hibernate/annotations/BatchSize.html

스터디의 회원은 각각 Collection<Study, Integer> 타입의 목록을 가지고 있습니다. 스터디당 참여율이나 신뢰도가 들어있는 콜렉션입니다. 그리고 스터디를 보여줄 때 각 회원들의 참여율과 신뢰도를 보여주도록 되어 있는데, 문제는 쿼리입니다.

Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id=?
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id=?

이런 쿼리가 회원 수*2 만큼 생성됩니다. 한 회원당 attendanceRate와 trustRate를 가지고 있기 떄문이죠. 바로 이런 경우 @BatchSize를 사용하면 쿼리를 대폭 줄일 수 있습니다. 설정은 간단하죠;

    @CollectionOfElements(targetElement = Integer.class)
    @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
    @BatchSize(size=30)
    private Map<Study, Integer> studyAttendanceRates;

    @CollectionOfElements(targetElement = Integer.class)
    @Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
    @BatchSize(size=30)
    private Map<Study, Integer> studyTrustRates;

설정을 한 뒤 쿼리 갯수는 대폭 줄어들게 됩니다. 물론 그만큼 빨라지죠.

Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: select studytrust0_.Member_id as Member1_0_, studytrust0_.element as element0_, studytrust0_.mapkey_id as mapkey3_0_ from Member_studyTrustRates studytrust0_ where studytrust0_.Member_id in (?, ?)
Hibernate: select studyatten0_.Member_id as Member1_0_, studyatten0_.element as element0_, studyatten0_.mapkey_id as mapkey3_0_ from Member_studyAttendanceRates studyatten0_ where studyatten0_.Member_id in (?, ?)

top


[하이버네이트] 컬럼 타입은 어떻게 명시하는게 좋을까?

모하니?/Coding : 2009.10.31 22:30


       @Column(columnDefinition="TEXT")
       private String descr;

이런 방법이 있습니다. 별로 좋은 방법은 아닙니다. postgresql에서는 괜찮지만, HSQL에서는 저 TEXT라는 SQL 타입이  못해서 해당 테이블을 만들지 못할겁니다.

columnDefinition 이 속성 자체가 컬럼 만들 때 사용할 SQL을 입력하는 부분이기 때문에, 각기 다른 밴더 DB에서 못 인식하는 경우도 생길 수 있는거죠.

그래서 타입을 선언하고 싶을 때는 하이버네이트 타입을 선언할 수 있는 @Type을 사용하는 것이 좋겠습니다. 그러면 하이버네이트가 컬럼을 만들 때 @Type에 선언된 하이버네이트 타입을 보고 DB에 적당한 SQL을 이용해서 컬럼을 만들어 줄 것이기 때문이죠.

    @Column
    @Type(type = "text")
    private String descr;

그래서, 이렇게 하는게 좋겠습니다.



top


[봄싹] XP 적용 시나리오 3. 개발하기

모하니?/Coding : 2009.10.29 00:57


본격적으로 개발을 해야하는데, 봄싹에서는 오프라인에서 페어로 작업을 해보기도 했지만, 그렇게 자주 충분히 페어 프로그래밍을 했다고 볼 수는 없습니다. 앞으로도 좀 더 꾸준히 시도를 해봐야 그 효용이나 장단점을 파악할 수 있을 것 같습니다. 개인적으로는 뭔가 대화를 나누면서 코딩할 상대가 있어서 안심이 되긴 합니다. DB update 쿼리가 어떻게 되더라? 이거 무슨 리팩토링이지? 이 메서드 이름 맘에 들어? 여기 중복인데 어떻게 제거하면 좋을지 잘 모르겠네.. 같은 식으로 대화를 나눌 수 상대와 함께라면 좋치 않겠어요?


먼저 개발을 진행하기에 앞서 구현하려는 기능을 한 번도 만들어 본적이 없다면, 어느 정도 자신있게 개발을 진행할만큼의 학습이 필요합니다. 그 과정을 파일럿이고 표현했는데, XP 책에서도 파일럿이라고 헀었는지 잘 모르겠습니다. (뭐라고 했는지 찾아보려고 다시 살펴 봤는데 못 찾아서 그냥 썼습니다.)

그다음 과정은 좀 특이하게 바로 개발을 진행하지 않고, 인수 테스트를 만듭니다. 고객이 해당 작업이 완료 됐다는 것을 확인할 수 있는 모종의 장치를 마련하는 것이죠. 고객이 코드를 볼 수 있다면 아주 행복할텐데, 봄싹은 다행히(?) 고객이 전부 개발자 입니다. 굳이 엑셀로 이쁜 포맷을 만들고, 테스트에서 엑셀 로딩해서, 결과를 엑셀에 다시 찍어주고, 고객은 엑셀에서 수식 비교로 해당 테스트가 잘 됐나 안 됐나 확인하는 귀찮은 일은 할 필요가 없습니다. 그렇치만, 인수 테스트 코드가 고객이 원하는 시나리오를 제대로 표현해주지 못하거나, 고객이 개발자인데도 테스트 코드를 읽기가 난해하다면 테스트를 수정해야겠죠.

그다음은 페어 프로그래밍과 TDD로 해당 작업을 구현하는 일입니다. 페어 프로그래밍은 사실 오프라인에서 만났을 때의 얘기지 주중 저녁이나 회사에서 틈틈히 코딩을 하는 봄싹 개발자에데는 다소 난해한 일입니다. 그래도 메신저등을 통해서 의견은 주고 받을 수 있으니 그것도 페어 프로그래밍으로 치도록 하죠.

그렇게헤서 작업이 끝나면, 담당자 두 명은 자신들이 예상했던 난이도와 시간에 비해 실제로는 난이도가 어땠으며 실제로 소요된 시간은 어느정도인지 기록합니다. 고객은 해당 작업 결과를 본 뒤 간략한 피드백을 줍니다. "담부턴 더 빨리 만들어 주세요" 라던지.. "참 잘했어요" 라던지 ㅋ

top


[봄싹] XP 적용 시나리오 2. 배포 계획하기

모하니?/Coding : 2009.10.29 00:43


봄싹이 언제 배포됐었더라... 기억이 가물가물 합니다. 8월 30일이었나. 이제 두 달을 향해가고 있군요. 다음 배포 일정은 임의로 11월 30일로 잡아두었습니다. 세 달은 지나치게 긴 것 같습니다. 이미 봄싹 개발자들끼리는 새로운 UI를 만끽하고 있는데 봄싹 회원들에게 멋진 UI를 빨리 보여드리지 못하는게 아쉽습니다. 배포 일정과 스코프를 잘못 계획했기 때문입니다. UI 개선만을 1차 유지보수 스코프로 정하고 배포했다면 이미 봄싹 회원들은 멋진 UI를 감상하고 계실텐데 말이죠...


먼저 배포 일정을 잡은 다음, 모든 개발자들의 측정이 완료된 스토리 카드 중에서 해당 일정 안에 개발을 할 수 있을 것으로 보이는 스토리 카드들을 고릅니다. 이 일은 전적으로 고객이 합니다. 고객 생각에 어떤 것이 가장 필요하고 비즈니스에 도움이 되는지 생각해서 고르면 되겠죠. 일단 최대한 빨리 중요한 것부터 서비스하고 싶다면 다소 어렵고 일정이 긴 스토리 하나만 고를 수 있겠고, 중요한 건 나중에 공개하고 일단은 기반이 되는 소소한 것들 부터 서비스하고 싶다면, 쉽고, 개발 일정이 짧게 걸릴 것으로 측정된 것들을 고르면 되겠습니다.

그런다음, 하나의 스토리를 구현하는데 필요한 세부 작업들을 개발자들과 논의합니다.

"발표 도메인 객체를 추가해야겠습니다."
"발표와 모임 객체에 연관 관계를 설정해야 겠어요."
"발표에 댓글/첨부자료/발표자 정보가 필요하겠군요."
"새로운 모임을 추가할 때 아예 발표 정보도 추가할 수 있도록 하죠."
"스프링 웹 플로우 학습이 필요할 것 같습니다."
"발표정보 추가할 때 발표자를 선택하는 부분에서는 Ajax를 도입할 수도 있겠네요."
...

이런식으로 하나의 스토리를 구현하는데 필요한 세부적인 작업목록들을 만들어 나갑니다. 이때, 개발자들이 의견을 많이 주어야 하며, 작업 목록 작성은 고객이 담당합니다.

고객이 작업 목록 하나를 작성할 때 마다, 스토리를 등록했을 때 처럼, 평가하는 과정을 거칩니다. 해당 작업은 덩어리가 너무 크다거나, 좀 더 명시적으로 수정해 달라거나 의견을 제시하면서 투표를 할 수 있죠. 혹은 고객이 명시한 작업을 코딩할 수 있겠다는 판단이 들면, 난이도, 걸리는 시간, 같이 작업하고 싶은 사람을 적어서 측정해줍니다.

고객은 가장 낮은 난이도를 제시한 개발자, 가장 낮은 시간을 제시한 개발자, 가장 많은 지명도를 가진 개발자 등의 정보 통계를 보고 해당 작업의 적입자를 찾아서 작업 담당자와 그 파트너를 지정해줍니다. 이렇게해서 하나의 작업에 두 명의 담당자를 지정해 주는겁니다.

그럼 이제 그 두명의 담당자는 서로 의견을 나눠가며 개발을 진행하면 되겠죠!

다음은 개발 과정에 대해 생각해보죠.
top


[봄싹] XP 적용 시나리오 1. 스토리 만들기

모하니?/Coding : 2009.10.29 00:21


봄싹 프로젝트 개발이 지나치게 자유롭다 보니 프로젝트 스코프도 애매해지기 시작했고, 현재 누가 어디를 얼만큼 개발했는지 파악하기가 힘들어졌습니다. 나름대로, 이슈트래커, CI 환경, VCS까지 갖출 건 다 갖추고 진행하고 있지만 그래도 뭔가 좀 부족한 감이 없지 않습니다. 그래서 XP 책에서 읽은 내용을 토대로 봄싹 나름대로의 개발 프로세스를 정리해볼까 합니다.

물론, XP installed 책에 나와있는 그대로 할 필요도 없고, 그 책도 일종의 권고사항이지 반드시 따라야 하는 건 아니것 같기 때문에 제 맘대로 봄싹에 필요하면서도 재미있게 개발을 진행할 수 있는 방법을 궁리해봤습니다. xp 책은 그런면에서 좋은 아이디어를 떠올리는데 아주 좋더군요!


먼저, 현재 이슈트래커나, 구글 그룹에 올리고 있는 할 일을 스토리로 정리하는 방법을 생각해봤습니다. 누군가가 고객의 입장에서 스토리를 하나 만듭니다.

"모임에서 있었던 발표 정보를 별도로 관리하면 좋겠다."

이런 카드를 만드는 순간, 그 사람은 고객이 됩니다. 그리고 해당 스토리를 수정/삭제/세부 스토리 등록 등을 하는 권한이 생기죠. 그리고 이 카드가 생기는 순간 모든 개발자에게 메시지가 갑니다. 평가해 달라고..

그럼 일부의 개발자들은 해당 스토리가 너무 애매하다고 더 구체적으로 설명해 달라고 "고치자"를 클릭하며 어떻게 고쳐달라며 '의견'을 제시합니다.

또 다른 일부의 개발자들은 해당 스토리가 너무 방대하다면 좀 더 세부적으로 쪼개 달라며 "나누자"를 클릭하고 어떻게 나누면 좋겠는지 '의견'을 제시합니다.

또 다른 일부의 개발자들은 현재 스토리를 개발할 수 있다는 판단하게 '얼마나 걸릴지', '난이도가 어느정도인지', '누구와 함께하고 싶은지' "측정"을 합니다.

모든 개발자들의 측정이 이루어지기 전까지 사용자는 개발자들의 의견을 반영하여 계속해서 스토리를 수정하거나, 스토리의 하위 스토리를 등록하게 되고, 개발자는 스토리가 변경되거나, 새로운 스터디가 추가될 때마다 계속해서 측정 또는 투표를 할 수 있습니다.

이런식으로 스토리를 정제해 나가는 겁니다. 재밌지 않을까요? JIRA를 어떻게 잘 설정해서 쓰면 이렇게 할 수 있을것도 같긴 한데.. 좀.. 복잡해 보이는 UI가 위 시나리오에 적당해 보이지는 않습니다. 적당한 툴이 있으면 좋겠는데 없으면 봄싹 이슈트래커도 나중에 만들어야겠네요.

다음은 이렇게 정리된 스토리들을 가지고 배포 계획을 세우는 방법에 대해 생각해보겠습니다.

top


[봄싹] 모임 추가 시나리오 - web flow (구현)

모하니?/Coding : 2009.10.23 17:57


<?xml version="1.0" encoding="UTF-8"?>
<flow xmlns="http://www.springframework.org/schema/webflow"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <secured attributes="ROLE_MEMBER" />
   
    <input name="studyId" required="true" type="java.lang.Integer" />
   
    <on-start>
        <evaluate expression="meetingService.createMeeting(studyId)" result="flowScope.meeting" />
    </on-start>
   
    <view-state id="addMeetingForm" model="meeting" view="add.jsp">
        <binder>
            <binding property="openDate" converter="shortDate" required="true" />
            <binding property="closeDate" converter="shortDate" required="true" />
            <binding property="openTime" converter="shortTime" required="true" />
            <binding property="closeTime" converter="shortTime" required="true" />
            <binding property="title" required="true" />
            <binding property="maximum" required="true" />
            <binding property="location" required="true" />
            <binding property="contents" required="true" />
        </binder>
        <transition on="proceed" to="addPresentationForm" />
        <transition on="submit" to="confrimMeetingDetail" />
        <transition on="cancel" to="cancel" bind="false" validate="false" />
    </view-state>
   
    <view-state id="addPresentationForm" model="presentation" view="presentation/add.jsp">
        <binder>
            <binding property="key" required="true" />
            <binding property="title" required="true"/>
            <binding property="topic" required="true"/>
            <binding property="summary" required="true"/>
            <binding property="presenter" converter="memberConverter"/>
        </binder>
        <on-render>
            <evaluate expression="meetingService.createPresentation(meeting)" result="viewScope.presentation"/>
        </on-render>
        <transition on="proceed" to="presentationList" history="discard">
            <evaluate expression="meetingService.addPresentation(meeting, presentation)"/>
        </transition>
        <transition on="cancel" to="cancel" bind="false" validate="false" />
    </view-state>

    <view-state id="presentationList" view="presentation/list.jsp">
        <transition on="delete" to="presentationList">
            <set name="requestScope.presentationKey" value="requestParameters.presentationKey" />
            <evaluate expression="meetingService.deletePresentation(meeting, presentationKey)" />
        </transition>
        <transition on="new" to="addPresentationForm" />
        <transition on="submit" to="confrimMeetingDetail" />
        <transition on="cancel" to="cancel" bind="false" validate="false" />
    </view-state>

    <view-state id="confrimMeetingDetail" view="confirmMeeting.jsp">
        <transition on="submit" to="submit" />
        <transition on="cancel" to="cancel" bind="false" validate="false" />
    </view-state>
   
    <end-state id="meetingEnd" view="externalRedirect:contextRelative:/study/view/${studyId}.do" />
   
    <end-state id="submit" commit="true" parent="#meetingEnd">
        <on-entry>
            <evaluate expression="meetingService.addMeeting(studyId, meeting)"/>       
        </on-entry>
    </end-state>

    <end-state id="cancel" parent="#meetingEnd" />

</flow>

일단, 이 플로우로 진입하면, addMeetingForm 뷰로 이동, 여기서 나온 transition에 따라 contirmMeetingDetail로 바로 가거나, addPresentationForm으로 이동.

addPresentationForm에서 presentationList로 이동하고, 여기서는 back할 수 없도록 history를 discard로 설정함.

대략 80 줄 정도의 XML 설정으로 아침에 구상한 플로우를 구현했습니다. 이 시나리오를 구현하는데 필요한 자바 코드는 서비스 메서드 몇 개 정도. 컨트롤러 코드는 하나도 없습니다. 만약 웹 플로우 없이, 스프링 MVC만을 이용해서 비슷한 플로우를 구현했다면 훨씬 복잡하고 코드도 길었을 텐데 다행입니다. 웹 플로우 사용법도 생각보다 간편하고 쉬웠던 것 같네요.

이제는 웹 플로우 테스트와 <persistent-context />에 대해 좀 알아봐야겠습니다.

top


[봄싹] 모임 추가 시나리오 - web flow

모하니?/Coding : 2009.10.23 11:49


모임을 추가할 때, 해당 모임에 있을 발표에 관한 정보도 추가하도록 웹 플로우를 구상했습니다. 단순 폼 처리보다 훨씬 복잡해질 수 있어서, 스프링 웹 플로우를 도입하기로 했죠. 그런데 막상 기본적인 사용법을 보고나니, 시나리오가 정해지지 않으면 개발을 진행하지 못하겠더군요. 그래서 구상에 들어갔습니다.

모임을 추가하고나서, 발표를 추가해야지.. 발표가 하나만 있는건 아니자나.. 그럼 한 화면에서 여러개를 추가할까?? 에이.. 뭔가 좀 거시기 하네.. 그럼 발표를 하나 추가하고, 발표 목록을 본 다음에 다시 하나 더 추가하고 이렇게 할까?


그래서 그린게 위와 같은 그림입니다. 그런데, 발표자가 확정되지 않았거나, 특정 발표 없이 진행되는 모임이라면 어찌할까? 그때도 무조건 발표 입력 폼을 들려야 하나.. 파란 부분을 서브 플로우로 묶었을 때, MeetingDetial에서 PresentationList로 back 할 수 있을까?  흠 뭐. 일단은 된다는 가정하에.

발표 정보가 한 개 있다면, MF -> PF -> PL -> MD
발표 정보가 두 개 있다면, MF -> PF -> PL -> PF -> PL -> MD
발표 정보가 없다면, MF -> PF -> PL -> MD

이상해.. @_@ 발표정보가 없는데 발표 폼이랑 발표 리스트를 들려야 하다니... 만약에 발표 폼 바인딩에 validation을 해야 하는 상황이면, 발표가 한 개있을 떄랑 발표가 없을 떄를 어떻게 구분한담... @_@ 안돼 안돼..

흠.. 그럼 PresentationList를 먼저 보여주면 어찌 될까나? 그려보지뭐..


발표 정보가 한 개 있다면, MF -> PL -> PF -> PL -> MD
발표 정보가 두 개 있다면, MF -> PL -> PF -> PL -> PF -> PL -> MD
발표 정보가 없다면, MF -> PL -> MD

아... 아닌데, 발표도 없는데 PL을 들릴 필요는 없자나..
그리고 발표를 추가할껀데, PL 부터 들리는 것도 불편하고.
이건 뭐.. 이전 보다 오히려 들려야 하는 폼 수가 늘어났자나.. @_@;;

0. 다시 PF가 진입점이 되도록 수정.
1. MF에서 MD로 바로 갈 수 있는 transition 추가.
2. MD에서 PL로 진입할 수 있는지 확인.(진입할 수 없다면, 서브 플로우 포기)


보자...

발표가 한 개 일 때, MF -> PF -> PL -> MD
발표가 두 개 일 때, MF -> PF -> PL -> PF -> PL -> MD
발표가 없을 때, MF -> MD

오퀘.. 이렇게 가야겠군!!!

top


[Spring Wev Flow(2.0.8)] SecurityFlowExecutionListener 패치 for Spring Security 3.X

모하니?/Coding : 2009.10.22 17:35


스프링 시큐리티 3.0 RC1이 나온지가 언젠데 스프링 웹 플로우는 아직도 시큐리티 2점대 기준이더군요. 스프링 웹 플로우 때문에 시큐리티 버전을 낮출수도 없는 노릇이고, 안 돌아가는 클래스 소스를 가져다 스프링 시큐리티 3.X에서 돌아가도록 수정했습니다.

웹 플로우 2.X는 아직 자바 5 기능을 도입하지 않았더군요. 스프링 플젝만 자바 5 기준으로 변경한건지.. 흠.. 그래서 고치는 김에 자바5 Generic을 도입해서 타입 세이프티를 보장하게 코드를 아주 약간만 손 봤습니다.

필요하신 분은 쓰세요~

/*
 * Copyright 2004-2009 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package springsprout.common.webflow;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.access.vote.AbstractAccessDecisionManager;
import org.springframework.security.access.vote.AffirmativeBased;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.access.vote.UnanimousBased;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.webflow.definition.FlowDefinition;
import org.springframework.webflow.definition.StateDefinition;
import org.springframework.webflow.definition.TransitionDefinition;
import org.springframework.webflow.execution.EnterStateVetoException;
import org.springframework.webflow.execution.FlowExecutionListenerAdapter;
import org.springframework.webflow.execution.RequestContext;
import org.springframework.webflow.security.SecurityRule;

/**
 * Flow security integration with Spring Security
 *
 * @author Scott Andrews
 * @author Keesun Baik(Whiteship)
 */
public class SecurityFlowExecutionListener extends FlowExecutionListenerAdapter {

    private AccessDecisionManager accessDecisionManager;

    /**
     * Get the access decision manager that makes flow authorization decisions.
     * @return the decision manager
     */
    public AccessDecisionManager getAccessDecisionManager() {
        return accessDecisionManager;
    }

    /**
     * Set the access decision manager that makes flow authorization decisions.
     * @param accessDecisionManager the decision manager to user
     */
    public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) {
        this.accessDecisionManager = accessDecisionManager;
    }

    public void sessionCreating(RequestContext context, FlowDefinition definition) {
        SecurityRule rule = (SecurityRule) definition.getAttributes().get(SecurityRule.SECURITY_ATTRIBUTE_NAME);
        if (rule != null) {
            decide(rule, definition);
        }
    }

    public void stateEntering(RequestContext context, StateDefinition state) throws EnterStateVetoException {
        SecurityRule rule = (SecurityRule) state.getAttributes().get(SecurityRule.SECURITY_ATTRIBUTE_NAME);
        if (rule != null) {
            decide(rule, state);
        }
    }

    public void transitionExecuting(RequestContext context, TransitionDefinition transition) {
        SecurityRule rule = (SecurityRule) transition.getAttributes().get(SecurityRule.SECURITY_ATTRIBUTE_NAME);
        if (rule != null) {
            decide(rule, transition);
        }
    }

    /**
     * Performs a Spring Security authorization decision. Decision will use the provided AccessDecisionManager. If no
     * AccessDecisionManager is provided a role based manager will be selected according to the comparison type of the
     * rule.
     * @param rule the rule to base the decision
     * @param object the execution listener phase
     */
    protected void decide(SecurityRule rule, Object object) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        List<ConfigAttribute> configAttrs = getConfigAttributes(rule);
        if (accessDecisionManager != null) {
            accessDecisionManager.decide(authentication, object, configAttrs);
        } else {
            AbstractAccessDecisionManager abstractAccessDecisionManager;
            List<AccessDecisionVoter> voters = new ArrayList<AccessDecisionVoter>();
            voters.add(new RoleVoter());
            if (rule.getComparisonType() == SecurityRule.COMPARISON_ANY) {
                abstractAccessDecisionManager = new AffirmativeBased();
            } else if (rule.getComparisonType() == SecurityRule.COMPARISON_ALL) {
                abstractAccessDecisionManager = new UnanimousBased();
            } else {
                throw new IllegalStateException("Unknown SecurityRule match type: " + rule.getComparisonType());
            }
            abstractAccessDecisionManager.setDecisionVoters(voters);
            abstractAccessDecisionManager.decide(authentication, object, configAttrs);
        }
    }

    /**
     * Convert SecurityRule into a form understood by Spring Security
     * @param rule the rule to convert
     * @return list of ConfigAttributes for Spring Security
     */
    protected List<ConfigAttribute> getConfigAttributes(SecurityRule rule) {
        List<ConfigAttribute> configAttributes = new ArrayList<ConfigAttribute>();
        Iterator<String> attributeIt = rule.getAttributes().iterator();
        while (attributeIt.hasNext()) {
            configAttributes.add(new SecurityConfig(attributeIt.next()));
        }
        return configAttributes;
    }
}

ps: 스프링 소스 직원님들.. 웹 플로우도 빨랑 3.0으로 올려줘요. 한 달에 이슈 한 개 처리하는건 너무 심한거 아니삼..??
top


[코드값] 애플리케이션에 DB id값이...

모하니?/Coding : 2009.10.21 14:57


c.add(Restrictions.eq("itemStatus", 5));

이게 뭔지 아시겠나요.. 5는 DB에 있는 id 값입니다.

이 값은 시스템 마다 달라질텐데... 저렇게 애플리케이션 코드에...
테이블의 특정 id값을 가지고 조회를 해도 되는건지....
뭔가.. 좀.. 아닌 것 같다는 느낌이 강하게 듭니다.

물론, 코드 데이터가 기본 데이터성이니까...
모든 시스템 마다 기본 데이터 셋을 정해서 그걸 올려놓고 쓰면 상관은 없겠지만..
그래도 모르는거죠.

기본 데이터로 올리지 않고 나중에 추가한 코드값을 가지고 DB에서 뭔가 조회를 해야 한다...
그럴 때도 위와 같은 코드가 생길텐데..

어찌해야 할지 몰라서 그냥 저렇게 놔뒀습니다. @_@;;
아.. 괴롭네요. 괴로워..
top


[봄싹] 메일 서비스를 알림 서비스로 통합하기

모하니?/Coding : 2009.10.20 21:23




알림 서비스는 현재 구글 토크와 트위터에 스터디와 모임 소식을 전달해주고 있습니다. 그런데 이전에 만든 메일 서비스가 이 것과 굉장히 비슷한 구조였습니다.


녹색은 스프링에서 제공하는 라이브러리이고, 파란색 계열이 봄싹 코드입니다. 보시면 알림 서비스와 거의 같은 구조지만, 사용하는 클래스가 조금 다르고, SendMailService가 구현하는 인터페이스가 없다는 것만 다를 뿐입니다.

이 두 개의 서비스를 사용하는 NotificationAspect를 보면 통합의 필요성을 더 확실하게 느낄 수 있습니다.

@Aspect
public class NotificationAspect {
  
    @Autowired SendMailService sendMailService;
    @Autowired JabberService messangerService;
    @Autowired TwitterService twitterService;
    @Autowired MemberService memberService;
...
    @AfterReturning(pointcut = "addStudyPointcut() && args(study)", argNames="study")
    public void sendMailAfterAddStudy(Study study){
        sendMailService.sendMail(new StudyMail(study, StudyStatus.OPEN, memberService.getMemberList()));
        StudyMessage msg = new StudyMessage(study, StudyStatus.OPEN, memberService.getMemberList());
        messangerService.sendMessage(msg);
        twitterService.sendMessage(msg);
    }
...

    @AfterReturning(pointcut = "addMeetingPointcut() && args(study, meeting)", argNames="study, meeting")
    public void sendMailAfterAddMeeting(Study study, Meeting meeting){
        sendMailService.sendMail(new MeetingMail(study, meeting, MeetingStatus.OPEN));
        MeetingMessage msg = new MeetingMessage(study, meeting, MeetingStatus.OPEN);
        messangerService.sendMessage(msg);
        twitterService.sendMessage(msg);
    }
}

sendMailService도 다른 알림 서비스와 같은 인터페이스를 구현하고, SpringSproutMessage 타입의 객체를 받아서 사용할 수 있다면. 어떤 일이 벌어질지 보이시나요?

저에게는 콜렉션이 보이고, 애스팩트의 여러 어브다이스에서 연달아 메서드를 호출해주는 구조의 중복을 제거할 수 있어 보입니다.

그래서 통합했습니다.


메시지쪽이 좀 까다로웠지만, 상속을 이용해서 나름 열심히 작업했습니다.

결과는 어떨까요?
1. 메시지 작성이 간편해졌습니다.
2. NotificationAspect가 다이어트를 했습니다.

원했던 성과를 거뒀습니다.

    <util:list id="notificationServices">
        <value>sendMailService</value>
        <value>jabberService</value>
        <value>twitterService</value>
    </util:list>

이렇게 빈 설정을 해 놓고..

@Aspect
public class NotificationAspect {
  
    @Autowired List<NotificationService> notificationServices;
    @Autowired MemberService memberService;

...

    @AfterReturning(pointcut = "addStudyPointcut() && args(study)", argNames="study")
    public void sendMailAfterAddStudy(Study study){
        sendMsg(new StudyMailMessage(study, StudyStatus.OPEN, memberService.getMemberList()));
    }

    @AfterReturning(pointcut = "addMeetingPointcut() && args(study, meeting)", argNames="study, meeting")
    public void sendMailAfterAddMeeting(Study study, Meeting meeting){
        sendMsg(new MeetingMailMessage(study, meeting, MeetingStatus.OPEN));
    }

    private void sendMsg(SpringSproutMessage msg) {
        for(NotificationService service : notificationServices)
            service.sendMessage(msg);
    }

...
}

NotificationAspect 코드를 이렇게 리팩토링 했습니다.

캬~~~ 엄청난 대규모 리팩토링이었는데 생각했던대로 되서 다행입니다. 이 작업을 진행하는 내내 테스트가 충분하지 않아서 굉장히 불안했었는데, 그래도 최소한의 테스트가 마지막 검증을 하는데 도움이 됐습니다. 역시.. 테스트는 있어야 합니다. 매우 필요한 존재에요;

다음 과제는 모임 등록시 SWF 적용하기!!

top


[리팩토링] 메일을 메시지로 통합하기

모하니?/Coding : 2009.10.20 00:11


이메일 서비스를 알림 서비스쪽으로 통합중입니다. 서비스 클래스를 통합하는 건 간단했습니다.

@Service
public class SendMailService implements NotificationService{
   
    @Autowired JavaMailSender mailSender;

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

}

NotificationService를 구현하기만 하면 끝이죠. 그런데 문제는 이제부터 시작이었습니다. SpringSproutMessage.. 이 녀석을 받아서 메시지를 보내도록 해야하는데, SendMailService는 기존에 SpringSproutMail을 상속받아 만든 Mail 클래스들을 받아서 사용하고 있었습니다.

SSMessage와 SSMail은 상당부분 비슷한 속성과 메서드가 있으면서도 메시지 내용 상으로는 다소 차이가 있었습니다. 구글 토크나 트위터에는 간단한 메시지와 링크주소만 보내지만, 이메일로는 좀 더 구체적인 정보들까지 HTML 형태로 보내기 때문이죠.

public class SpringSproutMail extends SpringSproutMessage {
   
    public static final String SUBJECT_PREFIX = "[봄싹]";
    public static final String SENDER_MAIL = "s2cmailer@gmail.com";
   
    private String subject;
    private String content;
    private String[] recievers;
    private String from;
    private boolean isHTML;
   
    public SpringSproutMail() {
        this.from = SENDER_MAIL;
        this.isHTML = true;
    }
   
  ...
   
    protected void setTo(Member member) {
        String[] tos = new String[1];
        tos[0] = member.getEmail();
        setRecievers(tos);
    }
   
    protected void setTo(Collection<Member> members) {
        String[] recievers = new String[members.size()];
        Iterator<Member> memberIterator = members.iterator();
        for(int i = 0 ; i < recievers.length ; i++){
            recievers[i] = memberIterator.next().getEmail();
            tos.add(recievers[i]);
        }
        setRecievers(recievers);
    }
   
}

일단은 Mail 쪽의 최상위 클래스인 SpringSproutMail이 SpringSproutMessage를 상속받도록 수정했습니다. 그랬더니 아무 문제없이 잘 돌아가더군요. 이상태에서 일단 정지입니다. 돌아가게 만든 상태에서 어떻게 리팩토링 해야할지 고민좀 해야겠습니다.
top


[JUnit] @Rule TemporaryFolder 사용하기

모하니?/Coding : 2009.10.16 16:26


public class DBUnitSupportTest {
   
    DBUnitSupport dbUnitSupport;

    @Rule public TemporaryFolder folder = new TemporaryFolder();
    @Autowired DataSource dataSource;
    File tempFile;
   
    @Before
    public void setUp() throws Exception{
        dbUnitSupport = new DBUnitSupport(dataSource);
    }
   
    @Test
    public void exportXls() throws Exception {
        tempFile = folder.newFile("temp.xls");
        dbUnitSupport.setExportedFile(tempFile);
        assertThat(tempFile.length(), is(0L));
        dbUnitSupport.exportXlsFrom("code", "item", "users", "role", "users_role");
        assertThat(tempFile.length(), not(0L));
    }
   
    @After
    public void after(){
        assertFalse(tempFile.exists());
    }

흠.. 좋군요!! 파일이 생겨나는 테스트를 하고 나서 매번 수동이든 코드로든 지워야 하는 번거로움이 없어졌습니다.

사실 파란색 코드는 지워도 그만인데, 아직은 TemporaryFolder Rule이 어떻게 동작하는지 확실하게 파악된 상태가 아니여서 남겨뒀습니다.

파일이 delete() 되지 않을 경우에 대비해서 말이죠. 사실 이 아래 글에 작성한 코드에는 치명적인 결함이 있었는데, 그 사실을 이번 테스트를 작성하다가 알게됐습니다. @_@;;

테스트 작성이 여러모로 도움을 주는군요.

참조: http://www.catosplace.net/blogs/personal/?p=116
top


[DBUnit] 테이블 데이터를 엑셀로 export

모하니?/Coding : 2009.10.16 12:48


테스트
    @Test
    public void exportXls() throws Exception {
        dbUnitSupport.exportXlsFrom("code", "item", "users", "role", "users_role");
        assertTrue(new File(DBUnitSupport.EXPORTED_XLS_FILE).exists());
    }

구현
    protected void exportXlsFrom(String... tableNames) throws Exception {
        IDataSet dataSet = getConnection().createDataSet(tableNames);
        XlsDataSet.write(dataSet, new FileOutputStream(new File(EXPORTED_XLS_FILE)));
    }

흠.. 간단하네요. 간단 간단..

그런데. 테스트 할 때 생기는 파일들은 수동으로 지우기도 귀찮고.. 버전관리에 들어가면 더더욱 안 될 듯하고..
JUnit을 4.7로 올리고, @Rule 이라는 걸 써봐야겠습니다.
top


[기초 데이터] 백업/복구 방안

모하니?/Coding : 2009.10.15 21:57


애플리케이션 동작에 필요한 최소한의 기본 데이터들이 있습니다. 이것들을 어떻게 관리해야 백업/복구가 쉬울지 고민입니다. 일단, 기초 데이터를 관리한다는 것 자체는 괜찮은 일인 것 같습니다. 기초 데이터가 매번 필요하긴 한데, 매번 애플리케이션을 새로 설치할 때 마다 입력해야 하는 수고를 덜 수 있으니까 말이죠. 그럼 어떻게 관리해야 할까요.. @_@

고민이 세부적으로 나뉩니다.

1. 우선, 기초 데이터를 SQL, Excel, XML 중 어떤 형태로 관리할 것인지.
2. 그 기초 데이터를 메이븐 빌드로 자동으로 넣을지, 별도의 자바 애플리케이션을 돌려서 넣을지, 사용자도 할 수 있도록 웹 애플리케이션에 기능을 만들어 넣어야 할지.
3. 매번 서버를 켤 때마다 기존의 기초 데이터로 운영/테스트 중인 데이터를 갈아 엎을지, 현재 운영/테스트 중인 데이터를 백업 받아와서 그것을 새로운 기초 데이터로 삼을지, 수동으로 관리할지.

일단 현재로써는 기초데이터가 자주 바뀔 가능성이 있고, 정확하지 않을 가능성이 높기 때문에 수동으로 관리해야 겠습니다. 수동으로 관리할 때는 Excel을 사용해서 일관적으로 바꾸는게 편하기 때문에 파일 형태로 Excel로 가져가는 것이 좋겠습니다. Excel로 데이터를 가져오고 넣을 때는 DBUnit을 활용하면 될 것 같습니다. 일단은 수동으로 관리자만 별도의 애프리케이션을 돌려서 넣을 수 있게 만들고, 그 일이 귀찮을 정도로 잦아 진다면 메이븐 빌드로 DBUnit 플러긴(당근 있겠죠?)을 이용해서 돌리면 될 듯 하군요.

그래서 내일의 회사일은
1. DBUnit으로 현재 DB의 데이터를 Excel로 뽑아보기.
2. 기초 데이터 마련하기.
3. 서버와 로컬 DB에 반영하기.



top


[봄싹 버그]] JSON 뷰와 하이버가 가져온 Proxy 객체

모하니?/Coding : 2009.10.15 18:27


JSON뷰랑 하이버 Proxy 객체가 만나면 JSON 뷰를 만들다 에러가 납니다.

하이버 객체가 Lazy 로딩을 할 수 없는 지점에서 Proxy 객체를 통해 collection에 접근해서 JSON 뷰로 만들 수 없는 데이터에 접근하여 발생하는 에러로 추측하고 있습니다. 에러가 좀 깔끔하게 떨어지면 해결책이나 원인을 찾기도 쉬울텐데.. 이건 뭐.. StackOverFlow 입니다. Q&A 게시판이 아니라. 정말 그 에러입니다.

이런 일이 발생한 대표적인 시나리오는 현재까지 세 군대 정도 됩니다.
- 로그인
- 출석체크
- 낙서장

이중에서 출석체크와 낙서장은 제가 해결했는데 그 방법이 비슷하지만 살짝 다릅니다.

먼저, 출첵의 경우 proxy객체를 직렬화 하는데, 필요 없는 객체를 JSON 뷰로 넘기고 있었습니다. @SessionAttribute에 등록한 객체들이 Model model 객체에 기본으로 들어가 있었고, 그 객체들(study, member, meeting)을 JSON 뷰로 직렬화 하다가 에러가 났습니다. 그래서 Model model을 깨끗하게 비워버리고 JSON 뷰로 넘겨주도록 헬퍼를 만들어 사용했습니다.

    @RequestMapping("/study/{studyId}/meeting/{meetingId}/confirm/{attendanceId}")
    public ModelAndView confirmMember(Model model, @PathVariable int studyId,
            @PathVariable int meetingId, @PathVariable int attendanceId,
            HttpSession session) {
        service.confirmAttendanceById(attendanceId);
        return JsonHelper.jsonViewWithCleanMap(model);
    }

두 번째 낙서장은 제가 맡은 부분은 아닌데, 계속 눈에 걸려서... 암튼 보니까 필요한건 Grafitty의 comments 뿐인데, Graffiy의 작성자(Member)를 포함한 모든 속성들을 다 가져오더군요. 그래서 JSON 뷰에서는 또 타고 타고 들어가다가 접근 못하는 부분(아마도 Member가 들고 있는 Set<Role> 타입의 프로퍼티)에서 JSON 뷰를 만들다 직렬화 에러를 냈을 겁니다. 이번에는 DAO에서 하이버네이트의 Projection을 이용해서 필요한 것만 가져오도록 쿼리를 수정해서 처리했습니다.

    @SuppressWarnings("unchecked")
    public List<Graffiti> getByWriteDate(Date writeDate) throws DataAccessException {
        Criteria c = getCurrentSession().createCriteria(Graffiti.class)
            .add(Expression.ge("writeDate", writeDate))
            .addOrder(Order.asc("writeDate"))
            .setProjection(Property.forName("contents"));
        return c.list();
    }

    @SuppressWarnings("unchecked")
    public List<Graffiti> getRecent10Contents() {
        Criteria c = getCurrentSession().createCriteria(Graffiti.class)
            .setMaxResults(10)
            .addOrder(Order.asc("writeDate"))
            .setProjection(Property.forName("contents"));
        return c.list();
    }

이 방법들은 완전한 대책이 아니라, 적당히 필요한 데이터만 간추리다보니 해결이 된겁니다. 땜빵이라고 하기도 좀 뭐하지만.. 해결책이라고 하기도 좀 뭐하지요.

좀 더 궁극적인 해결책을 생각해 봤는데;;

OSIV Fileter가 MappingJacksonJsonView와 뭔가 잘 안 맞는것 같습니다. JSON이 빌드할 때도 트랜잭션 경계 안에 들어있는 상태라면 하이버 프록시 객체에서 타고 타고 타고 들어갈 수 있는건데... 그게 안 되서 에러가 나는 거겠죠?? 결국은 AOP로 MappingJacksonJsonView의 특정 메서드를 실행하기 전에 트랜잭션을 열고.. 작업을 끝낸다음 트랜잭션을 닫는 작업을 해줘야 하는거 아닌지.. 고민입니다.

아이고;; 번역 헀어야 하는데.. 봄싹에 손을 대버리다니... 큰일이네... 큰일이야.. 에휴... 봄싹 중독인가. @_@

top


[권한] 3단 구조

모하니?/Coding : 2009.10.14 20:11


봄싹은 권한 관리는 3단계로 설계했습니다.

사용자 - 역할 - 권한
Member *---->1 Role *---->1 Right

현재 봄싹의 권한 관리 구조와 Role을 주로 사용하고 있습니다. 따라서 *와 **등을 이용해서 여러 URL을 큰 뭉탱이로 ROLE_ADMIN, ROLE_MEMBER로 구분하고 있죠. 굉장히 Coarse-grained 한 설정이죠. 이런 상태라면 설정하기는 간편하지만, 만약에...

"일반 회원 중에서 관리자는 아니지만 스터디를 관리할 수 있었으면 좋겠다."

이런 시나리오를 만족시키려면 다음과 해야합니다.

1. StudyManager라는 Role을 만듭니다.
2. 해당 사용자에게 StudyManager Role을 추가해줍니다.
3. URL 설정 및 메서드 보안 설정을 찾아다니며 hasRole(Role_StudyManager)를 추가해줘야 합니다.

만약에 URL 설정이 여러줄이고 메서드 보안이 여러줄이라면 어떻게 될까요??
이런 상태에서 RIght라는 것은 의미가 있기나 할까요?

그럼, URL 설정을 Right 기반으로 바꿨다고 가정하겠습니다. 아마 지금보다 더 세부적으로 URL 권한을 설정할테니 갈아타는데 손이 좀 갈 것으로 예상됩니다. Role 기반 설정에 비하면 fine-grained 합니다.

전부 Role 기반으로 바꿨다는 가정하에 다시 한 번 위의 시나리오를 적용하는 과정을 상상해 보겠습니다.

1. StudyManager라는 Role을 만들고
2. 해당 Role에 Add_Study, Update_Study, Delete_Study, End_Study, Start_Study 등 Study와 관련된 모든 권한을 추가해둡니다.
3. 해당 사용자에게 StudyManager 권한을 줍니다.

StudyManager라는 Role을 만드는 과정이 다소 귀찮을 순 있겠지만, A라는 사용자 말고 B 사용자에게 위와같은 시나리오를 적용해야 하는 상황이 온다면 어떨까요? 1, 2번은 필요없고 3번만 하면 됩니다.

"스터디 관리자여도 스터디 삭제는 못하게 하고 싶다. 그건 오직 관리자만 할 수 있게해야지"

이런 시나리오를 적용해야 한다면 Role 기반의 설정을 사용할때는 분명 study/34/delete.do URL이나 deleteStudy(study) 메서드에 hasRole(Role_StudyManager, Role_Admin)이라고 되어있을텐데, 저걸.. 다시 hasRole(Role_ADMIN)으로 바꿔줘야 할 겁니다.

그런데 Right기반으로 설정되어 있는 경우 hasRole(Right_Delete_Study)라고 설정되어 있을것이기 때문에 건드릴건 아무것도 없고, 단지 Role_StudyManager에서 Right_Delete_Study 만 빼주면 됩니다.

앗;; 전 이만 저녁먹으러...
top


[자바스크립트] confirm() 주의할 것

모하니?/Coding : 2009.10.13 13:04


<%@ tag pageEncoding="utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ attribute name="url" required="true" %>

<a id="btn_stop" href="<c:url value="${url}"/>"><img src="<c:url value="/images/study/stop.png"/>"/></a>

<script type="text/javascript">
$(document).ready( function(){
    $("#btn_stop").click( function() {
        if(confirm("종료 하시겠습니까?")) {
            $(this).parent().click();
        }
    });
});
</script>

분명히 confirm 팝업에서 "취소"를 눌렀는데도 그냥 진행이 되버리더군요. @_@;; 왜 이러나.. 싶어서 봄싹에 올렸더니 역시.. 성윤군이 원인과 해결책까지 제공! 캬~~ 어서 봄싹에 StackOverFlow 짭퉁 NullPE(NullPointerException) 메뉴를 추가해야 할텐데 말이죠.

원인은 간단하더군요. a 링크를 클릭할 때 이벤트를 등록했으니... 이벤트 처리하고 나서 링크 클릭한거 처리하느라 그렇게 된거더군요.

해결책도 역시 초간단...

$(document).ready( function(){
    $("#btn_stop").click( function() {
        if(confirm("종료 하시겠습니까?")) {
            $(this).parent().click();
        } else {
            return false;
        }
    });
});

else문 추가하고 false를 반환하면 끝. 크핫;

방법이 여러 개더군요.

    $("#btn_stop").click( function() {
        if(confirm("종료 하시겠습니까?") == false) {
            return false;
        }
    });

    $("#btn_stop").click( function(e) {
        if(confirm("종료 하시겠습니까?") == false) {
            e.preventDefault();
        }
    });

    $("#btn_stop").click( function(e) {
        return confirm("종료 하시겠습니까?");
    });

결국은 맨 마지막 걸로 고쳤습니다.

정우형/성유군/재일이형 쌩큐!
top


[테스트] 스프링의 MappingJacksonJsonView 초간단 학습 테스트

모하니?/Coding : 2009.10.13 12:10


public class SpringJsonVIewTest {
   
    class SampleObject{
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }
   
    MappingJacksonJsonView jsonView;
   
    @Test
    public void result() throws Exception {
        jsonView = new MappingJacksonJsonView();
        Map<String, Object> model = new HashMap<String, Object>();
        SampleObject s1 = new SampleObject();
        s1.setName("기선");
        model.put("s1", s1);
        SampleObject s2 = new SampleObject();
        s2.setName("갑수");
        model.put("s2", s2);
       
        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        jsonView.render(model, request, response);
       
        System.out.println(response.getContentAsString());
    }

}

이렇게 작성해봤습니다. Assertion 안했습니다.

JSON이 어떤 모양으로 생기는지 그냥.. 눈으로 확인하려구요. why not?

아참.. 결과는

{"s2":{"name":"갑수"},"s1":{"name":"기선"}}

이래요.
top


[제이쿼리] 마우스 오버/아웃 이벤트 사용하기

모하니?/Coding : 2009.10.12 20:59


출석체크를 할 때, 체크 모양의 버튼을 누르면 결석이 되고.. 결석 버튼을 누르면 출석이 되는 형태의... 비 직관적인 뷰를 만들었었습니다.

버튼을 두 개 만들기 귀찮아서 버튼 하나를 두고 On/Off 버튼 식으로 만들었는데, 현재 상태를 체크 모양 자체가 현재 상태를 나타내고, 그것을 한 번 누르면 결석으로 바뀌게 한 것이죠.

만든 저야.. 로직까지 포함해서 잘 이해가 되지만, 첨보는 팀원은 굉장히 햇갈려 하더군요. 이해합니다. 곰곰히 생각하다가 어제 맥북으로 사진 정리하다가 iPhoto에서 얼국 인식을 하는데, 내 얼굴인지 아닌지 확인할 때의 UI가 떠올랐습니다. 딱.. 그 UI가 출석체크와 비슷한 로직이었습니다.

다른 것은 하나.. 마우스가 올라갈 때, 해당 버튼이 어떤 이벤트를 할 지 알려준다는 것입니다.

"본인이면 클릭하세요." 에서 마우스를 클릭하면 "백기선님으로 확인되었습니다."로 바뀌고 거기서 마우스가 올라가면 "백기선님이 아닙니까?"로 바뀌죠. 그걸 또 다시 클릭하면, "백기선님이 아닙니다."로 바뀝니다.

오호.. mouse over 이벤트만 사용하면 되겠군...  생각하고 적용해봤습니다.

    $(".btn_attend_member").mouseover(function(){
        $(this).attr("src", "<c:url value='/images/remove-16x16.png'/>");
    }).mouseout(function(){
        $(this).attr("src", "<c:url value='/images/accept-16x16.png'/>");
    });

    $(".btn_absend_member").mouseover(function(){
        $(this).attr("src", "<c:url value='/images/accept-16x16.png'/>");
    }).mouseout(function(){
        $(this).attr("src", "<c:url value='/images/remove-16x16.png'/>");
    });

버튼이 아니라;; 이미지를 사용하고 있어서 좀.. 그렇긴 하지만 그건 나중에 바꾸기로 하고.. 일단 저렇게 이벤트를 등록해놓고 마우스를 올렸다 내렸다 해봤더니.. 잘 되는겁니다. ㅋㅋㅋ 그때까진 좋았죠.

하지만...

Ajax 요청을 보내고 나서 결석 상태를 출석으로 바꾸고 났을 때, 출석 상태인데도 마치 결석 상태처럼 마우스가 밖에 있으면 결석(현재 상태), 마우스가 이미지로 올라가면(다음 이벤트) 출석 상태를 보여주는 겁니다. @_@

문제 원인은 페이지 로딩시에 저 이벤트를 한 번 등록해두면 설령 Ajax 요청 처리에서 css를 바꾼다 하더라도 저 이벤트들을 다시 등록하진 않기 때문입니다.(좀 더럽지만... 화장실에서 볼 일 보며 곰곰히 생각해보니.. 왜 그런지 알겠더군요.)

그래서..

var changeImageSrcWhenMouseOverOut = function () {
    $(".btn_attend_member").mouseover(function(){
        $(this).attr("src", "<c:url value='/images/remove-16x16.png'/>");
    }).mouseout(function(){
        $(this).attr("src", "<c:url value='/images/accept-16x16.png'/>");
    });

    $(".btn_absend_member").mouseover(function(){
        $(this).attr("src", "<c:url value='/images/accept-16x16.png'/>");
    }).mouseout(function(){
        $(this).attr("src", "<c:url value='/images/remove-16x16.png'/>");
    });
};

이렇게 함수를 하나 만들고, 이 녀석을 페이지 로딩 할 때 한 번, Ajax 요청을 처리한 뒤 한 번 호출 하도록 코딩했더니.. 원하던데로 동작하게 되었습니다. 음하핫;;

안타깝지만, 관리자만 할 수 있는 기능이라 여러분을 볼 수가 없겠군요... @_@
top


[작명 고민] 하이버네이트 get/find류 작명 규약 1

모하니?/Coding : 2009.10.05 17:12


참조: http://blog.xebia.com/2009/04/03/jpa-implementation-patterns-retrieving-entities/

아시다시피, 하이버네이트에서 영속화 중인 객체를 가져오는 방법은 get()과 load()가 있습니다. 둘 다 가져올 객체 클래스와 id 값을 넘겨주면 원하는 객체를 가져올 수 있습니다.

일단 중요한 차이점 하나는 가져다 달라고 하는 객체가 없을 때 get()은 null을 반환하고 load()는 ObjectNotFountException을 던진다는 것입니다. 또, get()은 항상 DB에서 꺼내오게 쿼리가 날아가며 load()는 프록시를 반환한 뒤 실제로 객체의 다른 속성 값들이 필요할 때 쿼리가 날아가도록 Lazy loading을 활용할 수 있습니다.

그러나 여기서 말하려는건 get, load가 아니라 Session.createQuery() 또는 createCriteria()를 이용해서 만드는 DAO의 메서드 작명 지침과 그 규약에 대한 것입니다.

DAO를 만들다 보면, 검색 류의 메서드들을 많이 추가하게 되는데, 그 이름을 지을 때마다 get으로 할까 find로 할까 by 뒤에 매개변수 타입을 적어줄까 아니면 어차피 메서드 매개변수에 있으니까 생략할까.. 이런 고민을 하게 되는데 맨 위 참조 글을 적은 분이 잘 정리해 두셨더군요.

위 글을 적으신 분은 EntityManager API의 find() 메서드와 createQuery() 메서드에 따라 규약을 만들었더군요. 그것을 응용해서 Session API의 createQuery(), createSqlQuery(), createCriteria(), get(), load() 등을 사용하는 DAO의 메서드 이름과 그 동작방식에 대한 규약을 정할 수 있을 것 같습니다.

그래야만 아래처럼 애매한 이름의 DAO 메서드들이 만들어 지지 않을테니까요.


여러 Study 객체를 가져오는데, 어떤 것은 getStudyList 어떤 것은 findXXXStudies 라니..
- get과 find의 구분도 없고
- Study 복수형을 쓸지 List를 붙일지 규약도 없네요.
- 그리고 무엇으로 검색을 한다는 것도 추측이 안 되고 말이죠.

그리고 하나 더 Finder를 제공해주는 AridPOJOs라는 걸 사부님 블로그에서 얼핏 봤었는데 자세히 좀 살펴보고 싶어지는군요. Finder까지 기본 제공해주는 GenericDao!! 멋지자나요. 이미 AridPOJOs에서는 Finder를 만들면서 위와 같은 고민을 했었겠죠?

흠... 생각 좀 해봐야겠습니다.

top


[봄싹] 새 기능 소개

모하니?/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


[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


[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




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