도메인구현 KSUG 20151128
-
Upload
beom-kyun-choi -
Category
Technology
-
view
7.450 -
download
0
Transcript of 도메인구현 KSUG 20151128
몇 가지도메인 구현 이야기
최범균([email protected]), 2015-‐11-‐28
내용• DIP• AGGREGATE과 참조• CQRS• 이벤트
2
#1, 아키텍처와 DIP
3
흔한 구조
ReserveService JavaMailService
Reservation ReservationRepository
4
인프라스트럭처
도메인impl패키지
응용
모듈 구조
ReserveService
JavaMailService
Reservation ReservationRepository
JpaReservationRepository
JPA
5
인프라스트럭처
도메인impl패키지
응용
인프라로의 의존
ReserveService
JavaMailService
Reservation ReservationRepository
JpaReservationRepository
JPA
6
인프라에 의존할 때의흔한 단점
• 구현교체의어려움
• 응용서비스나 도메인로직테스트어려움
7
원인à고수준/저수준이 뒤섞임,즉, 저수준이 고수준에 영향을 줌
ReserveService:예약을 위한 응용 로직 à고수준Java API로 메일을 발송à 저수준
도메인:객체 영속성 à고수준JPA로 보관à 저수준RDBMS에 보관 à저저수준
• 고수준모듈 : 의미있는단일기능을제공하는모듈• 저수준모듈 :고수준모듈의기능을구현하기위해필요한하위기능의실제구현 8
DEPENDENCY INVERSION PRINCIPLE
• 뒤섞임을 제거하려면à고수준입장에서 저수준 구현을추상화해서의존을뒤집음
ReserveService <<interface>>EmailNotifier
JavaMailEmailNotifier
응용 로직을 구성하는“이메일 통지하기"를 표현
9
ReserveService
JavaMailService
DIP 적용예
“저수준의 상세한 내용 없이 구현하기” 참고, http://javacan.tistory.com10
DIP?
도메인
impl패키지
SomeDomainService ReservationRepository
JpaReservationRepository
11
응용 ReserveService
인프라스트럭처
EmailService
JavaEmailService
잘못된 DIP 적용
도메인
impl패키지
SomeDomainService ReservationRepository
JpaReservationRepository
12
응용 ReserveService
인프라스트럭처
EmailService
JavaEmailService
JPA
올바른 DIP 적용
도메인 SomeDomainService ReservationRepository
13
응용 ReserveService
인프라스트럭처
EmailNotifier
JavaEmailNotifier
JpaReservationRepository
DIP 적용아키텍처
ReserveService
Reservation ReservationRepository
JpaReservationRepository
EmailNotifier
JavaMailEmailNotifierController
MessageListener
14
노력• 끊임없는 추상화연습• DIP à고수준모듈입장에서추상화
15
#2, AGGREGATE, 참조
16
많은 객체
17
많은 객체
18
복잡도관계? 이해?
작은 것에매몰
상위 수준에서 묶어서 생각하면
19
AGGREGATE = 개념적으로 하나인 객체군
<<ROOT>>Order OrderLineShippingInfo
Orderer
20
AGGREGATE
• 관련된객체들의 묶음• 특징• 데이터변경시한단위로처리됨• 비슷한또는거의유사한라이프사이클을 갖는객체들• 특히, 삭제시함께삭제됨
• 경계를가짐• 기본적으로, 한Aggregate에 속한객체는다른Aggregate에
속하지않음• 필요성• 많은도메인모델을가능한간단하고이해가능한수준으
로만들필요• 연관의개수를줄임으로써복잡도감소
21
AGGREGATE = 개념적으로 하나인 객체군
Order order = orderRepository.findById(orderId);
order.changeShippingInfo(newShippingInfo);order.calcel();
완전체를 로딩
AGG ROOT가로직/일관성(트랜잭션)처리 책임
22
도메인 모델에서 AGG 찾기
Showing Reservation Customer
Movie DiscountStrategy Rule
SeatNo Grade
23
요구 사항에 따라AGG 경계 정의예매시영화의시간과좌석을할당한다 회원은등급을갖는다
영화는 상영일정을갖는다?
영화별로가격할인규칙이다르다(영화별로다른가격할인을갖는다?)
24
AGG 경계 : 규칙〮트랜잭션 범위
상영관리자가영화일정을추가해도영화정보는바뀌지않음
컨텐츠담당자가영화출연자정보를변경해도상영일정은바뀌지않음
동시성 처리를 할 때, Moive를 변경하는 동안 관련 Showing을 잠금 필요가 없음
Showing
Movie
25
AGG 경계 : 규칙〮트랜잭션 범위
Movie DiscountStrategy Rule
• 영화 별로 할인 정책이 고정된다.• 할인 정책 별로 적용 가능 규칙을 갖는다.
Movie가AGG 루트:• movie.updateDescription(desc);• movie.changeDiscountStrategies(discountStrategies);• movie.calculateFee(showing);
26
AGG 경계 : SRP
Movie DiscountStrategy Rule
• Movie는영화 정보 제공하는 책임만 갖도록 구성• 할인 계산은 별도 모듈로 분리
FeeCalculator
feeCalculator.calculate(movie, showing)
27
편리한 참조?
Showing Reservation Customer
Movie
reservation.getShowing().getTime()reservation.getShowing().getMovie().getTitle()reservation.getCustomer().getName()
28
AGG 간참조의 잠재적 문제편한탐색을오용
(불필요한)고민
showing.getMovie().changeDescription(…);
showing.changeDiscountStrategy(disStrategy);
// Showing.javapublic void changeDiscountStrategy(List<DiscountStrategy> strategies) {this.movie.changeDiscountStrategy(strategies);
}
Lazy Loading vs Earger Loading? OSIV?
29
AGG 간참조의 잠재적 문제확장 방해
Oracle
MySQL
Custom Rule Engine
30
AGG 간 ID로 참조하기
public class Showing {…private MovieId movieId;…
}
public class Reservation {…private ShowingId showingId;private CustomerId customerId;…
}
Showing Reservation Customer
31
연관 객체조합은 응용 서비스에서 처리
public class ReserveService {
public ReservationId reserve(ShowingId showingId, CustomerId customerId) {Showing showing = showingRepository.findById(showingId);checkNotNull(shwoing);Movie movie = movieRepository.findById(showing.getMovie());checkNotNull(movie);Customer customer = customerRepository.findById(customerId);checkNotNull(customer);
Reservation reservation = reservationFactory.create(showing, customer, movie.calculateFee(showing)
);return reservation.getId();}…
} 32
ID 참조의 특징• 복잡도낮춤• 모델복잡도• 구현복잡도
• Lazy Loading효과
• 오용방지• 응집도강화
• AGG별로 확장가능
• 조회성능영향• 별도모듈
• 더많은코드
33
노력• 인내필요• 당장바꾸기힘든DB 조인중심표준• 조인에집착하는개발자
• 기술을떠나구현할 수있는실력쌓기• 꼭ORM이있어야AGGREGATE를 구현할수있는것은아님• 기술중심이아닌도메인중심으로AGGREGATE를 생각
34
#3, CQRS
35
도메인 모델로 뷰처리${resList[0].customer.name}님의예매 목록:
<c:forEach var='res' items='${resList}'>예매번호 : ${res.number}영화 : <a href="…?id=${res.showing.movie.id">
${res.showing.movie.title}</a>
시간 : ${res.showing.time}금액 : ${res.fee}
</c:forEach> 참조로 다 연결한 경우
List<Reservation> resList =reservationRepository.findByCustomer(customerId);
model.addAttribute("resList", resList);
36
복잡한 도메인의 조회기능 특징• 여러AGG에걸쳐데이터접근
Showing-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐id: ShowingIdtime: Time…
Reservation-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐
number: ReservationNoshowing: ShowingIdcustomer: CustomerIdfee: Money…
Customer-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐id: CustomerIdname: String…
Movie-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐id: MovieIdtitle: Stringdescription: String…
${resList[0].customer.name}님의예매목록:
<c:forEach9var='res'9items='${resList}'>예매번호 : ${res.number}영화 : <a9href="…?id=${res.showing.movie.id">
${res.showing.movie.title}</a>
시간 : ${res.showing.time}금액 : ${res.fee}
</c:forEach>
37
복잡한 도메인의 조회기능 특징• 성능중요• 최대한빨리사용자에게 화면을제공해야함
• 고민• 한방쿼리!• 레프트조인!• DB 전용기능!• …
38
한모델로 처리하면
한방쿼리!레프트조인!DB 전용기능!…
같은연관에대해Lazy LoadingEager LoadingBatch Loading…
도메인모델구현복잡도 증가
39
상태 변경모델과 조회 모델분리
[출처 : http://martinfowler.com/bliki/CQRS.html]
40
조회 전용모델
ReservationList-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐
customerName: Stringreservations: List<ReservationData>…
ReservationData-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐-‐
number: StringmovieId: StringmovieTitle: StringshowingTime: Timefee: Money…
${resList.customerName}님의 예매목록:
<c:forEach var='res' items='${resList.reservations}'>예매번호 :${res.number}영화 :<a href="…?id=${res.movieId">
${res.movieTitle}</a>
시간 :${res.showingTime}금액 :${res.fee}
</c:forEach>
41
다른 기술, 다른아키텍처 사용
DAO(MyBatis)
컨트롤러
뷰 모델
컨트롤러
응용 서비스
도메인
인프라(JPA)
42
다른 저장소 사용
DAO
컨트롤러컨트롤러
응용 서비스
도메인
인프라(JPA, 메시징)
43
같은 기술, 뷰모델추가
컨트롤러
뷰 데이터 모델
컨트롤러
응용 서비스
도메인
인프라(JPA)
응용 서비스
44
CQRS 이점• 모델복잡도감소
도메인자체에집중조회성능을위한코드없음
45
CQRS 이점• 조회성능향상에유리
조회전용DB를사용한처리량향상
조회에특화된쿼리
조회단위캐시기술적용
46
부담 요인• 더많은코드, 더많은기술
더 많은 코드
데이터 동기화 처리
더 많은 구현 기술
47
노력• 장점과단점을 고려해서 CQRS 도입여부결정필요• 트래픽이없는데조회전용저장소고집하지말것• 트래픽이증가하면조회전용저장소도입검토
• 도메인이 복잡하면 효과있음• 더많은코드à더많은시간 : X• 한모델로뷰처리비용>> 뷰모델로처리비용 : O
48
#4, 이벤트
49
예매 취소시 환급하려면?
• 도메인모델에서 처리?
50
public class Reservation {
public void cancel(RefundService refundSvc) {… //취소처리
refundStatus = State.REFUND_STARTED;
try {refundSvc.refund(this.paymentId);refundStatus = State.REFUND_COMPLETED;
} catch(SomeException ex) {…
}}
}
예매 취소시 환급하려면?
• 응용서비스에서 처리?
51
public class CancelReservation {
@Transactionalpublic void cancel(ReservationId resId) {Reservation res = findById(resId);res.cancel();
res.refundStarted();try {refundSvc.refund(res.getPaymentId());res.refundCompleted();
} catch(SomeException ex) {…
}}
}
고민거리 : 트랜잭션?
52
public class Reservation {
public void cancel(RefundService refundSvc) {… //취소처리
refundStatus = State.REFUND_STARTED;
try {refundSvc.refund(this.paymentId);
refundStatus = State.REFUND_COMPLETED;} catch(SomeException ex) {…
}}
}
public class CancelReservation {
@Transactionalpublic void cancel(ReservationId resId) {Reservation res = findById(resId);res.cancel();
res.refundStarted();try {refundSvc.refund(res.getPaymentId());res.refundCompleted();
} catch(SomeException ex) {…
}}
}
외부 서비스가 정상이 아닐 경우,트랜잭션 처리는?
고민거리 : 성능
53
public class Reservation {
public void cancel(RefundService refundSvc) {… //취소처리
refundStatus = State.REFUND_STARTED;
try {refundSvc.refund(this.paymentId);
refundStatus = State.REFUND_COMPLETED;} catch(SomeException ex) {…
}}
}
public class CancelReservation {
@Transactionalpublic void cancel(ReservationId resId) {Reservation res = findById(resId);res.cancel();
res.refundStarted();try {refundSvc.refund(res.getPaymentId());res.refundCompleted();
} catch(SomeException ex) {…
}}
}
외부 서비스 완료까지쓰레드 블록킹
고민거리 : 설계
54
public class Reservation {
public void cancel(RefundService refundSvc) {… //취소처리
refundStatus = State.REFUND_STARTED;
try {refundSvc.refund(this.paymentId);
refundStatus = State.REFUND_COMPLETED;} catch(SomeException ex) {…
}}
}
public class CancelReservation {
@Transactionalpublic void cancel(ReservationId resId) {Reservation res = findById(resId);res.cancel();
res.refundStarted();try {refundSvc.refund(res.getPaymentId());res.refundCompleted();
} catch(SomeException ex) {…
}}
}
예매 도메인 모델에환불 도메인 로직이 섞임
고민거리 : 설계
55
public class Reservation {
public void cancel(RefundService refundSvc) {… //취소처리
refundStatus = State.REFUND_STARTED;
try {refundSvc.refund(this.paymentId);
refundStatus = State.REFUND_COMPLETED;} catch(SomeException ex) {…
}}
}
public class CancelReservation {
@Transactionalpublic void cancel(ReservationId resId) {Reservation res = findById(resId);res.cancel();
res.refundStarted();try {refundSvc.refund(res.getPaymentId());res.refundCompleted();
} catch(SomeException ex) {…
}}
}
기능이 추가되면 필요한 객체 파라미터 추가?DI로 주입 시도?
적용해 볼만한 것
56
이벤트 + 비동기
이벤트•과거에벌어진어떤것• 주로상태의변화
• 이벤트예• 영화추가함(NewMoviedCreated)• 예매함(RevervationCreated)• 영화상영일정추가함(ShowingAdded)• 영화정보변경함(MovieInfoChanged)• 영화가격정책변경함(PricePolicyUpdated)• 예매취소함(ReservationCanceled)
57
이벤트의 구성• 구성• 발생주체(주로 식별자), 일렬번호/버전, 타입, 발생시간• 내용(payload)
• 예, 회원암호변경이벤트• 발생주체 : "회원mad"• 버전 : 1• 타입 : "PasswordChangedEvent"• 발생시간 : 2015-‐11-‐27 09:59:59• 내용 : {id:"mad", newPwd: "xxxx"}
58
이벤트 관련 구성요소
59
이벤트 생성 주체 이벤트 디스패처(이벤트 퍼블리셔)
이벤트 핸들러(이벤트 구독자)
이벤트
이벤트 용도 1
• 트리거• 다른기능을수행하기위한트리거로이벤트를사용• 예시
• 예매를하면SMS로통지한다.• 예매함이벤트àSMS 통지트리거
• 예매를취소하면환불한다.• 예매취소이벤트à환불트리거
60
예매취소
이벤트디스패처
예매취소됨이벤트핸들러예매
취소됨이벤트
예매취소됨이벤트
환불처리
이벤트 용도 2
• 데이터동기화• 다른시스템간데이터동기화목적으로이벤트사용• 예시
• 상영일정추가이벤트핸들러à조회전용저장소에 추가데이터반영
61
상영일정추가
이벤트디스패처
일정추가됨이벤트핸들러일정
추가됨이벤트
일정추가됨이벤트
일정데이터추가
커맨드 모델저장소
쿼리 모델저장소
도메인과 이벤트• 도메인의 상태변경을이벤트로 표현• "~할때", "~가발생하면", "만약~하면"등의요구사항이실
제로상태변경인지확인• 예시
• "영화정보를변경할때"àMovieInfoChangedEvent• "예매를취소하면"àReservationCanceledEvent
62
(도메인) 이벤트 발생과 처리
63
public class Reservation {public void cancel() {…취소로직Events.raise(new ReservationCanceledEvent(getId(), getPaymentNo(), …));
}}
public class CancelReservationService {@Transactionalpublic void cancel(ReservationId resId) {Events.register((ReservationCanceledEvent evt) -‐> refundSvc.refund(evt.getPaymentNo())
);Reservation resv = findById(resId);resv.cancel();
}}
이벤트 적용 장점• 서로다른도메인영역의 로직이섞이는것방지• 불필요한결합(coupling) 제거
64
예매!취소
이벤트!디스패처
예매!취소됨이벤트!핸들러예매!!
취소됨이벤트
예매!!취소됨이벤트
환불!처리
예매 취소에 더 이상 환불 로직 없음예매 도메인에서 환불 도메인으로의 의존 제거
이벤트 적용 장점• 이벤트핸들러 추가로기능확장
65
예매취소
이벤트디스패처
예매취소됨이벤트핸들러예매
취소됨이벤트
예매취소됨이벤트
환불처리
예매취소됨이벤트핸들러2
이메일통지예매
취소됨이벤트
동기 이벤트 처리의 단점• 트랜잭션 처리문제,성능(처리량) 문제
66
public class ReservationService {@Transactionalpublic void reserve(MovieId movieId, ShowingId showingId, CustomerId custId) {
Events.register((ReservationCreatedEvent evt) -‐> emailNotifier.notify(…)
);
…// 예약도메인로직에서이벤트발생
}}
이메일 발송 중 익셉션이 발생하면?
이메일 서버가 응답 시간이 길면?
비동기 : 이벤트 처리 시간• "A할 때, B해라"요구사항에서 A와B의간격• 도메인전문가의 '바로'는 '즉시실행'이아님
• 수용가능한지연허용범위가있음
• 예시• 예매취소시, (늦어도 30분이내에)환급처리함• 결제완료후, (늦어도다음날7시부터)배송상태를조회
할수있어야함• 티켓예매시, (최대10분안에)분단위티켓판매 통계에반
영함• 분단위티켓판매통계가10분주기로생성되어도 대세지장없음
67
비동기 : 이벤트와 데이터일관성• "A할 때, B해라"요구사항에서 A와B의일관성• 실제로는한트랜잭션이 아닌경우많음
• 예시• 이메일발송실패한다고 회원가입을못하지않음• 고객이예매취소하면,운영자가수동으로환불처리가능
68
응용/인프라
비동기 이벤트 처리 방식 1
• 이벤트디스패처에서 메시지 큐에전송
69
이벤트디스패처 메시지큐도메인 메시지
리스너
스토리지
도입시 고려사항• 글로벌 트랜잭션• 메시지큐가 비정상일 때 이벤트 재전송 방안
이벤트핸들러
비동기 이벤트 처리 방식 2
• 로컬이벤트핸들러가DB에 저장• 포워더가 이벤트를 전달
70
응용/인프라
이벤트디스패처
로컬핸들러도메인
메시지리스너
스토리지
메시지큐포워더
단일 트랜잭션으로처리이벤트
저장
이벤트를 주기적으로 읽어와 전달어디까지 전달했는지 추적
DB 저장과 포워더 구현예• 쇼핑몰, ERP,택배사연동
71
쇼핑도메인
쇼핑+이벤트DB
포워더
주문/결제시관련 이벤트를한 트랜잭션으로 저장
ERP연동모듈
택배사연동모듈
1분 주기로 새로전달할 이벤트를읽어와 외부 시스템에전달
비동기 이벤트 처리 방식 3
• 로컬 이벤트핸들러가DB에 저장• 이벤트수신측에서 이벤트를 직접가져감(pull)• 이벤트제공API이용 (RESTful API등)
72
응용/인프라
이벤트디스패처
로컬핸들러도메인
스토리지
이벤트저장
이벤트(REST) API
이벤트Fetcher
이벤트핸들러
단일 트랜잭션으로 처리
읽어올 이벤트 범위를지정해서 가져와 핸들러에 전달
이벤트 데이터 제공함도메인과 같은 프로세스에서실행해도 무방
비동기 이벤트의 특징• 이벤트생성자와 핸들러의트랜잭션 분리• 처리량향상• 고객에게빠른응답
• 구조가복잡해짐• 재발송,재처리등고려사항증가
73
이벤트 도입시 고려사항• 이벤트도착순서
• 이벤트발송실패시 재발송
• 이벤트재처리
74
노력• 사고전환• 상태의변경==이벤트• 순차실행à이벤트+핸들러• 트랜잭션일관성 :비동기처리+Eventually 일관성
• 기술집착버림• 이벤트,비동기자체에대한이해필요• 메시지큐가있어보이지만,간단한기술로도충분히구현
가능• 이벤트+ CQRS
75
#5, 정리
76
내용• DIP• AGGREGATE• CQRS• 이벤트
77