Skip to content

Latest commit

ย 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

msa โ€” CQRS + Event Sourcing ์ฃผ๋ฌธ ์„œ๋น„์Šค

์ฃผ๋ฌธ ์„œ๋น„์Šค์˜ CQRS ๊ฒฝ๋กœ๋งŒ ๋‹จ์ผ ๋ชจ๋“ˆ๋กœ ๊ตฌ์„ฑํ•œ ํ•™์Šต ํ”„๋กœ์ ํŠธ๋‹ค. ์ฃผ๋ฌธ์˜ ์ƒํƒœ๋ฅผ ํ…Œ์ด๋ธ”์— ์ €์žฅํ•˜์ง€ ์•Š๊ณ , ์ผ์–ด๋‚œ ์ผ (์ด๋ฒคํŠธ)๋งŒ ์Œ“์€ ๋’ค ์ฝ์„ ๋•Œ ๋‹ค์‹œ ์žฌ์ƒ (replay) ํ•œ๋‹ค.

์•„ํ‚คํ…์ฒ˜


1. ํ•œ๋ˆˆ์— ๋ณด๊ธฐ

Command (์“ฐ๊ธฐ) Query (์ฝ๊ธฐ)
์ง„์ž…์  OrderCommandController โ€” POST PATCH OrderQueryController โ€” GET
์„œ๋น„์Šค EventCommandService EventQueryService
ํ•˜๋Š” ์ผ ๋ถˆ๋ณ€์‹ ๊ฒ€์‚ฌ โ†’ ์ด๋ฒคํŠธ append โ†’ ๋ฐœํ–‰ ์ด๋ฒคํŠธ replay โ†’ OrderDto ๋ณต์›
์ €์žฅ์†Œ ์ ‘๊ทผ save() + ๊ฒ€์ฆ์šฉ ์ŠคํŠธ๋ฆผ ๋กœ๋“œ SELECT๋งŒ
๊ฒฐ๊ณผ 201/200 ๋นˆ ๋ฐ”๋”” OrderDto / List<OrderDto>

๋‘ ๊ฒฝ๋กœ๋Š” ์ปจํŠธ๋กค๋Ÿฌยท์„œ๋น„์Šค ์ˆ˜์ค€์—์„œ ์™„์ „ํžˆ ๊ฐˆ๋ผ์ ธ ์žˆ๊ณ , order_event ํ…Œ์ด๋ธ” ํ•˜๋‚˜๋กœ๋งŒ ๋งŒ๋‚œ๋‹ค.


2. ์™œ ์ด๋ ‡๊ฒŒ ๋‚˜๋ˆด๋‚˜ โ€” CQRS

CQRS (Command Query Responsibility Segregation)๋Š” "์ƒํƒœ๋ฅผ ๋ฐ”๊พธ๋Š” ์š”์ฒญ"๊ณผ "์ƒํƒœ๋ฅผ ์ฝ๋Š” ์š”์ฒญ"์„ ์„œ๋กœ ๋‹ค๋ฅธ ๋ชจ๋ธยท๊ฒฝ๋กœ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. ์ด ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ ์šฉํ–ˆ๋‹ค.

  • Command๋Š” ์กฐํšŒ ์‘๋‹ต์„ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค. POST/PATCH๋Š” ์ด๋ฒคํŠธ๋ฅผ ์Œ“๊ณ  ์ƒํƒœ ์ฝ”๋“œ๋งŒ ๋Œ๋ ค์ค€๋‹ค. ๋ฐ”๋€ ๊ฒฐ๊ณผ๊ฐ€ ๊ถ๊ธˆํ•˜๋ฉด GET์œผ๋กœ ๋‹ค์‹œ ๋ฌป๋Š”๋‹ค.
  • Query๋Š” ์ €์žฅํ•˜์ง€ ์•Š๋Š”๋‹ค. EventQueryService์—๋Š” save()๊ฐ€ ์—†๋‹ค. ์ฝ๊ธฐ ๋ชจ๋ธ (OrderDto)์€ ์š”์ฒญ๋งˆ๋‹ค ์ด๋ฒคํŠธ์—์„œ ๋ณต์›๋˜๋Š” ์ผํšŒ์„ฑ ๊ฒฐ๊ณผ๋ฌผ์ด๋ฉฐ ์–ด๋””์—๋„ ์˜์†ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • Command๊ฐ€ ์ด๋ฒคํŠธ๋ฅผ ์ฝ๋Š” ๊ฒƒ์€ Query๊ฐ€ ์•„๋‹ˆ๋‹ค. EventCommandService.updateUser()๋Š” ์ด๋ฒคํŠธ๋ฅผ ์Œ“๊ธฐ ์ „์— ํ•ด๋‹น ์ฃผ๋ฌธ์˜ ์ŠคํŠธ๋ฆผ์„ ๋กœ๋“œํ•ด "์ฃผ๋ฌธ์ด ์กด์žฌํ•˜๋Š”๊ฐ€", "์š”์ฒญ์ž๊ฐ€ ์†Œ์œ ์ž์ธ๊ฐ€"๋ฅผ ๊ฒ€์‚ฌํ•œ๋‹ค. ์ด๋Š” ๋ถˆ๋ณ€์‹ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•œ ์• ๊ทธ๋ฆฌ๊ฑฐํŠธ ๋กœ๋”ฉ์ด๊ณ , ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋Œ๋ ค์ค„ ์กฐํšŒ ์‘๋‹ต์„ ๋งŒ๋“œ๋Š” ๊ฒŒ ์•„๋‹ˆ๋‹ค. ์ด ๊ตฌ๋ถ„์„ ๋†“์น˜๋ฉด ์ด ํ˜ธ์ถœ์„ "CQRS ์œ„๋ฐ˜"์œผ๋กœ ์˜คํ•ดํ•˜๊ณ  ๋˜๋Œ๋ฆฌ๊ธฐ ์‰ฝ๋‹ค.

3. ์ƒํƒœ์˜ ์›๋ณธ โ€” Event Sourcing

order_event ํ…Œ์ด๋ธ”์ด ์œ ์ผํ•œ ์ง„์‹ค์ด๋‹ค

eventId | orderId | userId | eventType     | payload (JSON)                                   | timestamp
--------+---------+--------+---------------+--------------------------------------------------+----------
1       | a1      | u1     | EVENT_CREATED | {"productId":"P1","qty":2,"unitPrice":1000,"totalPrice":2000} | โ€ฆ
2       | a1      | u1     | EVENT_UPDATED | {"productId":"P1","qty":5,"unitPrice":1000,"totalPrice":5000} | โ€ฆ
3       | b2      | u1     | EVENT_CREATED | {โ€ฆ}                                              | โ€ฆ
  • append-only. ํ–‰์„ update/deleteํ•˜๋Š” ์ฝ”๋“œ๊ฐ€ ์—†๋‹ค. ์ƒํƒœ ๋ณ€๊ฒฝ์€ ํ•ญ์ƒ ์ƒˆ ์ด๋ฒคํŠธ๋‹ค.
  • userIdยทorderId๋Š” ์ปฌ๋Ÿผ, ์ฃผ๋ฌธ ๋‚ด์šฉ์€ payload (JSON, Gson) ์— ๋“ค์–ด๊ฐ„๋‹ค.
  • ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋‹ด๋Š” orders ํ…Œ์ด๋ธ”์€ ์˜๋„์ ์œผ๋กœ ๋‘์ง€ ์•Š์•˜๋‹ค. ์ƒํƒœ๋ฅผ ์บ์‹œํ•˜๋Š” ํ…Œ์ด๋ธ”์ด ์—†์œผ๋ฏ€๋กœ, ์ฝ๊ธฐ๋Š” ๋ฐ˜๋“œ์‹œ replay๋ฅผ ๊ฑฐ์นœ๋‹ค.

์“ฐ๊ธฐ โ€” ์ด๋ฒคํŠธ๋ฅผ ์Œ“๊ธฐ๊นŒ์ง€

RequestOrder โ”€โ”€@Validโ”€โ”€โ–ถ OrderDto โ”€โ”€โ–ถ EventCommandService
                                        โ”œโ”€ (PATCH) ์ŠคํŠธ๋ฆผ ๋กœ๋“œ โ†’ ์—†์œผ๋ฉด 404 / ์†Œ์œ ์ž ๋‹ค๋ฅด๋ฉด 403
                                        โ”œโ”€ OrderEvent ์ƒ์„ฑ, payload = Gson(EventPayload)
                                        โ”œโ”€ save()            โ† append
                                        โ””โ”€ publishEvent()    โ† ์ปค๋ฐ‹ ๋’ค OrderEventListener ์‹คํ–‰
  • createOrder/updateUser๋Š” @Transactional. save์™€ publishEvent๊ฐ€ ํ•œ ํŠธ๋žœ์žญ์…˜์— ๋ฌถ์ธ๋‹ค.
  • OrderEventListener๋Š” @TransactionalEventListener(AFTER_COMMIT)๋ผ ์ปค๋ฐ‹๋œ ๋’ค์—๋งŒ ์‹คํ–‰๋˜๊ณ , ๋กค๋ฐฑ๋˜๋ฉด ๋ถ€์ˆ˜ํšจ๊ณผ๋„ ์ผ์–ด๋‚˜์ง€ ์•Š๋Š”๋‹ค. ๋ฆฌ์Šค๋„ˆ ์˜ˆ์™ธ๋Š” ์‚ผํ‚จ๋‹ค โ€” ์ปค๋ฐ‹ ํ›„์— ๋˜์ง€๋ฉด "์ €์žฅ์€ ๋๋Š”๋ฐ 500 โ†’ ํด๋ผ์ด์–ธํŠธ ์žฌ์‹œ๋„ โ†’ ์ค‘๋ณต ์ด๋ฒคํŠธ"๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

์ฝ๊ธฐ โ€” replay

List<OrderEvent> events = repo.findByOrderIdOrderByTimestampAscEventIdAsc(orderId);
OrderDto order = new OrderDto();
events.

forEach(order::apply);   // CREATED โ†’ ์ „์ฒด ์ฑ„์›€, UPDATED โ†’ ๋ฎ์–ด์”€, DELETED โ†’ deleted=true
  • ์ •๋ ฌ์€ timestamp โ†’ eventId(์‹œํ€€์Šค). ๊ฐ™์€ ๋ฐ€๋ฆฌ์ดˆ์— ๋‘ ์ด๋ฒคํŠธ๊ฐ€ ๋“ค์–ด์™€๋„ ์‚ฝ์ž… ์ˆœ์„œ๋ฅผ ๋”ฐ๋ฅธ๋‹ค.
  • ๋ชฉ๋ก (GET /{userId}/orders)์€ userId์˜ orderId๋ฅผ ์ฒซ ์ด๋ฒคํŠธ (์ƒ์„ฑ) ์‹œ๊ฐ ์ˆœ์œผ๋กœ ๋‚˜์—ดํ•œ ๋’ค ๊ฐ๊ฐ replayํ•œ๋‹ค.
  • OrderDto๋Š” apply()๋กœ๋งŒ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€๋‹ค. setter๋กœ ์ง์ ‘ ์ฑ„์šฐ๋Š” ์กฐํšŒ ์ฝ”๋“œ๋Š” ์—†๋‹ค.

์ƒˆ ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ถ”๊ฐ€ํ•˜๋ ค๋ฉด โ€” ์„ธ ๊ณณ์„ ๊ฐ™์ด ๊ณ ์นœ๋‹ค

  1. EventType์— ์ƒ์ˆ˜ ์ถ”๊ฐ€
  2. EventCommandService์— ์ด๋ฒคํŠธ๋ฅผ ๋งŒ๋“ค์–ด save + publishEventํ•˜๋Š” ๋ฉ”์„œ๋“œ ์ถ”๊ฐ€
  3. OrderDto.apply()์— case + applyXxx() ์ถ”๊ฐ€

1โ†”3 ์ง์ด ์•ˆ ๋งž์œผ๋ฉด replay ์‹œ IllegalArgumentException์ด๋‹ค. ์ปดํŒŒ์ผ์€ ํ†ต๊ณผํ•˜๋‹ˆ ์ฃผ์˜ํ•œ๋‹ค. payload ํ•„๋“œ๋ช… (EventPayload)๊ณผ ์ฝ๊ธฐ ํด๋ž˜์Šค (OrderCreatedData/OrderUpdatedData) ํ•„๋“œ๋ช…์ด ๋‹ค๋ฅด๋ฉด Gson์ด ์กฐ์šฉํžˆ null ์„ ๋„ฃ๋Š”๋‹ค.


4. API

Method Path ๋™์ž‘ ์„ฑ๊ณต ์‹คํŒจ
POST /{userId}/orders EVENT_CREATED append 201 400 ๊ฒ€์ฆ ์‹คํŒจยท์˜ค๋ฒ„ํ”Œ๋กœ
PATCH /{userId}/orders/{orderId} EVENT_UPDATED append 200 400 / 404 ์—†๋Š” ์ฃผ๋ฌธ / 403 ํƒ€์ธ ์ฃผ๋ฌธ
GET /{userId}/orders userId์˜ ์ฃผ๋ฌธ ์ „๋ถ€ replay 200 [] ๊ฐ€๋Šฅ โ€”
GET /orders/{orderId} ์ฃผ๋ฌธ ํ•˜๋‚˜ replay 200 404 ์—†๋Š” ์ฃผ๋ฌธ

์š”์ฒญ ๋ฐ”๋”” (RequestOrder):

{
  "productId": "P1",
  "qty": 2,
  "unitPrice": 1000
}

productId ํ•„์ˆ˜, qty โ‰ฅ 1, unitPrice โ‰ฅ 0. totalPrice = qty ร— unitPrice๋Š” ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๊ณ„์‚ฐํ•˜๋ฉฐ int ๋ฒ”์œ„๋ฅผ ๋„˜์œผ๋ฉด 400.

EVENT_DELETED๋Š” OrderDto.apply()๊ฐ€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๋Œ€์‘ํ•˜๋Š” ์—”๋“œํฌ์ธํŠธ๋Š” ์•„์ง ์—†๋‹ค.


5. ํŒจํ‚ค์ง€ ๊ตฌ์กฐ

com.project.msa
โ”œโ”€โ”€ controller/   OrderCommandController (POST/PATCH) ยท OrderQueryController (GET)
โ”œโ”€โ”€ event/
โ”‚   โ”œโ”€โ”€ entity/     OrderEvent (Event Store ํ–‰) ยท EventType ยท EventPayload (payload ์Šคํ‚ค๋งˆ)
โ”‚   โ”œโ”€โ”€ repository/ OrderEventRepository
โ”‚   โ”œโ”€โ”€ service/    EventCommandService (์“ฐ๊ธฐ) ยท EventQueryService (์ฝ๊ธฐ/replay)
โ”‚   โ””โ”€โ”€ consumer/   OrderEventListener (@TransactionalEventListener)
โ”œโ”€โ”€ dto/          OrderDto (์ฝ๊ธฐ ๋ชจ๋ธ, apply๋กœ replay) ยท OrderCreatedData ยท OrderUpdatedData (payload ์—ญ์ง๋ ฌํ™”)
โ”œโ”€โ”€ vo/           RequestOrder ยท ResponseOrder (HTTP ๊ฒฝ๊ณ„)
โ””โ”€โ”€ exception/    OrderNotFoundException (404) ยท OrderAccessDeniedException (403) ยท GlobalExceptionHandler

๋ ˆ์ด์–ด ๋ฐฉํ–ฅ์€ controller โ†’ event.service โ†’ event.repository ๋‹จ๋ฐฉํ–ฅ์ด๋‹ค.


6. ์‹คํ–‰

./gradlew bootRun        # :8082
curl -X POST  localhost:8082/u1/orders -H 'Content-Type: application/json' -d '{"productId":"P1","qty":2,"unitPrice":1000}'
curl          localhost:8082/u1/orders
curl -X PATCH localhost:8082/u1/orders/{orderId} -H 'Content-Type: application/json' -d '{"productId":"P1","qty":5,"unitPrice":1000}'
curl          localhost:8082/orders/{orderId}

์ด๋ฒคํŠธ ์Šคํ† ์–ด๋ฅผ ์ง์ ‘ ๋ณด๋ ค๋ฉด H2 ์ฝ˜์†”: http://localhost:8082/h2-console (JDBC URL jdbc:h2:mem:testdb, ์‚ฌ์šฉ์ž sa, ๋น„๋ฐ€๋ฒˆํ˜ธ ์—†์Œ) โ†’ SELECT * FROM ORDER_EVENT ORDER BY EVENT_ID.

์Šคํƒ
Java 17 ยท Spring Boot 4.1.1 (Spring MVC) Spring Data JPA + H2 in-memory
Gson โ€” payload ์ง๋ ฌํ™” Jackson โ€” HTTP ์‘๋‹ต ์ง๋ ฌํ™”
Lombok ยท ModelMapper Bean Validation

H2๋Š” ์ธ๋ฉ”๋ชจ๋ฆฌ๋ผ ์žฌ๊ธฐ๋™ํ•˜๋ฉด ์ด๋ฒคํŠธ๊ฐ€ ์‚ฌ๋ผ์ง„๋‹ค. ์˜์† DB๋กœ ๋ฐ”๊พธ๋Š” ์ˆœ๊ฐ„ "์ด๋ฏธ ์ €์žฅ๋œ ์ด๋ฒคํŠธ๊ฐ€ ์ƒˆ applyXxx()๋กœ replay๋˜๋Š”๊ฐ€"๊ฐ€ ์‹ค์ œ ๋ฌธ์ œ๊ฐ€ ๋œ๋‹ค โ€” payload ๊ตฌ์กฐ๋ฅผ ๋ฐ”๊ฟ€ ๋•Œ ๊ฐ€์žฅ ๋จผ์ € ๋ด์•ผ ํ•  ์ง€์ ์ด๋‹ค.


7. ์ด ํ”„๋กœ์ ํŠธ๊ฐ€ ์˜๋„์ ์œผ๋กœ ํ•˜์ง€ ์•Š๋Š” ๊ฒƒ

  • ์Šค๋ƒ…์ƒทยทํ”„๋กœ์ ์…˜ ํ…Œ์ด๋ธ” โ€” ์กฐํšŒ ๋•Œ๋งˆ๋‹ค ์ „์ฒด replayํ•œ๋‹ค. ์ด๋ฒคํŠธ๊ฐ€ ๋งŽ์•„์ง€๋ฉด ๋А๋ ค์ง€์ง€๋งŒ ํ•™์Šต ๋ฒ”์œ„ ๋ฐ–.
  • ๋ณด์ƒ ์ด๋ฒคํŠธ โ€” ์ž˜๋ชป ์Œ“์ธ ์ด๋ฒคํŠธ๋ฅผ ๋˜๋Œ๋ฆฌ๋Š” ๊ฒฝ๋กœ๊ฐ€ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ์Œ“๊ธฐ ์ „ ๊ฒ€์ฆ (404/403/400)์ด ์ค‘์š”ํ•˜๋‹ค.
  • ํ˜ธ์ถœ ์ธก ์„œ๋น„์Šค โ€” ๋ณ„๋„๋กœ ๋‘์ง€ ์•Š๋Š”๋‹ค. curl๋กœ ์ง์ ‘ ํ˜ธ์ถœํ•œ๋‹ค.
  • ์ธ์ฆ โ€” ๊ฒฝ๋กœ์˜ {userId}๋ฅผ ๊ทธ๋Œ€๋กœ ์‹ ๋ขฐํ•œ๋‹ค. ์†Œ์œ ์ž ๊ฒ€์‚ฌ๋Š” "๊ฒฝ๋กœ userId == ์ฒซ ์ด๋ฒคํŠธ userId"๊นŒ์ง€๋งŒ.

About

๐Ÿ’ป CQRS ํŒจํ„ด ํ•™์Šต ์ €์žฅ์†Œ

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages