안녕하세요. 제품길드 H파티에서 ‘CPS 쇼핑 적립・쿠폰 기반 통합 상점’ 서비스를 전담하여 개발하고 있는 백엔드 엔지니어 장준수입니다. 이번 아티클에서는 통합 상점 프로젝트를 구축하는 과정에서 겪었던 고민과 문제를 어떤 기술과 기법을 접목하여 해결했는지 이야기하고자 합니다.
통합 상점 : 공급사 연동의 재설계
HandlerAdapter 패턴 응용과 멀티 모듈 적용기
핵심 키워드
#멀티모듈 #전략패턴 #관심사분리 #핸들러어댑터패턴 #ProfileGroup기능
프로젝트 소개

통합 상점 구축 프로젝트는 기존에 칩스 서비스에서만 제공되던 ‘모바일 쿠폰(상품) 기반의 상점’을 NBT의 다양한 서비스로 확장하는 작업입니다. 기존에는 각 서비스 내 상점에서만 리워드를 사용할 수 있었지만 이제 사용자는 여러 서비스에서 적립한 리워드를 ‘통합 상점’에서 사용해 모바일 쿠폰을 구매할 수 있게 되었습니다.
겪었던 문제

통합 상점은 다양한 모바일 쿠폰 공급사와 계약을 맺어 각 공급사로부터 제공받는 쿠폰을 사용자가 구매할 수 있도록 운영 중에 있습니다.
계약된 공급사로부터 연동 키와 쿠폰별 연동 코드 값을 받아 각 공급사가 지원하는 연동 방식(소켓・API 등)을 활용해 쿠폰을 발행하는데요. 공급사마다 연동 방식이 다르다 보니, 공급사별로 연동을 처리하는 코드도 제각각 다를 수밖에 없습니다. 예를 들어, 동일한 API 통신 내에서도 Json・XML 규격이 다른 경우가 있고, 콤마 등의 구분자를 통해 Text/Plain 형식으로 응답을 주는 공급사도 있습니다. 때문에 추후 통합 상점을 유지 보수하는 과정에서 아래와 같은 문제점이 생길 수 있었는데요.
지금부터 해당 문제점을 어떻게 분석하고, 해결했는지 이야기해 보겠습니다.
1. 애플리케이션 실행 모듈별로 공급사 구현 클래스가 중복 구현되어 있어 유지 보수가 어려움
2. Factory 패턴이 적절히 적용되지 않아 불필요하게 Factory 로직을 수정해야 함
3. 비즈니스 로직에서 공급사 구현체가 직접 DI(의존성 주입)되어 오남용될 가능성이 있음
문제 분석
1. 애플리케이션 실행 모듈별로 공급사 구현 클래스가 중복 구현되어 있는 점

현재 통합 상점 서비스는 멀티 모듈로 관리되고 있으며, 각 모듈의 역할은 다음과 같습니다.
chips-core
- 도메인별 비즈니스 정책을 구현하는 모듈
- 예 : Model(domain)・Interface 기반 Repository・Domain Service・DomainCommand
chips-common
- 모듈별 공통으로 적용될 유틸리티성 구현체들을 정의한 모듈
- 예 : Util・Constant・Extension Function・Exception 등
chips-clients
- 외부 통신 관련 구현체들을 정의한 모듈
- 예 : 외부 API・Socket 등
chips-app / chips-store / chips-scheduler
- 실제 실행 가능하며 비즈니스 로직 순서를 정의해놓은 모듈
- 예 : Usecase・Controller・Request/Response Dto・Filter, Interceptor 등
chips-infra
- database・미들웨어(MQ・LogStash etc.) 등 저장하고 통신함에 있어 인터렉션이 많이 발생하는 모듈
- 예 : Root Entity・JPA Repository・VO・Converter 등
이렇게 역할에 따라 모듈을 구조적으로 분리하였지만, 공급사 연동 코드는 체계적으로 분리되지 않은 채 실행 모듈마다 존재하는 문제가 있었습니다. 아마 초기 구현 당시 제한된 일정과 해당 코드들의 변경이 많지 않을 것이라는 판단하에 우선순위가 높은 작업들을 먼저 수행했을 겁니다.
해당 코드들이 분리되지 않았다 보니 공급사 구현 로직과 관련해 변경 사항이 발생할 때마다 중복으로 구현된 코드를 수정하고, 동기화 및 테스트・검증하는 작업이 많아졌습니다. 예를 들어, 연동된 쿠폰의 판매 중지에 따른 Failover 처리나 부족한 로그 정보 추가 등이 생기면 각 실행 모듈별로 테스트해야 하는 번거로움이 있었습니다.
2. Factory 패턴이 적절히 적용되지 않아 불필요하게 Factory 로직을 수정해야 함

현재 공급사별 쿠폰 생성 로직은 다음과 같은 구조로 구성되어 있습니다.
✅ 구조적으로 잘 적용된 부분
- 공급사별 API 응답 형식이 Json・XML・Text/Plain 등 다양함에도 불구하고, VendorService 인터페이스를 통해 관심사 분리가 잘 되어 있음
- 공급사의 API 형식과 통신 방식이 다름에도 불구하고, 이를 추상화하여 공통 비즈니스 로직에서 신경 쓰지 않도록 구성되어 있음
😭 그러나 아래와 같이 Factory 구현 방식에 문제가 있습니다.
✔︎ 공급사 추가 시, VendorServiceFactory를 직접 수정해야 함
- 공급사가 추가될 때마다 Factory 클래스에 새로운 구현체를 추가해야 하는 구조
- getService()의 when 문에 새로운 공급사를 등록해야 하는 문제
✔︎ 테스트 코드 수정
- Factory 클래스를 테스트하려면 모든 공급사 구현체를 Mocking 해야 함
- 신규 공급사 추가 시, Mock 대상이 계속 증가하게 되어 관리에 어려움
3. 비즈니스 로직에서 공급사 구현체가 직접 DI(의존성 주입)되어 오남용될 가능성이 있음
chips-app・chips-store・chips-scheduler의 실행 모듈 내에는 공급사별 구현체 클래스들이 정의되어 있습니다. 연동 초기 공급사측의 API 통신 테스트를 위해 개발 환경에서만 활용될 TestController를 만들어 통신 테스트를 진행했는데요. 해당 컨트롤러가 원래 의도와는 다르게 잘못된 레이어에 위치하게 되었고, 나아가 방치되거나 사용되지 않는 코드가 생길 가능성이 높아졌습니다. 이런 문제는 단순히 코드의 정리가 부족해서가 아니라, 공통 요청・응답 객체를 수정할 때 예상치 못한 영역에서 변경이 필요해지는 구조적인 문제로 이어졌습니다.
결국, 히스토리를 알 수 없는 코드의 수정과 검증에 불필요한 리소스를 낭비하는 상황이 발생했고, 프로젝트 진행 속도에도 영향을 미쳤습니다. 보다 근본적인 차원에서 이를 방지할 수 있는 기능적인 대책이 필요하다는 생각이 들었습니다! 위의 세 가지 문제점에서 개선이 필요한 부분은 아래와 같았습니다.
✅ 개선이 필요한 부분
- 공급사 연동 로직들을 별도 모듈로 분리하여 독립적으로 관리할 수 있도록 개선
- Factory 클래스 개선을 통해 신규 공급사 추가 시, 기존 코드를 수정하지 않는 구조로 변경
- 구현체 코드를 오남용하여 DI 할 수 있는 부분을 차단
문제 해결
1. chips-clients 모듈 내 공급사별 모듈 추가

위와 같이 chips-clients 내 coupon-vendors 모듈을 만들고 각 하위마다 연동한 공급사 모듈을 구성했습니다. 공급사 구현 모듈 내에서 API 통신은 FeignClient를 기본적으로 활용하고 있으며, 이를 위해 프로젝트의 dependencies에 포함했습니다.
또한, 모듈은 다음과 같은 주요 패키지로 구성했습니다.
- client : API 통신을 담당하는 FeignClient 구현
- config : 각종 설정을 관리
- constant : 각 모듈 내에서 사용하는 상수 값을 정의
- service : 공급사 구현체에 대한 로직을 정의
- util : 부가적인 처리 목적의 유틸성 함수를 정의
아래 코드는 위 VendorLinkedCouponProcessor 인터페이스의 구현체 모습입니다.

supports 함수를 활용해 Product 내 설정된 유니크한 공급사 코드와 일치하는지 확인한 후, 쿠폰 생성 시 createCoupon 함수가 실행됩니다. 이를 통해 각 공급사별로 상이한 통신 방식・API 응답 형식을 모듈화하여 일관된 구조로 통합할 수 있고 ,필요한 실행 모듈에 추가하여 효율적으로 활용할 수 있습니다.
현재는 테스트 코드가 완전히 구성되지 않았지만 향후 각 공급사 모듈 내에서 통신 방식・응답 코드 구조・인코딩 등을 고려한 통합 테스트를 작성해 모듈 내 통신 테스트 코드를 추가할 계획입니다. 공급사 연동 Spec 변경 시 테스트 코드를 활용해 지속적인 검증이 가능해지고 코드 변경 사항이 Git을 통해 관리됨에 따라 장기적으로 유지 보수성이 향상될 것이라 기대합니다. 또한 해당 모듈 구성을 통해 실행 모듈마다 중복되었던 공급사 구현 코드 문제도 해소할 수 있습니다.

✔︎ Profiles Group 기능
Spring Boot 2.4 이상 버전부터 지원하는 기능으로 역할별 application.yml 파일을 분리 정의하고, Profile 환경에 맞춰서 적용하는 방법입니다. 예시에서는 application-special-vendor.yml 파일에 필요한 공급사 설정 정보를 저장・관리하고, 실행 모듈에서 이미지와 같이 정의하여 활용할 수 있습니다.
2. HandlerAdapter 패턴을 활용한 Factory 코드 개선
Spring MVC를 공부해 보신 분이라면 아래 이미지가 굉장히 친숙할 겁니다.

위 이미지에서 HandlerAdapter는 Dispatcher Servlet이 HandlerMapping을 통해 찾은 Handler를 실행시키고자 접근하는 영역입니다. 다양한 형태로 구현된 Handler들은 하나의 어댑터 클래스를 바라보고 적절한 처리 대상을 찾아 Handler를 실행시키게 합니다.

위 이미지는 실제 Spring MVC Core 단에서 HandlerAdapter를 활용하는 코드의 일부분입니다.
DispatcherServlet은 init 시점 또는 특정 조건 부합 시, set 함수를 통해 HandlerAdapter의 구현체들을 가져와 세팅하고 실제 호출될 때에는 List 형태의 HandlerAdapter를 순회하며 supports에 구현한 조건에 맞는 해당 구현체를 찾아 반환합니다. 여기서 interface List 타입을 받아와 순회하는 점과 supports 함수를 활용하여 구현체를 찾는 점이 키포인트입니다.
해당 부분을 응용하면 쿠폰 공급사 Factory를 아래와 같이 개선할 수 있습니다.

VendorServiceFactory는 chips-core 모듈 내 VendorLinkedCouponProcessrFactory와 같이 의미 있는 이름으로 변경되었으며, getProcessor 함수 내 구현 코드 모습은 HandlerAdapter를 활용하는 부분과 유사해졌습니다. 이를 통해 새로운 공급사 구현체가 추가될 때마다 비즈니스 로직 레이어의 코드를 수정할 필요가 없어졌으며, Factory 자체 테스트 코드도 인터페이스 리스트 타입을 활용하게 되어 Mocking 할 대상을 추가할 필요 없이 유지 보수할 수 있게 되었습니다.
3. runtimeOnly를 통한 구현 클래스 DI 시점 변경
마지막으로 실행 모듈에서 build.gradle.kts 내 추가된 공급사 모듈 코드의 모습입니다.

여기서 implementation이 아닌 runtimeOnly로 의존성을 정의한 점에 주목해야 합니다. 공급사 구현체를 DI 하여 불필요하게 오남용하는 것을 방지하기 위해 runtimeOnly를 활용해 모듈 관계를 제한했습니다.

이렇게 설정하면 실제 애플리케이션 실행 시점에 해당 모듈들이 주입되어 활용됩니다.

이를 통해 모듈 간의 의존성을 명확하게 관리할 수 있습니다.
다만, 위 코드 예시는 두 가지 단점이 있습니다.
- 새로운 공급사가 추가될 때마다 실행 모듈에서 개별적으로 project를 명시해야 하는 번거로움
- Profiles Group 내 각 모듈별 application.yml 파일 그룹명을 명시해야 함
2번의 경우, 우아한형제들 권용근(서버 개발자) 님께서 제공해 주신 YML Importer 방식을 응용한다면 해결할 수 있을 것 같습니다. 다만, 1번의 경우 아직까지 적절한 방법을 찾지 못했습니다. yml 파일 그룹 추가 및 project 명시하는 정도는 충분히 신경 써서 관리할 수 있어 해당 부분의 개선은 추후 진행할 예정입니다.
“
문제를 해결하며 아래와 같은 레슨런을 얻을 수 있었습니다.
- supports 함수를 활용해 공급사 코드와 일치하는 구현 클래스를 선택하고, 쿠폰 생성 로직을 일관된 방식으로 처리하는 방법
- 공급사별로 상이한 통신 방식과 API 응답 구조를 모듈화하여 실행 모듈에서 재사용 가능하도록 개선하는 방식
- runtimeOnly를 활용하여 공급사 구현체의 직접적인 DI를 방지하고 실행 시점에 필요한 모듈만 주입하여 관리하는 전략
이번 개선 작업을 통해 신규 공급사 연동 속도를 기존 3-4일에서 테스트 포함 1일 이내로 대폭 단축했으며, 기존 공급사의 연동 코드 수정 시 발생할 수 있는 사이드 이펙트에 대한 부담을 줄이고 보다 쉽게 수정하고 테스트할 수 있는 구조를 마련했습니다.
현재 NBT 제품 길드 B2C 개발팀은 서비스의 유의미한 성장을 목표로 1-2주 단위의 스프린트 주기로 기능을 개발하고 배포하며, 각종 지표와 서비스 성능 향상을 위한 모니터링을 지속적으로 수행하고 있습니다. 기능 완결을 위한 라이프 사이클을 기민하게 추진하면서도 기술적 변화에 유연하게 대응하고 확장 가능한 코드 구조를 설계하기 위해 수많은 논의와 합리적인 타협을 거듭하고 있죠.
우리는 빠르게 변화하는 환경 속에서도 유연하고 성장 지향적인 팀 문화를 바탕으로 함께 고민하는 개발 문화를 지향합니다.
이런 환경이 흥미롭다면, 개발팀에 합류해 함께 성장해보는 건 어떨까요?