[하이버네이트] Hibernate Core 3.6.0.Final 나왔구나

참조: http://in.relation.to/Bloggers/HibernateCore360FinalRelease

하이버네이트가 코드를 Github로 옮기고 나서 첫 배포라고 합니다. 주요 내용은 다음과 같습니다,

– JDK 1.4 지원 중단

– hibernate-jmx와 hibernate-anntation을 hibernate-core 내부로 통합함

– 증진된 타입 지원, HHH-5138

– DTD 호스팅 변경, HHH-5485

– 새로운 시작문서를 비록한 문서 개선, Getting Started Guide

– 구분자, 컬럼 수준의 읽기/쓰기 표현식, 타입스탬프 버전 등을 지원하는 몇몇 애노테이션 개선

– 히스토리 엔티티를 작성하는 방법에 대한 대안책으로 새로운 이벤트 기능(ValidityAuditStrategy) 제공. 아담의 블로그 글 여기여기에서 자세한 내용 참조.

[스프링 트러블슈팅] @Transactional이 이상해…

11월 초나 중순 쯤 KSUG에서 세미나를 한다길래 발표자로 지원했습니다. 예상 날짜는 11월 13인데 어찌될지는 아직 모릅니다. 제가 구상한 주제는 바로 “스프링 트러블슈팅”. 개구리 올챙이적 모른다고.. 저도 처음 스프링, 하이버네이트로 프로젝트를 시작했을 때 사소한 버그도 못잡아 토비님께 여쭤보곤 했었는데 요즘은 강의 다니면서 스프링 관련 문제들 찾아주고 해결해주는 재미가 쏠쏠 합니다. 잡아주기도 하고 잡는 방법을 알려드리기도 하는데… 그런 내용을 전달해 드릴계획입니다.

이번엔 그 중에 하나로 오늘 오전에 해결했던 문제 중 하나를 소개해 드릴까 합니다. 소개만…

[java]
public interface MemberService {

}
[/java]

[java]
@Service
@Transactional
public class MemberServiceImpl implements MemberService {

}
[/java]

[java]
@Controller
public class MemberController {
@Autowired MemberServiceImpl service;
}
[/java]

이렇게 설정한 상태에서 ApplicationContext를 만들다가 에러가 납니다. MemberController에 주입할 MemberServiceImpl 빈을 못 찾아서 MemberController의 @Autowired MemberServiceImpl 부분에서 에러가 납니다.

이 상황은 딱.. 예전에 토비님 블로그에 올라왔던 상황입니다. 토비님은 그 글에 대한 해답편도 올려두셨는데.. 해답은 토스3의 14장을 보라는 내용이었습니다. 지금 이 상황이 바로 그 퀴즈에 대한 정답이기도 하죠.ㅋㅋ

지금 이 상황에서 @Transactional을 지우면 에러 없이 ApplicationContext가 만들어 지지만 원하는 대로 동작하진 않겠죠. 트랜잭션이 필요했는데 그걸 못쓰는 것이니까요. 그럼 무엇이 문제이고 그 해결책은 무엇일까요. 객체지향적으로 생각하면 해결책은 쉽게 알 수 있는데 문제 원인을 남에게 설명할 수 있을 정도로 파악하기는 쉽지 않을 수 있습니다. 스프링 AOP에 대한 기본 지식을 잘 갖추고 있으시거나 토스3에서 AOP부분을 잘 읽으신 분들이라면 쉽게 알 수 있는 내용이긴 하죠.

저는 이 것과 더불어 스프링을 사용하시면서 자주 만나게 될 트러블들을 소개 및 재현해 드리고 그 원인과 해결책을 소개하는 시간으로 KSUG에서 뵙겠습니다. 아참.. 그리고 그날 제 발표 시간에 깜짝 이벤트도 준비 중이오니 많은 참석 바랍니다.

[하이버네이트] 계층 구조당 테이블 매핑하기

code by 제준&기선

이전에 포스팅 했던 봄싹 스터디 게시판 설계하기를 이제 도메인 클래스로 옮겨 코딩할 차례인데 일단 하이버네이트로 계층 구조를 매핑하는 방법이 몇가지 있는데 그 중에서 어떤 방법을 사용할지 선택해야 합니다. 저는 게층 구조당 테이블 하나를 쓰는 전략을 선택했습니다. 이유는 Join을 쓰지 않아서 성능도 좋고 다형성도 되니까요. 대신 매핑 방법이 좀 귀찮긴한데.. 사실 계층 구조를 매핑하는 것 자체가 다들 좀 귀찮게 생겨서 이건 딱히 이 방법만의 단점이라고 보기는 힘들다고 생각합니다. 그 외에는 컬런이 NOT NULL이면 안된다는 문제가 있는데.. DB 스키마 결정권이 도메인 설계자한테 있다면 이 부분도 그리 크게 문제되진 않을 것 같네요. 아니면 성능이냐 제약 조건이냐 인데.. 역정규화까지 하는 마당에 당연히 성능쪽을 선택하시겠죠.

[java]
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
name = "POST_TYPE",
discriminatorType = DiscriminatorType.STRING
)
public class Post {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;

@ManyToOne
@DomainInfo(descr="작성자")
private Member writer;
@DomainInfo(descr="제목")
private String title;
@DomainInfo(descr="내용")
private String content;
@DomainInfo(descr="작성일")
private Date createdAt;
@DomainInfo(descr="수정일")
private Date modifidedAt;

[/java]

이 클래스가 상위 클래스로 다른 TextPost나 ImagePost 등 다른 Post에 공통으로 들어가는 속성들을 가지고 있습니다. 여기서 @DomainInfo라는 애노테이션은 무시해도 됩니다. 봄싹 프로젝트 내부에서 도메인 정보를 알려주기 위해 만든 애노테이션이지 JPA나 하이버네이트 애노테이션이 아닙니다. 저기서 중요한건 @Inheritance와 @DiscriminatorColumn 입니다. 계층 구조당 테이블 매핑 방법에서는 여러 하위 클래스 타입을 구분할 구분자가 필요한데 그 역할을 할 컬럼을 지정해주는데 @DiscriminatorColumn이고 상속 매핑 방법을 알려주는게 @Inheritance 입니다. JPA2에서는 이 부분에 뭔가 변화가 생겼는지 어쩐지 한번 봐야할텐데….흠.. 머 일단은!

[java]
@Entity
@DomainInfo("이미지글")
@DiscriminatorValue("IMAGE")
public class ImagePost extends Post {

@Column(name = "IMAGE_URL")
private String url;

@OneToMany(cascade={CascadeType.ALL})
@OrderBy("created DESC")
@DomainInfo("댓글")
@Cache(usage= CacheConcurrencyStrategy.READ_WRITE)
private Set<Comment> comments;

}
[/java]

그럼 이제 저 상위 클래스를 상속받는 하위 클래스르들을 만들면 되는데 그 중 하나가 바로 이미지글을 나타내는 ImagePost이고 여기서만 사용할 속성으로 url이 있고 comments가 있습니다.

[java]
@Entity
@DomainInfo("일반글")
@DiscriminatorValue("TEXT")
public class TextPost extends Post {

@ManyToOne
private TextPost rootPost;

@OneToMany(mappedBy = "rootPost")
private Set<TextPost> branchPosts;

@OneToMany(cascade={CascadeType.ALL})
@OrderBy("created DESC")
@DomainInfo("댓글")
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE)
private Set<Comment> comments;


}
[/java]

이건 TextPost로 하위글, 상위글 매핑이 들어가 있어서 좀 더 복잡해 보일 수 있지만 매핑은 뭐 간단합니다. ImagePost와 TextPost에서는 @DicriminatorValue를 잘 봐야 하는데 저 값이 모든 레코드에 추가로 들어가게 되고 저 값으로 분기해서 각 엔티티 타입으로 가져올 수 있습니다.

자.. 문제는 매핑은 했는데.. 이게 잘 동작하는지.. 어떻게 확인하죠? @_@;; 그래서 테스트를 해봐야합니다. 책에 나온대로 했으니 잘 되겠지… 블로그에서 본대로 했으니 잘 되겠지.. 이렇게 낙천적인 분들이라면 모르겠지만.. 전 제가 써먹었던 방법인데도 좀 불안해서(하이버 버전 마다 동작 방법이 달라질 수도 있고, 매핑 방법이 달라질 수도 있고, DB마다 달라질 수도 있으니.. 도무지 불안합니다.) 테스트를 해봐야 됩니다.

[java]
@Repository
public class TextPostRepositoryImpl extends HibernateGenericDao<TextPost> implements TextPostRepository {

public List<TextPost> getRootPostList() {
return getCriteria().add(Restrictions.isNull("rootPost")).list();
}

public List<TextPost> getBranchPostsOf(TextPost textPost) {
return getCriteria().add(Restrictions.eq("rootPost.id", textPost.getId())).list();
}

public List<TextPost> getParentPostList(int start, int end) {
return getCriteria().addOrder(Order.desc("createdAt")).list();
}

}
[/java]

우선 이렇게 GenericDAO를 사용해서 간단하게 TextPostRepository를 하나 만들어 놓고.. 제준군이 필요한 기능을 추가했군요.

[java]
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testContext.xml")
@Transactional
public class TextPostRepositoryImplTest {

@Autowired TextPostRepository repository;

@Test
public void di(){
assertNotNull(repository);
}

}
[/java]

제가 보통 제일 먼저 작성하는 테스트는 저렇게 테스트할 빈을 잘 가져오는지 확인하는 겁니다. 저걸로 뭘 확인하는거지? 당연히 빈으로 만들질 테고 @Autowired가 되겠지.. 라고 생각하실 수 있지만.. 사실 저 자체로 엄청나네 많은 것들이 테스트 됩니다. 저 테스트는 단위 테스트가 아니죠. 스프링 ApplicationContext에서 만드는 수 많은 빈들이 제대로 만들어지느지 확인하는 엄청나게 큰 테스트 입니다. 자세한 내용은 ‘토비의 스프링3’에도 언급되어 있으니 확인해 보시기 바랍니다.

어쨋든 여기서 확인하고 싶은 내용은 sessionFactory가 잘 만들어지는지 입니다. 잘 만들어지면 일단 매핑 자체에는 별 문제가 없다는 거거든요. 만약 매핑에 뭔가 문제가 있다면 저 상태에서 sessionFactory가 안만들어져서 에러가 날겁니다. 최소한의 코드로 많은 문제를 파악할 수 있으니 상당히 유용한 테스트라고 볼 수 있죠.

저게 잘 되면 그 다음에 본격적으로 테스트를 작성합니다.

[java]
@Test
public void getAllAndGetRootPostsOnly() {
TextPost rootPost = new TextPost();
rootPost.setTitle("스프링의 장점은?");
repository.add(rootPost);

TextPost post2 = new TextPost();
post2.setTitle("스프링의 장점은? 2");
repository.add(post2);

TextPost post3 = new TextPost();
post3.setTitle("스프링의 장점은? 3");
repository.add(post3);

TextPost post4 = new TextPost();
post3.setTitle("Why not?");
repository.add(post4);

post3.addBranch(post4);

assertThat(repository.getAll().size(), is(4));
assertThat(repository.getRootPostList().size(), is(3));
}
[/java]

이 테스트는 제준이가 작성한 테스트를 제가 변수명이라던지 확인할 내용을 추가해서 조금 테스트를 고쳤습니다. 최상위 글만 가져오는 부분인데 제준이가 이 부분이 잘 안되다고해서 코드에 손을 댔는데 제준이가 시도했던 방식은 Join을 사용해서 계층 구조를 표현하는 방법이었는데 무슨 문제였는지는 모르겠네요. 매핑 방법을 바꿔버렸으니.. 흠;;

[java]
@Test
public void addReplyToParent() {
TextPost rootPost1 = new TextPost();
rootPost1.setTitle("스프링의 장점은?");
repository.add(rootPost1);

TextPost childPost1 = new TextPost();
childPost1.setTitle("복잡한 엔터프라이즈 개발 간소화");
childPost1.setRootPost(rootPost1);
repository.add(childPost1);

TextPost childPost2 = new TextPost();
childPost2.setTitle("높은 추상화를 통한 객체지향 프로그래밍 극대화");
childPost2.setRootPost(rootPost1);
repository.add(childPost2);

TextPost rootPost2 = new TextPost();
rootPost2.setTitle("스프링의 장점은? 2");
repository.add(rootPost2);

TextPost rootPost3 = new TextPost();
rootPost3.setTitle("스프링의 장점은? 3");
repository.add(rootPost3);

TextPost rootPost = childPost1.getRootPost();
List<TextPost> branchPosts = repository.getBranchPostsOf(rootPost);

String result = "";
for (TextPost textPost : branchPosts) {
result += textPost.getTitle();
}

assertThat(result, containsString(childPost1.getTitle()));
assertThat(result, containsString(childPost2.getTitle()));
}
[/java]

이번것도 제준이가 작성해놓 테스트를 조금 손본 것이고 이쯤에서 변수명이 좀 고민 되는게 있는데 TextPost의 하위글과 상위글을 나타내는 변수명을 첨에는 parent-child라고 했다가.. root-branch로 바꿨는데.. reply를 보니까 왠지.. 더 그럴듯하네요.

글이 좀 길어지지만 자르기는 귀찮고.. 마지막으로 다형성도 테스트 해보죠. 이번엔 DBUnit을 써서 위에처럼 테스트 픽스처들을 자바코드로 만들지 않고 XML로 만들어 넣은 상태에서 테스트 해보죠.

[xml]
<dataset>
<member id="1" email="dosajun@email.com" />

<post id="1" writer_id="1" title="스프링은 무엇인가염?" content="토스3책 보삼" post_type="TEXT"/>
<post id="2" writer_id="1" title="하이버네이트는 무엇일까요?" content="하이버완벽가이드 보삼" post_type="TEXT"/>
<post id="3" writer_id="1" title="배고파" content="뭘 먹을까.." post_type="TEXT"/>
<post id="4" writer_id="1" title="나 토스3 샀어" content="인증샷" post_type="IMAGE" />
<post id="5" writer_id="1" title="나 iMAC 샀어" content="인증샷" post_type="IMAGE" />

</dataset>
[/xml]

이렇게 총 글이 5개있는데, 이 중에서 TextPost가 3개, ImagePost가 2개 입니다. PostRepository, ImageRepository, TextRepository를 각각 만들었다 치고 다음과 같이 테스트를 만들 수 있습니다.

[java]
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testContext.xml")
@Transactional
public class PostRespositoryImplTest extends DBUnitSupport{

@Autowired PostResposiroty postResposiroty;
@Autowired TextPostRepository textPostRepository;
@Autowired ImagePostRepository imagePostRepository;

@Test
public void di(){
assertNotNull(postResposiroty);
assertNotNull(textPostRepository);
assertNotNull(imagePostRepository);
}

@Test
public void postType() throws Exception {
insertXmlData("testData.xml");

assertThat(postResposiroty.getAll().size(), is(5));
assertThat(textPostRepository.getAll().size(), is(3));
assertThat(imagePostRepository.getAll().size(), is(2));
}
}
[/java]

간단하죠. 이 방법의 장점은 테스트 코드가 간결해지고 테스트용 데이터를 편집하기가 쉽다는 것, 특히 Excel을 써서 테스트 데이터를 만들 수도 있는데 복잡한 테스트 데이터가 필요할 땐 XML 보단 Excel을 더 유용하게 쓸 수 있을 겁니다. 하지만 테스트 픽스처가 눈에 보이지 않는다는 건 단점입니다. 저 XML 파일로 이동해서 대체 어떻게 구성되어 있는지 확인을 해봐야되죠. 흠.. 그래서 되도록이면 테스트 픽스처 구성을 간략하게 주석으로 남겨놓는게 나중을 위해서 도움이 되리라 생각합니다. 물론 그 보다 더 자동화된 방식으로 테스트 픽스처를 그래픽으로 보여주는 방법이 있으면 좋겠지만… 말이죠.

훔.. 이 정도면 된 것 같네요. 끝!!

[하이버네이트 3.5] Fetch Profile

참조: http://blog.frankel.ch/hibernate-hard-facts-part-6

기본으로 Xxx-To-Many 관계에서 Many 쪽 연관에 별다른 설정을 하지 않으면 lazy loading이 적요됩니다. 즉..

[java]
class Member {
private Set<Role>;
roles;

}
[/java]

이런 코드가 있을 때 만약 하이버네이트로 Member를 읽어오면 Member의 Roles까지 읽어오진 않습니다. 그러다 세션이 열려있는 상태에서 getRoles()를 호출하는 등으로 필요해지는 순간이 오면 그때 DB에 쿼리를 보내서 가져오게 되죠. 이렇게 필요해지면 가져오는걸 lazy loading이라고 하고 이렇게 하는 이유는 굳이 당장 필요없는 객체로 메모리를 잡아먹을 필요도 없기 때문입니다.

그런데 Roles를 Member 객체를 가져온 뒤에 자주 참조한다면 애초에 Member를 가져올 때 미리 가져다 놓고 캐싱을 해버리는게 더 좋습니다. 그럴때 사용하는 기능이 하이버네이트의 Fetch라는 것인데 FetchMode라고 해서 도메인 클래스에 애노테이션으로 또는 XML로 매핑을 할 때 이 모드를 설정해 둘 수 있습니다. LAZY는 위에서 설명했고 EAGER가 바로 LAZY와는 반대로 Member를 가져갈때 Roles도 전부 가져가라는 뜻이 됩니다.

그런데 이런 설정을 매핑에서만 설정할 수 있는게 아니라 Criteria나 HQL을 만들 때도 설정할 수 있죠. Critera.setFatchMode()를 사용하거나 FETCH JOIN이라는 HQL을 사용하면 됩니다. 그런데.. 매번 쿼리를 만들 때마다 이렇게 FetchMode를 설정하는게 귀찮을 수 있으니 하이버네이트 3.5에서는 미리 특정 관계에 대한 FetchMode를 정의해 놓고(펫치 프로파일) 쿼리를 만들 시점에 해당 그 팻치 프로파일을 가져다 쓸 수 있는 기능을 제공합니다.

[java]
@Entity
@FetchProfile(name = "customer-with-orders", fetchOverrides = {
@FetchProfile.FetchOverride(entity = Customer.class, association = "orders", mode = FetchMode.JOIN)
})
public class Customer {
@Id
@GeneratedValue
private long id;

private String name;

private long customerNumber;

@OneToMany
private Set<Order> orders;

// standard getter/setter

}
[/java]

from: http://docs.jboss.org/hibernate/stable/annotations/reference/en/html_single/#d0e3524

이렇게 orders는 기본으로 lazy loading이 적용되서 사실상 FetchMode=Lazy로 설정한 것과 동일하지만.. 쿼리를 만들 때 @FetchProfile에 정의한 걸 적용해서 마치 Eager로 설정한 것처럼 orders를 미리 읽어가는 쿼리를 만들 수도 있습니다.

[java]
Session session = …;
session.enableFetchProfile( "customer-with-orders" );  // name matches @FetchProfile name
Customer customer = (Customer) session.get( Customer.class, customerId );
session.disableFetchProfile( "customer-with-orders" ); // or just close the session

[/java]

from: http://docs.jboss.org/hibernate/stable/annotations/reference/en/html_single/#d0e3524

흠.. 이걸 처음 보고. 어차피 fetchMode 설정하는게도 한줄 필요하고 저기서도 fetch profile 때문에 한줄 필요하면 똑같은거 아닌가 생각했지만 fetchMode를 조정할 곳이 한군대로 축소된다는 장점이 있더군요. 여러군데에서 fetchMode를 조작하는 코드가 있다면 fetch profile을 써먹으면 괜찮겠네요.