안녕하세요, 올리브영 배송스쿼드에서 백엔드 엔지니어로 일하고 있는 애쉬입니다. 이번 글에서는 입사 후 처음 맡게 된 프로젝트를 진행하며 느꼈던 고민과 배움을 공유해보려 합니다.
입사 첫 프로젝트가 단순 기능 개발이 아닌 대규모 아키텍처 전환이라니... 설렘 반 걱정 반으로 시작했던 1년차의 치열한 레거시 탈출기를 공유합니다. 🚀
1. 전환의 계기
올리브영의 알림톡 발송 시스템은 주문, 결제, 배송 등 고객의 서비스 경험 속에서 핵심적인 소통 역할을 합니다. 1,600만 명 이상의 회원에게 실시간으로 소식을 전하는 매우 중요한 채널이죠. 하지만 비즈니스가 급성장하며 레거시 시스템은 점점 복잡해졌고, 변화하는 요구사항을 담아내기에는 한계에 부딪혔습니다. 특히 발송 플랫폼의 변화를 앞두고, 지속 가능한 구조를 위한 모던 아키텍처로의 전환이 필요했습니다.
1.1 왜 바꿔야 했을까?
알림톡 발송 시스템을 개편하게 된 이유는 하나의 문제가 아니라, 여러 한계가 동시에 드러났기 때문입니다. 먼저 알림톡 발송 플랫폼이 변경되면서, 기존 구조의 수정이 필요했습니다. 그런데 알림톡 발송과 관련된 로직이 여러 비즈니스 코드에 흩어져 있어, 해당 부분을 일일이 찾아 수정해야 하는 번거로움이 있었습니다. 또 데이터 조회 기준의 불일치 문제도 있었죠. 같은 내용이라도 알림 유형이나 서비스에 따라 서로 다른 테이블과 조건으로 데이터를 조회하는 경우가 있어, 일관성이 깨지고 기준이 모호했습니다. 마지막으로 올리브영은 레거시 시스템을 모던 아키텍처로 점진적 전환 중이기 때문에, 앞으로 모던 환경 중심으로 배송 알림을 확장하기 위해서라도 발송 기능을 하나의 구조로 일원화할 필요가 있었습니다.
1.2 어떻게 바꿔야 할까?
레거시 시스템과 모던 아키텍처 시스템 간 통신을 위해 Kafka 메시지를 사용하기로 했습니다. 기존 배송스쿼드에서 사용하던 컨슈머 서비스에서 알림톡을 발송하기 위해서죠. 여기서부터 신입 개발자의 고민이 시작됩니다...
기존 레거시 코드는 주문/배송 상태 변경 로직 안에서 메시지 발송 서비스를 직접 호출하고 있었고, 이 호출은 try-catch로 감싸져 있어 알림톡 발송에 실패하더라도 기존 비즈니스 로직에는 영향을 주지 않는 구조였습니다. 그래서 처음에는 레거시의 알림톡 발송 서비스를 호출하는 시점에 Kafka 메시지를 바로 생성하려고 했습니다.
하지만 코드를 작성하다 다시 살펴보니 알림톡 발송이 성공한 이후에 비즈니스 로직에서 오류가 발생하더라도, 이미 발송된 알림톡은 되돌릴 수 없다는 문제가 있었습니다. 즉, 실제 데이터 상태와 고객에게 전달된 알림이 어긋날 수 있는 구조였던 셈입니다. 오류 발생으로 같은 로직을 재실행한다면 알림이 중복 발송될 거고요.😱 그래서 알림톡 발송을 비즈니스 로직에서 분리하되, 주문/배송 상태 변경과 고객 안내의 명확한 순서가 보장되는 구조가 필요하다고 판단했습니다.
2. 전환 방법론
2.1 트랜잭션 동기화 구현
일반적으로 Spring 환경에서는 트랜잭션 동기화를 위해 @TransactionalEventListener를 활용한 트랜잭션 커밋 이후 이벤트를 처리하는 방식이 널리 사용됩니다.
하지만 저는 해당 방식을 사용할 수 없었는데요, 간략히 제가 겪었던 과정을 설명드려보겠습니다. 처음에는 @TransactionalEventListener를 사용해서 트랜잭션 커밋 후 이벤트를 처리하려고 했는데, 이 어노테이션은 Spring 4.2부터 지원되는 기능이었습니다. 문제는 레거시에서 하위 버전을 사용하고 있어서 TransactionPhase.AFTER_COMMIT 같은 클래스 자체가 존재하지 않았다는 점이죠. 그래서 Spring 4.2.5로 버전을 올렸는데, 하필 다른 모든 모듈이 의존하는 공통 모듈을 건드렸던 게 화근이었습니다. Maven에서 기존 프레임워크와의 의존성 충돌이 발생했고, spring-context만 올렸더니 spring-tx도 함께 올려야 한다는 오류가 연쇄적으로 터졌죠. 하나씩 해결하다 보니 빌드는 성공했지만 서버 실행 시 NoClassDefFoundError가 발생했습니다. 이 오류는 클래스패스에 4.2.5와 하위 버전이 섞여 있으면서, 컴파일 타임에는 4.2.5 클래스를 참조했지만 런타임에는 하위 버전의 클래스가 로드되어 메서드 시그니처가 맞지 않아 발생한 것이었죠. 버전을 잘 맞춰서 이 문제를 해결하고 나서는 제가 수정하지 않은 부분에서도 빈 생성 오류가 발생했습니다. 🫠
이렇게 하나를 해결하면 새로운 오류 메시지가 발생하는 상황의 연속을 경험하면서... 개발 경험이 부족했던 저는 운영 환경에서도 예상치 못한 오류가 발생할까 걱정되어 이벤트 리스너 구조를 고집하기 보다 차선책을 택하기로 했습니다. 레거시 시스템을 처음 크게 수정하는 프로젝트였는데, 제가 생각한 대로 흘러가지 않고 무수한 오류들을 마주하며 레거시가 얼마나 복잡한 시스템인지, 왜 전사 차원에서 모던화를 빠르게 추진하는지 체감할 수 있었습니다.
제가 선택한 방법은 TransactionSynchronizationManager를 활용한 커밋 이후 콜백 메서드 실행이었습니다. 이 방식이 이벤트를 발행하거나 리스너를 등록하는 구조는 아닙니다. 하지만 트랜잭션이 성공적으로 커밋된 이후에만 특정 로직이 실행되도록 보장할 수 있기 때문에, 실행 시점 보장 측면에서는 이벤트 처리와 동일한 효과를 가집니다. 즉, 구현 방식은 단순한 콜백이지만 “데이터가 확정된 이후에만 후속 처리가 시작된다”는 중요한 조건을 만족시킬 수 있었고, 이를 통해 알림톡 발송 흐름을 비즈니스 트랜잭션으로부터 안전하게 분리할 수 있었습니다.
현재 트랜잭션 커밋 후 바로 Kafka 메시지를 발행하는 방식을 사용했는데, 메시지 발행이 실패할 경우에는 실패 로그를 DB에 저장하여 별도로 재처리할 수 있도록 설계되어 있습니다. 다행히 운영 환경에서 서버 장애로 인한 메시지 유실은 발생하지 않았지만, 이번 개발 과정을 회고해보니 더 높은 안정성이 요구되는 환경이라면 Outbox Pattern을 통해 이벤트를 DB에 먼저 저장한 후 별도 프로세스가 발행하도록 하거나, 재시도 메커니즘 같은 방어책들을 추가로 고려해볼 필요가 있겠다는 생각이 들었습니다. 당장은 필수는 아니지만 이런 부분들도 점진적으로 개선해나가면 좋을 것 같습니다.
✅ 레거시 비즈니스 로직 + 커밋 후 Kafka 메시지 발행
public class NotificationEvent {
private final String orderNo;
private final String notificationType;
public NotificationEvent(String orderNo, String notificationType) {
this.orderNo = orderNo;
this.notificationType = notificationType;
}
}
@Component
public class NotificationAfterCommitHandler {
private final MessagePublishService messagePublishService; // Kafka 메시지 발행 서비스
public void handle(NotificationEvent event) {
// 트랜잭션 커밋 이후에만 실행될 로직 등록
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 커밋이 완료된 이후에만 후속 처리 수행
messagePublishService.publish(
event.getOrderNo(),
event.getNotificationType()
);
}
}
);
}
}2.2 MSA(Microservices Architecture) 환경에서의 알림톡 처리 흐름
이제 레거시 시스템과 모던 아키텍처 시스템이 어떻게 연결되고, 어떤 시점에 실제 발송 흐름이 시작되는지 살펴보겠습니다. 레거시에서 트랜잭션 커밋 이후에 Kafka 메시지를 발행하고, MSA의 Delivery Consumer 서비스가 이를 소비(consume)합니다. 이제부터는 레거시가 아니라, Delivery Consumer가 모든 발송 처리를 전담하게 됩니다.
배송에서 관리하고 있는 알림톡이 약 40개 정도로 꽤 많은 편이라, 어떻게 나눠서 기능화할지 설계와 구현에 많은 시간을 쏟았는데요. 우선 레거시에서 발행된 메시지에 알림톡 유형이 있으니, 같은 배송 유형별(ex. 오늘드림 / 일반배송)로 분리하여 처리하도록 했습니다. 배송 유형별로 안내하는 내용이 비슷하기 때문에, 같은 쿼리를 사용해 데이터를 조회하도록 한 거죠. 그런데 막상 테스트해보니 같은 배송 유형 안에서도 이 배송이 일반 주문인지, 교환 주문인지 또는 취소된 항목은 없는지에 따라서 필요한 데이터가 조금씩 상이하여 하나의 쿼리로 특정 배송 유형에 대한 모든 범위의 알림톡을 커버하지 못했습니다. 수정과 테스트를 반복하며 모든 유형을 최소 갈래로 분류하여 공통 쿼리를 수정해 나갔습니다. 이 작업을 하면서도 난항을 겪어서 그냥 알림 유형별로 조회 쿼리를 만들까도 생각했지만😅 그러면 레거시와 다른 점이 없기 때문에! 열심히 고민한 결과 배송 유형 내에 같은 조건끼리 묶어서(ex. 일반 / 교환) 데이터를 조회할 수 있도록 했습니다. 이렇게 데이터 조회까지 완성한 뒤에는 알림톡 내 링크 생성, 알림톡 발송 요청 API 등 공통 기능을 구현해서 모던 시스템을 완성할 수 있었습니다.
위에서 설명한 내용을 단계별로 정리해보면 아래와 같습니다.
-
컨슈머가 메시지 소비
- Kafka를 통해 전달된 메시지를 소비합니다.
- 이 메시지에는 주문번호, 알림톡 유형과 같은 최소한의 필수 정보만 포함되어 있습니다.
-
알림톡 유형에 따라 배송 유형 분기
- 컨슈머는 전달받은 알림톡 유형을 기준으로 일반 배송, 오늘드림, 픽업, 선물하기 중 어떤 배송 유형의 알림인지 판단합니다.
-
공통 기준으로 알림톡 정보 조회
- 배송 유형이 결정되면, 해당 알림에 필요한 공통 데이터와 템플릿 정보를 DB에서 일관된 기준으로 조회합니다.
- 과거처럼 서비스마다 서로 다른 테이블을 조회하지 않고, 하나의 기준 테이블만 조회하도록 통합했습니다.
-
템플릿 구성 후 발송 요청
- 조회한 데이터를 바탕으로 알림톡 템플릿을 채우고, 최종적으로는 하나의 공통 발송 API를 통해 알림톡 발송 요청이 이루어집니다.
- 어떤 배송 유형이든, 실제 외부 알림 플랫폼으로 나가는 통로는 항상 하나로 통일되어 있습니다.
즉, 레거시에서는 언제 알림을 보내야 하는지만 결정하고, 컨슈머에서는 어떤 내용을, 어떤 방식으로, 안정적으로 보낼지를 전담하는 구조로 역할이 완전히 분리되었습니다. 이렇게 공통화한 덕분에 새로운 알림톡을 추가할 때 매우 간편한 구조가 완성되었습니다. 템플릿과 발송 시점만 추가하면 끝이죠. 여담이지만 이후에 제가 알림톡 추가 작업도 진행했는데, 모던 전환 미리 해두길 정말 잘했다는 생각과 함께 보람이 느껴졌습니다.
3. 전환 과정에서의 배움
신입 개발자로서 이번 프로젝트는 단순한 기능 구현을 넘어, 시스템을 설계하는 관점에서 많은 고민을 하게 만든 경험이었는데요. 제가 느낀 내용들은 한 번 정리해보겠습니다.
3.1 아키텍처 설계의 중요성
코드를 작성하기 전에 시스템 전체의 흐름을 이해하는 것이 무엇보다 중요했습니다. 초반에 여러 제약을 고려하지 않고 단순한 시스템 설계 후 코드를 작성했다가, 부족한 점을 보완한 후 다시 작성하며 시간이 거의 두 배로 들었습니다. 입사 초반에는 '개발자는 코드를 잘 짜야 한다'는 생각이 강했다면, 이 프로젝트를 통해 '설계를 잘 하는 개발자가 되고 싶다'고 생각이 바뀌었습니다. 특히 단일 서비스에서 해결되던 문제도, 분산 환경에서는 여러 시스템 간 데이터 흐름을 고려해야 한다는 점을 가장 크게 느꼈습니다.
3.2 철저한 테스트의 필요성
아무리 설계를 잘 해놓더라도 허점이 있기 마련인데요. 이런 점은 코드만 놓고 보면 보이지 않는 경우가 많습니다. 그래서 QA팀과 함께 직접 여러 케이스를 테스트해보며 이상한 점은 없는지, 누락된 기능은 없는지 꼼꼼히 확인했습니다. 특히 알림톡은 고객에게 직접 발송되고 안내하는 기능이라 작은 실수도 치명적일 수 있어, 다양한 케이스를 반복 테스트하며 안정성을 확보하려 노력했습니다.
3.3 MSA 구조에서의 비즈니스 흐름
입사 전에는 제대로 된 MSA 환경을 접하기 어렵기 때문에 추상적으로만 알고 있던 구조를 이번 프로젝트를 통해 좀 더 구체적으로 이해할 수 있었습니다. MSA에서는 하나의 비즈니스 흐름이 여러 시스템으로 나뉘어 처리되기 때문에, DB 트랜잭션 이후의 처리까지 포함해 전체 흐름의 일관성을 어떻게 유지할지가 중요하다는 것을 배웠고, 이후 올리브영의 다른 마이크로 서비스와 통신하며 협업해야 하는 작업에서 정말 많은 도움을 얻을 수 있었습니다.
4. 전환 후 개선된 점
저의 피땀눈물로 완성한 전환 이후 시스템은 훨씬 단순하고 안정적으로 개선되었습니다. 주요 개선 사항을 표와 함께 정리해볼까요?
| 구분 | 기존 (Legacy) | 개선 후 (Modern) |
|---|---|---|
| 아키텍처 구조 | 서비스 로직 내 발송 코드 결합(Monolithic) | Kafka 기반 이벤트 주도 아키텍처 (EDA) |
| 발송 시점 보장 | 비즈니스 로직 결과와 별개로 발송 | afterCommit 활용 트랜잭션 정합성 확보 |
| 데이터 조회 | 서비스별 산재된 테이블/조건 조회 | 통합 기준으로 일관된 데이터 조회 |
| 관리 주체 | 각 비즈니스 도메인 서비스 | 배송 컨슈머(Delivery Consumer)로 일원화 |
| 확장성 | 알림톡 추가 시마다 레거시 코드 수정/배포 | 템플릿 등록 및 메시지 발행으로 낮은 공수 |
4.1 유지보수성 향상
- 알림톡 관련 코드가 모던 알림톡 서비스 한 곳으로 집중되면서 관리 포인트가 크게 줄었습니다.
- 데이터 조회 기준도 공통화되어 서비스별 테이블 불일치 문제가 해소되었습니다.
- 수정 시 영향 범위 파악과 장애 대응 속도 모두 개선되었습니다.
4.2 확장성과 유연성 증가
- 알림 발송 시점이 이벤트 기반으로 정의되어, 이벤트 추가만으로 확장이 가능해졌습니다.
- 알림 기능이 특정 서비스에 결합되지 않고 독립된 구조로 분리되었습니다.
- 향후 배송 정책이나 서비스 유형이 추가되어도 최소한의 변경으로 대응할 수 있습니다.
4.3 공통 재시도 로직 구축 (확장)
- 레거시에서는 공통 재시도 로직을 독립적으로 구성하기가 어려웠으나, 모던 환경에서 실패 건을 별도 테이블에 저장해 비동기적으로 재처리하는 공통 재시도 구조를 새롭게 설계했습니다.
- 이를 통해 단순한 기능 구현을 넘어 운영 관점의 안정성까지 함께 확보할 수 있었습니다.
5. 마무리하며
이번 프로젝트는 저에게 단순한 마이그레이션을 넘어, 시스템을 설계하고 개선하는 개발자로 성장하는 계기가 되었습니다. 트랜잭션 동기화라는 기술적 도전, 메시지 기반 구조 전환, 테스트를 통한 안정성 확보와 같은 과정을 통해 유지보수성과 확장성을 모두 갖춘 배송 알림톡 시스템을 완성할 수 있었죠. 무엇보다 이번 경험은 신입 개발자로서 시스템을 바라보는 시야를 넓혀준 좋은 시작이었습니다. 앞으로 올리브영에서 어떤 문제를 해결하게 될지, 또 어떤 기술적인 도전을 하게 될지 매우 기대되고 다음 번에 더 성장한 모습으로 돌아오겠습니다. 감사합니다!

