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์ ๊ด๊ณ๋ฅผ ๋ณด์ฌ์ค๋๋ค. ์ด๋ค์ 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๋ก ์ ๋ฌ๋๊ณ , 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๋ 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 ๊ณต์ ๋ฌธ์: https://docs.axoniq.io/reference-guide/
- Axon Framework GitHub: https://github.com/AxonFramework/AxonFramework
- "Implementing Domain-Driven Design" by Vaughn Vernon
- "CQRS Journey" by Microsoft patterns & practices team
- "Microservices Patterns" by Chris Richardson
์ด ๊ธ์์ ๋ค๋ฃฌ ๋ด์ฉ๋ค์ ๋ ๊น์ด ์ดํดํ๊ณ ์ถ๋ค๋ฉด, ์์ ์๋ฃ๋ค์ ์ฐธ๊ณ ํ์๊ธฐ ๋ฐ๋๋๋ค. ํนํ Axon Framework ๊ณต์ ๋ฌธ์๋ ์ต์ ์ ๋ณด์ ์์ธํ ์ฌ์ฉ๋ฒ์ ์ ๊ณตํ๋ฏ๋ก, ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ ๋ ํฐ ๋์์ด ๋ ๊ฒ์ ๋๋ค.
๊ด๋ จ ํค์๋
- ์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์ฌ๋ฅ๋ท์ ์ต์ AI ๊ธฐ์ ๊ณผ ๋ฒ๋ฅ ์ ๊ธฐ๋ฐํ์ฌ ์์ฌ์ ์ง์ ์ฌ์ฐ๊ถ์ ์ ๊ทน์ ์ผ๋ก ๋ณดํธํ๋ฉฐ,
๋ฌด๋จ ์ฌ์ฉ ๋ฐ ์นจํด ํ์์ ๋ํด ๋ฒ์ ๋์์ ํ ๊ถ๋ฆฌ๋ฅผ ๋ณด์ ํฉ๋๋ค.
ยฉ 2025 ์ฌ๋ฅ๋ท | All rights reserved.
๋๊ธ 0๊ฐ