잊지 않겠습니다.

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


지금까지 Jdbc, Hibernate, MyBatis와 같이 DB에 접근하는 다양한 방법에 대해서 알아봤습니다. 

각각의 DB를 다루는 기술들은 다양한 장점과 단점을 가지고 있습니다. 특히 myBatis는 기존의 sql의 재사용 또는 sql query만으로 Business Logic을 만들던 개발자들이 쉽게 접근할 수 있다는 장점이 있는것 같습니다. 그리고 Mapper를 이용하는 경우, Dao에 대한 구현 객체 생성을 거의 하지 않아도 된다는 장점을 가지고 있고요. 또 다른 장점은 무엇이 있을까요? 이 부분에 대해서는 코드를 보면서 서로간에 토론을 해보는것도 좋을 것 같습니다.

지금까지 우리는 MVC 모델의 M에 대한 기술적인 접근을 알아봤습니다. DB에 sql적 접근을 하는 Jdbc는 좀 다른 영역이라고 할 수 있고, myBatis와 Hibernate는 결국은 Jdbc를 한번 wrapping 한 형태라고도 할 수 있습니다. 제가 한번 질문을 던져보도록 하겠습니다. myBatis는 ORM일까요?

myBatis는 ORM이 아닙니다. ORM은 DB가 갖는 relation 관계에 대한 객체화에 주목합니다. 그렇지만, myBatis는 DB에 대한 query값에 대한 표현에 주목하고 있는 형태의 query에 대한 관리 포인트에 중점을 두고 있습니다. 이 둘의 차이는 조금 있습니다. 이 부분에 대해서 다시한번 알아보도록 하겠습니다.
Hibernate에서 사용된 ORM 객체들은 Entity object라고 불리웁니다. 그렇지만, myBatis의 객체는 VO, DTO라고 불리웁니다.
이에 대한 정의는 다음과 같습니다.

Entity Object
프로세스 상에 존재하는 데이터 개체로 물리적이거나 추상적인 것을 모두 포함합니다. 실제 DB의 코드에서 동작이 가능한 객체입니다. 지금 저희가 만든 예제에서는 Book, User, UserHistory 등이 각 객체로서 동작할 수 있습니다. 객체는 method를 가질 수 있으며, 생명 주기 및 객체간의 관계를 갖습니다.

VO(Value Object), DTO(Data Transfer Object)
단순히 값을 저장하는 객체입니다. Layer에서 Layer로 값을 전달할 때 주로 사용이 되며, 값에 대한 재 사용이 이루어지지 못합니다. 실제 Object에 대한 Model을 표현하기보다는 데이터를 저장하고, 전달하는데에 초점이 맞춰져 있습니다. VO, DTO를 사용하기 보다는 Map을 이용한 Dictionary 구조의 객체를 사용해도 동일한 결과를 가지고 올 수 있습니다. Entity Object를 화면에 보이기 위해서 또는 DB의 특정 query 결과를 다른 Layer에서 사용하기 위해 주로 사용됩니다.
차이를 아시겠나요? 단순히 Hibernate에서는 DB에 대한 값의 전달 뿐 아니라, 객체의 주기와 객체와 객체간의 관계까지 가지고 있는 Instance되어 있는 객체로서 움직입니다. 그렇지만, VO에서는 단순한 query값에 대한 return값을 의미하게 됩니다. 이 부분의 차이는 매우 큽니다. VO로 주로 작업을 하게 되면 결국은 절차 지향적 프로그래밍으로 동작하게 됩니다. Flow가 흐르듯이 method1 call, method2 call 식으로요. 그렇지만, 객체를 이용하게 되는 경우, 객체간의 상호 동작에 의하여 프로그램이 개발이 되게 됩니다. 이 두 객체의 사용 문제는 국내에서만 뜨거운 감자입니다. 외국은 이미 Entity가 평정을 한 상태이지만, 국내는 무척 말이 많군요. 전에 Daum에서 조사한 결과로는 해외는 8:2, 국내는 2:8 수준이라고 했습니다. 이 부분에 대한 논쟁을 따로 정리해봤습니다. 코드를 한줄 더 짜는 것보다, 이 부분에 대한 내용을 한번 정리해보는 것은 개발자들에게 있어서 굉장히 중요한 시간입니다. 다들 읽어보시고, 같이 이야기해보는 시간을 갖도록 하겠습니다.


Entity vs DTO, VO - 국내의 myBatis와 Hibernate 사용 논쟁에 대해서.


myBatis, Hibernate에 대한 사용빈도를  명확하게 측정할 수는 없지만, 검색 빈도로 간접적으로 사용빈도의 %를 알아볼 수 있습니다. 







그리고, 개발 기술에 대한 Trend를 한눈에 보기에 편한 indeed 사이트의 job trend입니다.  (참고삼아 PowerBuilder도 한번 넣어봤습니다.)
먼저, java와 C#에 대해서 알아보기 위해서 한번 indeed.com에서의 결과를 확인해보도록 하겠습니다.


전세계적으로도 역시 java가 C#보다 2배 이상의 우위를 갖는 것을 알 수 있습니다. 여기서 파란 라인이 Hibernate입니다. 밑에 0점에 깔려 있는 애들은 myBatis, iBatis, JPA를 의미하고요.

자, 이제 Hibernate와 myBatis, iBatis, JPA, powerbuilder를 비교해보도록 하겠습니다. 


일단 JPA가 너무 낮은 것에 대해서는 전문가들의 의견은 JPA가 Hibernate를 기반으로 구성이 되는 것이 일반적이기 때문에 JPA 대신에 Hibernate를 적는 경우가 많기 때문이라는 말을 하고 있습니다. 이제 Hibernate, iBatis, myBatis를 한번 비교해보도록 하지요. 이 정도면 해외에서는 iBatis와 myBatis는 거의 죽은 기술입니다. 그런데 왜 국내는? 이라는 것은 상당한 논쟁을 가지고 옵니다. 이 부분에 대한 java community에서의 논쟁입니다. 

IBatis와 Hibernate 기술적 트렌드에 대한 작은 생각입니다.
제가 알고 있기에는 세계적으로는 HIbernate가 대세이고
한국은 IBatis가 대세 정도로 알고 있습니다.
물론 제 개인적인 생각일수도 있을것 같습니다.
제 생각에는 Hibernate 프레임웍의 특성을 보면 비교적 잘 설계되고 복잡도가 상대적으로 높은편인것 같고,
반면에 IBatis는 기능은 간단하지만 SQL을 직접쓰기가 편리하고,
많은 학습없이 쉽게 사용할 수 있는것 같습니다.
그래서 전 여러분은 이 두가지 프레임웍크에 대한 생각을 듣고 싶습니다.
여러분이 이 두가지 프레임웍을 선택 하실 기회가 되거나 선택하셔야 한다면 어떤것을 왜 선택하시겠습니까?

단연 Hibernate 아닐까요?
iBatis는 2년 정도 사용했습니다만 효용성으로 따져서는 Hibernate가 압도적인거 같습니다.
진짜 개발자의 칼퇴근을 도와주는 도구는 Hibernate라고 생각되네요.
Hibernate가 초기 학습비용이 크지만 개발자 전원이 모두 알아야 할 필요는 없다고 생각됩니다.
몆몆 사람들이 Db설계와 매핑을 해준다면 나머지 분들이 도메인 설계를 하면 됩니다.
문제는 Hibernate 매핑을 다루는 사람이 고객의 신뢰와, DBA급의 권한과, Hibernate를 이해하며, 클래스설계에도 익숙해야 할듯 한데
한국에서는 그게 쉽지 않을듯 하네요.

현재 스프링으로 프로젝트를 진행중인데 Spring 2.5.6 + iBatis 조합으로 개발하고 있습니다.
초기에 Hibernate도 고려해봤는데 아직 사용법에 익숙하지 않고 여러개의 테이블을 조인해야 되는 상황에다
또 다양한 통계쿼리를 직접 작성해야 될 경우가 많아서 iBatis를 선택했습니다.
iBatis만 선택해도 어느정도의 장점을 누릴수 있지만 진정한 OR Mapping의 장점을 얻으려면 Hibernate가 해답일 거라는 생각이 드네요.
물론 DB설계가 깔금해야 되고 관련 framework에 대한 지식이 있다라는 전제가 깔려야 겠지요.
OR Mapping과 Sql mapping은 완전 개념이 틀린 것 같습니다.

약간 지나친 생각일 수도 있겠으나 전 iBatis를 쓸 꺼면 그냥 Spring JDBC를 쓰는 게 나을 꺼라 생각합니다.
CoC 스타일로 RowMapper와 SqlParameterSource를 쓰면 웬만한 SQL 매핑 문제는 해결할 수 있고, 동적 쿼리 작성 부분만 잘 잡아놓으면 iBatis가 제공하는 거의 대부분을 Spring JDBC로 커버할 수 있을 것 같습니다.
iBatis는 쿼리를 xml 파일로 따로 빼놓아서 일종의 추상화를 시키려고 하는 데 이 부분은 쓸데없는 오버헤드가 되기 쉽지 않나 생각합니다.

이 내용에 대한 이야기는 참 자주 나오네요. KSUG 에서 논의한 것만 해도 벌써 여러번 인 듯 합니다.
근데 한가지 궁금한게, Hibernate 를 SI 프로젝트에서도 사용하나요?
회사 내부 프로젝트나 자체 솔루션 개발 같은 경우 개발 조직이 공부해서 Hibernate 를 사용하면 되겠지만,
(Hibernate 에 대한 교육 비용이 존재하는 것은 모두 공감하고 계시다고 생각합니다.)
SI 프로젝트에 투입된 외주 개발자들에게 비용과 시간까지 들여가며 Hibernate 를 교육시키긴 어렵지 않나 생각합니다.
또한 iBatis 같은 경우 XML 이 따로 떨어져 있으므로 엉성한 query 를 잡아내기가 약간은 더 수월한데
Hibernate 의 경우 소스를 일일이 까 봐야 알 수 있을 것으로 생각하고, 또한 까봐도 잘 알아보기가 힘들것 같아요.
제 경우 6개월 정도 되는 프로젝트 표준을 잡을 때 Hibernate 는 위의 이유 등으로 아얘 처음부터 제외를 하고 시작했습니다.
개발자 구할 때 "Hibernate 가능자" 를 조건에 내걸었다면야 가능하겠지만, SI 프로젝트가 많은 우리나라 상황에서는
Hibernate 가 확산되기 어렵다고 생각합니다. 혹시 SI 프로젝트에서 Hibernate 를 적용해 보셨나요? 그런 경험이 있으셨다면 경험담을 듣고 싶네요.

상황에 따라 다르겠지만, 개발 업체가 기술셋 결정하는 사람과 다른 회사에서 오고
유지보수 담당자와 다른 경우라면, 더군다나 누가 들어올지 모르는 프로젝트라면 Hibernate 적용은 엄두도 못낼 일이죠.
개발자 입장에서 그다지 필요성을 느끼지 못하는 EA니 TRM 등을 논하는 이유나 '변화관리' 등의 이슈가 무관하지 않은 내용인데...
그런 이야기를 꺼내지 않더라도 종종 책임감 없는 인사가 개인적인 선호(?)로 기술만 선정해놓고 제대로 가이드도 하지 못해 프로젝트에 고통을 심어놓고, 시스템엔 버그를 심어놓고 나가는 경우가 있습니다.

이런 얘기가 나오면 항상 질문하시고 싶은 두가지가 있습니다.
- 하이버네이트는 정말 학습비용이 높나요?
- 하이버네이트를 안쓰는 이유가 여러 테이블 조인이나, 리포트(통계)쿼리가 있기 때문이라는 것이 과연 타당한 이유인가요?
저는 둘 다 아니라고 봅니다.
일반 DBA와 같은 역할을 해주는 하이버네이트의 매핑 담당자가 있다면 개별 개발자들은 별로 학습할 게 없습니다. 하이버네이트 매핑이 만들어져 있다면 90%이상의 쿼리들은 자바도 알고 SQL도 아는 개발자라면 1-2일 교육이면 충분히 다 작성합니다. HQL이 SQL에 비해서 뭐가 어렵나요? 게다가 하이버네이트의 튜닝은 개발 후 별도로 해도 그만이니까 오히려 초보자들이 덜 부담을 가지고 개발도 가능합니다. 어짜피 초대형 DB의 초고성능 쿼리가 필요한 상황이라면 SQL도 DBA가 미리 작성을 해주고 그것을 가져다 사용하는 식일테니까, 하이버네이트도 HQL이랑 최적화된 매핑 만들어서 개발자들에게 제공하면 됩니다.
나머지 10%의 리포트 쿼리나 DB의 특수한 기능을 사용하는 쿼리도, 그냥 하이버네이트의 Native SQL 쿼리매핑을 사용해서 SQL 그대로 쓰면 됩니다. 하이버네이트 안에서 일반 SQL을 사용할 수 있게 된 것은 언제부터인지 기억도 안날만큼 오래전부터 지원됐습니다. iBatis만큼 편리하게 Native SQL/XML-Entity(또는 DTO) 매핑도 해줍니다. 당연히 Stored Procedure도 호출해서 사용할 수 있습니다.

> - 하이버네이트는 정말 학습비용이 높나요?
제 생각에 하이버네이트의 전체 학습 비용이 ibatis를 배우는 다 비용보다 크다고 생각하지는 않습니다.
다만 사람들이 ibatis를 다 알고 쓰지 않는 다는 거...
아마 대부분 SI 개발자들은 ibatis의 dynamic sql이나 cache 기능도 모를 거에요.
결과를 객체에 담지도 않을 거고요.
결국 이미 알고 있는 SQL 지식을 활용해서 뭔가 앞선 기술(처럼 보이는) 뭔가를 쓴다는 이미지를 노리는 것 아닐지...
사실 생짜 JDBC 작업을 하는 것 보다는 ibatis가 편하니까요. 다만 우리에게는 spring jdbc가 있지요. ㅎㅎ

> - 하이버네이트를 안쓰는 이유가 여러 테이블 조인이나, 리포트(통계)쿼리가 있기 때문이라는 것이 과연 타당한 이유인가요?
하이버네이트 안 쓰는 이유로 이런 이유는 별로 들어보지 못했습니다.
> HQL이 SQL에 비해서 뭐가 어렵나요?
사실 별로 어렵지 않지요. 다만 이미 개발자들이 SQL은 친숙한데 HQL은 새로 배워야 한다는 것이 문제지요. SI는 프로젝트
는 파견 개발자가 대부분인데 이 친구들이 참 대책없는 경우가 많습니다. struts나 spring의 아주 기본 내용만 교육해도
그 걸 이해 못해서 버벅이는 일이 많지요.
hibernate가 native sql로 쿼리할 수 있는 기능도 가지고 있지만 그거 설명하면 10명 중 한두명 이해하면 다행일것 같아요.
사실 전 ibatis가 세상에 없었다면 참 좋았겠다는 생각입니다. 꼭 hibernate를 써야만 하는 건 아니지만 분명히 jdbc를 직접 쓰는 것 보다 hibernate 같은 orm을 쓰면 비용이 훨씬 줄어드는 임계점이 있는데 ibatis가 그 임계점을 훨씬 뒤로 밀어버린 것 같아요. 기존에 습득한 기술을 우려 먹을 수 있는 기간이 늘어나는 거죠.
제가 hibernate를 쓰지 않는 상황이 아쉬운 건 단순히 hibernate라는 좋은 기술을 쓰지 못하기 때문이 아니라 ORM의 보급이 늦어짐과 동시에 객체지향 기술도 보급이 되지 못하는 것 아닌지 생각이 들어서 입니다.

> - 하이버네이트는 정말 학습비용이 높나요?
제 생각에 하이버네이트의 전체 학습 비용이 ibatis를 배우는 다 비용보다 크다고 생각하지는 않습니다.
ibatis랑 비교해서가 아니라 하이버네이트라는 기술자체가 높은 초기 학습비용을 가진 것인가 하는 질문이었습니다. ResultSet을 뷰단까지 직접 끌고 가서 사용하던 10+년전 방식이 아니라면 대부분 DTO에 결과를 담아서 주고 받는 DAO 패턴에 익숙할 것입니다. iBatis도 마찬가지지요. 요즘은 맵을 선호하기도 한다지만 그렇다고 객체에 담는 걸 모를까요? (설마...) 정보를 의미있는 자바 객체에 담아서 전달하고 매핑하는 것은 이미 익숙한 작업입니다. 그런면에서 하이버네이트와 같은 ORM이 그다지 특별한 이해력을 요구한다고 보지 않습니다.
다만 사람들이 ibatis를 다 알고 쓰지 않는 다는 거...
아마 대부분 SI 개발자들은 ibatis의 dynamic sql이나 cache 기능도 모를 거에요.
결과를 객체에 담지도 않을 거고요.
저는 ibatis의 고급기능이 하이버네이트보다 어렵더라고요. 하이버네이트 2시절에도 xdoclet, 3에선 annotation을 이용해서 매핑정보를 넣다보니, XML로 만든 매핑정보는 보기만 해도 머리에 쥐가 나더군요.
> HQL이 SQL에 비해서 뭐가 어렵나요?
사실 별로 어렵지 않지요. 다만 이미 개발자들이 SQL은 친숙한데 HQL은 새로 배워야 한다는 것이 문제지요. SI는 프로젝트
는 파견 개발자가 대부분인데 이 친구들이 참 대책없는 경우가 많습니다. struts나 spring의 아주 기본 내용만 교육해도
그 걸 이해 못해서 버벅이는 일이 많지요.
대책없는 개발자들은 SQL가지고도 동작은 하지만 성능문제를 일으키는 각종 악성 쿼리를 만들어 냅니다. 대책없는 개발자는 뭘 해도 대책없습니다.
hibernate가 native sql로 쿼리할 수 있는 기능도 가지고 있지만 그거 설명하면 10명 중 한두명 이해하면 다행일것 같아요.

하이버네이트의 Query 오브젝트를 사용할 줄 안다면 native SQLQuiery를 사용하는 방법은 쉽게 마스터 할 수 있다고 봅니다. Native SQL을 오브젝트 매핑 안하고 scalar(List of array)로 가져올거라면 아래와 같이 한줄이면 충분합니다. 이미 존재하는 Entity에 매핑하는 것은 메소드 하나 더 붙이면 되고, 리포트 쿼리라 임의의 DTO로 매핑하는 것도 두 줄이면 충분합니다. 저는 예제 보여주고 10분 설명하면 10명 모두 "간단하네" 하더군요.

sess.createSQLQuery("SELECT * FROM CATS").list();

사실 전 ibatis가 세상에 없었다면 참 좋았겠다는 생각입니다. 꼭 hibernate를 써야만 하는 건 아니지만 분명히
jdbc를 직접 쓰는 것 보다 hibernate 같은 orm을 쓰면 비용이 훨씬 줄어드는 임계점이 있는데 ibatis가 그 임계
점을 훨씬 뒤로 밀어버린 것 같아요. 기존에 습득한 기술을 우려 먹을 수 있는 기간이 늘어나는 거죠.
제가 hibernate를 쓰지 않는 상황이 아쉬운 건 단순히 hibernate라는 좋은 기술을 쓰지 못하기 때문이 아니라 ORM
의 보급이 늦어짐과 동시에 객체지향 기술도 보급이 되지 못하는 것 아닌지 생각이 들어서 입니다.
저는 ibatis 쓰지 말고 하이버네이트 쓰자는 얘기는 별로 하고 싶지 않습니다. 개발만 잘 한다면 뭐 기술이야 입맛대로 또는 영업&정치적인 이유에 따라서 쓰면 되는 것이고요.
다만 하이버네이트를 사용하지 않는 이유를 하이버네이트 자체에 심각한 문제가 있기 때문인 듯이 둘러대는 것은 맘에 안듭니다.

전 솔직히 Hibernate는 어렵습니다.
특히나 쿼리를 생성하기 위한 'Criteria' 사용법이 너무 어렵습니다. 일반적인 조인관계는 그렇다쳐도
사용하기 위해선 맵핑 설정을 충분히 고려해서 사용해야하고 차근차근 개발하기에는 무리가 있더군요.
iBatis는..초기에 조금 사용해봐서 고급기능까지는 안해봤지만, 어렵게 접근은 안해도 되더군요.
그래서 앞으로는 좀더 Hibernate를 공부하면서 Spring JDBC를 사용해보려 합니다. ㅎㅎ

> 좀더 진실해지자면,
Hibernate를 어렵게 느끼는것은 객체지향 프로그래밍에 익숙하지 않아서가 아닐까요?
iBatis가 쉽게 느껴지는건 내가 좀더 절차지향을 편하게 느껴지는 습관 때문일거라는 생각 입니다.
그러면 사실 나는 OOP가 가능한 JAVA를 사용하면서 절차지향적인 면을 더 편하게 느끼고 있는 것입니다.
앨런 홀럽의 말처럼, JAVA를 절차지향적으로 작성하면 JAVA를 사용할 이유가 없습니다.
차라리 절차지향 언어로 작성하는것이 더 효율적일지도 모릅니다.

왜 iBatis 가 절차지향적이고 Hibernate 가 객체지향적이라고 생각하시는지 궁금해요.
sql 들어가면서 나름 OOP 개념으로 의욕적으로 설계를 하다가 깨지는 경향 (껍데기뿐인 모델 등등) 이 있다, 그리고 Hibernate 와 같은 ORM 도구를 "잘" 활용할 경우 OOP 적으로 좀 더 탄탄한 설계를 할 수 있다고 하는 글은 본 것 같은데 절차지향과 연관하여 이야기 하신 것은 잘 모르겠어요. 앞부분을 이해를 하지 못하여, 뒷 단락에 하신 말씀도 이해가 되지 않습니다. 절차지향보다는 OO - relation DB 컨셉의 짬뽕이 문제가 아닐까 해요.
다시 되돌아와서 ORM 도구를 잘 활용하면 더 OOP적으로 좋은 설계를 이끌어 낸다는 점에는 동의합니다.

밑에 강명성님 글을 보고 생각난 것인데 Join 때문에 Hibernate를 못 쓰겠다는 말이 나오는 이유가 성한님 말씀 처럼 객체지향에 익숙하지 않은 것과 관계 있다고 생각이 듭니다.
이건 사실 제가 iBatis를 사용해서 쿼리 결과를 객체에 담는 방법을 개발자들에게 설명하면서 받은 질문 때문에 알게 된 건데요.

RDBMS에서는 DB의 table 평면 공간에 배치되어 있고 서로 동등한 수준에서 관계를 맺는 구조이고 객체 지향에서는 여러 클
래스들이 상속이나 구성을 통해서 관계를 맺는 구조인데 이 두 구조 간의 차이가 생각보다 큰 것 같습니다.
간단히 말해서 RDBMS에서 SQL을 사용하면 이런 저런 테이블을 어떻게 복잡하게 조인해서든 원하는 컬럼 한 두개를 평면적으로
읽어 올 수 있는데 객체를 사용하면 입체적인 구조로 담아와야 한다는 거죠. 뭔가 미리 계획을 잘 세워야 하고 거추장스럽다는 인상을 갖는 듯 했습니다.

단순히 join때문이 아니라 HQL을 토드나 오렌지에서 테스트해 볼 수 없어서 싫어하시는게 아닐까요?
전 프로젝트에서 개발자분들은 HQL보다는 flush같은걸 잘 모르셔서 문제가 생기더군요

따지자면 생명 주기도 있고 lazy loading도 있고 여러가지 걸림돌(?)들이 있지요.
위에서 토비님이 질문하신 것 중 두번째에서 제가 join은 별 문제 없는 것 같다고 말을 하고 보니 이런 생각이 나더라고요. 그
래서 말씀드려 봤습니다. ^^
HQL이야 LOG에 찍히는 sql 구분 사용해서 토드(이 비싼 놈)에서 돌려보면 되기는 하지만 일단 ORMap 담당자가 이런 일
을 해주면 개발자들은 신경 안 써도 되겠죠.
ORMap 담당자는 필수 같아요. ^^

토드처럼 모델(엔티티) 구조를 보면서 HQL을 이클립스 등에서 직접 만들고, 실행 해볼 수 있는 하이버네이트 툴도 꽤 오래전부터 사용되고 있습니다.

Hibernate 사용에 있어 테이블 조인 결과를 어떻게 object에 매핑하느냐와 같은 사용법 문제는 큰 이슈가 아니라고 봅니다. 오히려 많은 SI 프로젝트를 보면, 새로운 시스템 도입 과정에서 업무 분석도 불충분한데 온갖 방법론이나 기술 도입을 통해 결과적으로 만든 시스템에 대해 누구도 만족하지 못하는 경우를 경험합니다. '최종 사용자'는 물론이고 '유지보수 담당자'도 일관성을 찾기 힘든 시스템을 마지못해 인수받는 모습을 어렵지 않게 찾아볼 수 있습니다. 공교롭게 규모가 커서 예산이 충분할수록 이런 현상은 더 심해지는 듯도 보입니다.
정보 시스템 개발을 할 경우 시스템 핵심정보라 할 수 있는 "모델"은 주로 ERD에 녹아듭니다. 현장에서 객체 모델 필요성(?)을 주장한지 5년째이던 2007년 즈음에 현실을 인정했습니다. 넓은 강당에서 고객과 ER 모델러가 커다란 ERD를 펴놓고 격론을 벌이는 모습을 보고서 단념했죠. 과연 객체 모델을 가지고 키맨 사이에 논쟁을 벌이는 날이 언제 올까요? 일회성으로 진행하는 프로젝트에선 요원할 일일 지도 모릅니다.
성철님이 말씀하신 바대로 능력(?)을 전혀 가늠할 수 없는 외주 개발자가 무척 많습니다. 거기에 연차까지 많으면 설득에도 오랜 시간과 노력이 필요합니다. 그런 분을 이끌고 촉박한 시간 안에 프로젝트를 하는데 Hibernate를 쓰자고 하는 일은 헛심쓰기에 가까운 경우가 많습니다. 특히, 업무 설계가 엉성한 경우는 Sub-query와 UNION 없이는 화면 하나도 구현이 불가한데, 이런 경우 굳이 Hibernate를 도입이 주는 이점이 별로 없습니다.

RDB의 join만큼 기껏 설계한 ERD의 가치를 떨어뜨리는 것도 없다고 봅니다.
1:n을 join해서 한방에 가져오면 catersian product에 theta-join이 일어나 1:n의 릴레이션의 의미는 사라진 전혀 새로운 포맷을 가진 직관적이지 않은 결과가 돌아옵니다. 과연 그게 이해하기 쉬운 것인가요? 차라리 ERD에 나타난 엔티티 릴레이션의 구조대로 1:n의 관계를 유지해서 가져오는 하이버네이트와 같은 ORM이 ERD의 개념을 더 충실하게 나타내고 있다고 봅니다.
서브쿼리건 UNION이건 어짜피 SQL을 쓰나 Hibernate를 쓰나 쿼리작성 복잡도는 비슷합니다. 차라리 join의 갯수가 증가하면 HQL이 SQL보다 훨씬 이해하기도 편하고, 역시 ERD에도 잘 부합됩니다.
구지 ORM의 특징이라고 하는 상속개념도 ERD에서 다 나타납니다. 그걸 물리적인 스키마로 표현하면 명시적으로 드러나지 않을 뿐이지, 원래 ERD에 있던 개념이 사라지는 것은 아닙니다. 하이버네이트가 ORM으로 성공한 이유는 백그라운드의 RDB를 무시하지 않고, 그 특징을 적극 활용하면서 프로그래밍 인터페이스 적인 측면을 강조했기 때문이지, 무슨 RDB의 ERD설계와 개념을 제치고 다른 걸 들고 나와서가 아닙니다. 하이버네이트를 마치 ODB처럼 생각하는 것은 완전 잘못된 이해입니다. 하이버네이트는 RDB에 최적화된 또다른 접근방법의 하나일 뿐입니다. 테이블과 인덱스로 구성된 RDB에 SQL이라는 추상적인 인터페이스를 도입한 것과 다를바 없다고 봅니다.
이미 익숙한 기술을 고집해서 새롭고 보다 나은 기술에 저항했던 것은 ISAM에서 RDB로 넘어올 때도 마찬가지였고, 4GL을 도입할 때도, 자바가 메인스트림에 등장했을 때도, 웹 프로그램이 주류가 되려고 할 때도 마찬가지였습니다. 가치를 아는 사람들이 늘어가고 언젠가 임계점을 넘으면 ORM이나 post-RDB 기술이 SQL을 밀어내고 주류가 될겁니다. 아마 그때까지는 "ERD가 기업에는 중요하니 하이버네이트는 안된다"는 FUD가 계속 나타나겠지만요. 차라리 우리는 제대로된 ERD도 없고, 그때마다 주먹구구식으로 만들어서 테이블이 엉망진창이라 매핑할 엄두도 못내겠다라고 하면 차라리 이해가 갈텐데 말이죠.

온라인 상에 Hibernate에 대한 FUD를 퍼뜨리는 사람이 있을 순 있겠지만, 실제 현장에서 FUD 탓에 Hibernate 도입을 꺼리지는 않는다 생각합니다.
본질은 SI 프로젝트에서 주로 형식적인 부분과 절차적인 면은 중요시하지만, 내용에 해당하는 설계 자체에 대해서는 심각하게 다뤄지지 않는다는 점이죠. 외주 개발을 하는 SI 특성상 시스템을 사용하는 고객사 환경을 감안하지 않을 수 있습니다. 이를 위해 EA니 ITA 등을 논합니다. 수 년전부터 금융, 공공 등을 필수로 수행을 했죠. EA 안에서도 DA와 AA는 두드러진 연계점을 찾아보기 힘들더군요.
ERD는 데이터에 대한 정적인 뷰이기 때문에 실제 최종 사용자가 애플리케이션을 사용하는 시점에서 어떻게 데이터를 쓰는지. 가령, 어떻게 묶여서(가령 객체 형태로) 쓰이는지,수정이나 노출(조회) 빈도는 어떤지 등등 동적인 측면에 대해서도 충분히 고민을 해야 합니다. 만일, 이런 내용을 협의하고 고민하고, 그에 따라 시스템 속성을 변경할 필요가 있다면... 객체모델(UML로 그렸든.. 사람들 머리속에 있든)은 강력한 도구가 될 것입니다. 이런 곳에서라면 Hibernate는 정말 좋은 솔루션이라고 생각합니다. 그리고, 추후 그럴 가능성이 있는 곳이라면 학습 비용이나 변화에 대한 저항이 존재하더라도 충분히 대가를 치를 가치가 있겠죠.

하이버네이트 적용 성공에 대한 관건은 RDB의 DBA와 같은 역할을 하는 ORMA가 있어야 한다고 생각됩니다.
그래서 개발자 개개인의 기술에 대한 학습부담을 줄여주어야 합니다. 필요에 따라 HQL이나 페칭 전략 등에 대한 검증과 조언을 해주고, 개발자들의 코드에 나온 Raw SQL을 분석해서 모델레벨의 튜닝을 최적화 해주는 독립적인 작업을 지속적으로 수행해줘야 하죠.
하이버네이트의 장점은 모델레벨에서 페칭, JOIN 방식이나 캐슁 등에 영향을 주는 설정을 개발된 코드나 HQL에 영향을 주지 않고도 조절이 가능하다는 점입니다. 그런면에서 SQL보다 개발자들 수준에 영향을 덜 받을 수 있는 장점이 있습니다만, 그만큼 숙련된 하이버네이트+RDB를 모두 잘아는 뛰어난 전문가가 한명씩은 필요하다는 부담이 될 수도 있습니다.
물론 페칭 방식, 세컨레벨 엔티티 캐쉬에 따라서 HQL에서 한번에 가져올 것이냐, 일부를 가져온 뒤에 캐쉬에서 lazy loading을 하게 할 것이냐의 결정은 코딩 구조에 영향을 주기도 합니다. 그런 결정은 대부분 초반에 어느정도 내려져야 할것입니다. 가장 손쉬운 것은 리드온리성 레퍼런스 정보는 캐슁을 한다는 정도면 되겠죠. 쿼리 캐슁은 나중에도 얼마든지 조정이 가능하니까 뭐 상관없습니다만. 그런 정도 팁들은 JPWH 책 한권만 봐도 아주 자세하게 잘 나와있습니다.
HQL은 복잡한 쿼리 위주라면 named query로 해서 XML로 뺀 후 ORMA/DBA가 관리하게 하는 것도 좋은 방법입니다. "데이터가 회사의 가장 중요한 자산이다"라고 생각하는 기업이라면 SQL에는 개발자가 손도 못대게 하고, DBA들이 미리 만들어서 제공하는 경우가 많을 겁니다. HQL도 마찬가지의 접근을 할 수 있죠.

SI현실을 가지고 얘기하면 사실 끝이 없습니다. 엉망인 것도 워낙 제각각이라서 말이죠. 친한 DB컨설턴트의 말을 들어보면 ERD보고 SQL도 제대로 못만드는 개발자들도 여전히 많이 있다고 합니다.
SI현실을 까놓고 보자면 스프링을 왜썼나 모르겠다 싶은 프로젝도 많이 보입니다. 개발자 수준은 안되고, 학습부담은 크고, 유지보수에 대한 대책이나 후속조치도 불분명하고, 현 상황에서 썼을 때 그다지 장점도 없어보이는 대표적인게 스프링이라고 생각됩니다. 친숙한 JSP Model1이나 개발자들이 제법 익숙한 스트럿츠1+헬퍼 클래스 또는 벤더 지원과 툴도 빵빵한 EJB를 쓰면 되는데 굳이 장점이 좀 있다고 SI현실에 부합하지 않는 어려운 스프링을 도입해야 할까요?
물론 스프링을 안쓰는 곳은 FUD 때문이거나 다른 기술적인 이유가 아닌, 더 현실적인 타당한 이유가 있겠죠.
그래도 "EJB는 표준기술이니까 다들 익숙하고, 스프링은 오픈소스니까 학습비용도 크고, 서버도 톰캣에서밖에 안돌고 메인프레임 연결도 할 수 없고 어쩌고 등등" 하면 곱게 못넘어갈 것 같습니다.
또는 현실적인 문제는 극복할 의지와 가능성이 있는데, FUD 때문에 꺼리는 사람이 있다면 잘못이해하고 있는 것을 바로 잡아주는 것이랑 "SI 현실을 무시하고 스프링 써라"라고 말하는 것은 분명히 다릅니다.
저는 스프링을 쓰고 싶으나 분산트랜잭션이 필요해서 스프링을 도입못하고 있다는 사람을 만나본 적이 있습니다. 스프링에서도 훨씬 세련된 방식으로 분산트랜잭션을 사용할 수 있다고 설명해줬더니 아주 좋아하더군요. 물론 당장에는 다른 현실적인 한계로 인해서 스프링을 도입 안할 수 있습니다. 하지만 다음 번에는 좀 더 도입할 의욕을 가지고 장애물을 극복하고 도전할 용기를 그만큼 더 낼 수 있을 거라고 기대합니다.
하이버네이트에 대해서도 마찬가지 생각입니다. 현실적으로는 무리다라고 말하는 것과 기술적인 오해를 가지고 힘들겠다라고 말하는 것은 분명 다릅니다.
솔직히 한국에서 하이버네이트에 대한 제대로된 기술적인 비판이나 분석이 진행된 것을 본 기억이 없습니다. 그냥 "성능이 떨어져서..", "데이터 모델이 복잡해서..", "복잡한 조인 쿼리가 있어서..", "개발자들이 배우기 힘들어서.." 라는 식입니다. 좀 구체적인 내용을 가지고 얘기를 하면 좋겠는데 말이죠. 그러면 충분히 답변 가능한 것들이고 반박도 할 수 있고, 증거도 보일 수 있을텐데 말이죠.
그런데 하이버네이트 제법 쓰이는 것 같습니다. 상대적으로 아직 미미할지는 모르겠지만, 수십억원 급 또는 그 이하의 중소규모 프로젝트들을 중심으로 제법 쓰이고, 만족스럽게 결과를 냈다는 얘기도 종종 들리더군요. 작년에 벌써 200여군데의 현장에 적용했다고 하는 SDS의 애니프레임도 하이버네이트를 사용하지 않았나요? 하이버네이트는 스프링과 비슷하게 한번 제대로 쓰면 계속 쓰고 싶어지는 중독성이 있는 것 같습니다. 어쩌면 생각보다 빨리 주류기술로 올라설지 모르죠.
* FUD (Fear, Uncertainty, and Doubt)

이 글을 보니 위에 제가 애매하게 쓴 제 생각을 더 명확히 정리할 수 있네요.
ORM이 ERD 구조를 더 충실하게 유지한다고 하셨는데 맞습니다. 그런데 그런 특성이 ORM 도입을 힘들어하는 이유가 되는 것 같습니다.
우리나라 개발자들을 결과를 빨리 얻기 원하는 것 같습니다.
쿼리 하나 날려서 원하는 결과를 얻고 이 결과를 기계적으로 화면 단에 전달에 표시하기를 원하는 거죠. Data가 구조화된 상태
로 관리되는 것은 DB로 충분하지 굳이 메모리에까지 구조화된 상태로 가지고 오기를, 그래서 그 자료를 다시 어떤 로직으로 처리하
기를 원하지 않는 것 같습니다.
데카르트 곱이 ERD의 구조를 깨트린다고 하셨는데 데카르트 곱의 결과를 가지고 어떤 처리를 할 것이 아니라 그것 자체가 결과라
면 사실 그런 구조 파괴가 문제되지는 않으니까요.
저는 이렇게 SQL에 대부분의 주요 로직이 들어가고 Java 코드는 단순히 인자를 전달하고 결과를 반환하는 일을 주로 하는 방식의 개발 관행이 바뀌지 않는다면 ORM은 일할 양이 늘어나는 번거로운 기술로 받아들여질 듯 합니다.

결과를 빨리 얻기를 원하는 점은 개발자에 국한한 이야기는 아닌 듯 합니다.
그리고, 팀 단위 작업이라면 ERD는 아무 개발자에게나 맡기지 않잖아요.
ERD를 구성하는 어휘도 관리하고, 여력이 있는 경우 모델링과 검증을 위한 전문 인력을 두기도 하고
논리와 물리로 단계를 나누어 정규화도 하고, 이후에도 ERD에 입각해서 DB 객체를 만들어내고 관리하는 과정에 상당한 공수를 투입합니다.
즉, 중요하게 취급하죠.
하지만, ORM에 대해서도 그럴까요? ORM은 EA/ITA, SOA처럼 큰 돈을 보장하는 fancy한 기술이 아니기 때문에
지속적으로 노하우나 이점을 실제로 맛 보고, 이를 공유하고 가시화 되어지는 긴 과정이 필요한지 모릅니다.
어떤 면에서는 Spring이 주류로 올라선 과정을 ORM도 겪어야 하지 않나 싶고, 이미 그런 과정 중에 있는지도 모르죠.

하이버네이트가 결과를 빨리 얻는데는 더 낫다는 것을 알면 좋을텐데 말이죠.
lazy loading이랑 object navigation을 써서.. 대충 엔티티 하나 가져와서 뷰단에서 마구 관련 정보를 다 끌어와 붙이고 추가해도 되자나요. 매번 가져올 필드랑 조인할 테이블이 생기면 SQL을 고치고 화면도 고치고 하는 것보다 훨씬 편한데 말이죠.

애니프레임에 대해 말씀하신 부분에 오해의 소지가 있어 관련 조직에 몸담고 있는 제가 다시 부연 설명을 드리면,
일단 작년 한해동안 SDS 에서 자체적으로 수행한 프로젝트가 200 여군데가 되지는 않습니다^^; 아마도 예전의 SYSTEMiER 를 포함하여 SDS에서 수주하여 Framework 지원까지 발생한 프로젝트 수로 생각됩니다.
또한 대부분의 현장 프로젝트에는 SQL Mapper 류인 QueryService 가 사용되었고, ORM 기술로 현장 프로젝트를 진행한 사례는 많지 않은걸로 알고 있습니다. 위에서 여러분들이 말씀하신 SI 프로젝트의 대표적인 사례겠죠. 동일한 유형의 단순 CRUD App. 를 코드 제너레이션 기반으로 찍어낼 때는 확실히 ORM 이 장점이 있고 사용 사례도 있습니다만..
애니프레임에서는 Hibernate 에 대한 문서화나 일부 기능 확장 및 Sample App. 등에 레퍼런스를 제공하기도 하고, 내부적으로 진행되는 프레임워크 기능 영역에 현재 JPA 기반으로 진행중인 부분도 있습니다만, 아직까지 현장에서 ORM 을 채택하는 데는 진입장벽이 있는것 같습니다. 최근에 릴리즈된 AnyframeGen 에서는 JPA 기반의 persistence 영역을 동등한 레벨에서 지원하고 있고 계속적으로 ORM 에 대한 가이드를 강화하려고 노력은 하고 있습니다.
개인적으로 ORM 를 현장에서 사용할 때 가장 문제가 되는 부분은 relation 관계를 nested 형태의 객체로 작성하는 것 자체가 아닐까 합니다. SQL 을 직접 사용하는 경우 객체 맵핑을 한다 하더라도(그냥 Map을 쓰는 경우가 현장에서는 더 많죠) flat 한 형태로 대부분 처리하였을 것인데 이 부분의 변경은 영향이 가장 큰것 같습니다. 특히 데이터를 담고 있는 객체는 UI 를 비롯한 전체 레이어에서 사용되므로 변경 영향이 큰것 같습니다. 특히 우리나라에서 많이 사용되는 X-Internet 류의 데이터 처리 객체로 컨버전을 해야하는 경우가 상당히 많은데 이런 부분도 어려움이 존재하고요..
SQL 을 직접 사용하는 경우보다는 transaction 발생이 미뤄질 때 exception 에 대한 처리, fetching 전략, lazy loading, clustering 환경에서의 cache sync. 문제 등 고려할 것들이 더 많아지므로 저부터도 쉽게 접근하기는 어려운 것 같습니다.
점차로 우리나라에서도 ORM 이 많이 사용될 것은 의심치 않습니다. 그렇게 되려면 현장의 성공사례가 많아지고 이에 대해 제대로 리드할 줄 아는 기술자가 많아져야 하겠죠.


정말로 많은 이야기들이 나옵니다. 이러한 이야기들은 전에 서과장님과 저랑 자주 하던 이야기이기도 했고요. 



아래는 토비의 스프링 3.1로 유명한 토비의 글입니다. 


내가 가장 좋아하고 많이 사용하는 자바 프레임워크는 스프링과 하이버네이트다. 거의 5년전 이 두개의 프레임워크를 처음 접했을 때의 충격과 감동은 지금도 잊혀지지 않는다. 두가지 다 내가 가졌던 오랜 고정관념을 확실히 깨뜨려주었고, 자바 프로그래머로서의 정체성을 다시금 확인할 수 있게 해주었다. 자바가 객체지향언어이며 비록 웹 개발을 한다고 할지라도 구지 ASP인지 JSP인지 구분이 안가는 JSP+스크립트성 자바코드 방식이 아닌 진정한 자바의 객체지향언어로서의 장점을 지키며 개발할 수 있다는 것을 분명 확인할 수 있었다. DB지상주의자들의 오만한 주장처럼 자바(또는 일반 프로그래밍 언어)는 그저 DB를 호출하고 UI랑 연동시켜주는데 사용하는 껍데기 인터페이스 뿐이다라는 수십년된 구닥다리 생각이 얼마나 시대에 뒤떨어지고 무지에서 나온 것인지도 분명히 깨닫게 되었다.

특히 하이버네이트는 당시 10년이 넘도록 매진해온 RDB의 경험과 매력을 사실 한층 더 가치있게 활용하는 법을 알려준 가장 고마운 프레임워크이다. 80년데 DBase시리즈로 출발해서 클리퍼,폭스베이스,폭스프로 등을 경험하다 93년에 처음 오라클7.3을 대형 프로젝트에서 처음 경험 했을 때 알게된 RDB의 매력과 능력이 당시에는 존재하지도 않았던 자바라는 멋진 언어와 이렇게 잘 조화되어서 양쪽의 장점을 다 살려서 빠르고 정확하고 멋지게 개발할 수 있구나라는 생각을 하게 만들어주었다. 당시 하이버네이트는 버전 2.6이었고, Gavin King이 호주 멜번에서 주로 EJB기반의 프로젝트에 참여하던 시절에 그의 매일 밤과 주말 시간을 모두 희생해서 열정적으로 개발하고 지원하던 사실상 1인 개발 프레임워크였던 시절이었다. 그럼에도 I모사의 책상 앞에서만 똑똑한 연구진들이 만들어내, 기업의 덩치로 밀어붙여 억지로 표준기술로 만들어버린 EntityBean이라는 자바역사상 가장 웃음거리가 된 기술보다 백만배쯤 낫다고 느껴졌다. 사용자들의 반응도 뜨거웠다. 결국 Gaving King은 JBoss에 영입되어 하이버네이트 개발을 책임지는 풀타임 오픈소스 개발자가 되었다.

그 당시 Hibernate vs JDBC와 관련된 논쟁이 많이 있었다. 그때 나름 공감을 받던 주장은 이렇다.
만약 애플리케이션 개발자가 DB설계를 직접 할 수 있거나, 영향력을 줄 수 있는 위치에 있다면 하이버네이트가 좋고, 아니고 DBA팀을 비롯한 애플리케이션 쪽은 전혀 모르고 DB만 고려하고 설계하거나 이미 오래전에 만들어져서 운영준인 레거시DB라는 JDBC를 선택하는 것이 낫다
이유가 뭘까? 왜 같은 DB이고 같은 애플리케이션이 결국 사용할 것인데 개발자들이 직접 DB를 관장하면 하이버네이트에 적합하고, 아니라면 부적합한 것일까? 어떤 이들은 이것을 하이버네이트는 객체DB스타일의 설계가 필요하기 때문에 ODB적인 구조로 DB를 특별하게 만들어야 하기 때문에 객체설계 후 그에 따른 DB스키마 작성이 필요하기 때문이라고 말한다. 하이버네이트의 기본도 모르는 바보들의 주장이다. 

하이버네이트는 순수 객체기반 영속성 솔루션인 엔티티빈에 대한 반감과 저항으로 등장한 기술이다. 하이버네이트가 지향하는 것은 진정한 O"R"M이다. 저 R이 관계형DB의 R이라는 것을 모르지는 않을텐데 말이다. 왜 DB를 써서 ODM이라고 하지 않고 ORM이라고 구지 관계형DB라는 것을 명시했을까? 그 이유는 하이버네이트는 관계형DB와 자바의 매핑이 목적인 프레임워크이기 때문이다. 그 말은 하이버네이트에 적합하게 따로 설계된 DB가 아닌, 가장 관계형DB다운 DB라면 하이버네이트와 완벽하게 연동이 될 수 있다는 말이다.

레거시DB와 하이버네이트 2.x가 함께 사용하기에 부적합했던 이유는 딱 한가지이다. 무식한 DBA나 DB설계자들 탓에 정규화도 제대로 되지 않은 엉망인 구조의 DB들인 경우가 있었기 때문이다. 물론 필요에 따라 비정규화를 한 경우도 있겠지만, 국내 한 유명 DB컨설턴트의 푸념처럼 정규화를 거치지도 안고 처음부터 그냥 생각없이 퍼포먼스를 위해서라는 근거도 없는 명분으로 비정규화부터 하는 식의 엉터리 DB설계들이 판을 치기 때문이라는 것이다. 하이버네티는 2.x조차도 RDB의 바른 설계원칙을 따라 만들어진 DB라면 완벽하게 매핑시킬 수 있다.

또 한가지 주장은 퍼포먼스이다. ORM은 JDBC를 그대로 가져다가 사용하는 것에 비해서 분명 퍼포먼스의 손해가 있다. 하지만 이 가정은 아주 단순한 기본적인 아이디어일 뿐이다. 혹 하이버네이트 1.x라면 들어맞을지도 모르겠다. 하지만 하이버네이트는 수많은 ORM의 퍼포먼스 향상 기법을 도입했고, 단지 매핑된 객체와 하이버네이트의 기반 HQL을 이용해서 거의 완벽에 가깝게 그것이 만들어 내는 SQL을 제어할 수 있다. 초기 ORM기술의 바보같은 n+1 퀴리 문제 같은 것은 더 이상 하이버네이트에서는 아무 문제가 아니다. 모델은 그대로 1tom 이더라도 그 정보를 조인해서 한번에 가져오는 것은 아주 간단한 일이다. 혹 하이버네이트에 무지한 자들이 하이버네이트는 JDBC/SQL을 사용하지 않는 것처럼 착각하는데 하이버네이트도 결국 JDBC를 이용해서 SQL로 DB와 커뮤니케이션 한다. 숙련된 하이버네이트 개발자들은 엔티티 객체를 다루면서 그것이 백그라운드에서 만들어 내는 SQL을 완벽하게 함께 머리로 그릴 수 있는 능력이 있다. 게다가 하이버네이트는 매우 간단한 설정만으로 객체베렐, 퀴리 레벨의 다양한 캐슁을 지원한다. 그것도 분산서버에서 트랜잭션까지 지원하는 캐슁도 아무런 문제없이 지원한다. DB스타일에 따라 캐슁이 적절히 사용되면 별다른 캐슁기술을 적용하지 않은 JDBC코드와 비교해서 훨씬 나은 성능을 보여줄 수도 있다.

JDBC를 이용한 애플리케이션도 이전처럼 ResultSet을 jsp까지 끌어가서 화면에 바로 출력하는 미련한 짓은 하지 않을 것이다. 다 DAO레이어를 만들고, 어떤 스타일이든 DTO를 사용해서 객체에 정보를 담는다. 그런 오버헤드가 사실상 전체 퍼포먼스에 미치는 영향은 극히 미미한데다 그것으로 얻을 수 있는 개발생산성과 버그의 위험도를 줄일 수 있는 가능성등의 장점이 워낙 크기 때문에 당연히 다들 DTO를 사용한다. 그런데 왜 하이버네이트의 매핑은 무슨 엄청난 오버헤드가 있어서 별 대단한 것도 아닌 시스템에서조차 성능때문에 못쓴다라고 하는 것일까?

DB의 성능에 대한 요구는 DB자체의 발전만으로는 커버가 불가능하다. 경험해본 사람들은 DB의 스케일업과 아웃이 얼마나 큰 비용이 드는지는 잘 알것이다. 클러스터링이 가장 어렵고, 된다고 해도 엄청난 비용이 드는 것이 DB이다. 작년엔가 모 금융사에서 메인메모리만 수백G짜리 수십억(백억이 넘던가..) 대의 초대형 DB서버를 구입해서 콘솔리데이션 프로젝트를 했었는데 얼마나 끔찍하게 고생했는지 거기에 참여했던 컨설턴트를 통해서 들은 적이 있다. 그래서 이제는 각종 Data Grid 와 같은 분산 기술들이 자바와 같은 객체지향언어와 결합해서 엄청난 속도로 발전하고 있다. DB의 절대왕좌를 차지하고 있는 오라클 조차 Coherence의 Tangosol을 인수했다는 것을 모르는가? Coherence는 수백대의 클러스터 그리드를 이용해서 미국의 한 금융 벤치마크에서 백만 TPS를 기록했다고도 한다. 국내의 경우에도 리니어하게 스케일링되는 구조에서 노드당 1500TPS이상을 가뿐하게 기록했다고 나와있다. 리니어스케일이 되는 이런 기술들이 하이버네이트와 얼마나 잘 결합되는지는 하이버네이트-Coherence의 역사를 보면 아주 잘 알 수 있을 것이다. 시대가 이렇게 빠르게 흘러가는데 아직도 기업의 DB관리는 보수적이고 어쩌고 저쩌고 하면서 한번 배운 기술 마르고 닳도록 울궈먹기 모드로 꼭 들어가야 하는 것일까?

스프링의 초기에도 그랬던 것처럼 그런 이유는 무지해서 그렇다. 잘 모르거나, 어설픈 얕은 지식으로 그냥 자신의 무지를 성능탓으로 돌리는 것이다. 그것은 마치 스프링으로 개발하면 메인프레임과 연동은 불가능하다(EJB만 된다)는 헛소리와 마찬가지 주장이다.

다시 위의 DB설계의 책임이 누구에게 있냐에 따른 하이버네이트 적용의 판단기준을 살펴보자. 하이버네이트 팀은 저런 상황을 잘 파악하고 있었다. 그리고 이상적인 모델만 현실에 존재하지 않는다는 것을 잘 파악했다. 그래서 하이버네이트 3.0이 나온 것이다. 이미 3년 전에 말이다. 하이버네이트3.0은 정규화가 잘된 깔끔한 DB뿐 아니라 바보 DB모델러들이 어설프게 설계한 레거시 DB도 매핑하는데 아무런 문제가 없게 만들어졌다. 게다가 엔티티 단위가 아닌 각종 복잡한 프로젝션 쿼리(일명 리포트 쿼리)도 완벽하게 지원한다. 역시 객체 단위가 아닌 벌그 업데이트도 당연히 지원한다. 사실상 SQL레벨에서 하던 거의 모든 작업을 하이버네이트의 OR매핑 장점을 그대로 살린채로 다 적요할 수 있다. 심지어 DB에 아주아주 specific한 네이티브 쿼리를 사용해야 하는 경우도 얼마든지 적용된다. iBatis못지않은 SQL-객체 바인딩도 지원한다. 심지어 그 결과를 DTO가 아닌 엔티티로 가져와서 작업하는 것도 가능하다. 하이버네이트를 쓰면 못쓰는 DB특화된 SQL이 있어서라는 엉터리 주장을 하는 사람들을 보면 한심하기 짝이 없다. 

오늘 아침에 읽은 글이다.
"스터디 끝나고, 간단히 식사를 하면서 왜 국내에서는 하이버네이트 보다 iBatis가 더 각광 받느냐에 대한 의견 교환이 있었습니다. 비즈니스 모델링 단계에서 개발자 혹은 설계자가 자료 구조에 대한 고민을 하고 되는데, 객체지향 분석/설계에 충실하다면 객체 모델링을 통해 도출된 데이터 구조 자체를 저장소(repository)로 손쉽게 매핑(mapping)할 수 있는 하이버네이트가 유리하다는 점에서는 동의했습니다. (사실 제 첫번째 직장이 객체형 데이터베이스 회사였기 때문에 충분히 수긍할 만한 의견이었습니다.)
하지만 국내 IT 현장에서 가장 큰 발언권을 가진 사람들은 개발자가 아니라 시스템 운영자들 혹은 데이터베이스 관리자들입니다. IT를 잘 모르는 경영진 입장에서는 아무래도 회사의 자산이라고 여겨지는 것이 바로 데이터베이스이고 프로그램은 덜 중요해 보이거든요. 그러다 보니, 시스템 개발 현장에서 가장 먼저 그리고 중요하게 분석/설계되는 것은 업무 요건이 아니라 바로 데이터베이스 설계입니다. 데이터 구조가 먼저 설계되고 쿼리를 만들고 그 위에 어플리케이션을 탑재하는 순서로 개발하니 당연히 하이버네이트는 적합한 프레임워크로 고려될 수 없게 되는 것입니다."
제대로 DB설계가 된다면 왜 그것이 하이버네이트에는 적합하지 않다는 말인가? 참 답답하다. 이 글을 읽다가 답답한 마음이 들어 이 글을 쓰기 시작했다.

물론 나도 하이버네이트가 모든 곳에 항상 적용가능하고 낫다고 보지는 않는다. 예를 들어, DB는 간단하고 부하만 큰 포탈의 게시판, 블로그 등은 ORM이 적합한 곳이 당연히 아니다. 객체 매핑 해봐야 무슨 이득이 있겠는가. 사실상 저런데는 RDB조차 적합하지 않다. 4억명의 회원을 보유한 ebay는 잘 알려진대로 DB트랜잭션조차 사용하지 않는다고 한다. 올바른 선택이다. 또 데이터의 입출력은 거의 없는 분석위주의 OLAP시스템도 하이버네이트 쓰는 것이 불가능하지는 않으나 별 장점이 없다. 그럴땐 OLAP쿼리 관리나 깔끔히 하면서 iBatis 같은게 낫다고 하겠다. 고객이 정말 무식과 고집을 겸비하고 있는데다, 밉보이면 향후 프로젝트에 지장이 있을 것 같은데 무조건 JDBC기반 DAO를 쓰겠다고 주장하면 살금살금 꼬셔서 SpringJDBC정도만 어찌 도입해보는게 나을 것이다. 정치적인 이유지만 어쨌든 시스템을 원활히 개발하기 위한 어쩔 수 없는 선택이 있다는 것도 안다. 또 애플리케이션은 DB의 인터페이스 용으로만 쓰고 모든 로직이 다 Stored Procedure에 저장되어있는 구닥다리 시스템이고 그것을 고칠 마음이 전혀 없다면 역시 하이버네이트는 선택의 대상이 아니다. 하지만 그 외의 모든 경우라면 하이버네이트는 충분히 적합하고 적용할 수 있고 많은 장점이 있다. 하지만 개발자들이 먼저 나서서 하이버네이트는 성능이 어쩌고, 적합하지 않아서 어쩌고 하는 것은 정말 아니다. 제대로 검증해보고 타당한 증몀을 해봤는가? 장단점을 충분히 진지하게 따져봤는가?

대형사이트? 더욱 더 적극 도입해야 한다. 물론 개발자가 많으면 평균 수준이 떨어지고, 각자 가진 기술수준이 차이가 많이 나서 기술수준 자체를 낮춰야 할 수도 있겠다. SQL만 달랑 넣으면 DTO부터 전 레이어의 기본코드가 딱 만들어지는 초간단 툴들이 요즘 인기있는 이유가 그것이라고 한다. 그만큼 다른 많은 것을 손해를 보고 가야함은 당연한 것이지만. 그렇다고 언제고 SQL만 알고 자바 문법만 아는 사람들로만 시스템 개발을 할 것인가? 오픈소스는 안돼라고 외치던 많은 곳에서 이미 스트럿츠1을 시작으로 이미 스프링과 iBatis 같은 고급 프레임워크 까지 적용이 이뤄지고 있다. 적절한 전략과 충분한 준비와 시간이 있다면 얼마든지 도입 할 수 있다. 개발자가 많으면 구현방식의 통일이 어렵고, 버그의 발생가능성이 높아진다. 하이버네이트와 같은 자바에 충실한 개발방식이 얼마나 개발자의 스트레스를 덜어주고, 생산성을 높이며 버그발생을 줄여주고, DB와 자바, UI까지 왔다갔다 정신없는 것을 충실하게 자바 모델에만 집중해서 개발할 수 있게 해주는지 안다면, 대형사이트일 수록 과감한 도입이 필요할지도 모른다. 그 비싸고 많은 모델러와 컨털선트는 왜 쓰는데…

내가 실무 프로젝트를 초기에 경험 할때는 코볼에 ISAM에 익숙한 노땅 개발자들의 전성시대에서 RDB와 같은 신기술이 막 떠오르려고 하던 때이다. 또 틈새를 비집고 UniSQL과 같은 허접 ODB도 설쳐대던 때다. 그때 ISAM에 익숙한 선배 개발자들의 푸념이 기억난다. 메인프레임에서 ISAM으로 했으면 이 정도는 순식간에 완벽하게 착착 다 만들었을 텐데 이게 무슨 장난감 같은 유닉스에, 복잡한 언어인 C에, 배우기도 힘든 3-Tier기술에, 성능도 떨어지는 RAD툴에 RDB까지 정신이 없다고 투덜대던 시절이다. 왜 복잡하고 지저분한 SQL을 이용하는, ISAM에 비해 성능도 떨어지는 RDB를 써야 하냐는 그때 그 사람들의 모습이 바로 지금 SQL에 익숙한데 왜 하이버네이트와 같은 새로운 기술을 또 공부해야 하냐라고 저항하는 개발자들의 모습과 꼭 닮았다.

물론 하이버네이트가 좋으니 당장 바꿔라라고 말하고 싶지 않다. 심지어 하이버네이트 사이트에도 프로젝트 1개월 남았는데 갑자기 하이버네이트로 바꾸자 따위의 생각은 꿈도 꾸지 마라라고 FAQ에 나와있었다. 개발 패러다임의 변화이니 충분한 시간을 가지고 학습하고 최적화된 기술을 익히는 과도기를 거쳐야 할 것이다. 그런 이유에서라면 당장 도입을 지연하는 것은 이해한다. 하지만 별 것도 아닌 중소형 사이트에 시간도 있고, 새로운 프레임워크도 익힌다면서 하이버네이트는 "그거 한국에서는 안돼"라는 말로 아무런 근거도 검증도 없이 그냥 iBatis나 쓰자라는 식으로 가는 모습은 정말 안타깝다. 제발이지 좀 근거를 가지고 구체적으로 비판을 하려면 했으면 좋겠다. 제발이지 나도 화딱지 나서 외국의 개발자들에게 "한국 개발자들은 무식하고 겁쟁이라 하이버네이트는 쓸 줄도 모릅니다"라고 하지 않도록…


참고자료입니다.  (출처 : http://www.slideshare.net/daumdna/devon-2012-b4-orm)

이렇게 된 이유는 과연 무엇일까요??



Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


지금까지 우리는 DB에 접속을 하고, DB에 있는 내용을 이용해서 Service를 구성하는 방법에 대해서 알아봤습니다. 
기술들은 다들 자신만의 색깔을 가지고, 개발자들을 좀 더 편하게 하기 위해서 발전되어 왔습니다. 그렇기 때문에 개발자들마다 자신이 선호하는 기술들이 따로 있는 것이고요.
그렇지만, 우리가 지금까지 Model을 하는 부분에 대해서는 한가지만은 확실히 나올 수 있습니다. 

"각자의 영역으로 분리"

이것은 DB를 다루는 Model 뿐 아니라, 모든 객체와 개발에서의 Layer가 지켜야지 되는 원칙이라고 할 수 있습니다. 

일반적으로 우리가 사용한 Model은 다음과 같은 package로 나뉠 수 있습니다. 

com.xyzlast.bookstore.entities
com.xyzlast.bookstore.vo
com.xyzlast.bookstore.dto
com.xyzlast.bookstore.dao
com.xyzlast.bookstore.repositories
com.xyzlast.bookstore.services

구성될 수 있는 package에 대해서 다시 정리하고, 최종적인 web application의 데이터의 흐름에 대해서 알아보도록 하겠습니다. 

Packages

entities package

Hibernate와 같은 ORM framework를 사용하는 경우, object와 persistence model간의 관계를 구성한 ORM 객체가 위치하는 영역입니다. 또는 Table에 1:1 mapping을 해서 구성하는 경우도 있습니다. 그렇지만, entities를 사용한다는 것은 기본적으로 DDD를 사용하는 것과 동일합니다. 객체과 그 객체의 관계에 대한 구성을 코드에 녹여내는 방식을 주로 사용하게 됩니다. 

vo package / dto package

vo(value object), dto(data transfer object)의 경우에는 일반적으로 persistence model에서 넘어오는 값들을 단순 parsing할 때 사용됩니다. 여기에 가장 대표적인 기술로 mybatis를 소개하였고, 이는 db에서 얻어오는 값을 view에서 단순 표시하기 위한 방법으로도 자주 사용하게 됩니다. 
이 두 package는 myBatis를 Domain Layer의 Framework로 사용하는 경우에는 거의 필수로 사용됩니다. 또는 개발시에 Model 영역에서 Controller/View 로 데이터를 넘길때, DTO 객체를 만들어서 넘길때 사용되기도 합니다. 이때는 vo가 View Object의 의미로 사용되기도 합니다.

dao

직접 DB에 query를 보내고, entity, vo, dto를 얻어오는 영역입니다. DB를 사용하는 기술에 따라 다르게 구성이 되는 것이 일반적이며, CRUD에 대한 모음으로도 구성될 수 있습니다. 단순 CRUD가 이루어진다고 해도, DB에 대한 기술적 영역이 바뀌게 될 가능성은 항상 열어두고 작업을 하는 것이 좋을 것 같습니다. 그렇기 위해서는 interface로 정의된 dao를 사용하는 것이 보다더 효과적으로 구성이 가능하게 됩니다. dao로 지정하게 되는 것은 controller, domain 모두에서 dao를 사용하겠다는 의미입니다. Hibernate와 같은 ORM을 사용하는 경우에는 repository pattern으로 접근하는 것이 더 좋습니다.

repositories

Repository는 Dao와 매우 유사한 개념입니다. 이 두 용어의 차이점은 기능이 아닌, 사용되는 코드의 위치입니다. Domain Layer에서만 사용되는 경우에는 repository, Domain뿐 아니라 모든 영역에서 사용된다면 dao로 개발하게 됩니다. 이 둘의 차이는 Model을 풀어가는 pattern의 차이입니다. 보다더 pattern 상위적인 개념이 Repository이고, Dao는 database 적 개념이 강한 Object라고 생각하면 좀 더 이해가 쉬울 것 같습니다. 

services

business logic 영역입니다. 여러개의 dao object들과 entity 또는 vo/dto object들을 얻어내고, 그 객체들간의 로직이 구성되는 영역입니다. 일반적으로 transaction의 단위 영역이 된다는 것을 명심해주세요. 
위의 구성에서 결국은 model은 3개 이상의 package로 구성이 됨을 알 수 있습니다. 남의 코드를 보더라도 대부분 위와 비슷한 구성으로 package가 구성된 경우가 많으니 참고바랍니다. 

Domain Framework의 비교

다음은 지금까지 알아본 Model을 접근하는 Framework의 조합에 대해서 한번 정리해보도록 하겠습니다. 
Framework특징장점단점
No Framework - PreparedStatement 를 이용하는 방법1. Native Query 구성1. native query를 이용한 학습 시간의 단축1. 오타가 발생하는 경우, query를 실행하기 전까지 에러를 확인할 수 없다.
2. 중복 코드가 많이 발생된다.
3. connection의 관리를 비롯한 Transaction의 처리를 모두 수동으로 해줘야지 된다.
4. java 코드에 sql 코드가 들어가기 때문에 관리 및 debug가 힘들다.
JdbcTemplate1. Spring JdbcTemplate 이용
2. DataSource 이용
3. Native Query

1. native query를 이용한 학습 시간의 단축
2. connection 관리
3. Transaction 관리
1. 오타가 발생한 경우, query를 실행하기 전까지 에러를 확인할 수 없다.
2. 중복 코드가 많이 발생된다.
3. java 코드에 sql 코드가 들어가기 때문에 관리 및 debug가 힘들다.
Hibernate1. HQL, Criteria
2. DataSource
3. Spring Transaction
1. 다양한 reference
2. 객체를 이용한 query의 처리로 인하여, 코드양을 줄일 수 있다.
3. 프로그램 설계 및 구성 부분에 대해서 장점을 갖는다. (DDD와 같은 객체 지향적 구성 가능)
4. Table의 변경 또는 DB의 변경에 유연한 장점을 갖고 있다.
5. 동적 query가 자유스럽다.
1. 학습 시간의 소요
2. criteria, HQL 모두 오타에 취약한 구조
3. 단순 CRUD에 대한 코드양 증가 - 1, 2번 항목 보다는 코드 양이 적다.
Hibernate + queryDSL1. JQL, Criteria
2. DataSource
3. type-safe
4. code generate
1. 다양한 reference
2. 객체를 이용한 query의 처리로 인하여, 코드양을 줄일 수 있다.
3. 프로그램 설계 및 구성 부분에 대해서 장점을 갖는다. (DDD와 같은 객체 지향적 구성 가능)
4. Table의 변경 또는 DB의 변경에 유연한 장점을 갖고 있다.
5. 오타가 발생할 수 없는 type-safe 한 query를 작성할 수 있다.
6. sql query와 비슷한 문법으로, 학습에 도움을 줄 수 있다.
1. 학습시간의 소요
2. 단순 CRUD에 대한 코드양 증가
2. 설정이 까다롭다.
Hibernate + JPA + queryDSL + Spring Data JPA
1. JQL, Criteria
2. DataSource
3. type-safe
4. code generate
1. JPA - java 표준
2. 객체를 이용한 query의 처리로 인하여, 코드양을 줄일 수 있다.
3. 프로그램 설계 및 구성 부분에 대해서 장점을 갖는다. (DDD와 같은 객체 지향적 구성 가능)
4. Table의 변경 또는 DB의 변경에 유연한 장점을 갖고 있다.
5. 오타가 발생할 수 없는 type-safe 한 query를 작성할 수 있다.
6. sql query와 비슷한 문법으로, 학습에 도움을 줄 수 있다.
7. CUD, select 문의 코딩양이 현격하게 줄어든다.
8. Hibernate 코드 역시 사용 가능하고 유연한 방법으로 대처가 가능하다.
9. Repository Interface code에 의한 가독성 향상
10. Spring @MVC의 @InitBinder와의 연동이 가능하기 때문에, Converter를 작성하는 시간을 줄일 수 있다.
1. 학습 시간의 소요
2. 설정이 까다롭다.
3. repository 객체를 두개를 만드는 것이 필요하다. (repository, repositorysupport)

myBatis (iBatis)1. Native Query
2. DataSource
3. ibatis-spring 이용
1. 국내의 많은 사용자
2. sql query의 관리를 하는 것이 가능하다.
1. 객체가 아닌 VO type의 이용으로 인한, 프로그램 architecture의 발전 가능성이 낮아짐
2. java 언어 뿐 아니라, sql 로의 확장으로 인하여 관리 코드가 늘어남

지금 전 이정도로 정리를 해봤는데, 다른 분들은 어떻게 정리를 할 수 있을까요? 각 방법에 대한 장/단점을 파악하는 것이 중요합니다. 적어도 이곳에 있는 분들만이라도요.
그리고, 만약에 외부 Project에 참여하게 되는 경우에는, 그 Project에 맞는 개발 방법을 이용해서 개발을 하는 것이 중요합니다.


Domain Framework 구성시의 pom.xml 구성

각각 Spring을 이용한 Domain Framework를 사용시의 pom.xml의 구성을 한번 알아보도록 하겠습니다. 
먼저 DataSource는 기본적으로 BoneCP를 사용합니다. BoneCP를 사용하는 경우, pom.xml은 다음과 같이 구성이 되어야지 됩니다. 

    <dependency>
      <groupId>com.jolbox</groupId>
      <artifactId>bonecp</artifactId>
      <version>0.7.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>14.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.2</version>
    </dependency>

Spring JdbcTemplate
: spring core, spring bean, spring jdbc, spring tx 를 이용합니다. 

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
    </dependency>

Spring + Hibernate 
: spring core, spring bean, spring jdbc, spring transaction, spring orm + hibernate core

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>

Spring + Hibernate + queryDSL
: spring core, spring jdbc, spring transaction, spring orm, spring context, spring context support + hibernate core + querydsl apt, querydsl query, querydsl jpa
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-sql</artifactId>
        </dependency>


Spring + Hibernate + queryDSL + Spring Data JPA
: spring core, spring jdbc, spring transaction, spring orm, spring context, spring context support + hibernate core, hibernate-entitymanagement + querydsl apt, querydsl query, querydsl jpa + spring data jpa

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-apt</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysema.querydsl</groupId>
            <artifactId>querydsl-sql</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>

Spring + myBatis
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>

자, 지금까지 같이 공부했던 모든 Persistence Framework들의 pom 정보를 알아보셨습니다. 이제 마지막으로 Web Application에서의 데이터의 흐름을 한번 알아보도록 하겠습니다. 

Web Application에서의 데이터의 흐름 

Domain Model을 이용해서 Web Application을 구성하는 방법은 두가지가 있습니다. 원칙을 지키고자 하는 Pure Domain Model과 Domain Model의 자유로운 사용이 특징인 Domain Model Everywhere입니다. 
먼저 Domain Model Everywhere에 대해서 알아보도록 하겠습니다. 

Domain Model Everywhere





Repository, Service, Controller, View가 모두 Entity Model을 가지고 동작하는 방식입니다. 이 방식은 Domain의 Entity 객체가 Repository 뿐 아니라, Controller, View에 표현되는 방법까지 가지게 되게 됩니다. 이렇게 되면 Entity Model이 매우 커지게 됩니다. 그리고 객체의 "단일책임원칙"을 위반하게 되는 단점을 가지고 있습니다. 이 방법의 장단은 다음과 같습니다. 

장점 :
# 개발하기에 빠르고 편리함

단점 : 
# 복잡한 View의 표현이 매우 힘듭니다.
# Domain Model에서 View Logic이 포함되기 때문에 Domain Model의 순수성이 떨어지고 객체의 단일 책임원칙을 위반하게 됩니다.
# REST 서버와 같이 외부와의 통신 API에 사용하게 되는 경우에는 Domain Model이 변경되면 API가 바뀌게 되기 때문에 큰 문제를 야기할 수 있습니다.

장점보다는 단점이 좀더 많아보입니다. 보시면 전에 제가 .NET으로 만든 mobile framework가 바로 이 모습입니다. 원칙상으로는 다음 방법을 더 권장하는 편입니다. 


Pure Domain Model


Domain Model은 서비스와 Repository에서만 사용하고, Controller와 View에서는 DTO를 새로 만들어서 사용하는 방법입니다. 이는 Domain의 순수성을 지키게 되는 큰 장점을 가지고 있습니다. View에서만 DTO를 사용하는 것이 BL의 변화에 따른 View의 변경을 격리할 수 있는 좋은 방법이 됩니다. 

장점 : 
# 순수한 Domain Model - 객체지향적인 OOP 모델

단점
# DTO를 따로 만드는 것이 귀찮고, 성가시고, 괴롭다.
# DTO는 또 다른 중복 코드를 만들어낸다. 
# DTO와 Domain Model간의 Convert를 따로 만들어줘야지 된다. 
# anti-pattern 중 하나입니다. 사용하지 않아야지 된다고들 이야기합니다.

Domain Model Everywhere pattern의 경우에는 anti-pattern이라고도 말하는 사람이 있을정도로 호불호가 매우 강하게 갈리는 pattern중 하나입니다. 개인적으로도 최대한 Pure Domain Model을 이용해서 개발하는 것이 좋다고 생각합니다. 



............................................................................ 현실은 ? OTL

지금까지 개발을 해본 결과. 다음과 같은 상황에서는 반드시 DTO를 사용해야지 됩니다. 

1. REST 서버와 같은 API의 response.
: Domain Model은 자주 바뀔 수 있습니다. 그렇지만, client와 통신을 하게 되는 API의 response는 바뀌면 문제가 생기게 됩니다. 따라서 이 둘은 반드시 분리를 해야지 됩니다.

2. 여러 Domain Model이 결합되어서 만들어지는 새로운 View에 대한 DTO
: View가 너무너무나 복잡해서 서로간에 연관성이 없는 Domain Model을 response로 보내줘야지 되는 경우에는 DTO를 만드는 것이 좋습니다. 

이 두가지 경우를 제외하고..... DTO를 모두 만드는 것은 다음과 같은 문제가 발생합니다. 

1. 객체가 너무 많이 만들어집니다. 지금 DataWindow의 package와 같이 각 SP의 숫자만큼의 package같이 객체들이 구성되게 됩니다. 이는 객체의 naming rule 및 관리가 매우 힘들어지는 결과를 가지고 옵니다.
2. 많이 만들어진 객체 숫자 만큼의 Converter가 필요합니다. Domain Model을 DTO로 바꿔주고 DTO를 Domain Model로 바꿔주는 Convert 숫자가 필요하게 됩니다.
3. 위 이유로 관리 포인트가 3개로 늘어나게 됩니다. - Domain Model, DTO, Converter

>> 배보다 배꼽이 더 큰 사태가 발생할 수 있습니다. Domain Model Everywhere가 절대로 좋은 것은 아닙니다. 그렇지만 개발 시간과 관리 포인트를 생각해서 Domain Model Everywhere로 만들고, 그 객체들을 차츰차츰 DTO로 변환시켜가는 과정이 Application을 더욱더 깔끔하게 만드는 과정이 되게 된다고 생각합니다.


결론입니다. 

# 처음에는 Domain Model Everywhere로 Project를 시작합니다.
# Domain Model로 처리하기 힘든 경우에는 DTO를 사용해서 객체의 변환을 합니다.
# 최대한 Pure Domain Model을 유지하기 위해서 계속 노력합니다.

* Domain Model Everywhere를 지원하기 위해서 Spring은 JPA Model Object를 위한 OpenEntityManagerInViewFilter와 Hibernate를  위한 OpenSessionInViewFilter를 지원하고 있습니다. 

지금까지 Domain Model과 Persistence Layer간의 연결에 대해서 알아봤습니다. Domain Layer는 Business Logic이 표현되는 가장 중요한 영역입니다. 그리고 이에 대한 다양한 표현 방법 및 Spring의 사용방법에 대해서 알아봤습니다. 이제 다음 시간부터는 Controller, View를 한번 알아보도록 하겠습니다. 이제 우리가 매일 보는 web page를 만드는 법을 알아보도록 하겠습니다.


Posted by Y2K
,

15. Spring과 iBatis

Java 2013. 9. 11. 14:18

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



Spring에서 myBatis를 사용하기 위해서는 Hibernate는 Spring이 Hibernate에 대한 SessionFactory, DataSource를 제공하고 있는 것에 비해, 오히려 mybatis 측에서 Spring에 대한 library를 제공하고 있습니다. 

pom.xml에 다음과 같은 내용을 추가해주시길 바랍니다. 

<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis-spring</artifactId>
    <version>1.1.1</version>
</dependency>

pom.xml에 적용후, command창에서 mvn dependency:tree 를 한번 실행시켜보시면 다음과 같은 결과를 볼 수 있습니다. 




기본적으로 mybatis-spring은 spring 3.1RELEASE를 가지고 개발된 jar입니다. 따라서, mybatis-spring을 참조하는 즉시, spring 3.1Release가 바로 추가가 됩니다. 그런데, 저희 프로젝트는 기본적으로 Spring 3.2RELEASE를 기반으로 구성되어 있습니다. 이런 경우 어떻게 해야지 될까요?
다행히도, Spring은 하위 호환성을 100% 맞추고 있습니다. 따라서, mybatis-spring에서 사용하고 있는 Spring 3.1 module을 모두 제거하고, Spring 3.2 RELEASE를 모두 사용하도록 maven의 dependency를 조절해줘야지 됩니다. maven에서 dependency 된 jar안에 포함된 다른 module을 제거하는 것은 다음과 같은 포멧으로 pom.xml을 수정해줘야지 됩니다.

이러한 경우에 pom.xml을 수정하는 방법은 2가지가 있습니다. 

1. 각 dependency에서 각 library들을 exclude 시켜주는 방법
2. dependencyManagement에서 각 library들의 version을 관리해주는 방법

개인적으로는 2번째의 방법이 좀 더 나은 것 같습니다. dependencyManagement를 이용해서 mybatis-spring의 dependency 충돌을 해결한 pom.xml의 일부입니다. 

   <repositories>
        <repository>
            <id>mybatis-snapshot</id>
            <name>MyBatis Snapshot Repository</name>
            <url>https://oss.sonatype.org/content/repositories/snapshots</url>
        </repository>
    </repositories>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>3.2.1.RELEASE</version>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-jdbc</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-orm</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-tx</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>3.2.1.RELEASE</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.1.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.22</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-orm</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib-nodep</artifactId>
            <version>2.2.2</version>
        </dependency>
    </dependencies>

이제 dependency를 다시한번 확인해보도록 하겠습니다. 

mybatis-spring에서 의존하고 있던 jar들은 모두 제거가 된 것을 알 수 있습니다. 그리고 Spring에 필요한 jar들이 모두 추가 됨을 볼 수 있습니다.

마지막으로 applicationContext.xml을 추가하도록 하겠습니다. myBatis는 기본적으로 spring에서 제공하는 DataSource를 그대로 이용할 수 있습니다. DataSource를 그대로 이용한다는 것은 TransactionManager를 이용한 @Transactional 역시 사용이 가능하다는 뜻입니다. 지금까지 해왔던 내용대로 설정을 할 수 있습니다. 다만 차이가 있다면 Spring을 사용하지 않고 myBatis를 이용할 때는 SqlSession을 SqlSessionFactory에서 직접 얻어오는 방식을 채택했습니다. 그렇지만, Spring을 이용하는 경우, mybatis-spring에서 제공하는 SqlSessionFactoryBean과 SqlSessionFactoryTemplage을 이용해서 작업을 한다는 차이 이외에는 테스트 코드 자체도 큰 차이가 없습니다.

다음은 applicationContext.xml 입니다. 

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">
  <context:property-placeholder location="classpath:spring.property" />
  <bean id="dataSource"
    class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="${jdbc.driver}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
  </bean>
  <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="configLocation" value="classpath:mybatis.xml" />
  </bean>
  <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg ref="sqlSessionFactory" />
  </bean>
  <bean id="transactionManager"
    class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

dataSource와 transactionManager는 JdbcTemplate을 사용할때와 완전히 동일합니다. 그리고 Hibernate에서 사용하던 LocalSessionFactoryBean 대신에 SqlSessionFactoryBean과 SqlSessionTemplate를 사용하는 차이만을 가지고 있습니다. 

이제 구성된 설정을 이용해서 각 Dao interface들을 얻어보도록 하겠습니다. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@Transactional
public class BookDaoTest {
    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;
    private BookDao bookDao;
    
    @Before
    public void setUp() {
        assertThat(sqlSessionTemplate, is(not(nullValue())));
        bookDao = sqlSessionTemplate.getMapper(BookDao.class);
        bookDao.deleteAll();
        assertThat(bookDao.countAll(), is(0));
    }
    
    @Test
    public void add() {
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.add(book);
        }
    }
    
    @Test
    public void getAll() {
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.add(book);
        }
        List<Book> allBooks = bookDao.getAll();
        for(Book book : allBooks) {
            System.out.println(book);
        }
    }

기존에는 각각의 session에서 commit를 실행시켜줬지만, 이제는 @Transactional을 이용한 세련된 Transaction을 하고 있는 것을 볼 수 있습니다. 사용법 역시 큰 차이가 있지 않으며 Spring과 interface를 조합한 사용 계층에서의 큰 변경 없이 코드의 기술을 완전히 변경을 시킬 수 있는 것을 알 수 있습니다. 
코딩하는 연습을 하는 시간을 갖겠습니다. 나머지 코드들을 모두 작성해주세요. 


매우 간편한 설정 및 사용의 편의성. mybatis가 현업에서 가장 많이 사용되고 있는 이유가 보이시나요? 이렇게 쉬울수는 없다. 라는 느낌이 맞을 정도입니다. 


Summary

지금까지 구성된 bookstore를 myBatis-spring을 이용해서 구성해주세요. 



Posted by Y2K
,

Spring 4.0 에서 제공되는 핵심 기능 변경사항중 가장 주목할 점이 있다면 WebSocket의 지원입니다.


일단 Java에서는 Java EE 7에서 WebSocket에 대한 기본 API는 모두 구성이 마쳐져 있습니다. 당장 WebSocket을 지원하는 Web Page를 구성하기 위해서는 다음과 같은 조건들이 필요합니다.


1. Java EE 7 이상 지원

2. 최신 WAS 지원 (tomcat 8 이상, jetty 9.0.4 이상)


Spring에서는 WebSocket을 다음과 같은 방법으로 지원하고 있습니다.


1. @ServerEndPoint를 이용한 Java 기본 API

2. WebSocketHandler를 이용한 구성 - Spring WebSocket API


Spring WebSocket API의 경우, Java 기본 API와는 구성이 다릅니다. 이와 같은 구성을 갖게 되는 이유는 SocketJS와 같은 WebSocket을 이용하는 다른 API들을 사용할 수 있도록 한번 Wrapping을 거친 구성을 가지게 하는 것이 목표이기 때문입니다.. Spring WebSocket API의 경우, WebSocketHandler가 가장 핵심이 되고, 이를 이용한 코드 구성에 대해서 알아보도록 하겠습니다. 


먼저, pom.xml에 필요한 dependency를 설정합니다.

         <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-websocket</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>


websocket의 경우, servlet 3.0을 사용해야지 되고, websocket에 대한 dependency는 다음과 같습니다.


        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.websocket</groupId>
            <artifactId>javax.websocket-api</artifactId>
            <version>1.0</version>
        </dependency>


간단한 EchoService를 지원하는 WebSocket 지원 WebPage를 구성합니다. 그에 따른 WebSocketHandler는 TextWebSocketHandlerAdapter를 이용합니다. 여담이지만, Spring에서 Adapter라는 접미사가 붙은 객체들은 대부분 상속을 통해서 좀더 편하게 설정들을 할 수 있도록 도와주는 Spring에서 제공하는 일종의 Helper Class 또는 Parent Class 들이라고 할 수 있습니다.


WebSocketHandlerAdapter에서는 다음 3개의 method를 주목할 필요가 있습니다. 


1. afterConnectionEstablished(WebSocketSession session)

: WebSocket connection이 발생되었을 때, 호출되는 method입니다. connection open이 된 후기 때문에, 해줘야지 될 일들을 처리하면 됩니다. 


2. afterConnectionClosed(WebSocketSession session, CloseStatus status)

: WebSocket connection이 끊겼을 때, 호출되는 method입니다. connection close가 된 후, 일을 처리하면 됩니다. 


3. handleMessage(WebSocketSession session, WebSocketMessage<?> message)

: 핵심적인 method입니다. 실질적인 통신 method입니다. socket.accept() 후에 onMessage 에서 처리될 일들을 이곳에서 coding하면 된다고 생각하면 됩니다. 


한번 코드를 확인해보도록 하겠습니다. 


public class EchoWebSocketHandler extends TextWebSocketHandlerAdapter {

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        String payloadMessage = (String) message.getPayload();
        session.sendMessage(new TextMessage("ECHO : " + payloadMessage));
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // Connection이 구성된 후, 호출되는 method
        super.afterConnectionEstablished(session);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // Connection이 종료된 후, 호출되는 method
        super.afterConnectionClosed(session, status);
    }
}


WebSocket을 지원하는 Handler의 구성이 모두 마쳐진 후, WebSocketHandler를 Spring @MVC에 통합하기 위해서는 Config를 다음과 같이 구성합니다. 


@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        // WebSocket을 /echo 에 연결합니다.
        registry.addHandler(echoHandler(), "/echo");

        // SocketJS 지원 url을 /socketjs/echo에 연결합니다.
        registry.addHandler(echoHandler(), "/socketjs/echo").withSockJS();
    }

    @Bean
    public WebSocketHandler echoHandler() {
        return new EchoWebSocketHandler();
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

매우 단순한 구조로 Config를 구성할 수 있습니다. Config에서 주목할 것은 WebSocketConfigurer interface입니다. 이 interface를 통해서 WebSocket Handler를 아주 쉽게 구성할 수 있습니다. 


마지막으로 web.xml을 대신할 DispatcherWebApplicationIntializer 입니다. 특별한 것은 없고, Servlet Configuration에서 만들어진 WebConfig.class를 반환합니다. 


public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { WebConfig.class };
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/" };
    }

    @Override
    protected void customizeRegistration(Dynamic registration) {
        registration.setInitParameter("dispatchOptionsRequest", "true");
    }

}


이제 WebSocket을 테스트 하기 위해 간단한 HTML Page를 구성해보도록 하겠습니다.


echo.html은 다음과 같이 구성할 수 있습니다. 


<!DOCTYPE html>
<html>
<head>
    <title>WebSocket/SockJS Echo Sample (Adapted from Tomcat's echo sample)</title>
    <style type="text/css">
        #connect-container {
            float: left;
            width: 400px
        }

        #connect-container div {
            padding: 5px;
        }

        #console-container {
            float: left;
            margin-left: 15px;
            width: 400px;
        }

        #console {
            border: 1px solid #CCCCCC;
            border-right-color: #999999;
            border-bottom-color: #999999;
            height: 170px;
            overflow-y: scroll;
            padding: 5px;
            width: 100%;
        }

        #console p {
            padding: 0;
            margin: 0;
        }
    </style>

    <script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>

    <script type="text/javascript">
        var ws = null;
        var url = null;
        var transports = [];

        function setConnected(connected) {
            document.getElementById('connect').disabled = connected;
            document.getElementById('disconnect').disabled = !connected;
            document.getElementById('echo').disabled = !connected;
        }

        function connect() {
            if (!url) {
                alert('Select whether to use W3C WebSocket or SockJS');
                return;
            }

            ws = (url.indexOf('socketjs') != -1) ? 
                new SockJS(url, undefined, {protocols_whitelist: transports}) : new WebSocket(url);

            ws.onopen = function () {
                setConnected(true);
                log('Info: connection opened.');
            };
            ws.onmessage = function (event) {
                log('Received: ' + event.data);
            };
            ws.onclose = function (event) {
                setConnected(false);
                log('Info: connection closed.');
                log(event);
            };
        }

        function disconnect() {
            if (ws != null) {
                ws.close();
                ws = null;
            }
            setConnected(false);
        }

        function echo() {
            if (ws != null) {
                var message = document.getElementById('message').value;
                log('Sent: ' + message);
                ws.send(message);
            } else {
                alert('connection not established, please connect.');
            }
        }

        function updateUrl(urlPath) {
            if (urlPath.indexOf('socketjs') != -1) {
                url = urlPath;
                document.getElementById('sockJsTransportSelect').style.visibility = 'visible';
            }
            else {
              if (window.location.protocol == 'http:') {
                  url = 'ws://' + window.location.host + urlPath;
              } else {
                  url = 'wss://' + window.location.host + urlPath;
              }
              document.getElementById('sockJsTransportSelect').style.visibility = 'hidden';
            }
        }

        function updateTransport(transport) {
          transports = (transport == 'all') ?  [] : [transport];
        }
        
        function log(message) {
            var console = document.getElementById('console');
            var p = document.createElement('p');
            p.style.wordWrap = 'break-word';
            p.appendChild(document.createTextNode(message));
            console.appendChild(p);
            while (console.childNodes.length > 25) {
                console.removeChild(console.firstChild);
            }
            console.scrollTop = console.scrollHeight;
        }
        function clear() {
            $('#message').html('');
        }
    </script>
</head>
<body>
<div>
    <div id="connect-container">
        <input id="radio1" type="radio" name="group1" onclick="updateUrl('/tutorial01/echo');">
            <label for="radio1">W3C WebSocket</label>
        <br>
        <input id="radio2" type="radio" name="group1" onclick="updateUrl('/tutorial01/socketjs/echo');">
            <label for="radio2">SockJS</label>
        <div id="sockJsTransportSelect" style="visibility:hidden;">
            <span>SockJS transport:</span>
            <select onchange="updateTransport(this.value)">
              <option value="all">all</option>
              <option value="websocket">websocket</option>
              <option value="xhr-polling">xhr-polling</option>
              <option value="jsonp-polling">jsonp-polling</option>
              <option value="xhr-streaming">xhr-streaming</option>
              <option value="iframe-eventsource">iframe-eventsource</option>
              <option value="iframe-htmlfile">iframe-htmlfile</option>
            </select>
        </div>
        <div>
            <button id="connect" onclick="connect();">Connect</button>
            <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
        </div>
        <div>
            <textarea id="message" style="width: 350px">Here is a message!</textarea>
        </div>
        <div>
            <button id="echo" onclick="echo();" disabled="disabled">Echo message</button>
        </div>
    </div>
    <div id="console-container">
        <div id="console"></div>
    </div>
</div>
</body>
</html>


이제 구성된 Page의 테스트를 위해 maven jetty를 build 항목에 추가합니다. 


    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.8</version>
                <configuration>
                    <downloadSources>true</downloadSources>
                    <downloadJavadocs>true</downloadJavadocs>
                    <wtpversion>2.0</wtpversion>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.0.4.v20130625</version>
                <configuration>
                    <webApp>
                        <contextPath>/${project.artifactId}</contextPath>
                    </webApp>
                </configuration>
            </plugin>
        </plugins>
    </build>


구성후, mvn jetty:run 명령어를 통해서 실행이 가능합니다. 


websocket-tutorial01.zip





Posted by Y2K
,

14. iBatis (myBatis)

Java 2013. 9. 10. 10:27

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



ibatis (mybatis)

지금까지 직접 JDBC를 이용한 DB접속 방법과 Hibernate를 이용한 DB 접속 방법을 알아봤습니다.
DB라는 관계형 데이터를 java라는 객체 지향의 언어에 mapping을 시키는 과정은 참 여러가지 기술적인 요인들과 방법들을 계속해서 내보이고 있습니다.
그 중에서 SQL을 직접 사용하는 가장 low level의 JDBC, 그리고 SQL을 최대한 사용하지 않고 객체만으로 표현하는 Hibernate는 그 대표적인 기술이라고 할 수 있습니다. 그런데, 국내에서는 다른 기술을 더 많이 쓰고 있는 것이 사실입니다. 

iBatis(아래 부터는 myBatis)가 바로 그 기술입니다. 다른 것보다는 이 기술은 직접 sql을 사용한다는 큰 차이를 가지고 있습니다. 그렇지만, 기존에 우리가 만들었던 것과 같은 entity-dao-service layer를 충실히 지킬 수 있으며 기존의 SP와 같은 legacy sql query를 직접적으로 사용하고, code로서 관리한다는 점이 가장 큰 차이입니다.

iBatis는 JDBC를 이용한 반복적인 코드를 획기적으로 줄이는 것을 목표로 가지고 있으며, 개발자들에게 게을러질 수 있는 권리를 보장하고 있습니다. 

ibatis의 구조

Hibernate와 비슷하게 SqlSessionFactory, SqlSession이라는 두개의 객체를 가지고 있습니다. 기본적으로 mybatis.cfg.xml 파일을 이용해서 DB에 대한 연결 설정과 각각의 mapper를 구성하는 설정으로 구성되어 있습니다.

ibatis에서 DB에 대한 연결을 구성하는 방법은 다음과 같습니다. 

1. SqlSessionFactory 구성 
2. SqlSessionFactory를 통한 SqlSession을 구성
3. SqlSession을 통해 mapper를 구성

여기서 새로운 개념들이 조금 나오게 되는데요. Hibernate에서 많은 개념들을 차용해온것들이 나오게 됩니다. 

먼저, SqlSessionFactory는 이름 그대로 SessionFactory입니다. 

일단 SqlSession을 통해서 얻어지는 mapper는 기본적으로 dao 객체들입니다.

 
백문이 불여 일타. 한번 지금까지 만들어진 코드를 구성해보도록 합시다. 

기존 jdbcTemplate에서 사용한 Book, User, History 객체와 Dao에 대한 interface를 모두 카피해와서 새로운 프로젝트를 구성합니다.
myBatis는 maven central repository에 없습니다. 다음 repository 설정을 추가하고, dependency를 추가하도록 합니다. 이와 같이 maven central repository에서 지원하지 않는 library들은 자신만의 repository를 갖는 경우가 많습니다. 그리고 사내에서는 nexus라는 maven server를 설치해서, 사내 repository를 구성해서 사용하는 경우도 많습니다.


  <repositories>
    <repository>
      <id>mybatis-snapshot</id>
      <name>MyBatis Snapshot Repository</name>
      <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </repository>
  </repositories>

그 후, myBatis를 추가합니다.
   <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.1.1</version>
    </dependency>

   
myBatis에서 DB에 접근하기 위한 순서인 SqlSessionFactory를 먼저 구성해보도록 하겠습니다. 
SqlSessionFactory는 xml 파일로 설정하게 되며, 다음과 같이 설정할 수 있습니다. 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="bookstore">
    <environment id="bookstore">
      <transactionManager type="JDBC" />
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost/bookstore" />
        <property name="username" value="root" />
        <property name="password" value="qwer12#$"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/userDao.mapper.xml" />
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/bookDao.mapper.xml" />
    <mapper resource="com/xyzlast/mybatis/bookstore/dao/mappers/historyDao.mapper.xml"/>
  </mappers>
</configuration>

먼저 myBatis는 한개의 xml에 여러개의 connection 정보들을 담을 수 있습니다. 각각의 환경에 대한 id를 설정하고, 그 환경에 대한 기본 설정을 해주게 됩니다. DataSource에서 자주 보던 driver, url, username, password에 대한 설정을 하게 되는 것을 알 수 있습니다. 그리고 mapper들을 설정해주게 됩니다. 이 mapper들은 xml로 구성되어 있으며, 1 개의 method에 1:1로 mapping되는 SQL query가 들어가 있습니다. 일반적으로 interface 이름 + mapper.xml 의 명명 규칙을 따르게 됩니다.

다음은 bookDao.mapper.xml 파일의 내용입니다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xyzlast.mybatis.bookstore.dao.BookDao">
  <resultMap type="com.xyzlast.mybatis.bookstore.entities.Book" id="BookResult">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <result property="author" column="author"/>
    <result property="publishDate" column="publishDate"/>
    <result property="rentUserId" column="rentUserId"/>
    <result property="status" column="status" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
  </resultMap>

  <select id="get" parameterType="int" resultMap="BookResult">
    SELECT * FROM books WHERE id = #{bookId}
  </select>
  <select id="getAll" resultMap="BookResult">
    SELECT * FROM books
  </select>
  <select id="countAll" resultType="int">
    SELECT count(*) FROM books
  </select>
  <insert id="add" parameterType="com.xyzlast.mybatis.bookstore.entities.Book" useGeneratedKeys="true" keyColumn="id">
    INSERT INTO books(name, author, publishDate, comment, status, rentUserId)
    VALUES(#{name}, #{author}, #{publishDate}, #{comment}, #{status.value}, #{rentUserId})
  </insert>
  <delete id="delete" parameterType="int">
    DELETE FROM books WHERE id = #{bookId}
  </delete>
  <delete id="deleteAll">
    DELETE FROM books
  </delete>
  <update id="update" parameterType="com.xyzlast.mybatis.bookstore.entities.Book">
    UPDATE books SET
    name = #{name}, author = #{author}, publishDate=#{publishDate}, status=#{status.value}, rentUserId=#{rentUserId}
    WHERE id = #{id}
  </update>
  <select id="search" parameterType="String" resultMap="BookResult">
    SELECT * FROM books
    WHERE name like "%${value}%"
  </select>
</mapper>


먼저, 가장 주시해서 봐야지 되는 것은 mapper의 namespace입니다. namespace에는 interface에 대한 package명이 포함된 full name이 들어가야지 됩니다. 이 부분에 대한 설정이 잘못 되어 있으면 사용할 수 없습니다. 그 다음으로 봐야지 될 것은 resultMap입니다. return되는 query문의 결과가 어떤 DTO/VO 객체에 어떤 property에 mapping되는지에 대한 설정이 여기에 기록이 됩니다. 재미있는 것이 BookStatus enum값의 mapping입니다. org.apache.ibatis.type.EnumOrdinalTypehandler를 이용해서 enum값과 BookStatus를 mapping시킬 수 있습니다. 

BookDao interface와 mapping.xml 파일을 한번 비교해보도록 하겠습니다.




query를 각각 type에 맞추어 select/insert/delete/update로 나눠 등록을 하게 됩니다. 또한 query의 id는 각 method의 이름과 1:1로 mapping이 되게 됩니다. myBatis를 이용하는 경우에는 interface에 대한 객체를 따로 만들지 않기 때문에 코딩양이 줄어 들 수 있습니다. 또한, sql query를 project에서 관리하고 있기 때문에 query에 대한 관리 역시 용의한 장점을 가지고 있습니다. 

이제 SqlSessionFactory를 얻어내는 과정은 모두 완료되었습니다. SqlSessionFactory는 application에서 딱 1개만 존재하면 됩니다. 이제 이 SqlSessionFactory에서 SqlSession을 얻어오는 과정은 Hibernate에서 SessionFactory를 얻어내는 과정과 완전히 동일합니다. 다음은 BookDao의 테스트 코드의 일부입니다. 

public class BookDaoTest {
    private SqlSession session;
    private BookDao bookDao;
    private SqlSessionFactoryGenerator sqlSessionFactoryGenerator;

    @Before
    public void setUp() throws IOException {
        sqlSessionFactoryGenerator = new SqlSessionFactoryGenerator();
        sqlSessionFactoryGenerator.setXmlFilename("mybatis.xml");
        SqlSessionFactory factory = sqlSessionFactoryGenerator.getSqlSessionFactory();

        session = factory.openSession();
        bookDao = session.getMapper(BookDao.class);
        bookDao.deleteAll();
        session.commit();
        assertThat(bookDao.countAll(), is(0));
    }

    @After
    public void tearDown() {
        session.close();
    }

    @Test
    public void add() {
        int count = bookDao.countAll();
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.add(book);
            session.commit();
            count++;
            assertThat(bookDao.countAll(), is(count));
        }
    }

SqlSessionFactory를 통해, SqlSession을 얻어내고 update/delete/insert에 대한 commit을 직접 행하도록 코드를 작성했습니다. 또한 모든 method가 완료되면 Hibernate와 동일하게, 반드시 Session을 닫아줘야지만 됩니다. 

Hibernate는 객체에 대한 xml 설정 또는 annotation 설정을 하는데 반하여, myBatis는 action에 설정을 하는 것을 주목해주세요. 이 둘의 차이는 매우 큰 차이를 가지고 오게 됩니다. 그리고, 지금 객체를 entities package에서 얻어오게 되었지만, 실질적으로 이 객체는 DTO또는 VO 객체가 됩니다. DB에서 값을 가지고 오는 역활만을 담당하는 객체로 보고 개발을 진행하는 것이 좋습니다. 


Summary

myBatis를 이용한 DAO 객체에 대해서 알아봤습니다. 지금까지 구성한 서비스까지 한번 구현해보세요. 이번에는 Spring을 사용하지 않고 구현하는 것이 목표입니다. 다음 장에서는 Spring을 이용해서 더욱더 간단하게 myBatis의 설정을 구축하는 것을 보여드리도록 하겠습니다. 





Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


queryDSL과 Spring Data JPA 모두 DAO에 대한 접근 방법을 제어하고, 코드양을 줄일 수 있는 좋은 방법입니다. 그렇지만, Spring Data JPA의 경우에는 규격대로 되어 있는 select 구문, update 구문 이외에는 사용할 수 없는 단점을 가지고 있습니다. 이러한 단점을 극복하는 좋은 방법은 Spring Data JPA에 queryDSL을 결합해서 사용하는 것입니다. 이렇게 되면, Spring Data JPA와 queryDSL의 모든 장점을 사용할 수 있습니다. 

queryDSL과 Spring Data JPA를 연동해서 사용하기 위해서는 maven repository에 다음과 같은 설정을 해야지 됩니다. 

    <repository>
      <id>spring-snapshot</id>
      <name>Spring Maven SNAPSHOT Repository</name>
      <url>http://repo.springsource.org/libs-snapshot</url>
    </repository>

그리고 버젼에 유의해서 Spring Data JPA와 queryDSL을 설정해줘야지 됩니다. queryDSL이 2.x에서 3.x대로 넘어가면서 객체의 이름이 많이 변경이 되었기 때문에 상호간의 호환에 문제가 있습니다. queryDSL 버젼은 3.1로, Spring Data JPA의 버젼은 1.4.0.BUILD.SNAPSHOT으로 설정해줍니다. 지금 queryDSL을 지원하는 Spring Data JPA의 경우에는 beta version이지만, 곧 정식버젼이 나온다고 하니 잠시 기다릴수 있을 것 같습니다.

다음은 설정될 maven pom 파일의 properties입니다. 

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
    <hibernate.version>4.1.10.Final</hibernate.version>
    <hibernate.validator.version>4.3.1.Final</hibernate.validator.version>
    <spring.version>3.2.2.RELEASE</spring.version>
    <querydsl.version>3.1.0</querydsl.version>
    <spring.jpa.version>1.4.0.BUILD-SNAPSHOT</spring.jpa.version>
  </properties>

이렇게 설정을 모두 마치고 코드를 한번 다시 살펴보도록 하겠습니다.

QueryDslPredicateExecutor

select 구문의 핵심입니다. QueryDslPredicateExecute는 JpaRespository interface와 같이 사용할 수 있습니다. interface는 다음과 같은 method를 포함합니다. 

public interface QueryDslPredicateExecutor<T> {
    T findOne(Predicate predicate);
    Iterable<T> findAll(Predicate predicate);
    Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
    Page<T> findAll(Predicate predicate, Pageable pageable);
    long count(Predicate predicate);
}


Predicate는 queryDSL의 query입니다. 이 interface를 상속하는 경우 BookRepository의 코드는 다음과 같이 구성될 수 있습니다. 

@Repositorypublic interface BookDao extends JpaRepository<Book, Integer>, QueryDslPredicateExecutor<Book> {

}

다른 코드는 전혀 필요없습니다. 이제 query를 한번 구성해보도록 하겠습니다. 

search에 대한 코드를 구성해보도록 하겠습니다. Spring Data JPA는 like 구문을 지원하지만, 모든 전/후에 대한 Like만 지원되기 때문에, 기존의 코드로는 구성이 불가능했습니다. 
다음과 같은 코드로 구성이 가능합니다. 

    @Test
    public void testProdicate() {
        String bookName = "책이름";

        QBook qBook = QBook.book;
        Predicate predicate = qBook.name.like(bookName + "%").and(qBook.status.eq(BookStatus.CanRent));
        Iterable<Book> books = bookDao.findAll(predicate);

        for(Book book : books) {
            assertThat(book.getName().contains(bookName), is(true));
        }
    }

Type에 safe 하고, 간단한 query 구문으로 code를 구성하는 것이 가능합니다. findAll method가 queryDSL의 Predicate를 지원하기 때문에 이러한 코드를 구성할 수 있습니다.
findAll을 이용하는 경우, 이제 select에 대한 이슈는 거의 해소가 가능합니다. 그래도 아직 문제가 조금 더 남아있습니다. 그 부분은 바로 각 항목에 대한 max, min 값을 구하는 Predicate query와 다양한 update,delete를 하는 query들을 만들어주는 것이 아직은 불가능합니다. 이러한 문제를 해결하고, queryDSL의 모든 기능을 지원하기 위해서는 QueryDslRepositorySupport를 사용해야지 됩니다. 

QueryDslRepositorySupport

QueryDslPredicateExecutor의 경우에는 select를 이용해서 객체를 얻어올 때 주로 사용됩니다. 그렇지만, 이에 대한 다른 접근이 필요합니다. 객체에 대한 update, delete의 경우에는 좀더 다양하게 처리하는 것이 필요합니다. 

예를 들어 다음 query를 처리할 때, hibernate의 경우에는 HQL 또는 Native query를 이용해서 처리하는 방법밖에는 없습니다. 

update users set name = 'ykyoon' where name='abc';

delete from users where name='abc';

이러한 일괄 변경 및 업데이트, 또는 특정 값의 sum, min, max, average 값을 도출하기 위해서 사용하는 것이 QueryDslRepositorySupport입니다. QueryDslRepositorySupport의 code를 한번 알아보도록 하겠습니다. 

@Repository
public abstract class QueryDslRepositorySupport {

    private final PathBuilder<?> builder;

    private EntityManager entityManager;
    private Querydsl querydsl;

    public QueryDslRepositorySupport(Class<?> domainClass) {
        Assert.notNull(domainClass);
        this.builder = new PathBuilderFactory().create(domainClass);
    }

    @PersistenceContext
    public void setEntityManager(EntityManager entityManager) {

        Assert.notNull(entityManager);
        this.querydsl = new Querydsl(entityManager, builder);
        this.entityManager = entityManager;
    }

    @PostConstruct
    public void validate() {
        Assert.notNull(entityManager, "EntityManager must not be null!");
        Assert.notNull(querydsl, "Querydsl must not be null!");
    }

    protected EntityManager getEntityManager() {
        return entityManager;
    }

    protected JPQLQuery from(EntityPath<?>... paths) {
        return querydsl.createQuery(paths);
    }

    protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
        return new JPADeleteClause(entityManager, path);
    }

    protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
        return new JPAUpdateClause(entityManager, path);
    }

    @SuppressWarnings("unchecked")
    protected <T> PathBuilder<T> getBuilder() {
        return (PathBuilder<T>) builder;
    }

    protected Querydsl getQuerydsl() {
        return this.querydsl;
    }
}

abstract class 이기 때문에, 상속을 받아서 구현해야지 됩니다. 먼저, User Id 값중 max 값을 얻어오는 code를 한번 알아보도록 하겠습니다.

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }


다음은 Update입니다. 

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }


마지막으로 Delete입니다. 

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }

매우 간단하게 구현하는 것이 가능합니다. 우리가 지금까지 만든 JpaRepository와 QueryDslPredictExecutor와 같이 사용하는 경우에는 거의 모든 query들을 처리하는 것이 가능하게 됩니다. 전체 코드는 다음과 같습니다. 

@Repository
public class CalDao extends QueryDslRepositorySupport {
    public CalDao() {
        super(User.class);
    }

    public int sumUserIds() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.id.max());
    }

    public Long updateNameByName() {
        QUser qUser = QUser.user;
        update(qUser).where(qUser.name.eq("abcde")).set(qUser.name, "가나다").execute();
        return 0L;
    }

    public void deleteByName(String name) {
        QUser qUser = QUser.user;
        delete(qUser).where(qUser.name.eq(name)).execute();
    }
}

queryDSL + Spring Data JPA에서의 Hibernate 이용

사용하다보면 Hibernate의 기능을 사용해야지 될 때가 발생할 수 있습니다. 이는 기존의 coding이 Hibernate 기준으로 된 경우도 해당될 수 있을 것이고, Hibernate의 다양한 Criteria를 이용해보고 싶은 생각도 생길 수 있습니다. 
먼저, 계속해서 이야기드리는 것은 JPA라는 규격자체가 Hibernate라는 구현체 위에서 동작하는 것이기 때문에 Hibernate의 Session을 얻어내는것도 가능합니다. 다음과 같은 코드로 Session을 얻어서 Hibernate와 동일하게 처리하는 것도 가능합니다. 

    public Session getSession() {
        return (Session) getEntityManager().getDelegate();
    }


Summary

지금까지 queryDSL과 Spring Data JPA를 이용한 Model 구성에 대해서 알아봤습니다. 약 2개월간 계속해서 장/단을 뽑아보면서 제일 우리에게 좋은 Model Framework조합이 무엇인가를 고민했던 결과입니다. 이러한 개발 방법은 다음과 같은 장점을 가지고 있습니다. 

1. type-safe 한 query를 만들 수 있습니다.
2. 객체지향적인 코드 구성이 가능합니다.
3. Hibernate Criteria의 단점인 일괄 update / delete 문의 처리가 가능합니다.
4. Repository의 코딩양을 줄일 수 있습니다.
5. eclipse의 intellisense의 지원을 받을 수 있습니다.

최종적으로 구성되는 package입니다. 이 구성으로 대부분의 Model에 관련된 Project가 구성이 될 예정입니다. 




# config : ApplicationConfiguration class가 위치할 package입니다.
# entities : entity 객체들이 위치할 package입니다.
# repositories : JpaRepository, QueryDslPredictExecutor를 상속받은 Repository interface가 위치할 package입니다.
# repositories.support : QueryDslRepositorySupport를 상속받은 객체 또는 repositories에서 사용할 Predict를 지원하는 객체들이 위치할 package입니다.
# services : BL에 관련된 서비스가 위치할 package입니다.
# utils : 데이터의 변환 등 다양한 경우에 사용되는 utility class가 위치하는 package입니다.

여기서 지금까지 이야기하던 Dao와 Repository에 대한 정의를 다시 할 필요성이 있습니다. 

이 둘에 대한 정의는 다음과 같습니다. 

"DAO는 데이터베이스에서 값을 꺼내와 도메인 오브젝트로 반환해주거나 적절한 값으로 반환해주는 계층을 일컫는다. Repository는 한 도메인 오브젝트에 대해서 객체의 값을 보증해주기 위해 도메인 내부에서 데이터베이스와 소통하는 객체을 일컫는다."

말이 어렵습니다. 조금 더 단순하게 말하면....

# DAO는 DB과 연결되어, Domain Object(=Entity)로 변환하는 것을 의미합니다.
# Repository는 DB와 연결되어, Domain Object(=Entity)로 변환합니다. 단, Domain 내부에서만 사용됩니다.

뒤의 조건이 하나가 더 붙어있으면 Repository이고, DB에 접속하는 객체를 어느곳에서나 사용가능해야지 되면 DAO입니다. Repository pattern이란, DB에 접근하는 영역을 Domain Layer, 즉 Model 영역에서만 사용하게 되는 것을 의미하고, 이것은 n-tier system에 적합한 책임영역의 분리가 되는 코드를 의미합니다. 
만약에 Controller Layer에서 DB에 접근하게 되고, 사용하게 된다면, 그것은 더 이상 Repository가 될 수 없습니다. 그렇지만, Model/Domain Layer에서만 접근하게 된다면 이것은 DAO가 아닌 Repository가 되게 됩니다. 너무나 유사한 개념이지만, 영역을 나누는 의미에서 사용하는 용어의 차이라고 생각하시면 될 것 같습니다. 
우리가 개발하는 것은 DAO가 아닌 Repository가 되게 됩니다. (이것은 저도 잘못 개발하고 있던 내용중 하나입니다.) 이 둘간의 영역 차이는 매우 자주 나오는 문제입니다. 만약에 서비스가 아닌 다른 영역(BL이 아닌 다른 영역)에서 DB에 접근하게 되는 객체를 만든다면.. 이것은 DAO를 만들어주게 되는 것이 맞습니다. 
이건 pattern입니다. 무엇이 옳고 그른 문제는 아닙니다. Layer에 의한 명백한 의미를 나누기 위해서 Repository로 사용하는 것이 좀 더 나을 것 같습니다. 

정리입니다. 
queryDSL + Spring Data JPA를 이용한 Domain Layer의 개발을 주로 하게 될 것입니다. 이 부분은 지금 Open Source 측에서도 밀고 있는 추세이기도 하고, 계속해서 발전이 되어가고 있는 분야이기도 합니다. 
이제 다음시간에는 만약에 우리가 외부 SI를 나가게 된다면, 주로 사용하고 있는 myBatis(iBatis)에 대해서 간략하게 알아보도록 하겠습니다. 


Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



queryDSL과 Spring Data JPA에 대한 소개를 하기 전에 먼저 JPA에 대한 설명을 하도록 하겠습니다.

JPA란?

Java Persistence API의 약자입니다. 
영속성 객체에 대한 Java의 접근 방법을 정의한 API로, 객체를 통한 Persistence 영역의 접근을 제공하고 있습니다. 

객체를 통한 Persistence 영역에 대한 접근을 하기 위해, 사용되는 것이 ORM Framework 들이고, 그중에서 가장 선두를 달리고 있는 것이 Hibernate입니다. 

Hibernate와 JPA간의 차이를 한번 알아보도록 하겠습니다. 


HibernateJPA
DB에 대한 접근(DataSource)SessionFactoryEntityManagerFactory
DB의 query executorSessionEntityManager
INSERT 대응 methodsave | saveOrUpdatemerge
UPDATE 대응 methodupdate | saveOrUpdatemerge
DELETE 대응 methoddeleteremove
query 적용 방법Criteria, HQL (Hibernate query Language), Native SQL
* Criteria : session.createCriteria
* HQL : session.createQuery
* Native SQL : session.createSQLQuery
Criteria, JPQL (java persistence query language), Native SQL
* Criteria의 문법은 Hibernate와 큰차이를 보이고 있음. JPA3.0에서는 기존의 Hibernate와 통일성을 주는 방향으로 변화가능성이 있다고 이야기함
* JPQL의 문법은 HQL과 완전 동일
* JPQL : em.createQuery
* Native SQL : em.createNativeQuery

접근 하는 방식은 거의 1:1로 동일합니다. 다만 query의 적용방법에서 차이를 보이게 되는데요. HQL이 JPA의 표준이 되면서, 기존 Hibernate에서 주로 사용되고 있던 Criteria보다 근간에는 JPQL 또는 HQL을 더 많이 사용하게 되는 추세입니다. 다음 JPA 버젼에서는 Criteria의 문법변경 이야기가 있으니, 그 때는 다시 역전되지 않을까.. 하는 생각을 가지고 있습니다. 

지금부터 소개할 queryDSL과 Spring Data JPA는 JPA를 기반으로 하는 기술입니다. 
소개드릴 queryDSL과 Spring JPA Data는 다음과 같은 장점이 있습니다. 

1. Hibernate와 같은 save, update, delete를 사용할 수 있습니다.
2. type-safe 한 query를 작성 가능합니다.
3. sql 작성자들과 query를 code로 옮기기가 용의합니다.
4. DAO logic의 코드양이 획기적으로 줄어들 수 있습니다.

이번 장에서는 Hibernate, JPA를 기반으로 하는 Model에 대한 library들을 좀 더 알아보도록 하겠습니다. 

queryDSL

.NET에서 linq가 나온 후, linq의 사상을 옮겨온 Framework입니다. type-safe한 query 작성 및 Criteria 보다 보기 편한 query를 만드는 것이 목적입니다.

queryDSL은 query에 대한 Q-Object를 이용해서 마치 sql query문과 비슷하게 query문을 작성할 수 있습니다. 매우 깔끔하게 보이기도 하고, .NET에서 linq를 하던 사람들에게 좀 더 익숙하기도 한 개발 방법입니다. queryDSL을 사용하면 다음과 같은 query 문을 작성해서 처리 가능합니다. 다음은 queryDSL을 이용한 select query입니다. 

List<Customer> result = query.from(customer)
    .where(customer.lastName.like("A%"), customer.active.eq(true))
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);    

select * from customer where lastname like 'A%' and active = 1 order by lastname asc, firstname desc;

보시면 query문과 매우 비슷한 구조를 가지고 있습니다.

queryDSL과 나중에 소개드릴 Spring Data JPA는 java persisance area 표준에 따르고 있습니다. JPA라고 불리우는 이 표준은 Hibernate가 주축이 되어, Hibernate의 ORM적 구성이 Java의 표준이 되어버린 구성입니다. 지금 구성된 queryDSL은 Hibernate와 같은 JPA 상에서 구성되는 Library입니다. 따라서, Hibernate로 구성된 BookStore가 그대로 사용되어질 수 있습니다. 

개발 순서는 다음과 같습니다. 

1. Hibernate ORM 규칙에 따른 JPA annotation을 이용한 객체 정의
2. maven을 이용한 compile
3. Query Definition을 이용한 query 작성

먼저 1번 항목인 Hibernate ORM 규칙에 따른 JPA annotation을 모두 구성한 프로젝트에 다음 library들을 모두 추가합니다. (version에 유의해주세요!)
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.build.resourceEncoding>UTF-8</project.build.resourceEncoding>
    <hibernate.version>4.1.10.Final</hibernate.version>
    <hibernate.validator.version>4.3.1.Final</hibernate.validator.version>
    <spring.version>3.2.2.RELEASE</spring.version>
    <querydsl.version>3.1.0</querydsl.version>
  </properties>

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>${hibernate.validator.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-core</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-apt</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-jpa</artifactId>
      <version>${querydsl.version}</version>
    </dependency>
    <dependency>
      <groupId>com.mysema.querydsl</groupId>
      <artifactId>querydsl-sql</artifactId>
      <version>${querydsl.version}</version>
    </dependency>

그리고 build > plugins 에 다음 plugin을 추가합니다. 

      <plugin>
        <groupId>com.mysema.maven</groupId>
        <artifactId>maven-apt-plugin</artifactId>
        <version>1.0.4</version>
        <executions>
          <execution>
            <goals>
              <goal>process</goal>
            </goals>
            <configuration>
              <outputDirectory>target/generated-sources/java</outputDirectory>
              <processor>com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
            </configuration>
          </execution>
        </executions>
      </plugin>

그리고 command 창에서 mvn compile을 실행해서 code를 compile합니다. 
compile 후, target directory의 generated-sources를 봐보시길 바랍니다. 안에 보시면 Query 코드들이 만들어져 있습니다. 


이제 target/java 에서 우클릭해서, Use Source Code를 선택해주세요.


이제 queryDSL을 사용할 준비를 모두 마쳤습니다. 

기존 코드인 countAll과 search를 한번 비교해보도록 하겠습니다. 

Hibernate Criteria
    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        Long count = (Long) session.createCriteria(Book.class)
                      .setProjection(Projections.rowCount()).uniqueResult();
        if(count == null) {
            return 0;
        }
        return count.intValue();
    }

    @Override
    public List<Book> search(String name) {
        Session session = sessionFactory.getCurrentSession();
        @SuppressWarnings("unchecked")
        List<Book> books = session.createCriteria(Book.class)
                                  .add(Restrictions.like("name", name, MatchMode.ANYWHERE))
                                  .list();
        return books;
    }

queryDSL
    @Override
    public int countAll() {
        HibernateQuery query = new HibernateQuery(sessionFactory.getCurrentSession());
        QBook qBook = QBook.book;
        Long count = query.from(qBook).uniqueResult(qBook.count());
        return count.intValue();
    }

    @Override
    public List<Book> search(String name) {
        HibernateQuery query = new HibernateQuery(sessionFactory.getCurrentSession());
        QBook qBook = QBook.book;
        return query.from(qBook).where(qBook.name.like("%" + name + "%")).list(qBook);
    }

마치 직접 query를 사용하고 있는 것과 비슷한 문법을 보여줍니다. 이 방법은 Hibernate의 Criteria를 사용할 때 발생할 수 있는 property의 직접 타이핑에 의한 에러를 방지할 수 있으며, 개발자들이 보다더 쉽게 query를 작성할 수 있도록 도와주게 됩니다. 
기존의 CUD는 Session의 save, update, delete를 기존과 같이 사용하고, Read method만 queryDSL을 사용해서 개발의 속도를 향상하고, 오타를 방지할 수 있습니다. 

다음은 queryDSL의 예시입니다. (queryDSL homepage에서 보다 많은 예시를 볼 수 있습니다.)

JOIN
QCat cat = QCat.cat;
QCat mate = new QCat("mate");
QCate kitten = new QCat("kitten");
query.from(cat)
    .innerJoin(cat.mate, mate)
    .leftJoin(cat.kittens, kitten)
    .list(cat);
from Cat as cat
    inner join cat.mate as mate
    left outer join cat.kittens as kitten

Order
QCustomer customer = QCustomer.customer;
query.from(customer)
    .orderBy(customer.lastName.asc(), customer.firstName.desc())
    .list(customer);
from Customer as customer
    order by customer.lastName asc, customer.firstName desc

Group By
query.from(customer)
    .groupBy(customer.lastName)
    .list(customer.lastName);
select customer.lastName
    from Customer as customer
    group by customer.lastName

매우 많은 query 문이 존재하고, sql query로 대부분이 작성 가능합니다. 꼭 한번 사용해보시길 바랍니다. 

하나 더 말씀드린다면, 이 부분은 지금 저희 개발에서 Domain의 main 기술이 될 것입니다. Hibernate를 바로 사용하는 것에 대한 부담을 해결할 수 있습니다. 그리고 query를 만드는 개발 패턴을 좀 더 효율적으로 구성할 수 있다는 장점을 가지고 있습니다. 



Spring Data JPA

Spring Data JPA는 ORM Framework입니다. JPA 기반의 repository를 아주 빨리, 쉽게 개발할 수 있는 방법을 제시하고 있습니다. 지금까지 보던 모든 기술들(JdbcTemplate, Hibernate, myBatis)에서 repository는 객체만 바뀔뿐, 코드의 중복은 계속해서 나타나게 됩니다. 특히 우리가 CRUD라고 부르는 영역의 CUD 코드의 경우에는 거의 대부분이 중복이 되는 경우가 많습니다. 이러한 문제점을 착안하여 Spring Data - JPA project가 시작되게 되었습니다. 

Spring Data JPA는 단독으로 동작하는 ORM Framework가 아닙니다. JPA ORM engine을 기반으로 동작하는 ORM Framework입니다. 여기에서 JPA ORM engine이 될 수 있는 표준적인 JSR 220 을 만족하는 ORM engine은 다음과 같습니다. 

# Hibernate
# Google App Engine for JAVA
# TopLink

Google App Engine이 RDBMS를 완벽하게 지원하지 못하고, TopLink의 경우에는 Oracle에서 판매하는 상용제품이며 Oracle DB에만 특화가 되어있는 ORM입니다. 그렇다면, 실질적으로 지금 사용할 수 있는 선택의 폭은 Hibernate 밖에는 존재하지 않습니다. 지금 상태로는 Spring Data JPA는 Hibernate를 기반으로 동작하는 ORM Framework이다. 라고 생각해주시면 좋을 것 같습니다. 

Spring Data JPA를 사용하기 위해서는 다음을 추가합니다. (Hibernate 프로젝트 생성에서 시작합니다.)

    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-jpa</artifactId>
      <version>1.3.0.RELEASE</version>
    </dependency>

Spring Data JPA는 Spring 중 다음 jar 들과 연관관계를 갖습니다. 

spring-core, spring-beans, spring-context, spring-orm, spring-aop, spring-tx

이제 JPA의 기반이 되는 Hibernate를 추가해주도록 하겠습니다. 

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>${hibernate.version}</version>
    </dependency>

마지막으로 Spring Framework를 모두다 추가하도록 하겠습니다. 

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>${spring.version}</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>${spring.version}</version>
    </dependency>

이제 pom.xml 설정이 모두 마쳐졌습니다. 

조금 장황할정도로 pom.xml 에 설정이 많이 들어가게 됩니다. 

Spring JPA Data는 Dao/Repository의 반복적인 코드가 될 수 있는 CUD를 자동화된 코드로 지원합니다. Spring JPA Data에서 제공하는 interface는 다음과 같습니다.

# Repository
# CrudRepository
# JpaRepository

이 중에서 우리가 사용할 녀석은 다른 interface를 모두 상속한 JpaRepository<T, C>입니다. JapRepository의 선언은 전에 Hibernate의 GenericDao의 선언과 거의 동일합니다. 단 두번째 인자가 PK에 대한 객체 Type을 넣어주는것만 다릅니다. Book, User, History 모두 int를 사용하고 있기 때문에, Integer를 두번째 Type에 넣어주면 됩니다. 
코드는 Hibernate에서 작성된 annotation이 모두 적용된 entity 객체들을 모두 project에 copy를 합니다. 먼저 단순하게 BookDao interface를 다음과 같이 작성합니다.

public interface BookDao extends JpaRepository<Book, Integer> {

}

interface 구현이 아닌 interface 상속을 통해서 구현이 되어 있는 것을 주의해주세요. JpaRepository<T,C> 의 T에는 Target이 되는 Class가 들어가고, C에는 Target Class의 @Id property의 객체값이 들어갑니다. (int인 경우, Integer)

자. 이제 지금까지 만들었던 BookDaoImpl의 코딩은 모두 끝났습니다. interface에 대한 코딩 작업이 전혀 필요하지 않습니다. 그 이유는 기본적으로 전에 보셨던 GenericDao에 대한 확장과 비슷한 방법입니다. Spring Data JPA는 JpaRepository, Repository, CrudRepository를 상속받은 interface를 모두 찾아내서, 동적인 객체를 생성합니다. 우리가 따로 코드를 만들어줄 필요가 전혀 없는것이지요. GenericDao의 결정판입니다. ^^

이제 테스트 코드를 작성해보도록 하겠습니다. 기존의 테스트 코드에서 약간의 변경만 있으면 됩니다. countAll()을 count()로, add, update를 모두 save로 변경만 시켜주면 테스트 코드가 모두 완성됩니다. 

@SuppressWarnings("unused")
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false)
@Transactional
public class BookDaoTest {
    @Autowired
    private BookDao bookDao;
    @Autowired
    private HistoryDao historyDao;
    @Autowired
    private UserDao userDao;
    private static Logger logger = LoggerFactory.getLogger(BookDaoTest.class);

    @Before
    public void setUp() {
        logger.trace("==== setUp started");
        assertThat(bookDao, is(not(nullValue())));
        historyDao.deleteAll();
        userDao.deleteAll();
        bookDao.deleteAll();
        assertThat(bookDao.count(), is(0L));
        logger.trace("=== setUp ended");
    }

    @Test
    public void saveAndCount() {
        long currentCount = bookDao.count();
        long index = 0L;
        List<Book> books = getBooks();
        for(Book book : books) {
            bookDao.save(book);
            index++;
            assertThat(bookDao.count(), is(currentCount + index));
        }
    }

마지막으로 applicationContext.xml을 구성합니다. applicationContext는 지금까지 보던것과 약간 많이 다릅니다. 구성된 applicationContext.xml 입니다. 

  <context:property-placeholder location="spring.properties" />
  <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
    <property name="driverClass" value="${connect.driver}" />
    <property name="jdbcUrl" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
    <property name="idleConnectionTestPeriodInMinutes" value="60" />
    <property name="idleMaxAgeInMinutes" value="240" />
    <property name="maxConnectionsPerPartition" value="30" />
    <property name="minConnectionsPerPartition" value="10" />
    <property name="partitionCount" value="3" />
    <property name="acquireIncrement" value="5" />
    <property name="statementsCacheSize" value="100" />
    <property name="releaseHelperThreads" value="3" />
  </bean>
  <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="dataSource" ref="dataSource" />
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
  </bean>
  <tx:annotation-driven transaction-manager="transactionManager" />
  <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="packagesToScan" value="xyzlast.bookstore.jpa.bookstore01.entities"/>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaVendorAdapter" ref="hibernateVendor"/>
    <property name="jpaPropertyMap" ref="jpaPropertyMap"/>
  </bean>  
  <util:map id="jpaPropertyMap">
    <entry key="hibernate.dialect" value="${connect.dialect}" />
  </util:map>  
  <bean id="hibernateVendor" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
    <property name="showSql" value="true"/>
  </bean>
  <jpa:repositories base-package="xyzlast.bookstore.jpa.bookstore01.repository" transaction-manager-ref="transactionManager" />

@Configuration을 통해서, ApplicationConfiguration이 어떻게 구성이 되어 있는지 한번 알아보도록 하겠습니다.

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages= {"com.xyzlast.bookstore.dao"})
@PropertySource("classpath:spring.properties")
@ComponentScan(basePackages = { "com.xyzlast.bookstore.dao", "com.xyzlast.bookstore.services", "com.xyzlast.bookstore.utils" })
public class HibernateBookStoreConfiguration {
    @Autowired
    private Environment env;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        return configHolder;
    }

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
        dataSource.setUsername(env.getProperty("connect.username"));
        dataSource.setPassword(env.getProperty("connect.password"));
        dataSource.setDriverClass(env.getProperty("connect.driver"));
        dataSource.setJdbcUrl(env.getProperty("connect.url"));
        dataSource.setMaxConnectionsPerPartition(20);
        dataSource.setMinConnectionsPerPartition(3);
        return dataSource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        LocalContainerEntityManagerFactoryBean entityManagerFactory = new LocalContainerEntityManagerFactoryBean();
        entityManagerFactory.setPackagesToScan("com.xyzlast.bookstore.entities");
        entityManagerFactory.setDataSource(dataSource());
        entityManagerFactory.setJpaVendorAdapter(hibernateJpaVendorAdapter());
        entityManagerFactory.setJpaProperties(japProperties());
        return entityManagerFactory;
    }

    @Bean
    public Properties japProperties() {
        Properties properties = new Properties();
        properties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));
        return properties;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setEntityManagerFactory(entityManagerFactory().getObject());
        return transactionManager;
    }

    @Bean
    public HibernateJpaVendorAdapter hibernateJpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter =  new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(true);
        return hibernateJpaVendorAdapter;
    }

    // Hibernate를 이용하는 경우, 반드시 HibernateExceptionTranslator가 Bean에 등록되어야지 된다.
    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }
}

설정을 보시면, 이제 기존에 가지고 있던 Hibernate 관련 SessionFactory가 모두 제거가 된 것을 알 수 있습니다. JPA는 java에서 persistance layer에 접근하는 방식을 표준화 한것입니다. 이에 대한 구현체로 가장 유명한 것이 Hibernate가 되기 때문에, Hibernate를 이용한 JPA를 이용하기 때문에, JPA로 Hibernate를 한번 더 싸는 형태로 구성이 되게 됩니다. 

# jpaPropertyMap : jpa의 추가 속성들을 집어넣는 Map입니다.
# hibernateVendor : ORM vendor를 지정하는 객체입니다.
# entityManagerFactory : entityManagerFactory는 Hibernate에서의 SessionFactory와 동일한 역활을 하는 객체입니다. entityManagerFactory를 구성합니다. 속성으로는 Entity로 등록할 객체가 모여있는 package를 지정하고 dataSource, ORM vendor, property를 지정합니다.
# transactionManager : transactionManager를 구성합니다.
# jpa:repositories : repository(dao) 인터페이스가 위치한 package를 지정하고, TransactionManager를 지정합니다.

이제 테스트 코드를 돌려보면 정상적으로 돌아가는 것을 알 수 있습니다. 그리고 BookDao interface의 객체를 System.out.println으로 찍어보면 다음과 같습니다. (HistoryDao 코드만 제외!. HistoryDao의 findByUser, findByBook method는 아직 구현되지 않았습니다.)

org.springframework.data.jpa.repository.support.SimpleJpaRepository@74279e1e

SimpleJpaRepository라는 객체를 Spring Data JPA가 만들어서 @Autowired 해서 사용하는 방식으로 변경하게 됩니다. 이 객체는 CRUD에서 CUD는 이미 지원하고 있고, R의 경우에는 select * from books 정도의 전체를 얻어오는 query를 지원하고 있습니다. 만들어진 interface에 where 조건을 붙인 query를 어떻게 사용하는지 알아보도록 하겠습니다. 


findByXXXX method의 선언

HistoryDao의 findByBook, findByUser의 경우에는 History의 book, user property를 참고해서 where 절을 만들게 됩니다. 이러한 작업 역시 중복 작업이 될 수 있기때문에, 이러한 method에 대해서 Spring Data JPA는 다음과 같은 방법을 이용합니다.

findBy{PropertyName} 을 이용하면, 자동으로 where 조건을 만족하는 구문을 만들어주게 됩니다. 그래서 구현되는 interface는 다음과 같습니다. 

@Repository
public interface HistoryDao extends JpaRepository<History, Integer> {
    List<History> findByUser(User user);
    List<History> findByBook(Book book);
}


이렇게 선언을 해주면, Spring JPA Data가 자동으로 모든 method의 구현코드를 만들어줍니다. GenericDao의 결정판이라고 이야기드린 이유를 아시겠나요? ^^

KEYWORDSAMPLEJPQL SNIPPET
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
BetweenfindByStartDateBetween… where x.startDate between 1? and ?2
LessThanfindByAgeLessThan… where x.age < ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
IsNullfindByAgeIsNull… where x.age is null
IsNotNull,NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> age)… where x.age not in ?1

Spring Data JPA를 이용한 Sort, Paging


findByXXX method의 경우에는 Sort와 Paging이 확장이 가능합니다. 

기본적으로 Spring Data JPA의 경우에는 method 이름을 이용한 Sort를 지원하고 있습니다. 때에 따라서, Dynamic한 Sort를 넣어줘야지 되는 경우가 종종 존재할 수 있습니다. 이러한 경우, method의 가장 마지막 parameter에 Sort 객체를 넣어주면 다양한 Sort를 지원할 수 있습니다. 기본적인 사용은 다음과 같습니다. 조금 아쉬운것이, Sort의 경우에는 OrderBy의 Property를 문자열로 넣어줘야지 되는 단점을 가지고 있습니다. 약간 아쉬운 점이라고 할 수 있습니다.

public interface BookDao extends JpaRepository<Book, Integer> {
    List<Book> findByNameLike(String name);
    List<Book> findByNameLike(String name, Sort sort);
}

    @Test
    public void findByNameLinkeUsingSort() {
        String bookName = "book";
        String bookNameQuery = "%" + bookName + "%";
        Sort sort = new Sort(Sort.Direction.DESC, "status");
        List<Book> books = bookDao.findByNameLike(bookName, sort);
        for(Book book : books) {
            assertThat(book.getName().contains(bookNameQuery), is(true));
        }
    }

다음은 Paging입니다. 기본적으로 Paging은 Sort의 사용법과 동일하게 parameter의 마지막에 Pageable 항목을  추가해서 Page의 index, size, sort를 모두 지원 가능합니다. Paging을 기본적으로 제공하는 것은 findAll method입니다. findAll에서 Paging을 지원하는 예제 코드는 다음과 같습니다. 

    @Test
    public void findAllUsingPaging() {
        Sort sort = new Sort(Sort.Direction.DESC, "status");
        PageRequest pageRequest = new PageRequest(0, 10, sort);
        Page<Book> books = bookDao.findAll(pageRequest);

        assertThat(books.getNumberOfElements() <= 10, is(true));
        assertThat(books.getNumber(), is(0));
    }


Summary

queryDSL은 type-safe 한 query를 작성할 수 있도록 도움을 줍니다. 개발에 있어서 가장 큰 문제가 될 수 있는 오타에 의한 에러를 막아줄수 있는 확실한 방법이고, query문에 유사한 포멧을 가지게 되기 때문에 SQL 개발자에게도 유용한 개발 방법이 될 수 있습니다. 그리고 Spring Data JPA는 DAO에 대한 코드양을 줄이고, 직관적인 포멧의 코딩을 가능하게 합니다. Spring이나 지금 신생으로 크고 있는 Open Source이기도 하고, 개발에 있어서 많은 도움을 받을 수 있습니다. 

이제 다음장에서는 queryDSL과 Spring JPA Data간의 결합을 시도해볼 예정입니다. 지금까지 구현된 코드를 한번 타이핑을 해보시고 만들어보시길 바랍니다. 감사합니다.






Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



저번 과제를 진행하시면, 무언가 코드의 중복이 일어나는것 같은데... 하는 느낌을 가지실 수 있을겁니다. 

그 부분은 각 code의 get, add, delete, countAll, deleteAll code의 부분이 계속되는 코드 패턴의 반복으로 나타나게 됩니다. 이 부분에 대한 코드 수정을 한번 해보도록 하겠습니다. 

간단히 각 dao의 add method의 차이를 한번 알아보도록 하겠습니다. 

void add(User user);
void add(Book book);
void add(History history);

method의 형태가 매우 유사합니다. 세부 코드를 보시면 더 비슷합니다.

public void add(final User user) {
    executor.execute(new HibernateAction() {
         session.save(user);
    });
}

이 내부의 코드 중에서 변화하는 부분과 변화하지 않는 부분을 나누면, 객체의 Type 이외에는 변화하지 않는 것을 알수 있습니다. 이렇게 Type만이 다르고, 코드가 동일한 경우에는 Generic을 이용해서 중복 코드를 제거할 수 있습니다. 

이제 상속을 통해서 코드의 중복을 한번 제거해보도록 하겠습니다. 기존 interface의 중복이 될 수 있는 get, add, delete, countAll, deleteAll에 대한 interface를 GenericDao interface로 따로 구성하도록 합니다. 이제 다른 Interface들은 GenericDao interface를 상속받아 구성될 것입니다.

public interface GenericDao<T> {
    List<T> findAll();
    int countAll();
    T findById(final int id);
    void update(final T t);
    void add(final T t);
    void delete(final T t);
    void deleteAll();
}


그리고, 이 interface를 받는 GenericDaoImpl을 구성해보도록 하겠습니다. 이제 모든 DaoImpl은 GenericDaoImpl을 상속받아 사용할 예정입니다.

public abstract class GenericDaoImpl<T> implements GenericDao<T> {
    private final Class<T> type;
    protected final HibernateSqlExecutor executor;

    public GenericDaoImpl(Class<T> type, HibernateSqlExecutor executor) {
        this.type = type;
        this.executor = executor;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        return (List<T>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(type).list();
            }
        });
    }

    @Override
    public int countAll() {
        Long count = (Long) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(type).setProjection(Projections.rowCount()).uniqueResult();
            }
        });
        return count.intValue();
    }

보시면 생성자부분에 새로운 코드가 들어가게 됩니다. Class<T>가 바로 그것인데요. Class의 Type값을 생성자에 넣어서, Session의 대상이 되는 class를 지정하게 됩니다. 

이제 상속받은 BookDao interface와 BookDaoImpl을 살펴보도록 하겠습니다. 

public interface BookDao extends GenericDao<Book> {
    List<Book> search(String name);
}

public class BookDaoImpl extends GenericDaoImpl<Book> implements BookDao {

    public BookDaoImpl(HibernateSqlExecutor executor) {
        super(Book.class, executor);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> search(final String name) {
        return (List<Book>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(Book.class)
                .add(Restrictions.like("name", name, MatchMode.ANYWHERE))
                .list();
            }
        });
    }
}

상당히 코드가 재미있게 변경이 됩니다. 

이렇게 변경할 수 있는 것은 객체만으로 통신할 수 있기 때문입니다. 따로 sql query문과 같은 다른 문자열을 다루는 것이 전혀 없고, 객체만으로 RDBMS와 통신할 수 있는 장점이 가장 보이는 코드이기도 하지요. 

한가지 더 이야기드린다면, 나중에 나올 Spring JPA Data의 경우에는 이 GenericDao interface를 극단적으로 발전시킨 경우가 만들어집니다. 

꼭 한번 이런식으로 코드를 변경시켜보시길 바랍니다. 그리고 덤으로 Generic에 대한 개념을 꼭 알아두시길 바랍니다. (C++의 경우에는 Template을 찾아보시면 됩니다.)


Spring + Hibernate

지금까지 작성되었던 Hibernate를 이용한 dao, service layer를 spring을 이용하도록 수정해보도록 하겠습니다. 

먼저 조금 소개를 한다면, Spring과 Hibernate는 원래 사이가 좋은 편은 아니였습니다.
예전 Spring 1.x와 Hibernate 2.x 간에는 개발자간에 매우 심각한 대립이 존재를 했었고, 그로 인한 키보드 배틀이 엄청나게 있었지요.
가장 큰 이유는 바로 전까지 제가 만들었던 HibernateSqlExecutor와 같은 HibernateTemplate을 Spring에서 제공하고 있었습니다. Hibernate는 DB에 접근하는 방법을 Session을 바로 얻어서 사용하도록 되어 있는데, 이러한 Raw level 접근을 Spring을 사용함으로서 제약을 걸게 되어 있었던 것이 사실입니다. 

그렇지만, Hibernate 3.x대로 넘어가고, Spring이 2.x대로 넘어가면서 화해(?)를 하게 됩니다. Spring 측에서 HibernateTemplate을 사용하지 않고, Spring에서 Hibernate를 사용할 수 있도록 Spring Framework의 큰 부분을 변경했습니다. 그리고 그에 맞추어 Hibernate에서는 SessionFactory에서 getCurrentSession() method를 추가함으로서, Spring의 기본적인 Transaction 방법에 맞추어 Hibernate를 이용한 DB 접근을 가능하게 하였습니다. 

둘간의 관계는 Spring 자체는 application framework입니다. Hibernate는 ORM framework고요. Hibernate 자체가 Spring보다 범위가 작은 Framework라고도 할 수 있습니다. Java의 모든 application은 Spring을 사용할 수 있지만, DB를 사용하지 않는 Application은 Hibernate를 사용하지 않을테니까요. 그럼에도 불구하고, Java의 최고 양대 open source framework는 spring과 hibernate입니다. 둘은 open source가 세상을 얼마나 바꾸어 놓을 수 있는지 보여주었고, 상업적으로도 엄청난 성공을 거뒀습니다. spring은 지금 vmware에서 제공되고 있고, hibernate는 weblogic을 제공하는 JBOSS에서 제공되고 있습니다. 

잡설이 길었습니다. spring은 이러한 hibernate를 위한 library를 따로 분리해서 사용하고 있습니다. pom.xml에 다음 jar가 추가되어야지 됩니다. 

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.2.0.RELEASE</version>
    </dependency>

Hibernate 까지 포함한 전체 jar의 구성은 다음과 같습니다. (BoneCP를 사용해서 ConnectionPool을 사용할 예정입니다.)

    <!-- Test 관련 jar -->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.2.1.RELEASE</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>
    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.9.Final</version>
    </dependency>

    <!-- Spring 관련 jar -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>3.2.1.RELEASE</version>
    </dependency>
    <!-- boneCP 관련 jar -->
    <dependency>
      <groupId>com.jolbox</groupId>
      <artifactId>bonecp</artifactId>
      <version>0.7.1.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>14.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.2</version>
    </dependency>

사용되는 jar들이 꽤나 많습니다. 이건 세발의 피입니다. ㅠㅠ 나중에 좀 사용하다보면 pom.xml 파일이 600 line이 넘어가는 경우가 허다하게 나옵니다. ㅠㅠ

이제 spring을 이용한 transaction 구성과 BookDao, UserDao, HistoryDao의 구현 객체를 수정하도록 하겠습니다. 이제 Transaction을 spring에서 관리할 예정이기 때문에 기존의 HibernateSqlExecutor를 제거할 예정입니다. 그리고 Transaction을 맺는것은 한개의 Session을 Dao Action에 계속해서 사용하는 형식이 됩니다. 
이 부분은 Hibernate에서 Transaction/Session에 대한 관리 부분과 연관이 됩니다. 기본적으로 Hibernate는 Session을 beginTransaction(), commit(), rollback()을 이용해서 Session의 변경사항을 db에 반영하도록 되어 있습니다. 그런데, Spring의 @Transactional을 이용하면 @Transactional의 시작시점에서 자동으로 session.beginTransaction()의 시작과, commit(), rollback()을 구성해주게 됩니다. 따라서 전체 코드는 다음과 같이 수정이 되게 됩니다. (GenericDaoImpl만 코드를 살펴보도록 하겠습니다.)

public abstract class GenericDaoImpl<T, K extends Serializable> implements GenericDao<T, K> {
    private final Class<T> type;
    protected SessionFactory sessionFactory;

    public GenericDaoImpl(Class<T> type) {
        this.type = type;
    }

    @Override
    @SuppressWarnings("unchecked")
    public List<T> findAll() {
        Session session = sessionFactory.getCurrentSession();
        return session.createCriteria(type).list();
    }

    @Override
    public int countAll() {
        Session session = sessionFactory.getCurrentSession();
        return ((Long) session.createCriteria(type).setProjection(Projections.rowCount()).uniqueResult()).intValue();
    }

    @Override
    @SuppressWarnings("unchecked")
    public T findById(final K id) {
        Session session = sessionFactory.getCurrentSession();
        return (T) session.get(type, id);
    }

    @Override
    public void update(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.update(t);
    }

    @Override
    public void add(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.save(t);
    }

    @Override
    public void delete(final T t) {
        Session session = sessionFactory.getCurrentSession();
        session.delete(t);
    }

    @Override
    public void deleteAll() {
        Session session = sessionFactory.getCurrentSession();
        @SuppressWarnings("unchecked")
        List<T> result = (List<T>) session.createCriteria(type).list();
        for(T r : result) {
            session.delete(r);
        }
    }
}


Spring에서 SessionFactory를 @Autowired해서 사용할 예정입니다. 이제 applicationContext.xml을 작성하도록 하겠습니다. 

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">  
  <context:property-placeholder location="classpath:spring.properties" />
  <!--   Repository scan -->
  <context:component-scan base-package="com.xyzlast.bookstore.dao"/>
    
  <!-- BoneCP connection Pool DataSource 설정 -->
  <bean id="dataSource" class="com.jolbox.bonecp.BoneCPDataSource" destroy-method="close">
    <property name="driverClass" value="${connect.driver}" />
    <property name="jdbcUrl" value="${connect.url}" />
    <property name="username" value="${connect.username}" />
    <property name="password" value="${connect.password}" />
    <property name="idleConnectionTestPeriodInMinutes" value="60" />
    <property name="idleMaxAgeInMinutes" value="240" />
    <property name="maxConnectionsPerPartition" value="30" />
    <property name="minConnectionsPerPartition" value="10" />
    <property name="partitionCount" value="3" />
    <property name="acquireIncrement" value="5" />
    <property name="statementsCacheSize" value="100" />
    <property name="releaseHelperThreads" value="3" />
  </bean>
  <!--   Spring에서 제공하는 Hibernate4용 SessionFactory 생성 -->
  <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:hibernate.cfg.xml"/>
    <!--     entity가 위치한 package를 scan -->
    <property name="packagesToScan" value="com.xyzlast.bookstore.entities"/>
  </bean>
  <!--   Hibernate4용 TransactionManager 설정 -->
  <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
  </bean>
  <!--   TransactionManager 등록 -->
  <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

@Configuration을 이용한 code base configuration은 다음과 같이 구성됩니다. 

@Configuration
@EnableTransactionManagement
@PropertySource("classpath:spring.properties")
@ComponentScan(basePackages = { "com.xyzlast.bookstore.dao", "com.xyzlast.bookstore.services", "com.xyzlast.bookstore.utils" })
public class HibernateBookStoreConfiguration {
    @Autowired
    private Environment env;

    @Autowired
    private SessionFactory sessionFactory;

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
        PropertySourcesPlaceholderConfigurer configHolder = new PropertySourcesPlaceholderConfigurer();
        return configHolder;
    }

    @Bean
    public DataSource dataSource() {
        BoneCPDataSource dataSource = new BoneCPDataSource();
        dataSource.setUsername(env.getProperty("connect.username"));
        dataSource.setPassword(env.getProperty("connect.password"));
        dataSource.setDriverClass(env.getProperty("connect.driver"));
        dataSource.setJdbcUrl(env.getProperty("connect.url"));
        dataSource.setMaxConnectionsPerPartition(20);
        dataSource.setMinConnectionsPerPartition(3);
        return dataSource;
    }

    @Bean
    public LocalSessionFactoryBean sessionFactory() {
        Properties properties = new Properties();
        properties.setProperty("hibernate.dialect", env.getProperty("hibernate.dialect"));
        properties.setProperty("hibernate.show_sql", env.getProperty("hibernate.show_sql"));

        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(dataSource());
        sessionFactory.setHibernateProperties(properties);
        sessionFactory.setPackagesToScan("com.xyzlast.bookstore.entities");

        return sessionFactory;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        HibernateTransactionManager transactionManager = new HibernateTransactionManager();
        transactionManager.setDataSource(dataSource());
        transactionManager.setSessionFactory(sessionFactory);
        return transactionManager;
    }

    // Hibernate를 이용하는 경우, 반드시 HibernateExceptionTranslator가 Bean에 등록되어야지 된다.
    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }
}


하나 재미있는 코드 구성이 Configuration에서 나오게 됩니다. 먼저, sessionFactory입니다. SessionFactory를 반환하는 것이 아닌, LocalSessionFactoryBean을 반환하게 됩니다. 이는 Spring ApplicationContext에서 FactoryBean을 등록시켜, 그 FactoryBean을 통한 재 생성이 되게 됩니다. 마치 prototype scope로 SessionFactory를 생성해서 사용하는 것 처럼, SessionFactory를 사용할 때마다 새로 반환시키는 구성이 되게 됩니다. 마지막으로 HibernateExceptionTranslator 입니다. Hibernate에서 반환되는 Exception을 Spring JDBCTemplate과 유사한 Exception으로 변경시키는 Bean입니다. 이 Bean이 등록되지 않으면 Exception이 발생되게 됩니다. 

약간 설정이 어려운 감이 없지는 않습니다. 그렇지만 기본적으로 DataSource를 만들어주고, 만들어진 DataSource를 기반으로 SessionFactory를 등록한다. 라는 기본 개념은 JdbcTemplate을 사용할때와 별반 차이가 없습니다. 
그리고, Hibernate를 사용하는 경우에는 CUD에 대한 처리가 매우 단순해지는 코드적 장점을 가지고 있으며, 무엇보다 DB의 객체 자체를 Domain의 Model로 대응시켜서 사용가능하다는 큰 장점을 가지고 있습니다. 꼭 사용법을 익혀두시길 바랍니다. 


Hibernate Criteria Examples

Hibernate로 하는 query문은 Criteria라는 객체지향적 문법으로 만들어집니다. 이 문법의 장점은 다음과 같습니다. 

# DB에 종속적이지 않은 문법을 만들어냅니다. : Criteria로 만들어진 코드는 Dialect에 의해서 DB의 종류에 맞는 SQL로 변환되어 실행됩니다.
# BL 로직을 표현하기 쉽다. : SQL이라는 DB의 언어에서 객체지향적 java code로 만들어진 문법은 알아보기가 쉽습니다.

다음은 Criteria 문법으로 표현되는 SQL query 문에 대한 예시들입니다.

Entity Select query

select * from SM_WAITING where openDate = '20120510' and phone_id = '123456' and enteranced = 'N' and deleted = 'N' order by seq;

        List<Waiting> waitings = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.openDate", yyyymmdd))
                .add(Restrictions.eq("phoneNumber", phoneNumber))
                .add(Restrictions.eq("entranced", false))
                .add(Restrictions.eq("deleted", false))
                .addOrder(Order.asc("id.sequence")).list();


Count Row

select count(*) from SM_WAITING where shopId = 'sm00000001' and openDate = '20120101' and seq > 3 and enteranced = 'N' and deleted = 'N';

        Long remainTeamCount = (Long) session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", waiting.getId().getShop()))
                .add(Restrictions.eq("id.openDate", waiting.getId().getOpenDate()))
                .add(Restrictions.lt("id.sequence", waiting.getId().getSequence()))
                .add(Restrictions.eq("entranced", false))
                .add(Restrictions.eq("deleted", false))
                .setProjection(Projections.rowCount()).uniqueResult();


Get MAX/MIN/SUM value

select max(seq) from SM_WAITING where shopId = 'sm00000001' and openDate = '20120101'

        Integer maxSeq = (Integer) session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", shop))
                .add(Restrictions.eq("id.openDate", yyyymmdd))
                .setProjection(Projections.max("id.sequence"))
                .uniqueResult();


Paging query (oracle 기준)

select * from (
         select rownum as rnum, name, address from Waiting where phonenumber = '1011234550'
    ) where rnum between (:PageNo * (:PageNo-1)) and ((:PageNo * (:PageNo-1)) + :PageSize )

        List<Waiting> waitings = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("phoneNumber", phoneNumber))
                .setFirstResult(pageIndex * pageSize).setMaxResults(pageSize)
                .list();


SubQuery

SELECT *
  FROM PIZZA_ORDER
 WHERE EXISTS (SELECT 1
                 FROM PIZZA
                WHERE PIZZA.pizza_size_id = 1
                  AND PIZZA.pizza_order_id = PIZZA_ORDER.pizza_order_id)

Criteria criteria = Criteria.forClass(PizzaOrder.class,"pizzaOrder");
DetachedCriteria sizeCriteria = DetachedCriteria.forClass(Pizza.class,"pizza");
sizeCriteria.add("pizza_size_id",1);
sizeCriteria.add(Property.forName("pizza.pizza_order_id").eqProperty("pizzaOrder.pizza_order_id"));
criteria.add(Subqueries.exists(sizeCriteria.setProjection(Projections.property("pizza.id"))));
List<pizzaOrder> ordersWithOneSmallPizza = criteria.list();


JOIN

select * from smWait w, smShop s where s.shop_name = 'ykyoon'

        List<Waiting> waits = session.createCriteria(Waiting.class)
                .add(Restrictions.eq("id.shop", shop))
                .list();


몇몇 대표적인 query들을 뽑아봤습니다. where 조건은 add로 Restrictions로 처리하고, count와 같은 계산 절은 Projections 로 처리가 되는 것을 알 수 있습니다. 여러 query 들을 한번 만들어보시길 바랍니다. 많이 익숙해질 필요가 있습니다. 


Posted by Y2K
,

10. Hibernate

Java 2013. 9. 9. 11:13

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.


 Hibernate는 지금까지 SQL을 이용한 DB query 방법이 아닌, 객체를 이용한 SQL auto generate를 통한 DB 접속 방법을 제공합니다. 이런 객체를 이용한 방법을 ORM(object relation model)이라고 합니다. 먼저, ORM에 대해서 간단한 소개를 해보도록 하겠습니다. 

ORM

java 언어의 발전은 sw 개발에 있어서 객체지향의 개발 방법으로 오는 혜택에 매료가 되었습니다. 그렇지만, 모든 웹 및 기업의 데이터가 저장되어 있는 RDBMS와 OOP간의 괴리의 차이에 의한 비용의 증가가 계속해서 발생하게 되었습니다. OOP적 개발 방법과 RDBMS의 관계형 개발 방법론이 끊임없이 충돌하게 되는 것이지요. 서로간에 전쟁(war) 라는 표현을 사용할 정도로 논란이 매우 큰 문제입니다. java 측에서는 relation 기술의 탓으로 돌리고, data 전문가들은 OOP 기술의 문제라고 약간은 소모적인 논쟁으로 계속해서 가게 되었지요.

이때, ORM(Object Relation Model)은 이러한 불일치 기술에 대한 solution을 지칭할 때 사용됩니다. 

ORM은 4가지로 구성이 되어 있습니다.

1. Persistence class에 대한 기본적인 CRUD를 수행하는 API
2. class 자체나 class의 property를 참조하는 query를 작성할 수 있는 API
3. mapping meta data 작성을 위한 기반 API
4. ORM 구성이 Transaction과 상호 동작하며 최적화를 수행하도록 돕는 API


왜 ORM을 사용해야지 되는가?

1. 생산성 : SQL관련 코드를 제거하고 객체간의 관계를 명확하게 그릴수 있습니다. 그리고 Domain에 집중할 수 있는 설계 구조를 가지고 옵니다.
2. 유지보수성 : Domain을 구현했기 때문에 Domain에 대한 명확한 정의가 나타납니다. Domain의 BL이 변경, 추가 되었을 때 그에 대한 수정이 쉽습니다.
3. 성능 : 가장 큰 이슈입니다. VM을 설명할 때, VM보다 일반 assembly로 compile되는 언어가 더 빠르지만 이제 사용되지 않는 이유랑 동일합니다. ORM 자체에 이미 많은 최적화 방법들이 구현되어 있고, 그 현인들의 지식을 사용할 수 있다는 점에서 더욱더 큰 장점을 가지고 올 수 있습니다.
4. 밴더 독립성 : ORM은 DB와 DB의 방언(각 DB만의 함수들)로부터 추상화 되어 있습니다. DB의 독립성은 매우 큰 장점을 가지고 오는데, 개발에 있어서 오히려 더 나은 장점을 가지고 옵니다. 개발자들은 자신의 개발 PC에 가벼운 DB(mysql)를 설치해서 개발을 하고, 실 서버에서는 Data에 최적화된 DB를 이용해서 서비스를 할 수 있는 장점을 가지고 있습니다. 이는 생산성과도 직결되는 문제입니다.
5. Java 표준 : ORM은 JSR 220에 의해서 java에 표준적인 기술로 인정을 받았습니다.


Domain Driven Design

에릭 에반스 (Eric Evans) 는 과거의 지혜와 경험들을 종합하여 도메인-드리븐 디자인 (Domain Driven Design) 이라는 방법론을 제시 했습니다.
단순 객체지향 세계에서 살던 개발자들은  이 굉장하지만 새로운 개념에 어려움을 느껴 발표된지 몇년이 지난 후에야 관심을 가지게 되었죠.
DDD가 도대체 뭔데? 어떻게 해서든지 돌아가기만 하면 되는거 아냐! 하면서 무심히 지나쳤던 것들에 대해 체계적으로 설명하는 방법론이죠. 하지만 여전히 DDD는 어려운 것, 그저 한때 유행하는 버즈 워드로 인식되고 있는 경향이 있습니다. DDD가 무엇인지 처음 듣는 개발자도 많을 것입니다. DDD의 전체적인 철학을 쉽게 요약하고 있는 블로그 포스트 DDD: How to tackle complexity  번역으로 DDD 카테고리를 시작합니다. 


DDD (Domain Driven Design) 에서는 어플리케이션 도메인을 표현하기 위한 오브젝트 모델을 만듭니다.
이 모델은 도메인의 모든 관계와 로직을 담고 있습니다. 이렇게 하는 목적은 도메인의 복잡성을 관리하기 위함 입니다. DDD 에는 매우 많은 개념과 패턴이 투입되어 있으나, 정제된 두개의 큰 그림으로 그 복잡성에 태클을 걸 수 있습니다.

1. 도메인의 개념을 명확하게 표현합니다.
2. 더욱 심도 있는 통찰을 위해 지속적인 리팩토링을 수행합니다.

복잡성이란 자체의 복잡한 정도를 의미합니다. 복잡한 것은 이해하기 어렵습니다. 이해하기 어려우면 금방 알아 들을 수 없습니다.

이것이 실제 이슈 입니다 : 복잡한 소프트웨어는 이해하기 어렵습니다. 이것이 바로 모든 사람이 업데이트 하기를 두려워 하여 아예 처음부터 다시 만드는 이유입니다. 아마 첫번째나 두번째는 해킹 하듯이 코드를 추가해서 원하는 바를 이룰 수 있을지 모르지만, 각각의 해킹은 더 이상 시도하는 것이 의미가 없을 때까지 복잡성과 추잡함을 증가시킵니다. 이것을 다른 말로 실패 라고 합니다.

그래서 우리는 이 복잡성을 극복해야 합니다. DDD의 첫번째 방법은 객체지향, 모델 과 추상화의 장점을 얻는 것 입니다. 하지만 이건 매우 광범위 하죠. 우리는 이 오브젝트와 모델들을 어떻게 구조화 해야 하는지 알아내야 합니다. 이것이 DDD가 도메인의 개념을 명시적으로 표현하자는 아이디어의 입니다.

아이디어는 간단합니다. 여러분의 도메인에 새로이 적용되는 개념이 있다면 모델에서 확인할 수 있어야 합니다. 중요한 개념을 확인하기 위해서 코드를 뒤져서는 안됩니다. 그 개념은 모델에서 오브젝트로써 표현되어야 합니다. 특정 조건에서만 발생하는 액션이 있다고 합시다, 이 조건들이 중요하지 않다면 그 액션을 수행하도록 그저 IF Statement 메소드로 처리하면 됩니다. 하지만, 그 조건들이 도메인에서 중요하다면 코드로 부터 감추는 것만으로는 부족합니다. 그 조건들을 수행하도록 Policy Object가 조건들을 표현해야 합니다. 이제 조건들은 당신의 도메인에서 명시적으로 표현됩니다.

이 아이디어 들은 Factories, Repositories, Services, Knowledge Levels 등등으로 표현될 수 있습니다. 이 것은 여러분의 시스템을 이해 가능하도록 만드는 중요한 부분입니다.

DDD가 작동하도록 만드는 두번째 아이디어는 "Deeper Insight"를 위한 지속적인 리팩토링 입니다. Deeper Insight 란 이미 가지고 있는 도메인 모델에서 새로운 어떤 것을 발견하게 된다면 대충 끼워넣지 말고 도메인에서 중요한 요소인지 반드시 알아내라는 것을 의미합니다. 만약 중요하다면 새로 이해한 것이 명확하게 표현 되도록 모델을 리팩토링 해야 합니다. 이 리팩토링은 사소할 때도 있고, 매우 중요 할 때도 있습니다.

도메인 모델이 표현성을 잃게 되면 점점 더 부서지고, 점점더 복잡해 지며 점점 더 어려워 집니다. 여러분의 모델이 단순하며  표현력과 정확성을 유지할 수 있도록 항상 싸워야 합니다. 당신이 운이 좋다면 에릭 에반스가 말하는 Break Through [전에는 불가능 했던 것이 새로운 가능성과 통찰력이 갑자기 나타나는 일] 것을 경험할  수도 있습니다. 그렇게 된다면 진짜 운이 좋은 것입니다. 당신이 운이 좋지 않더라도 리팩토링은 적어도 모델이 유연성을 요구할때 유연함을 만족 시킬 수는 있습니다. 이것은 미래에 나타날 통찰력과 리팩토링 요소를 더 쉽게 핸들링 할 수 있음을 의미합니다.


위 내용과 같이, DDD는 우리가 표현하고자 하는 실세계의 데이터 프로세스 자체를 컴퓨터 언어로 옮겨가는 과정입니다. 

예를 들어, SQL로 작업을 하게 되면 다음과 같은 대화가 나오게 됩니다. 

"TB_USER"에서 SELECT를 할때, POINT Column으로 Order By DESC로 얻어오고, 그 POINT값이 100점 이상인 경우에는 LEVEL column값을 2로 업데이트를 시키면 됩니다.

자, 방금 말한 내용을 실제 BL을 설계한 기획자에게 이야기를 해보도록 합시다. 과연 어떤 말인지 알아들을수 있을까요? SQL 개발자들이라면 가능하겠지요. DDD로 모델링을 거치면 다음과 같은 대화를 할 수 있습니다.

User를 표로 보여줄 때, Book Point값이 높은 순서대로 보여주고, Book Point값이 100점이상인 경우에는 Reader로 사용자 Level을 높여줘서 보여주면 됩니다.


자, 이제 좀더 말이 쉬워졌습니다. 이 말을 우리가 객체 지향 언어로 표현하면 어떻게 될까요? 

List<User> users = userDao.getAll();
users.sort("Book Point");
for(User user : users) {
    if(user.getBookPoint() >= 100) {
        user.setStatus("Reader");
    }
}
return users;

SQL query로 표현하면 다음과 같이 표현할 수 있습니다. 

UPDATE TB_USER SET LEVEL = 2 WHERE POINT >= 100;
SELECT * FROM TB_USER ORDER BY POINT DESC;


데이터 중심으로 보는 것과 모델링을 중심으로 보는것. 이 두가지의 차이는 그 데이터의 구조를 모르는 사람이 로직을 읽을수 있느냐, 없느냐에 따라 갈리게 됩니다. 그리고 우리가 문장으로 설명할 수 있는 BL을 code에 어떻게 녹여내느냐를 고민을 해야지 됩니다. 

최종적으로 DDD는 다음 목표들을 갖습니다.

Ubiquitous Language
Domain 중심의 SW팀에서는 모든 참가자들(사용자, 도메인전문가, 설계자, 프로그래머, 분석가)간에 동일한 의미를 갖는 공통된 언어를 갖는다. 심지어 공통된 언어는 Code의 Object로 구현이 가능해야지 된다.

Layered Architecture pattern
UI와 Model이 결합되어 있으면, UI가 바뀌는데에 따라 Model이 변경되어야지 됩니다. 따라서 UI, BL, Modeling간에 분리가 가능한 Layer들이 만들어져야지 됩니다. 기준에 따라 Layer를 나누고, 역할을 부여하는 작업이 필요합니다. 이에 대한 장점은 다음과 같습니다.

1. Layer들이 재사용될 수 있어야지 됩니다.
2. 표준을 지원한다.
3. 종속성을 국지적으로 최소화한다.
4. 교환 가능성이 확보된다.
5. 동작이 변경된 경우, 단계별 재작업이 필요하다.

기본적으로 Domain Model은 다음 4개의 Layer로 구분시켜서 사용하게 됩니다. (어디서 많이 본 내용입니다. ㅋ)




# Infrastructure Layer : 상위 계층을 지원하는 일반화된 기술적 기능을 제공. 공통 Library, Engine, Framework 영역
# Domain Layer : 업무 개념과 업무 상황에 대한 정보. 업무 규칙을 표현하는 Layer.
# Application Layer : 작업을 정의하고 조정하는 영역. Domain 객체로 작업을 위임하는 역활을 담당.
# UI Layer : 정보를 노출하고 입력을 받아들이는 영역

이러한 구조는 결국은 Domain의 격리, 즉 분리가 이루어지게 됩니다.  이러한 Layer architecture중 가장 대표적인 방식이 MVC 입니다. 



Model 은 기본 기능을, View는 User Interface를 의미하게됩니다. Controller는 M과 V간의 직접적인 연결을 막음으로서 독립성을 유지하는 역활을 하게 됩니다. 추후에 Spring @MVC에서 보다 더 설명이 될 영역이기도합니다.  

Smart UI anti pattern
모든 업무로직을 사용자 인터페이스에 넣는 설계 방법을 Smart UI pattern이라고 합니다. 

특징은 
- application을 작은 기능으로 잘게 나누고,
- 나뉜 기능을 분리된 UI로 구현. 업무 규칙이 분리된 UI에 들어가게 합니다.
- 분리된 업무 규칙은 RDBMS를 이용해서 데이터를 공유하고 실행합니다. 주로 SP가 이 용도로 사용됩니다.
- 주로 자동화된 UI 구축 도구와 시각적인 프로그래밍 도구를 이용합니다. 

DDD pattern에서 가장 피해야지 되는 것이 바로 UI Pattern입니다.
UI pattern이라는 것은 지금도 매우 자주 쓰이고 있는 패턴입니다. 이 패턴의 가장 큰 문제는 우리가 사용하는 데이터 및 Domain에 대한 모든 로직을 보여지는 View에 맞추게 됩니다. 실질적인 데이터의 흐름을 방해하고, 언제나 바뀔 수 있는 View 영역에 Model이 영속되기 때문에 application의 변경에 취약한 약점을 갖게 됩니다. 

대표적인 Smart UI pattern의 도구가 PowerBuilder입니다. Data Window의 Smart UI를 이용하기 위해, UI에 따른 Model과 로직이 꼬인 상태로 존재하게 됩니다. 이는 필연적으로 코드의 중복을 가지고 오게 되며, 아직까지 UI에 대한 명확한 테스트를 할 수 없는 현 시점에서 유지보수가 거의 불가능한 시스템을 만들게 됩니다. *anti pattern은 쓰지 말라고 있는겁니다.*

Entities pattern
ID를 갖는 unique한 object를 갖는 pattern입니다. 이는 RDBMS의 PK와 연결시켜, 이 객체가 유일한 어떤 값임을 나타내는 방법을 제공합니다.

Service pattern
서비스는 기능을 처리하거나, Entity로 구별할 수 없는 것을 지칭합니다. 가장 주로 사용되는 것은 BL의 Group을 표현하는 것이 가장 많습니다.

Factory pattern
각각의 Object, Service의 생성방법에 대한 통일성을 갖는것을 목표로 합니다. 어떤 객체를 생성할 때, 초기화 해줘야지 되는 값이라던지 실행시켜줘야지 되는 method가 독특하게 존재한다면 개발자 및 참여자들은 Domain에 집중할 수 없습니다. Spring의 ApplicationContext는 이런 경우 가장 좋은 해결 방법이 됩니다.

Repository pattern
생성된 객체나 Model이 외부(주로 DB)와 연동되어 생명주기를 가질때, 그 생명주기에 대한 관리 Focus가 되는 Layer가 존재해야지 됩니다. dao로 구현되는 것이 일반적이며, Spring에서는 @Repository로 지정하는 것이 일반적입니다.

DDD의 사상을 반영하기 위해 주로 사용되는 툴이 ORM이고, java에서 가장 오랜 시간동안 개발이 되고 사랑받고 있는 Hibernate가 그 선두 주자라고 할 수 있습니다.


Hibernate를 이용한 Dao의 개발

Hibernate를 이용한 Dao를 개발해보도록 하겠습니다. 먼저, 새로운 maven 프로젝트를 하나 만들어주세요. 그리고, 다음 jar들을 추가하도록 합니다.

    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>4.1.9.Final</version>
    </dependency>
    <dependency>
      <groupId>c3p0</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.1.2</version>
    </dependency>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>

그리고 entities와 dao, util package를 구성해주고, 기존의 dao의 interface와 entity를 모두 구성해주세요. file을 copy 한 후, package 명을 변경해도 좋습니다. 최종적으로 모든 객체가 다 완성되면 다음과 같은 형태가 될 것입니다. 


먼저, ORM 을 사용하기 위해서는 Hibernate가 정확히 어떻게 객체들과 DB간에 연결되어있는지 정의하는 방법이 필요합니다. Hibernate는 2개의 방법을 제공하고 있는데, hbm.xml 파일을 이용한 xml mapping과 annotation을 이용한 mapping을 제공하고 있습니다. 


지금까지 우리가 만든 객체들의 관계는 어떻게 될까요?

먼저, Book은 대여한 User와의 관계가 있습니다. 이 관계는 N:0의 관계를 갖습니다.  또한 여러개의 History를 가질 수 있습니다.
그리고 History는 관계된 User와 Book간의 모든 관계를 갖습니다. 
마지막으로 User는 어떤 관계를 가질까요? Table상에서는 관계가 나타나지 않습니다. 그렇지만, 하나의 User는 여러개의 Book을 대여할 수 있고, 여러개의 History를 가질 수 있습니다.  이를 반영할 수 있는 객체로 표현해보면 다음과 같은 객체를 선언할 수 있습니다.

public class Book {
    private int id;
    private String name;
    private String author;
    private BookStatus status;
    private Date publishDate;
    private User rentUser;
    private String comment;
    private List<History> histories;
}

public class History {
    private int id;
    private User user;
    private Book book;
    private ActionType action;
    private Date insertDate;
}

public class User {
    private int id;
    private String name;
    private String password;
    private UserLevel level;
    private int point;
    private List<History> histories;
}

각 객체들로 서로간에 동작을 이해할 수 있는 코드들이 나오게 됩니다. 이제 이 객체를 DB와 연결을 시켜야지 됩니다. DB와 연결을 시키는 방법은 전통적인 xml을 사용하는 방법과 @annotation을 이용한 방법 두가지로 나눌 수 있습니다.  기본적으로 이제 java 표준이 된 @annotation을 이용한 ORM mapping을 중심으로 알아보도록 하겠습니다. xml을 이용한 mapping 방법은 각자 숙제로 남겨두도록 하겠습니다. 

Hibernate를 이용하기 위해서는 먼저 DB의 접근을 제공해야지 됩니다. 이는 SessionFactory를 통해서 이루어지며, SessionFactory에서 얻어지는 Session을 이용해서 DB에 접근하게 됩니다. 그리고 만들어진 Session을 이용해서 DB에 객체의 query를 만들어주고, 그 query를 실행하게 됩니다.  

지금까지 나온 개념을 좀 정리해보도록 하겠습니다. 

1. Entity : Domain Model과 Persistence Object간의 연결 객체
2. SessionFactory : Entity를 얻어내기 위한 Session을 관리하는 객체
3. Session : Entity를 Persistence Layer에서 얻어내는 객체

가 됩니다. 조금 더 간단히 생각해보면, Session은 JdbcTemplate와 비슷한 개념을 가지고 있고, SessionFactory는 DataSource와 비슷한 개념을 가지게 됩니다. 단 기능면에서는 좀 많은 차이를 가지고 있지요. 

SessionFactory를 구성하기 위해서는 hibernate.cfg.xml 파일을 통해서 이루어지며, 파일의 구성은 다음과 같습니다.

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"><hibernate-configuration>
  <session-factory>
    <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property>
    <property name="hibernate.connection.password">qwer12#$</property>
    <property name="hibernate.connection.url">jdbc:mysql://localhost/bookstore3</property>
    <property name="hibernate.connection.username">root</property>
    <property name="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</property>
    <property name="hibernate.show_sql">true</property>
    <property name="hibernate.c3p0.min_size">5</property>
    <property name="hibernate.c3p0.max_size">20</property>
    <property name="hibernate.c3p0.timeout">300</property>
    <property name="hibernate.c3p0.max_statements">50</property>
    <property name="hibernate.c3p0.idle_test_period">3000</property>
    <mapping class="com.xyzlast.hibernate.bookstore.entities.User" />
    <mapping class="com.xyzlast.hibernate.bookstore.entities.Book" />
    <mapping class="com.xyzlast.hibernate.bookstore.entities.History" />
  </session-factory>
</hibernate-configuration>


파일이 매우 깁니다. 그리고 내용이 복잡해보이지만, 기존의 DataSource를 이용했을 때와 큰 차이가 없습니다. driver_class, password, username, url의 경우에는 기존의 DataSource와 완전히 동일합니다. 
dialect의 경우, hibernate와 db간의 연결을 해주기위한 설정입니다. 우리가 사용한 MySql5의 경우 MySQL5InnoDBDialect를 사용을 하며 이것은 Oracle을 사용하는 경우에는 Oracle10Dialect를 사용하면 됩니다. JDBC가 DB를 연결할 때 사용하는 것이 driver_class라면, 그 driver class와 Hibernate간에 연결하는 중간다리 역활을 하는 것이 dialect입니다. 

Hibernate는 기본적으로 c3p0 connection pool을 사용하도록 구성되어 있습니다. 앞으로는 사용하지는 않을것이지만, c3p0에 대해서 알아두시는것도 괜찮습니다. 
그리고, 나머지 c3p0로 시작되는 설정은 모두 connection pool에 대한 설정들입니다. 지금까지 우리가 만든 application은 한개의 DB connection만을 가지고, 처리를 했습니다. 만약에 web system에 올렸다면 이는 큰 문제가 되는 것으로, db에 query를 날릴때 딱 1개만의 연결을 갖게 됩니다. 여러 사용자가 동시에 사용을 할때, 먼저 사용자가 connection을 갖게 되면 그 다음 처리를 못해주게 됩니다. c3p0에 대한 설정들은 다음과 같습니다. 

1. min_size : connection pool의 최소 갯수를 지정합니다. connection을 application이 시작한 다음 최소 5개는 만들도록 설정한 상태입니다.
2. max_size : connection pool의 최대 갯수를 지정합니다. connection을 application이 시작한 후, 최대 20개까지 만들도록 설정했습니다.
3. timeout : connection의 생명 시간을 지정합니다. 처음 만들어진지 300sec가 지나면 connection을 소멸하도록 합니다.
4. max_statement : 1개의 connection당 최대 query문을 지정합니다. 최대 50개의 query문이 쌓이게 되면 DB측에 flush로 한번에 보내버립니다. 이는 Transaction의 성능에 영향을 미치고 db에 대한 bulk 작업에 도움을 줍니다.
5. idle_test_period : connection을 유지하고 있지만, db측에서 connection을 끊어버린 것을 확인하는 시간입니다. 이는 db server가 connection을 같이 관리할 때, 이 값을 통해서 자신의 connection을 계속해서 유효하도록 만드는 시간주기입니다. 

나머지 mapping class의 경우, mapping된 class를 추가해주는 방법입니다. 

자, 이제 객체 선언 코드들입니다. 너무 길어서 get/set code는 제거되었습니다. 한번 확인해보도록 하겠습니다. 

@Entity
@Table(name="books")
public class Book {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;
    @Column(name="name")
    private String name;
    @Column(name="author")
    private String author;
    @Column(name="status")
    @Enumerated(EnumType.ORDINAL)
    private BookStatus status;
    @Column(name="publishDate")
    private Date publishDate;
    @JoinColumn(name="rentUserId", nullable=true)
    @ManyToOne
    private User rentUser;
    @Column(name="comment", nullable=true)
    private String comment;
    @OneToMany(mappedBy="book")
    private Set<History> bookHistories = new ArrayList<>();
}


@Entity
@Table(name="histories")
public class History {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @ManyToOne
    @JoinColumn(name="userId")
    private User user;
    @ManyToOne
    @JoinColumn(name="bookId")
    private Book book;
    @Enumerated(EnumType.ORDINAL)
    @Column(name="actionType")
    private ActionType action;
    @Column(name="insertDate")
    private Date insertDate;
}

@Entity
@Table(name="users")
public class User {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;
    @Column(name="name")
    private String name;
    @Column(name="password")
    private String password;
    @Enumerated(EnumType.ORDINAL)
    @Column(name="level")
    private UserLevel level;
    @Column(name="point")
    private int point;
    @OneToMany(mappedBy="user")
    private List<History> histories = new ArrayList<>();
    @OneToMany(mappedBy="rentUser")
    private List<Book> rentBooks = new ArrayList<>();
}

@annotation이 많이 생겼습니다. 각각의 @annotation에 대해서 간단히 알아보도록 하겠습니다. 

@Entityentity 객체임을 지정하는 annotation입니다.
@Tableentitty 객체와 연결되는 table을 지정하는 annotation입니다.
@IdPK를 지정하는 annotation입니다.
@ColumnDB의 Column과 1:1로 mapping되는 Property를 지정하는 annotation입니다.
@GeneratedValuePK의 값이 지정되는 방법을 결정할 수 있습니다. AUTO의 경우 mysql의 AUTO_INCREMENT에 해당됩니다. 각각의 DB에 따라 다른 값을 넣어주는 것도 가능합니다.(ex : oracle의 sequence)
@Enumeratedenum 값과 1:1로 mapping이 되는 것을 지정합니다.
@ManyToOne자신과 같은 객체들이 다른 한개의 객체에 연관이 있음을 지정합니다. 이는 N:1의 속성을 지정하게 됩니다.
@JoinColumn@ManyToOne과 같이 사용됩니다. Join 되는 Column을 지정합니다. (FK column)
@OneToMany자신이 다른 객체들에 연관이 있음을 지정합니다. 이는 1:N 또는 N:N의 속성을 지정합니다.

여기에서 개발자들에게 개념적으로 힘든 부분이 @ManyToOne과 @JoinColumn, @OneToMany입니다. 먼저 지금까지 만든 entity에 대한 개념을 한번 더 정립해보도록 하겠습니다. 


하나의 엔티티 클래스는 다음의 요구조건을 충족해야 합니다:

# 클래스 선언부에 javax.persistence.Entity 어노테이션을 반드시 명시하여야 합니다.
# 기본 생성자를 반드시 포함해야 합니다. 기본생성자는 인수가 없는 생성자를 의미합니다. 만일, 인수를 포함한 생성자를 사용한다면 명시적으로 기본 생성자를 만들어야만 합니다.
# 클래스를 fianl로 선언해서는 안됩니다. 영속화 대상이 아닌 필드나 메소드는 반드시 final로 선언합니다.
# 엔티티 인스턴스가 세션빈의 리모트 비지니스 인터페이스와 같이 detached object형태로 전달되는 경우, 클래스는 반드시 Serializble 인터페이스를 구현해야 합니다.
# 엔티티는 엔티티 또는 non-엔티티 클래스 모두 확장(extend)이 가능하며, non-엔티티 클래스는 엔티티 클래스를 확장할 수 있습니다.
# 영속화 인스턴스 변수는 반드시 private, protected, package-private중 하나로 선언되어야 하며, 엔티티 클래스의 메소드에 의해 직접 참조될 수 있습니다. 클라이언트는 엔티티의 상태를 접근자(accessor)또는 비지니스 메소드를 통해 접근이 가능합니다.

또한, 영속상태의 엔티티는 엔티티의 인스턴스 변수 또는 자바빈 스타일의 속성에 의해 접근할 수 있습니다. 필드 또는 속성은 반드시 다음의 자바 언어 타입에 따릅니다.

# 자바 원시 타입
# java.lang.String
# 그외 직렬화 가능(Serializable) 타입들
# 자바 원시타입의 Wrappers
# java.math.BigInteger
# java.math.BigDecimal
# java.util.Date
# java.util.Calendar
# java.sql.Date
# java.sql.Time
# java.sql.Timestamp
# 사용자 정의 직렬화 타입들
# byte[]
# Byte[]
# char[]
# Character[]
# 열거형(Enumerated) 타입들
# 다른 엔티티 또는 엔티티 컬렉션
# 내장형(Embeddable) 클래스

각 엔티티들은 다음과 같은 관계를 가질 수 있습니다. 

# One-to-one : 각 엔티티 인스턴스는 하나의 인스턴스가 다른 엔티티와 연관됩니다. One-to-one 관계는 javax.persistence.OneToOne 어노테이션으로 해당 필드에 정의합니다.
# One-to-many : 하나의 엔티티 인스턴스가 다수의 다른 엔티티 인스턴스와 연관됩니다. 영업주문의 경우 다수의 라인 아이템을 가집니다. 즉, Order 엔티티는 여러개의 LineItem 을 가지므로 이들 사이에는 One-to-many 관계가 선언되어야 하므로 javax.persistence.OneToMany 어노테이션을 사용합니다.
# Many-to-one : 다수의 인스턴스가 하나의 다른 엔티티와 연관됩니다. 이것은 One-to-many 와 반대입니다. 영업주문의 경우 LineItem은 Order에 대해 Many-to-one 관계가 성립되므로 javax.persistence.ManyToOne 어노테이션을 지정합니다.
# Many-to-many : 이 인스턴스는 다수의 인스턴스가 각기 다른 엔티티들과 연관됩니다. 예를들어, 학교에서 각 수업들은 다수의 학생들과 연관이 있으며, 학생 역시 다수의 수업을 듣고 있습니다. 이경우 수업과 학생은 Many-to-Many 관계가 성립되며 javax.persistence.ManyToMany 어노테이션을 사용합니다.

엔티티 연관의 방향 : 엔티티간의 연관 관계는 단방향 또는 양방향이 될 수 있습니다. 양방향 관계에서 한쪽은 소유자측이 되며 그 반대는 피소유자가 됩니다. 단방향 연관은 소유자측만 대변합니다. 연관관계에서 소유자측은 영속성 런타임(Persistence Runtime)이 연관관계에 있는 데이터를 어떻게 갱신할지 결정합니다.

양방향 관계

양방향 관계에서, 각 엔티티는 서로 상대필드나 속성에 대한 참조를 가집니다. 예를들어, User는 어떤 History 인스턴스들이 있는지 알고 있으며 History는 자신이 어떤 User에 속해있는지 알고 있습니다. 이 경우 이들은 양방향 관계를 가지고 있다고 볼 수 있습니다.
양방향 관계는 반드시 다음의 규칙을 따릅니다:

# 양방향 관계의 반대편(피소유자)은 반드시 소유자측을 mappedBy를 사용해 참조해야 합니다. mappdBy는 @OneToOne, @OneToMany 또는 @ManyToMany 어노테이션에서 사용할 수 있습니다. mappedBy 는 소유자 엔티티의 필드나 속성과 대응됩니다.
# Many-to-one 양방향 관계에서 Many측 관계는 mappedBy요소를 사용할 수 없습니다. Many측은 항상 관계에서 소유자측이어야 합니다.
# one-to-one 양방향 관계에서, 소유자측은 다른측에 대한 FK를 가지는쪽입니다.
# Many-to-many 양방향 관계는 양쪽중 둘 중 아무나 소유자가 될 수 있습니다.


entity와 entity간의 관계에 대한 정리가 조금 되셨나요? 이 부분이 이해가 되지 않는다면 Dao부분을 구성할 수 없습니다. 꼭 코드와 같이 이해를 해주시길 바랍니다. 

자, 이제까지 만들어진 객체를 기반으로 Dao를 작성해보도록 하겠습니다. 

먼저, SessionFactory를 생성해주는 객체를 구성해보도록 하겠습니다. SessionFactory는 Configuration 객체를 통해서 구현이 되며, 위에 소개된 hibernate.cfg.xml 파일을 기반으로 구성이 되게 됩니다. 코드는 다음과 같습니다. 

public class HibernateSessionFactoryBuilder {
    public static SessionFactory build(String filename) {
        Configuration cfg = new Configuration();
        cfg.configure(filename);
        ServiceRegistryBuilder serviceRegistryBuilder = new ServiceRegistryBuilder()
                                                            .applySettings(cfg.getProperties());
        SessionFactory sessionFactory = cfg.configure().buildSessionFactory(
                serviceRegistryBuilder.buildServiceRegistry());
        return sessionFactory;
    }
}

그리고, Hibernate는 기본적으로 Session을 통해서 DB와 연결합니다. 그리고 Session의 모든 Action은 Transaction을 기반으로 동작하게 됩니다. 따라서, Template-callback pattern을 이용해서 Hibernate의 Transaction을 처리하는 구문을 만들어주는 것이 구성이 용의합니다. HibernateSqlExecutor를 작성하고, 그 코드는 다음과 같이 동작하게 됩니다. 

public class HibernateSqlExecutor {
    private final SessionFactory sessionFactory;

    public HibernateSqlExecutor(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Object execute(HibernateAction action) {
        Session session = sessionFactory.openSession();
        Transaction transaction = session.beginTransaction();
        try {
            Object ret = action.doProcess(session);
            transaction.commit();
            return ret;
        } catch(Exception ex) {
            transaction.rollback();
            throw ex;
        } finally {
            session.close();
        }
    }
}

public interface HibernateAction {
    Object doProcess(Session session);
}

이 HibernateSqlExecutor를 기반으로 구성되는 Hibernate의 Dao는 다음과 같이 꾸며지게 됩니다. 

public class BookDaoImpl implements BookDao {
    @Autowired
    private SessionFactory sessionFactory;
    
    private HibernateSqlExecutor executor;
    
    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public HibernateSqlExecutor getExecutor() {
        return executor;
    }

    public void setExecutor(HibernateSqlExecutor executor) {
        this.executor = executor;
    }

    @Override
    public void add(final Book book) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(book);
                return null;
            }
        });
    }

    @Override
    public Book get(final int id) {
        return (Book) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Book book = (Book) session.get(Book.class, id);
                return book;
            }
        });
    }

    @Override
    public void update(final Book book) {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(book);
                session.update(book);
                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public void deleteAll() {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                List<Book> books = session.createCriteria(Book.class).list();
                for(Book book : books) {
                    session.delete(book);
                }
                return null;
            }
        });
    }

    @Override
    public int countAll() {
       return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(Book.class)
                        .setProjection(Projections.rowCount())
                        .uniqueResult();
                if(count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }

    @Override
    public void delete(final Book book) {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.delete(book);
                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> getAll() {
        return (List<Book>) executor.execute(new HibernateAction() {
                @Override
                public Object doProcess(Session session) {
                    return session.createCriteria(Book.class).list();
                }
            });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Book> search(final String name) {
        return (List<Book>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(Book.class)
                        .add(Restrictions.like("name", name, MatchMode.ANYWHERE))
                        .list();
            }
        });
    }
}


public class UserDaoImpl implements UserDao {
    @Autowired
    private SessionFactory sessionFactory;
    @Autowired
    private HibernateSqlExecutor executor;

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public HibernateSqlExecutor getExecutor() {
        return executor;
    }

    public void setExecutor(HibernateSqlExecutor executor) {
        this.executor = executor;
    }


    @Override
    public int countAll() {
        return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(User.class).setProjection(Projections.rowCount())
                        .uniqueResult();
                if (count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }

    @Override
    public User get(final int id) {
        return (User) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.get(User.class, id);
            }
        });
    }

    @Override
    public void update(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(user);
                session.update(user);
                return null;
            }
        });
    }

    @Override
    public void delete(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.refresh(user);
                session.delete(user);
                return null;
            }
        });
    }

    @Override
    public void add(final User user) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(user);
                return null;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<User> getAll() {
        return (List<User>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(User.class).list();
            }
        });
    }

    @Override
    public void deleteAll() {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                @SuppressWarnings("unchecked")
                List<User> users = session.createCriteria(User.class).list();
                for (User user : users) {
                    session.delete(user);
                }
                return null;
            }
        });
    }
}

public class HistoryDaoImpl implements HistoryDao {
    @Autowired
    private SessionFactory sessionFactory;
    @Getter
    @Setter
    private HibernateSqlExecutor executor;
    
    @Override
    public void add(final History userHistory) {
        executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                session.save(userHistory);
                return null;
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByUser(final User user) {
       return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("user", user))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public void deleteAll() {
       executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                List<History> userHistories = session.createCriteria(History.class).list();
                for(History userHistory : userHistories) {
                    session.delete(userHistory);
                }
                return null;
            }
        });
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByBook(final Book book) {
        return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("book", book))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }

    @Override
    public int countAll() {
        return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(History.class).setProjection(Projections.rowCount())
                        .uniqueResult();
                if (count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }
}

코드가 많이 바뀌었습니다. 한번 코드에 대해서 논해보도록 하겠습니다. 
Hibernate는 Session을 통해서 DB에 대한 접근을 행하게 됩니다. 그리고 Session은 Criteria를 통해, 각 객체에 대한 query를 생성하게 됩니다. 그리고 Criteria는 Restriction과 Projection을 통해서 각각의 DB에 query를 행하고, CUD의 경우에는 save, update, delete라는 action을 통해서 구성하게 됩니다. 

먼저, countAll()을 하는 code를 간단하게 살펴보도록 하겠습니다. 

    @Override
    public int countAll() {
       return (int) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                Long count = (Long) session.createCriteria(Book.class)
                        .setProjection(Projections.rowCount())
                        .uniqueResult();
                if(count == null) {
                    return 0;
                }
                return count.intValue();
            }
        });
    }

위 코드에서 setProjection을 통해서, rowCount를 하는 것을 지정합니다. 그리고 그 값을 uniqueResult를 통해서 얻어옵니다. rowCount라는 값 자체는 저 query가 수행되면 단일 값으로 나오기 때문에 uniqueResult를 통해서 얻어오게 됩니다. 

그럼 다음은 어떤 코드를 한번봐볼까요? HistoryDao의 getByUser를 한번 봐보도록 하겠습니다. 

    @SuppressWarnings("unchecked")
    @Override
    public List<History> getByUser(final User user) {
       return (List<History>) executor.execute(new HibernateAction() {
            @Override
            public Object doProcess(Session session) {
                return session.createCriteria(History.class)
                        .add(Restrictions.eq("user", user))
                        .addOrder(Order.desc("insertDate")).list();
            }
        });
    }

보시면, History객체의 user property와 동일한 user를 list로 얻어오는 것을 직관적으로 지정하는 것을 알 수 있습니다. 


마지막으로, 테스트 코드를 통해서 Hibernate의 강력함을 한번 느껴보도록 하겠습니다. 지금 객체가 가장 복잡하게 꼬여있다고 생각되는 History에 대한 테스트 코드입니다. 

public class HistoryDaoImplTest {
    private HibernateSqlExecutor executor;
    private BookDaoImpl bookDao;
    private UserDaoImpl userDao ;
    private HistoryDaoImpl historyDao;
    
    @Before
    public void setUp() {
        SessionFactory sessionFactory = HibernateSessionFactoryBuilder.build("hibernate.cfg.xml");
        executor = new HibernateSqlExecutor();
        executor.setSessionFactory(sessionFactory);
        
        bookDao = new BookDaoImpl();
        bookDao.setExecutor(executor);
        
        userDao = new UserDaoImpl();
        userDao.setExecutor(executor);
        
        historyDao = new HistoryDaoImpl();
        historyDao.setExecutor(executor);
        
        historyDao.deleteAll();
        bookDao.deleteAll();
        userDao.deleteAll();
        
        assertThat(bookDao.countAll(), is(0));
        assertThat(userDao.countAll(), is(0));
        assertThat(historyDao.countAll(), is(0));
        
        for(Book book : getBooks()) {
            bookDao.add(book);
        }
        
        for(User user : getUsers()) {
            userDao.add(user);
        }
    }
    
    @Test
    public void addAndCount() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        List<User> users2 = userDao.getAll();
        for(User user : users2) {
            System.out.println("==========================================");
            System.out.println("User : " + user.getName());
            for(History history : user.getHistories()) {
                System.out.println("History : BOOK NAME >" + history.getBook().getName());
            }
        }
    }
    
    @Test
    public void getByUser() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        for(User user : users) {
            List<History> histories = historyDao.getByUser(user);
            assertThat(histories.size(), is(books.size()));
        }
    }
    
    @Test
    public void getByBook() {
        List<Book> books = bookDao.getAll();
        List<User> users = userDao.getAll();
        int count = 0;
        for(Book book : books) {
            for(User user : users) {
                History history = new History();
                history.setAction(HistoryActionType.RENT);
                history.setBook(book);
                history.setUser(user);
                history.setInsertDate(new Date());
                historyDao.add(history);
                count++;
                assertThat(historyDao.countAll(), is(count));
            }
        }
        
        for(Book book : books) {
            List<History> histories = historyDao.getByBook(book);
            assertThat(histories.size(), is(users.size()));
        }
    }
    
    private List<User> getUsers() {
        User user1 = new User();
        user1.setName("name01");
        user1.setPassword("password01");
        user1.setPoint(99);
        user1.setLevel(UserLevel.NORMAL);
        
        User user2 = new User();
        user2.setName("name02");
        user2.setPassword("password02");
        user2.setPoint(101);
        user2.setLevel(UserLevel.READER);
        
        User user3 = new User();
        user3.setName("name03");
        user3.setPassword("password03");
        user3.setPoint(301);
        user3.setLevel(UserLevel.MVP);
        
        return Arrays.asList(user1, user2, user3);
    }
    
    private List<Book> getBooks() {
        Book book1 = new Book();
        book1.setName("book name01");
        book1.setAuthor("autor name 01");
        book1.setComment("comment01");
        book1.setPublishDate(new Date());
        book1.setStatus(BookStatus.NORMAL);
        
        Book book2 = new Book();
        book2.setName("book name02");
        book2.setAuthor("autor name 02");
        book2.setComment("comment02");
        book2.setPublishDate(new Date());
        book2.setStatus(BookStatus.NOWRENT);
        
        Book book3 = new Book();
        book3.setName("book name03");
        book3.setAuthor("autor name 03");
        book3.setComment("comment03");
        book3.setPublishDate(new Date());
        book3.setStatus(BookStatus.MISSING);
        
        List<Book> books = Arrays.asList(book1, book2, book3);
        return books;
    }
}

대부분의 테스트코드와 비슷하게 데이터를 지워버리고, 신규 데이터를 넣도록 구성이 되어 있습니다. addAndCount의 실행결과는 다음과 같습니다. 

==========================================
User : name01
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name02
History : BOOK NAME >book name01
History : BOOK NAME >book name03
==========================================
User : name02
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name03
History : BOOK NAME >book name02
History : BOOK NAME >book name01
==========================================
User : name03
Hibernate: select histories0_.userId as userId0_3_, histories0_.id as id2_3_, histories0_.id as id2_2_, histories0_.actionType as actionType2_2_, histories0_.bookId as bookId2_2_, histories0_.insertDate as insertDate2_2_, histories0_.userId as userId2_2_, book1_.id as id1_0_, book1_.author as author1_0_, book1_.comment as comment1_0_, book1_.name as name1_0_, book1_.publishDate as publishD5_1_0_, book1_.rentUserId as rentUserId1_0_, book1_.status as status1_0_, user2_.id as id0_1_, user2_.level as level0_1_, user2_.name as name0_1_, user2_.password as password0_1_, user2_.point as point0_1_ from userHistories histories0_ left outer join books book1_ on histories0_.bookId=book1_.id left outer join users user2_ on book1_.rentUserId=user2_.id where histories0_.userId=?
History : BOOK NAME >book name01
History : BOOK NAME >book name03
History : BOOK NAME >book name02

내용이 복잡해보이지만, 신기한 특성이 보이지 않나요? User 객체를 한개 얻어왔을 뿐인데, User에 딸린 History가 모두 얻어집니다. 그리고 그 History는 Book객체를 모두 가지고 있고, 이건 DB에 저장된 결과를 보여주고 있습니다. 이와 같이 Hibernate는 DB와 실제 객체간의 연결을 매우 powerful 하게 지원하고 있습니다. DB에 저장된 결과를 마치 고구마 줄기 뽑듯이 객체에 대한 데이터를 들고올 수 있습니다.  

한번 여기까지 Dao 코드를 모두 구성해보고 동작을 확인해보시길 바랍니다. 그리고 console에 Hibernate의 action이 발생하는 경우, 모두 query가 console에 나오게 되어있기 때문에, action에 따른 query 발생이 어떻게 동작하고 있는지 확인하시면 더 재미있는 코드를 짜보실수 있습니다. 

Summary

Hibernate를 통한 Dao의 구성을 알아봤습니다. 여기서 지금 알아본 Hibernate에 대한 내용은 너무나 미약합니다. Hibernate의 경우 책 한권 이상으로 끝나는 내용이 아닙니다. Hibernate의 경우, 굉장히 방대한 양의 Framework입니다. 구성에 대해서 많은 고민을 해보시고, 코드를 한번 더 보시면서 이게 어떤 의미인지 고민을 해보시길 바랍니다. 

한번 더 정리를 하도록 하겠습니다. Hibernate의 경우에는 SessionFactory를 이용한 Session을 통해 DB와 데이터 교환을 행하게 됩니다. SessionFactory는 DataSource를 한번 감싼 형태로, 기존 Jdbc와 비교를 한다면 DataSource와 동일한 위치에 있습니다. 그리고 Session은 jdbcTemplate과 유사한 기능을 행합니다. 이번 예제에서는 HibernateSqlExecutor를 통해서 Session에 query를 보내는 action을 행했습니다. 
그리고, 오늘 제가 보여드린 Hibernate는 Hibernate라는 빙산의 일각일 뿐입니다. xml을 통한 mapping, Set이 아닌 List를 통한 OneToMany mapping 등, 여러가지 내용들이 너무나도 많습니다. 이 부분에 대해서는 좀더 좀더 고민해보시고, 찾아서 공부를 하시는것이 좋습니다.  Hibernate를 통한 지금 DB의 구성 코드에 대해서 전체를 다 작성을 해보시고, 기존 코드와 비교를 해보세요. 이 부분이 진행이 정상적으로 되지 못하면 뒷부분 부터가 많이 힘들어집니다.



Posted by Y2K
,

* 사내 강의용으로 사용한 자료를 Blog에 공유합니다. Spring을 이용한 Web 개발에 대한 전반적인 내용에 대해서 다루고 있습니다.



지금까지 우리는 1개의 Project를 이용해서 간단한 Dao와 Service를 구성해봤습니다. 그렇지만, 실제 프로젝트에서는 여러개의 Project간에 연결이 되어 구성이 되는 것이 일반적입니다. 일반적으로 프로젝트를 구성한다면 약 3개이상의 Project가 구성이 되는것이 대부분입니다. 

다음은 일반적인 Web Project의 구성입니다. 

# Core / Common : Entity, Dao, Service를 갖는 jar project 입니다.
# PublicWeb (CustomWeb) : 사용자에게 보여질 web page입니다. 
# PrivateWeb (AdminWeb) : 관리자용 web page입니다. 사용자에게 공개되는 page가 아닌, 개발자들이 유지보수를 위한 페이지로 만드는 경우가 많습니다.
# Resources : Web에 사용되는 java script 및 image resource, html tag 등을 관리하는 jar project 입니다.

이와 같이 구성이 된다면, 서로간에 build가 되는 순서 역시 변경이 되어야지 됩니다. Core/Common project가 먼저 build가 되고, Resource, PublicWeb, PrivateWeb 순으로 build가 되는 것이 일반적일 것입니다. 그런데, 지금까지 우리가 구성한 project는 pom.xml을 이용한 단일 project만이 지원이 되는 것이 사실입니다. 이런 단일 project가 아닌 multi project를 maven으로 관리하는 법에 대해서 알아보도록 하겠습니다. 

maven에서 multi project를 관리하기 위해서는 parent-child pom 관계가 만들어져야지 됩니다. parent project는 child project에 대한 root project로 구성이 되게 됩니다. 또한 child의 child는 구성이 불가능합니다. 

구성할 프로젝트는 다음과 같습니다. 

ProjectGroup IdArtifactIdcompile orderDescription
Parent Projectcom.xyzlast.bookstorebookstore-Root Project
CommonBLcom.xyzlast.bookstorecommon-bl1Business Logic 구성 Project (Entity, Dao, Service 구성)
WebResourcecom.xyzlast.bookstoreweb-resources2Web Resource를 위치 - image, css, javascript에 대한 common library
PublicWebcom.xyzlast.bookstorepublic-web3사용자들이 방문할 web page
AdminWebcom.xyzlast.bookstoreadmin-web3관리자가 방문할 web page

위 5개의 project는 서로간에 상호 연관성을 갖게 됩니다. 따라서, 위 표와 같이 compile order역시 지정이 가능해야지 됩니다. 

이 예제는 새로운 workspace를 구성해서 살펴보는 것이 좋습니다. 기존 eclipse에서 export preferance기능을 이용해서 사용자 설정을 모두 export 시켜두시고 작업에 임하시는것이 좋습니다. 

1. Parent Project의 생성

새로운 workspace folder에서 다음 명령어를 실행합니다. 

mvn archetype:create -DgroupId=com.xyzlast.bookstore -DartifactId=bookstore

만들어진 bookstore folder안에 있는 pom.xml 을 다음과 같이 수정합니다. 

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.xyzlast.bookstore</groupId>
  <artifactId>bookstore</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <name>bookstore</name>
  <url>http://maven.apache.org</url>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
</project>

이제 다시 eclipse를 실행후, workspace를 새로만든 workspace로 변경합니다. 


그전까지 사용하던 설정을 다시 얻어와야지 되기 때문에 File > Import를 이용해서 preferance를 다시 얻어 설정합니다. 후에 eclipse를 종료 후, 다시 시작해주는것이 좋습니다.
File > Import를 이용해서 Existing Maven Projects 를 선택 후, bookstore를 import 시킵니다. 



이제 Parent Project의 구성은 모두 마쳐졌습니다. 


- Eclipse를 이용하는 방법

Eclipse에서 new maven project를 선택. Project type을 pom-root로 선택해서 신규 프로젝트를 선택, 생성합니다.



2. Child Project의 구성

child project를 구성하는 것은 eclipse에서 진행하도록 하겠습니다. build 순서에 따라 common-bl을 먼저 구성하도록 하겠습니다. 
New Project를 선택해서, Maven Module을 선택합니다. (Maven Project가 아닙니다!)



archeType은 기존 project를 만든것과 동일하게 maven-archetype-quickstart를 선택해줍니다.

groupId 값 및 만들어질 package 이름을 정합니다. package 이름은 jar 이름이 됩니다. 그리고 버젼을 기억하기 쉬운 1.0.0으로 변경합니다.





Finish를 누르면 다음과 같이 Project가 만들어집니다. 


이제 나머지 Project들을 모두 만들어줍니다. Resource의 경우에는 동일하게 maven-archetype-quickstart로 구성을 해주고, PublicWeb, PrivateWeb의 경우에는 maven-archetype-webapp 으로 구성을 해주세요. 모든 Project의 구성이 마쳐지면 다음과 같은 모습이 만들어집니다. 



bookstore(root) project를 확인해보면 다음과 같은 Folder구조를 갖게 됩니다.


그리고 실제 폴더 구조 역시 Root Project에 하위 폴더가 만들어져서 Project가 구성되게 됩니다. Root Project의 pom.xml 을 한번 확인해보도록 하겠습니다. 기존까지는 없던 modules 항목에 다음과 같은 항목들이 추가 되어 있을것입니다. 

  <modules>
    <module>common-bl</module>
    <module>web-resources</module>
    <module>public-web</module>
    <module>private-web</module>
  </modules>

modules 안에 있는 내용대로 build process가 진행되게 됩니다. 확인을 위해, command를 이용해서 mvn compile을 진행해보도록 하겠습니다.




이제 Project간에 서로간에 package를 copy 하는 과정을 추가하도록 하겠습니다. 먼저 Project는 common-bl과 web-resources는 서로간에 dependencies가 없고, public-web, private-web은 common-bl과 web-resources에 dependency를 가지게 됩니다. Root Project에 dependency를 갖는 project를 등록해서 각각의 Project에서 사용하도록 하겠습니다. 

Root Project (BookStore Project)의 pom.xml를 열어, 다음과 같은 항목을 추가합니다. 

 <dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.xyzlast.bookstore</groupId>
            <artifactId>common-bl</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.xyzlast.bookstore</groupId>
            <artifactId>web-resources</artifactId>
            <version>1.0.0</version>
        </dependency>        
    </dependencies>
  </dependencyManagement>

이제 dependency를 가질 private-web과 public-web에 다음과 같은 dependency를 추가하도록 합니다. 

  <dependencies>
    <dependency>
        <artifactId>common-bl</artifactId>
        <groupId>com.xyzlast.bookstore</groupId>
    </dependency>
    <dependency>
        <artifactId>web-resources</artifactId>
        <groupId>com.xyzlast.bookstore</groupId>
    </dependency>
  </dependencies>

버젼이 들어가지 않는것을 확인해주세요. 버젼정보은 root project에서 관리되는 버젼을 따라가게 됩니다. 
추가 된 후에, pom.xml의 dependencies를 보면 다음과 같이 표시됩니다. 




추가를 모두 마치고 나서 다시 maven에서 package를 진행해보도록 하겠습니다. 

mvn clean package

packaging 작업이 모두 마쳐진 후, private-web 또는 public-web의 target/private-web/WEB-INF/lib 에 보시면 작업한 common-bl과 web-resources가 jar 형태로 compile 되어서 copy 되어 있는 것을 확인할 수 있습니다. 





3. Parent Project 설정

maven을 이용한 multi project를 구성을 하면, parent의 속성이 child project들에게 상속이 됩니다. 
지금 구성은 특별히 고치지 않았다면 J2SE-1.5로 compile이 되도록 되어 있습니다. 이것을 java 1.7로 compile이 되도록 parent project를 수정해주도록 합니다. build의 PlugIn에 다음 항목을 추가하도록 하겠습니다. 

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.0</version>
        <configuration>
          <source>1.7</source>
          <target>1.7</target>
          <encoding>utf8</encoding>
        </configuration>
      </plugin>

추가후, eclipse에서 전체 project의 maven->update를 행하면, project의 build compiler가 변경됨을 알 수 있습니다. 이와 같이, Parent Project에 설정한 항목은 하위 Project에 전달되어 처리가 되게 됩니다. 또한, Parent Project에 dependency에 추가가 되면, Child Project 들에 모두 추가가 됩니다. junit 4.11를 Parent project에 추가하고, child Project에서는 모든 dependency를 제거시켜주세요. pom을 수정후, child project의 pom.xml의 dependency Hierarchy는 다음과 같이 표시가 됩니다. 




common-bl, web-resource는 폴더 모양으로, 자신과 같은 Maven Module로 인식이 되고, junit은 물병모양(jar)로 인식되는 것을 확인해주세요. 


4. Eclipse 설정

개발된 code를 이용해서 서로간에 개발을 하기 위해서 eclipse에 약간의 설정 변경이 필요합니다. common-bl과 web-resources를 참조하고 있는 public-web과 private-web의 project 설정을 변경해야지 됩니다. project의 property에서 java build path 항목에서 common-bl과 web-resources를 추가해주도록 합니다. 




이제 Eclipse에서 public-web과 private-web은 common-bl의 객체에 대한 접근을 할 수 있습니다.


 

Nexus 서버를 이용한 사내 Common Jar의 이용

사내에서 개발을 하게 되면, 기존의 개발 Library들에 대한 재사용을 하게 됩니다. 그리고 몇몇 팀들은 이러한 Common Jar만 계속해서 기능을 업데이트시키고, 개발을 하는 팀이 생기게 됩니다. 대체적으로 기술지원팀이나 RND 팀이 이런 일을 맡게 되지요. 

이렇게 개발된 Common Jar를 배포하기 위한 서버가 Nexus 서버입니다. 
Nexus 서버는 우리가 지금까지 libarary를 등록하기 위해서 사용한 maven central repository와 동일한 기능을 제공합니다. 다만, 관리자가 추가로 등록한 jar를 pom을 통해서 관리를 할 수 있는 기능을 따로 제공하는 차이가 있습니다. 사내에서 사용되는 maven central repository라고 보시면 기능이 완전히 동일합니다. 

다음 주소에 nexus 서버가 설치되어 있습니다.


nexus 서버의 이용은 매우 단순합니다. 기본적으로 pom.xml에 다음과 같은 설정을 추가하시면 nexus 서버를 사용하실 수 있습니다. 

  <repositories>
    <repository>
      <id>local</id>
      <url>http://192.168.13.209:8080/nexus/content/groups/public</url>
      <releases>
        <enabled>true</enabled>
      </releases>
      <snapshots>
        <enabled>true</enabled>
      </snapshots>
    </repository>
  </repositories>

nexus 서버를 이용하는 경우에는 우리가 사내에서 개발한 jar를 서로간에 공유해서 사용하는 것이 가능합니다. 그리고 maven central에서 제공하지 않는 jar를 등록해서 사용하는 것 역시 가능합니다. 대체적으로 oracle, mssql과 같이 3rd party에서 제공하는 jar를 사용하는 경우에는 nexus 서버를 이용해서 사내에서 공유해서 사용하는 것이 일반적입니다. 

많은 것들을 등록해서 사용할 수 있지만, 제가 전에 즐거운 기다림 프로젝트를 하면서 외부 저장소가 필요했었던 jar들은 2개가 있었습니다. 아래 2개의 jar를 등록했으니, pom에 사내 nexus를 등록했을 때와, 등록하지 않았을때를 비교해보시길 바랍니다. 

mssql jdbc driver
<dependency>
  <groupId>com.microsoft.sqlserver.jdbc</groupId>
  <artifactId>sqljdbc</artifactId>
  <version>3.0</version>
</dependency>

java pns (iPhone push module)
<dependency>
  <groupId>com.googlecode.javapns</groupId>
  <artifactId>javapns-jdk16</artifactId>
  <version>2.2</version>
</dependency>


Summary

maven을 이용한 multi project 설정과 nexus 서버를 이용한 jar 구성에 대해서 알아봤습니다. 바로 전 시간까지 구성하던 BookStore는 한개의 Project로만 구성이 되어있습니다. 그렇지만, 이는 실전에서는 쓰이지 못합니다. 

오늘 보여드린 내용들이 결국은 실전에서 사용되는 구성이 됩니다. 우리가 곧 개발할 시스템의 경우에도 마찬가지고, 타팀에서 역시 마찬가지로 구성이 될 것이라고 생각합니다. 

그 이유는 공통된 Business Logic이 존재하고, 이 Logic에 대한 접근 방법이 다른 Web 또는 Application이 여러개 존재할 수 있기 때문입니다. 이러한 다각도적인 접근방법에 있어서, 이러한 multi project 구성은 필수적입니다. 이번에 보여드린 구성은 가장 대표적인 구성입니다. Domain(Model) Layer에 대한 project와 Controller와 View에 대한 project. 그리고 여러개의 Controller/View에 대한 공통 static resource (css, javascript, image)에 대한 Project 구성은 아주 당연하게 사용되는 영역입니다. 일반적으로 모든 web은 3개의 project를 이용해서 구성된다고 생각하시면 좀 더 고민이 줄 수 있을 것 같습니다. 





Posted by Y2K
,