AI와 함께, 멈추지 않는 OMS를 개선한 방법
author
postNo
status
thumbnail
description
category
tags
createdAt
updatedAt

시작하기

OMS(Order Management System)는 이미 정상적으로 주문을 처리하고 있었다. 장애가 난 시스템을 복구하거나, 당장 새로운 기능을 만들어야 했던 것도 아니다. 그럼에도 코드를 다시 들여다본 이유는 단순했다.
지금 잘 동작하는 것과, 앞으로도 안전하고 빠르게 바꿀 수 있는 것은 다른 문제이기 때문이다.
주문 한 건에는 상품, 회원, 혜택, 배송, 구독, 재고, 외부 판매 채널 등 여러 도메인이 관여한다. 흐름을 따라가 보니 같은 정보를 여러 단계에서 반복해서 조회했고, 상품 수가 늘어날수록 검증 쿼리도 함께 늘어났다. 주문 준비부터 저장, 재고 반영, 후처리까지 너무 많은 책임이 한 흐름에 모여 있기도 했다.
당장 장애를 만들지는 않더라도 이런 구조는 트래픽이 몰릴 때 데이터베이스 부담을 키운다. 작은 정책 하나를 바꿀 때도 수정·리뷰·테스트 범위가 넓어지고, 한 영역의 변경이 주문 전체에 영향을 줄 가능성도 커진다.
그래서 이번 작업의 목표를 단순한 성능 개선만이 아니었다.
  • 반복 조회와 병목을 줄일 것
  • 코드의 책임과 경계를 분명히 할 것
  • 기존 동작을 지키며 안전하게 변경할 것
  • 개선 효과를 운영 지표로 증명할 것
notion image
이미지 1. 하나의 주문 흐름에 조회·검증·저장·후처리 책임이 얽혀 있던 구조

클래스의 책임과 역할을 분리했다

기존 주문 생성 흐름에는 다음과 같은 일이 한곳에 모여 있었다.
  • 주문에 필요한 정보 조회
  • 상품·회원·배송 정책 검증
  • 주문 데이터 조립
  • 데이터베이스 저장
  • 재고 반영
  • 외부 시스템 연동과 후처리
이를 단순히 파일 여러 개로 나누는 데 그치지 않고, 같은 이유로 바뀌는 코드는 함께 두고 다른 이유로 바뀌는 코드는 분리했다. 이는 SOLID의 단일 책임 원칙(SRP)을 적용하는 과정이었다.
역할이 명확해지면 변경 이유도 명확해진다. 배송 정책이 바뀌면 배송 검증 영역을 보면 되고, 재고 반영 방식이 바뀌면 주문 데이터 조립 코드까지 함께 건드릴 필요가 없다.

구체적인 구현보다 계약에 의존한다

주문 코드가 상품·회원·재고의 내부 구현을 직접 알수록 시스템은 강하게 결합된다. 이번 개선에서는 각 역할이 무엇을 제공해야 하는지 인터페이스와 입력·출력의 형태를 명확히 하는 방향으로 정리했다.
이는 의존 역전 원칙(DIP)과도 이어진다. 주문은 다른 도메인의 상세 구현이 아니라, 필요한 결과를 돌려주는 계약에 의존해야 한다. 그래야 내부 구현이 바뀌어도 주문 전체가 함께 흔들리지 않는다.
notion image
이미지 2. 업무 의미에 따라 책임과 테스트 경계를 분리한 구조

읽기 좋은 코드는 테스트하기도 좋다

책임이 작아지면 입력과 출력이 분명해지고, 테스트할 범위도 줄어든다. 거대한 주문 생성 전체를 실행해야만 확인할 수 있던 정책을 작은 단위로 검증할 수 있다.
테스트하기 좋은 코드는 우연히 만들어지지 않는다.
  • 외부 의존성을 경계 밖으로 밀어낸다.
  • 한 함수나 클래스가 하나의 판단에 집중하게 한다.
  • 같은 입력에 같은 결과를 내도록 만든다.
  • 부수 효과가 발생하는 지점을 명시한다.
  • 실패 조건과 예외를 코드에 드러낸다.
이런 코드는 사람의 리뷰 속도를 높일 뿐 아니라 AI가 변경 의도를 파악하고 적절한 테스트를 제안하는 데도 유리하다. 결국 읽기 좋은 코드는 사람과 AI가 함께 안전하게 작업하기 위한 공통 인터페이스다.

길드

왜 TF나 정식 프로젝트가 아니라 ‘길드’였을까?
이 일은 스쿼드의 약속된 일정을 중단할 만큼 긴급하지 않았다. 하지만 정상적으로 운영되는 시스템의 기반을 다지는 일은 기능 개발에 밀려 계속 뒤로 가기 쉽고, 미룰수록 다음 변경의 비용은 커진다.
OMS는 내가 아임웹에서 처음 맡아 초기부터 오랫동안 기여해 온 제품이다. 한동안 다른 제품에 집중한 시기도 있었지만, 그동안 쌓여온 문제와 병목은 계속 파악하고 있었다. 제품의 맥락과 구조를 오래 이해해 온 만큼, 누구보다 깊이 문제에 접근할 수 있다고 생각했다.
그래서 정규 로드맵을 흔들지 않는 작은 실험으로 시작했다. 비록 혼자 참여한 개선 프로젝트였지만, 혼자만 알고 끝내는 작업은 아니었다. 조사한 내용, 변경 이유, 검증 결과, 다음 개선 후보를 공개했다. 동료가 변경을 검토하고 필요하면 이어갈 수 있도록 기록을 남겼다.
 
작게 시작하고, 근거를 공개하고, 동료가 이어갈 수 있는 형태로 남긴다.
 

어떻게 수정했나

AI를 적극적으로 활용했다. 반복되는 코드의 패턴을 찾고, 책임을 분리할 후보를 정리하고, 테스트 케이스를 보완하는 데 AI는 분명히 큰 도움이 됐다. 하지만 OMS는 주문과 결제, 재고의 일관성을 다루는 중요한 시스템이다. AI가 만든 변경을 그대로 신뢰할 수는 없었다.
AI는 작업 속도를 높여주는 도구였고, 변경의 책임은 여전히 개발자에게 있었다.

작은 배포를 반복하며 개선했다

큰 범위를 한 번에 수정하지 않았다. 먼저 현재 코드와 운영 지표를 살펴보고, 이번 사이클에서 안전하게 바꿀 수 있는 범위를 정했다.
책임이 섞인 지점, 반복 조회, 과도한 동시 실행, 다른 도메인의 데이터베이스에 직접 접근하는 지점처럼 개선 후보는 많았다. 이를 한 번에 처리하기보다 위험도와 의존관계를 기준으로 하나의 범위를 선택했다.
처음 세운 계획을 끝까지 한 번에 실행한 것도 아니다. 작은 변경을 배포하고 운영 지표와 회귀 여부를 확인한 뒤, 그 결과를 반영해 다음 수정 범위와 순서를 다시 계획했다.
수정 범위 계획 → 작은 단위의 PR → 코드 검수 → 테스트 → 배포 → 다음 범위 재계획
각 변경은 다음 과정을 반복했다.
  1. 이번에 수정할 범위와 영향을 계획한다.
  1. AI가 수정안을 사람이 한 번에 검수할 수 있는 매우 작은 단위의 PR로 분리한다.
  1. 변경 이유와 코드, 영향 범위를 사람이 직접 검수한다.
  1. 단위 테스트와 기존 회귀 테스트를 수행한다.
  1. 배포하고 운영 지표와 이상 여부를 확인한다.
  1. 확인한 결과를 바탕으로 다음 수정 범위를 다시 계획한다.

사람이 검수할 수 있는 크기를 AI의 작업 단위로 삼았다

AI는 각 사이클에서 현재 수정 범위를 조사하고, 변경 후보와 테스트 케이스를 정리했다. 범위가 독립적인 조사는 여러 작업으로 나누고, 결과를 다시 모아 충돌과 누락을 확인했다.
사람 쪽의 작업 책임자는 나 혼자였기 때문에 여러 AI가 만든 결과를 한꺼번에 검수할 수는 없었다. 그래서 AI에게 전체 수정안을 한 번에 구현하도록 하지 않고, 사람이 한 번에 검수할 수 있는 크기가 AI의 작업 단위가 되도록 하네스 환경을 구성했다. AI는 수정 범위를 작은 PR로 분리해 구현하고, 각 변경은 사람의 검수와 테스트를 통과한 뒤에만 다음 단계로 이어지도록 했다.
 
notion image
이미지 3. 계획부터 배포까지를 여러 번 반복한 개선 사이클
이 사이클을 반복하면 문제가 생겼을 때 원인을 좁히기 쉽고, 되돌려야 할 범위도 작아진다. 앞선 변경의 실제 효과를 확인한 뒤 다음 결정을 내릴 수 있어, 중요한 시스템을 전면 재작성하는 것보다 안전하게 개선을 이어갈 수 있었다.
 

실제로 바꾼 것들

반복 조회를 재사용했다

여러 단계가 동일한 배송 설정, 쇼핑몰 정책, 상품 정보, 회원 혜택을 다시 조회하고 있었다. 요청 초기에 필요한 정보를 준비해 컨텍스트로 전달하고, 이후 단계가 같은 결과를 재사용하도록 바꿨다.

상품 수만큼 늘어나던 검증을 배치화했다

상품마다 같은 종류의 검증을 반복하던 흐름은 상품 수가 늘수록 쿼리 수도 함께 증가했다. 필요한 상품을 모아서 한 번에 조회하고 검증하도록 바꿨다.
쿠폰 조회, 적립금 조회, 배송 설정 조회 등 여러 설정, 데이터 조회에서 같은 데이터를 n번 호출하고 있던것은 한 번으로 줄였다. 구매 수량 처리에서는 주문하는 상품 수 만큼 늘어나는 Writer DB SELECT 를 제거하고, Reader 조회도 한 번의 배치 쿼리로 축소했다.

조회와 쓰기의 경계를 정리했다

재고명이나 만료 쿠폰 로그처럼 조회 성격의 쿼리를 Reader로 이동하고, Writer 트랜잭션에는 꼭 필요한 UPDATE와 INSERT만 남겼다. 저장 중 불필요하게 connection과 lock을 오래 점유하지 않도록 하기 위해서다.

정확성 문제도 함께 고쳤다

성능을 살피는 과정에서 다른 사이트의 주문이 잘못 연결될 수 있는 조건, 요청한 상품 일부가 조회되지 않아도 검증을 통과할 수 있는 조건도 발견했다. 단순히 빠르게 만드는 것보다 올바른 결과를 만드는 일이 우선이었다.

캐시와 single-flight를 적용했다

짧은 시간에 같은 요청이 몰리는 구간에는 짧은 TTL의 캐시와 single-flight를 적용했다. 동일한 계산을 동시에 여러 번 실행하지 않고, 한 번의 결과를 함께 사용하도록 했다.

검수와 테스트는 선택이 아니었다

변경마다 기존 계산 결과와 처리 순서를 최대한 유지했다. 단위 테스트뿐 아니라 실제 주문 시나리오와 기존 회귀 테스트를 함께 확인했고, 배포 후에는 connection, slow-query, 응답시간과 실패율을 살폈다.
한 번의 큰 배포로 끝내지 않고 최소 단위의 변경을 반복 배포한 이유도 여기에 있다. 문제가 생기면 원인을 좁히기 쉽고, 되돌려야 할 범위도 작아진다.

얼마나 개선되었나

구조가 좋아 보이는 것만으로 개선을 증명할 수는 없다. 그래서 Writer DB connection과 slow-query 지표를 각각 개선 전후로 비교했다.

1. Writer DB connection 사용량

Datadog에서 동일한 조건의 안정 운영 구간을 비교했다.
지표
변화
평균
15.9% 개선
p50
15.0% 개선
p95
14.1% 개선
최대
19.2% 개선
관측 범위
전체 사용 구간 하향
정상 운영 구간의 Writer connection 사용량은 약 15~16% 감소했다. 전후 관측 범위의 폭은 모두 28이었다. 따라서 변동성이 줄었다기보다 전체 connection 사용 구간 자체가 낮아졌다고 해석하는 것이 정확하다. 7월 13일 동일 시간대와 비교해도 평균 약 17.6% 감소해 같은 방향을 확인했다.

무엇이 connection을 줄였나

  • 구매 수량 처리의 필요하지 않은 Writer SELECT 를 제거했다. Reader 조회는 한 번의 조회로 배치화하고, 조회성 SELECT를 Reader로 이동했다.
  • 쿠폰 조회, 적립금 조회, 배송 설정 조회 등을 N → 1번 조회로 줄였다.
  • 회원·쇼핑몰·구독 컨텍스트와 상품 조회 결과를 재사용하고 디지털·선물 검증을 배치화했다.
  • 반복 호출이 많은 조회 경로에는 캐시와 single-flight를 적용했다.
Writer pool 상한 자체는 낮추지 않았다. 따라서 이번 감소는 pool 제한에 따른 결과가 아니라 쿼리 제거·배치화·Reader 전환·캐시가 합쳐진 실제 점유 감소로 보는 것이 타당하다.
 

2. DB slow-query

slow-query 알림에서 실행시간이 포함된 메시지를 기준으로 두 비교일의 24시간 전체를 확인했다. 긴 SQL의 후속 분할 메시지는 건수에서 제외했고, 두 기간 모두 동일한 운영 환경과 임계값을 적용했다.
지표
변화
전체 알림 건수
69.2% 개선
시간당 알림
69.2% 개선
평균 실행시간
63.0% 개선
p95
88.7% 개선
최대
56.8% 개선
300초 이상
100% 개선
동일 30분 구간
100% 개선
이미지 4. 개선 전 값을 100으로 환산한 주요 지표 변화
 

어디에서 줄었나

첫 번째는 주문과 마이그레이션 정보를 결합하는 조회였다. 개선 전에는 평균 400초대의 알림이 184건 발생했지만, 개선 후 같은 형태의 알림이 사라졌다. 조인 조건을 인덱스에 맞춰 변경했고 쿼리 성능 개선을 위해 쿼리 구조를 변경한게 가장 직접적인 개선 요인으로 보인다.
두 번째는 주문 상태별 개수를 집계하는 조회였다. 알림이 893 → 221건으로 약 75.3% 감소했다. 캐시·single-flight 적용과 쿼리 단순화, 상관 조건 보강이 직접적인 개선 요인으로 보인다.
  • Read 알림: 1,753 → 563건 (-67.9%)
  • Write 알림: 144 → 22건 (-84.7%)
앞서 설명한 주문 생성 최적화는 Writer connection 감소에는 직접 기여했지만, 이번 slow-query 결과의 중심은 주문 목록과 상태별 개수 집계 조회였다. 따라서 조회 쿼리의 slow-query 감소에는 간접 효과로 분류하는 것이 정확하다.
 

마무리

이번 작업을 통해 다시 확인한 것은, 오래된 시스템일수록 큰 결심보다 작은 근거가 중요하다는 점이다.
이미 동작하는 것을 존중하면서 바꾸려는 이유를 기록하고, 한 번에 하나씩 수정하고, 테스트하고, 실제 지표로 확인해야 한다. AI는 이 과정을 훨씬 빠르게 만들어줬지만 판단과 검수의 중요성까지 대신해주지는 않았다.
오히려 AI 시대에는 코드의 책임과 경계, 이름과 테스트가 더 중요해졌다. 사람이 오해하기 쉬운 코드는 AI도 오해한다. 반대로 의도가 잘 드러나는 코드는 사람이 빠르게 리뷰할 수 있고, AI도 더 정확하게 변경을 보조할 수 있다.
혼자 시작했지만 혼자 끝내기 위한 길드는 아니었다. 정규 로드맵을 흔들지 않고 가능성을 확인하고, 결과와 근거를 남겨 동료가 다음 개선을 이어갈 수 있게 만드는 것이 목적이었다.
OMS 개선은 아직 끝나지 않았다. 하지만 끝나지 않아도 괜찮다.
좋은 시스템은 한 번의 거대한 재작성으로 완성되는 것이 아니라, 오늘보다 내일 더 안전하게 바꿀 수 있도록 만드는 작은 개선의 연속이기 때문이다.