Axon Framework: DDD ๋ฐ CQRS ๊ตฌํ˜„ ๐Ÿš€

์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - Axon Framework: DDD ๋ฐ CQRS ๊ตฌํ˜„ ๐Ÿš€

 

 

์•ˆ๋…•ํ•˜์„ธ์š”, ๊ฐœ๋ฐœ์ž ์—ฌ๋Ÿฌ๋ถ„! ์˜ค๋Š˜์€ Java ์ƒํƒœ๊ณ„์—์„œ ์ฃผ๋ชฉ๋ฐ›๊ณ  ์žˆ๋Š” Axon Framework์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. Axon Framework๋Š” ๋„๋ฉ”์ธ ์ฃผ๋„ ์„ค๊ณ„(Domain-Driven Design, DDD)์™€ ๋ช…๋ น ์ฟผ๋ฆฌ ์ฑ…์ž„ ๋ถ„๋ฆฌ(Command Query Responsibility Segregation, CQRS) ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ํŠนํ™”๋œ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ด ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ†ตํ•ด ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํ˜„๋Œ€์˜ ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์—์„œ๋Š” ๋ณต์žก์„ฑ์„ ๊ด€๋ฆฌํ•˜๊ณ  ์‹œ์Šคํ…œ์˜ ์œ ์—ฐ์„ฑ์„ ๋†’์ด๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. Axon Framework๋Š” ์ด๋Ÿฌํ•œ ์š”๊ตฌ์‚ฌํ•ญ์„ ์ถฉ์กฑ์‹œํ‚ค๋ฉด์„œ๋„, ๊ฐœ๋ฐœ์ž๋“ค์ด ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์— ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค. ํŠนํžˆ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜, ์ด๋ฒคํŠธ ์†Œ์‹ฑ(Event Sourcing)์„ ์ ์šฉํ•˜๊ณ ์ž ํ•˜๋Š” ํ”„๋กœ์ ํŠธ์— ๋งค์šฐ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.

ย 

์ด ๊ธ€์—์„œ๋Š” Axon Framework์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋ถ€ํ„ฐ ์‹ค์ œ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•, ๊ทธ๋ฆฌ๊ณ  ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๊นŒ์ง€ ์ƒ์„ธํžˆ ๋‹ค๋ฃจ๊ฒ ์Šต๋‹ˆ๋‹ค. ๋˜ํ•œ, ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ Axon Framework๋ฅผ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ค ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ๋Š”์ง€์— ๋Œ€ํ•ด์„œ๋„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์žฌ๋Šฅ๋„ท์˜ '์ง€์‹์ธ์˜ ์ˆฒ'์—์„œ ์ œ๊ณตํ•˜๋Š” ์ด ๊ธ€์„ ํ†ตํ•ด, ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์—ญ๋Ÿ‰์„ ํ•œ ๋‹จ๊ณ„ ๋” ๋†’์ผ ์ˆ˜ ์žˆ๊ธฐ๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ’ก

1. Axon Framework ์†Œ๊ฐœ ๐Ÿ“š

Axon Framework๋Š” Java ๊ธฐ๋ฐ˜์˜ ์˜คํ”ˆ์†Œ์Šค ํ”„๋ ˆ์ž„์›Œํฌ๋กœ, ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ๋„๊ตฌ์™€ ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ด ํ”„๋ ˆ์ž„์›Œํฌ์˜ ์ฃผ์š” ํŠน์ง•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • DDD ์ง€์›: ๋„๋ฉ”์ธ ์ค‘์‹ฌ์˜ ์„ค๊ณ„๋ฅผ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.
  • CQRS ํŒจํ„ด: ๋ช…๋ น๊ณผ ์ฟผ๋ฆฌ๋ฅผ ๋ถ„๋ฆฌํ•˜์—ฌ ์‹œ์Šคํ…œ์˜ ํ™•์žฅ์„ฑ๊ณผ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.
  • ์ด๋ฒคํŠธ ์†Œ์‹ฑ: ์ƒํƒœ ๋ณ€๊ฒฝ์„ ์ด๋ฒคํŠธ๋กœ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.
  • ๋ฉ”์‹œ์ง• ์ธํ”„๋ผ: ๋น„๋™๊ธฐ ํ†ต์‹ ์„ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๋ฉ”์‹œ์ง• ์‹œ์Šคํ…œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.
  • ํ™•์žฅ์„ฑ: ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜์— ์ ํ•ฉํ•œ ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

ย 

Axon Framework๋Š” 2009๋…„์— ์ฒ˜์Œ ์ถœ์‹œ๋œ ์ดํ›„, ์ง€์†์ ์ธ ๋ฐœ์ „์„ ๊ฑฐ๋“ญํ•ด์™”์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ๋Š” ๋งŽ์€ ๊ธฐ์—…๋“ค์ด ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ด€๋ฆฌํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ Axon Framework๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก Axon Framework์˜ ์ฒ ํ•™

Axon Framework์˜ ํ•ต์‹ฌ ์ฒ ํ•™์€ "๋ณต์žก์„ฑ์˜ ๊ด€๋ฆฌ"์ž…๋‹ˆ๋‹ค. ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด ๋ณต์žกํ•ด์งˆ์ˆ˜๋ก, ์ด๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์ง‘๋‹ˆ๋‹ค. Axon Framework๋Š” ์ด๋Ÿฌํ•œ ๋ณต์žก์„ฑ์„ ๋‹ค๋ฃจ๊ธฐ ์œ„ํ•œ ๊ตฌ์กฐํ™”๋œ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

Axon Framework๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์˜ ๋ช…ํ™•ํ•œ ๋ถ„๋ฆฌ
  • ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„
  • ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์‹œ์Šคํ…œ์˜ ์‰ฌ์šด ๊ตฌํ˜„
  • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ ํ–ฅ์ƒ
  • ์œ ์ง€๋ณด์ˆ˜์„ฑ ๊ฐœ์„ 

ย 

์ด์ œ Axon Framework์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋“ค์„ ์ž์„ธํžˆ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์—ฌ๋Ÿฌ๋ถ„์€ Axon Framework๊ฐ€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋” ๊นŠ์ด ์ดํ•ดํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

Axon Framework ๊ตฌ์„ฑ์š”์†Œ Command Event Query

์œ„ ๋‹ค์ด์–ด๊ทธ๋žจ์€ Axon Framework์˜ ์ฃผ์š” ๊ตฌ์„ฑ์š”์†Œ์ธ Command, Event, Query์˜ ๊ด€๊ณ„๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์ด๋“ค์€ CQRS ํŒจํ„ด์˜ ํ•ต์‹ฌ์„ ์ด๋ฃจ๋ฉฐ, Axon Framework์—์„œ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.

2. Axon Framework์˜ ํ•ต์‹ฌ ๊ฐœ๋… ๐Ÿง 

Axon Framework๋ฅผ ์ œ๋Œ€๋กœ ์ดํ•ดํ•˜๊ณ  ํ™œ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋ช‡ ๊ฐ€์ง€ ํ•ต์‹ฌ ๊ฐœ๋…์„ ์•Œ์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์„น์…˜์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๊ฐœ๋…๋“ค์„ ์ž์„ธํžˆ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

2.1 Command ๐Ÿ“ฎ

Command๋Š” ์‹œ์Šคํ…œ์—์„œ ์–ด๋–ค ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋ผ๋Š” ์ง€์‹œ๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, "์ƒˆ ์‚ฌ์šฉ์ž ์ƒ์„ฑ", "์ฃผ๋ฌธ ์ทจ์†Œ" ๋“ฑ์ด Command์— ํ•ด๋‹นํ•ฉ๋‹ˆ๋‹ค.


public class CreateUserCommand {
    private final String userId;
    private final String username;

    public CreateUserCommand(String userId, String username) {
        this.userId = userId;
        this.username = username;
    }

    // Getters...
}

ย 

Command๋Š” ๋ถˆ๋ณ€(immutable)๊ฐ์ฒด๋กœ ์ƒ์„ฑ๋˜๋ฉฐ, ์‹œ์Šคํ…œ์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š” ์˜๋„๋ฅผ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. Command Handler๋Š” ์ด๋Ÿฌํ•œ Command๋ฅผ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ Event๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค.

2.2 Event ๐ŸŽ‰

Event๋Š” ์‹œ์Šคํ…œ์—์„œ ๋ฐœ์ƒํ•œ ์ค‘์š”ํ•œ ๋ณ€ํ™”๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. "์‚ฌ์šฉ์ž๊ฐ€ ์ƒ์„ฑ๋จ", "์ฃผ๋ฌธ์ด ์ทจ์†Œ๋จ" ๋“ฑ์ด Event์˜ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.


public class UserCreatedEvent {
    private final String userId;
    private final String username;

    public UserCreatedEvent(String userId, String username) {
        this.userId = userId;
        this.username = username;
    }

    // Getters...
}

ย 

Event๋Š” ๊ณผ๊ฑฐ ์‹œ์ œ๋กœ ๋ช…๋ช…๋˜๋ฉฐ, ์ด๋ฏธ ๋ฐœ์ƒํ•œ ์‚ฌ์‹ค์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. Event๋Š” ์‹œ์Šคํ…œ์˜ ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ์ „ํŒŒ๋˜์–ด ํ•„์š”ํ•œ ์ž‘์—…์„ ํŠธ๋ฆฌ๊ฑฐํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

2.3 Query ๐Ÿ”

Query๋Š” ์‹œ์Šคํ…œ์˜ ํ˜„์žฌ ์ƒํƒœ์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์š”์ฒญํ•˜๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. "์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ", "์ฃผ๋ฌธ ๋ชฉ๋ก ์กฐํšŒ" ๋“ฑ์ด Query์˜ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.


public class GetUserQuery {
    private final String userId;

    public GetUserQuery(String userId) {
        this.userId = userId;
    }

    // Getter...
}

ย 

Query๋Š” ์‹œ์Šคํ…œ์˜ ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ๋‹จ์ˆœํžˆ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. Query Handler๋Š” ์ด๋Ÿฌํ•œ Query๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

2.4 Aggregate ๐Ÿ“ฆ

Aggregate๋Š” ๊ด€๋ จ๋œ ๊ฐ์ฒด๋“ค์˜ ์ง‘ํ•ฉ์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ์ผ๊ด€์„ฑ ์žˆ๋Š” ๊ฒฝ๊ณ„๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค. Aggregate๋Š” Command๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  Event๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์ฃผ์ฒด์ž…๋‹ˆ๋‹ค.


@Aggregate
public class UserAggregate {
    @AggregateIdentifier
    private String userId;
    private String username;

    @CommandHandler
    public UserAggregate(CreateUserCommand command) {
        apply(new UserCreatedEvent(command.getUserId(), command.getUsername()));
    }

    @EventSourcingHandler
    public void on(UserCreatedEvent event) {
        this.userId = event.getUserId();
        this.username = event.getUsername();
    }
}

ย 

Aggregate๋Š” ๋„๋ฉ”์ธ ๋ชจ๋ธ์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ ํ‘œํ˜„ํ•˜๋ฉฐ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Šํ™”ํ•ฉ๋‹ˆ๋‹ค. Axon Framework์—์„œ Aggregate๋Š” ์ด๋ฒคํŠธ ์†Œ์‹ฑ์„ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

2.5 Event Sourcing ๐Ÿ“œ

Event Sourcing์€ ์‹œ์Šคํ…œ์˜ ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ผ๋ จ์˜ ์ด๋ฒคํŠธ๋กœ ์ €์žฅํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ถ”์ ํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ํŠน์ • ์‹œ์ ์˜ ์ƒํƒœ๋ฅผ ์žฌ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ”‘ Event Sourcing์˜ ์žฅ์ 

  • ์™„๋ฒฝํ•œ ๊ฐ์‚ฌ ์ถ”์  ๊ฐ€๋Šฅ
  • ์‹œ์Šคํ…œ ์ƒํƒœ์˜ ์‹œ๊ฐ„ ์—ฌํ–‰ ๊ฐ€๋Šฅ
  • ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ์บก์ฒ˜
  • ์„ฑ๋Šฅ๊ณผ ํ™•์žฅ์„ฑ ํ–ฅ์ƒ

Axon Framework๋Š” Event Sourcing์„ ๊ธฐ๋ณธ์ ์œผ๋กœ ์ง€์›ํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2.6 Saga ๐Ÿ”„

Saga๋Š” ์žฅ๊ธฐ ์‹คํ–‰ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ž…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ Aggregate์— ๊ฑธ์ณ ์žˆ๋Š” ํŠธ๋žœ์žญ์…˜์„ ์กฐ์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.


@Saga
public class OrderSaga {
    @Autowired
    private transient CommandGateway commandGateway;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCreatedEvent event) {
        String paymentId = UUID.randomUUID().toString();
        commandGateway.send(new ProcessPaymentCommand(paymentId, event.getOrderId(), event.getAmount()))
            .exceptionally(ex -> {
                commandGateway.send(new CancelOrderCommand(event.getOrderId()));
                return null;
            });
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentProcessedEvent event) {
        commandGateway.send(new ShipOrderCommand(event.getOrderId()));
    }

    @EndSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderShippedEvent event) {
        // Saga ends
    }
}

ย 

Saga๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ , ์žฅ์•  ์ƒํ™ฉ์—์„œ๋„ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Axon Framework ํ•ต์‹ฌ ๊ฐœ๋… ๊ด€๊ณ„๋„ Command Aggregate Event Saga

์œ„ ๋‹ค์ด์–ด๊ทธ๋žจ์€ Axon Framework์˜ ํ•ต์‹ฌ ๊ฐœ๋…๋“ค ๊ฐ„์˜ ๊ด€๊ณ„๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. Command๊ฐ€ Aggregate๋กœ ์ „๋‹ฌ๋˜๊ณ , Aggregate๊ฐ€ Event๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉฐ, Saga๊ฐ€ ์ด๋Ÿฌํ•œ ํ”„๋กœ์„ธ์Šค๋ฅผ ์กฐ์ •ํ•˜๋Š” ๋ชจ์Šต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ํ•ต์‹ฌ ๊ฐœ๋…๋“ค์„ ์ดํ•ดํ•˜๊ณ  ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด, Axon Framework๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์ด๋Ÿฌํ•œ ๊ฐœ๋…๋“ค์„ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

3. Axon Framework ๊ตฌํ˜„ํ•˜๊ธฐ ๐Ÿ› ๏ธ

์ด์ œ Axon Framework์˜ ํ•ต์‹ฌ ๊ฐœ๋…์„ ์ดํ•ดํ–ˆ์œผ๋‹ˆ, ์‹ค์ œ๋กœ ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ด ์„น์…˜์—์„œ๋Š” ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ๋ฅผ ํ†ตํ•ด Axon Framework์˜ ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ๋“ค์„ ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

3.1 ํ”„๋กœ์ ํŠธ ์„ค์ • ๐Ÿ—๏ธ

๋จผ์ €, Axon Framework๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ํ”„๋กœ์ ํŠธ ์„ค์ •๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค. Maven์„ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ , pom.xml ํŒŒ์ผ์— ๋‹ค์Œ ์˜์กด์„ฑ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค:


<dependencies>
    <dependency>
        <groupId>org.axonframework</groupId>
        <artifactId>axon-spring-boot-starter</artifactId>
        <version>4.5.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>

ย 

์ด ์„ค์ •์€ Axon Framework์™€ Spring Boot๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ, H2 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ์ธ๋ฉ”๋ชจ๋ฆฌ ์ €์žฅ์†Œ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

3.2 Command ๊ตฌํ˜„ ๐Ÿ“ฎ

์ด์ œ ๊ฐ„๋‹จํ•œ "์‚ฌ์šฉ์ž ์ƒ์„ฑ" ๋ช…๋ น์„ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:


public class CreateUserCommand {
    @TargetAggregateIdentifier
    private final String userId;
    private final String username;

    public CreateUserCommand(String userId, String username) {
        this.userId = userId;
        this.username = username;
    }

    // Getters...
}

ย 

@TargetAggregateIdentifier ์–ด๋…ธํ…Œ์ด์…˜์€ ์ด Command๊ฐ€ ์–ด๋–ค Aggregate ์ธ์Šคํ„ด์Šค๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๋Š”์ง€ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค.

3.3 Event ๊ตฌํ˜„ ๐ŸŽ‰

Command์— ๋Œ€์‘ํ•˜๋Š” Event๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:


public class UserCreatedEvent {
    private final String userId;
    private final String username;

    public UserCreatedEvent(String userId, String username) {
        this.userId = userId;
        this.username = username;
    }

    // Getters...
}

ย 

์ด Event๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ์Œ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค.

3.4 Aggregate ๊ตฌํ˜„ ๐Ÿ“ฆ

์ด์ œ Command๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  Event๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š” Aggregate๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:


@Aggregate
public class UserAggregate {
    @AggregateIdentifier
    private String userId;
    private String username;

    @CommandHandler
    public UserAggregate(CreateUserCommand command) {
        apply(new UserCreatedEvent(command.getUserId(), command.getUsername()));
    }

    @EventSourcingHandler
    public void on(UserCreatedEvent event) {
        this.userId = event.getUserId();
        this.username = event.getUsername();
    }

    protected UserAggregate() {
        // Required by Axon
    }
}

ย 

์ด Aggregate๋Š” CreateUserCommand๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , UserCreatedEvent๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. @EventSourcingHandler ๋ฉ”์„œ๋“œ๋Š” Event๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ Aggregate์˜ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค.

3.5 Query ๊ตฌํ˜„ ๐Ÿ”

์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” Query๋ฅผ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:


public class GetUserQuery {
    private final String userId;

    public GetUserQuery(String userId) {
        this.userId = userId;
    }

    // Getter...
}

ย 

๊ทธ๋ฆฌ๊ณ  ์ด Query๋ฅผ ์ฒ˜๋ฆฌํ•  Handler๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:


@Component
public class UserQueryHandler {
    private final Map<String, String> users = new HashMap<>();

    @EventHandler
    public void on(UserCreatedEvent event) {
        users.put(event.getUserId(), event.getUsername());
    }

    @QueryHandler
    public String handle(GetUserQuery query) {
        return users.get(query.getUserId());
    }
}

ย 

์ด Handler๋Š” UserCreatedEvent๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ณ , GetUserQuery๋ฅผ ์ฒ˜๋ฆฌํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

3.6 Controller ๊ตฌํ˜„ ๐ŸŽฎ

๋งˆ์ง€๋ง‰์œผ๋กœ, ์ด ๋ชจ๋“  ๊ฒƒ์„ ์—ฐ๊ฒฐํ•  Controller๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:


@RestController
@RequestMapping("/users")
public class UserController {
    private final CommandGateway commandGateway;
    private final QueryGateway queryGateway;

    public UserController(CommandGateway commandGateway, QueryGateway queryGateway) {
        this.commandGateway = commandGateway;
        this.queryGateway = queryGateway;
    }

    @PostMapping
    public CompletableFuture<String> createUser(@RequestBody CreateUserRequest request) {
        String userId = UUID.randomUUID().toString();
        return commandGateway.send(new CreateUserCommand(userId, request.getUsername()));
    }

    @GetMapping("/{userId}")
    public CompletableFuture<String> getUser(@PathVariable String userId) {
        return queryGateway.query(new GetUserQuery(userId), String.class);
    }
}

ย 

์ด Controller๋Š” HTTP ์š”์ฒญ์„ ๋ฐ›์•„ Command์™€ Query๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , Axon์˜ Gateway๋ฅผ ํ†ตํ•ด ์ด๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก Axon Framework์˜ ์žฅ์ 

์œ„์˜ ๊ตฌํ˜„์„ ํ†ตํ•ด Axon Framework๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ์ฃผ์š” ์žฅ์ ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋ช…ํ™•ํ•œ ์ฑ…์ž„ ๋ถ„๋ฆฌ: Command, Event, Query๊ฐ€ ๊ฐ๊ฐ ๋ถ„๋ฆฌ๋˜์–ด ์žˆ์–ด ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.
  • ํ™•์žฅ์„ฑ: ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋…๋ฆฝ์ ์œผ๋กœ ํ™•์žฅ๋  ์ˆ˜ ์žˆ์–ด, ์‹œ์Šคํ…œ์˜ ํ™•์žฅ์ด ์šฉ์ดํ•ฉ๋‹ˆ๋‹ค.
  • ์œ ์—ฐ์„ฑ: ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๊ธฐ์กด ๊ธฐ๋Šฅ์„ ์ˆ˜์ •ํ•  ๋•Œ, ๋‹ค๋ฅธ ๋ถ€๋ถ„์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ์„ ์ตœ์†Œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ: ๊ฐ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•  ์ˆ˜ ์žˆ์–ด, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์™€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๊ฐ€ ์‰ฌ์›Œ์ง‘๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๊ตฌํ˜„์„ ํ†ตํ•ด Axon Framework๋ฅผ ์‚ฌ์šฉํ•œ ๊ธฐ๋ณธ์ ์ธ CQRS ํŒจํ„ด๊ณผ ์ด๋ฒคํŠธ ์†Œ์‹ฑ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์ข€ ๋” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค๊ณผ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ์˜ ํ™œ์šฉ ๋ฐฉ์•ˆ์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

Axon Framework ๊ตฌํ˜„ ํ๋ฆ„๋„ Controller Command Aggregate Event Event Store Query Model

์œ„ ๋‹ค์ด์–ด๊ทธ๋žจ์€ Axon Framework๋ฅผ ์‚ฌ์šฉํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๊ธฐ๋ณธ์ ์ธ ํ๋ฆ„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. Controller๊ฐ€ Command๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ์ด๋Š” Aggregate์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋˜์–ด Event๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ๋ฐœ์ƒํ•œ Event๋Š” Event Store์— ์ €์žฅ๋˜๊ณ , ๋™์‹œ์— Query Model์„ ์—…๋ฐ์ดํŠธํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

4. Axon Framework์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๐Ÿš€

์ง€๊ธˆ๊นŒ์ง€ Axon Framework์˜ ๊ธฐ๋ณธ์ ์ธ ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. ์ด์ œ Axon Framework๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋ช‡ ๊ฐ€์ง€ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

4.1 Saga ๊ตฌํ˜„ ๐Ÿ”„

Saga๋Š” ์—ฌ๋Ÿฌ Aggregate์— ๊ฑธ์ณ ์žˆ๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ฃผ๋ฌธ ์ฒ˜๋ฆฌ ๊ณผ์ •์„ Saga๋กœ ๊ตฌํ˜„ํ•ด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:


@Saga
public class OrderSaga {
    @Autowired
    private transient CommandGateway commandGateway;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCreatedEvent event) {
        String paymentId = UUID.randomUUID().toString();
        commandGateway.send(new ProcessPaymentCommand(paymentId, event.getOrderId(), event.getAmount()))
            .exceptionally(ex -> {
                commandGateway.send(new CancelOrderCommand(event.getOrderId()));
                return null;
            });
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentProcessedEvent event) {
        commandGateway.send(new ShipOrderCommand(event.getOrderId()));
    }

    @EndSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderShippedEvent event) {
        // Saga ends
    }
}

ย 

์ด Saga๋Š” ์ฃผ๋ฌธ ์ƒ์„ฑ, ๊ฒฐ์ œ ์ฒ˜๋ฆฌ, ๋ฐฐ์†ก ์ฒ˜๋ฆฌ์˜ ์ „์ฒด ๊ณผ์ •์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„์—์„œ ๋ฐœ์ƒํ•˜๋Š” Event๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ , ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์œ„ํ•œ Command๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค.

4.2 Snapshotting ๐Ÿ“ธ

Event Sourcing์„ ์‚ฌ์šฉํ•  ๋•Œ, Aggregate์˜ ์ƒํƒœ๋ฅผ ์žฌ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ์˜ค๋ž˜ ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Snapshotting์€ ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค:


@Aggregate
public class LargeAggregate {
    @AggregateIdentifier
    private String id;
    private List<String> items = new ArrayList<>();

    @CommandHandler
    public void handle(AddItemCommand command) {
        apply(new ItemAddedEvent(command.getId(), command.getItem()));
    }

    @EventSourcingHandler
    public void on(ItemAddedEvent event) {
        this.id = event.getId();
        this.items.add(event.getItem());
    }

    @EventSourcingHandler
    public void on(SnapshotEvent event) {
        this.id = event.getId();
        this.items = event.getItems();
    }

    @SnapshotTriggerDefinition(snapshotter = "mySnapshotter")
    public Snapshotter getSnapshotter() {
        return new EventCountSnapshotTriggerDefinition(100);
    }
}

ย 

์ด ์˜ˆ์ œ์—์„œ๋Š” 100๊ฐœ์˜ ์ด๋ฒคํŠธ๋งˆ๋‹ค ์Šค๋ƒ…์ƒท์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Aggregate์˜ ์ƒํƒœ๋ฅผ ๋น ๋ฅด๊ฒŒ ๋ณต์›ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

4.3 Upcasting ๐Ÿ”„

์‹œ๊ฐ„์ด ์ง€๋‚˜๋ฉด์„œ Event์˜ ๊ตฌ์กฐ๊ฐ€ ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. Upcasting์€ ์ด์ „ ๋ฒ„์ „์˜ Event๋ฅผ ์ƒˆ ๋ฒ„์ „์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:


public class UserCreatedEventUpcaster extends SingleEventUpcaster {
    @Override
    protected boolean canUpcast(IntermediateEventRepresentation intermediateRepresentation) {
        return intermediateRepresentation.getType().getName().equals("com.example.UserCreatedEvent");
    }

    @Override
    protected IntermediateEventRepresentation doUpcast(IntermediateEventRepresentation intermediateRepresentation) {
        return intermediateRepresentation.upcastPayload(
            new SimpleSerializedType(UserCreatedEvent.class.getName(), "2.0"),
            org.dom4j.Document.class,
            document -> {
                Element rootElement = document.getRootElement();
                rootElement.addElement("email").setText("default@example.com");
                return document;
            }
        );
    }
}

ย 

์ด Upcaster๋Š” ์ด์ „ ๋ฒ„์ „์˜ UserCreatedEvent์— email ํ•„๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

4.4 Metadata ํ™œ์šฉ ๐Ÿท๏ธ

Metadata๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Event๋‚˜ Command์— ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ํฌํ•จ์‹œํ‚ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@CommandHandler
public void handle(CreateUserCommand command, @MetaDataValue("userId") String userId) {
    apply(new UserCreatedEvent(command.getUserId(), command.getUsername()))
        .withMetaData(Collections.singletonMap("createdBy", userId));
}

@EventHandler
public void on(UserCreatedEvent event, @MetaDataValue("createdBy") String createdBy) {
    // Use the metadata
}

ย 

์ด ์˜ˆ์ œ์—์„œ๋Š” Command๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ Metadata์—์„œ userId๋ฅผ ๊ฐ€์ ธ์˜ค๊ณ , Event๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ๋•Œ createdBy ์ •๋ณด๋ฅผ Metadata์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

4.5 Subscription Queries ๐Ÿ””

Subscription Queries๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ดˆ๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ›์€ ํ›„ ์ถ”๊ฐ€์ ์ธ ์—…๋ฐ์ดํŠธ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@RestController
@RequestMapping("/users")
public class UserController {
    private final QueryGateway queryGateway;

    @GetMapping("/{userId}")
    public Flux<String> getUserUpdates(@PathVariable String userId) {
        SubscriptionQueryResult<String, String> queryResult = queryGateway.subscriptionQuery(
            new GetUserQuery(userId),
            String.class,
            String.class
        );

        return Flux.concat(
            Flux.from(queryResult.initialResult()),
            queryResult.updates()
        );
    }
}

ย 

์ด ์˜ˆ์ œ์—์„œ๋Š” ์‚ฌ์šฉ์ž ์ •๋ณด์˜ ์ดˆ๊ธฐ ์ƒํƒœ๋ฅผ ๋ฐ›์€ ํ›„, ํ•ด๋‹น ์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—…๋ฐ์ดํŠธ๋  ๋•Œ๋งˆ๋‹ค ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐ŸŒŸ Axon Framework์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ํ™œ์šฉ ํŒ

  • Saga๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•  ๋•Œ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋„ˆ๋ฌด ๋งŽ์€ ๋กœ์ง์„ Saga์— ๋„ฃ์ง€ ์•Š๋„๋ก ์ฃผ์˜ํ•˜์„ธ์š”.
  • Snapshotting์€ ์„ฑ๋Šฅ ํ–ฅ์ƒ์— ๋„์›€์ด ๋˜์ง€๋งŒ, ๋„ˆ๋ฌด ์ž์ฃผ ์Šค๋ƒ…์ƒท์„ ์ƒ์„ฑํ•˜๋ฉด ์˜คํžˆ๋ ค ์„ฑ๋Šฅ์ด ์ €ํ•˜๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Upcasting์€ ์ด๋ฒคํŠธ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์„ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ง€๋งŒ, ๊ฐ€๋Šฅํ•œ ํ•œ ์ด๋ฒคํŠธ ๊ตฌ์กฐ๋ฅผ ์•ˆ์ •์ ์œผ๋กœ ์œ ์ง€ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • Metadata๋Š” ์œ ์šฉํ•˜์ง€๋งŒ ๊ณผ๋„ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜๋ฉด ์ฝ”๋“œ์˜ ๋ณต์žก์„ฑ์ด ์ฆ๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊ผญ ํ•„์š”ํ•œ ์ •๋ณด๋งŒ Metadata์— ํฌํ•จ์‹œํ‚ค์„ธ์š”.
  • Subscription Queries๋Š” ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์œ ์šฉํ•˜์ง€๋งŒ, ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰์ด ์ฆ๊ฐ€ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ ์ ˆํžˆ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ๋“ค์„ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด Axon Framework๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋”์šฑ ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ์„น์…˜์—์„œ๋Š” ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ Axon Framework๋ฅผ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

5. ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ์˜ Axon Framework ํ™œ์šฉ ๐Ÿ’ผ

Axon Framework๋Š” ๋‹ค์–‘ํ•œ ๋„๋ฉ”์ธ๊ณผ ๊ทœ๋ชจ์˜ ํ”„๋กœ์ ํŠธ์—์„œ ํ™œ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์„น์…˜์—์„œ๋Š” ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ Axon Framework๋ฅผ ์–ด๋–ป๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์–ด๋–ค ์ด์ ์„ ์–ป์„ ์ˆ˜ ์žˆ๋Š”์ง€ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

5.1 ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„ ๐Ÿ—๏ธ

Axon Framework๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ๋งค์šฐ ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค:


@Configuration
public class AxonConfig {
    @Bean
    public CommandGateway commandGateway(CommandBus commandBus) {
        return DefaultCommandGateway.builder().commandBus(commandBus).build();
    }

    @Bean
    public CommandBus commandBus() {
        return SimpleCommandBus.builder().build();
    }

    @Bean
    public EventBus eventBus() {
        return SimpleEventBus.builder().build();
    }

    @Bean
    public QueryBus queryBus() {
        return SimpleQueryBus.builder().build();
    }
}

ย 

์ด ์„ค์ •์„ ํ†ตํ•ด ๊ฐ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค๋Š” ์ž์ฒด์ ์ธ Command, Event, Query ๋ฒ„์Šค๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์„œ๋น„์Šค ๊ฐ„์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถ”๊ณ  ๋…๋ฆฝ์ ์ธ ํ™•์žฅ์ด ๊ฐ€๋Šฅํ•ด์ง‘๋‹ˆ๋‹ค.

5.2 ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„ ๐ŸŒ

Axon Framework์˜ ์ด๋ฒคํŠธ ์†Œ์‹ฑ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜์—ฌ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@Configuration
public class AxonConfig {
    @Bean
    public EventStorageEngine eventStorageEngine() {
        return new InMemoryEventStorageEngine();
    }

    @Bean
    public EventStore eventStore(EventStorageEngine storageEngine) {
        return EmbeddedEventStore.builder()
            .storageEngine(storageEngine)
            .build();
    }

    @Bean
    public EventBus eventBus(EventStore eventStore) {
        return SimpleEventBus.builder()
            .eventStore(eventStore)
            .build();
    }
}

ย 

์ด ์„ค์ •์„ ํ†ตํ•ด ๋ชจ๋“  ์ด๋ฒคํŠธ๋ฅผ ์ €์žฅํ•˜๊ณ  ์žฌ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ด๋ฒคํŠธ ์ €์žฅ์†Œ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๊ณผ๊ฑฐ์˜ ์ƒํƒœ๋ฅผ ์žฌ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

5.3 ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค ๊ด€๋ฆฌ ๐Ÿ”„

Saga๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@Saga
public class OrderProcessingSaga {
    @Autowired
    private transient CommandGateway commandGateway;

    @StartSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderCreatedEvent event) {
        String paymentId = UUID.randomUUID().toString();
        commandGateway.send(new ProcessPaymentCommand(paymentId, event.getOrderId(), event.getAmount()));
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void handle(PaymentProcessedEvent event) {
        commandGateway.send(new PrepareShipmentCommand(event.getOrderId()));
    }

    @SagaEventHandler(associationProperty = "orderId")
    public void handle(ShipmentPreparedEvent event) {
        commandGateway.send(new ShipOrderCommand(event.getOrderId()));
    }

    @EndSaga
    @SagaEventHandler(associationProperty = "orderId")
    public void handle(OrderShippedEvent event) {
        // Saga ends
    }
}

ย 

์ด Saga๋Š” ์ฃผ๋ฌธ ์ƒ์„ฑ๋ถ€ํ„ฐ ๊ฒฐ์ œ, ๋ฐฐ์†ก ์ค€๋น„, ๋ฐฐ์†ก๊นŒ์ง€์˜ ์ „์ฒด ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋‹จ๊ณ„๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ๋งˆ๋‹ค ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ์‹œ์ž‘ํ•˜๋Š” Command๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค.

5.4 ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™” ๐Ÿ”„

Subscription Queries๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@RestController
@RequestMapping("/orders")
public class OrderController {
    private final QueryGateway queryGateway;

    @GetMapping("/{orderId}")
    public Flux<OrderStatus> getOrderStatus(@PathVariable String orderId) {
        SubscriptionQueryResult<OrderStatus, OrderStatus> queryResult = queryGateway.subscriptionQuery(
            new GetOrderStatusQuery(orderId),
            OrderStatus.class,
            OrderStatus.class
        );

        return Flux.concat(
            Flux.from(queryResult.initialResult()),
            queryResult.updates()
        );
    }
}

ย 

์ด ์˜ˆ์ œ์—์„œ๋Š” ์ฃผ๋ฌธ ์ƒํƒœ์˜ ์ดˆ๊ธฐ๊ฐ’์„ ๋ฐ›์€ ํ›„, ์ฃผ๋ฌธ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5.5 ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๐Ÿš€

Axon Framework์˜ ๋ถ„์‚ฐ ์•„ํ‚คํ…์ฒ˜ ์ง€์›์„ ํ™œ์šฉํ•˜์—ฌ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@Configuration
public class AxonConfig {
    @Bean
    public CommandBus commandBus() {
        return DistributedCommandBus.builder()
            .connector(springCloudConnector())
            .build();
    }

    @Bean
    public SpringCloudCommandRouter springCloudCommandRouter(DiscoveryClient discoveryClient) {
        return SpringCloudCommandRouter.builder()
            .discoveryClient(discoveryClient)
            .build();
    }

    @Bean
    public SpringCloudHttpBackupCommandRouter springCloudHttpBackupCommandRouter(DiscoveryClient discoveryClient) {
        return SpringCloudHttpBackupCommandRouter.builder()
            .discoveryClient(discoveryClient)
            .build();
    }

    @Bean
    public SpringCloudConnector springCloudConnector(SpringCloudCommandRouter commandRouter,
                                                     SpringCloudHttpBackupCommandRouter backupCommandRouter) {
        return SpringCloudConnector.builder()
            .cloudCommandRouter(commandRouter)
            .backupCommandRouter(backupCommandRouter)
            .build();
    }
}

ย 

์ด ์„ค์ •์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ ์ธ์Šคํ„ด์Šค์— ๊ฑธ์ณ Command๋ฅผ ๋ถ„์‚ฐ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋Š” ์‹œ์Šคํ…œ์˜ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ค๊ณ  ํ™•์žฅ์„ฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ์˜ Axon Framework ํ™œ์šฉ ํŒ

  • ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ๊ฐ ์„œ๋น„์Šค์˜ ๊ฒฝ๊ณ„๋ฅผ ๋ช…ํ™•ํžˆ ์ •์˜ํ•˜๊ณ , ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์€ ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • ์ด๋ฒคํŠธ ์†Œ์‹ฑ์„ ๋„์ž…ํ•  ๋•Œ๋Š” ์ดˆ๊ธฐ์— ๋ชจ๋“  ๋„๋ฉ”์ธ์— ์ ์šฉํ•˜๊ธฐ๋ณด๋‹ค๋Š”, ์ค‘์š”ํ•˜๊ณ  ๋ณต์žกํ•œ ๋„๋ฉ”์ธ๋ถ€ํ„ฐ ์ ์ง„์ ์œผ๋กœ ์ ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • Saga๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ๊ฐ€๋Šฅํ•œ ํ•œ ์ž‘๊ณ  ๋‹จ์ˆœํ•˜๊ฒŒ ์œ ์ง€ํ•˜์„ธ์š”. ๋„ˆ๋ฌด ๋ณต์žกํ•œ Saga๋Š” ๊ด€๋ฆฌํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”๋ฅผ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ์„ฑ๋Šฅ๊ณผ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰์„ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋™๊ธฐํ™”ํ•  ํ•„์š”๋Š” ์—†์Šต๋‹ˆ๋‹ค.
  • ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ๋•Œ๋Š” ๋„คํŠธ์›Œํฌ ์ง€์—ฐ, ์žฅ์•  ์ƒํ™ฉ ๋“ฑ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. Axon Framework์˜ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ์ ๊ทน ํ™œ์šฉํ•˜์„ธ์š”.

Axon Framework๋ฅผ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•  ๋•Œ๋Š” ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ํŠน์„ฑ์„ ์ž˜ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. Axon Framework๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ ์ ˆํžˆ ํ™œ์šฉํ•˜๋ฉด, ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6. Axon Framework์˜ ๋ฏธ๋ž˜์™€ ์ „๋ง ๐Ÿ”ฎ

Axon Framework๋Š” ๊ณ„์†ํ•ด์„œ ๋ฐœ์ „ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์˜ ๋ฏธ๋ž˜ ํŠธ๋ Œ๋“œ๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์„น์…˜์—์„œ๋Š” Axon Framework์˜ ๋ฏธ๋ž˜ ์ „๋ง๊ณผ ์•ž์œผ๋กœ์˜ ๋ฐœ์ „ ๋ฐฉํ–ฅ์— ๋Œ€ํ•ด ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

6.1 ํด๋ผ์šฐ๋“œ ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์› ๊ฐ•ํ™” โ˜๏ธ

Axon Framework๋Š” ์ด๋ฏธ ํด๋ผ์šฐ๋“œ ํ™˜๊ฒฝ์—์„œ์˜ ์šด์˜์„ ์ง€์›ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์•ž์œผ๋กœ ๋”์šฑ ๊ฐ•ํ™”๋œ ํด๋ผ์šฐ๋“œ ๋„ค์ดํ‹ฐ๋ธŒ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Kubernetes์™€์˜ ํ†ตํ•ฉ์ด ๋”์šฑ ์‹ฌํ™”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


apiVersion: apps/v1
kind: Deployment
metadata:
  name: axon-application
spec:
  replicas: 3
  selector:
    matchLabels:
      app: axon-application
  template:
    metadata:
      labels:
        app: axon-application
    spec:
      containers:
      - name: axon-application
        image: your-axon-application:latest
        ports:
        - containerPort: 8080
        env:
        - name: AXON_AXONSERVER_SERVERS
          value: axonserver-0.axonserver:8124,axonserver-1.axonserver:8124,axonserver-2.axonserver:8124

ย 

์ด๋Ÿฌํ•œ Kubernetes ์„ค์ •์„ ํ†ตํ•ด Axon ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์‰ฝ๊ฒŒ ๋ฐฐํฌํ•˜๊ณ  ์Šค์ผ€์ผ๋งํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6.2 ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํ†ตํ•ฉ ๊ฐ•ํ™” โšก

Axon Framework๋Š” ์ด๋ฏธ ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์„ ์ง€์›ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์•ž์œผ๋กœ ๋”์šฑ ๊ฐ•ํ™”๋œ ํ†ตํ•ฉ์ด ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ๋ชจ๋“  API๊ฐ€ ๋ฐ˜์‘ํ˜•์œผ๋กœ ์ œ๊ณต๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@RestController
@RequestMapping("/users")
public class UserController {
    private final ReactiveCommandGateway commandGateway;
    private final ReactiveQueryGateway queryGateway;

    @PostMapping
    public Mono<String> createUser(@RequestBody CreateUserCommand command) {
        return commandGateway.send(command);
    }

    @GetMapping("/{userId}")
    public Mono<UserDTO> getUser(@PathVariable String userId) {
        return queryGateway.query(new GetUserQuery(userId), UserDTO.class);
    }
}

ย 

์ด๋Ÿฌํ•œ ๋ฐ˜์‘ํ˜• API๋ฅผ ํ†ตํ•ด ๋”์šฑ ํšจ์œจ์ ์ด๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6.3 AI์™€์˜ ํ†ตํ•ฉ ๐Ÿค–

์ธ๊ณต์ง€๋Šฅ๊ณผ ๋จธ์‹ ๋Ÿฌ๋‹์˜ ๋ฐœ์ „์— ๋”ฐ๋ผ, Axon Framework๋„ ์ด๋Ÿฌํ•œ ๊ธฐ์ˆ ๊ณผ์˜ ํ†ตํ•ฉ์„ ๊ฐ•ํ™”ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ ๋ถ„์„ํ•˜์—ฌ ์ด์ƒ ์ง•ํ›„๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@Component
public class AnomalyDetector {
    private final EventProcessor eventProcessor;
    private final AnomalyDetectionModel model;

    @EventHandler
    public void on(DomainEvent event) {
        if (model.detectAnomaly(event)) {
            eventProcessor.publishEvent(new AnomalyDetectedEvent(event));
        }
    }
}

ย 

์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์‹œ์Šคํ…œ์˜ ์ด์ƒ ๋™์ž‘์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6.4 ๋ณด์•ˆ ๊ฐ•ํ™” ๐Ÿ”’

๋ณด์•ˆ์˜ ์ค‘์š”์„ฑ์ด ๊ณ„์†ํ•ด์„œ ์ฆ๊ฐ€ํ•จ์— ๋”ฐ๋ผ, Axon Framework๋„ ๋”์šฑ ๊ฐ•ํ™”๋œ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด๋ฒคํŠธ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•œ ๋ธ”๋ก์ฒด์ธ ๊ธฐ์ˆ ์˜ ๋„์ž… ๋“ฑ์ด ๊ณ ๋ ค๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@Configuration
public class AxonConfig {
    @Bean
    public EventStorageEngine eventStorageEngine(BlockchainService blockchainService) {
        return new BlockchainEventStorageEngine(blockchainService);
    }
}

ย 

์ด๋Ÿฌํ•œ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ์ด๋ฒคํŠธ์˜ ์œ„๋ณ€์กฐ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ  ์‹œ์Šคํ…œ์˜ ์‹ ๋ขฐ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

6.5 ๋‹ค์–‘ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด ์ง€์› ๐ŸŒ

ํ˜„์žฌ Axon Framework๋Š” ์ฃผ๋กœ Java ์ƒํƒœ๊ณ„๋ฅผ ๋Œ€์ƒ์œผ๋กœ ํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์•ž์œผ๋กœ๋Š” ๋‹ค์–‘ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด๋ฅผ ์ง€์›ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, Kotlin์— ๋Œ€ํ•œ ์ง€์›์ด ๋”์šฑ ๊ฐ•ํ™”๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:


@Aggregate
class UserAggregate {
    @AggregateIdentifier
    private lateinit var userId: String

    @CommandHandler
    constructor(command: CreateUserCommand) {
        apply(UserCreatedEvent(command.userId, command.username))
    }

    @EventSourcingHandler
    fun on(event: UserCreatedEvent) {
        userId = event.userId
    }
}

ย 

์ด๋ฅผ ํ†ตํ•ด ๋” ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด Axon Framework๋ฅผ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๐ŸŒŸ Axon Framework์˜ ๋ฏธ๋ž˜ ์ „๋ง

  • ํด๋ผ์šฐ๋“œ ๋„ค์ดํ‹ฐ๋ธŒ ํ™˜๊ฒฝ์— ๋”์šฑ ์ตœ์ ํ™”๋œ ๊ธฐ๋Šฅ ์ œ๊ณต
  • ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจ๋Ÿฌ๋‹ค์ž„์˜ ์ „๋ฉด์  ๋„์ž…
  • AI์™€ ๋จธ์‹ ๋Ÿฌ๋‹ ๊ธฐ์ˆ ๊ณผ์˜ ํ†ตํ•ฉ์„ ํ†ตํ•œ ์ง€๋Šฅํ˜• ์‹œ์Šคํ…œ ๊ตฌ์ถ• ์ง€์›
  • ๋ธ”๋ก์ฒด์ธ ๋“ฑ ์ตœ์‹  ๋ณด์•ˆ ๊ธฐ์ˆ ์˜ ๋„์ž…์„ ํ†ตํ•œ ์‹œ์Šคํ…œ ์‹ ๋ขฐ์„ฑ ๊ฐ•ํ™”
  • ๋‹ค์–‘ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด ์ง€์›์„ ํ†ตํ•œ ์ƒํƒœ๊ณ„ ํ™•์žฅ

Axon Framework๋Š” ๊ณ„์†ํ•ด์„œ ์ง„ํ™”ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ์˜ ์ตœ์‹  ํŠธ๋ Œ๋“œ๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๋“ค์€ ์ด๋Ÿฌํ•œ ๋ณ€ํ™”๋ฅผ ์ฃผ์‹œํ•˜๊ณ  , ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ๊ณผ ๊ฐœ์„ ์‚ฌํ•ญ์„ ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ ๋”์šฑ ๊ฐ•๋ ฅํ•˜๊ณ  ์œ ์—ฐํ•œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค. Axon Framework์˜ ๋ฏธ๋ž˜๋Š” ๋ถ„์‚ฐ ์‹œ์Šคํ…œ, ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ์•„ํ‚คํ…์ฒ˜, ๊ทธ๋ฆฌ๊ณ  ๋„๋ฉ”์ธ ์ฃผ๋„ ์„ค๊ณ„์˜ ์›์น™์„ ๋”์šฑ ํšจ๊ณผ์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ๋‚˜์•„๊ฐˆ ๊ฒƒ์œผ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค.

7. ๊ฒฐ๋ก  ๋ฐ ์š”์•ฝ ๐Ÿ“

์ง€๊ธˆ๊นŒ์ง€ ์šฐ๋ฆฌ๋Š” Axon Framework์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค. Axon Framework๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ฃผ์š” ๋‚ด์šฉ์„ ์š”์•ฝํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • Axon Framework๋Š” DDD, CQRS, ์ด๋ฒคํŠธ ์†Œ์‹ฑ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ํŠนํ™”๋œ Java ๊ธฐ๋ฐ˜์˜ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.
  • Command, Event, Query์˜ ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•ด ์‹œ์Šคํ…œ์˜ ์ฑ…์ž„์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜๊ณ , ํ™•์žฅ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Aggregate๋ฅผ ํ†ตํ•ด ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ํšจ๊ณผ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Saga๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Event Sourcing์„ ํ†ตํ•ด ์‹œ์Šคํ…œ์˜ ๋ชจ๋“  ์ƒํƒœ ๋ณ€ํ™”๋ฅผ ์ถ”์ ํ•˜๊ณ , ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๊ณผ๊ฑฐ ์ƒํƒœ๋ฅผ ์žฌ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • Axon Framework๋Š” ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜, ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๋™๊ธฐํ™”, ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๋“ฑ ๋‹ค์–‘ํ•œ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ํ™œ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋ฏธ๋ž˜์—๋Š” ํด๋ผ์šฐ๋“œ ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์› ๊ฐ•ํ™”, ๋ฐ˜์‘ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํ†ตํ•ฉ, AI์™€์˜ ๊ฒฐํ•ฉ ๋“ฑ์„ ํ†ตํ•ด ๋”์šฑ ๋ฐœ์ „ํ•  ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋ฉ๋‹ˆ๋‹ค.

Axon Framework๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ณต์žกํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋”์šฑ ํšจ๊ณผ์ ์œผ๋กœ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋ชจ๋“  ๋„๊ตฌ๊ฐ€ ๊ทธ๋ ‡๋“ฏ, Axon Framework๋„ ์ ์ ˆํ•œ ์ƒํ™ฉ์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ์˜ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ํŒ€์˜ ์—ญ๋Ÿ‰์„ ๊ณ ๋ คํ•˜์—ฌ Axon Framework์˜ ๋„์ž…์„ ๊ฒฐ์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์•ž์œผ๋กœ Axon Framework๋Š” ๊ณ„์†ํ•ด์„œ ๋ฐœ์ „ํ•˜๊ณ  ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๋“ค์€ ์ด๋Ÿฌํ•œ ๋ณ€ํ™”๋ฅผ ์ฃผ์‹œํ•˜๊ณ , ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ํ•™์Šตํ•˜๋ฉฐ, ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด ๋‚˜๊ฐ€์•ผ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. Axon Framework๋ฅผ ํ†ตํ•ด ๋” ๋‚˜์€ ์†Œํ”„ํŠธ์›จ์–ด๋ฅผ ๋งŒ๋“ค์–ด ๋‚˜๊ฐ€๋Š” ์—ฌ์ •์— ์—ฌ๋Ÿฌ๋ถ„์„ ์ดˆ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ’ก ๋งˆ์ง€๋ง‰ ์กฐ์–ธ

  • Axon Framework๋ฅผ ๋„์ž…ํ•  ๋•Œ๋Š” ํŒ€ ์ „์ฒด๊ฐ€ DDD, CQRS, ์ด๋ฒคํŠธ ์†Œ์‹ฑ ๋“ฑ์˜ ๊ฐœ๋…์„ ์ž˜ ์ดํ•ดํ•˜๊ณ  ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋ ค ํ•˜์ง€ ๋ง๊ณ , ์ ์ง„์ ์œผ๋กœ ๋„์ž…ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
  • Axon Framework์˜ ๊ณต์‹ ๋ฌธ์„œ์™€ ์ปค๋ฎค๋‹ˆํ‹ฐ๋ฅผ ์ ๊ทน ํ™œ์šฉํ•˜์„ธ์š”. ๋งŽ์€ ์ •๋ณด์™€ ๋„์›€์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•  ๋•Œ๋Š” ์ถฉ๋ถ„ํ•œ ํ…Œ์ŠคํŠธ์™€ ๋ชจ๋‹ˆํ„ฐ๋ง ์ „๋žต์„ ์ˆ˜๋ฆฝํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  • Axon Framework๋Š” ๋„๊ตฌ์ผ ๋ฟ, ์ข‹์€ ์„ค๊ณ„์™€ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋Œ€์ฒดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ํ•ญ์ƒ ๊ธฐ๋ณธ์— ์ถฉ์‹คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

Axon Framework๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๊ด€๋ฆฌํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์ž…๋‹ˆ๋‹ค. ์ด ๊ธ€์„ ํ†ตํ•ด Axon Framework์— ๋Œ€ํ•œ ์ดํ•ด๋ฅผ ๋†’์ด๊ณ , ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ธ์‚ฌ์ดํŠธ๋ฅผ ์–ป์œผ์…จ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. ์•ž์œผ๋กœ์˜ ์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ ์—ฌ์ •์—์„œ Axon Framework๊ฐ€ ์—ฌ๋Ÿฌ๋ถ„์—๊ฒŒ ํฐ ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๊ธฐ๋Œ€ํ•ฉ๋‹ˆ๋‹ค.

์ฐธ๊ณ  ์ž๋ฃŒ ๐Ÿ“š

์ด ๊ธ€์—์„œ ๋‹ค๋ฃฌ ๋‚ด์šฉ๋“ค์„ ๋” ๊นŠ์ด ์ดํ•ดํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด, ์œ„์˜ ์ž๋ฃŒ๋“ค์„ ์ฐธ๊ณ ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค. ํŠนํžˆ Axon Framework ๊ณต์‹ ๋ฌธ์„œ๋Š” ์ตœ์‹  ์ •๋ณด์™€ ์ž์„ธํ•œ ์‚ฌ์šฉ๋ฒ•์„ ์ œ๊ณตํ•˜๋ฏ€๋กœ, ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•  ๋•Œ ํฐ ๋„์›€์ด ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.