Whiteship's Note

'하이버네이트'에 해당되는 글 118건

  1. 2010.07.27 [하이버네이트 VS JPA] 객체 다루기 (2)
  2. 2010.07.23 [하이버네이트 배치 추가] flush와 clear (2)
  3. 2009.11.10 [하이버네이트] @BatchSize로 쿼리 갯수 대폭 줄이기
  4. 2009.11.04 [메이븐] 하이버네이트 플러그인
  5. 2009.10.31 [하이버네이트] 컬럼 타입은 어떻게 명시하는게 좋을까?
  6. 2009.10.15 [봄싹 버그]] JSON 뷰와 하이버가 가져온 Proxy 객체
  7. 2009.10.05 [작명 고민] 하이버네이트 get/find류 작명 규약 1 (2)
  8. 2009.09.08 [하이버네이트] 1 + 2N select 문제 해결하기 (4)
  9. 2009.09.07 [하이버네이트] 2차 캐싱 적용하기
  10. 2009.08.07 [하이버네이트] 애매한 에러 메시지 때문에 삽질.. @_@ (6)
  11. 2009.07.28 [하이버네이트] OneToMany에 FetchType.EAGER 사용시 어떤 일이 생길까?
  12. 2009.07.23 [하이버네이트]롹킹과 성능 사이에 서다. (1)
  13. 2009.07.03 하이버네이트, 스프링 MVC에서 enum 사용하기 3
  14. 2009.07.03 하이버네이트, 스프링 MVC에서 enum 사용하기 2
  15. 2009.07.02 하이버네이트, 스프링 MVC에서 enum 사용하기 (4)
  16. 2009.07.02 결국 그냥 만들어버린 JPA 문서 자동화 툴 (4)
  17. 2009.06.27 [하이버네이트] Session-Per-XXX
  18. 2009.06.25 [하이버네이트] detached 객체의 동일성 (4)
  19. 2009.06.24 [하이버네이트 퀴즈] Flush (8)
  20. 2009.02.04 스프링 2.5 환경에서 하이버네이트 사용하기
  21. 2008.11.17 하이버네이트, 스프링, 트랜잭션, OSIV(Open Session In View) 패턴
  22. 2008.11.14 하이버네이트 Criteria 검색 쿼리 골치
  23. 2008.11.12 @Repository를 쓴다면 하이버네이트 예외 변환기 직접 만들 필요 없습니다. (4)
  24. 2008.11.03 하이버네이트가 문제인가요? 자신이 문제인가요? (14)
  25. 2008.10.08 스프링에서 하이버네이트와 JDBC 같이 사용할 때 트랜잭션 처리는?
  26. 2008.09.19 @CollectionOfElements 애노테이션
  27. 2008.09.09 하이버네이트 VS JPA
  28. 2008.09.08 하이버네이트를 이용한 DAO 코드에서 int 타입의 리턴값과 SQLException은 무슨 의미가 있을까...
  29. 2008.08.16 하이버네이트 3.3.0 GA 릴리즈~
  30. 2008.07.30 하이버네이트 Criteria 다루기 - 중복일까 아닐까 (2)

[하이버네이트 VS JPA] 객체 다루기

Hibernate/etc : 2010.07.27 17:09


JPA를 언젠간 써야 할텐데 아직도 하이버네이트가 그냥 편해서... @_@;; 암튼 이 둘은 객체를 다루는 API가조금 다른데 그걸 정리해둡니다.

 하이버네이트(Session) JPA(EntityManager) 설명 
save() persist()  저장(정확하게는 Pesistent 상태로 변경) 
 get() find()  DB에서 가져오기 
 load()  getReference() 프록시 가져오기 
 delete() remove()  삭제(정확하게는 Deleted 상태로 변경) 
update()  없음  reattach 다시 부착하기(정확하게는 Detached 상태에서 Persistent 상태로 변경) 
 merge() merge()  merge 병합하기(get() 해온 다음에 Detached 객체의 상태를 복사해간다. 


왠지 CRUD가 다 있어 보이지만 사실 아래 두 줄은 Update 관련 API가 아니라 Detached 상태의 객체를 Persistent 상태로 만들기 용 메서드가 뭐 이것들을 이용해서 Detached 상태 객체를 DB에 반영해서 Update 쿼리를 발생시킬 수도 있지만.. 사실 진정한 Update는 API로 존재하지 않는다. 

즉.. Persistent 상태의 객체를 가지고 어떤 속성을 변경했다 치자.. 이때 굳이 어떤 API를 써서 Update 문을 발생시키지 않아도 된다는 것이다. 

Session session = getSession(); 
Transaction tx = session.beginTransaction(); 
Book book = (Book) session.get(Book.class, 12); 
book.setName("토비의 스프링 3"); 
tx.commit(); 
session.close(); 

저렇게 변경하고 아무것도 실행하지 않는다. 왜일까? 퀴즈닷.
저작자 표시
신고
top


[하이버네이트 배치 추가] flush와 clear

Hibernate/etc : 2010.07.23 14:18


배치 작업이라는 것이 DB에서 데이터를 읽어온 다음 뭔가 수정하고 뭔가를 다시 DB에 넣는 작업인데 이런 작업을 하이버네이트로 할 때 조심해야 할 것이 있습니다.

                InvDailyClosing invDailyClosing = new InvDailyClosing();
                invDailyClosing.setDate(today);
                invDailyClosing.setLocation(location);
                invDailyClosing.setItem(item);
                invDailyClosing.setQtyStart(qtyStart);
                invDailyClosing.setInvInList(invInList);
                invDailyClosing.setInvOutList(invOutList);
                invDailyClosing.closing();
                dao.add(invDailyClosing);

이렇게 작성하면 끝난것 같지만 사실 좀 위험한 코드입니다. 만약 저 코드가 for 루프 안에 있고 굉장히 여러번 반복 된다면 언젠가는 MemoryOutOfException을 보게 될 겁니다. "아니 왜?" 라고 하시는 분들이 계실텐데요. 

흠... 하이버네이트 1차 캐시 때문에 그런 현상이 생깁니다. 하이버네이트는 Persistent 상태의 객체를 하이버네이트 Session에 담아둡니다. 여기사 1차 캐시입니다. 캐시니까 저안에 담아놓고 재사용할 수 있습니다. DB에 다녀오는 횟수를 줄일 수 있겠죠. 그런데 언젠가는 이 캐시를 DB와 동기화 시켜야 합니다. 그래야 Session에 담겨있는 객체(Pesistent 객체)를 지지고 볶은 것이 DB에 반영이 되겠죠. 그렇게 DB와 Session 상태를 동기화 시키는것을 Flush라고 하는데.. 또 Persistent 상태라는 것은... 아.. 이런.. 안되겠군요. @_@ 그냥 하이버네이트 완벽 가이드를 읽어주세요.

아무튼 너무 많이 쌓여서 메모리가 부족해지는 상황이 발생하지 않도록 계속해서 Session을 DB에 동기화 시키고 Session을 비워주는게 좋습니다.

하이버네이트 Session의 flush()와 clear()이 바로 그런 용도로 존재하죠. 그래서 

                InvDailyClosing invDailyClosing = new InvDailyClosing();
                invDailyClosing.setDate(today);
                invDailyClosing.setLocation(location);
                invDailyClosing.setItem(item);
                invDailyClosing.setQtyStart(qtyStart);
                invDailyClosing.setInvInList(invInList);
                invDailyClosing.setInvOutList(invOutList);
                invDailyClosing.closing();
                dao.add(invDailyClosing);
             dao.flushAndClear();

이렇게 하이버네이트용 GenrericDao에 flushAndClear()라는걸 만들어 놓고 써주면 됩니다. 주의하실 것은... 반드시 flush 먼저 하고 나서 clear 해야 합니다. 반대로 하면 음식 담긴 쟁반을 서빙도 안하고 설것이 해버리는거나 마찬가지..

저작자 표시
신고
top


[하이버네이트] @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


[메이븐] 하이버네이트 플러그인

Build/Maven : 2009.11.04 10:34


참조:
http://kwon37xi.springnote.com/pages/2275410
http://mojo.codehaus.org/maven-hibernate3/hibernate3-maven-plugin/componentproperties.html

1. 메이븐 pom.xml 에 플러그인 추가하기

            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>hibernate3-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <componentProperties>
                        <drop>false</drop>
                        <create>true</create>
                        <format>true</format>
                        <export>false</export>
                        <jdk5>true</jdk5>
                        <outputfilename>schema.ddl</outputfilename>
                        <configurationfile>src/hibernate.cfg.xml</configurationfile>
                        <propertyfile>database.properties</propertyfile>
                    </componentProperties>
                    <components>
                        <component>
                            <name>hbm2ddl</name>
                            <implementation>annotationconfiguration</implementation>
                        </component>
                    </components>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>postgresql</groupId>
                        <artifactId>postgresql</artifactId>
                        <version>8.2-507.jdbc3</version>
                    </dependency>
                </dependencies>
            </plugin>

굵은 글씨 부분은 사용하시는 프로젝트에 맞게 변경해야 할 부분입니다.

일단, 구조를 보면 configuration에 크게 두가지가 들어있는데, 하나는 component들을 담고 있는 components이고, 다른 건 componentProperties입니다. 제가 사용하려는건 hbm2ddl이기 때문에 hbm2ddl 컴포넌트만 등록해 두었습니다. 그와 관련된 설정은 componentProperties여기에 들어있죠.

먼저, drop은 기존의 DB의 테이블 들을 깔끔하게 날려버릴 sql을 만들지 여부를 나타냅니다. 기본 값은 false기 때문에 위와 같은 설정에서는 생략해도 됩니다.

create는 뭔지 아시겠죠? 패스. 기본값은 true입니다.

format은 보기 좋은 형태로 SQL 출력 형태를 다듬어 줍니다. 기본 값은 false입니다.

export는 현재 DB에 적용을 할꺼냐는 건데.. 위험하기 때문에 일단은 false가 좋겠습니다. 그런데 기본값은 true입니다. 하지만, DB 마이그레이션 자동화가 되어있다면 true로 놓고 매번 DB 스키마를 새롭게 업데이트 한 다음 백업했던 데이터를 채워주면 되겠습니다.

jdk5는 역시 패스. 기본값은 false입니다.

outputfilename는 ddl을 출력할 파일 이름인데, 이 파일은 프로젝트/target/hibernate3/sql 밑에 생깁니다.

configurationfile는 하이버네이트 설정 파일 위치를 알려줍니다.

propertyfile는 하이버네이트가 DB에 접근할 때 필요한 프로퍼티를 가지고 있는 파일 위치를 알려줍니다.

2. 하이버네이트 설정 파일 만들기

<!DOCTYPE hibernate-configuration SYSTEM
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <mapping class="springsprout.domain.Member" />
        <mapping class="springsprout.domain.Comment" />
        <mapping class="springsprout.domain.Resource" />
        <mapping class="springsprout.domain.Right" />
        <mapping class="springsprout.domain.Role" />
        <mapping class="springsprout.domain.wiki.Histories" />
        <mapping class="springsprout.domain.wiki.History" />
        <mapping class="springsprout.domain.wiki.WikiDocument" />
        <mapping class="springsprout.domain.study.Study" />
        <mapping class="springsprout.domain.study.Presentation" />
        <mapping class="springsprout.domain.study.Meeting" />
        <mapping class="springsprout.domain.study.Attendance" />
        <mapping class="springsprout.domain.seminar.Seminar" />
        <mapping class="springsprout.domain.seminar.SeminarComer" />
        <mapping class="springsprout.domain.notice.Notice" />
        <mapping class="springsprout.domain.main.Graffiti" />
        <mapping class="springsprout.domain.file.UploadFile" />
    </session-factory>
</hibernate-configuration>

애노테이션기반으로 설정한 경우에는 위와 같이 애노테이션으로 맵핑한 클래스들을 알려줍니다.

3. 하이버네이트 프로퍼티 추가하기

hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
hibernate.connection.username=***
hibernate.connection.password=***
hibernate.connection.driver_class=org.postgresql.Driver
hibernate.connection.url=***

이런 설정을 프로퍼티 파일에 추가하거나 새로운 프로퍼티 파일을 만들어서 추가해줍니다.

끝!!!

이제 hbm2ddl 골을 실행할 수 있습니다.

제가 원한건 새로운 스키마용 ddl이 아니라, 기본 DB에서 수정할 것들만 알려주는 수정용 ddl인데 어떻게 설정해야 되는지 잘 모르겠군요. 그런 기능을 지원해주는 건지 아닌지도 몰겠고.. componentProperties를 보면 update 속성이 있긴한데, 이 녀석에 true를 줘버리면 ddl 자체가 안 만들어집니다. @_@;;


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


[봄싹 버그]] 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


[작명 고민] 하이버네이트 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


[하이버네이트] 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


[하이버네이트] 애매한 에러 메시지 때문에 삽질.. @_@

Hibernate/etc : 2009.08.07 21:50


DEBUG - OpenSessionInViewFilter.doFilterInternal(207) | Closing single Hibernate Session in OpenSessionInViewFilter
DEBUG - SessionFactoryUtils.closeSession(784) | Closing Hibernate Session
2009. 8. 7 오후 9:30:52 org.apache.catalina.core.StandardWrapperValve invoke
심각: Servlet.service() for servlet springsprout threw exception
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: springsprout.domain.Member.studies, no session or session was closed
    at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:358)
    at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(AbstractPersistentCollection.java:350)
    at org.hibernate.collection.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:343)
    at org.hibernate.collection.PersistentSet.add(PersistentSet.java:189)
    at springsprout.domain.Member.addManegedStudy(Member.java:154)
    at springsprout.modules.study.StudyService.add(StudyService.java:24)
    at springsprout.modules.study.StudyService$$FastClassByCGLIB$$6d06dbc0.invoke(<generated>)
    at net.sf.cglib.proxy.MethodProxy.invoke(MethodProxy.java:149)
...

에러의 원인이 무엇일까? 에러 로그에서 가장 중요한 부분이라고 생각하는 부분을 진하게 표시했다. 제 1의 단서는 에러 메시지이고, 제 2의 단서는 에러가 발생한 지점 중 내가 코딩을 한 부분이다. 스프링, 하이버는 많은 유저와 테스트 그리고 지속적인 관리를 통해 내가 작성한 코드보다 더 신빙성이 높기 때문이다.

어쨋든.. 생각해보자. 에러 메시지가 뭐라고 하는가?? 세션이 없다고?? 왜??? 난 분명 @Transactional을 사용했다. 트랜잭션이 없을리 없는데.. 왜 그럴까.. 이해가 되지 않는다. 세션이 닫혔다고?? 왜??? 이해가 되지 않는다. 이런 일이 발생한 적은 없었다.

혹시나해서 DEBUG 모드로 스프링 로그를 찍어보았다. 해당 에러가 나기 직전에 이러한 로그를 볼 수 있다.

DEBUG - HibernateTransactionManager.doBegin(504) | Preparing JDBC Connection of Hibernate Session [org.hibernate.impl.SessionImpl@7650bc]
DEBUG - HibernateTransactionManager.doBegin(569) | Exposing Hibernate transaction as JDBC transaction [com.mchange.v2.c3p0.impl.NewProxyConnection@a681c3]
DEBUG - HibernateTransactionManager.doGetTransaction(437) | Found thread-bound Session [org.hibernate.impl.SessionImpl@7650bc] for Hibernate transaction
DEBUG - AbstractPlatformTransactionManager.handleExistingTransaction(468) | Participating in existing transaction

분명 Session을 잘 가져왔고, 세션이 닫힌다는 얘기도 없다. 이 바로 다음에 에러가 발생한다. 막막하군.. 그럼 이제 내가 작성한 코드를 살펴보자.

    public void add(Study study) {
        Member currentMember = securityService.getCurrentMember();
        currentMember.addManegedStudy(study);
        repository.add(study);
    }

    public void addManegedStudy(Study study) {
        getStudies().add(study);
        study.addMember(this);
        getManagedStudies().add(study);
        study.setManager(this);
    }

빨간색으로 표시한 부분이 에러가 발생하는 지점이고 최종적으로 getStudies()를 호출할 때 발생한다. 모르겠다. 무엇이 문제일까?

테스트를 만들어보자.

    @Test
    public void add() throws Exception {
        Study study = new Study();
        Member member = new Member();
        memberService.addWithoutConfirm(member);
       
        service.add(study, member);

        assertEquals(member, study.getManager());
        assertTrue(study.getMembers().contains(member));
    }

스프링 통합 테스트를 만들어서 테스트해보았다. 이 테스트는 잘 돌아간다. 더 미궁속으로 빠져든다. 그런데, 테스트와 실제 코드가 같지 않다. 테스트를 편하게 하기 위해 조금 다른 코드를 이용했다. 둘의 차이는 무엇일까?

테스트에서의 member 객체 상태는 Persistent다. 그러나... 실제 코드에서 member 객체 상태는 어떨까? 아차차.. 이런.. 문제의 실마리가 보이는 듯 하다.

그렇다. 실제 코드에서 currentMember 객체는 detached 상태인 것이다. 아.. 이런.. 단서에 너무 의존하다가 눈앞에 있는 객체 상태를 보지 못했다. 나의 실수다. 하이버네이트를 다룰 때 가장 중요한 것 중 하나가 바로 객체의 상태인데. 그것을 못 보다니 한심하다는 생각이 밀려온다. 이미 지난 일이다. 어쩔 수 없다. 다음번엔 잘 찾아내야지.

단 하나.. 바라는게 있다면, 세션이 없다는 얼토당토 않은 에러 메시지 말고, detached 객체에서 lazy loading이 발생한다고 메시지를 출력해주면 좋겠다. 그랬다면... 좀 더 쉽게 해결할 수 있었을 법한 문제였다.
신고
top


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

모하니?/Coding : 2009.07.28 20:28


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

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

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

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

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

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

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

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

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

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

~~ from P ~~

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

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

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

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

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

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

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

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

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

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

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

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


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

모하니?/Coding : 2009.07.23 18:09


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

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

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

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

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

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

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

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

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

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

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

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

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

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


하이버네이트, 스프링 MVC에서 enum 사용하기 3

분류없음 : 2009.07.03 18:53


1. Character 값을 DB에 저장하는 enum도 지원하도록 구현했고..
2. UserType 생성을 좀 더 간편화 했습니다.

public enum FamillyCate implements PersistentEnum {

    FATHER('f', "부"), MOTHER('m', "모"), BROTHER('b', "형제"), SISTER('s', "자매");
   
    private final Character value;
    private final String descr;
   
    private FamillyCate(Character value, String descr) {
        this.value = value;
        this.descr = descr;
    }
   
    public Character getValue() {
        return value;
    }
    public String getDescr() {
        return descr;
    }
...
}

이렇게.. Character 값을 DB에 저장할 enum을 사용할 수 있습니다. 이 enum에 대한 UserType 생성은 다음과 같습니다.

public class FamillyCateType extends GenericEnumUserType<FamillyCate>{
}

이 enum에 대한 PropertyEditor는?

binder.registerCustomEditor(FamillyCate.class, new GenericEnumPropertyEditor<FamillyCate>(FamillyCate.class));

캬,.. enum에 대한 UserType과 PE를 전부 코드 한 줄로.. 끝낼 수 있습니다. GenericEnumUserType와 GenericEnumPropertyEditor 코드는 비공개입니다. 영원히~

자 그럼 오늘은 이만 하고,, 다음 번엔 enum 목록을 가져올 때 순서를 정해서 가져오는 방법을 마련해보도록 하겠습니다.

ps: 오랜만에 dm 서버나 돌려봐야겠네요. 방명록에 누가 요청하셔서;;
신고
top


하이버네이트, 스프링 MVC에서 enum 사용하기 2

모하니?/Coding : 2009.07.03 16:37


어제에 이어 오늘도 물고 늘어진다. 이번에 해결한 문제는, DB에 저장할 값을 int 타입 뿐만이 아니라 String 타입을 사용해도 무방하도록 코드를 수정했다.

public enum UserCate implements PersistentEnum {
   
    ADMIN("admina", "관리자"), STAFF("staff", "직원"), SUPP("supp", "협력업체");
   
    private final String value;
    private final String descr;
   
    private UserCate(String value, String descr) {
        this.value = value;
        this.descr = descr;
    }

    public String getValue() {
        return value;
    }
    public String getDescr() {
        return descr;
    }
   
...

}

이건 DB에 저장할 값으로 String 값을 사용할 enum 이고..

public enum CodeCate implements PersistentEnum {

    COLOR(10, "색상"), SIZE(20, "사이즈"), PAYTERM(30, "지불조건"), SHIPVIA(40, "운송방식");
   
    private final Integer value;
    private final String descr;
   
    private CodeCate(Integer value, String descr) {
        this.value = value;
        this.descr = descr;
    }
   
    public int getValue() {
        return value;
    }
    public String getDescr() {
        return descr;
    }

...

}

이건 DB에 저장할 값으로 int 값을 사용할 enum이다.

하이버네이트가 필요로 하는 UserType을 만들어 보자.

public class CodeCateType extends GenericEnumUserType<CodeCate>{

    public CodeCateType() {
        super(CodeCate.class);
    }
   
}

public class UserCateType extends GenericEnumUserType<UserCate>{

    public UserCateType() {
        super(UserCate.class);
    }
   
}

끝이다. 도메인 객체 타입에 설정해보자. 구현에 필요한 코드만 파란색으로 강조를 했다. 생성자 만드는 부분까지 없앨 수 있을 것 같다. 3차 구현에서 해보자.

Code.java

    @Column
    @Type(type="koma.domain.usertype.CodeCateType")
    CodeCate codeCate;

User.java

    @Column
    @Type(type="koma.domain.usertype.UserCateType")
    UserCate userCate;

자.. 이러면 하이버네이트가 CodeCate는 Integet SQL 타입 컬럼을 만들어 주고 UserCate는 String SQL 타입(varchar)를 만들어 줄 것이다.

스프링 MVC에서 바인딩 할 때 사용할 PropertyEditor는 어떨까? 한 줄 씩이다.

        binder.registerCustomEditor(CodeCate.class, new GenericEnumPropertyEditor<CodeCate>(CodeCate.class));
        binder.registerCustomEditor(UserCate.class, new GenericEnumPropertyEditor<UserCate>(UserCate.class));

끝인가?? 글쎄.. 모르곘다. Charater를 추가하는 것도 별 문제 없을 듯 하다. 해보자. 이러다가 모든 타입을 지원해야 하는건 아니겠지?? Char까지만 해보자. 여기서도 생성자에 클래스를 주고 있는데 3차에서는 저 코드를 없앨 수 있게 구현해보자.

자.. 하이버, 스프링에서 자바 enum 사용 간편화 3차 구현 ㄱㄱㅆ
신고
top


하이버네이트, 스프링 MVC에서 enum 사용하기

모하니?/Coding : 2009.07.02 18:19


하이버네이트와 스프링 풀셋으로 구성되어 있는 웹 애플리케이션에서 자바 enum을 사용할 때 생기는 이슈가 뭘까?

1. DB에 어떤 값을 넣을 것이고,
2. 화면에는 어떤 값을 보여주고 어떻게 바인딩 할 것인가?

이 두 가지라고 한다. 그 밖에 이슈 될만한 것은.. 흠.. 뭐.. 없지 않을까 싶다. 왜 이슈일까?

1번 문제를 보자. DB에 잘 안 들어갈까? 하이버네이트로 맵핑을 해보자.

enum UserType {
 MEMBER, MANAGER
}

@Entity
class Member {
...
  @Column
  UserType userType;
}

이 상태로도 SessionFactory를 생성하는데 별 문제도 없을 뿐더러, 읽고 저장하기가 잘 된다. 문제는 DB에 들어가는 값이다. DB에 들어가는 값을 보면, UserType.MEMBER는 0, UserType.MANAGER는 1이 integer 컬럼에 저장된다. enum의 ordinal() 메서드가 반환해주는 값을 그대로 저장한 것이다. 문제는 ordinal() 값이 고정이 아니라, enum 순서에 따라 바뀐다는 것이다. 이런... 그럼 안 되겠다. ordinal 말고, String을 저장하고 싶다면, JPA의 @Enumerated 애노테이션을 추가해주면 된다. @Enumerated(EnumType.STRING) 이렇게 말이다. 이 것을 사용하면 방금 말한 ordinal 문제는 사라질 것이다. DB에는 ordinal이 반환하는 Integer대신 enum의 name이 저장될 것이다.

그런데.. 어떤 이유에선가 굳이 integer 값을 DB에 저장하고 싶다면 어찌해야 될까? 이제부터 복잡해진다. 일단 enum에 필드를 하나 추가하고, EJ2가 추천하는 방법으로 enum을 구현한다. 다음에는 하이버네이트의 UserType 인터페이스를 구현한 클래스를 하나 만들고,

    @Type(type="koma.domain.usertype.CodeCateUserType")
    @Column
    CodeCate codeCate;

이런식으로 하이버네이트의 @Type 애노테이션을 이용하여 맵핑 방법(db에 어떻게 저장하고, db에서 어떻게 꺼내 올 것인가)을 담고 있는 UserType 구현체를 지정해주어야 한다. 이 구현체를 만들 때는 UserType 인터페이스가 제공하는 메서드 10개 정도를 구현해야 된다. 귀찮은 일이다. 그래서 GenericEnumUserType 이라는 클래스를 만들었다. 간단하게 상속만 하고, 생성자만 만들면 되도록 귀찮을 일을 줄여놨다. 자 그럼 일단 첫 번째 문제는 해결이다.

두 번째 문제는 첫 번째에 비하면 비교적 쉽다. 지난 프로젝트에서 PropertyEditor와 씨름을 했던탓에 면역이 생긴 것 같다. 화면에 Enum을 보여줄 떄 enum의 name을 보여주고 싶진 않을것이다. 역시 새로운 필드를 추가해야겠다. 그리고 화면에 보여줄 때는 그 값을 출력하고, 화면에서 어떤 것을 선택했을 때에는 아까 DB에 입력한 값을 선택해서 가져오도록 화면 코드를 작성했다.

다음은 그렇게 해서 가져온 integer 값을 Enum 객체로 샥 바꿔주는 일을 할 PropertyEditor를 만드는 것이다. 간단하다. getAsText()에서는 getValue()로 가져온 객체를 내가 사용하는 enum으로 타입을 변환 한 다음 아까 추가한 필드의 getter를 사용하여 String 값을 넘겨주었다. 이제 화면에서 사용자 친화적인 문구를 볼 수 있을 것이다. 다음은 setAsTest(String text)를 재정의 하여 text는 화면에서 선택한 enum이 DB에 입력하는 값인 integer 값일 것이다, 일단 Integer.parseInt()를 해야 겠다. 아.. 이런.. Enum 클래스에서 valueOf(Class, String) 메서드를 제공해준다. 하지만 난 int 값을 사용하기로 마음 먹었으니 저 클래스는 사용하지 못하겠다. 유틸을 하나 만들었다. Enum 클래스와 int 값을 받아서 해당 int 값을 가지고 있는 Enum을 돌려받는.. 그런 클래스다. 자 그럼 이제 이 유틸을 이용해서 setAsText(String text) 구현도 마칠 수 있다. 이러한 PropertyEditor 역시 매번 만들어 쓰기 귀찮으니깐, 아예 클래스를 만들지 않고 객체만 만들어 사용할 수 있는 GenericEnumPropertyEditor를 만들었다. 두 번째 문제도 해결됐다.

오늘 내가 할 일은 이게 끝인 듯 하다. 자 그럼 잠깐 회고를 해보자.

DB에 int 값이 아닌 enum의 name 문자열을 저장한다면 어떻게 될까?

일단, UserType을 만들 필요가 없어진다. 아까도 이야기 했듯이 @Column과 @Enumerated(EnumType.STRING)를 사용하면 UserType 없이고, 문자열로 enum을 DB에 저장할 수 있다. GenericEnumUserType도 필요가 없고, 매번 UserType 클래스를 만들어야 하는 수고도 줄어든다.

다음, 화면에서 enum 목록(Arrays.asList(enum.values());를 사용하면 간단)을 보여줄 때, enum에 추가한 사용자 친화적인 설명을 담고 있는 descr 속성에 담겨있는 값을 보여주고, 실제로 선택하는 값이 DB에 저장하는 int값이 아닌 enum의 name이라면 어떻게 바뀔까? getAsText() 구현은 동일하고, setAsText()에서 받아오는 값이 Enum의 name이니깐, Enum.valueOf(Class, String)을 사용할 수 있다. 굳이 Util 클래스를 만들 필요도 없고, setAsText() 구현도 간단해진다. 다만, Enum 마다 PropertyEditor 객체를 지정해 줘야 하는 건 어쩔 수 없다. 하지만 이건 정말 일도 아니다. 새로운 클래스를 추가하는 것도 아닌데 이 일이 뭐 크게 대수겠는가.

결국.. DB에 어떤 이유로 인해 enum의 interger 값을 저장하는 것이 enum의 name 문자열을 저장하는 것보다 훨씬 복잡하고, 귀찮은 것 같다.

DB에 int를 저장하는게 좋을까 string을 저장하는게 좋을까? integer 값을 저장해야 하는 별다른 이유가 없다면 나는 enum의 name을 저장하고 싶다.

수정은 내일.. 오늘은 이만 퇴근..

===========================

할려고 했으나.. 이게 끝이 아니란다. DB에 저장할 enum 필드를 선택할 수 있게 해야 되고(결국 위에서 실컷 고민한게.. 물거품처럼 하얘지는 느낌이다.),

enum 목록을 가져올 때 정렬을 할 수 있어야 한단다.(그럼 이것도 Arrays.asList(enum.values()); 만으로는 어림 없을 듯 하다.)

또한 i18n까지도..

@_@
신고
top


결국 그냥 만들어버린 JPA 문서 자동화 툴

모하니?/Coding : 2009.07.02 11:42


지난 번에 살펴본 hbm2doc로는 사부님이 원하는 문서를 만들기가 버거워서, 예전에 물개선생님이 만드셨다는 코드를 참조해서 비슷하게 만들었습니다.

@Entity
@Table(name = "users", uniqueConstraints = @UniqueConstraint(columnNames = { "loginid" }))
@SequenceGenerator(name="user_sq", sequenceName="user_sq")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    Integer id;
    @Version
    Integer version;
    @ColumnInfo(value = "아이디", description="중복 되는 값은 사용할 수 없음")
    @Column(length = 50, unique = true)
    String loginid; // identity

    @ColumnInfo("이름")
    @Column(length = 50)
    String name;
    @Column(length = 50)
    String pwd;
    @Column(length = 50)
    @Index(name="email_idx")
    String email;
    @Column
    int usertype;
    @Column(length = 50)
    String title;
    @Column(length = 50)
    String dept;
    @Column(length = 50)
    String tel;
    @Column(length = 50)
    String mobile;
    @Column(length = 50)
    String addr;
    @Column
    @Temporal(TemporalType.DATE)
    Date birthday;

    @ManyToMany
    Set<Role> roles;

    @OneToMany(cascade = { CascadeType.ALL})
    Set<Familly> famillies;

이렇게 애노테이션이 난무 하는 도메인 클래스에 대한 정보 + 새로 추가한 애노테이션 @ColumnInfo를 사용하여 화면에서 해당 필드를 보여주는 이름(또는 도메인 전문가가 사용하는 용어)과 설명을 추가할 수 있습니다.

어제 하루 종일 만들고 오늘 아침에 조금 다듬은 결과물은 다음과 같습니다.


코딩은 TDD로 시작했는데, 저 위에 보이는 HTML 만드는 코드는 참조하던 코드를 짜집기해서 만들었습니다.

클래스 구조는 대충.

DocGenerator ---> DocWriter ---> EntityInfo

이렇습니다. DocGenerator에 엔티티 클래스 목록을 주면, DocWriter로 문서를 생성해 내는데, 이 때 new EntityInfo(엔티티 클래스) 생성자를 사용하여 애노테이션에서 정보를 축출하여 담고 있는 Info 클래스를 만들어 사용합니다.

DocGenerator는 기초 정보(Dialect, 엔티티 클래스, 타겟 폴더, ...) 수집 및 퍼사드 역할을 하고, DocWriter는 실제로 문서 생성을 책임지는데, 인터페이스를 둬서 여러 형태의 문서 작성기를 사용할 수 있도록 했습니다. 현재는 HTMLDocWriter만 구현되어 있습니다. EntityInfo는 리플렉션을 사용하여 주어진 클래스와 그 클래스에 붙어있는 애노테이션을 활용하여 화면 출력에 필요한 정보들을 수집해 두는 역할을 합니다. 일종의 DTO라고 봐야하나..

실행은 DocGeneratorRunner의 main 메서드를 약간 수정해서 실행하면 되겠습니다.


의존성, 프로젝트 이런거 없이 그냥 소스 코드만 묶었습니다. 아마.. 하이버네이트 애노테이션.jar, persistence.jar, 그리고 HTML을 처리할 때 사용한 hq-1.jar라는 라이브러리가 필요할 겁니다.

코드에 보시면 Writer 쪽에 심각한 중복 코드가 있는데.. 아직 해결하진 않았습니다. 캬캬캬
Info 쪽 코드도 전혀 깔끔하지 않습니다. 애노테이션에서 정보를 축출하여 초기화 하는 부분(생성자)을 일관성 있게 수정하고 싶은데.. 하진 않았습니다.
HTML에 어떤 DB 스키마에 해당하는 컬럼 타입인지 알려주기 위해 Dialect도 출력할까 했지만.. 역시 아직 하지 않았습니다. 이건 뭐 간단할 것 같으니 금새 추가할 수 있을지도 모르겠네요.
신고
top


[하이버네이트] Session-Per-XXX

Hibernate/Chapter 11 : 2009.06.27 17:52


참조: JPWH 11장

Session-Per-Operation: 479p. 안티 패턴, 하나의 오펴레이션(메서드) 당 새로운 세션을 만들어 사용하는 것. 성능상 병목지점이 될 수 있다.

Session-Per-Request: 479p. pesistence context 범위를 데이터베이스 트랜재션 범위와 동일하게 유지하는 것. 즉 트랜잭션 당 새로운 세션을 만들어 사용하는 것으로 볼 수 있다. 짧은 conversation(하나의 request-response 쌍)을 처리할 때 적당하다.

Session-Per-Conversation: 489p. persistence context를 복잡한 conversation(여러 request-response 쌍) 스콥으로 확장(extending)하는 방법. conversation 마다 새로운 session을 만든다. detached 객체를 사용하여 conversation을 구현하는 방법의 대안책이 될 수 있다.

ps: 복잡하다. persist(), saveOrUpdate()가 괜히 있는게 아니였다. SPC의 경우 인터셉터를 이용할 수 있고, currentSession을 어딘가에 저장해 두었다가 다음 request 처리시에 재사용해야 한다. 또한, detached 객체를 사용한 Conversation 구현과, SPC를 사용한 Conversation 구현은 맘대로 정하는게 아니다. 경우에 따라, SPC를 사용해야만 하는 경우가 있다. 자세한건.. 내일??
신고

'Hibernate > Chapter 11' 카테고리의 다른 글

[하이버네이트] Session-Per-XXX  (0) 2009.06.27
top


[하이버네이트] detached 객체의 동일성

Hibernate/Chapter 9 : 2009.06.25 13:54


참조: JPWH 9.2.2 ~ 9.2.3

두 가지 동일성이 있다. 자바 객체 동일성과 DB 동일성이 있다. 자바 동일성은 == 으로 비교를 하고, DB 동일성은 주키 값을 비교한다. 자바 동일성과 DB 동일성이 모두 같을 조건을 Scope of object identity 라고 한다.

그 중에 세 가지 조건은 다음과 같다.
- No identity scope
- Persistence context-scoped identity
- Process-scoped identity

이 중에 하이버네이트는 Persistence context-scoped identity를 구현했다.

Session session1 = sessionFactory.openSession();
Transaction tx1 = session1.beginTransaction();

// "1234" 식별자로 Item 가져오기
Object a = session1.get(Item.class, new Long(1234) );
Object b = session1.get(Item.class, new Long(1234) );

( a==b ) // True, a와 b는 동일하다.
tx1.commit();
session1.close();

// a와 b 레퍼런스는 detached 상태 객체가 된다.

Session session2 = sessionFactory.openSession();
Transaction tx2 = session2.beginTransaction();

Object c = session2.get(Item.class, new Long(1234) );
( a==c ) // False, detached 객체 a와 persistent 객체 c는 동일하지 않다.(session context가 다르기 때문에..)

tx2.commit();
session2.close();

따라서 이렇게 된다. 하지만 여전히 DB id는 가지고 있기 때문에, a, b, c를 id 값으로 비교하면 모두 같은 객체로 인식할 수 있다. equlas()로 비교할 땐 equals()를 어떻게 구현하느냐에 따라 달리질 것이다. equals()를 별도로 구현하지 않으면 Object의 equals()를 사용할테니 == 비교와 다를 바가 없다.

equals() 비교가 중요해지는 시기는 Set 컬렉션을 사용할 때다. 아시다시피 Set은 컬렉션에 요소를 추가하기 전에 기본에 추가되어 있는 것들과 equals() 비교를 해본 다음에 false인 것만 추가한다.

그렇다면, 위의 코드가 모두 깥는 뒤 (detached) 객체 3개(a, b, c)를 Set에 추가하면 어떻게 될까? 과연 Set에는 몇 개의 객체가 들어갈까?

2개다. 일단, equals()를 별도로 구현하지 않아서, == 비교를 할 텐데 이미 위에서 == 비교를 해봤더니 a와 b는 같은 객체를 참조하고 있고, c는 또 다른 객체기 때문이다.

원하던 결과는 몇 개 일까? 한 개일 것이다. 셋 다 같은 DB 레코드를 가리키기 때문에, Set에는 한 개만 들어가는게 맞을 것이다. 그렇게 하려면 어떻게 해야 할까?

DB id를 사용해서 동일성을 비교하는 equals()를 재정의하면 될 것이다. equals()를 재정의하면 hashCode()도 반드시 재정의해서 같은 객체는 같은 해쉬코드를 반환하도록 해야겠다.

결론적으로, detached 객체를 다룰 때는 동일성에 주의하고, 동일성 문제가 발생할 시 equals()와 hashCode()를 적절하게 재정의 할 것.




신고
top


[하이버네이트 퀴즈] Flush

Hibernate/etc : 2009.06.24 14:08


    @Transactional
    @Test
    public void crud() throws Exception {
        Emp emp = new Emp();
        emp.setName("ks");
        ed.save(emp);

        assertThat(ed.getAll().size(), is(1));
        assertThat(ed.get(emp.getId()).getName(), is("ks"));

        emp.setName("tb");
        ed.update(emp);
        assertThat(ed.get(emp.getId()).getName(), is("tb"));

        ed.delete(emp);
        assertThat(ed.getAll().size(), is(0));
    }

이런 테스트가 있는데, 콘솔 창에 쿼리를 봤더니.

Hibernate: insert into Emp (id, dept_id, name) values (null, ?, ?)
Hibernate: call identity()
Hibernate: select emp0_.id as id4_, emp0_.dept_id as dept3_4_, emp0_.name as name4_ from Emp emp0_
Hibernate: delete from Emp where id=?
Hibernate: select emp0_.id as id4_, emp0_.dept_id as dept3_4_, emp0_.name as name4_ from Emp emp0_

update문이 빠져있다. 여기서 발생하는 의문점이 한 두가지가 아니다.

1. DB에 update가 되지도 않았는데 테스트는 어떻게 통과한 것일까?

2. 왜 update 문은 날아가지 않은 것일까?

3. 역으로, 왜 insert와 delete는 날아간 것일까?

이 세 가지 의문을 해결하려면 위에서 작성한 코드를 좀 더 자세히 살펴볼 필요가 있다. 바로 ed.save(), ed.get(), ed.getAll(), ed.update(), ed.delete()  들이다. 이 녀석들이 어떻게 구현되어 있는지 보지 않고서는 알 수 없다. 또하나 Flush 모드 역시 알아야 한다.

- Flush 모드는 기본 모드인 AUTO를 사용했다.
- save(), get(), update(), delete()는 하이버네이트의 Session API와 동일하다고 생각하면 되며, getAll()은 다음과 비슷하게 구현되어 있다. session.createQuery("from Emp"); 실제로는 이 모든게 GenericDao 구현체에 들어있어서 약간 다르긴 하지만, 본질은 그렇다.
- 테스트는 @Transactional한 녀석으로 기본으로 rollback될 녀석이다.

자.. 이제 위 세가지 질문에 대답할 수 있을 것이다. 그랬다면, 다음 퀴즈도 덤으로 풀어보자.
update 쿼리를 볼 수 있는 방법은 현재 두 가지 정도가 떠오른다.

4. 위 테스트 코드에서 한 줄을 삭제하여 update 쿼리가 콘솔에 찍히게 해보자.

5. 위 코드에 ed.flush()를 어디에 추가하면 update문을 볼 수 있을까?

정답은 비공개.. 영원히..
신고
top


스프링 2.5 환경에서 하이버네이트 사용하기



참조 요약: Spring One 2008 Wokring With Hibernate in a Spring 2.5 Environment

스프링의 HibernateTemplate
(Hibernate 3.1 이전)

- 스프링이 관리하는 트랜잭션을 사용한다.
- 예외 번역 제공

public class HibernateClinic extends HibernateDaoSupport {
...
}

Native Hibernate DAO
(Hibernate 3.1+_

- 트랜잭션 훅(hook)을 제공한다.
- transactional session을 찾는 로직을 제공할 수 있게 한다. sessionFactory.getCurrentSession()
- 예외 번역 제공 @Repository PersistenceExceptionTranslation post processor
- 순수 하이버네이트 API만 사용할 수 있다.

단위 테스트 종류
- "Logical" 단위 테스트
- 통합 테스트

스프링으로 통합 테스트하기
- DI
- 애플리케이션 컨텍스트 로딩 줄이기
- 자동 롤백
- 하이버네이트 + JDBC 조합

AbstractTransactionalDataSourceSpringContextTests
- Ctrl + Shift + T ATDSSCT
- 애플리케이션 컨텍스트 로딩, 캐슁
- DI
- JdbcTemplate 제공

WAR에서 하이버네이트 VS OSGi 개발
- 일반 WAR 배포
  - SessionFactory가 클래스패스에 도메인 타입들을 찾는다.
  - 저장소(DAO, repositoru)들이 SessionFactory를 사용한다.
- OSGi 개발
  - 여러 도메인 번들로 쪼갠다.
  - SessionFactory는 도메인 타입에 접근해야 한다.
  - 저장소는 SessionFactory에 접근해야 한다.

OSGi 환경에서 하이버네이트
- "infrastructure" 번들
  - SessionFactory 서비스를 제공하고
  - 모든 도메인 타입을 가져온다.
- 여러 도메인 모델
  - 도메인 타입을 공개한다.
  - SessionFactory 서비스를 사용한다.
- Petclinic 예제를 제공한다.

=====

발표에서 다루는 내용이 너무 많아서 다소 산만했습니다. 하이버네이트만 집중적으로 다뤘으면 어땠을까 싶더군요. @Repository 얘기가 나오니까 컴포넌트 스캔 얘기로 새고.. 도메인 객체에 repository 객체 주입하는 얘기가 나오니까 @Configurable이랑 aspectj 얘기로 새고.. 테스트 얘기 나오니까 Test Context 쪽으로 또 얘기가 새버렸다가 나중엔 다시 하이버네이트랑 OSGi 얘기 조금 꺼내고 끝~~

멋진건 54분이라는 짧은 발표 시간 중에도 저렇게 많은 내용과 중간 중간 전부 데모까지 보여줬다는 겁니다.
신고
top


하이버네이트, 스프링, 트랜잭션, OSIV(Open Session In View) 패턴

Spring/Chapter 12 : 2008.11.17 15:10


참조

No Hibernate Session bound to thread 에러로 시작한 OSIV 얘기
http://forum.springframework.org/archive/index.php/t-33082.html

논쟁에서 언급한 HibernateTemplate API
http://static.springframework.org/spring/docs/2.5.x/api/org/springframework/orm/hibernate3/HibernateTemplate.html

스프링의 OpenSessionInViewFilter API
http://static.springframework.org/spring/docs/2.5.x/api/org/springframework/orm/hibernate3/support/OpenSessionInViewFilter.html

하이버네이트 위키 OSIV
http://www.hibernate.org/43.html

손권남님의 OSIV 정리
http://kwon37xi.springnote.com/pages/1075048

InfoQ에서 Spring In Production 요약한 것
http://www.infoq.com/news/2007/11/spring-production

댓글에 보면 OSIV 패턴이 안티 패턴인가에 대한 내용이 있음
http://blog.springsource.com/2007/11/09/download-the-spring-in-production-white-paper/

Tip4에서 OSIV 패턴은 안티 패턴이라고 한다.
http://www.realsolve.co.uk/site/tech/orm-performance.php

이미 세션이 닫힌 상태에서 뷰에서 Detached 객체에서 아직 로딩하지 않은 객체에 접근하면 에러(LazyInitializationException)가 발생합니다. 이 에러는 정상적인 에러죠. 하이버네이트 맵핑을 Lazy loading으로 최소한의 객체만 로딩하도록 하고 필요할 때 연관을 맺고 있는 객체를 추가로 가져오도록할 때 흔히 발생하는 아주 정상적인 에러입니다.

이런 상황을 극복하려면 필요한 객체를 미리 팻치(Eager Fatch)해둔 상태로 로딩하는 메소드를 DAO쪽에 구현해서 뷰에서 추가로 세션을 사용할 필요없이 뷰 랜더링을 마치도록 하면 되기도 하고..

아니면 OSIV 패턴을 사용해서, 인터셉터나 필터를 적용해서 뷰를 완전히 랜더링 할 때까지 세션을 열어 두는 겁니다. 그럼 뷰를 랜더링 하다가 필요한 객체가 있으면 세션을 사용해서 로딩할 수 있겠죠. 그럼 위의 방법처럼 DAO에 이른 팻치를 해서 가져오는 별도의 메소드나 쿼리를 만들지 않아도 되겠죠.

흠.. 하지만 뷰에서 자신도 예측하지 못한 쿼리가 계속 발생할 여지도 있고, 웹 단에서 데이터 캡슐화를 깨기 때문에 OSIV를 안티패턴으로 보고 사용을 최소화 해야 한다는 주장도 있습니다.

스프링 + 하이버네이트 조합을 쓸 때 OpenSessionInViewFilter를 자주 사용하는데, 이 API는 아주 잘 봐둘 필요가 있습니다. 그 중에서 가장 주목해야 할 부분은 다음과 같습니다.

1. 기본 플러시 모드는 FlushMode.NEVER입니다. NEVER는 deprecated 됐는데, 아직 사용하고 있습니다. 이 걸 왜 주의해야 하냐면, OSIV를 등록하고 만약 서비스 계층에 @Trasaction을 설정하지 않았다고 해봅시다. 그럴 때 트랜잭션 처리는 됩니다. 왜냐면 OSIV 때문이죠. 요청이 들어오면 해당 요청을 처리할 쓰레드에 새로운 세션 만들고 그걸로 트랜잭션 만들어서 쓰기 때문에 트랜잭션은 있는데 문제는 플러시 모드가 NEVER라서 명시적으로 flush()를 호출하기 전까지는 절대로 DB에 반영이 안 됩니다.

2. Conversation을 하나의 세션을 늘리는 방법(extending a sesion for conversation)으로 구현할 경우에, persistence 객체의 reassociation을 최대한 요청 처리 초기에 해야 합니다. 안 그럼 나중에 동일한 객체가 또 있다고 충돌 날 수가 있습니다. 이 문제에 대응하기 위해서 singleSession이라는 옵션에 false 값을 줄 수도 있지만, 그렇게 하면 매 요청 마다 새로운 Session 만들어서 사용하겠다는 것이고 Conversation을 구현하는 다른 방법(detached 객체를 가지고 구현하는 방법)으로 구현해야 될 것 같습니다. 즉 코딩할 때 조금 주의해서 단일 세션으로 Conversation을 사용할 것이냐, 아님 단순하게 요청 마다 새로운 세션으로 만들고 객체들을 붙였다 띄었다 할 것이냐 인데.. 필요한 객체를 계속 하나의 세션에 들고 유지하는게 좀 더 효율적이지 않을까 싶네요.


신고
top


하이버네이트 Criteria 검색 쿼리 골치



select this_.id as y0_ from Post this_ where this_.board_id=?

select this_.id as id1_0_, this_.author as author1_0_, this_.board_id as board7_1_0_, this_.contents as contents1_0_, this_.created as created1_0_, this_.title as title1_0_, this_.updated as updated1_0_
from Post this_
where this_.id in (?, ?, ?, ?, ?, ?) and  lcase(this_.title) like ? or lcase(this_.contents) like ?

===============================================

select this_.id as id1_0_, this_.author as author1_0_, this_.board_id as board7_1_0_, this_.contents as contents1_0_, this_.created as created1_0_, this_.title as title1_0_, this_.updated as updated1_0_
from Post this_
where this_.board_id=? and (lcase(this_.title) like ? or lcase(this_.contents) like ?)

================================================

select this_.id as id1_0_, this_.author as author1_0_, this_.board_id as board7_1_0_, this_.contents as contents1_0_, this_.created as created1_0_, this_.title as title1_0_, this_.updated as updated1_0_
from Post this_
where this_.id in
(select this0__.id as y0_ from Post this0__ where this0__.board_id=?)
and (lcase(this_.title) like ? or lcase(this_.contents) like ?)

=================================================

셋 다 똑같네.. 아.. 이런 ㅠ.ㅠ OTL... 뭐야 ㅠ.ㅠ 어떻게 짜야되지. 흠...

(lcase(this_.title) like ? or lcase(this_.contents) like ?)

이 부분이 잘못 됐나? title에 ?가 있거나 contents에 ?가 있는 것들 검색하려는데 분명 테스트 데이터에는 두 개가 나와야 되는데 계속 한 개만 나오네. 흠... 모르겠네... 모르겠어..

=====================
id | title       | contents
1  | keesun | toby
2  | toby      | toby
3  | toby      | keesun
=====================

이 상태에서 서브 쿼리나 in 뒤에 담겨있는 id는 전부 1, 2, 3이라고 치고... titke이 keesun이거나 contents가 keesun이 row를 갖다 달라는 거자나.

그럼 1번 row를 보면 title이 keesun이니까 맞자나. title이 keesun 이자나 contents는 아니고 그럼 일단 하나.
그리고 3번은 contents가 keesun이니까 맞고..

그럼 결과가 2개가 되야 하는데

결과는 1번만 가져오는... 이 상황은 ..;;; 어렵네~
신고
top


@Repository를 쓴다면 하이버네이트 예외 변환기 직접 만들 필요 없습니다.

Spring/Chapter 12 : 2008.11.12 10:52


스프링이 2.0부터 제공하고 있었는데, 이제서야 알게 됐습니다. 저는 그동안 뭘...;;;; 한 거죠.. ㅋㅋㅋ 이 것 참..;; 혹시 저만 빼고 다들 알고 계셨던건 아니겠죠? 저는 게다가 항상 @Repository 애노테이션을 쓰고 있었거든요. 그런데도 몰랐습니다. @Repository 애노테이션 API에도 안 나와있네요.

하이버네이트 예외 변환기가 언제 필요하냐면.
1. 하이버네이트 DAO 구현을 스프링 API에서 독립적으로 구현하고 싶을 때. 다른 말로 하이버네이트 SessionFacotry를 직접 주입해서 사용하고 싶을 때.
2. 하이버네이트 예외를 스프링 DataAccessException으로 변환하고 싶을 때.

위 두 가지 조건이 만족한다면 사용하고 싶을 겁니다. 그럴 때 만약 @Reposity라는 애노테이션으로 빈 스캔을 사용해서 빈을 등록하고 있다면 예외 처리기를 만들 필요 없이 빈 하나만 등록해주면 끝납니다.

<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

끝~ 저 클래스는 이름이 암시하듯이 스프링의 빈 포스트 프로세서로, 등록된 빈 중에 @Repository가 붙어있는 빈에 persistenceExceptionTranslationAdvisor 이런 어드바이저를 적용한 프록시 빈으로 교체해주는 멋진 녀석입니다. AOP와 빈 포스프 프로세서의 조합. 캬오... 스프링엔 이렇게 멋진 코드가 곳곳에 숨어있군요.

핵심코드 감상 하기...
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> targetClass =
                (bean instanceof Advised ? ((Advised) bean).getTargetSource().getTargetClass() : bean.getClass());
        if (targetClass == null) {
            // Can't do much here
            return bean;
        }
       
        if (AopUtils.canApply(this.persistenceExceptionTranslationAdvisor, targetClass)) {
            if (bean instanceof Advised) {
                ((Advised) bean).addAdvisor(this.persistenceExceptionTranslationAdvisor);
                return bean;
            }
            else {
                ProxyFactory proxyFactory = new ProxyFactory(bean);
                // Copy our properties (proxyTargetClass etc) inherited from ProxyConfig.
                proxyFactory.copyFrom(this);
                proxyFactory.addAdvisor(this.persistenceExceptionTranslationAdvisor);
                return proxyFactory.getProxy(this.beanClassLoader);
            }
        }
        else {
            // This is not a repository.
            return bean;
        }
    }

위 코드는 유겐 휄러와 로드 좐슨이 작성한 코드입니다.

ProxyFactory를 사용해서 직접 프록시 만드는 방법은 어제 올린 스크린 캐스팅에 포함되어 있었죠. Advised 인터페이스에 대해서도 언급 했었구요. 스프링 AOP 스캐를 보신 분들이라면 무리 없이 이해하실 수 있을 겁니다. 귿~
신고
top


하이버네이트가 문제인가요? 자신이 문제인가요?

모하니?/Thinking : 2008.11.03 12:12


"람보르기니가 있는데 운전 면허증이 없습니다. 그래서 람보르기니를 못타고 다니는데 그게 람보르기니 때문인가요 저 때문인가요."

저는 국내 대형 사이트나 금융권을 비롯해서 흔히 말하는 SI 환경을 경험해 본적이 없습니다. 지금이 11월이니까 이제 개발자로 일한지 거의 1년이 되어갑니다. 그동안의 개발 경험은 금융관련 컨설팅 회사에 아키텍트로 들어갔다가 두 달만에(사실은 한 달만에 냈지만, 한 달은 처리 기간) 사직서를 내고, 지금 있는 회사로 와서 조그마한 유통업에서 사용할 웹 애플리케이션을 사부님한테 배워가면서 스프링, 하이버네이트로 만들었고, 그걸 만드는 과정 중에 얼마전에 공개했으나 별로 빛을 못보고 있는 OSAF를 뽑아냈습니다.

저는 대형 사이트니 초대형 엔터프라이즈니 하는 것에 대한 경험은 없지만 스프링, 하이버네이트를 좋아하고 그만큼 공부도 하고 있고 실무에 적용도 해봤습니다. 한 번은 하이버네이트 버전을 올려놓고 수정할 것들을 제대로 하지 않아서 사부님이 밤을 새면서 버그를 잡은 적도 있습니다.

이런 제가 감히 하이버네이트를 대형 싸이트에서도 쓸 수 있다느니 없다느니 주장하는건 좀 웃긴 일이겠죠. 그래서 전 그런 주장을 하기 보다는 대형 프로젝트나 SI에서 일하시는 분들에게 물어보곤 합니다.

"하이버네이트도 괜찮은것 같은데 왜 iBatis를 쓰고 계신가요?"

답변은 굉장히 다양합니다. 그 중에서 어느정도 공감이가고 그럴 만하다고 생각하는 답변들은 다음과 같습니다.

- 한글로된 참고자료가 없다.
- 개발자가 하이버네이트를 모른다.
- 기술 리더가 iBatis는 익숙한데 하이버는 모른다.
- 레거시 DB를 써야 하는데 정규화가 엉망이다.

이 중에서 마지막 것만 기술적인 문제처럼 보이는데, 사실은 정규화를 제대로 안 했다는 건 DBA가 제 역할을 못한 거라고 볼 수 있으니 결국엔 하이버 문제라기 보단 DB쪽 관계자들의 문제입니다.

한국인이 꼭 한글로된 레퍼런스만 읽어야 한드는 법이 있는 것도 아닌데, 한글로 된 자료가 없다는 핑계를 계속 하는 건 자신의 역량을 제한하고 스스로를 가두는 일밖에 안 된다고 생각합니다. 하이버네이트 관련 자료는 온라인에서 정말 쉽고 많이 찾을 수 있습니다. 그리고 좋은 책도 있죠.(지금 번역도 하고 있습니다.) 그런 것들을 안 본다는 건 그만큼 노력해야 할 필요성을 못 느낀 것 뿐이고, 한글 레퍼런스가 없어서 공부를 못한다는 건 좀 핑계라고 생각합니다. 그래서 이것도 역시 개발자의 문제인거지 하이버네이트의 문제는 아닙니다. 당장 구글에서 hibernate와 iBatis로 검색을 해서 어떤 단어의 검색 결과가 더 많은지 눈으로 직접 확인해보시기 바랍니다.

다음으로 개발자가 좋은 기술이 있지만 몰라서 못쓴다. 이것 역시 하이버네이트의 문제가 아니죠. 개발자의 역량 문제입니다. 하이버네이트를 완전히 마스터 해야 할 필요까진 없겠지만, 적어도 HQL이 날아가면 하이버가 만들어 주는 SQL이 대강 어떠 어떠한 걸꺼라는 예측을 할 수 있는 정도까지만 공부를 하면 될 듯 합니다. 그 뒤에 최적화나 캐슁은 DBA나 소수의 기술 리더가 책임지면 되겠죠. 어쨋거나 이 문제 역시 하이버네이트의 문제는 아닙니다. 개발자가 공부를 안 한게 문제지..

마지막 기술 리더는 프로젝트에서 어떤 프레임워크를 사용 할지 결정할 때 지대한 영향을 주며, 개발자들에게 기술을 전파할 의무가 있다고 생각합니다. 그런 점에서 기술 리더는 현재 프로젝트에 가장 적합한 기술을 객관적으로 평가하고 그 중에서 선택을 해야 하는데 자신이 하이버네이트를 몰라서 자신에게 익숙한 iBatis를 선택한다는 건 물론 타당합니다. 그래야겠죠. 하이버네이트를 쓰라고 해놓고 자신이 못 도와주면 안되니까요. 하지만 만약 그 프로젝트에 하이버네이트가 더 적합한데도 그렇게 선택할 수밖에 없었다면 올바른 선택이긴 하지만 기술 리더에겐 분명히 잘못이 있는거라고 생각합니다. 물론 기술을 잘 모르는 고객이나 개발자들은 잘 모르겠죠. '저 사람이 기술 짱이니까. 저사람 선택이 맞는거겠지'..라고 안일하게 생각하는 개발자 자신의 잘못도 크지만, 이런 인식을 퍼지게 내버려 둔 잘못은 더 큽니다. 오히려 솔직하게 고객이나 개발자에게 지금 프로젝트는 하이버네이트가 더 좋아 보이는데 내가 잘 몰라서 도입을 못하겠다. 이렇게 말하는 기술 리더가 있다면 그 사람이 진정한 기술 짱입니다. 현재는 아니지만 미래의 진정한 짱이 될 수 있겠죠.

윗 글 단락들의 크기로만 봐도 제가 생각하는 문제 원인의 비중을 짐작할 수 있으시겠죠? 맞습니다. 저는 기술 리더가 가장 큰 책임이 있고, 그 다음은 개발자, 그 다음 정규화 제대로 안하는 DBA 순으로 생각하고 있습니다.

저는 위에서 생각한 저 네 가지 답변 말고 하이버네이트 기술 자체를 가지고 올타 그르다를 논하는 답변을 들으면 그 주장에 대한 증거를 찾고 반론에 대한 기술적인 증거를 찾기 전에 일단 이런 의문부터 생깁니다.
외국의 유명한 업체들의 개발자도 하이버네이트를 쓰고 있다. 그 사람들은 바보가 아니다. 그 사람들이 진행하는 프로젝트의 규모가 작은 것도 아니다. 그 사람들도 레거시 DB를 쓰고 있다. 하이버네이트를 iBatis 못지 않게 아주 많이 잘 쓰고 있다. 하이버네이트에 기술적인 결함이 있었다면, 그 사람들이 왜 쓰고 있는거지.. 말도 안돼. 개빈 킹이 가만 있을 만한 성격도 아닌거 같은데.. JBoss에서 노는 것도 아닐텐데 말이지..
반면에 같은 질문을 했을 때 가장 대표적인 기술적인 답변들은 다음과 같습니다.

- 테이블 중심이라 블라 블라..
- 레거시가 블라 블라..
- DB에 특화된 쿼리가 블라 블라..
- SQL을 직접 블리 블라..
- 성능이 블라 블라..

말이 안 되자나요. 저런 문제들이 있는데 유명 해외 업체 개발잘들은 무슨 깡으로 하이버네이트를 쓸까요. 이번 12월에 Spring One America에 가서 직접 물어보고 오겠습니다.

"What kind of framework is used for your persistence layer?"

"How many projects have you experienced with Hibernate?

"Have you ever used Hibernate with legacy DB? Is it really too hard to do that?"

"What about performance?"

"What do you think about Hibernate and iBatis?"

물론 이 질문들은 덤일 뿐이고, 스프링 관련 질문도 많이 하고 와야죠.  가기 전에 S1A에 가서 질문할 것과 말할 것들을 미리 정리해 둬야겠습니다.
신고
top


스프링에서 하이버네이트와 JDBC 같이 사용할 때 트랜잭션 처리는?

Hibernate/etc : 2008.10.08 16:23


별로 할 일이 없습니다.

PlatformManager는 하이버네이트가 사용하는
org.springframework.orm.hibernate3.HibernateTransactionManager

이걸 그대로 사용하면 되고, JDBC 코딩을 할 때는 그냥 JdbcTemplate을 사용하면 알아서 트랜잭션이 적용됩니다.

그런데 만약에 JdbcTemplate을 사용하지 못하고, DataSource를 직접 사용해야 할 경우에는 다음과 같이 TransactionAwareDataSourceProxy를 사용하면 된다고 합니다.

<bean id="rawDataSource" class="whatover youuse"/>

<bean id="dataSource" class="TransactionAwareDataSourceProxy">
  <constructor-arg ref="rawDataSource" />
</bean>

DataSource를 직접 사용하는 코드가 엄청나게 많아서 손을 못댈 경우에는 저렇게 dataSource를 스프링이 관리하는 트랜잭션을 알고 있는 데이터소스로 바꾸면 된다고 하는데, 해보진 않았습니다. 제가 해보고 싶었던 건 하이버네이트가 flush()를 하지 않은 데이터에 대한 JDBC쿼리로 인한 예외 상황인데...

테스트를 잘못 짠건지.. 잘 안 되더군요.

@Repository
public class MemberDao {

    @Autowired
    SessionFactory sessionFactory;
   
    SimpleJdbcTemplate jdbcTemplate;
   
    @Autowired
    public MemberDao(DataSource dataSource) {
        jdbcTemplate = new SimpleJdbcTemplate(dataSource);
    }
   
    public void add(Member member){
        sessionFactory.getCurrentSession().save(member);
    }
   
    public int update(Member member){
        return jdbcTemplate.update(
                "UPDATE Member SET age = ? WHERE name = ?", member.getAge(), member.getName());
    }

}

이런 DAO를 만들었습니다. add()는 하이버네이트로하고 update()는 JdbcTemplate으로 했습니다.

@Service
@Transactional
public class MemberService {

    @Autowired
    MemberDao memberDao;
   
    public void foo(){
        Member member = new Member();
        member.setName("keesun");
        memberDao.add(member);
       
        member.setAge(20);
        memberDao.update(member);
    }
   
}


그리고 서비스 코드는 저렇게 트랜잭션 처리를 하고, keesun이라는 객체를 하나 만들어서 저장하고, 나이를 추가한다음에 JDBC로 update문을 날립니다.

제가 원했던 결과는..

에러가 나는 겁니다.

그러나..

Hibernate: select nextval ('hibernate_sequence')
Hibernate: insert into Member (age, name, id) values (?, ?, ?)
Hibernate: update Member set age=?, name=? where id=?

에러가 나질 않고, 너무도 자연스럽게 동작해버려서 당황했습니다. 특히 마지막 줄의 쿼리는 제가 JdbcTemplate으로 날린 쿼리랑은 완전 다른 하이버네이트가 만든 쿼리가 날아갔습니다. 이게 대체;;; 무슨 일인지..  흠..

결과적으로는 아~무 걱정없이 하이버네이트랑 JdbcTemplate을 같이 사용할 수 있다는 것이지만, 제가 원했던 상황이 발생하지 않아서 좀 우울합니다.
신고
top


@CollectionOfElements 애노테이션

Hibernate/Annotation : 2008.09.19 11:31


참조
http://www.hibernate.org/hib_docs/annotations/api/org/hibernate/annotations/CollectionOfElements.html

Entity 타입 콜렉션 말고 Value 타입 콜렉션을 맵핑할 때 사용하는 애노테이션 입니다.

    @CollectionOfElements
    private List<Integer> hobbies;

간단하죠. JPA는 아니고 하이버네이트 애노테이션입니다. fetch와 targetClass 속성을 가지고 있는데, targetClass는 콜렉션 타입을 명시하지 않았을 때 사용하면 됩니다.

신고

'Hibernate > Annotation' 카테고리의 다른 글

@CollectionOfElements 애노테이션  (0) 2008.09.19
Table 애노테이션  (0) 2008.06.19
JPA @Transient  (0) 2007.09.27
JPA @JoinColumn  (1) 2007.09.22
JPA @ManyToOne  (0) 2007.09.22
JPA @OneToMany  (0) 2007.09.22
top


하이버네이트 VS JPA

Hibernate/Chapter 10 : 2008.09.09 22:28


 하이버네이트
JPA
 Transaction API를 사용하여 JDBC와 JTA를 사용할 수 있다.  EntityTransaction API는 resource-local 트랜잭션을 사용할 때에만 유용하다.
 하이버네이트 JTA랑 EJB의 CMT를 연동할 때 사용할 수 있다.  Java SE와 Java EE 에서 DB 커넥션 이름이 바뀌는 것 빼고 추가적인 설정은 필요없다.
 하이버네이트 automatic versioning을 사용해서 optimistic concurrency control을 제일 효율적으로 제공한다.  자동 버전관리로 낙천적인 동시성 관리하는 것을 표준화 했다.


신고
top


하이버네이트를 이용한 DAO 코드에서 int 타입의 리턴값과 SQLException은 무슨 의미가 있을까...

Hibernate/study : 2008.09.08 11:12


JDBC로만 코딩을 해오신 분에게 하이버네이트를 사용하여 만들 DAO에 필요한 메소드들을 정의해서 커밋해달라고 부탁했습니다. 그리고 다음과 같은 코드를 받았습니다.

public int update(final Task task) throws SQLException {
..
}

public int delete(final Task task) throws SQLException {
...
}

객체와 레코드가 맵핑되어 있는데 그 수를 셀 필요가 있을까요?

이 코드는 하이버네이트가 그 동안 저에게 얼마나 많은 도움을 줬는지 알게 해주는 코드였습니다. 상대방과 위에서 정의한 리턴값의 용도와 의도를 물어보고, 하이버네이트의 update와 JDBC update 개념이 다르다는 것과 하이버네이트 최신 버전은 모두 RuntimeException을 던진다는 이야기를 해줬습니다. 자세한 내용은 생략하겠습니다. 이미 이전에 하이버네이트의 update에 대해 장문의 글을 쓴적도 있고, RuntimeException에 대해서는 말할 필요도 없을테니까요.

성실한 누군가를 교육한다는건 정말 재밌는 일입니다. 기회가 되면 스크린캐스팅을 해서 올리고 싶은데, 실제 나가는 진도에 비해 공백이 너무 커서 스크랜캐스팅 편집도 해야 되는데 도무지 그럴 짬은 안나네요 ㅋ
신고
top


하이버네이트 3.3.0 GA 릴리즈~

Hibernate/etc : 2008.08.16 11:04


http://in.relation.to/Bloggers/HibernateCore330GoesGA

주요 변경 사항
1. 빌드를 Maven기반으로 변경.
2. 여러 모듈 jar로 세분화 함.
3. 2차 캐시 SPI 재설계.
4. JBossCache 2.x를 2차 캐시 프로바이더로 통합.

http://www.infoq.com/news/2008/08/hibernate-33

대충 읽었는데, OSGi가 유명해 지면서, OSGi에서 발생하는 하이버네이트 문제들(SessionFactory의 동적인 변경, 클래스로딩 이슈)에 대해 알고는 있는데, 이번 배포판에서는 아직 해결이 안 됐다고 합니다. 하지만, 적극적으로 해당 이슈들을 모두 해결할 의사는 있다고 합니다. 시간 문제라는거죠. 캬~ 좋아 좋아.
신고
top


하이버네이트 Criteria 다루기 - 중복일까 아닐까

모하니?/Coding : 2008.07.30 13:31


    @Override
    public List<T> search(P params, OrderPage orderPage) {
        // total rowcount
        orderPage.setRowcount((Integer) (addRestrictions(
                getSession().createCriteria(this.persistentClass), params)
                .setProjection(Projections.rowCount()).uniqueResult()));

        // pages list
        Criteria c = addRestrictions(getSession().createCriteria(
                this.persistentClass), params);
        orderPage.applyPage(c);
        orderPage.applyOrder(c);

        return c.list();
    }

    /**
     * template method for search
     *
     * @param c
     * @param params
     * @return
     */
    protected Criteria addRestrictions(Criteria c, P params) {
        return c;
    }

위 코드에서 중복이 보이시나요? 안 보이신 다구요?

    @Override
   public List<T> search(P params, OrderPage orderPage) {
       // total rowcount
       orderPage.setRowcount((Integer) (addRestrictions(
               getSession().createCriteria(this.persistentClass), params)
               .setProjection(Projections.rowCount()).uniqueResult()));

       // pages list
       Criteria c = addRestrictions(getSession().createCriteria(
               this.persistentClass), params);
       orderPage.applyPage(c);
       orderPage.applyOrder(c);

       return c.list();
   }

어떤가요. 중복 이죠? 그러나..

Criteria c = addRestrictions(getSession().createCriteria(
               this.persistentClass), params);
orderPage.setRowcount((Integer) (c.setProjection(Projections.rowCount()).uniqueResult()));
orderPage.applyPage(c);
orderPage.applyOrder(c);

대강 이런 식으로 리팩터링 해보면 하이버네이트는 요상한 쿼리와 함께 에러를 뱉어냅니다.

전체 Row 갯수를 반환하는 Criteria(쿼리는 select count(*).. )이런식으로 시작)를 다시 Order와 Page 처리를 할 때 사용하면 이상한 쿼리(select count(*).. order by ... 이게 이상한 이유는 order by에서 사용한 컬럼이 group by에 있어야 하는데 groupd by를 정의한 적이 없거니와, 사실 두 번째 쿼리는 count(*)가 없어야 하는데 앞에서 만들어둔 Criteria에 이어 붙인 꼴이 되어서 이상해졌습니다.)가 되버립니다.

중복처럼 보이지만 제거하면 코드가 깨지는... 요상한 경우. 이거 어떻게 처리하는게 좋을까요? 전 요리 조리 해보다가 그냥 뒀습니다.
신고
top




: 1 : 2 : 3 : 4 :





티스토리 툴바