본문으로 건너뛰기
ElastiCORE 3.0 은 릴리스 후보(RC)입니다: Maven Central 의 3.0.261022-rc1. 2.2 라인 프로젝트는 2.2 문서를 보세요.
버전: 3.0

새 파일 만들기

규칙은 이미 있는 파일을 고치는 것뿐 아니라 새 종류의 파일도 만들 수 있습니다. 예제: 도메인 이벤트를 메시지 브로커로 보낼 때 이벤트마다 토픽 이름이 필요합니다. 코드 곳곳에 문자열로 흩지 않고, DSL 에 한 번 적어 상수 클래스로 생성합니다.

model/entity.yml
  PurchaseOrder:
meta: entity @root @expose(20) @event(Placed(total), Shipped, Cancelled) @topic(petstore.order) @owner(customerId)

생성되는 파일 - PetstoreEventTopics.java:

package io.elasticore.sample.petstore.domain.event;

import java.util.HashMap;
import java.util.Map;

/**
* PetstoreEventTopics - where each domain event of the domain is published.
*
* Generated from @topic by the pet store rules. Change the DSL, not this file.
*/
public final class PetstoreEventTopics {

public static final String PURCHASE_ORDER_PLACED = "petstore.order.placed";
public static final String PURCHASE_ORDER_SHIPPED = "petstore.order.shipped";
public static final String PURCHASE_ORDER_CANCELLED = "petstore.order.cancelled";

private static final Map<String, String> TOPICS = new HashMap<>();

static {
TOPICS.put("io.elasticore.sample.petstore.domain.event.PurchaseOrderPlacedEvent", PURCHASE_ORDER_PLACED);
TOPICS.put("io.elasticore.sample.petstore.domain.event.PurchaseOrderShippedEvent", PURCHASE_ORDER_SHIPPED);
TOPICS.put("io.elasticore.sample.petstore.domain.event.PurchaseOrderCancelledEvent", PURCHASE_ORDER_CANCELLED);
}

private PetstoreEventTopics() {
}

/** The topic of an event, by its class name as the outbox records it; null when it has none. */
public static String topicOf(String eventType) {
return TOPICS.get(eventType);
}
}

새 파일에는 세 가지가 필요합니다.

무엇어디
1파일 선언 - 어느 도메인·엔티티에, 어떤 이름으로, 어느 패키지에룰셋이 DomainArtifactContributor/ArtifactContributor 구현
2파일의 뼈대 - 고정된 부분과 ${빈칸}스켈레톤 elasticore-template/v3/<target>.tmpl
3빈칸 채우기 - 모델에서 읽어@Rule(target = "<target>") 규칙

1. 파일 선언​

생성기는 새 파일 종류를 미리 알지 못합니다. 룰셋이 "이 도메인에는 이런 파일이 하나 있다" 고 알려 줍니다.

파일이구현할 인터페이스메서드채우는 규칙의 모양
도메인마다 하나DomainArtifactContributorartifactsFor(DomainSpec d)(DomainSpec d)
엔티티마다 하나ArtifactContributorartifactsFor(EntitySpec e)(EntitySpec e)
public class PetstoreRules implements CodeRules, DomainArtifactContributor {

@Override
public List<ContributedArtifact> artifactsFor(DomainSpec d) {
for (EntitySpec e : d.entities()) {
if (topicOf(e) != null && !e.events().isEmpty()) {
return Collections.singletonList(
ContributedArtifact.of("eventTopics", "event", className(d)).orNamespace("entity"));
}
}
return Collections.emptyList(); // no @topic in this domain: no file
}

ContributedArtifact.of("eventTopics", "event", "PetstoreEventTopics"):

값뜻
"eventTopics"새 target 이름. 이 파일을 채울 규칙의 target, 그리고 스켈레톤 파일 이름(eventTopics.tmpl)
"event"파일이 놓일 패키지 키 (env.yml 의 namespace). namespace: base: io.elasticore.sample.petstore 면 …domain.event
"PetstoreEventTopics"클래스(파일) 이름
.orNamespace("entity")event 키가 없는 프로젝트(레이어드)에서 대신 쓸 키

namespace: base: … 가 만들어 주는 키:

키패키지
entity, enumeration…domain.model
event…domain.event
domainService, domainRepository…domain.service, …domain.repository
service, dto…application, …application.dto
inboundPort / queryPort, port…application.port.in / …application.port.out
repository, persistence…infrastructure.persistence
control…adapter.in.web

우리 파일만의 자리가 필요하면 namespace 에 한 줄 더 쓰고(messaging: io.elasticore.sample.petstore.infrastructure.messaging) 그 키를 씁니다. 조건이 맞지 않으면 빈 목록을 돌려줍니다 - 파일도 없습니다.

2. 스켈레톤​

규칙 모듈의 src/main/resources/elasticore-template/ 아래, 언어별 폴더에 target 이름으로 둡니다.

언어경로
Javaelasticore-template/v3/eventTopics.tmpl
Kotlinelasticore-template/kt/eventTopics.tmpl
ps-rules/src/main/resources/elasticore-template/v3/eventTopics.tmpl
package ${packageName};

import java.util.HashMap;
import java.util.Map;

/**
* ${className} - where each domain event of the domain is published.
*
* Generated from @topic by the pet store rules. Change the DSL, not this file.
*/
public final class ${className} {

${constantList}

private static final Map<String, String> TOPICS = new HashMap<>();

static {
${entryList}
}

private ${className}() {
}

/** The topic of an event, by its class name as the outbox records it; null when it has none. */
public static String topicOf(String eventType) {
return TOPICS.get(eventType);
}
}
  • ${packageName} 은 규칙이 채우지 않습니다. 생성기가 artifactsFor 에서 고른 패키지를 미리 넣으므로 파일 위치와 package 줄이 어긋날 수 없습니다.
  • 채우지 않은 ${빈칸} 은 오류가 아니라 빈 줄이 됩니다. 이름 오타는 테스트의 unfilledSlots() 로 잡습니다.
  • ${importList} 나 ${classAnnotationList} 를 한 줄로 두면 규칙의 addImport(...)·ensureClassAnnotation(...) 이 그 자리에 들어갑니다. 위 예는 import 가 고정이라 직접 적었습니다.

3. 빈칸 채우기​

    @Rule(value = "ps-event-topics", target = "eventTopics")
void eventTopics(DomainSpec d) {
SourceCode c = ctx(d);
c.requireSyntaxAnyOf("ps-event-topics", Syntax.JAVA, Syntax.KOTLIN);
boolean kotlin = c.syntax() == Syntax.KOTLIN;

for (EntitySpec e : d.entities()) {
String topic = topicOf(e);
if (topic == null) continue;
for (String event : e.events()) {
String constant = upperSnake(e.getName() + event);
String eventClass = e.eventPackage() + "." + e.eventClassName(event);
if (kotlin) {
c.varLine("constantList", " const val %s = \"%s.%s\"", constant, topic, kebab(event));
c.varLine("entryList", " \"%s\" to %s,", eventClass, constant);
} else {
c.varLine("constantList", " public static final String %s = \"%s.%s\";", constant, topic, kebab(event));
c.varLine("entryList", " TOPICS.put(\"%s\", %s);", eventClass, constant);
}
}
}
// ${packageName} is already the package artifactsFor chose.
c.var("className", className(d));
}

/** @topic(petstore.order), with or without quotes. */
private static String topicOf(EntitySpec e) {
String v = e.metaValue("topic");
if (v == null) return null;
v = v.replace("\"", "").replace("'", "").trim();
return v.isEmpty() ? null : v;
}

private static String className(DomainSpec d) {
return d.className() + "EventTopics";
}

/** PurchaseOrderShipped -> PURCHASE_ORDER_SHIPPED */
private static String upperSnake(String name) {
return name.replaceAll("([a-z0-9])([A-Z])", "$1_$2").toUpperCase();
}

/** PartiallyShipped -> partially-shipped */
private static String kebab(String name) {
return name.replaceAll("([a-z0-9])([A-Z])", "$1-$2").toLowerCase();
}
코드뜻
c.var("className", v)${className} 을 채움
c.varLine("constantList", fmt, args…)${constantList} 자리에 줄을 하나씩 더함
e.events()@event 이름들: [Placed, Shipped, Cancelled]
e.eventClassName(ev), e.eventPackage()생성된 이벤트 클래스: PurchaseOrderShippedEvent, …domain.event
requireSyntaxAnyOf(...)이 규칙이 쓸 수 있는 언어. TypeScript 같은 다른 언어 생성에 쓰이면 이상한 파일을 조용히 만드는 대신 그 파일을 실패로

새 파일의 줄은 규칙이 직접 쓰므로 여기서는 언어로 나눕니다. 어노테이션·import 처럼 생성기가 쓰는 것에는 이 분기가 필요 없습니다.

생성 로그:

    eventTopics  src/main/java/io/elasticore/sample/petstore/domain/event/PetstoreEventTopics.java  WROTE

-PlistRules 에는 새 target 이 그 규칙과 함께 나옵니다:

  eventTopics
0 once ps-event-topics PetstoreRules

누가 이 파일을 관리하나​

스켈레톤의 첫 줄로 정합니다.

스켈레톤 첫 줄생성된 파일은
(표시 없음) - 위 예제생성기 것. DSL 이 바뀌면 다시 씀. 손으로 고치면 건너뛰고 알림
// Initially generated by ElastiCORE; no longer managed - do not remove this line.개발자 것. 처음 만든 뒤 고치면 그대로 둠 (채워 넣을 틀에)

Kotlin​

같은 룰셋이 Kotlin 프로젝트(language = 'kotlin')에서도 돕니다. 새 파일만은 Kotlin 스켈레톤이 따로 있어야 합니다 - 없으면 Java 스켈레톤으로 떨어지지 않고 그 파일이 실패(ERROR)로 남습니다.

ps-rules/src/main/resources/elasticore-template/kt/eventTopics.tmpl
package ${packageName}

/**
* ${className} - where each domain event of the domain is published.
*
* Generated from @topic by the pet store rules. Change the DSL, not this file.
*/
object ${className} {

${constantList}

private val TOPICS: Map<String, String> = mapOf(
${entryList}
)

/** The topic of an event, by its class name as the outbox records it; null when it has none. */
@JvmStatic
fun topicOf(eventType: String): String? = TOPICS[eventType]
}

엔티티마다 파일 하나​

도메인에 하나가 아니라 루트마다 하나(PurchaseOrderEvents)를 만들려면 ArtifactContributor 를 구현합니다:

public class PerRootRules implements CodeRules, ArtifactContributor {

@Override
public List<ContributedArtifact> artifactsFor(EntitySpec e) {
if (!e.aggregateRoot() || e.events().isEmpty()) return Collections.emptyList();
return Collections.singletonList(
ContributedArtifact.of("rootEvents", "event", e.getName() + "Events").orNamespace("entity"));
}

@Rule(value = "ps-root-events", target = "rootEvents")
void rootEvents(EntitySpec e) {
SourceCode c = ctx(e);
for (String event : e.events()) {
c.varLine("eventList", " %s.class,", e.eventClassName(event));
}
c.var("className", e.getName() + "Events");
}
}

스켈레톤은 같은 방식으로 elasticore-template/v3/rootEvents.tmpl. 룰셋은 여러 개여도 됩니다 - 빌드의 rules = [...] 에 더하세요.

기본 스켈레톤을 바꾸고 싶다면​

ElastiCORE 의 .tmpl 을 통째로 바꾸지 마세요. 버전이 오를 때마다 따라가야 합니다. 대신 그 파일을 끄고 (끄기) 새 파일로 만드세요.

생성된 클래스 쓰기​

PetstoreEventTopics 는 Outbox 와 함께 씁니다(env.yml 의 outbox: true, 메인 클래스에 @EnableScheduling). OutboxPublisher 를 구현해 토픽을 고릅니다:

@Component
public class TopicOutboxPublisher implements OutboxPublisher {

@Override
public void publish(OutboxEnvelope message) {
String topic = PetstoreEventTopics.topicOf(message.getEventType());
if (topic == null) throw new IllegalStateException("No topic for " + message.getEventType());
kafkaTemplate.send(topic, message.getAggregateId(), message.getPayload());
}
}

Outbox 의 동작(재시도, 순서 보장)은 파생 값과 이벤트에 있습니다.