좌충우돌 ORM개발기Daum 김영한
목차• 프로젝트 구조와 모순
• Why ORM?
• DEMO 프로젝트
• ORM 적용시 고민할 2가지 구조
• Domain Model Everywhere
• DTO, Pure Domain Model
프로젝트 구조와 모순
화면 중심 프로젝트
• 화면 중심으로 프로젝트 구조를 구성
• View 단위로 개발자 투입
• Service, Dao, Entity를 각자 만들어 써야함, 공유하는 것은 허용하지 않음
회원관리 화면 Package controller (MemberController) service (MemberService) dao (MemberDao) entity (Member)
상품 관리 화면 Package controller (ProductController) service (ProductService) dao (ProductDao) entity (Product)
회원관리화면 상품관리화면 주문관리화면
A개발자 B개발자 C개발자
회원관리화면
상품관리화면
주문관리화면
A개발자
B개발자
C개발자
MemberController MemberService MemberDao MemberEntity
ProductController ProductService ProductDao ProductEntity
OrderController OrderService OrderDao OrderEntity
회원관리화면
상품관리화면
주문관리화면
A개발자
B개발자
C개발자
MemberController MemberService MemberDao MemberEntity
MemberDao MemberEntity
MemberDao MemberEntity
ProductController ProductService ProductDao ProductEntity
OrderController OrderService OrderDao OrderEntity
ProductDao ProductEntity
중복!!!
• 프로젝트가 계속 진행되면...
• 같거나 유사한 Query가 무수히 나타남
• 회원정보 조회의 경우 11번의 거의 동일한 쿼리 발견
• 수정이 아주 어려움
• 월화수목금금금... --;
화면중심 프로젝트 구조는수 많은 중복을 만든다.
일반적인 프로젝트
• 데이터와 비지니스 로직을 중심으로프로젝트를 설계한다.
회원관리, 상품관리controller - MemberController - ProductControllerservice - MemberService - ProductServicedao - MemberDao - ProductDaoentity - Member - Product
MemberController
ProductController
MemberService
ProductService
MemberDao
ProductDao
Member Product
실전에선멀티 프로젝트
FRONT
ADMIN
BATCH
A,B개발자
C,D개발자
E개발자
ADMIN
BATCH
FRONTMemberDao
MemberEntity
MemberDao
MemberEntity
MemberDao
MemberEntity
중복!!!
CORE
멀티 모듈 프로젝트
MemberDao
MemberEntity
MemberDao
MemberEntity
MemberDao
MemberEntity
FRONT ADMIN BATCH
사용되는 Member의 정보
FRONT
MemberEntity
회원이름 나이 성별
MemberEntity
회원이름 나이 성별
주민번호
회원번호
전화번호
사용되는 Member의 정보
ADMIN
사용되는 Member의 정보
BATCH
MemberEntity
회원이름 성별
주민번호
회원번호
데이터 불일치
• CORE로 통합시 또 다른 문제 발생
• SQL의 Projection은 각 모듈의 비즈니스 로직과 View에 최적화
• findFrontMemberfindAdminMember ...???
데이터 불일치 해결 방안
• Row단위로 전체 조회
• 특별한 경우만 최적화
• 파레토법칙(20/80법칙)
하지만...
Join의 경우
회원 회원관리직원멤버십카드
class Member { Card card; Manager manager;
Card getCard(); Manager getManager();}
회원 회원관리직원멤버십카드
Join의 경우
회원 회원관리직원멤버십카드
Front
SQL : Member Join CardCode : member.getCard()
member.gerManager() ???
Join의 경우
NullPointException!!!NullPointException!!!NullPointException!!!NullPointException!!!
Join의 경우
회원 회원관리직원멤버십카드
Admin
SQL : Member Join ManagerCode : member.getManager()
member.getCard() ???
Join의 경우
NullPointException!!!NullPointException!!!NullPointException!!!NullPointException!!!
Join의 경우
회원 회원관리직원멤버십카드
Batch
SQL : Member Join Manager Join CardCode : member.getManager(), member.getCard()
불필요한 조인, 성능 낭비
Join의 경우
회원 회원관리직원멤버십카드
Front
Admin
Batch
Join의 경우
• Entity가 실행한 SQL에 데이터를 의존
• Entity에 구멍이남
• 결국 비즈니스 로직이 SQL에 의존
• 모든 데이터를 메모리에 올리는 것은 현실적이지 않음
프로젝트 구조와 모순의결론
• Entity가 SQL에 의존
• 구멍난 Entity
• 신뢰할 수 없는 Entity
• 진정한 의미의 계층 분할이 어려움
Why ORM?
ORM을 사용하는 이유• Projection 문제
• Join 관련 데이터 연결 문제
• Entity 데이터에 대한 신뢰
• 제대로 된 프로젝트 구조
• CRUD SQL좀 알아서 해줬으면
• 진정한 객체 지향 개발 가능
• 도메인 주도 개발 가능
SQL을 사용해야 하는 이유
• 우리가 사용 중인 DB는 RDB다!!!
• 통계 데이터
• 아주 복잡한 View
ORM을 사용하는진짜 이유!
컴퓨터가 할 수 있는 일을 더이상 사람이 하지 말자 나는 SQL MAPPER가 아니다!
JDBC xBatis
ORM
여기서 잠깐!
ORM에 관한 오해Q : OBJECT DB같은 것 아닌가요? 이미 망한 기술로 알고 있는데요?
A : OBJECT DB가 아닙니다. ORM은 RDB를 사용해야 한다는 것을 인정하고 RDB를 객체지향 적으로 사용 할 수 있도록 중간 역할을 하기위해 만들어 졌습니다. 그래서 이름도 Object-Relational-Mapping 입니다.
ORM에 관한 오해Q : 성능이 느리다.
A : 지금 JAVA가 느리다고 하는 것과 비슷합니다. 잘 알고 쓰게되면 SQL보다 최적화 하기가 쉽습니다. 가장 중요한 것은 파레토의 법칙입니다. 때로는 순수SQL을 써서 최적화 해야 하는 경우도 있습니다.
예) 여러 Table에 insert시 Hibernate는 한번에 batch insert를 시도함, 1차 케시를 통해 한번 조회한 데이터 조회시 메모리에서 읽어옴
ORM에 관한 오해
Q : ORM을 사용하면 SQL을 못쓴다.
A : 이거야 말로... ORM도 결국은 SQL을 작성해서 DB 와 통신하게 됩니다. 필요하면 언제든지 직접 SQL을 작성 할 수 있습니다. 물론 myBatis 같은 것과 함께 사용하는 것도 가능합니다.
ORM에 관한 오해
Q : SQL을 몰라도 되나요?
A : 오히려 SQL에 관해서 잘 알아야 합니다. 최종적으로는 SQL이 생성되니까요.
ORM에 관한 오해Q : 공부할 내용이 많으면 어쩌지...
A : 이건 인정합니다. --; 중요한 것은 ORM의 철학을 이해하는 것입니다. 하지만 이것저것 다 떠나서 단순하게 사용하더라도 충분히 유용합니다. 궁극적으로는 ORM을 활용해서 제대로 된 DDD(도메인 주도 개발)를 하는 것이 목표라고 생각합니다.
ORM에 관한 오해
Q : 모두 iBatis, myBatis만 사용하는데요?
A : 진실은?
2012년 8월 한국 구글 검색
myBatis31%
iBatis49%
Hibernate12%
JPA8%
myBatis iBatis Hibernate JPA
2012년 8월 인도 구글 검색iBatis3%
Hibernate86%
JPA11%
myBatis iBatis Hibernate JPA
2012년 8월 미국 구글 검색myBatis
2%iBatis4%
Hibernate78%
JPA16%
myBatis iBatis Hibernate JPA
2012년 8월 전세계 구글 검색myBatis
3%iBatis3%
Hibernate61%
JPA33%
myBatis iBatis Hibernate JPA
ORM을 사용하고 싶은데 어떻게 시작하나요?
• 공부해야 합니다!
• JPA?
• Hibernate?
• SpringDataJPA
• QueryDSL
DEMO 프로젝트
• 사용된 기술 : JPA2, Hibernate, Spring-Data-JPA, QueryDSL, Spring, Maven
• source : https://github.com/holyeye/devon2012
멤버십 프로젝트
회원카드
회원카드
카드적립 포인트
1 : N N : 1
1:
N
적립률
도메인 모델
Create
도메인 객체@Getter @Setter@Entitypublic class Member extends BaseEntity<Long>{
private String name; private int age; @OneToMany(mappedBy="member") List<MemberCard> memberCards = new ArrayList<MemberCard>();
public void addMemberCard(MemberCard memberCard) { memberCards.add(memberCard); }}
@Getter @Setter@Entitypublic class Card extends BaseEntity<Long>{
private String name; private int rate; //적립률 //create public CardPoint createCardPoint(int money) { return new CardPoint(name,money,rate); }}
@Getter @Entitypublic class MemberCard extends BaseEntity<Long>{
@OneToMany(cascade=CascadeType.PERSIST) @JoinColumn(name="memberCard_id") private List<CardPoint> cardPoints = new ArrayList<CardPoint>(); @ManyToOne private Member member; @ManyToOne private Card card; //BIZ
public void payMoney(int money) { CardPoint createCardPoint = card.createCardPoint(money); addCardPoint(createCardPoint); } //VIEW Logic public int getTotalPoint() { int totalPoint = 0; for (CardPoint cardPoint : cardPoints) { totalPoint += cardPoint.getPoint(); } return totalPoint; }}
회원 조회, QueryDSL
@RequestMapping("member/home") public String home(MemberCond cond, Model model) {
//QUERY DSL QMember qMember = QMember.member; BooleanExpression containsName = qMember.name.contains(cond.getName()); BooleanExpression gtAge = qMember.age.gt(cond.getAge()); model.addAttribute("members",memberRepository.findAll(containsName.and(gtAge)));
return "member/home"; } public interface MemberRepository extends
JpaRepository<Member, Long>, QueryDslPredicateExecutor<Member>{
}
select * from Member memberwhere (member.name like '%서%') and member.age > 26
회원 등록, SpringDataJPA
@RequestMapping("member/saveMember") public String saveMenu(Member member) {
memberRepository.save(member); return "redirect:/member/home"; }
public interface MemberRepository extendsJpaRepository<Member, Long>, QueryDslPredicateExecutor<Member>{
}
`
회원 수정, Web Binding
/member/memberUpdateForm?id=
@RequestMapping("member/memberUpdateForm") public String memberUpdateForm(@RequestParam("id") Member member, Model model) {
model.addAttribute("member", member); return "member/memberSaveForm"; }
1
Member
회원카드 결제하기 = ORM 저장
10000만원 결제, 10% 적립
회원카드
카드적립 포인트
Domain Driven Design 살짝 맛보기
Aggregate ROOT
Aggregate(집합)
MemberCardRepository
회원카드 결제하기 = ORM 저장 @RequestMapping("memberCard/payMoney") public String payMoney( @RequestParam("memberCardId") MemberCard memberCard, @RequestParam("money") int money) { memberCardService.payMoney(memberCard, money); return "redirect:/memberCard/home"; } @Service
@Transactionalpublic class MemberCardService { public void payMoney(MemberCard memberCard, int money) { memberCard.payMoney(money); }} public class MemberCard {
@OneToMany(cascade=CascadeType.PERSIST) @JoinColumn(name="memberCard_id") private List<CardPoint> cardPoints = new ArrayList<CardPoint>(); public void payMoney(int money) { CardPoint createCardPoint = card.createCardPoint(money); cardPoints.add(createCardPoint); }
public class Card{
private String name; private int rate; public CardPoint createCardPoint(int money) { return new CardPoint(name,money,rate); }}
Auto SAVE
`
`
Spring-Data-JPA 소개• interface로 Repository 자동 생성
(CRUD, 조회)
• simple queriesex) findByName(String name) select * from member where name={name}
• pagination
• Web Binding
• Specifications, QueryDSL지원
QueryDSL 소개
• type-safe JPA queries - 단단하다
• 단순하고 쉽다.
• 지속적인 버전업
• Spring Data에서도 사용가능
ORM 적용시 고민할 2가지 구조
• ORM을 적용하는 방법은 크게 2가지
• Domain Model Everywhere
• Pure Domain Model
Domain Model Everywhere vs
Pure Domain Model
Domain Model Everywhere
Controller Service Repository
Entity+Getter,Setter
+BizLogic+ViewLogic
King
View
• Domain Model을 모든 Layer에서 적극 사용하자.
• OSIV(Open Session In View) 사용
• Getter, Setter 사용
• DTO는 중복 악마다!
Domain Model Everywhere
• 장점• 개발하기에 빠르고 편리함
• 단점• 복잡한 View 표현 힘듬
• Domain Model의 Getter, Setter를 노출, View로직 추가
• Domain Model의 순수성이 떨어짐
• XML, JSON, 외부와의 통신API 위험 = 도메인 모델은 생각보다 자주 변함
Domain Model Everywhere
Domain Model Everywhere
Controller Service Repository
Entity+Getter,Setter
+BizLogic+ViewLogic
King
View
Pure Domain Model
Controller Service Repository
Entity+BizLogic
View
DTO+Getter,Setter
+ViewLogic
Converter
• 도메인모델에 Getter, Setter는 지옥행
• 도메인모델에 View로직은 지옥행
• OSIV(Open Session In View)는 지옥행
• 도메인모델을 객체답게 만들어라!
Pure Domain Model
Pure Domain Model
Robert C. Martin(uncle bob) 필독서!!!
OOP캡슐화, 다형성내부 변수를 노출하지 않음
일반적으로 Getter, Setter 를 이용해서 내부 변수를 모두 노출사이비 캡슐화
Pure Domain Model
객체(Domain Model)
자료 구조(DTO)
Pure Domain Model
객체(Domain Model)
+
자료 구조(DTO)
Pure Domain Model
객체(Domain Model)
자료 구조(DTO)
+ =
잡종
Pure Domain Model
잡종이런 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다.
양쪽 세상에서 단점만 모아 놓은 구조다. 그러므로 잡종 구조는 되도록 피하는 편이
좋다.- Clean Code, Chapter 6 객체와 자료구조 -
• 그럼 어떻게 개발하라고요--;
Pure Domain Model
• CQRS Pattern, DTO 활용
• 도메인 모델 = 비즈니스 로직
• View는 DTO만 사용
Pure Domain Model
CQRS
• Command Query Responsibility Segregation
• 등록,수정,삭제 같은 명령어와조회용 쿼리를 분리!
CQRS
• QueryService
• CommandService
Command Query
변경
검증
비즈니스 로직수정
가끔 변경됨 자주 변경됨
O X
O X
• 장점
• 순수한 Domain Model
• 단점
• DTO를 만드는 것은 귀찮다, 성가시다. 괴롭다.
• DTO는 또다른 중복이다.
• DTO와 Domain Model의 Convert를 고민해야 한다.
Pure Domain Model
결론
그럼 어떻게 해요?
여기서 부턴 지극히 주관적입니다.
순수한 객체No Getter, Setter!Law of Demeter
SRP, DIP, OCP, ISP, LSP
도메인 객체는순수해야 한다!
Domain Model Everywhere
객체의 순수성을 파괴하는 사악한 방법!
Getter, Setter 필수Entity = 자료구조+객체
이라고 믿고 살았습니다...직접 개발해 보기 전까진...
진짜 ORM 개발을 해보신 분은 아시겠지만...
도메인 모델만으로모든 View를 표현하는 것은
한계가 있습니다.
항상 DTO를 만드는 것은실용적이지 않습니다.
항상 DTO를 만들면 도메인 객체에 Getter, Setter만들 필요가 없고, VIEW로직 필요 없고순도 100% 도메인 객체를 유지가능 합니다, 하지만 DTO를 아주 많이 만들어야 하고 또한
ENTITY를 DTO로 바꾸고 DTO를 ENTITY로 변경해야 하는 컨버팅 필요. 관리 포인트가 3가지로 늘어남. 아주 괴로움. 때로는 배보다 배꼽이 큽니다!
결론
Domain Model Everywhere 로 시작Domain Model로 처리하기 힘든복잡하고 특별한 경우에는CQRS, DTO를 사용
Domain Model Everywhere는 진짜 실용적이고 개발하기 편합니다.DTO가 필수인 경우 = ex) 복잡한 View, [json, xml, 외부노출 API]
CQRS, DTO, Domain Model Everywhere는 프로젝트 규모와 상황을 판단해서 적용해야 합니다.지극히 주관적인 생각으로는 프로젝트 시작시 Domain Model Everywhere로 시작하고 CQRS, DTO는 필요성을 느끼게
되면 추가해 가는 것이 좋다고 생각합니다. 순도 100%는 아니지만 80%는 지킬 수 있다고 생각합니다.
• Domain Driven Design
• Pure Domain Model 을 유지하며 실용적으로 개발하는 방법
• DTO를 생성하는 방법, 활용법
• ex) http://modelmapper.org/
• ex) Builder Pattern
우리의 과제
• 이터너티님의 블로그(http://aeternum.egloos.com)
• 백기선님 블로그 (http://whiteship.me)
• 이미 2005년에 작성된 토비님의 아티클Domain Model vs. DTO : http://toby.epril.com/?p=99
• 실전 코드가 있는 자바지기님의 DTO에 관한 최근 아티클 : http://www.slipp.net/wiki/pages/viewpage.action?pageId=2031636
• Javacan님의 DDD 발표자료 : http://javacan.tistory.com/entry/DDD-%EC
%8C%A9%EA%B8%B0%EC%B4%88-%EC%84%B8%EB%AF%B8%EB%82%98-%EB%B0%9C%ED%91%9C-%EC%9E%90%EB%A3%8C
좋은 자료
• Clean Code (Chapter 6 객체와 자료 구조)
• http://codebetter.com/gregyoung/2010/02/16/cqrs-task-based-uis-event-sourcing-agh/
• http://martinfowler.com/bliki/CQRS.html
참고
감사합니다.
시간이 부족해 발표하지 못한 내용입니다.Domain Model Everywhere에서
DTO, CQRS를 사용해 Pure Domain Model로 리펙토링되는 상황을 연출해 보았습니다.
Entity and View
• Entity는 비즈니스 로직을 위한 것이지 View를 위한 것이 아니다.
Entity and View
회원ENTITY+BIZ LOGIC
Entity and View
회원ENTITY+BIZ LOGIC+ViewA로직+ViewB로직+ViewC로직
.....
View A
View B
View C
그래 나 뚱뚱하다!
Entity and View
• Entity가 View 로직 때문에 뚱뚱해진다.
• OSIV 사용
• JSON, XML 문제점• 객체 그래프를 찾아 가는 것을 View개발자가 힘들어 한다. ex) member.getCard().getData()...
Entity and View
• View를 위한 DTO 가 필요한 시점
Entity and View
회원ENTITYBIZ LOGIC+ViewA로직+ViewB로직+ViewC로직
.....
View A
View B
View C
그래 나 뚱뚱하다!
Entity and View
회원ENTITYBIZ LOGIC
View A
View B
View C
회원DTO+ViewA로직+ViewB로직+ViewC로직
Entity and View
회원ENTITYBIZ LOGIC
View A
View B
View C
회원DTO+ViewA로직+ViewB로직+ViewC로직
View D 회원관리DTO+ViewD로직
Entity and View
• View와 비즈니스 로직의 요구사항 다름
• Entity = 비즈니스 로직
• DTO = VIEW
뚱뚱한 Service
• 이제는 Service가 뚱뚱해 진다.
뚱뚱한 Service
회원ENTITYBIZ LOGIC+ViewA로직+ViewB로직+ViewC로직
.....
View A
View B
View C
그래 나 뚱뚱하다!
뚱뚱한 Service
회원ENTITYBIZ LOGIC+ViewA로직+ViewB로직+ViewC로직
.....
View A
View B
View C
그래 나 뚱뚱하다!
회원Service+회원등록()+회원수정()+회원탈퇴()
+getMember()
뚱뚱한 Service
회원ENTITYBIZ LOGIC
회원DTO+ViewA로직+ViewB로직+ViewC로직
View A
View B
View C
회원Service+회원등록()+회원수정()+회원탈퇴()
+getMemberDTO()
뚱뚱한 Service
회원ENTITYBIZ LOGIC
View A
View B
View C
회원DTO+ViewA로직+ViewB로직+ViewC로직
View D 회원관리DTO+ViewD로직
회원Service+회원등록()+회원수정()+회원탈퇴()
+getMemberDTO()+getMemberManageDTO()
...DTO()
그래 나 뚱뚱하다!
뚱뚱한 Service
• 음... 이건 어떻게 하지?
뚱뚱한 Service
• 등록,수정,삭제 같은 명령어와 조회용 쿼리를 분리하자!
뚱뚱한 Service
회원ENTITYBIZ LOGIC
View A
View B
View C
회원DTO+ViewA로직+ViewB로직+ViewC로직
View D 회원관리DTO+ViewD로직
회원Service+회원등록()+회원수정()+회원탈퇴()
+getMemberDTO()+getMemberManageDTO()
...DTO()
그래 나 뚱뚱하다!
뚱뚱한 Service
회원ENTITYBIZ LOGIC
View A
View B
View C
회원DTO+ViewA로직+ViewB로직+ViewC로직
View D 회원관리DTO+ViewD로직
회원CommandService+회원등록()+회원수정()+회원탈퇴()
회원QueryService+getMemberDTO()
+getMemberManageDTO()...DTO()
CQRS
• Command Query Responsibility Segregation
• 등록,수정,삭제 같은 명령어와조회용 쿼리를 분리!
CQRS
• QueryService
• CommandService
Command Query
변경
검증
비즈니스 로직수정
가끔 변경됨 자주 변경됨
O X
O X
CQRS 적용 전출처 : http://martinfowler.com/bliki/CQRS.html
CQRS 적용 후출처 : http://martinfowler.com/bliki/CQRS.html
멀티 모듈인 경우
• 멀티 모듈인 경우를 생각해보자
CORE
FRONT ADMIN BATCH
회원Service+회원등록()+회원수정()+회원탈퇴()
+getMemberDTO()+getMemberManageDTO()
...DTO()
CORE
FRONT ADMIN BATCH
회원CommandService+회원등록()+회원수정()+회원탈퇴()
회원QueryService+getMemberDTO()
+getMemberManageDTO()...DTO()
CORE
FRONT ADMIN BATCH
회원CommandService+회원등록()+회원수정()+회원탈퇴()
회원QueryService+getMemberDTO()
+getMemberManageDTO()+getFrontMemberDTO()+getBatchMemberDTO()
멀티 모듈인 경우
• 각각의 모듈에 특화된 Query
• Core에 두는 것이 옳은가?
CORE
FRONT ADMIN BATCH
회원CommandService+회원등록()+회원수정()+회원탈퇴()
회원QueryService+getMemberDTO()
+getMemberManageDTO()+getFrontMemberDTO()+getBatchMemberDTO()
CORE
FRONT ADMIN BATCH
회원CommandService+회원등록()+회원수정()+회원탈퇴()
회원QueryService+getMemberDTO()
회원FrontQueryService회원BatchQueryService
회원AdminQueryService
CORE
FRONT ADMIN BATCH
회원CommandService+회원등록()+회원수정()+회원탈퇴()
회원FrontQueryService 회원BatchQueryService회원AdminQueryService
멀티 모듈인 경우
• 모듈에 특화된 조회서비스는 해당 모듈에서 관리
• Core모듈의 복잡성이 줄어듬
• 비즈니스 로직은 Core에서 관리
감사합니다.
Recommended