[하이버네이트] 계층 구조당 테이블 매핑하기
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 파일로 이동해서 대체 어떻게 구성되어 있는지 확인을 해봐야되죠. 흠.. 그래서 되도록이면 테스트 픽스처 구성을 간략하게 주석으로 남겨놓는게 나중을 위해서 도움이 되리라 생각합니다. 물론 그 보다 더 자동화된 방식으로 테스트 픽스처를 그래픽으로 보여주는 방법이 있으면 좋겠지만... 말이죠.
훔.. 이 정도면 된 것 같네요. 끝!!