🥒

틀린 할인 가격에 대한 문제 해결

팀에서 운영하는 이 애플리케이션은 카프카(Kafka)를 통해 카탈로그 데이터를 수신한다. 애플리케이션은 이 수신한 데이터를 엘라스틱서치(Elasticsearch, ES)에 저장한다. 애플리케이션은 카탈로그 데이터 중 가격 데이터가 변경되었다면 카탈로그의 할인 가격을 추가로 갱신한다. 여기서 카탈로그 데이터를 엘라스틱서치에 저장하는 과정을 A라고 하고, 카탈로그의 할인 가격을 갱신하는 과정을 B라 하자. 각 과정에 대한 세부 내용은 다음과 같다.

A: 카탈로그 데이터 저장 과정

카탈로그 데이터 저장 과정은 간단하다. 애플리케이션은 카프카를 통해 카탈로그 데이터를 수신한다. 애플리케이션은 수신한 데이터를 그대로 엘라스틱서치에 저장한다. 이 과정이 전부다.

B: 카탈로그의 할인 가격 갱신 과정

카탈로그의 할인 가격 갱신 과정은 카탈로그 데이터 저장 과정(A)보다 복잡하다. 애플리케이션이 수신한 카탈로그 데이터 중 만약 가격 데이터가 변경되었다면 애플리케이션은 이 카탈로그의 아이디(ID) 값을 새로운 카프카 토픽(Topic)으로 발행한다. 그리고 이 애플리케이션은 발행한 카탈로그의 아이디 데이터를 다시 수신한다. 애플리케이션은 카탈로그의 아이디 값으로 엘라스틱서치로부터 카탈로그의 데이터를 조회한다. 그 이후 애플리케이션은 카탈로그의 아이디와 가격, 그리고 다른 정보를 이용하여 외부 API를 호출하여 할인 가격 정보를 가져온다. 애플리케이션은 외부 API로부터 가져온 할인 가격으로 엘라스틱서치에 저장된 데이터를 갱신한다.

애플리케이션은 A와 B 과정을 담당하면서 엘라스틱서치에 저장된 카탈로그 데이터를 최신화한다. B 과정에서 가격이 변경된 카탈로그의 아이디 값을 카프카로 발행하지 않고 처리할 수도 있다. 그러나 이 부분은 이 문제 해결 과정에서 핵심이 아니기 때문에 다루지 않겠다.

문제

문제는 애플리케이션이 B과정을 A과정보다 더 빠르게 처리하면서 할인 가격 데이터를 이상한 값으로 갱신하고 있었다. 카탈로그의 할인 가격이 실제와 맞지 않은 값으로 변경되었기 때문에 비즈니스 도메인에 나쁜 영향을 주었다.

카탈로그는 가격뿐만 아니라 이름, 이미지, 그리고 제조사 등의 다양한 데이터를 가지고 있다. A 과정에서 애플리케이션이 처리하는 대상은 달라진 카탈로그의 수 만큼이고, B 과정에서 처리하는 대상은 가격이 달라진 카탈로그의 수 만큼이다. A 과정에서 애플리케이션이 처리하는 대상의 크기를 100으로 생각한다면 B 과정에서의 크기는 반드시 100보다 작거나 같다. 극한의 경우를 생각한다면 B 과정에서의 크기는 A에 비해 매우 작거나 0일 수도 있다.

애플리케이션이 A 과정에서 데이터를 제때 처리하지 못해서 처리 지연이 있고, 이에 반해 애플리케이션이 B 과정에서 데이터를 어떠한 지연없이 처리하고 있다고 가정하자. B 과정에서 애플리케이션은 카탈로그를 엘라스틱서치로부터 조회한다. 이 때 조회한 카탈로그가 과연 최신의 데이터일까? A 과정에서 처리 지연이 있기 때문에 애플리케이션이 아직 카탈로그를 변경하지 못한 상황이 있을 수 있다. 다시 말하면, 애플리케이션이 B 과정에서 조회한 카탈로그가 최신 데이터가 아닐 수 있다. 이러한 상황에서 애플리케이션은 조회한 예전 카탈로그로 할인 가격을 조회하고 엘라스틱서치에 이를 갱신한다. 그 이후 애플리케이션이 A 과정을 끝낸다면 카탈로그의 데이터는 실제 값과 같지만, 할인 가격은 실제 값과 다르게 된다.

애플리케이션이 A 과정보다 B 과정을 먼저 처리하면서 아직 변경하지 않은 카탈로그를 이용한다. 변경되지 않은 카탈로그를 이용하면서 애플리케이션은 실제와 다른 할인 가격을 가져오고 엘라스틱서치에 갱신한다.

해결

문제 해결의 핵심은 애플리케이션이 카탈로그를 조회하는 시점이다. 이 시점에서 반드시 카탈로그가 최신 데이터, 실제와 같은 데이터임을 보장해야한다. 아니면 애플리케이션이 카탈로그를 조회하는 단계를 없애는 것이다. 

  • 애플리케이션이 B 과정에서 카탈로그를 조회하기 전, A 과정에서 카탈로그 데이터를 반드시 먼저 처리한다.
  • 애플리케이션이 B 과정에서 카탈로그를 조회하지 않도록 카프카로부터 카탈로그 아이디 값뿐만 아니라 다른 데이터도 같이 수신한다.

이번 문제 해결을 진행할 때는 이 두 가지 방법 중에서 전자를 택했다. 애플리케이션이 A 과정을 처리한 이후에 가격이 변동된 카탈로그만 따로 걸러낸다. 애플리케이션이 이 걸러낸 카탈로그를 카프카 토픽으로 발행하도록 개선했다.

두 번째 방법을 선택하지 이유는 카탈로그의 아이디 값만 발행하는 또 다른 애플리케이션이 존재했기 때문이다. 일부 애플리케이션은 카탈로그의 아이디와 다른 정보를 같이 발행하고, 일부 다른 애플리케이션에서는 아이디 정보만 발행하는 상황이 우리가 시스템을 관리하기 힘들게 만들 수 있다. 그렇다고 해서 다른 애플리케이션까지 변경하기에는 수정 사항이 너무 많았다.

반성

모든 상황에서 문제가 나타나지 않기 때문에 이 문제의 원인을 찾아내기 어려웠다. 무심코 모니터링을 하다가 카프카 랙(lag)을 발생하고 있다는 점을 생각해서 추론해나갔다. 로컬 환경이나 개발 환경에서 데이터 지연이 발생하는 상황이 없어서 A 과정보다 B 과정이 먼저 진행할 수 있다는 부분도 생각하지 못했다.

추가적으로 처음에는 이 애플리케이션이 B 과정을 처리할 때, 엘라스틱서치로부터 카탈로그 데이터를 조회하는 부분이 없었다. B 과정에서 사용하는 외부 API를 변경하면서 카탈로그의 아이디 값 외에도 다른 값도 조회하도록 애플리케이션이 변경되었다. 이 외부 API 변경이 위의 문제 상황을 만들거라고 생각하지 못했다.

할인 가격이 실제 가격과 일치하지 않다는 것은 내부에서 발견하지 않았다. 외부에서 발견되어 우리 팀까지 오게 된 상황이다. 처음부터 이 문제에 대해서도 깊이 고민하지 않았다. 당시에 데이터가 일치하지 않은 경우가 더러 있어 이전처럼 복구만 진행했다. 이번에도 이렇게 처리하면 괜찮겠지라고 생각했다. 항상 궁금해하고 깊이 있게 찾아봐야 하는 자리에 있으면서 그러지 못했다.