[SaaS] ์๊ฐ์ฌํ์ด ๊ฐ๋ฅํ ์์คํ ์ํคํ ์ฒ
[SaaS] ์๊ฐ์ฌํ์ด ๊ฐ๋ฅํ ์์คํ ์ํคํ ์ฒ ๊ด๋ จ
์๋ ํ์ธ์ ํ๋งํ์ดํผ์์ ๋ฐฑ์๋ ์์ง๋์ด๋ก ๊ทผ๋ฌดํ๊ณ ์๋ Manggo์ ๋๋ค. ์ฌ๋ฌ๋ถ๋ค์ด ํด๊ฒฐํ๊ณ ์๋ ๋๋ฉ์ธ ๋ฌธ์ ๋ค ์ค ๋ณ๊ฒฝ ์ฌํญ ํ๋ํ๋๊ฐ ์ค์ํ๊ฑฐ๋, ๋ณ๊ฒฝ ์ฌํญ๋ณ๋ก ๋ ํฌํธ๋ฅผ ์ ๊ณตํ๋ค๊ฑฐ๋, ๋ณ๊ฒฝํ ์์ ์ผ๋ก ๋์๊ฐ ๋ฌด์ธ๊ฐ๋ฅผ ํด์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ์์ผ์ค๊น์? ๊ทธ๋ด ๋ ์์ฃผ ์ข์ ํจํด์ด ๋ฐ๋ก ์ด๋ฒคํธ ์์ฑ ์ ๋๋ค. ์ด๋ฒคํธ ์์ฑ์ ๋งํด ํ์ธ๋ฌ๊ฐ ์ด Development of Further Patterns of Enterprise Application Architecture ์์ ์๊ฐ๋์์ต๋๋ค.
๋งํดํ์ธ๋ฌ์ ๊ธ์ ์ ํ์๋ ์ด๋ฒคํธ ์์ฑ์ ์ค๋ช ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied for the same lifetime as the application state itself.
ํ์ด์ ์ค๋ช ํด๋ณด์๋ฉด, ์ผ๋ฐ์ ์ธ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ํ๋ฅผ ์ ์ฅํ๊ณ , ๊ณ์ ๋ณ๊ฒฝํฉ๋๋ค. ์ด๊ฒ๊ณผ ๋ค๋ฅด๊ฒ, ์ด๋ฒคํธ ์์ฑ ํจํด์ ์ฌ์ฉํ๋ ์ ํ๋ฆฌ์ผ์ด์ ์ ํ์ฌ ์ํ๊ฐ ๋ง๋ค์ด์ง๊ธฐ๊น์ง ๋ฐ์ํ ์ด๋ฒคํธ๋ฅผ ์ ์ฅํฉ๋๋ค. ์ด ์ด๋ฒคํธ๋ค์ ์ฐจ๋ก๋๋ก ์ฌ์ํ๋ฉด ํ์ฌ์ ์ํ๋ฅผ ์ ์ ์์ต๋๋ค.
Background
์ํ ๊ธฐ๋ฐ ์์คํ vs ์ด๋ฒคํธ ๊ธฐ๋ฐ ์์คํ
์ผ๋ฐ์ ์ธ ๊ฐ๋ฐ ํ๊ฒฝ์์๋ ๋ฐ์ดํฐ์ ๋ณ๊ฒฝ์ ๊ฐํ ํ ์์์ฅ์น์ ๋ฐ๋ก ๋ฐ์ํ๊ธฐ ๋๋ฌธ์, ์์์ฅ์น์๋ ์ต์ข
์ค๋
์ท๋ง ๋จ์์์ต๋๋ค. ๊ฐ๋จํ ์ฌ์น์ฐ์ฐ์ผ๋ก ์๋ฅผ ๋ค์ด๋ณด์๋ฉด, ์์์ฅ์น์ 11์ด๋ผ๋ ์ซ์๊ฐ ์์๋ 11์ด (0 + 1 + 1 + โฆ + 1)
๋ก ์์ฑ๋์๋์ง, ((0 + 1) * 10 + 1)
๋ก ์์ฑ๋์๋์ง ๋ชจ๋ฅด๊ณ ๊ฒฐ๊ณผ๊ฐ์ธ 11๋ง ์ ์ ์์ต๋๋ค. ์ด๋ ๋ง์ง๋ง ์๋๋ฆฌ์ค์์ *10
์ด ์๋ +10
์ ํ๋ ๋ฒ๊ทธ๋ฅผ ๋ง๋ค์๋ค๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? ์์์ฅ์น์๋ 12๋ผ๋ ๊ฐ์ด ์ ์ฅ๋์ ๊ฒ์ด๊ณ ์ด๋์ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ๋์ง ์ฐ๋ฆฌ๋ ์ถ์ ์ ํ ์ ์์ต๋๋ค. ํด๋น ๋ฒ๊ทธ๋ ์ฐ๋ฆฌ๊ฐ *
์ฐ์ฐ์๋ฅผ ๊ณฑ์ฐ์ฐ์ด ์๋ ํฉ์ฐ์ฐ์ผ๋ก ์๋ชป ๊ฐ๋ฐํ์ ์๋ ์๊ณ , ์ฌ์ฉ์๊ฐ ํฉ์ฐ์ฐ์ผ๋ก ์๋ชป ์
๋ ฅํ์ ์๋ ์์ต๋๋ค.
์ ์์์ ์ด๋ฒคํธ ์์ฑ์ ์ ์ฉ์ ํ๋ฉด ์ด๋ป๊ฒ ๋ ๊น์? *10
์ +10
์ผ๋ก ์ฒ๋ฆฌํ๋ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ๋ค๋ ๊ฐ์ ์๋ *10
์ด๋ผ๋ ์ด๋ฒคํธ๊ฐ ์๊ธฐ ๋๋ฌธ์ *10
์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ๋ ์๋ชป๋ ๋ก์ง๋ง ๋ณ๊ฒฝํด์ฃผ๋ฉด ํด๊ฒฐ์ด ๊ฐ๋ฅํฉ๋๋ค. +10
์ผ๋ก ์๋ชป ์
๋ ฅ๋์์ ๊ฒฝ์ฐ *10
์ด ์๋ +10
์ด๋ฒคํธ๊ฐ ์์ฌ์๊ธฐ ๋๋ฌธ์ ์ฌ์ฉ์ ์
๋ ฅ์ด +10
์ผ๋ก ์๋ชป ๋์์ ๋ฐ๋ก ํ์ธ์ด ๊ฐ๋ฅํฉ๋๋ค.
์์ ๊ฐ์ ์์์ ๋ฒ๊ทธ๋ฅผ ์ถ์ ํ๊ธฐ ์ํด ์ฌ์ฉ์ ์ ๋ ฅ์ ์์๋๋ก ๋ก๊ทธ๋ก ์์์ ํด๊ฒฐ ํ ์๋ ์์ต๋๋ค. ๋ํ ๋ก๊ทธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๋ก์ง์ ๋ฌธ์ ์ ์ด ์๋์ง ํน์ ์ ๋ ฅ์ด ์๋ชป๋์๋์ง ์ฐพ์์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐ ํ ์๋ ์์ต๋๋ค. ๊ฐ๋จํด ๋ณด์ด๋ ๊ณผ์ ์ด์ง๋ง, ๋ก๊ทธ์ ๋๋ฉ์ธ ๋ฐ์ดํฐ๊ฐ ๋ฌด๊ฒฐ์ฑ์ ๋ง์ถ๊ธฐ ์ํ ์์ , ํธ๋์ญ์ ์ ๋ณด์ฅํ๊ธฐ ์ํ ์์ , ๋ก๊ทธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ ๋ ฅ์ ๋ค์ ํด๋ณด๊ณ ๋ก์ง์ ์์ ํ๋ ์์ , ์์ ๋ ๋ก์ง์ ๊ธฐ๋ฐ์ผ๋ก ์์์ฅ์น์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๋ ์์ ๋ฑ ๋งค์ฐ ๊น๋ค๋กญ๊ณ ํ๋ ๊ณผ์ ์ด ๋ ๊ฒ์ผ๋ก ์์๋ฉ๋๋ค.
Why ์ด๋ฒคํธ ์์ฑ?
๊ณ ๊ฐ์ฌ์ธ ๋ฏธ์ฉ์๋ฃ ๋ณ์์์ ํ์๊ฐ ๋ด์์ ํ๊ฒ ๋๋ฉด ์ ๊ธฐ์ ์ด๊ณ ๋ค์ํ ๊ณผ์ ๋ค์ ๊ฑฐ์ณ์ ์์ ์ ๋ฐ๊ณ ๊ท๊ฐ๋ฅผ ํ๊ฒ ๋ฉ๋๋ค.
ํ์๊ฐ ๋ด์์ ํด์ ํค์ค์คํฌ๋ก ์ ์๋ฅผ ํ๊ฑฐ๋ ๋ฐ์คํฌ ์ง์์๊ฒ ์ด์ผ๊ธฐํ์ฌ ์ ์๋ฅผ ํ๊ณ ์์ ์๋ด์ ๋ฐ๊ฑฐ๋ ๋ฐ๋ก ์์ ์ ๋ฐ๋ ์ค๋น๋ฅผ ํ๊ฑฐ๋, ์์ ์ ์ฌ๋ฌ๊ฐ ๋ฐ๋๋ค๋ฉด ์ด๋ค๊ฒ๋ถํฐ ๋ฐ์ ์ค๋น๋ฅผ ํ๊ณ ์ด๋ค ์์ฅ๋์๊ฒ ์์ ๋ค์ ๋ฐ์๊ฑด์ง, ์์ ํ ์ฌ์ง์ ์ดฌ์ํด์ผ ํ๋์ง ์ง์ ๊ด๋ฆฌ๋ฅผ ๋ฐ์์ผ ํ๋์ง ๋ฑ ์์๊ฐ ์ ํด์ ธ ์๋ ๊ฒ๋ค๋ ์์ง๋ง ๊ทธ๋๊ทธ๋ ์ํฉ์ ๋ฐ๋ผ์ ์ ๊ธฐ์ ์ผ๋ก ํ์์ ๋ด์ ํ๋ฆ์ด ๋ฌ๋ผ์ง๊ฒ ๋ฉ๋๋ค.
์ด๋ฌํ ๊ณผ์ ์์์ ๋ณ์์์๋ ์ด๋ค ์ง์์ด ์ด๋ค ํ์์๊ฒ ์ด๋ค ๊ณผ์ ๋ค์ ๊ฑฐ์น๊ฒ ํ๋์ง ์ถ์ ํ๊ณ ์ถ์ ๋์ฆ๊ฐ ์์ผ๋ฉฐ ์๋น์ค๋ก ์ ๊ณต์ ๋ฐ๊ณ ์ถ์ด ํฉ๋๋ค. ๋ํ ๋ฐ์ ๋ณ์์์์ ํ์์๊ฒ ์ ๊ณต๋๋ ๊ฒฝํ๋ค์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ ๊ฒ์ ์ต๋ํ ์ค์ด๊ณ ๋ฌธ์ ๊ฐ ๋ฐ์ํ์๋ค๋ฉด ์ ํํ๊ณ ๋น ๋ฅด๊ฒ ๋์์ ํ๊ณ ์ถ์ ๋์ฆ๊ฐ ์์ต๋๋ค. ์ด๋ฐ ์๊ตฌ์ฌํญ๋ค์ ๋ง์กฑ์ํค๊ธฐ ์ํด์ ํ๋งํ์ดํผ B2B SaaSํ์์๋ ๊ฒฐ์ , ์์ฝ, ๋ณ์ ๋ด์ ํ๋ก์ธ์ค ๋ฑ ์ด๋ง๋ก๊ทธ(audit log)๋ฅผ ์ ๊ณตํด์ผ ํ๋ฉฐ ๊ณผ๊ฑฐ์ ์ฌ์ค์ ๊ธฐ๋ฐํ ์ฐ์ฐ์ด ์ค์ํ ๋๋ฉ์ธ๋ค์ ์ด๋ฒคํธ ์์ฑ์ ์ ์ฉํ๊ฒ ๋์์ต๋๋ค. ์ด์ ๊ฐ์ ๊ฒฝํ์ ๋ฐํ์ผ๋ก ์ด๋ป๊ฒ ์ด๋ฒคํธ ์์ฑ์ ์ ์ฉํ๊ณ ๊ตฌํํ๋์ง ์๊ฐ๋ฅผ ํ๊ณ ์ ํฉ๋๋ค.
์ด๋ฒคํธ ์์ฑ ๊ตฌํ
ํ๋ก์ฐ ์์ฝ
์ด๋ฒคํธ ์คํธ๋ฆผ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ ์ฉ์์ผ์ ์ค์ ๋ก ์ฌ์ฉํ๋ ํ๋ก์ฐ๋ฅผ ์ฐจํธ๋ก ์์ฝํด๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- Client๋ก๋ถํฐ ๋ช ๋ น API๋ฅผ ์์ฒญ๋ฐ์ต๋๋ค.
- Controller๊ฐ ์์ฒญ์ StreamCommand๋ฅผ ํฌํจํ Message๋ก ๋ณํํ์ฌ Headspring์ผ๋ก ์ ๋ฌํฉ๋๋ค.
- Headspring์ StreamCommand์ ๋ค์ด์๋ streamId๋ฅผ ๊ธฐ๋ฐ์ผ๋ก EventStore๋ก๋ถํฐ ์ด๋ฒคํธ๋ค์ ์กฐํํฉ๋๋ค.
- Headspring์ ์กฐํ๋ ์ด๋ฒคํธ๋ค๊ณผ EventHandler๋ค์ ์ด์ฉํ์ฌ ๋ณต์(rehydrate)๊ณผ์ ์ ๊ฑฐ์ณ ๊ฐ์ฅ ์ต์ State๋ฅผ ๋ง๋ญ๋๋ค.
- ๋ง๋ค์ด์ง State์ ์์ฒญ์ผ๋ก๋ถํฐ ๋ฐ์ Command๋ฅผ CommandExecutor์ ์ ๋ฌํ์ฌ ์ด๋ฒคํธ๋ฅผ ์์ฑํฉ๋๋ค
- ์์ฑ๋ ์ด๋ฒคํธ๋ฅผ EventStore์ ์ ์ฅํฉ๋๋ค.
- EventStore๋ ์ด๋ฒคํธ๋ฅผ ์ ์ฅํ๊ณ ํด๋น ์ด๋ฒคํธ๋ฅผ MessageBus๋ก ๋ณด๋ด ์ด๋ฒคํธ๋ฅผ ์ธ๋ถ์ ๋ฐํํฉ๋๋ค.
์ด๋ฒคํธ ์์ฑ์ ๊ตฌํํ๊ธฐ ์ํด์๋ ์ด๊ธฐ ์ํ ํฉํ ๋ฆฌ(state seed factory), ์ด๋ฒคํธ ์ ์ฅ์(event store), ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ธฐ(event handler), ์ด๋ฒคํธ ๋ฐํ๊ธฐ(command executor), ์ํ ๋ณต์๊ธฐ(rehydrator)๊ฐ ๊ธฐ๋ณธ์ ์ผ๋ก ํ์ํฉ๋๋ค. ์ฌ์ ์ง์์ ๋งํฌํด๋ Loom ์ ์ฅ์์ ์ด ๋ชจ๋ ์์๋ค์ ์ธํฐํ์ด์ค์ ๊ตฌํ์ฒด๊ฐ ์ ์ ๋ฆฌ๋์ด ์์ต๋๋ค.
์ด์ ์ ํฌ ํ์์ ๊ตฌํํ ์์ฝ ์์คํ ์ ์์๋ก ๊ฐ ์์๋ค์ ์ค๋ช ํ๊ณ ์ ํฉ๋๋ค. ์์ฝ ์์คํ ์ ํ์์ ์์ฝ, ๋ณ์ ์ ๋ฌด ํ๋ก์ธ์ค ๋ฑ์ ๊ด๋ฆฌํ๋ ๋๋ฉ์ธ์ ๋๋ค. ์ด ๋๋ฉ์ธ์ ์ด๋ฆ์ Schedule์ด๋ผ๊ณ ์ง์๊ณ , State๋ก ์ ์ํ์์ต๋๋ค. ๊ฐ์ํ๋ Schedule์ ๊ธฐ๋ฐ์ผ๋ก ๊ฐ ์ปดํฌ๋ํธ๋ค์ ๊ตฌํ์ ์๊ฐํ๊ฒ ์ต๋๋ค.
State
Schedule ์ํ ์ ๋๋ค. ์ํ์ ์์ฑ๋ค๊ณผ ์์ฑ์, ๊ทธ๋ฆฌ๊ณ ์ด๊ธฐ ์ํ ํฉํ ๋ฆฌ๊ฐ ํฌํจ๋์ด ์์ต๋๋ค. setter ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ง ์๊ณ copy ๋ฐฉ์์ ์ด์ฉํ๊ธฐ ์ํด์ Lombok์ Builder ๋ฐ toBuilder ์ด๋ ธํ ์ด์ ์ ์ด์ฉํ์์ต๋๋ค.
public enum ReservationStatus {
RESERVED,
CANCELED
}
@Getter
@NoArgsConstructor
public class Reservation {
private OffsetDateTime startDateTime;
private OffsetDateTime endDateTime;
private ReservationStatus status;
@Builder(toBuilder = true)
public Reservation(
OffsetDateTime startDateTime,
OffsetDateTime endDateTime,
ReservationStatus status
) {
this.startDateTime = startDateTime;
this.endDateTime = endDateTime;
this.status = status;
}
}
@Getter
public class Schedule {
private String id;
private String visitorId;
private Reservation reservation;
private List<Object> events;
@Builder(toBuilder = true)
public Schedule(
String id,
String visitorId,
Reservation reservation,
List<Object> events
) {
this.id = id;
this.visitorId = visitorId;
this.reservation = reservation;
this.events = events;
}
public static Schedule seedFactory(String id) {
return Schedule.builder()
.id(id)
.reservation(new Reservation())
.build();
}
}
Event
์ด๋ฒคํธ ์์ฑ์ ์ํ์ ๋ณ๊ฒฝ์ฌํญ๋ค์ ์ด๋ฒคํธ๋ก ๊ด๋ฆฌํ๊ธฐ ๋๋ฌธ์ ๋ณ๊ฒฝ๋๋ ๋ฐ์ดํฐ๋ค์ ์ด๋ฒคํธ์ ๋ด์์ผํฉ๋๋ค.
Schedule์์ ๋ฐํ๋๋ ๋๋ฉ์ธ ์ด๋ฒคํธ๋ค์ ๋งค์ฐ ๋ง์ง๋ง ๊ทธ์ค์์ Reserved
, ReservationCanceled
๋ ์ด๋ฒคํธ๋ค์ ์ด์ฉํ์ฌ ๊ฐ๋จํ๊ฒ ์๊ฐํ๋๋ก ํ๊ฒ ์ต๋๋ค.
public record Reserved(
String visitorId,
OffsetDateTime startDateTime,
OffsetDateTime endDateTime
) {
}
// ๋ ๋ง์ ์์ฑ๋ค์ด ์์ง๋ง ์ด๋ฒ ๊ธ์์๋ ์๋ตํฉ๋๋ค
public record ReservationCanceled() {
}
Command
์ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๋ ๋ช ๋ น๋ค ์ ๋๋ค
public record Reserve(
String visitorId,
OffsetDateTime startDateTime,
OffsetDateTime endDateTime
){
}
public record CancelReservation() {
}
CommandExecutor
๋ช
๋ น์ ๋ฐ์์ ์ด๋ฒคํธ๋ฅผ ์์ฑํ๋ ๋ช
๋ น ์คํ๊ธฐ ์
๋๋ค. CommandExecutor
์ธํฐํ์ด์ค๋ loom-java์ ์ ์๋์ด ์์ต๋๋ค. ReserveCommand
๋ฅผ ์ฒ๋ฆฌํ ๋ ์ค๋ณต์์ฝ์ ๋ฐฉ์งํ๋ค๋ ๋๋ฉ์ธ ๋
ผ๋ฆฌ๊ฐ ์กด์ฌํ๋ค๊ณ ๊ฐ์ ํด๋ณด๊ฒ ์ต๋๋ค.
public class ReserveCommandExecutor implements CommandExecutor<Schedule, Reserve> {
@Override
public Iterable<Object> produceEvents(Schedule state, Reserve command) {
if (!state.getEvents().isEmpty()) {
throw new RuntimeException("Already reserved");
}
return List.of(
new Reserved(
command.visitorId(),
command.startDateTime(),
command.endDatetTime()
)
);
}
}
๋๋ฉ์ธ ๋ช ๋ น์ ๋ช ๋ น์ ์ ์์ ์ผ๋ก ์คํ์ํฌ ์ ์๋์ง์ ๋ํด์ ๊ฒ์ฆ์ด ํ์ํ๋ฉฐ ํน์ ์ผ์ด์ค์ ๋ํด์ ์คํจ๋ฅผ ํด์ผ ํฉ๋๋ค. ๊ทธ๋ ๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ๊ธฐ ์ State์ ์ด๋ฏธ ์ด๋ฒคํธ๊ฐ ์๋์ง ๊ฒ์ฆํ ํ ์๋ค๋ฉด ์์ธ๋ฅผ ๋ฐ์์ํค๊ณ ๊ทธ๋ ์ง ์๋ค๋ฉด ์ด๋ฒคํธ๋ฅผ ์๋ตํฉ๋๋ค.
EventHandler
๋ฐํ๋ ์ด๋ฒคํธ๋ฅผ ์ฒ๋ฆฌํ์ฌ ์ํ๋ฅผ ๋ณต์์ํค๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ธฐ ์
๋๋ค. ์ด๋ฒคํธ๋ ๊ณผ๊ฑฐ์ ๋ฐ์๋ ์ฌ์ค์ด๋ฉฐ ์ด๋ฏธ ๋ฐ์์์ ์ ๊ฒ์ฆ์ด ๋์๊ธฐ ๋๋ฌธ์ ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ธฐ๋ ์ด๋ฒคํธ์ ๋ํด์ ๋ฐ๋ก ๊ฒ์ฆํ์ง ์์ผ๋ฉฐ ํญ์ ์ฑ๊ณตํด์ผ ํฉ๋๋ค. EventHandler
์ธํฐํ์ด์ค๋ loom-java์ ์ ์๋์ด ์์ต๋๋ค
public class ReservedEventHandler implements EventHandler<Schedule, Reserved> {
@Override
public Schedule handleEvent(Schedule state, Reserved event) {
return state.toBuilder()
.reservation(
state.getReservation().toBuilder()
.startDateTime(event.startDateTime())
.endDateTime(event.endDateTime())
.status(ReservationStatus.RESERVED)
.build()
)
.visitorId(event.visitorId())
.build();
}
}
EventStore
์ด๋ฒคํธ๋ฅผ ์ ์ฅํ๊ณ ์กฐํํ๋ ์ ์ฅ์ ์
๋๋ค. ์ด๋ฒคํธ๋ฅผ ์ ์ฅํ๋ collectEvents
์ ์กฐํํ๋ queryEvents
๋ฉ์๋๋ฅผ ์ ์ํ๊ณ ์์ผ๋ฉฐ, ์์ธํ ์ธํฐํ์ด์ค๋ loom-java์ ์ ์๋์ด ์์ต๋๋ค.
public class ScheduleEventStore implements EventStore {
/* ... ์๋ต ... */
private final ScheduleEventRepository scheduleEventRepository;
private final MessageBus messageBus;
/* ... ์๋ต ... */
public ScheduleEventStore(
/* ... ์๋ต ... */
ScheduleEventRepository scheduleEventRepository,
MessageBus messageBus
/* ... ์๋ต ... */
) {
/* ... ์๋ต ... */
this.scheduleEventRepository = scheduleEventRepository;
this.messageBus = messageBus;
/* ... ์๋ต ... */
}
@Override
public void collectEvents(
String tenantId,
String processId,
String initiator,
String predecessorId,
String streamId,
long startVersion,
Iterable<Object> events
) {
/* ... ์๋ต ... */
scheduleEventRepository.saveAll(
tenantId,
processId,
initiator,
predecessorId,
streamId,
startVersion,
eventList,
typeStrategy
);
List<ScheduleEventDataModel> pendingEvents = scheduleEventRepository.getPendingEvents(
tenantId,
streamId
);
sendMessages(tenantId, streamId, pendingEvents);
scheduleEventRepository.makeEventsPublished(tenantId, pendingEvents);
}
/* ... ์๋ต ... */
}
์ด๋ฒคํธ ์คํ ์ด ๋ ์ฝ๋ ๋ณ๋ก ๋ฉ์ธ์ง๊ฐ ๋ฐํ๋์๋์ง ์๋์๋์ง ์ฒดํฌ๋ฅผ ํ๊ณ , ๋ฐํ์ด ๋์ง ์์ ๋ชจ๋ ์ด๋ฒคํธ๋ฅผ ๋ฉ์ธ์ง ๋ธ๋ก์ปค๋ก ์ ๋ฌํ๊ฒ ๋ฉ๋๋ค. ์ด๋ฅผ ํตํด์ ์ด๋ฒคํธ๊ฐ At Least Once ๋ฐํ์ด ๋ ์ ์๋๋ก ํฉ๋๋ค.
MessageBus
๋ Apache Kafka, Kinesis, AWS SNS + SQS, Azure EventHub ๋ฑ ๋ค์ํ ๋ฉ์ธ์ง ๋ธ๋ก์ปค์ค ์์๋ฅผ ๋ณด์ฅํ ์ ์๋ ๋ฉ์ธ์ง ๋ธ๋ก์ปค๋ฅผ ์ด์ฉํ์ฌ ๊ตฌํํฉ๋๋ค. ์ ํฌ๋ Kinesis๋ฅผ ํตํด์ ๊ตฌํํ์์ต๋๋ค.
Event Stream
์ด๋ฒคํธ ์คํธ๋ฆผ์์๋ ์ฒซ๋ฒ์งธ ์ด๋ฒคํธ ๋ถํฐ ๊ฐ์ฅ ์ต์ ์ ์ด๋ฒคํธ๋ฅผ ๊ฐ์ง๊ณ State๋ก ๋ณต์์ํฌ ์ ์์ด์ผ ํ๊ณ , ๋ณต์๋ ์ํ์์ ๋ช
๋ น์ ๋ฐ์ ๋ค์ ์ด๋ฒคํธ๋ฅผ ๋ฐํํ ์ ์์ด์ผ ํฉ๋๋ค. loom-java์์๋ Headspring
์ด๋ผ๋ abstract class๋ฅผ ํตํด ๋ ๊ฐ์ง ๊ธฐ๋ฅ์ด ๊ตฌํ๋์ด ์์ต๋๋ค.
Rehydrator
๋ผ๋ ๋ณต์๊ธฐ๋ฅผ ์์๋ฐ์ผ๋ฉฐ MessageHandler
๋ฅผ ๊ตฌํํ๋๋ก ๋์ด ์์ต๋๋ค.
public abstract class Headspring<S> extends Rehydrator<S>
implements MessageHandler
๋ช
๋ น ๋ฉ์ธ์ง๋ฅผ ์ฒ๋ฆฌํ๋ ๊ตฌํ์ ๋ณด๋ฉด, 1. ์ต์ ๋ฒ์ ์ ์ํ๋ฅผ ๋ณต์์ํค๊ณ 2. ๋ช
๋ น ์คํ๊ธฐ๋ฅผ ์คํ์์ผ ์ด๋ฒคํธ๋ฅผ ์์ฑํ๊ณ 3. ์์ฑ๋ ์ด๋ฒคํธ๋ฅผ ์ด๋ฒคํธ ์คํ ์ด์ ์ ์ฅํฉ๋๋ค. rehydrateState
๋ ์ฒซ๋ฒ์งธ ์ด๋ฒคํธ๋ถํฐ ์์ฐจ์ ์ผ๋ก ์ ์ฉํ์ฌ State๋ก ๋ณํํด๋๊ฐ๋๋ค.
@Override
public void handle(Message message) {
StreamCommand<?> command = (StreamCommand<?>) (message.getData());
Snapshot<S> snapshot = rehydrateState(command.getStreamId());
String predecessorId = message.getId();
eventStore.collectEvents(
getStateType(),
message.getProcessId(),
message.getInitiator(),
predecessorId,
command.getStreamId(),
snapshot.getVersion() + <span class="hljs-number">1,
produceEvents(snapshot.getState(), command));
}
public final Snapshot<S> rehydrateState(String streamId) {
return foldl(
this::handleEvent,
Snapshot.seed(streamId, seedFactory.apply(streamId)),
stream(eventReader.queryEvents(stateType, streamId, <span class="hljs-number">1)));
}
์ด๋ฒคํธ ์คํธ๋ฆผ์์ ์ฒ๋ฆฌํ ์ ์๋ ์ด๋ฒคํธ ์ฒ๋ฆฌ๊ธฐ๋ค๊ณผ ๋ช ๋ น ์คํ๊ธฐ๋ค์ ์ฃผ์ ๋ฐ์์ ์คํธ๋ฆผ์ ์์ฑ์์ผ์ค๋๋ค.
public class ScheduleHeadspring extends Headspring<Schedule> {
public ScheduleHeadspring(
EventStore eventStore
) {
super(
eventStore,
Schedule::seedFactory,
List.of(
new ReserveCommandExecutor(),
new CancelReservationCommandExecutor(),
/* ... ์๋ต ... */
),
List.of(
new ReservedEventHandler(),
new ReservationCanceledEventHandler(),
/* ... ์๋ต ... */
)
);
}
}
์ด๋ฒคํธ ์์ฑ ์ ์ฉ ํ
์ด๋ฒคํธ ์์ฑ์ ์ ์ฉํ๋ฉด์ ์ ํฌ๊ฐ ๊ฐ์ฅ ํฌ๊ฒ ์ฒด๊ฐํ ์ฅ์ ๊ณผ ํด๊ฒฐํด์ผ ํ๋ ์ฑ๋ฆฐ์ง์ ์๊ฐํ๊ณ ์ ํฉ๋๋ค.
์ฅ์
DDD(Domain Driven Design)๋ฅผ ์ ์ฉํ๊ธฐ ์์ํ๋ค.
- DDD์ ์๊ฐ๋์ด ์๋ ์ ๊ทธ๋ฆฌ๊ฒ(Aggregate)ํจํด์ ์ ์ฉํ๊ธฐ ๋๋ฌด ์ข์์ต๋๋ค. ์ด๋ฒคํธ ์คํธ๋ฆผ์ ํ๋์ ์ ๊ทธ๋ฆฌ๊ฒ์ผ๋ก ์ ์ํ๋ฉด DDD์์ ์ด์ผ๊ธฐ ํ๋ ์ ๊ทธ๋ฆฌ๊ฒ ํจํด์ ๋ง์กฑํ๊ฒ ๋ฉ๋๋ค.
- ๋๋ฉ์ธ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ ์นํ๋ ์์ํฌ, ORM๋ฑ ์ค์ ๋๋ฉ์ธ์ ์๋ ์ปจํ ์คํธ๋ค์ ์ฃผ์ ํ์ง ์๊ณ ๊ฐ๋ฐํ๊ธฐ ์์ํฉ๋๋ค. ์์ ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ฅผ ์ด์ฉํ์ฌ ์ด๋ฒคํธ๋ฅผ ์์ฑํ๊ณ ์ํ๋ฅผ ๋ณต์ํ๋ ๋ก์ง๋ง ์์ฑํ๋ฉด ๋๊ธฐ ๋๋ฌธ์ ์ค์ ๋๋ฉ์ธ์ ์จ๋ํํฐ๋ก ์ค์ผ์ํค์ง ์๊ณ ํ๋ก๊ทธ๋จ์ผ๋ก ์์ฑ์ด ๊ฐ๋ฅํฉ๋๋ค. ์ฆ ๋๋ฉ์ธ ๊ด์ฌ์ฌ๊ฐ ์๋ ๊ธฐ์ ๋ค์ ์์กดํ์ง ์๊ณ ๋๋ฉ์ธ ๋ก์ง์ ์์ฑํ๊ธฐ ์์ํฉ๋๋ค.
- ๋น๊ต์ ๋ณต์กํ ๋๋ฉ์ธ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์ ํ์์ ์ข ์ข EventStorming์ ์ด์ฉํ๋๋ฐ, ์ด๋ฒคํธ ์คํ ๋ฐ์ ํตํด ํ์์ ์ ์ํ ๋ช ๋ น ๋ฐ ์ด๋ฒคํธ๋ค์ ๊ธฐ๋ฐ์ผ๋ก ๊ฐ๋ฐ์ ํ๋ฉด ๋๊ธฐ ๋๋ฌธ์ ์ค์ ๋๋ฉ์ธ์ ํ๋ก๊ทธ๋จ์ผ๋ก ๋ฐ์ํ๊ธฐ ์์ํฉ๋๋ค.
์ฑ๋ฆฐ์ง
์ด๋ฒคํธ ์์ฑ์ ์ด์ฉํ์ฌ ์์์ ์๊ฐํ ๊ฒ ๋ฟ๋ง ์๋๋ผ ๋ค๋ฅธ ์ฅ์ ๋ค์ ์ทจํ๊ธด ํ์ง๋ง ํด๊ฒฐํด์ผ ํ๋ ์ฑ๋ฆฐ์ง๋ํ ์กด์ฌํ์ต๋๋ค. ์กฐํ๋ฅผ ํ ๋ ์ฑ๋ฅ์์ ํฐ ๋ถํ๊ฐ ๋ฐ์ํ ์ ์๋ค๋ ์ฌ์ค์ธ๋ฐ์, ์คํธ๋ฆผ ํ๋์ ๋ํด์ ์กฐํ๋ฅผ ํ๋ค๋ฉด ํด๋น ์คํธ๋ฆผ์ ์ด๋ฒคํธ๊ฐ ๋ณ๋ก ์์๋ ํฐ ๋ถํ๊ฐ ๊ฐ์ง๋ ์๊ฒ ์ง๋ง ์ฌ๋ฌ ์คํธ๋ฆผ์ ๋ํ ์กฐํ ์กฐ๊ฑด์ ๋ง์ถฐ์ผ ํ ๋ ํน์ ํ๋์ ์คํธ๋ฆผ์ ์ด๋ฒคํธ๊ฐ ๋ง์๋ ์ค๋ ์ท ํน์ ์กฐํ๋ชจ๋ธ์ด ํ์ํ ์ ์์ต๋๋ค.
์๋ก ๋ค์๋ Schedule ๋๋ฉ์ธ์ ๊ฒฝ์ฐ, A์๊ฐ๋ถํฐ B์๊ฐ๊น์ง ์กํ์๋ ์ค์ผ์ค์ ์กฐํํด์ผ ํ๋ ์ ์ฆ์ผ์ด์ค๊ฐ ์กด์ฌํฉ๋๋ค. ์ผ๋ฐ์ ์ธ ๊ฐ๋ฐ ๋ฐฉ์์์๋ State์ ์์ฑ์ ๊ธฐ๋ฐ์ผ๋ก DB์ ์ฟผ๋ฆฌ๋ฅผ ์์ฑํ์ฌ ์คํํ ์ ์์ง๋ง, ์ด๋ฒคํธ ์์ฑ์ ์ ์ฉํ์ ๋๋ State๊ฐ DB์ ์ ์ฅ๋์ง ์๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ์คํธ๋ฆผ ๋ณต์๊ณผ์ ์ ๊ฑฐ์ณ ์ํ๋ฅผ ๋ง๋ ํ ํํฐ๋ง์ ํด์ผํฉ๋๋ค. ์ด ๊ณผ์ ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋งค์ฐ ํฐ ๋ถํ๋ฅผ ์ค ์ ์์ต๋๋ค.
์ด๋ฐ ๋ฌธ์ ๋ฅผ ๊ทน๋ณตํ๊ธฐ ์ํด์ ์ ํฌ๋ CQRSํจํด์ ์ ์ฉํ์ฌ ํด๊ฒฐํ์์ต๋๋ค. ๋ช ๋ น๋ชจ๋ธ์ ์ด๋ฒคํธ ์์ฑ์ ํตํด ๊ตฌํํ๊ณ , ์กฐํ๋ชจ๋ธ์ ๋ฐ๋ก ๋์ด ์กฐํ ์ ์ฆ์ผ์ด์ค์ ๋ํ ์ฑ๋ฆฐ์ง์ ๊ทน๋ณตํ ์ ์์ต๋๋ค. ์ด์ ๋ํ ์๊ฐ๋ ๋ค์ ๊ธ์์ ์์ฑํ๋๋ก ํ๊ฒ ์ต๋๋ค.