본문 바로가기

개발

신입 개발자의 3천 줄 if문 레거시 파괴하기

(YOUTHCON'25에서 발표한 내용을 바탕으로 작성했습니다. 발표에서는 시간 상의 이유로 담지 못했던 내용도 틈틈이 추가했습니다.🙂)

 

이 글은 다음과 같은 독자를 대상으로 작성했습니다.

 

✔︎ 처음으로 거대한 레거시 코드를 마주하고 "이걸 내가 건드려도 될까?"라는 두려움을 느껴본 주니어 개발자

✔︎ 이론과 현실 사이의 코드 괴리가 왜 생기는지 궁금한 주니어 개발자

 

결론부터 말씀드리면 저는 이 작업을 성공적으로 마무리하게 됩니다! 다음 사진은 레거시 개선 작업 배포 후 저의 실장님께서 보내주신 DM인데요. 어마무시해 보이는 레거시를 개선하는 일, 저도 했으니 누구나 도전할 수 있다는 희망적인 메시지를 전달하고 싶다는 마음으로 이 글을 작성하게 되었습니다.

새벽까지 함께 해주신 실장님 감사합니다..(_ _)

 

회사에서 처음으로 레거시 개선 프로젝트를 진행하게 되었을 때 저는 고작 8개월 차 개발자였습니다. 제가 개선해야 하는 레거시는 '쿠폰'이었는데요. 저는 그때 당시 쿠폰에 대해 아무것도 아는 것이 없었습니다. 게다가 요구사항도 조금 불명확했습니다. 시니어 개발자 A 분께서는 저에게 "쿠폰 관련 로직을 DB화 하면 됩니다."라고 말씀하시고, 또 다른 시니어 개발자 B 분께서는 "이거 괴물 메서드 제거하는 작업이에요."라는 말씀을 하셨습니다. 또 기획자분이 저에게 전달주신 기획서에는 어드민 기능 추가에 대한 이야기만 있었습니다. 이러한 요구사항을 들은 저는 쿠폰 로직을 DB화 한다는 것이 어떤 의미인지, 또 괴물 메서드를 왜 제거해야 하는지, 어드민 기능 추가는 쿠폰 로직 개선과 어떤 연관성을 갖는 것인지 등 저에게 주어진 태스크가 무엇인지에 대한 감이 잘 오지 않았습니다.

 

그리고 이 프로젝트에서 저를 가장 막막하게 만들었던 것은 쿠폰 도메인이 결제 로직이라는 점이었습니다. 개선 대상인 결제 API 메서드는 이미 몇 천 줄 규모였고, 쿠폰 로직은 그 내부에 있었습니다. 뿐만 아니라 결제 메서드 내부에서는 쿠폰 로직 이외에도 제품 배송, 포인트 적립, 결제 금액 계산・・. 등등 정말 다양하고 많은 일들이 벌어지고 있었습니다. 또 실제로 팀 내에서 1시간 동안 발생한 쿠폰 에러가 3,400만 원의 손실로 이어지는 현장에 제가 있었기 때문에 저의 두려움과 걱정은 극대화되어 갔습니다.

 

이러한 막막한 상황 속에서 제가 어떻게 레거시를 개선해 낼 수 있었는지를 다음과 같은 순서로 공유드리겠습니다.

 

1. 쿠폰의 동작 이해해 보기

2. 문제 정의하기

3. 문제 제거하기

4. 리스크 관리하기

5. 결과

 

레거시 개선기 1: 쿠폰의 동작 이해해 보기

저는 쿠폰에 대해 정말 아무것도 몰랐기 때문에 먼저 쿠폰을 이해해 봐야겠다고 생각했습니다. 하지만 곧바로 어떻게 쿠폰을 파악해야 할지 막막해졌습니다. 문제는 여러 가지가 있었지만, 가장 큰 이유는 10년 동안 운영되었다고 하는 이 쿠폰에 대해 정리되어 있는 히스토리 문서가 없었기 때문입니다. 또 앞선 결제 로직 이슈와 관련된 이유들로 인해 저의 두려움은 굉장히 커진 상태였기 때문에 저는 모든 걸 눈으로 직접 하나하나 확인해 보자는 결론에 이르게 됩니다. 많은 분들이 AI의 도움을 받지 않고 개발하는 분들을 '원시인'이라고 표현하시던데, 제 안의 원시인이 나타난 것입니다! 제 안의 원시인 개발자는 다음과 같은 특징을 갖고 있는데요.

원시인 개발자 특징.jpg

 

수작업에 굉장히 특화된 인물이라고 볼 수 있습니다. :D 그럼 제 안의 원시인과 함께 어떻게 한 땀 한 땀 쿠폰을 이해할 수 있었는지 소개드리겠습니다. 먼저 쿠폰을 직접 사용해 봤습니다. 쿠폰을 어떻게 발급받는지, 어떻게 사용하는지, 어떤 종류가 있는지 등 쿠폰과 관련된 기능들을 모두 사용해 보면서 쿠폰 역할에 대한 전반적은 윤곽을 잡았습니다.

 

이후에는 사내 곳곳에 흩어진 히스토리를 찾았습니다. 먼저 슬랙과 컨플루언스에 '쿠폰'이라는 키워드로 검색해 나온 문서들을 하나도 빠짐없이 읽어봤습니다. 과거에 진행되었던 프로모션이나 CS(Customer Service) 이슈를 살펴보면서 쿠폰이 어떤 식으로 운영되는지, 또 어떤 이슈들이 있는지 등을 파악했습니다. 과거 동료들이 진행했던 쿠폰 프로모션과 관련한 PR도 모두 확인했습니다. 쿠폰 로직 3천 줄이 10년 동안 그대로 방치되었던 것은 아니고, 몇 번의 개선 시도가 있었기 때문에 이와 관련한 작업 PR을 살펴보는 것도 히스토리 파악에 큰 도움이 되었습니다.

 

또 DB에 적재된 데이터도 확인했습니다. 쿠폰을 사용했을 때 각 테이블에 어떤 데이터들이 쌓이는지, 쿠폰 사용을 취소했을 때는 어떤 데이터들이 바뀌는지, 또 쿠폰 사용으로 인해 영향받는 다른 테이블/컬럼은 없는지 등을 파악하며 DB 구조에 대해서도 이해하고자 했습니다. 이렇게 했는데도 이해가 안 되는 부분은 히스토리를 가장 많이 알고 계시는 실장님, 도메인 전문가이자 DBA 분, 쿠폰과 관련된 작업을 담당해 봤던 팀원들, 프론트엔드 파트장님 등 여러 분들께 여러 차례 질문하며 이해해 나갔습니다.

 

다음으로는 제가 인텔리제이에서 제가 가장 좋아하는 기능 중 하나인데요. 바로 디버거를 활용하는 것입니다. 쿠폰을 사용했을 때 호출되는 결제 메서드의 가장 첫 번째 줄에 breakpoint를 찍고, 한 줄씩 실행시키면서 코드를 파악했습니다. 특히 복잡한 의존성을 갖는 구조이기 때문에 제가 수정하게 될 부분이 제가 생각했던 영향 범위를 벗어나서 문제를 발생시키지는 않을지에 대해서 가장 주의하며 살펴봤습니다. 복잡한 의존성을 갖는 구조에서는 다음과 같은 문제가 생길 수 있기 때문입니다.

예를 들어 위와 같은 구조에서 제가 수정하고자 하는 서비스는 CouponService 내부의 useCoupon() 메서드라고 가정해 봅시다. CouponService의 useCoupon()이라는 메서드에서 쿠폰을 사용하고, 고객 포인트를 적립하고 있었지만 단일 책임 원칙(SRP)을 위반한 이 메서드를 개선하고자 고객 포인트 적립 로직을 분리했다면 DeliveryService나 PointService에서는 멀쩡히 잘 적립되던 고객 포인트가 더 이상 적립이 되지 않는 문제가 생길 것입니다. 실제로 팀 내의 다른 프로젝트에서도 이렇게 연쇄적으로 연결되어 있는 구조로 인한 사이드이펙트가 이슈로 이어지는 일이 많이 있었고, 해당 결제 로직은 저희 서비스에서 가장 복잡한 괴물 메서드였기 때문에 더더욱 꼼꼼히 살펴봤습니다. 또한 필요한 경우 옛날 커밋 기록을 추적해보기도 했습니다.

 

이렇게 쿠폰의 역할부터 쿠폰을 관리하는 DB와 코드까지 모두 살펴봤습니다. 이후 제가 이해한 내용에 기반해 쿠폰과 관련된 문서를 작성해 봤습니다. 쿠폰의 역할이 뭔지, 또 쿠폰을 표현할 때 어떤 용어를 사용하는지, 어떤 종류가 있는지, 관련 테이블에는 뭐가 있는지, 작업 시 주의사항은 무엇인지 등을 컨플루언스에 정리해 보았습니다. 여담이지만, 이렇게 파악한 내용을 저의 언어로 정리해 보면서 조각조각 파편화 되어 이해되지 않던 쿠폰에 대한 정보가 한 번에 이해되는 순간이 있었는데요. 이때 쿠폰 10년의 역사를 들여다본 느낌이라 레거시가 재미있다고 생각하기 시작했습니다.

 

레거시 개선기 2: 문제 정의하기 (개발 관점)

우아한테크코스에서 '메서드는 10줄 이하로 유지한다'는 요구사항을 습관처럼 지키면서 개발을 해왔습니다. 그럼에도 불구하고 저는 if-else가 3,000줄에 달하더라도, 이미 안정적으로 운영되고 있고 개발 효율에 큰 문제가 없다면 레거시 자체가 문제가 된다고 생각하지는 않았습니다. 그러면 저희의 레거시에는 어떤 문제가 있었을까요?

 

먼저 문제를 정의하기에 앞서 제가 개선해야 하는 부분이 어디였는지에 대해 소개드리겠습니다. 개선 대상 로직은 제품 판매 서비스에서 제품 구매하기 버튼을 클릭했을 때 호출되는 API 내부 로직에 있었습니다. (저희 회사의 제품 판매 서비스는 대외 공개가 불가하기 때문에 무신사의 제품 판매 페이지 화면으로 대체하였습니다.)

(출처: 무신사)

이 구매하기 버튼을 클릭했을 때 호출되는 결제 API에서는 크게 다음과 같은 순서로 실행됩니다.

 

1. 사용자가 쿠폰을 사용해 결제 요청

2. 쿠폰의 할인 금액 계산

3. 계산된 할인 금액을 기반으로 결제 처리

 

그리고 제가 개선해야 했던 부분은 2번에 해당하는 할인 금액을 계산하는 로직이었습니다. 그렇다면 무려 3,000 줄에 달하는 이 할인 금액 로직은 대체 어떤 모습이었을까요?

.

.

.

.

.

바로 이런 모습이었습니다!

[쿠폰 할인 금액 계산 로직]

 

쿠폰 id가 1에 해당하면 고객 포인트를 적립하고, 쿠폰 id 1을 사용하여 제품을 1개 구매한 경우 제품을 10,000원 할인하고, 또 2개를 구매하면 2만 원을 할인하고・・. 이처럼 쿠폰의 할인 금액과 혜택에 대한 정보가 무수한 if-else 체인으로 누적되어 있었습니다. 이 코드의 문제점이 뭐라고 생각하시나요? 많은 문제점이 눈에 보이실 텐데요. 제가 생각하기에 이 코드의 가장 큰 문제점은 DB 단에서 관리되어야 할 쿠폰 개별에 대한 정보(A)와 코드 단에서 관리되어야 할 쿠폰 기능에 대한 정보(B)가 코드에 혼재되어 있다는 점이었습니다.

사진의 if문 내부를 확인해 보면 제품 1개, 10,000원, 20,000원, 10% 등 쿠폰 id 별 할인 금액 등 DB에서 조회해야 할 정보와, 할인 금액을 빼거나 제품에 할인율을 곱한 만큼을 빼는 등의 계산 동작이 모두 코드 단에서 관리되고 있는 것을 보실 수 있을 것입니다. 이렇게 쿠폰에 대한 정보와 기능이 혼재되어 관리되는 코드로 인해 구조를 추상화시키지 못하고 새로운 쿠폰이 추가될 때마다 새로운 if-else를 추가하는 작업으로 이어지게 됩니다. 즉, 쿠폰 추가가 코드 수정과 배포에 강하게 결합되어 있어 자동화가 사실상 불가능한 구조였습니다.

 

예를 들어보겠습니다. 사용자 A가 coupon.id = 101에 해당하는 쿠폰을 사용해 제품 1개를 구매했다고 가정해 봅시다. 그러면 결제 API를 호출하게 되고, 결제 API 내부에서는 쿠폰의 할인 금액을 계산하기 위해 위 사진에 있는 3천 줄 if문까지 도달하게 됩니다. 그리고 첫 번째 if문을 만나게 되면, 사용자가 사용한 coupon.id는 1이 아니기 때문에 지나칩니다. 두 번째 else if문도 coupon.id가 2에 해당하는 쿠폰에 대한 정의이기 때문에 지나갑니다. 이렇게 쭉 coupon.id가 100에 해당하는 마지막 분기문까지 확인을 해봐도 coupon.id가 101에 해당하는 쿠폰 할인 금액에 대한 정보가 없습니다. 결과적으로는 사용자가 쿠폰을 사용했음에도 결제 금액이 0원으로 계산되는 문제가 발생하게 됩니다.

 

따라서 이 코드의 가장 큰 문제점은 기존의 쿠폰과 정확히 동일한 동작을 하는 신규 쿠폰을 추가하기 위해 코드 id에 매핑되는 할인 금액 계산 로직을 매번 새로 작성해야 하고, 코드 작업이 이루어졌으니 당연히 검증과 배포도 반복적으로 진행해야 한다는 점이었습니다. 이렇듯 쿠폰의 할인 금액을 계산하는 로직이 매번 if-else를 하나하나 추가하는 방식으로 구현되어 왔기 때문에 쿠폰 할인 금액을 계산하는 로직은 더 이상 자동화가 불가능한 것이었습니다. 그러면 이러한 코드는 어디에서 시작된 걸까요?

 

이것 또한 예시로 설명해 보겠습니다. 먼저 초기의 쿠폰 coupon.id = 1을 사용하면 구매한 제품 개수에 상관없이 10,000원 할인되도록 구현해 달라는 요구사항이 있습니다. 그리고 이 할인 정보를 다음과 같은 테이블에 저장하게 됩니다.

 

[초기 coupon 테이블 구조]

 

이후 시간이 흘러 쿠폰 규칙이 늘어나고, coupon.id = 20인 쿠폰이 추가되고 이에 대해 다음과 같은 요구사항을 전달 받습니다.

 

  제품을 1개 구매하면 10,000원 할인한다.

  제품을 2개 구매하면 20,000원 할인한다.

 

그리고 이 정보를 테이블에 추가하기 위해 위의 [초기 coupon 테이블 구조] 이미지의 coupon 테이블을 다시 확인해 봅시다. 1번 요구사항에 대한 할인 금액 10,000원은 discount_amount 컬럼에 추가하면 될 것 같은데, 2번 요구사항에 대한 20,000원 할인 금액은 어디에서 관리할 수 있을까요? 아마 꽤 명확한 해결 방법이 떠오르실 테지만, 저희의 레거시는 다음과 같은 선택을 합니다.

뿐만 아니라 시간이 흘러 새로운 유형의 쿠폰이 추가되었을 때도 새로운 테이블을 설계하지 않고 기존 coupon 테이블에 컬럼을 확장하는 방식으로 대응해 이 coupon 테이블은 점점 비대해졌습니다. 이로 인해 관리되지 않는 컬럼이 점점 늘어났고, 더 이상 coupon 테이블은 쿠폰에 대한 정보를 제대로 표현할 수 없는 구조가 되었습니다. 이렇게 초기의 쿠폰 스키마 설계에서 제1 정규화를 위반하면서 더 이상 쿠폰에 대한 규칙을 테이블만으로는 표현할 수 없어, 쿠폰에 대한 정보가 코드 단의 if-else까지 이동할 수밖에 없었던 것입니다.

 

그래서 저는 제가 해결해야 하는 문제를 다음과 같이 두 가지로 정리했습니다.

 

(개발 관점) 지속적으로 확장되는 쿠폰 규칙을 고려하지 않은 구조

(운영 관점) 쿠폰 하나를 추가하는 데 최소 2~3일, 길게는 1주일 이상 소요

 

레거시 개선기 3: 문제 제거하기 (개발 관점)

이제 문제를 파악했으니 문제를 제거할 차례입니다. 문제에 대한 해결 방법은 간단합니다. 테이블을 정규화하면 되는 것인데요. 과도하게 많은 쿠폰 정보를 관리하던 쿠폰 테이블 1개를 쿠폰 테이블 8개로 정규화했습니다. 그리고 기존의 데이터를 모두 정규화된 쿠폰 테이블로 모두 마이그레이션 했습니다. 하지만 앞서 언급한 것처럼 쿠폰에 대한 정보는 코드와 DB에 분산되어 있었습니다. 또 if-else 내부에만 있는 것이 아니라, 쿼리, 서비스 코드, 도메인 등 산재되어 있었습니다. DB 데이터 또한 마찬가지였습니다. 쿠폰 테이블에서 호출되는 프로시저, 결제와 관련된 테이블 등 비규칙적으로 흩어져있었습니다. 이러한 정보를 점진적으로 하나씩 파악해 운영 중인 쿠폰 정보를 기준으로 마이그레이션 해나갔습니다. 이렇게 마이그레이션 하니 코드와 DB 단에 흩어져있던 쿠폰 정보를 DB 기반 관리 구조로 일원화할 수 있었습니다.

 

이 시점에서 다시 레거시 개선기 2에서 파악했던 코드 단의 문제를 확인해 보겠습니다. DB 단에서 관리되어야 할 쿠폰 개별에 대한 정보(A)와 코드 단에서 관리되어야 할 쿠폰 기능에 대한 정보(B)가 코드에 혼재되어 있다는 점이었는데요. 테이블을 정규화 함으로써 쿠폰 개별에 대한 정보(A)를 코드 밖, DB 단으로 분리할 수 있게 된 것입니다.

 

그러면 이제 코드 단에서 관리되어야 할 쿠폰 기능에 대한 정보(B)를 개선해 볼 수 있습니다. 3천 줄의 if문을 모두 분석해 보니 실제로 쿠폰의 유형은 다음과 같이 총 3가지였습니다. (다소 각색이 있습니다.)

[쿠폰 타입 별 계산 로직]

쿠폰 타입 A: 결제 금액 - {결제 금액 * 할인율}

쿠폰 타입 B: 결제 금액 - {할인 금액}

쿠폰 타입 C: 결제 금액 - {n 번째 구매 제품 금액}

 

따라서 전략 패턴을 사용해 쿠폰 타입별 역할을 각 구현체에 정의했습니다. 그리고 DB에서 쿠폰 id로 조회한 쿠폰의 타입과 일치하는 구현체를 실행시키고, 또 DB에서 쿠폰 id로 조회한 할인 정보를 활용해 할인 금액을 계산하도록 자동화할 수 있었습니다. 이렇게 개선하니 새로운 if-esle문 없이도 DB에 저장되어 있는 쿠폰 타입과 할인 금액 정보를 활용해 할인 금액 계산이 가능해졌습니다. 또한 새로운 쿠폰 타입을 추가해 달라는 요구사항이 생기더라도, 기존 쿠폰 타입 구현체는 건드리지 않고 새로운 쿠폰 타입에 해당하는 구현체를 새로 추가하는 것만으로도 확장 가능한 구조로 개선할 수 있었습니다. 그럼 이제 예제를 통해 개선된 로직을 살펴보겠습니다.

 

[신규 쿠폰 추가 작업]

1. coupod.id = 101에 해당하는 쿠폰을 추가해달라는 요청을 받습니다. 그리고 다음과 같은 요구사항이 있다고 가정하겠습니다.

☐ 쿠폰 타입은 B로 지정한다.

☐ 제품 1개 시 할인 금액은 10,000원으로 계산된다.

 2개 구매 시 할인 금액은 20,000원으로 계산된다.

 

2. 개발자인 저는 전달받은 요구사항에 맞춰 앞서 정규화한 8개의 테이블에 쿠폰 정보를 저장합니다. 8개의 테이블 중 쿠폰의 타입과 쿠폰 할인 금액 저장 테이블은 다음과 같은 모습일 겁니다.

[8개의 테이블에 쿠폰 정보 추가]

이렇게 DB에 추가만 완료되면 이제 coupon.id = 101에 대한 쿠폰 추가 작업은 완료된 것인데요! 기존에는 새로운 쿠폰을 추가하려면 무수한 if-else문의 마지막에 또 다른 else-if 블록을 추가해 신규 쿠폰에 대한 할인 혜택 정보를 추가해야 했다면, 이제는 DB에 쿠폰에 대한 정보를 저장하는 것만으로도 쿠폰 추가작업을 완료할 수 있게 된 것입니다. 그럼 이제 사용자가 쿠폰을 사용했을 때의 로직을 확인해 보겠습니다.

 

[쿠폰 할인 금액 계산 로직]

1. 사용자가 coupon.id = 101에 해당하는 쿠폰을 사용해 제품 1개에 대한 결제 요청을 진행한다고 가정합니다.

2. 코드 단에서는 [8개의 테이블에 쿠폰 정보 추가] 테이블에 추가했던 테이블을 조회해 쿠폰의 타입을 확인합니다. coupon.id가 101인 쿠폰 타입은 B입니다.

3. 쿠폰 타입별 구현체를 확인해 쿠폰 타입 B에 해당하는 구현체를 실행합니다. 쿠폰 타입 B의 할인 금액 계산 로직은 결제 금액 - {할인 금액}입니다.

4. 이제 할인 금액을 계산합니다. 3번 {할인 금액}을 조회하기 위해 DB를 조회합니다. coupon.id는 101, 제품 수는 1개이므로 할인 금액은 10,000원입니다.

5. 결과적으로 사용자가 coupon.id = 101을 사용해 제품 1개를 구매했을 때의 할인 금액은 10,000원으로 계산됩니다.

이렇듯 DB에 쿠폰 정보를 추가하는 것만으로도 쿠폰 할인 금액이 계산되도록 자동화되었습니다!

 

레거시 개선기 2: 문제 정의하기 (운영 관점)

그런데 제 눈엔 개선된 프로세스에서 자동화가 가능한 또 하나의 항목이 눈에 띄었습니다. 바로 정규화한 8개의 테이블에 쿠폰 정보를 저장하는 작업입니다. 사실 이 부분은 초반에 기획자 분께서 저에게 전달 주셨던 기획서의 어드민 기능을 추가에 해당하는 요구사항이었습니다. 어드민 기능 추가 요구사항의 의미는, 기획자도 어드민 기능을 통해 신규 쿠폰을 추가할 수 있도록 쿠폰 추가 과정을 자동화해 달라는 요청이었습니다. 따라서 기획자에게 신규 쿠폰 추가에 대한 요구사항을 전달 받으면 개발자인 제가 직접 DB에 쿠폰 정보를 추가해야 했던 그 과정을 어드민 기능으로 자동화하면 되는 것입니다. 그렇게 어드민 기능까지 추가 완료하니 다음과 같이 정말 간단한 프로세스로 쿠폰 추가 작업을 완전히 자동화할 수 있었습니다.

 

1. 기획자가 어드민 쿠폰 등록 기능을 사용해 신규 쿠폰 정보 입력 및 추가함

2. 어드민에 등록 완료한 쿠폰 정보는 DB에 저장됨

3. 쿠폰 추가 완료! 🎉

 

레거시 개선기 4: 리스크 관리하기

이렇게 코드 작업을 마쳤으니 이제 테스트를 진행할 단계입니다. 테스트에 앞서 저는 다시 걱정이 되기 시작했습니다. TDD로 구현하긴 했는데 제 테스트 코드는 단위 테스트 수준에서 멈췄고, 또 테스트 코드가 있더라도 개발 서버에서는 지속적으로 에러가 발견되었습니다. 또 결제 로직이라는 두려움과 에러가 발생하면 어떻게 복구해야 할지 등 어떻게 안전하게 리스크를 관리해야 할지에 대한 수많은 두려움이 생겼기 때문입니다.

 

이때 저의 시니어 개발자 A께서 과거에 저에게 해주신 말씀이 떠오릅니다.

 

🤵🏻‍♂️ 시니어 개발자 A: 저는 버튼 하나하나 다 눌러봤어요.

 

그리고 제 안의 원시인 개발자가 다시 나타납니다.

다시 모든 걸 수작업으로 테스트하기 시작했습니다. 가장 먼저 테스트 시나리오를 직접 작성해 1개의 쿠폰 당 거의 100개가 넘는 테스트 케이스로 직접 수동 테스트를 진행했습니다. 또 제가 작업한 코드를 다시 디버깅하며 놓친 부분은 없는지, 이 코드가 어떤 영향을 주고받는지를 직접 확인했고, 데이터 정합성 검증 쿼리를 작성해 하나씩 실행시켜 보면서 눈으로 이상 데이터를 확인했습니다. 이밖에도 사이드이펙트를 확인하기 위해 페이지 내에 눌러볼 수 있는 모든 버튼을 눌러보거나 기존 API의 JSON 응답값을 직접 비교하는 등 수작업으로 할 수 있는 많은 방법을 생각해 보며 하나씩 테스트를 해나갔습니다.

 

이렇게 하나씩 다 확인하며 불안해하는 저에게 시니어 개발자 A가 오셔서 이런 방법을 소개해주셨습니다.

 

1. Feature Toggle 적용하기

제가 회사에서 작업했을 때는 'Switch Flag'라는 용어를 사용했고, 제 지인의 회사에서는 'Kill Switch'라는 용어를 사용한다고 하더라고요. 이 방법은 DB 컬럼을 플래그 값으로 두고, 이 플래그 값에 따라 실행해야 하는 코드를 분기 처리 하는 것입니다. 코드와 테이블 구조는 다음과 같은 모습입니다.

 

api_switch 테이블의 is_switch 컬럼이 false인 경우 신규 메서드를 호출하고, true인 경우에는 레거시 메서드를 호출하도록 설계하는 방식입니다. 이렇게 설계하면 혹여나 수정한 신규 메서드에서 오류가 생겼을 때 별도로 롤백을 위한 배포 작업 없이 DB 컬럼 값을 업데이트하는 것만으로도 롤백이 가능하게 됩니다. 저희 회사는 한 번 배포를 할 때 약 20분 ~ 40분 사이로 소요되었고, 또 아마 다른 많은 회사에서도 배포에 상당 시간이 소요될 것이라고 생각합니다. 이때 이 Feature Toggle 방식을 적용하면 모니터링 중 발견한 이슈가 더 큰 이슈로 번지기 전에 바로 롤백하여 빠른 이슈 대응이 가능하다는 장점이 있습니다. 특히 이렇게 수많은 레거시 로직이 얽혀있는 작업에서는 언제 어디에서 에러가 터질지 예측하기 어렵기 때문에 항상 이슈에 대응하기 위한 방법들을 미리 고려하는 것이 중요하다고 생각합니다. 그런 점에서 이 방식이 저에겐 정말 많은 걱정을 덜어주었습니다.

 

참고로 배포 후 모니터링 기간 동안은 롤백이 가능해야 하기 때문에 이 모니터링 기간 동안에는 신규 쿠폰 추가 시에도 기존의 if-else 작업도 병행해야 합니다. (마이그레이션을 완료했더라도, 기존의 coupon 테이블은 건드리지 않았기 때문에 기존의 레거시 코드도 실행될 수 있는 환경입니다.) 롤백을 진행했는데도 기존 if-else에 할인 금액 계산 로직이 없다면 할인 금액은 0원으로 계산될 테니까요!

 

2. AWS Lambda를 활용한 이슈 알림 자동화하기

캘린더에 알람을 맞춰두고 일정한 시간 간격으로 쿼리를 실행해 보며 데이터 정합성을 검증하던 저에게 저의 시니어 개발자 A께서 AWS Lambda를 활용한 이슈 알림 자동화 방법을 소개해주셨습니다. AWS Lambda는 서버를 직접 구축하거나 관리하지 않고도 코드를 실행할 수 있는 '서버리스 컴퓨팅 시스템'입니다. AWS Lambda는 EventBridge라는 서비스를 활용해 정기적으로 코드를 실행할 수도 있습니다. 그래서 저희 서비스에 AWS Lambda가 정기적으로 호출할 수 있는 API를 하나 만들고, 해당 API에서는 DB를 조회해 정합성 검증을 진행하도록 구현합니다. 그리고 데이터에 이상이 있을 경우 응답값으로 Lambda에게 전달해 Lambda에서 슬랙으로 알림을 전송하도록 구현하면 됩니다. 사람이 매번 직접 쿼리를 실행한다면 당연히 실수가 발생할 수 있고, 또 상황에 따라 확인이 불가능할 수도 있기 때문에 이렇게 이슈에 대한 모니터링은 최대한 자동화하는 것이 가장 좋다고 생각합니다.

 

결과

이렇게 모든 작업을 마치니 4개월이 흘렀습니다. 그리고 저는 배포 버튼을 눌렀습니다! 결과는 어떻게 됐을까요?

.

.

.

.

🎉🎉🎉

쿠폰 작업 시간 1주일에서 1분으로 단축

해당 개선 작업으로 인한 에러 0건

🎉🎉🎉

 

그리고 이 작업 이후 저는 "초기 테이블이 쿠폰을 표현할 수 있는 구조였다면, 거대한 if문은 필요 없지 않았을까?"라는 생각이 들면서 레거시는 작은 규칙 하나를 무시할 때마다 조금씩 무너지고, 그 누적이 레거시가 된다는 생각으로 이어졌습니다. 또 최근 생성형 AI가 정말 빠르게 멋진 코드를 만들어주는 걸 보면서 좋은 프로그램 구조를 위해 고민하는 시간이 정말 의미가 있는 것일지에 대한 고민을 했었는데요. 이러한 레거시를 개선해 보니 견고한 프로그램은 결국 좋은 설계를 위해 고민한 시간이 쌓여 만들어진다는 생각을 하게 되었습니다.

 

그리고 이 레거시 개선 작업은 저에게 많은 것을 남겼습니다. 저는 이제 제 작업뿐만 아니라 제 작업의 영향 범위까지도 고려할 줄 아는 넓은 시야를 갖게 되었고, 도메인을 깊게 이해하는 것이 곧 문제를 이해하는 것이라는 점도 알게 되었습니다. 또한 단순히 테스트 코드로만 테스트를 할 줄 알았던 저에게 서비스의 안전성을 책임지는 테스트에는 수많은 다양한 기법이 존재한다는 것도 배울 수 있는 기회였습니다. 그리고 무엇보다 저는 사실 이 레거시를 개선하는 작업이 꽤 재밌었습니다. 이 프로젝트 덕분에 레거시에 대한 애정과 관심이 생긴 것입니다!

 

저에게 수많은 고민도 남겼습니다. 더 효율적이고 안전하게 테스트할 수 있는 방법은 없었을지, 낮은 트래픽 환경이라 드러나지 않은 오류는 없었을지, 데이터 규모가 더 컸다면 어떻게 안전하게 마이그레이션 할 수 있었을지 등 이 프로젝트를 경험하지 않았다면 주니어 개발자로서는 고민해 볼 생각도 못했을 수도 있습니다. 이런 고민을 남겼다는 것 자체만으로도 저에게 이 프로젝트는 정말 의미 있는 경험이 되었습니다.

 

레거시 개선, 그 후

이렇게 저의 레거시 개선 과정을 모두 소개드렸는데요. '신입 개발자의 3천 줄 if문 레거시 개선하기'라는 거창한 제목에 비해 제가 이 문제를 해결한 방법은 DB 정규화나 전략패턴과 같이 간단한 방법이었습니다. 혹시 "3천 줄 레거시 별 거 없네ㅋ"라고 생각하신 분이 계시다면, 제 글의 취지를 정확히 이해하셨는데요! 누군가 제게 "이제 레거시 자신 있으신가요?"라고 묻는다면 제 대답은 여전히 "아니요"입니다. 저는 레거시가 많이 어려웠고, 지금도 여전히 두렵습니다. 

 

하지만 두려운 마음 덕분에 오히려 더 꼼꼼하고 조심스럽게 확인할 수 있었고 그게 레거시를 이해하는 데 가장 큰 도움이 되었습니다. 저는 레거시를 충분히 다뤄본 경험도 없었고, 해당 작업에서는 많은 개발자 분들이 알고 계시는 방법으로 문제를 해결했습니다. 그래도 진짜 잘 해내고 싶다는 마음은 분명했고, 이런 마음가짐 덕분에 끝까지 해낼 수 있었다고 생각합니다. 그래서 저는 앞으로도 모르는 문제 앞에서 도망치기보다 이해하려는 선택을 하려고 합니다. 그러니 해본 적 없는 큰 작업 앞에서 저와 비슷한 고민을 가지고 계신 주니어 개발자가 계시다면, 저처럼 하나씩 이해해 나가는 선택만으로도 충분히 시작할 수 있다는 이야기를 전하고 싶었습니다.

 

발표 후

발표 다음날 링크드인을 통해 다음과 같은 재미있는 질문도 받았었는데요!

 

사실 주니어 개발자인 저에게 빠르게 발전하는 AI가 조금은 위협적이라고 생각한 적도 있었습니다. 지금도 그 위협감이 아예 사라졌다고 할 수는 없지만, 그래도 이제는 AI에 대해 거부감을 느끼거나 배척해야 할 대상으로 생각하지는 않습니다. 해당 레거시 개선 작업 이후에도 잠깐 레거시 개선 프로젝트에 참여한 적이 있었는데, 그때는 이 프로젝트와는 다르게 다양한 생성형 AI를 사용하며 최대 효율로 레거시를 개선해 보기 위해 다양한 시도와 경험을 해보기도 했습니다. 그리고 앞으로도 이러한 AI를 최대한 잘 활용해보고 싶다는 생각을 합니다! 또 반면에 제가 이 프로젝트에서의 문제를 DB 정규화와 같은 기초적인 CS 지식을 바탕으로 해결한 것처럼, 기본기를 꾸준히 학습하는 것도 중요하다고 생각합니다. 그래서 앞으로는 AI를 도구로 잘 활용하는 동시에, 기본기를 단단히 다지는 방향으로 꾸준히 학습해 나가고 싶습니다.