Temporal.io 개발/운영 팁
데이터 오케스트레이션 툴로 Temporal를 운영하면서 얻은 팁들을 정리한다. Temporal의 기본적인 구조는 알고 있어야 이해가 가능한 글이다.
Temporal의 목적은 작기적으로 실행되는 프로세스를 안정적으로 완료하는 것이다. 이를 중심으로 팁들이 구성된다.
1. 엑티비티는 최대한 작게 만든다. 하나의 일만 하게 한다.
Temporal은 “장기적으로 실행되는 프로세스를 안정적으로 완료”하는게 목적이다보니 재실행 프로세스가 정말 안정적이다. 어떠한 장애가 발생하더라도 가장 마지막에 엑티비티 바로 다음부터 실행된다. 그렇기때문에 하나의 엑티비티가 하는 일이 많아진다면 중복실행될 확률이 높아진다. 예를 들어 3가지 일을 하는 엑티비티A가 존재할 때, 엑티비티A 진행 과정에서 2개의 일을 완료하고 1개의 일이 남았을 때 장애가 발생한다면 Temporal은 다시 엑티비티A부터 실행하기때문에 이미 완료한 2개의 일을 다시 실행하고 나머지 하나의 일을 실행하게 된다.
2. 중복 실행 여부를 확인하는 코드는 최소화한다.
첫번째 팁의 연장선이다. 하나의 엑티비티가 하나의 일만하는 구조가 아니라면 어쩔수 없이 엑티비티 내부의 작업들이 이미 실행됐는지 확인하는 코드를 추가하게 된다. Temporal는 순서를 보장해주는 게 최대 장점이기때문에 엑티비티를 최대한 작게 만들어 중복실행 확인 코드를 최소화하자. 이게 Temporal 장점을 극대화하는 일이다. 그리고 “중복확인을 하는 코드 하나정도야 추가해도 되지않나? 함수 실행전에 한줄 추가하면 되는거 아닌가?” 생각할수 있다. 하지만 중복 확인 코드가 하나둘 쌓이다보면 어느새 코드 유지보수가 힘들어지고 전체 흐름을 파악하는게 힘들어진다.
다만 정말 중요한 데이터여서 특정조건에만 단 한번 실행되거나 만들어져야 하는 것이라면 중복 코드를 추가하는 게 좋다.
중복 코드가 많아지면 Temporal를 이용하는 장점이 거의 사라진다고 할수 있다. Kafka나 RabbitMQ로 이벤트 하나하나 실행시키는 것과 크게 다를 바가 없기때문이다. 큰장점이 없는데 새로운 기술 스택을 하나 추가하는 건 큰 낭비다.
3. 워크플로우의 입력값은 DTO를 이용하여 구조화 하고 순수 객체로 만들어야 한다.
Temporal의 워크플로우간의 데이터 전달, 엑티비티간의 데이터 전달에 데이터를 직렬화/역직렬화한다. 그래서 전달 값으로 객체를 이용하면 원하는대로 전달이 안되는 경우가 있다. 메서드는 당연히 전달안된다. 정말 값만을 DTO로 구조화하여 전달해야한다. 특히나 Nest.js에서 이용하는 데코레이터들을 이용하면 의도한대로 작동하지않기때문에 이용하지않아야한다.
4. 워크플로우 체인을 만든다면 단방향으로 구조화하는게 편하다.
기능을 구현하다보면 워크플로우가 다른 워크플로우를 실행하는 구조가 만들어지게 된다. 기눙 재활용이 가능해져서 좋긴하지만 만들다보면 순환적으로 워크플로우를 실행하는 구조가 생기게 된다. 알맞은 상황에 체인을 끊어낼수만 있다면 문제가 되지않지만, 이 또한 유지보수를 어렵게 만들기때문에 지양하는게 좋다. 워크플로우 체인이 필요하다면 하나의 메인 워크플로우를 두고 해당 메인 워크플로우가 서브 워크플로우들을 실행하는 구조로 짜는게 훨씬 유지보수에 편리하다. 그리고 재귀적으로 워크플로우를 실행해야할 때가 있다. 그럴 때는 내 워크플로우를 다시 실행하는 것보다 while문으로 실행하는게 더 모니터링하기 편하다(이건 개인마다 다를수 있으니 만들어보면서 본인의 방식을 찾는게 좋을듯함.)
5. 워크플로우 코드를 구성할 때 엑티비티 실행이 분기되면 에러가 발생한다.
처음 만들 때 겪게 되는 문제라 미리 알면 코드를 바꾸지않을수 있어서 좋지않을까해서 적는다.
Temporal은 “determinism”이란 단어를 강조한다. “결정성”이라고 하는데 “같은 입력이 주어졌을 때 항상 동일한 실행 흐름과 결과가 나오는 성질을 말합니다.” 이라고 한다. 그래서 꼭 같은 순서로 실행되어야 하고 워크플로우안에서는 조건에 따라 분기로 다른 실행 흐름이 생기면 에러가 발생한다.
1
2
3
4
5
6
7
8
9
export async function SendNotificationWorkflow(input: { type: string }) {
const user = await findUserToSendNotification(someCondition);
if (input.type === 'email') {
await sendEmail(); // 조건에 따라 실행
} else {
await logToSlack(); // 또는 이쪽 실행
}
}
워크플로우 코드에서는 최대한 간결하게 엑티비티를 순서대로 실행하는 코드만 남겨놓아야한다. 분기는 엑티비티에서 한다. 아래 코드 확인.
1
2
3
4
5
6
7
8
9
10
11
12
13
export async function SendNotificationWorkflow(input: { type: string }) {
const user = await findUserToSendNotification(someCondition);
await SendNotificationActivity(user);
}
export async function SendNotificationActivity(user) {
if (user.type == "something") {
await sendEmail(); // 조건에 따라 실행
} else {
await logToSlack(); // 또는 이쪽 실행
}
}