1. 서론

 

Software 개발 및 유지보수 단계에서 요구사항에 의하여 데이터 모델은 변하기 마련입니다. 그리고 바뀌는 데이터모델에 맞춰 Event 또한 형태가 변합니다. 이때, 이전 발행된 Event와 앞으로 적재되는 Event의 형태는 다르게 됩니다. 따라서 Replay 과정에서 변경된 Event를 적용하는데 있어 문제가 발생되지 않도록 코드를 통한 중재가 필요합니다. Axon 에서는 이를 위해 Event Upcasting 기능을 제공합니다. 이번 포스팅에서는 코드를 통하여 Event Upcasting을 적용하는 방법을 알아보겠습니다.


2. Versioning

 

 

Application 개발 후 요구사항 변경으로 계정 가입시에 회사명 정보가 추가되며, Read Model MView(Materialized View)에도 회사명 정보가 포함되고, 회사명이 기입되지 않으면 N/A로 표시된다고 가정하겠습니다.

 

요구사항 변경 내용을 코드로 구현하면 다음과 같습니다.


1. Command 수정을 위해서 API 변경이 필요합니다. Command 모듈내 dto 패키지에 위치한 HolderDTO 파일을 엽니다.

 


2. DTO 클래스에 company 정보를 추가합니다.

 

HolderDTO.java

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class HolderDTO {
    private String holderName;
    private String tel;
    private String address;
    private String company;
}

3. Common 모듈 commands 패키지내 HolderCreationCommand 파일을 엽니다.

 


4. Command 클래스에 company 정보를 추가합니다.

 

HolderCreationCommand.java

@AllArgsConstructor
@ToString
@Getter
public class HolderCreationCommand {
    @TargetAggregateIdentifier
    private String holderID;
    private String holderName;
    private String tel;
    private String address;
    private String company;
}

5. Command 모듈 service 패키지에 위치한 TransactionServiceImpl 파일을 엽니다.

 


6. Service 클래스에서 계정 생성 로직에 Company 정보를 넘기도록 수정합니다.

 

TransactionServiceImpl.java

@Service
@RequiredArgsConstructor
public class TransactionServiceImpl implements TransactionService {
	(...중략...)
    @Override
    public CompletableFuture<String> createHolder(HolderDTO holderDTO) {
        return commandGateway.send(new HolderCreationCommand(UUID.randomUUID().toString()
                , holderDTO.getHolderName()
                , holderDTO.getTel()
                , holderDTO.getAddress()
                , holderDTO.getCompany())
                );
    }
    (...중략...)
}

 

7. Common 모듈의 build.gradle 파일을 엽니다.

 


8. build.gradle 파일에 axon-messaging 의존성을 추가합니다.

 

build.gradle

ext{
    axonVersion = "4.2.1"
}
bootJar { 
    enabled = false 
}
jar {
    enabled = true
}
dependencies{
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    implementation group: 'org.axonframework', name: 'axon-messaging', version: "$axonVersion"
}

9. Common 모듈 events 패키지 내 HolderCreationEvent 파일을 엽니다.

 


10. Event에 변경된 사항을 추가합니다. 이때 Event에는 변경이 발생하므로, Event의 변경이 발생했음을 알리는 마커가 필요합니다. 이때 사용되는 어노테이션이 @Revision입니다. Revision 표시를 통해서 실제 Event가 발생되었을 때 EventStore에는 해당 Event의 버전이 저장되며, 추후 Event Upcasting 시에 해당 정보가 사용됩니다.

 

HolderCreationEvent.java

@AllArgsConstructor
@ToString
@Getter
@Revision("1.0")
public class HolderCreationEvent {
    private String holderID;
    private String holderName;
    private String tel;
    private String address;
    private String company;
}

11. Command 모듈 aggregate 패키지내에 위치한 HolderAggregate 파일을 엽니다.

 


12. Aggregate 클래스 Event 발행 로직에 company 정보를 전달할 수 있도록 변경합니다.

 

HolderAggregate.java

@AllArgsConstructor
@NoArgsConstructor
@Aggregate
@Slf4j
public class HolderAggregate {
	(...중략...)
    @CommandHandler
    public HolderAggregate(HolderCreationCommand command) {
        log.debug("handling {}", command);

        apply(new HolderCreationEvent(command.getHolderID(), command.getHolderName(), command.getTel(), command.getAddress(), command.getCompany()));
    }
    (...중략...)
}

13. Query 모듈에 version 패키지를 생성한 다음 HolderCreationEventV1 클래스를 만듭니다. 해당 클래스는 변경 이전HolderCreationEvent에 대해서 변경 후에 어떻게 추가된 정보를 처리를 지정 용도로 사용됩니다.

 


14. HolderCreationEventV1 클래스 내용을 구현합니다.

 

HolderCreationEventV1.java

public class HolderCreationEventV1 extends SingleEventUpcaster {
    private static SimpleSerializedType targetType = new SimpleSerializedType(HolderCreationEvent.class.getTypeName(), null);

    @Override
    protected boolean canUpcast(IntermediateEventRepresentation intermediateRepresentation) {
        return intermediateRepresentation.getType().equals(targetType);
    }

    @Override
    protected IntermediateEventRepresentation doUpcast(IntermediateEventRepresentation intermediateRepresentation) {
        return intermediateRepresentation.upcastPayload(
                new SimpleSerializedType(targetType.getName(), "1.0"),
                org.dom4j.Document.class,
                document -> {
                    document.getRootElement()
                            .addElement("company")
                            .setText("N/A");
                    return document;
                }
        );
    }

 

코드 구현내역은 다음과 같습니다. 먼저 targetType에는 대상 Event를 지정합니다. 데모 프로젝트에서는 HolderCreationEvent가 변경되었으므로 해당 Class 타입을 지정합니다. 두번째 인자에 위치한 null 값은 최초에는 @Revision 정보를 명시하지 않았으므로 존재하는 값이 없기 때문에 null로 지정하였습니다.

 

doUpcast 메소드는 실제 Event Version을 확인하고 이전 버전의 Event가 들어왔을 때 행동해야할 내용을 기술합니다.

EventStore에 저장된 Event 내용이 XML로 지정되므로, XML로 되어있는 Payload 에서 신규 추가된 company 정보와 값이 없을 경우 입력될 Default 값 N/A를 setText 메소드를 통해 지정합니다. 또한 해당 작업을 수행할 대상을 Revision 정보가 null인 targetType으로 한정합니다. 따라서 해당 메소드를 통해서 확인할 수 있는 사실은 HolderCreationEvent에 수많은 Revision 정보가 존재하더라도, HolderEventCreationEventV1 클래스에서는 Revision 번호가 1.0과 null인 두 Event간의 속성값에만 영향을 미칩니다.

 

 


15. Query 모듈내 config 패키지에 존재하는 AxonConfig 파일을 엽니다.

 

 


14. AxonConfig 파일에 Event Chain을 등록합니다.

 

AxonConfig.java

@Configuration
public class AxonConfig {
	(...중략...)

    @Bean
    public EventUpcasterChain eventUpcasterChain(){
        return new EventUpcasterChain(
                new HolderCreationEventV1()
        );
    }
}

 

만약 Event의 버전이 여러개 생성되었다면, HolderCreationEventV1이외 여러개의 버전간 핸들러 클래스를 생성한 다음 Event Chain 생성 로직에 핸들러 인스턴스를 등록합니다.


15. 새로운 계정 등록 테스트를 진행한다음 Query Application을 Replay 시킵니다.

 

POST http://localhost:8080/holder
Content-Type: application/json

{
	"holderName" : "Kane",
	"tel" : "02-2645-5678",
	"address" : "OO시 OO구",
    "company" : "Korea"
}

 

c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=0df539ed-2ee2-41be-af32-0f6724a75da3, holderName=kevin, tel=02-1234-5678, address=OO시 OO구, company=N/A) , timestamp : 2020-01-07T12:11:21.047Z
c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=a5d267c6-fbd8-4f0d-b93b-03a8dbf09747, holderName=bruce, tel=02-5291-5678, address=OO시 OO구, company=N/A) , timestamp : 2020-01-07T12:12:14.238Z
c.c.q.p.HolderAccountProjection          : projecting HolderCreationEvent(holderID=1b77f5bc-73e6-4b7a-a1fd-d5453723b9c7, holderName=Kane, tel=02-2645-5678, address=OO시 OO구, company=Korea) , timestamp : 2020-01-11T13:00:10.487Z

 

위 내용은 Replay 이후 Application 로그 일부를 발췌한 내용입니다. 신규 생성한 Kane 사용자의 company는 입력값으로 정확히 등록이 되었으며, 기존에 발행된 Event는 N/A로 매핑된 것을 확인할 수 있습니다.


3. 마치며

 

이번 포스팅을 끝으로 Query Application에서 Event 처리와 관련된 내용을 마무리하겠습니다. 다음 포스팅에서는 Query Application에 저장된 Read Model을 기반으로 Query하는 방법에 대해서 다루도록 하겠습니다.

+ Recent posts