29CM 상품 할인 / 환원 배치 성능 향상시키기

brownbears
29CM TEAM
Published in
7 min readApr 5, 2021

안녕하세요 29CM 이진환입니다.

29CM에는 상품 가격 일괄 할인 / 환원이라는 2개의 배치가 있습니다. 일괄 할인이란 상품의 할인 프로모션으로 인해 일괄적으로 원래의 가격보다 더 싸게 변경하는 것을 의미하고 일괄 환원은 할인 프로모션이 종료되면 다시 원래의 가격으로 변경하는 것을 의미합니다.

월초마다 29CM 이벤트를 위해 가격을 할인하거나 환원하는 상품의 개수가 늘어나다 보니 먼저 시작한 배치의 처리 시간이 길어지면 그 사이에 실행된 또 다른 배치의 수정이 반영되지 않는 문제가 존재해 상품 가격이 의도대로 변경되지 않는 문제가 발생했습니다.

아래는 위 문제 상황의 예시입니다.

위 예시의 정상적인 동작은 상품의 가격이 00:00에 5만원으로 환원이 완료되고, 이후 00:10에 3만원으로 할인이 적용되어야 하지만 처음 00:00에 시작한 환원 배치의 처리 시간이 지연되어 00:50에 완료됐고 그 사이에 3만원으로 수정했던 할인 이력이 반영되지 않은 상황입니다.

즉, 00:10에 시작한 할인의 내용이 override 돼 배치가 실행되지 않은 것과 같은 문제입니다.

해결 방안

이러한 문제를 해결하기 위해서 아래와 같이 작업을 정리했습니다.

  • 일괄 할인 / 환원배치 처리 속도 향상 (처리 지연 구간 수정, 배치 병렬 처리)
  • 젠킨스 파이프라인을 활용하여 일괄 할인과 환원의 배치를 병렬로 실행

사실 내부 구조와 로직을 변경하는 것이 문제 해결의 근본적인 방법이었지만 연말연시의 대규모 프로모션이 예정된 상황이라 개발팀에 주어진 시간은 2~3일 정도였습니다.

따라서 기존 로직은 최대한 수정하지 않는 방향으로 하되 로직을 너무 많이 수정해야 한다면 다른 방안을 찾기로 했습니다.

일괄 할인 / 환원의 배치 처리 속도 향상

그 당시 일괄 할인 및 환원 로직에서 4,000개 가량의 상품을 처리하는데 약 13분의 시간이 소요되었습니다. 배치의 속도 향상을 위해 처리 지연이 발생하는 구간을 파악하고 수정한 다음, 병렬로 배치가 실행될 수 있도록 개발 순서를 정했습니다.

디버깅을 통해 발견한 처리 지연 구간은 아래와 같았습니다.

  • x에 상품 가격 정보를 동기식으로 전달하는 구간 (ES 싱크 작업)
  • Database에 상품 가격 정보를 1건씩 반영하는 구간 (DB 싱크 작업)

ES 싱크 작업을 동기식에서 비동기식으로 변경하는 작업은 이미 29CM 내부에서 사용 중인 Celery를 활용해 쉽게 변경할 수 있었습니다.

그러나 DB 싱크 작업의 경우, 코드 구조상 수정해야 하는 부분이 많고 현재 29CM에서 사용 중인 Django 버전에 따른 bulk_update() 미지원 이슈로 인해 기존 코드를 그대로 유지하기로 했습니다. (주어진 시간은 2~3일 밖에 없었기 때문이죠)

작업 시간 관계상 ES 싱크 작업만 비동기식으로 변경한 다음 물리적인 서버의 리소스를 최대한 활용하여 성능을 극대화하는 식으로 배치 속도를 개선하고자 했습니다.

  • 멀티 프로세스
  • 멀티 스레드

작업하기에 앞서 해당 배치는 CPU bound한 작업보다는 I/O bound한 작업이 주를 이루어 (DB에서 할인 및 환원 상품 대상 조회, 변경된 가격으로 DB에 반영) 멀티 스레드가 더 적합하다는 예상을 했습니다.

다음은 실제 작업 내용입니다.

멀티 프로세스의 경우 -처리해야 하는 리스트를 복제된 프로세스에 일정한 비율로 넘겨주고 각 프로세스에서 분기 처리해야 최적의 성능을 낼 수 있는데 현재 로직은 상품을 1개씩 처리하는 구조라서 상품을 1개씩 프로세스에 넘기다 보니 작업을 준비하는 비용이 더 크게 발생해 로직을 변경하지 않고선 성능이 좋지 않았습니다.

멀티 스레드의 경우 - 선택할 수 있는 라이브러리로 asyncio 와 concurrent의 ThreadPoolExecutor클래스가 있었습니다. 두 방식 모두 실제 동작은 동일하지만, 작업하는데 더 익숙한 cuncurrent의 ThreadPoolExcutor 클래스를 선택해 진행했습니다. 스레드의 worker는 12개로 지정해 DB 커넥션은 13개를 사용했고 내부 로직은 크게 변경하지 않았습니다.

다음은 기존 배치, ES 싱크만 변경한 배치, 멀티 프로세스, 멀티 스레드 총 4가지 버전으로 처리 시간을 테스트한 표입니다.

기존 배치의 속도가 너무 느려서 4000개 까지만 테스트를 했고 멀티 프로세스의 경우, 상품 4,000개를 테스트할 때 ES 싱크를 비동기 처리한 배치보다 느려서 테스트를 진행하지 않았습니다.

멀티 스레드가 더 적합할 거라는 초반에 예상대로 가장 빠른 모습을 볼 수 있습니다.

젠킨스 파이프라인을 활용하여 일괄 할인과 환원의 배치를 병렬로 실행

다음으로는 젠킨스에 할인과 환원이 각각의 job으로 등록된 것을 파이프라인으로 변경하는 작업을 진행했습니다.

처음에는 기존에 등록된 job을 연결할 수 있을 줄 알았는데 젠킨스에서 지원을 안 하는 건지 관련된 내용을 찾을 수 없었습니다. 따라서 기존에 등록된 job을 재사용하지 않고 Pipeline script를 새로 작성했습니다.

일괄 할인과 환원 배치 실행을 위해 새로 생성한 파이프라인의 내부 구조는 직렬 처리 방식이 아니라 병렬 처리 방식이 되도록 했습니다. 만약 직렬 처리 방식으로 할인 배치 완료 후에 환원 배치를 시작시키면 아래와 같은 이슈가 발생할 수 있습니다.

00:00 에 할인과 환원을 해야할 상품이 있다고 가정

직렬 처리 방식에 따라 할인을 완료한 후에 환원을 실행하게 됨

만약 00:00 에 시작한 일괄 할인이 30분이 걸렸다고 가정

그러면 00:30 에 일괄 환원 적용이 시작되는데 해당 상품의 일괄 환원 시작 시간은 00:00 — 문제 발생

아래는 위 내용들을 반영한 Pipeline script 코드입니다.

위와 같이 설정하면 할인과 환원을 병렬로 처리하고 두 작업이 모두 끝날 때까지 다음 예약 배치는 대기하게 됩니다. 즉, 00:00의 할인이 00:50분에 끝난다 해도 00:10의 환원 배치는 대기하므로 가격 수정이 되지 않는 문제를 해결할 수 있었습니다.

마무리

엔지니어링 작업에서는 항상 목표와 제약 사항이 존재한다고 생각합니다. 이번 상품 할인 / 환원 배치 성능 향상 작업에서도 1) 할인과 환원 로직 실행 과정에서 발생하는 override 이슈를 해결해야 한다는 목표와 2) 주어진 시간은 2~3일 정도밖에 되지 않는다는 제약 사항이 있었습니다. 그리고 이런 것들이 이번 작업의 중요한 의사 결정 기준이 된다고 생각했습니다. 근본적인 이슈를 해결하기 위해서는 당연히 대대적인 코드 수정과 설계 변경이 필요했지만, 위와 같이 명확한 의사 결정의 기준이 있었기 때문에 약간의 코드만을 수정하여 대응하는 과정에 대한 불편한 감정은 없었습니다. 다행히 해당 작업 이후 상품의 할인과 환원에 대한 override 이슈는 사라졌습니다. 다만 할인과 환원 대상 상품 수가 많아질수록 배치 실행 시간도 늘어나는 이슈는 여전히 존재했는데 이에 대한 해결 과정은 추후 또 다른 아티클을 통해서 공유할 수 있도록 하겠습니다.

감사합니다.

함께 성장할 동료를 찾습니다.

29CM (주)무신사 는 3년 연속 거래액 2배의 성장을 이루었습니다.

이제 더 큰 성장을 위해 기존 모놀리틱 서비스 구조를 마이크로서비스 구조로 전환하고, 앵귤러 기반 프론트엔드 코드를 리엑트로 전환하는 등의 기술적인 시도를 진행하고 있습니다.

함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다

많은 지원 부탁합니다!

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

--

--