Channel API 개발 (1) — 데이터 동기화

김준성
FRIP
Published in
7 min readSep 27, 2022

안녕하세요 프립 백엔드 개발자 김준성입니다.
백엔드 개발자로 전향 후 처음 작업했던 프로젝트를 공유해보려 합니다.
2021년 12월 프립을 합류해 파트너사에 제공하는 Channel API를 만드는 신규 프로젝트를 맡아서 진행했는데요. 이 프로젝트를 진행하면서 무슨 고민을 하였는지 그리고 어떻게 해결하려 했는지에 대한 내용들을 중점으로 적어보려 합니다.

들어가기 앞서

먼저 작업을 시작하기 전 상황을 말씀드리자면, 2021년 말 프립의 서버 구조가 통째로 변경되었습니다. 그래서 제가 합류한 시기에는 이미 모놀리식 구조 + Rest API를 사용하던 환경에서 MSA 구조 + GraphQL로 변경되었고 클라이언트, 서버 간의 통신도 GraphQL를 통해 이루어졌습니다.

Channel Sync API 주요 기술 스택
-
Spring(Java)
- GraphQL + DGS Library
- Kafka
- Datadog

모르는 기술이 있으실 분들을 위해 아주아주 간략하게 기능에 대해서만 설명드리자면

GraphQL은 Facebook에서 만든 API 쿼리 언어로, 백엔드에서 데이터를 가져갈 수 있는 환경을 구성해두면 클라이언트는 자신이 원하는 데이터만 선택해서 가져갈 수 있습니다.

Kafka는 LinkedIn에서 만든 pub/sub 모델의 메시지 큐입니다. publish 된 많은 토픽들 중 자신이 원하는 토픽만 subscribe 하여 데이터를 수신합니다.

Datadog은 모니터링 및 분석 프로그램입니다. 서버의 상태 및 로그를 확인할 수 있는 다양한 기능이 존재합니다.

앞으로 Frip Channel API를 FCA라고 명칭 하겠습니다.

Channel API가 해야 하는 주요 작업 3가지

  1. FCA DB 동기화
  2. FCA Rest API 서버 구축 (Frip 상품 정보 제공 및 주문을 중개)
  3. API Gateway 환경 구성 및 인증 처리

제가 맡았던 작업은 1번 DB 동기화와 2번 RestAPI 서버 구축입니다.

이번 글은 1번 DB 동기화와 관련된 내용을 말씀드리도록 하겠습니다.

FCA 데이터 동기화 — Channel Sync API

FCA DB는 프립에서 실시간 운영 중인 DB(Frip DB)와 독립적인 DB입니다.
따라서 Frip DB의 데이터를 FCA DB에 동기화하는 작업이 필요합니다.
해당 작업을 하는 API를 Channel Sync API라 명칭 하겠습니다.

DB를 동기화하는 구조에 대해서도 고민을 해봤었는데요.
3가지 방법이 있었습니다.

1.Scheduler를 통해 주기적으로 필요한 데이터를 운영 중인 서버 Graphql로 요청하고 응답 값을 Parsing 하여 DB에 저장한다.

[장점]
- Frip 서버에서 작업할 것이 거의 없음

[단점]
- 지속적으로 Frip DB에 데이터를 호출하는 작업이 필요. (서버의 부하가 지속적으로 발생)
- GraphQL의 응답 형태가 Rest API와는 다른 형태로 넘어오는데 Spring에서 이를 VO 모델로 Deserialize 하는 것이 불편함.

2. DB를 Master(Frip DB) & Slave(FCA DB) 구조로 만든다.

[장점]
- Frip 서버에서 작업할 것이 거의 없음

[단점]
- 두 DB 간의 종속성이 발생
- 두 DB의 컬럼 구조가 다름

3. data가 변경 시 발송되는 Kafka Event를 활용하여 데이터를 수신하고, Parsing 하여 DB에 저장한다.

[장점]
- 장기적으론 서버의 부하가 적음
- 추후 Event에서도 payload의 값을 다 확인할 수 있는 것이 좋다고 판단

[단점]
- FCA에서는 필요하나 Event에는 빠져 있는 data가 존재.
이를 확인하고 추가해주는 작업이 필요
- Event 수신 순서에 따라 정합성이 깨질 수 있음.
- 카프카 장애의 대한 대응 필요

구조 선택
두 DB의 컬럼이 다르고 종속성이 발생하는 문제로 인해 DB Master & Slave 구조는 사용하지 않기로 하였고, 장기적으로 봤을 때 1번보다는 3번을 택하는 것이 서버의 부하가 적고 작업을 하는 데 있어 더 편할 것 같다고 판단을 하여 3번 Kafka Event 수신 방식을 택하였습니다.

작업 시 고려했던 점들

아직 모든 문제를 해결한 것은 아니지만 작업 시 주요하게 고려했던 부분들에 대해서 정리해보자면 다음과 같습니다.

1. Kafka 이벤트에 payload 추가는 가능하나 제거는 불가능하다(힘들다)
kafka는 publish 해두면 그것을 필요한 곳에서 구독하여 사용하는 형태이기에 이벤트 페이로드를 함부로 제거시켰다가는 그 데이터를 사용하고 있던 api에서는 장애가 발생할 수 있습니다.

그래서 FCA에서 필요한 payload를 추가할 경우에도 나중에 제거가 불가능할 수 있으니 꼭 들어가야 하는 payload인지 신경을 더 썼습니다.

2. 이벤트 순서가 실제 발생 순서와 다를게 올 수 있다.
kafka는 같은 파티션 안에서는 순서를 보장해서 받을 수 있지만, 다른 파티션 혹은 다른 토픽 간에는 순서를 보장하여 받을 수 없습니다.

정확하게는 한 topic안에서 파티션끼리는 key를 이용하여 순서를 보장받을 수 있지만 프립의 기존의 이벤트들은 key를 사용하지 않고 라운드-로빈의 방식으로 이벤트를 보내고 있었습니다. 이 경우 같은 토픽이라도 파티션끼리는 순서를 보장받을 수 없습니다.
* 라운드-로빈 방식은 각 파티션마다 순서대로 한 번씩 데이터를 쌓는 방식

저는 spring-kafka 라이브러리의 @KafkaListener를 통해 이벤트를 수신했고, 한 토픽 내에서 순서가 다르게 오는 경우는 극히 드물었습니다. 같은 테이블의 데이터를 변경하는 두 토픽의 순서가 달리 올 경우엔 상태 값에 따른 예외 처리 코드를 추가하여 데이터 정합성이 맞도록 해주었습니다.

만약 같은 토픽 내에서도 순서를 보장받고 싶었을 경우에는 event 발생 시간에 따라 처리하도록 했을 것 같습니다.

3. 이벤트를 제대로 처리하지 못하는 장애 상황 발생 (Runtime & SQL Exception, Kafka 기능 장애)

kafka는 offset이라는 개념을 통해 자신이 읽을 데이터 순서를 관리하게 되는데, 성공/실패 여부에 따라서 관리하는 게 어려운 구조입니다.

예를 들어 1번 offset commit (처리 실패), 2번 offset commit (처리 성공)했을 경우에 다음 읽을 순서는 3번이 됩니다. 다시 1번을 읽을 순 있지만 이 경우에는 1번부터 다시 순차적으로 읽게 되고, 2번은 2번 처리되게 됩니다. 이는 멱등성이 보장되지 않는 환경에서는 문제가 발생할 수 있습니다.

그리고 카프카 자체에서도 장애가 발생하여 이벤트를 수신 못하는 경우들이 발생할 수 있는데 Frip 서버에서 broker 혹은 producer 장애로 event를 send 하지 못하거나 FCA에서 kafka consumer 리밸런싱, broker 장애 등으로 이벤트를 수신하지 못할 수도 있습니다.

4. 데이터 정합성을 확인할 수 있는 작업이 필요하다

위에서 여러 상황으로 인해 이벤트를 받지 못하거나 제대로 된 순서로 처리하지 못할 수 있다는 것을 설명드렸습니다.

이는 두 가지 방식으로 해결을 하려 했는데 모니터링과 주기적인 정합성 확인 스케줄러입니다.

Datadog에선 로그를 통해 FCA에서 받은 Payload 및 처리 과정을 확인할 수 있으며 대부분의 에러는 이 로그들을 통해 확인이 가능합니다.

정합성 확인 스케줄러는 에러 발생 여부를 확인하기 위해 필요합니다. 아직 개발 진행 중에 있으며 이는 Frip과 FCA 각각 지정한 시간 사이의 변경된 데이터 목록을 가져와 id , updatedAt, Status 값들을 비교하면서 정합성을 확인합니다.

Datadog 모니터링 중

5. 데이터 정합성이 맞지 않는 경우 재동기화

데이터 정합성이 맞지 않을 경우 다시 정확한 데이터로 재동기화하는 과정이 필요한데 이 부분도 어떤 방식으로 처리하는 게 좋을지 아직 고민중이며, 개발진행 중에 있습니다.

마치며

1편에서는 Channel API를 개발 중 DB 동기화 과정에서겪었던 고민들을 공유드렸습니다. 2편에서는 Channel API 서버 문서화했던 경험을 공유드리겠습니다.

--

--