39
이벤트 소싱(Event Sourcing) 학습 내용 공유 신림프로그래머, 최범균, 2015-03-21 [email protected]

Event source 학습 내용 공유

Embed Size (px)

Citation preview

Page 1: Event source 학습 내용 공유

이벤트 소싱(Event Sourcing) 학습 내용 공유

신림프로그래머, 최범균, 2015-03-21 [email protected]

Page 2: Event source 학습 내용 공유

다룰 내용 •  이벤트와 이벤트 소싱 •  이벤트 소싱을 이용한 도메인 구현 •  성능 •  정리

2  

Page 3: Event source 학습 내용 공유

지난 70일간의 몸무게 증감 기록 •  전날과의 무게 차이 기록

-­‐2.5  

-­‐2  

-­‐1.5  

-­‐1  

-­‐0.5  

0  

0.5  

1  

1.5  

3  

Page 4: Event source 학습 내용 공유

최종 무게 변화량? •  증감을 모두 더해서 최종 변화량 구함

4  

(-0.5) + 0.2 + 0 + (-0.6) + (-0.2) +

0.2 + 0 + 0.2 + 0.2 + 0.3 +

....

....

(-0.8) + (-1.5) + (-0.1) + 0.7 + 0.5 +

0.2 + 0 + (-0.6) + (-0.5) + 0.4

Page 5: Event source 학습 내용 공유

최종 무게 변화량? •  증감을 모두 더해서 최종 변화량 구함

5  

당일공개 (-0.5) + 0.2 + 0 + (-0.6) + (-0.2) +

0.2 + 0 + 0.2 + 0.2 + 0.3 +

....

....

(-0.8) + (-1.5) + (-0.1) + 0.7 + 0.5 +

0.2 + 0 + (-0.6) + (-0.5) + 0.4

Page 6: Event source 학습 내용 공유

-­‐3  

-­‐2  

-­‐1  

0  

1  

2  

이벤트 •  과거에 벌어진 어떤 것을 이벤트로 정의 – 주로 상태의 변화

6  

몸무게가 1Kg 줄었음

몸무게가 1Kg 늘었음

Page 7: Event source 학습 내용 공유

이벤트의 구성 •  구성 – 생성자, 일렬번호/버전, 타입, 발생 시간 – 내용(payload)

•  예, 회원 암호 변경 이벤트 – 생성자: "회원mad" – 버전: "1" – 타입: "PasswordChangedEvent" – 발생시간: 2015-03-21 09:59:59 – 내용: {id:"mad", newPwd: "xxxx"}

7

Page 8: Event source 학습 내용 공유

이벤트 소싱(Event Sourcing)

8  

* http://martinfowler.com/eaaDev/EventSourcing.html

어플리케이션의 모든 상태 변화를 순서에 따라 이벤트로 보관한다.

Capture all changes to an application state as a sequence of events.

Page 9: Event source 학습 내용 공유

이벤트 소싱과 현재 상태

9  

-0.3kg 몸무게 변했음 이벤트 -0.6kg 몸무게 변했음 이벤트 0.4kg 몸무게 변했음 이벤트 -1.2kg 몸무게 변했음 이벤트

... -0.1kg 몸무게 변했음 이벤트 -0.5kg 몸무게 변했음 이벤트 1.2kg 몸무게 변했음 이벤트 -0.8kg 몸무게 변했음 이벤트 0.2kg 몸무게 변했음 이벤트

발생 순서대로 최초 이벤트부터 마지막 이벤트까지 차례대로 재현하면 현재(최종) 상태가 됨!

이벤트 저장소(Event Store)

Page 10: Event source 학습 내용 공유

10  

이벤트 소싱과 도메인 구현

Page 11: Event source 학습 내용 공유

도메인의 기능 •  암호 변경 예: 엔티티 로딩+로직+상태 변경 – 엔티티(객체 또는 데이터) 로딩

•  로직 수행에 필요한 엔티티 객체(또는 데이터) 로딩 – 로직

•  oldPw가 현재 암호와 일치하는지 검사 •  일치하지 않으면, 암호 변경 실패(예, 익셉션 발생)

– 상태 변경 •  암호를 newPw로 변경함

11

Page 12: Event source 학습 내용 공유

도메인 구현 : SQL(데이터) 중심

12  

-- 서비스: 흐름 처리, 도메인 로직 수행 public void changePw(ChangePwCmd cmd) { MemberDto m = dao.selectById(cmd.getId()); if (m == null) throw new NoMemException(); if (!m.getPwd().equals(cmd.getOldPw())) throw new BadPwException(); m.setPwd(cmd.getNewPw()); dao.updatePw(m); }

-- DAO: 쿼리 실행 public MemberDto selectById(String id) { pstmt = conn.prepareStatement( "select * from member where mem_id=?"); pstmt.setString(1, id); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { MemberDto dto = new MemberDto(); dto.setId(rs.getString("mem_id")); dto.setPwd(rs.getString("pwd")); return dto; } ...close } public void updatePw(MemberDto dto) { pstmt = conn.prepareStatement( "update member set pwd = ? " + "where mem_id = ?" pstmt.setString(1, dto.getPwd()); pstmt.setString(2, dto.getId()); pstmt.executeUpdate(); ... }

-- DTO: 데이터 보관 private String id; private String pwd; public String getPassword() { return pwd; } public void setPassword(String pwd) { this.pwd = pwd; } ...get/set

상태 변경  

Page 13: Event source 학습 내용 공유

도메인 구현 이벤트 소싱 전 - ORM

13  

-- 서비스: 도메인 객체 이용 흐름 처리 public void changePw(ChangePwCmd cmd) { Member m = repo.findById(cmd.getId()); if (m == null) throw new NoMemException(); m.changePw(cmd.getOldPw(), cmd.getNewPw()); }

-- Member 엔티티: -- 매핑 설정, 도메인 기능, 상태 변경 @Id @Column("mem_id")private String id; @Column("pwd")private String password; public void changePw( String oldPw, Stirng newPw) { if (!this.password.equals(oldPw)) throw new BadPwException();

this.password = newPw; }

-- 리포지토리: 도메인 객체 로딩 public Member findById(String id) { // ... ORM 관련 코드 return entityMgr.find(Member.class, id); }

상태 변경  

Page 14: Event source 학습 내용 공유

도메인 구현 이벤트 소싱 적용 •  변화되는 것

14  

영역 적용 전 (RDBMS/ORM) 적용 후

도메인 객체 로딩 SQL : SELECT 쿼리 ORM : 프레임워크가 매핑 설정을 이용해서 SELECT 쿼리 실행

-  이벤트로부터 도메인 객체 생성 -  도메인 객체의 이벤트 핸들러를

이용해 상태 변경 반영

도메인 기능 SQL : 서비스 클래스 ORM : (일부) 엔티티 클래스

-  도메인 객체가 수행 -  상태 변경을 위한 이벤트 생성

상태 변경 반영 (데이터 변경)

SQL : Insert/Update/Delete 쿼리 ORM : 엔티티 프로퍼티를 변경하면 ORM 프레임워크가 알맞은 쿼리 실행

-  도메인 객체가 발생한 이벤트를 저장소에 보관

Page 15: Event source 학습 내용 공유

도메인 객체 로딩 1 •  이벤트 목록 발생 순서대로 저장되어 있다면

15  

회원 ID "mad"와 관련된 이벤트

MemberCreatedEvent("mad", "bk", "[email protected]", "pw") EmailVerifiedEvent("mad") MemberEnabledEvent("mad") PasswordChangedEvent("mad", "newPw") MemberDisabledEvent("mad", "도용의심")

Page 16: Event source 학습 내용 공유

도메인 객체 로딩 2 •  이벤트를 로딩해 도메인 객체에 적용 – 도메인 객체의 이벤트 핸들러를 실행할 때, 인자

로 이벤트 객체 전달

16  

MemberCreatedEvent("mad", "bk", "[email protected]", "pw") EmailVerifiedEvent("mad") MemberEnabledEvent("mad")

PasswordChangedEvent("mad", "newPw") MemberDisabledEvent("mad", "도용의심")

-- MemberRepository public Member load(String id) { List<Event> events = eventStore.load(id); if (events.isEmpty()) return null; Member mem = new Member(); for (Event evt : events) { mem.on(evt); // 이벤트 핸들러 } return mem; }

Page 17: Event source 학습 내용 공유

도메인 객체 로딩 3 •  도메인 객체의 이벤트 핸들러 메서드

17  

mem = new Member(); Member 클래스의 이벤트 핸들러 메서드

mem.on(memCreatedEvent)

on(MemberCreatedEvent e) { this.id = e.getId(); this.name = e.getName(); this.email = e.getEmail(); this.password = e.getPassword(); this.emailValid = false; this.enabled = false; }

mem.on(emailVerifiedEvent) mem.on(memEnabledEvent)

on(EmailverifiedEvent e) { this.emailVaild = true; } on(MemberEnabledEvent e) { this.enabled = true; }

mem.on(pwdChangedEvent) on(PasswordChangedEvent e) { this.password = e.getNewPassword(); }

mem.on(memDisabledEvent) on(MemberDisabledEvent e) { this.enabled = false; }

이벤트  순차  적용

최신  상태  

Page 18: Event source 학습 내용 공유

도메인 객체 로딩 4

18  

Member m = memberRepository.load("mad");

MemberCreatedEvent("mad", "bk", "[email protected]", "pw") EmailVerifiedEvent("mad") MemberEnabledEvent("mad") PasswordChangedEvent("mad", "newPw") MemberDisabledEvent("mad", "도용의심")

m 객체 id = "mad" name = "bk" email = "[email protected]" password = "pw" validEmail = true enabled = false

// load() 메서드 Member mem = new Member(); for (Event evt : events) { mem.on(evt); }

최신상태  

Page 19: Event source 학습 내용 공유

도메인 객체 기능의 변화 1 •  도메인 로직 수행 + 이벤트 생성

19  

-- Member 클래스 public PasswordChangedEvent changePassword( String oldPw, String newPw) { if (!this.password.equals(oldPw)) { throw new IdPasswordNotMatchingException(); } // this.password = newPw; 상태 변경하지 않음 // 변경할 상태 정보를 담은 이벤트 생성 return new PasswordChangedEvent(this.id, newPw); } public void on(PasswordChangedEvent e) { this.password = e.getNewPassword(); }

상태는 이벤트 핸들러에서만 변경  

도메인 기능 메서드는 상태를 변경하는 대신, 변경할 정보를

담은 이벤트 생성  

Page 20: Event source 학습 내용 공유

상태 변경 이벤트 생성 및 보관  상태를 변경시키는 기능  

도메인 객체 기능의 변화 2 •  상태를 변경시키는 모든 도메인 기능은 알맞

은 이벤트를 발생시킴!

20  

new Member(id, name, email, pw, time) à MemberCreatedEvent(id, name, email, pw, time)

mem.verifyEmail(key) à EmailVerifiedEvent(id) MemberEnabledEvent(id)

mem.changePassword(op, np) à PasswordChangedEvent(id, np)

mem.disable(reason) à MemberDisabledEvent(id, cause)

Page 21: Event source 학습 내용 공유

어플리케이션 서비스의 변화 •  도메인 객체가 발생한 이벤트를 저장

21  

public class ChangePasswordService { public void changePassword(ChangePasswordCommand cmd) { Member mem = memRepository.load(cmd.getMemberId()); PasswordChangedEvent evt = mem.changePassword(cmd.getOldPw(), cmd.getNewPw()); eventStore.save(evt); mem.on(evt); } ... }

1. 도메인 객체 로딩

2. 도메인 객체 기능 실행 , 결과로 이벤트 생성

3. 이벤트 저장

4. 도메인 객체의 상태 변경

Page 22: Event source 학습 내용 공유

이벤트 보관 •  물리적인 저장소에 보관 필요 – RDBMS, NoSQL, 파일 등에 보관

•  도메인 객체 별 이벤트 목록 관리 – 이벤트의 발생 순서 유지

22  

회원1 관련 이벤트

version: 1, type: MemberCreatedEvent values: {id: "1", name: "...", ....}

version: 2, type: EmailVerifiedEvent values: {id: "1"}

version: 3, type: MemberEnabledEvent values: {id: "1"}

회원2 관련 이벤트

version: 1, type: MemberCreatedEvent values: {id: "2", name: "...", ....}

version: 2, type: EmailVerifiedEvent values: {id: "2"}

version: 3, type: MemberEnabledEvent values: {id: "2"}

version: 4, type: PasswordChangedEvent values: {id: "2", newPw: "newpw"}

Page 23: Event source 학습 내용 공유

23  

성능

Page 24: Event source 학습 내용 공유

도메인 이벤트가 쌓이면?

24  

회원1 관련 이벤트

version: 1, type: MemberCreatedEvent values: {id: "1", name: "...", ....}

version: 2, type: EmailVerifiedEvent values: {id: "1"}

version: 3, type: MemberEnabledEvent values: {id: "1"}

version: 1,000, type: SomeEvent values: {id: "1", ....}

.........  

한 도메인 객체의 최종 상태를 구하기 위해 한번에 1,000개의 이벤트를 로딩한다면, 전반적인 응답 속도 저하

Page 25: Event source 학습 내용 공유

스냅샷snapshot으로 로딩 속도 향상

25  

회원1 관련 이벤트

version: 1, type: MemberCreatedEvent values: {id: "1", name: "...", ....}

version: 2, type: EmailVerifiedEvent values: {id: "1"}

version: 999, type: MemberEnabledEvent values: {id: "1"}

version: 1,000, type: SomeEvent values: {id: "1", ....}

.........  

특정 버전을 기준으로 이전 이벤트를 누적한 스냅샷 생성

스냅샷 id: 1, version: 999

스냅샷을 기준으로 이후 버전만 로딩

Page 26: Event source 학습 내용 공유

여러 도메인 객체에 대한 조회는? •  다음을 이벤트 소싱으로 처리하려면? – 100만 회원 대상, 최근 1주일 가입 신청자 중

아직 이메일 인증을 하지 않은 회원 찾기

26  

1번 객체 로딩�검사

2번 객체 로딩�검사

100만번 객체 로딩�검사

3번 객체 로딩�검사

................  

겁나게 느린 조회 응답 속도!

Page 27: Event source 학습 내용 공유

조회 전용 모델로 조회 속도 향상 •  시스템을 다음의 두 가지로 분리 – 기능 실행(Command) / 상태 조회(Query)

•  CQRS(Command Query Responsibility Segregation)

27  

이벤트 소싱 기반 도메인 모델

데이터 기반 조회 모델 구현

이벤트 스토어 데이터 저장소

UI  

상태를 변경하는 명령은 이벤트 소싱 기반 모델에 전달

상태 조회 요청은 데이터 기반 모델에 전달

이벤트를 전달해서 조회에 맞는 모델 생성

Page 28: Event source 학습 내용 공유

뷰 전용 모델  

조회 전용 모델 처리 예

28  

MemberCreated Event

EmailVerified Event

insert into NOVERIEDMEM values (....)

Event Store

delete from NOVERIEDMEM where ...

RDBMS

UI   데이터 조회  

Page 29: Event source 학습 내용 공유

29  

기능 변경의 유연함

Page 30: Event source 학습 내용 공유

주문 도메인 취소 예 •  이벤트 소싱 기반 Order 도메인 객체

30  

public class Order { private OrderState orderState; private Payment payment; ... public List<Event> cancel() { if (!orderState.canCancel()) throw new CanNotCancelException(orderState); Event refundedEvent = payment.refund(); return Arrays.asList(refundedEvent, new CanceledEvent()); } public void on(RefundedEvent evt) { payment.on(evt); } public void on(CanceledEvent evt) { orderState = OrderState.CANCELD; } }

Page 31: Event source 학습 내용 공유

취소 시간을 추가하려면? •  이벤트 소싱 기반 Order 도메인 객체

31  

public class Order { private OrderState orderState; private Payment payment; private Date canceledTime; ... public List<Event> cancel() { if (!orderState.canCancel()) throw new CanNotCancelException(orderState); Event refundedEvent = payment.refund(); return Arrays.asList(refundedEvent, new CanceledEvent()); } … public void on(CanceledEvent evt) { orderState = OrderState.cancelState(); canceledTime = evt.getOccuredTime(); // 수정 전 발생 도메인 객체에도 기능 적용 } }

추가한 코드

Page 32: Event source 학습 내용 공유

만약 RDBMS/SQL 기반이었다면?

32

alter table ORDER add column CANCELED_TIME timestamp

pstmt = conn.prepareStatemet("update ORDER set state = 'CANCELED', " + "CANCELED_TIME = ? where ORDER_ID = ?"); … pstmt.setTimestamp(1, new Timestamp(orderDto.getCanceledTime()));… pstmt.executeUpdate();

pstmt = conn.prepareStatement("select * from ORDER where ORDER_ID = ?"); … rs = pstmt.executeQuery(); OrderDto dto = …; dto.setCanceledTime(rs.getTimestamp("CANCELED_TIME"));…

public class OrderDto { … private Date canceledTime; …get/set 추가}

update ORDER set CANCELED_TIME = 어떻게든구해서설정 where ORDER_ID = ?

Page 33: Event source 학습 내용 공유

주문 금액을 long에서 Money로 바꾸면? •  이벤트 소싱 기반 Order 도메인 객체

33  

public class Order { private Money totalAmount; // 기존 long totalAmount ... public OrderPlacedEvent2 place() { … // 기존: return new OrderPlacedEvent(…, totalAmt); // 새로운 타입 이벤트 생성으로 변경 return new OrderPlacedEvent2(…., Money.won(totalAmt)); } public void on(OrderPlacedEvent2 evt) { … totalAmount = evt.getTotalAmount(); } public void on(OrderPlacedEvent evt) { … // 기존에 이미 생성한 이벤트 반영 (호환성유지) totalAmount = Money.won(evt.getTotalAmount()); } }

public class Money { private BigDecimal value; private Currency currency; public static Money won(long value) { return new Money(value, Currency.WON); } … }

새로운 이벤트 타입으로  기존 이벤트 데이터 영향없이

구현 변경

기존 이벤트에 대한  어렵지 않은 호환성 처리

Page 34: Event source 학습 내용 공유

만약 RDBMS/SQL 기반이었다면?

34

alter table ORDER add column TOTAL_AMT2 double; alter table ORDER add column TOTAL_AMT2_CURRENCY varchar(3); update ORDER set TOTAL_AMT2 = TOTAL_AMT, TOTAL_AMT2_CURRENCY='WON'; alter table ORDER drop column TOTAL_AMT; -- 용감한 선택!

pstmt = conn.prepareStatement("select * from ORDER where ORDER_ID = ?"); … rs = pstmt.executeQuery(); OrderDto dto = …; Money totalAmt = new Money(rs.getDouble("TOTAL_AMT2"), Current.of(rs.getString("TOTAL_AMT2_CURRENCY")));dto.setTotalAmt(totalAmt);…

public class OrderDto { private Money totalAmt; … }

pstmt = conn.prepareStatement("insert into ORDER values (?, ?, …, ?, ? "); … pstmt.setDouble(10, orderDto.getMoney().getValue());pstmt.setString(11, orderDto.getMoney().getCurrency().toString());…

Page 35: Event source 학습 내용 공유

35  

정리

Page 36: Event source 학습 내용 공유

장점 •  DB에 의존적이지 않은 도메인 코드 구현 – 테이블이나 ORM 기술의 제한/제약에서 벗어남

•  기능 변경 – 하위 호환 처리가 상대적으로 쉬움 – 이벤트로부터 완전히 새로운 도메인 객체의 생성도 가능

•  버그 추적 용이 – 이벤트를 차례대로 검사하면서 버그 원인 추적 가능

•  객체 지향/DDD와 좋은 궁합 – 복잡한 도메인을 객체 지향적으로 구현하기에 좋음

•  CQRS와 좋은 궁합 – 조회 관련 코드를 도메인에서 분리 – 조회 모델 분리로 조회 성능 향상 가능

36  

Page 37: Event source 학습 내용 공유

단점 •  익숙하지 않음 –  SQL 위주(데이터 중심) 개발 성향인 경우 적응 힘듬

•  단순 모델에는 적합하지 않음 – 단순 모델에 적용하기엔 구현이 복잡해짐

•  도구 부족 – 이벤트 소싱과 CQRS 지원 프레임워크 부족

•  운영시 어려움 – 이벤트 데이터만으로는 최신 상태의 빠른 확인 불가

•  CQRS 필수!

37  

Page 38: Event source 학습 내용 공유

참고자료 •  Event Sourcing Basics :

http://docs.geteventstore.com/introduction/event-sourcing-basics/ •  CQRS : http://martinfowler.com/bliki/CQRS.html •  관련 도구 – Axon Framework : http://www.axonframework.org/ – EventStore : http://www.geteventstore.com – Akka Persistence(실험 버전) :

http://doc.akka.io/docs/akka/snapshot/scala/persistence.html •  학습하면서 연습한 코드 :

https://github.com/madvirus/evaluation 38  

Page 39: Event source 학습 내용 공유

39