1. 동기 방식

프로그램을 개발할 때는 보통 작업을 위에서부터 아래로 순차적으로 진행하는 동기(synchronized) 방식을 떠올릴 수 있다. 동기 방식은 이전 작업이 끝날 때까지 다음 작업이 진행되지 않는다. 즉, 코드의 순서가 곧 실행 순서가 되는 것이다.

 

동기 방식은

  • 프로그램의 흐름을 직관적으로 이해할 수 있고,
  • 디버깅도 용이하고,
  • 툴을 사용하면 코드를 그대로 쫓아가면서 분석할 수 있다.

 

하지만 이 방식이 외부연동을 만날 경우, 고려해야할 것들이 생긴다.

  • 먼저 외부 연동 실패가 전체 기능의 실패인지,
  • 외부 연동 서비스의 응답시간은 어떻게 되는지 등(연동 서비스의 응답 시간이 길어질수록 전체 응답 시간이 느려지게 되므로)

 

따라서 작업을 진행하기 위해 반드시 외부 연동 결과가 필요한 게 아니라면, 동기 방식 대신 비동기 방식으로 연동하는 것을 고민해볼 필요가 있다.

 

2. 비동기 방식

비동기 방식은 이전 작업이 끝날 때까지 기다리지 않고 바로 다음 작업을 처리하는 것을 의미한다. 즉, 비동기 방식을 사용하면 외부 연동이 끝날 떼까지 기다리지 않고 바로 다음 작업을 진행할 수 있다. 이전 작업을 응답을 대기하는 시간을 기다리지 않아도 되므로 사용자에게 빠른 응답을 제공할 수 있다. 

 

생각보다 비동기 방식을 적용해도 되는 연동 서비스들이 많이 있다.

  •  푸시 서비스 연동 - 쇼핑몰에서 주문이 들어오면 판매자에게 푸시 보내기
  • 포인트 서비스 연동 - 학습을 완료하면 학생에게 포인트 지급
  • 검색 서비스 연동 - 컨텐츠를 등록할 때 검색 서비스에도 등록
  • SMS 발송 서비스 연동 - 인증 번호를 요청하면 SMS로 인증 메시지 발송

 

위 예시들에는 몇 가지 공통 특징이 있는데,

  1. 연동에 약간의 시차가 생겨도 문제가 되지 않는다.
    • 쇼핑몰 주문이 완료된 후 1분 뒤에 판매자에게 푸시가 나가도 판매에 지장이 없다, 등록된 컨텐츠가 검색 결과에 10초 뒤에 나타나도 컨텐츠 등록에 문제가 되지 않는다.
  2. 일부 기능은 실패했을 때 재시도가 가능하다.
    • 푸시 발송에 실패했을 경우 재시도를 통해 푸시가 발송될 수 있다, 학습 완료 포인트 지급 실패 시 몇 초 뒤에 다시 시도해 포인트 지급하면 된다, 인증 번호가 SMS로 오지 않으면 '다시 받기' 기능을 통해 인증 번호 재수신 가능하다
  3. 연동에 실패했을 때 나중에 수동으로 처리할 수 있는 기능도 있다.
    • 검색 서비스 연동에 실패해 컨텐츠가 검색에 노출되지 않을 때 문의 및 관리 툴을 사용해 연동할 수 있다
  4. 연동 실패를 무시해도 되는 경우가 있다. 
    • 주문이 들어왔을 때 판매자에게 푸시가 발송되지 않더라도 판매에는 큰 문제가 생기지 않는다.

 

책에 언급된 실제 비동기 적용 사례는 다음과 같다.

  • 포인트 지급 : 사용자가 미션을 달성하면 포인트를 지급하는데 포인트 서비스와의 연동을 비동기로 처리했다.
  • 주문 정보 동기화 : 주문 시스템에 생성된 주문 정보를 회원 관리 시스템에 반영할 때 비동기로 동기화했다.
  • 택배사에 집하 요청 : 회원이 쇼핑몰에서 물건을 주문하면 택배사에 집하 요청을 하는데, 비동기로 집하 요청 데이터를 전송했다.

 

비동기 방식은 다양한 방식으로 구현할 수 있는데, 그는 다음과 같다.

  • 별도 스레드로 실행하기
  • 메시징 시스템 이용하기
  • 트랜잭션 아웃박스 패턴 사용하기
  • 배치로 연동하기
  • CDC 이용하기

 

3. 별도 스레드로 실행하기

비동이 연동을 하는 가장 쉬운 방법은 별도 스레드로 실행하는 것이다. 

예를 들어, 푸시 서비스를 비동기로 연동하고 싶다면 새로운 스레드를 생성하여 연동하는  코드를 실행할 수 있다. 

// 1. 별도 스레드 이용해 비동기
public OrderResult placeOrder(OrderRequest req) {
  // 주문 생성 처리
  
  // 별도 스레드를 이용해서 푸시를 비동기로 발송
  new Thread(() -> pushClient.sendPush(pushData)).start();
  
  return successResult(...);  // 푸시 발송을 기다리지 않고 리턴
  }
  
  
// 2. 스레드 풀로 비동기
ExecutorService executor = Executors.newFixedThreadPool(50);
  
public OrderResult placeOrder(OrderRequest req) {
  // 주문 생성 처리
  
  // 스레드 풀을 이용해서 푸시를 비동기로 발송
  executor.submit((). -> pushClient.sendPush(pushData));
  
  return successResult(...);  // 푸시 발송을 기다리지 않고 리턴
}

// 3. 프레임워크가 제공하는 비동기 기능 사용
public class PushService {
  @Async
  public void sendPushAsync(PushData pushData) { // 푸시 알림 비동기로 전송
    pushClient.sendPush(pushData);
    //.. 기타 코드
    }
  }

 

스프링 프레임워크의 @Async 애노테이션을 사용할 때는 메서드에 비동기 실행과 관련된 단어를 추가하는 것이 좋다. Async가 붙지 않으면 해당 메서드가 비동기로 실행되는지 아닌지 알 수 없기 때문이다. 또 비동기로 실행될 경우 exception이 발생해도 catch 블록이 실행되지 않기에 예외 확인도 어렵다.

Q. 비동기로 실행될 때 catch 블록이 동작하지 않는 이유는 무엇일까?

 

주의할 점 - 트랜잭션 범위 안에서 비동기 코드를 실행

특히 트랜잭션 범위 안에서 비동기 코드를 실행할 때는 트랜잭션 연동 여부에 주의해야 한다. 블록 안에 있는 비동기가 exception을 전파하지 않아 롤백되어야 할 트랜잭션이 롤백 되지 않고 커밋될 수 있기 때문이다. 따라서 별도 스레드로 실행되는 코드는 내부 연동 과정에서 발생한 오류를 직접 처리해야 한다.

// 비동기로 실행되는 코드는
// 연동 과정에서 발생하는 오류를 직접 처리해야 한다.
@Async
public void sendPushAsync(PushDate pushData) {
  try {
    pushClient.senPush(pushData) {
  } catch(Exception e) {
  
    try {
      Thread.sleep(500);
    } catch(Exception ex) {}
    
    try { 
      pushClient.sendPush(pushData); // 재시도를 하거나
    } catch(Exception e1) {
      // 실패 로그를 남기거나 
      ...
    }
    
  }
}

 

4.1 메시징

서로 다른 시스템 간에 비동기로 연동할 때 주로 사용하는 방식은 메시징 시스템을 사용하는 것이다. 메시징 시스템은 아래 그림에서와 같이 데이터 연동이 필요한 두 시스템(시스템A, 시스템B) 사이에 위치한다.

이 시스템에선 다음과 같은 프로세스로 메시지가 전달된다.

  • 시스템A가 전달할 데이터로 메시지를 생성해 메시징 시스템에 전송하고,
  • 메시징 시스템은 메시지를 다시 시스템 B에 전달하고,
  • 시스템 B는 전달받은 데이터를 이용해 필요한 작업을 처리한다.

 

메시징 시스템 장점

  • 두 시스템이 서로 영향을 주지 않는다는 것이다.
    • 메시징 시스템은 시스템 A가 보낸 메시지를 일단 저장하고, 시스템 B의 성능에 맞게 메시지를 전달하는 중간에서 메시지를 보관하는 버퍼 역할을 한다. 
    • 다른 시스템에서 발생한 에러에 영향 받지 않을 수 있어 에러 발생 여부와 무관하게 내 시스템을 원활하게 동작시킬 수 있다.
  • 확장에 용이하다.
    • 시스템 A에 시스템 C를 연동하고 싶다면 그저 메시징 시스템에 시스템 C를 새로 연결하기만 하면 된다. 시스템 C를 추가하기 위해 시스템 A의 코드를 수정할 필요가 없다.

 

메시징 시스템 단점

  • 구조가 복잡해진다

 

생산자/소비자, 게시자/구독자

메시징 시스템에서 사용하는 용어로 생산자(Producer)와 소비자(Consumer), 게시자(Publisher)와 구독자(Subscriber)가 있다. 

  • 생산자 : 메시지를 생성해서 메시징 시스템에 보내는 측이고,
  • 소비자 : 메시징 시스템으로부터 메시지를 받아 처리하는 측이다.
  • 게시자 : 메시지를 생성/전송하는 측이고,
  • 구독자 : 메시지를 수신/사용하는 측이다.

 

4.2 메시징 시스템 툴

책에서 쓰인 기점으로 자주 쓰이는 메시징 시스템 툴로는 카프카, 래빗MQ, 레디스 pub/sub 등이 있다. 각 기술은 각기 다른 특징을 가지고 있으므로, 사용 목적에 맞는 기술을 선택해야 한다.

 

  • 카프카
    • 카프카를 메시징 시스테으로 고를 때 고려할 만한 특징들이다.
    • 높은 처리량을 자랑한다. 초당 백 만 개 이상의 메시지를 처리할 수 있다.
    • 수평 확장이 용이하다. 서버(브로커), 파티션, 소비자를 늘리면 된다.
    • 카프카는 메시지를 파일에 보관해서 메시지가 유실되지 않는다.
    • 1개의 토픽이 여러 파티션을 가질 수 있는데, 파티션 단위로 순서롤 보장한다. 하지만 토픽 수준에서는 순서를 보장할 수 없다.
    • 소비자는 메시지를 언제든지 재처리할 수 있다.
    • 풀(pull) 모델을 사용한다. 소비자가 카프카 브로커에서 메시지를 읽어 가는 방식이다.

 

  • 래빗MQ
    • 클러스터를 통해 처리량을 높일 수 있지만, 카프카보다 더 많은 자원을 필요로 한다.
    • 메모리에만 메시지를 보관하는 큐 설정을 사용하면 장애 상황 시 메시지가 유실될 수 있다.
    • 메시지는 큐에 등록된 순서대로 소비자에게 전송된다.
    • 메시지가 소비자에 전달됐는지 확인하는 기능을 제공한다.
    • Push 모델을 사용한다. 래빗MQ 브로커가 소비자에게 메시지를 전송한다. 소비자의 성능이 느려지면 큐에 과부하가 걸려 전반적으로 성능 저하가 발생할 수 있다.
    • 다재다능하다. AMQP, STOMP 등 여러 프로토콜을 지원하고, 게시/구독 패턴, 요청/응답 패턴, Point-to-Point 패턴을 지원한다. 우선순위를 지정해서 처리 순서를 변경할 수도 있다.

 

  • 레디스 pub/sub
    • 메모리를 사용하므로 지연 시간이 짧고, 래빗 MQ 대비 처리량이 높다.
    • 모델이 단순해서 사용하기 쉽다.
    • 구독자가 없으면 메시지가 유실된다.
    • 기본적으로 영구 메시지를 지원하지 않는다. 

 

메시징 시스템 비교

  • 레디스 pub/sub
    • 메시지 유실이 상관없을 때 사용
    • 사용법이 간단하다
    • 적은 장비 대비 높은 성능
  • 카프카
    • 대량 트래픽 상황(초당 수십만 - 수백만)에 사용하면 좋다
  • 래빗MQ
    • 트래픽 규모가 크지 않은 경우
    • 메시지 전송 순서가 중요한 경우
    • AMQP, STOMP 프로토콜 연동이 필요한 경우 사용하면 좋다

그 외 NATS, 액티브MQ, 로켓MQ, 펄사, SQS(AWS 제공) 등을 조직과 상황에 맞추어 사용할 수 있다.

 

4.3 메시지 생성 측 고려 사항

메시지를 생성할 때 고려할 점은 메시지 유실에 대한 것이다. 예를 들어 메시지 전송 과정에서 타임아웃이 발생할 수 있고 이는 생산자와 메시징 시스템 간의 네트워크 연결이 불안정하면 언제든지 발생할 수 있다.

 

이때 오류 처리를 위해 선택할 수 있는 방법은 다음과 같다.

  • 무시한다
    • 이 경우 메시지는 유실되고, 경우에 따라(단순 로그 메시지) 유실이 일부 허용될 수 있다.
  • 재시도한다
    • 일시적인 네트워크 불안정과 같은 오류는 재시도를 통해 해결될 수 있다. 하지만 메시지 전송을 재시도하는 과정에서 중복 메시지가 전송될 수 있기에 재시도하면서 중복 메시지를 보내지 않도록 해야한다.
    • 메시지마다 고유 식별자를 사용하면 메시지 소비자가 중복 메시지 여부를 판단하는데 도움이 된다.
  • 실패 로그를 남긴다
    • 추후 로그는 후처리에 사용될 수 있고(후처리에 필요한 정보를 담고 있어야 한다)
    • DB에 저장될 수도 있고 파일에 남길 수도 있다

 

4.4 메시지 생성과 DB 트랜잭션 연동

메시지 생성/전송이 DB 트랜잭션과 묶여있는 경우 처리 순서를 잘 고려해서 설계해야한다. DB트랜잭션에 실패했는데 메시지가 발송되면 잘못된 데이터가 전달될 수 있기 대문이다.

 

위 그림에서 1.4 단계에 DB 변경에 실패해 트랜잭션을 롤백했지만 메시지는 트랜잭션 롤백 전인 1.3 단계에 메시지를 전송했다. 따라서 사용자는 주문에 실패했다는 화면을 보았지만 주문이 완료되었다는 푸시 알람을 받는다.

 

잘못된 메시지가 전송되는 문제를 방지하려면 트랜잭션이 끝난 뒤에 메시지를 전송해야 한다. 이렇게 하면 메시지가 유효한 데이터를 포함할 수 있게 된다.(위 그림에서 1.5.1.1 참고)

 

4.4 글로벌 트랜잭션

  • 여러 자원(DB)에 대한 변경을 한 트랜잭션으로 묶어서 처리할 수 있는 트랜잭션
  • DB 수정 + 메시지 전송 처리를 하나의 트랜잭션으로 묶는 것을 의미한다
  • 2단계 커밋 사용(2-Phase Commit, 2PC)
  • 메시징 시스템 중 액티브 MQ가 글로벌 트랜잭션을 지원한다.
    • 메시지 전송 실패 -> DB 롤백 -> 메시지 전송을 취소해 실패에 대한 대응 처리를 단순화하는 기능이 있다
  • 단점으론 처리속도가 느려지고 성능에 영향을 줄 수 있다.
  • 글로벌 트랜잭션이 반드시 필요한 상황이 아니라면 DB 처리와 메시지 연동을 묶지 말자. DB에 데이터를 반영한 뒤에 메시지를 최대한 유실 없이 보내고 싶을 경우 트랜잭션 아웃박스 패턴을 검토해보자

 

5. 메시지 소비 측 고려 사항 - 중복 메시지 전송

메시지 소비자가 메시지를 중복 처리하는 경우는 다음과 같다.

  • 메시지 생산자 -> 메시징 시스템, 중복 메시지 전송
  • 소비자, 메시지 처리시 오류 발생으로 메시지 재수신
while(true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    for(ConsumerRecord<String, String> record : records) {
    Message m = messageConverter.convert(record.value());
    if (checkAlreadyHandled(m.getId()) { // 이미 처리했는지 확인
      continue; // 처리하지 않고 무시함
    }
    handle(m);
    recordHandledLog(m.getId()); // 처리여부 기록
    }
    ...
}

중복 메시지 전송을 예방하려면 각 메시지 당 고유한 ID + 처리여부를 사용하면 된다. 메시지 처리 여부는 1) DB 테이블에 기록하거나 2) 메모리에 집합(Set)으로 관리한다.(메모리 부족 에러가 발생하는 것을 막기 위해 일정 개수의 메시지 ID만 유지한다)

 

그 외에 중복 메시지 처리와 함께 메시지 소비자를 구현할 때 고려할 점은 메시지를 잘 소비하고 있는지 모니터링하는 것이다. 메시지 소비자의 처리 속도가 갑자기 느려지면 큐에 메시지가 계속 쌓이게 되고, 결국 큐가 가득 차게되면 생산자가 메시지를 큐에 넣지 못하게 막는 상황도 발생할 수 있기 때문이다. 따라서 메시지 소비자 모니터링이 필요하다.

 

6. 메시지 종류 : 이벤트와 커맨드

  • 이벤트
    • 어떤 일/활동이 발생했음을 알려주는 메시지다.
      • 사용자가 로그인에 실패했을 때 사용자 데이터는 변경되지 않을 수 있고, 사용자의 상태는 그대로 일 수 있지만, 사용자의 활동 결과로 '로그인 실패함' 이벤트가 발생한다.
    • 상태(데이터) 변경과 관련이 있다.
      • 예시로 '주문함' 이벤트는 주문이 생성됐다는 사실을 의미하고, '배송을 완료함' 이벤트는 배송 상태가 배송중 -> 배송 완료로 바뀐 것을 의미한다. 
    • 정해진 수신자가 없다(메시지 수신 후 동작은 소비자의 몫)
    • 소비자 확장에 적합하다
  • 커맨드
    • 무언가를 요청하는 메시지이다.
    • 수신자가 정해져있고, 메시지를 수신할 측의 기능 실행에 초점이 맞춰져 있다.
    • 예) 포인트 지급하기, 배송완료 문자 발송하기

 

7. 궁극적 일관성(Eventual Consistency)

주로 분산 시스템에서 데이터 복제를 다룰 때 사용되는데, 두 데이터 저장소 간 일관성을 보장하긴 하지만 즉시가 아닌 일정 시간이 지난 후에야 일관성이 맞춰진다는 특성을 가진다. 즉, 일시적으로는 두 저장소 간에 데이터 불일치가 발생할 수 있다는 뜻이다. 이를 메시징 시스템에 적용해 메시지가 전파되기 전까지는 두 시스템 간의 상태가 서로 불일치할 수 있다는 것이다.

 

8. 트랜잭션 아웃박스 패턴(Transactional Outbox Pattern)

메시징 시스템 연동 실패로 메시지 데이터 자체가 유실되지 않도록 보장하기 위해 메시지 데이터를 DB에 보관하고 저장된 메시지를 읽어 메시징 시스템에 전송하는 패턴을 트랜잭션 아웃박스 패턴이라고 한다.

 

트랜잭션 아웃박스 패턴은 하나의 DB 트랜잭션 내에서 다음 2가지 작업을 수행한다.

  • 실제 업무 로직에 필요한 DB 변경 작업을 수행한다.
  • 메시지 데이터를 아웃박스 테이블에 추가한다.

 

아웃박스 테이블에 쌓인 메시지 데이터는 별도의 메시지 중계 프로세스가 주기적으로 읽어서 메시징 시스템에 전송한다. DB 트랜잭션 범위에서 아웃박스 데이터를 추가하므로 메시지 데이터가 유실되지 않고, 트랜잭션 롤백 시 메시지 데이터도 함께 롤백되므로 잘못된 메시지 데이터가 전송될 일도 없다.

 

메시징 중계 서비스는 위 그림의 5번 ~ 7번 과정을 반복한다. 발송하지 않은 데이터를 조회해서 메시징 시스템에 전송하고, 전송에 성공하면 전송 완료 처리를 한다. 이렇게 하면 메시지 중복 전송을 예방할 수 있다.

 

// 아래 메서드를 주기적으로 호출해서 메시지를 전송한다
public void processMessages() {
    // 아웃박스 테이블에서 대기 메시지 데이터를 순서대로 조회함
    List<MessageData> waitingMessages = selectWaitingMessages();
    
    for (MessageData m : watingMessages) {
      try {
        snedMessage(m); // 메시지 전송
        markDone(m.getId()); // 발송 완료 표시
      } catch(Exception ex) {
        handleError(ex); // 메시지 발송에 실패한 경우 후속 처리
        break; // 에러가 발생했을 때 멈춤. 순서대로 발송하기 위함     
      }
    }
  }

위 코드에서 특정 메시지 전송에 실패하면 루프를 멈추는데, 그 이유는 메시지를 순서대로 전송하기 위함이다. 

 

발송 완료를 표시하는 방법은 2가지가 있는데,

첫 번째는 아웃박스 테이블에 발송 상태 컬럼을 두어서 3가지 상태(발송 대기, 발송 완료, 발송 실패)를 갖는다. 발송 대기 상태의 데이터를 조회하고 성공하면 발송 완료로 변경하는 방식이다.

 

두 번째는 메시지 중계 서비스가 성공적으로 전송한 마지막 메시지ID를 별도로 기록하는 방식이다. 예를 들어 파일이나 별도의 테이블에 작업한 마지막 메시지 ID를 저장해두고, 다음번 대기 메시지를 조회할 때 이 ID 이후의 메시지만 선택하는 것이다.

 

각 상황에 따라 적합한 방식이 다른데,

  • 발송 상태 컬럼
    • 메시지 처리 현황을 쉽게 모니터링할 수 있고
  • 작업 마지막 메시지 ID 기록
    • 2개 이상의 메시지 중계 서비스가 하나의 아웃박스 테이블을 함께 사용하는 환경일 때 각 중계 서비스가 고유하게 마지막 메시지 ID를 관리해야 하므로, 이 경우에는 마지막 메시지 id를 기록하는 방식이 더 적합할 수 있다.

 

9. 아웃박스 테이블 구조

아웃박스 테이블 구조는 다음과 같고, 각자 상황에 맞게 변형해서 사용하면 된다.

컬럼 타입 설명
Id big int 단순 증가값(PK), 저장된 순서대로 증가하는 값 사용
messageId varchar 메시지 고유 ID(고유키)
messageType varchar 메시지 타입, LoginFailed OrderPlaced 같은 상태값, 메시지 소비자가 메시지 타입을 이용해 알맞은 처리 수행
payload clob 메시지 데이터
status varchar 이벤트 처리 상태
- WAITING, DONE, FAILED
failCount int 실패 횟수
occuredAt timestamp 메시지 발생 시간
processedAt timestamp 메시지 처리 시간
failedAt timestamp 마지막 실패 시간

 

status 컬럼

status 컬럼의 경우 대기 상태인 메시지 데이터만 조회하기 때문에, 어떤 조건에서 '실패함' 상태로 바꿀지 결정해야 한다.

실패 횟수를 기준으로 자동으로 상태를 변경할 수 있고 상태를 모니터링 하다가 수동으로 변경할 수도 있다.

예시로 메시지 발송에 5회 실패하면 '대기' -> '실패함'으로 상태를 바꾸고 다음 메시지를 처리하거나

실패 횟수가 10회 이상일 경우 모니터링 시스템을 통해 운영팀에 메시지 발송 지연을 알리고, 운영팀이 수동으로 '실패함' 상태로 바꾸도록 할 수 있다.

 

일시적인 메시지 전송 실패가 발생할 수 있으므로 메시지 실패가 발생했다고 바로 '실패함' 상태로 바꾸지 말고, 한두 차례 재시도 후에 실패시 그때 '실패함'으로 상태를 변경하자.

 

상태가 '실패함'으로 바뀐 뒤에는 시스템 간 데이터 일관성이 깨질 수 있기에 알맞은 후속 조치를 해야 한다 memo나 remark 같은 컬럼을 추가해 실패 메시지를 아웃박스 테이블에 기록하면, 실패 이유를 파악하는 데 도움이 된다.

 

그 외 제외함(excluded)을 추가할 수 있다. 이는 실패가 아니라 수동으로 특정 메시지를 전송하고 싶지 않을 때 사용할 수 있다.

 

10. 배치 전송

배치 전송은 데이터를 비동기로 연동하는 가장 전통적인 방법이라고 할 수 있다. 메시징 시스템이 거의 실시간으로 데이터를 연동한다면, 배치는 일정 간격으로 데이터를 전송한다. 예시로 결제 승인 데이터를 모아서 다음 날 보내거나, 택배 발송 요청 데이터를 1시간 간격으로 보내는 식이다.

 

배치 전송 실행 과정은 보통 다음 과정을 따른다.

DB에서 전송할 데이터를 조회한다.

조회한 결과를 파일로 기록한다.

파일을 연동 시스템에 전송한다.

 

10.1 파일 전송 형식

파일 전송은 FTP나 SFTP 같은 파일 전송 프로토콜 혹은 SCP 같은 명령어를 이용해 수행한다. 주로 사용하는 파일 형식으론

1) 값1(구분자)값2(구분자)값3(구분자)값4

  • 구분자는 ^문자나 탭 문자처럼 값에 포함되지 않는 특수문자를 주로 사용한다.
  • 구현이 간단하고, 파싱속도가 빨라 널리 사용된다.

2) 이름1=값 이름2=값2 이름3=값3 이름4=값4

  • 이름과 값을 한 쌍으로 묶는 형식이다. 각 쌍은 공백 문자로 구분하며, 값에 공백이 포함될 경우 탭이나 ^같은 문자를 구분자로 사용한다.
  • 데이터 위치에 상관없이 데이터 조회가 가능하다는 장점이 있지만
  • 이름까지 데이터에 포함되므로 첫번째 형식에 비해 데이터 크기는 커진다는 단점이 있다.

3) JSON 문자열

  • 각 줄마다 JSON 형식으로 데이터를 전달하는 방식이다.
  • 대부분의 프로그래밍 언어는 JSON 변환 기능을 제공하므로 쉽게 구현할 수 있다.
  • 값 외에 프로퍼티 이름, 형식을 지키기 위한 콤마, 큰따옴표, 콜론 등의 문자를 포함해 데이터 크기가 커진다는 특징이 있다.

 

파일 형식 외에 송수신 주체, 시간, 경로, 데이터 전송 방법 등 정해야 할 요소들이 더 있다.

 

10.2 송수신 주체

파일로 데이터 전송 시 누가 전송의 주체가 될 것인지 결정해야 하는데,

파일 생성 시스템이 파일 소비 시스템에 업로드를 해주거나(파일 생성 시스템 -> 파일 소비 시스템)

파일 소비 시스템이 파일 생성 시스템으로부터 다운로드 받는 방식이 있다(파일 생성 시스템 <- 파일 소비 시스템).

 

10.3 시간

시간도 중요한 요소인데, 소비자 시스템은 특정 시점에 데이터를 필요로 한다. 예시로 업무일 기준으로 매월 5일까지 정산을 마쳐야 하는 조직이라면, 그 전에 월 단위 데이터를 받아야한다. 정해진 시점까지 데이터를 받지 못해 업무가 지연되면 그 화살이 개발팀으로 향하게 된다.

 

배치 파일은 데이터 누락 등 오류에 대응할 수 있는 시간을 벌기 위해 근무가 시작되는 오전 시간대에 전송을 처리할 때가 많다. 하지만 생산자 시스템이 글로벌 서비스라면 국내 시간대가 아닌 다른 시간대를 기준으로 파일을 받아야 하는데, 이 경우 생산자 시스템이 보내줄 수 있는 시간에 맞춰 소비자 시스템의 처리 시간을 변경해야 한다.

 

10.4 배치 데이터 전송 방법

1) 파일 전송

경로와 파일 이름도 규칙에 맞추어 설정해야하는데, 여러 서비스로 부터 파일을 받을 경우 파일이 저장될 경로나 이름이 충동할 가능성을 제거하기 위해서이다.

 

생산자 -> 소비자 파일 업로드 후 소비자 시스템의 동작 방식

  • 지정한 경로에 파일 존재 여부를 확인한다
    • 파일이 존재하면 파일로부터 데이터를 읽어온다
    • 파일이 없으면 알맞게 후처리한다
  • 읽어온 데이터를 시스템에 반영한다
  • 처리를 완료한 파일은 다른 폴더로 옮긴다
    • 동일한 파일 중복 처리 예방하기 위해서
  • 이동 완료한 파일을 백업해두면 재처리가 필요할 때 활용 가능하다

2) API 전송

위 방식은 데이터를 파일로 전송하는 경우에 해당한다. 만약 데이터 크기가 작거나 처리 항목이 적을 때 API를 이용해 데이터를 일괄 전송하는 방법도 있다. API를 사용하면 파일 생성, 파일 전송, 파일 처리 과정이 없으므로 구현이 더 단순해진다.

 

3) DB 읽기 전용으로 열기

이 방식은 같은 조직 내에서 데이터를 전달할 때 사용할 수 있는 방식으로, 개발 시간이 부족할 때 선택할 수 있는 방법이다. 

 

10.5 배치 재처리 기능

파일 생성 과정에서 오류가 발생하거나, 네트워크 상태가 좋지 않아 배치 파일을 지정한 시간에 전송하지 못할 때가 있다. 이처럼 전송에 실패할 경우가 생기므로 전송 실패 후 일정 시간 후에 재전송하는 기능을 구현하는 것이 좋다. 예를 들어 7시에 배치가 실행되고 평균 실행 시간이 20분 정도 걸린다고 하면, 7시 40분쯤 배치가 성공했는지 확인하고 실패했다면 재실행하는 방식으로 말이다. 이렇게 한두 번 정도만 재시도해도 수작업으로 재처리하는 번거로움을 줄일 수 있다.

 

재시도를 했음에도 실패를 하는 경우를 대비해 수동으로 배치를 쉽게 실행할 수 있도록 명령어나 API를 만들어 두는게 좋다. 문제가 생겼을 때 빠르고 편하게 배치를 재실행할 수 있다.

 

11. CDC(Change Data Capture)

CDC는 변경된 데이터를 추적하고 판별해서 변경된 데이터로 작업을 수행할 수 있도록 하는 소프트웨어 설계 패턴이다. 즉, 데이터의 변경 분을 전달하는 역할이다. 오라클이나 MySQL 같은 DBMS에서 데이터가 변경될 경우 그 변경 내용을 통지하는 기능이 있는데, CDC가 이 기능을 활용해서 구현한다.

 

  • insert, update, delete 쿼리를 실행으로 DB 데이터가 변경된다.
  • DB는 변경된 데이터를 CDC 처리기에 전송한다.
    • 변경 데이터는 레코드 단위로 전달되고, 각 레코드는 데이터 변경 방식(추가, 수정, 삭제)을 구분하는 플래그를 갖는다. 수정일 경우 변경된 컬럼의 변경전후 값을 포함해 어떤 컬럼이 어떻게 변경됐는지 확인할 수 있다.
  • DB는 커밋된 데이터만 변경된 순서에 맞게 전달한다.
  • CDC 처리기에는 롤백한 데이터가 전달되거나 잘못된 순서로 데이터가 전달되지 않는다.

 

CDC 처리기는 전달받은 변경 데이터를 확인하고 가공한 뒤에 대상 시스템에 전파한다. 

  • 변경 데이터를 그대로 대상 시스템에 전파(두 시스템 데이터가 거의 1:1 관계일 경우, ex. 회원정보 수정)
  • 변경 데이터를 가공/변환해서 대상 시스템에 전파(주문ID, 변경된 주문상태 등 수신자 시스템에 꼭 필요한 데이터만 전송해야 할 때)

 

목적에 따라 CDC 처리기는 DB, 메시징 시스템, API 등 다양한 대상에 데이터를 전파할 수 있다. 

  • 두 시스템 간 데이터 동기화가 목적이라면 단순히 DB와 DB 사이에 CDC를 두어 데이터를 복제할 수 있다.
  • 메시징 시스템에 데이터를 전파하면 여러 시스템에 변경된 데이터를 전달할 수 있어 확장에 유리하다.

 

12. CDC와 데이터 위치

CDC 처리기는 변경 데이터를 어디까지 처리했는지 기록해야 한다. 예시로 MySQL에서는 바이너리 로그를 이용해 CDC를 구현하는데, 각 로그 항목은 변경된 데이터와 로그 파일에서의 위치(포지션) 값을 갖는다. 이 위치를 기록해야 CDC 처리기 재시작 시 조회 시작 부분을 알 수 있다.

 

13. CDC가 유용할 때

일정에 여유가 없거나 코드가 복잡해질 수 있어 연동 코드 추가가 어려울 때 CDC를 사용하는 것이 유용할 수 있다.

책에서 언급한 예시로 신규 주문 시스템에서 발생한 주문 데이터를 기존 주문 시스템에 반영해야 하는 요구사항이 있었고, 신규 주문 시스템에서 생성/수정된 주문을 기존 주문 데이터 시스템에 전달해야 했다. 일정과 코드 복잡성의 이유로 신규 주문 시스템 코드를 수정하기에 어려운 상황이었고, 이때 CDC를 활용해 문제를 해결했다.

 

연동 대상 시스템이 구주문 DB와 컨텐츠 서비스로 2개였고, 서로 데이터 처리 속도가 달라 중간 메시징 시스템으로 카프카를 두었다. CDC로 연동 기능을 구현한 덕분에 신규 주문 시스템 개발 일정에 주는 영향을 최소화할 수 있다.

 

 

+ Recent posts