10주동안 이커머스 프로젝트를 진행하며 깨달은 것들

 

안녕하세요. 10주 동안 진행했던 이커머스 프로젝트가 마무리되었습니다. 이번 프로젝트는 단순히 기능을 구현하는 것을 넘어, 그동안 제대로 적용해보지 못했던 여러 기술과 개념을 실제로 부딪히며 배우는 과정이었습니다.

 

TDD부터 도메인 주도 설계(DDD), 멀티 모듈을 고려한 아키텍처, 그리고 성능 개선을 위한 캐시와 인덱스 활용까지. 매주 새로운 주제를 학습하고 코드에 적용하면서, 이론으로만 알던 것들이 실제 프로젝트에서 어떻게 작동하는지, 또 어떤 점들을 고려해야 하는지 체감할 수 있었습니다.

 

이번에는 10주간의 프로젝트를 진행하며 제가 겪었던 기술적인 고민과 문제 해결 과정, 그리고 그를 통해 무엇을 배우고 느꼈는지 정리해보도록 하겠습니다.

 

 

 

1주차: Test Driven Development

 

그동안 실무에서 API 개발보다는 JSP 같은 뷰 템플릿에 `ModelAttribute`로 데이터를 연동하는 코드를 주로 작성해왔습니다. 그래서 '테스트 코드 작성이 과연 가능할까?'라는 막연한 의문이 있었고, 테스트의 중요성은 이론으로만 알 뿐 크게 와닿지 않았습니다.

 

하지만 TDD를 적용하며 바텀업(Bottom-up) 방식으로 기능을 구현하는 과정은 무척 인상적이었습니다. 요구사항 변경으로 코드를 수정했을 때, 기존 테스트 코드가 미처 생각지 못한 사이드 이펙트(Side Effect)를 즉시 발견해주었습니다. 이때 '이래서 테스트가 안전망 역할을 하는구나'라고 처음으로 체감할 수 있었습니다.

 

물론 '하나의 API에 대해 어디까지 테스트하는 것이 적절할까?', '이 테스트가 모든 예외 케이스를 포함하고 있을까?'와 같은 '테스트 범위'에 대한 고민은 깊어졌습니다. 이런 고민을 하던 중 테스트 피라미드(Test Pyramid)의 개념을 접하며, E2E 테스트, 통합 테스트, 단위 테스트의 역할과 차이를 명확히 알게 되었습니다. 각 테스트가 어떤 목적을 가지며, 어느 계층에 집중해야 하는지 배울 수 있었지만, 이 감각을 온전히 익히는 것은 결국 경험의 몫이라는 생각이 들었습니다.

 

더불어 이 과정에서 테스트 더블(Mock, Fake, Stub 등)의 개념과 역할을 익혔습니다. 동시에 TDD를 통해 제 코드가 얼마나 절차지향적인지 객관적으로 돌아보게 되면서, 좋은 설계를 위해 더 많은 코드를 읽고 시야를 넓혀야겠다고 다짐하는 계기가 되었습니다.

 

 

 

2주차: Software Design

 

2주차에는 다이어그램을 그리며 소프트웨어 디자인을 구체화하는 작업에 집중했습니다. 실무에서 ERD는 자주 다뤄봤지만, 클래스 다이어그램처럼 객체지향 설계를 표현하는 다이어그램은 익숙지 않아 어려움을 겪었습니다.

 

특히 VO(Value Object) 개념을 이해하는 데 많은 시간을 썼습니다. 실무에서 여태 VO를 DTO와 거의 같은 의미로 사용해왔는데, 이번에 VO가 비즈니스 로직을 포함할 수 있는 '값' 자체를 표현하는 불변 객체라는 것을 제대로 학습하며 개념을 바로잡으려 노력했습니다.

 

설계 단계에서 또 다른 어려움은 '요구사항 정리'였습니다. 구체적인 화면이 없으니 머릿속이 정리가 되지 않아 요구사항 정의를 작성하는 것에 어려움을 느꼈습니다. 간단하게나마 와이어프레임을 그리고 나니 작업 속도를 낼 수 있었습니다. 하지만 개발을 진행하며 초기 요구사항이 계속 바뀌었고, 그에 맞춰 설계 문서를 계속 업데이트하는 것은 생각보다 어려운 일이었습니다. 설계와 구현의 간극이 벌어지는 것을 직접 경험하니, 설계 문서를 코드와 꾸준히 동기화하는 것이 얼마나 중요한지 깨달았습니다.

 

2주차에 느낀 가장 큰 깨달음은 클래스 다이어그램과 ERD의 근본적인 차이를 이해하게 된 것입니다. 이전까지 클래스 다이어그램을 만들 때 ERD와 거의 유사한 다이어그램을 만들었는데, 그 이유가 객체지향(데이터와 행동)과 관계형 데이터베이스(데이터)의 패러다임 차이를 제대로 모르기 때문이라는 것을 알게 됐습니다. 아직 부족한 점이 많아, 조영호 님의 책을 통해 객체지향 설계에 대해 더 깊이 공부해봐야겠다고 생각했습니다.

 

 

 

3주차: Domain Modeling

 

개인적으로 10주 과정 중 가장 어려웠던 한 주를 꼽으라면 단연 3주차 도메인 모델링이었습니다. DDD는 용어만 들어봤을 뿐, 왜 필요한지 제대로 이해하지 못한 상태였습니다. 그동안은 관성적으로 `Controller-Service-Repository`의 레이어드 아키텍처만 사용해왔는데, 이 구조의 가장 큰 문제는 모든 비즈니스 로직이 `Service` 계층에 집중되는 '서비스 계층 비대화'였습니다. 왜 계층을 나눠야 하는지에 대한 고민 없이 그저 코드를 넣기 바빴던 것입니다.

 

3주차에는 이런 문제를 해결하기 위해 Application ServiceDomain Service를 분리하는 시도를 했습니다. 외부와의 통신이나 트랜잭션 관리는 Application Service가 담당하고, 순수한 비즈니스 로직은 Domain Service에 담아보려 했습니다. 이때 Application Service가 일종의 '퍼사드(Facade)' 패턴 역할을 한다는 것도 처음 알게 되었습니다.

 

하지만 '관심사의 분리'라는 목표와 달리, 결과물은 엉망이었습니다. 잘 모르는 상태에서 구조만 흉내 내다보니, 계층 간 의존성이 역전되는 등 설계 원칙을 위배하는 코드가 많았습니다. 처음에는 제 코드의 문제점이 전혀 보이지 않았습니다. 하지만 다른 사람들의 코드를 참고하고 코드 리뷰를 받으면서 비로소 무엇이 잘못되었는지 눈에 들어오기 시작했습니다. 좋은 설계는 단순히 구조를 나누는 행위가 아니라, 각 계층의 책임과 의존성 방향을 명확히 정의하는 것에서 시작된다는 점을 배울 수 있었습니다.

 

 

 

4주차: Transactional

 

트랜잭션의 비관적/낙관적 락 같은 개념은 이론적으로 알고 있었지만, 실제 코드에 녹여내는 것은 전혀 다른 문제였습니다. 이번 프로젝트에서 마주한 문제는 `주문 생성()`이라는 하나의 큰 메서드 안에 주문과 결제 로직이 함께 묶여있다는 점이었습니다. 이로 인해 하나의 트랜잭션이 너무 많은 책임을 갖게 되어, 어디서부터 어디까지를 원자적인 작업 단위로 봐야 할지 고민이 깊었습니다.

 

이 문제를 해결하기 위해 기존의 `OrderFacade`를 책임에 따라 `OrderProcessor`와 `PaymentProcessor`라는 별도의 Application Service로 분리했습니다. 각 서비스가 주문과 결제라는 명확한 책임을 갖게 하고, 각각의 작업 단위에 맞는 트랜잭션 경계를 설정해주는 것이 목표였습니다.

 

이 과정에서 `@Transactional(propagation = Propagation.REQUIRES_NEW)` 속성을 처음으로 사용해보았습니다. 부모 트랜잭션의 성공 여부와 관계없이, 각 Processor가 자신만의 새로운 트랜잭션을 시작하고 커밋하도록 만드는 이 방식은 굉장히 신선한 경험이었습니다. 동시에, 트랜잭션을 분리하는 것이 단순히 장점만 있는 것이 아니라 어떤 상황에 사용해야 하고, 데이터 정합성을 위해 어떤 점들을 추가로 고려해야 하는지 배울 수 있는 기회였습니다.

 

 

 

5주차: Index & Cache

5주 차는 실무에서 겪던 쿼리 튜닝과 리팩토링에 대한 고민과 가장 맞닿아 있던 시간이었습니다. 특히 저의 고정관념을 깨준 것은, 무조건 Redis 같은 분산 캐시를 쓰는 것이 좋은 게 아니라 상황에 따라 인메모리 캐시도 충분히 효과적인 선택지가 될 수 있다는 점이었습니다.

 

'Redis를 쓴다'는 기술 자체가 중요한 게 아니라, 당면한 문제와 시스템 환경에 가장 알맞은 기술을 선택하는 것이 훨씬 중요하다는 점을 처음으로 깊게 느꼈습니다. 실제로 이때 배운 내용을 실무 코드에 적용해 조회 속도를 눈에 띄게 개선할 수 있었는데, 기술을 위한 기술이 아니라 실제 문제를 해결하기 위해 배운 것을 적용했다는 점에서 큰 성취감을 느꼈습니다.

 

 

5주차에는 k6를 이용한 부하 테스트도 처음 진행해보았습니다. `EXPLAIN`으로 쿼리 실행 계획을 분석해 병목 지점을 찾아내고, 인덱스를 최적화하며 직접 성능을 개선하는 과정이 무척 재미있었습니다. 이 경험을 통해 제가 문제를 진단하고 성능을 개선하는 과정 자체에 큰 흥미를 느끼는 사람이라는 것도 알게 되었습니다.

 

 

또한, Grafana에 Redis 플러그인을 설치해 모니터링 환경을 구축하고, k6로 부하를 발생시키며 대시보드를 통해 시스템 지표 변화를 실시간으로 확인하는 과정도 흥미로웠습니다. 이미 수집되고 있는 메트릭을 활용해 원하는 대시보드를 직접 구성해보는 경험은 성능 튜닝의 또 다른 재미를 느낄 수 있었습니다.

 

 

 

6주차: Resilience4j, Circuit Breaker

6주 차부터는 `FeignClient`, `Resilience4j`, `Circuit Breaker` 등 이전에 전혀 접해보지 못했던 생소한 개념들을 다루기 시작했습니다. 처음 보는 기술들이라 단순히 사용법을 익히기보다, '이런 아키텍처 패턴이 왜 필요한가?'라는 근본적인 질문에 답을 찾는 데 더 많은 시간을 쏟았습니다.

 

예를 들어 서킷 브레이커를 적용하면서 '실패율 임계치를 몇 %로 설정하는 게 정답일까?' 같은 질문에 한참을 고민했습니다. 더 나아가 '포인트 차감 로직은 주문 도메인과 결제 도메인 중 어디에 위치해야 하는가?' 같은 설계 문제에 부딪혔을 때는 머리가 더 복잡해졌습니다.

 

이런 고민의 과정 끝에 내린 결론은, 소프트웨어 설계에는 절대적인 '정답'이 없다는 것이었습니다. 모든 것은 비즈니스 정책과 현재 요구사항이라는 '맥락'에 따라 달라지며, 개발자의 역할은 흑백논리로 정답을 찾는 것이 아니라 주어진 상황에서 최적의 해결책을 찾아나가는 것임을 깨달았습니다.

 

 

지난주에 이어, Grafana 대시보드를 통해 부하 테스트 중 서킷 브레이커가 열리고 닫히는 상태를 직접 눈으로 확인하는 과정은 역시나 무척 흥미로웠습니다.

 

 

 

7주차: Decoupling with Event

이것은 커맨드다!

 

7주 차에는 이벤트 주도 아키텍처(EDA)를 배우면서, 그동안 제가 생각했던 '분리'가 사실은 분리가 아니었음을 깨달았습니다. 단순히 도메인별로 패키지를 나누면 책임이 분리된 것이라 생각했지만, 결국 주문 서비스가 결제 서비스를 직접 호출하는 구조에서는 둘 사이의 강한 결합을 피할 수 없었습니다.

 

EDA는 이 문제를 완전히 다른 방식으로 풀었습니다. A가 B를 직접 호출해 명령(Command)하는 대신, '주문이 생성되었다'는 사실(Event)을 세상에 알리면, 그 소식에 관심 있는 다른 서비스가 각자 할 일을 처리하는 방식이었습니다. 서비스 간의 의존성을 끊어내는 이 아이디어는 정말 새롭고 흥미로웠습니다.

 

처음에는 별도의 '결제 API'를 만들지 않고 이 구조를 구현해보려 했습니다. 하지만 모듈 간의 컴파일 시점 의존성을 끊어내지 못하면 진정한 분리라고 할 수 없었고, 결국 기존 방식과 다를 바 없다는 결론에 이르렀습니다. 결국 주문과 결제 API를 명확히 분리했고, 그러자 이전보다 코드가 훨씬 깔끔하고 역할이 명확해지는 것을 볼 수 있었습니다.

 

물론 구조가 바뀌자 새로운 고민이 생겼습니다. 이벤트 기반으로 동작하는 클래스들은 어떻게 명명해야 할지, 패키지 구조는 어떻게 가져가는 것이 가장 명확할지 같은 문제들이었는데요. 역시 개발에서 가장 어려운 일 중 하나는 '이름 짓기'라고 다시 한 번 느끼는 시간이었습니다.

 

 

 

8주차: Kafka

 

Kafka 학습 초반에는 컨슈머(Consumer)나 리스너(Listener) 같은 기본 용어조차 생소했습니다. 여기에 '카프카는 원래 어려운 기술'이라는 막연한 선입견까지 더해져 더 크게 혼란을 겪었던 것 같습니다. 하지만 데드 레터 큐(Dead Letter Queue), 아웃박스(Outbox) 패턴 같은 개념도 차근차근 공부하며 제 것으로 만들고자 노력했습니다.

 

Kafka를 실제 멀티 모듈 프로젝트에 적용하는 과정에서, 핵심적인 설계 문제 하나에 부딪혔습니다. 바로 Producer와 Consumer가 공유하는 이벤트 객체를 어디에 위치시킬 것인가 하는 점이었는데요.

 

가장 쉬운 선택은 하나의 공통 모듈에 이벤트 페이로드 클래스를 두고, 양쪽 서비스가 이를 함께 참조하는 것입니다. 코드 중복이 없고 당장의 관리도 편해 보였습니다. 저 역시 처음에는 더 관리하기가 쉬울 것 같아 이 방식을 택했는데요.

 

이 편리함은 숨겨진 비용을 내포하고 있었습니다. 공통 객체를 참조하는 순간, Producer와 Consumer 사이에는 눈에 보이지 않는 강한 컴파일 타임 의존성이 생깁니다. 이는 이벤트 명세가 조금이라도 변경되면, 이벤트를 사용하는 모든 서비스가 함께 수정되고 재배포되어야 함을 의미합니다. 결국 '느슨한 결합'이라는 EDA의 핵심 철학을 정면으로 위배하는 설계였습니다.

 

지금 다시 설계한다면, 초기 구현이 조금 번거롭더라도 각 서비스가 자신에게 필요한 데이터만 담아 독립적인 이벤트 객체를 정의하도록 만들 것입니다. 공유 모듈의 편리함이 오히려 서비스 간의 강한 결합을 야기한다는 점, 그리고 눈에 보이지 않는 의존성이 명시적인 API 호출만큼이나 위험할 수 있다는 것을 깨달은 한 주였습니다.

 

 

 

9주차: Redis ZSET 을 활용한 일간 랭킹

 

9주차에는 Redis의 ZSET을 이용해 일간 랭킹 시스템을 구현했습니다. Sorted Set이 점수 기반 정렬을 알아서 처리해주기 때문에 처음에는 비교적 쉽게 생각했습니다. 하지만 막상 구현에 들어가니 단순히 데이터를 정렬하는 것 외에 고려할 점이 훨씬 많았습니다.

 

예를 들어, 콘텐츠의 가중치는 어떻게 부여할지, 신규 콘텐츠가 주목받기 어려운 콜드 스타트(Cold Start) 문제는 어떻게 해결할지 같은 정책적인 고민이 뒤따랐습니다. 콜드 스타트 문제까지 해결해보고 싶었지만, 아쉽게도 시간이 부족해 구현하지는 못했습니다.

 

그래도 이번 기회에 슬라이딩 윈도우(Sliding Window)를 비롯한 여러 랭킹 전략에 대해 학습할 수 있었는데요. 부족했던 부분은 재충전 시간을 가진 뒤 다시 구현을 해보려고 합니다. 구현을 만족할 수 있는 부분까지 하지는 못했지만, 배운 개념을 블로그에 글로 정리하니, 더 확실하게 제 지식으로 남아 좋은 경험이었습니다.

 

 

 

10주차: Materialized View와  Spring Batch를 통해 만드는 주간/월간 랭킹 시스템

 

마지막주차는 그동안 제가 잘못 알고 있던 기술 개념들을 바로잡는 시간이었습니다. 특히 Materialized View(MV)와 Spring Batch의 동작 방식에 대해 가지고 있던 오해를 명확히 해소할 수 있었습니다.

 

저는 그동안 'MV를 왜 쓰지? 그냥 View를 쓰거나, 통계용 테이블에 스케줄러를 돌리면 되지 않나?'라고 생각해왔습니다. 미리 계산된 결과를 저장해두어 조회 성능을 극대화하는 MV의 핵심적인 이점을 제대로 이해하지 못했던 것인데요.

 

Spring Batch에 대한 오해도 있었습니다. 저는 청크(Chunk) 기반으로 작업을 처리하면, 전체 데이터에 대한 순위를 매기는 작업이 중간에 꼬일 것이라고 막연히 생각했습니다. 하지만 이는 청크 프로세싱의 동작 방식과, 제가 구현한 비즈니스 로직을 혼동한 것이었습니다. Spring Batch의 청크는 기본적으로 개별 아이템을 독립적으로 읽고(Read) 가공(Process)하는 데 집중합니다. 따라서 순위 계산처럼 전체 데이터셋에 대한 집계가 필요한 로직은, 개별 청크의 처리(Process) 단계가 아니라 모든 청크 처리가 완료된 후 별도의 단계(Step)나 후처리 과정(Listener)에서 수행하도록 설계하는 것이 올바른 접근이었습니다.

 

이번 주차의 또 다른 흥미로운 경험은, 스케줄러로만 실행해봤던 배치 작업을 별도의 '배치 전용 모듈'로 분리한 것이었습니다. 단순히 정해진 시간에 코드를 실행하는 것을 넘어, 배치 로직의 책임과 역할을 명확히 격리하는 이 경험을 통해 시스템 아키텍처를 더 넓은 시야로 보게 되었습니다.

 

10주차까지 진행하니 총 4개의 모듈이 완성되었는데요. API, Streamer, PG, Batch까지 총 4개의 모듈이 각자의 역할을 수행하며 함께 동작하는 것을 테스트하던 순간이 이번 주차에 가장 큰 성취감을 느낀 경험이었습니다. 물론 시간 부족으로 주간/월간 집계 테이블을 분리하지 못한 점이 아쉬움으로 남아, 이 부분은 추후에 꼭 다시 개선해 볼 예정입니다.

 

 

 

마무리하며

 

10주동안 매 주차마다 배운 기술들을 완벽하게 이해한 상태인지 묻는다면, 솔직하게 그렇지는 않습니다. 아직 부족한 부분이 많고 앞으로 공부해야 할 것도 많은데요. 하지만 완벽하지는 않더라도 'Done is better than perfect' 이라는 말처럼 프로젝트 하나를 끝까지 완성해냈다는 경험이 가장 큰 성취감을 주었습니다. 앞으로 개발자로서 여러 기술적 선택지 앞에서 고민하는 순간이 많아질 텐데, 이번 프로젝트에서 겪은 경험들이 그때마다 더 좋은 결정을 내리는 데 도움이 될 것이라 생각합니다.

 

긴 글 읽어주셔서 감사합니다!

'💡 ETC' 카테고리의 다른 글

2025년 회고  (4) 2025.12.31
나의 마지막 부트캠프 루프팩 백엔드 1기 수료 후기  (0) 2025.09.28
[Loop:PAK] WIL 2주차  (0) 2025.07.25
[Loop:PAK] WIL 1주차  (2) 2025.07.18
[F-Lab] Java Backend 멘토링 2개월 후기  (0) 2025.01.19