fos-blog/study
01 / 홈02 / 카테고리
01 / 홈02 / 카테고리

카테고리

  • AI 페이지로 이동
    • RAG 페이지로 이동
    • langgraph 페이지로 이동
    • agents.md
    • BMAD Method — AI 에이전트로 애자일 개발하는 방법론
    • Claude Code의 Skill 시스템 - 개발자를 위한 AI 자동화의 새로운 차원
    • Claude Code를 5주 더 쓴 결과 — 스킬·CLAUDE.md를 키워가는 방식
    • Claude Code를 11일 동안 쓴 결과 — 데이터로 본 나의 사용 패턴
    • Claude Code 멀티 에이전트 — Teams
    • AI 에이전트와 디자인의 새 컨벤션 — DESIGN.md, Google Stitch, Claude Design
    • 하네스 엔지니어링 실전 — 4인 에이전트 팀으로 코딩 파이프라인 구축하기
    • 하네스 엔지니어링 — 오래 실행되는 AI 에이전트를 위한 설계
    • 멀티모달 LLM (Multimodal Large Language Model)
    • AI 에이전트와 함께 MVP 만들기 — dooray-cli 사례
  • ai 페이지로 이동
    • agent 페이지로 이동
  • algorithm 페이지로 이동
    • live-coding 페이지로 이동
    • 분산 계산을 위한 알고리즘
  • architecture 페이지로 이동
    • [초안] 시니어 백엔드를 위한 API 설계 실전 스터디 팩 — REST · 멱등성 · 페이지네이션 · 버전 전략
    • [초안] API Versioning과 Backward Compatibility: 시니어 백엔드 관점 정리
    • 캐시 설계 전략 총정리
    • [초안] CJ푸드빌 커머스/F&B 도메인 설계 면접 대비 — 슬롯 경험을 주문·결제·쿠폰·매장 상태 설계로 번역하기
    • [초안] 커머스 Spring 서비스에 Clean/Hexagonal Architecture를 실용적으로 적용하기
    • [초안] 커머스 주문 상태와 데이터 정합성 기본기 — CJ푸드빌 면접 대비
    • [초안] 쿠폰/프로모션 동시성과 정합성 기본기 — 선착순·중복 사용 방지·발급/사용/복구
    • [초안] DDD와 도메인 모델링: 시니어 백엔드 관점의 전술/전략 패턴 실전 가이드
    • [초안] Decorator & Chain of Responsibility — 행동을 체인으로 조립하는 두 가지 방식
    • 디자인 패턴
    • [초안] 분산 아키텍처 완전 정복: Java 백엔드 시니어 인터뷰 대비 실전 가이드
    • [초안] 분산 트랜잭션과 Outbox 패턴 — 왜 2PC를 피하고 어떻게 대신할 것인가
    • 분산 트랜잭션
    • [초안] e-Commerce 주문·결제 도메인 모델링: 상태머신, 멱등성, Outbox/Saga 실전 정리
    • [초안] F&B 쿠폰·프로모션·멤버십·포인트 설계
    • [초안] F&B · e-Commerce 디지털 채널 도메인 한 장 정리 — CJ푸드빌 디지털 채널 백엔드 면접 대비
    • [초안] F&B 주문/매장/픽업 상태머신 설계 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] F&B 이커머스 결제·환불·정산 운영 가이드
    • [초안] Hexagonal / Clean Architecture를 Spring 백엔드에 적용하기
    • [초안] 대규모 커머스 트래픽 처리 패턴 — 1,600만 고객과 올영세일을 버티는 설계
    • [초안] 레거시 JSP/jQuery 화면과 신규 API가 공존하는 백엔드 운영 전략
    • [초안] MSA 서비스 간 통신: Redis [Cache-Aside](../database/redis/cache-aside.md) × Kafka 이벤트 하이브리드 설계
    • [초안] Observability 입문: 시니어 백엔드가 장애를 탐지하고 대응하는 방식
    • [초안] Outbox / Inbox Pattern 심화 — 분산 메시징의 정합성 문제를 DB 트랜잭션으로 풀어내기
    • [초안] 결제 도메인 멱등성과 트랜잭션 재시도 기본기
    • [초안] 시니어 백엔드를 위한 Resilience 패턴 실전 가이드 — Timeout, Retry, Circuit Breaker, Bulkhead, Backpressure
    • [초안] REST API 버저닝과 모바일 앱 하위 호환성 — CJ푸드빌 디지털 채널 백엔드 관점
    • [초안] Strategy Pattern — 분기문을 없애는 설계, 시니어 백엔드 인터뷰 핵심 패턴
    • [초안] 시니어 백엔드를 위한 시스템 설계 입문 스터디 팩
    • [초안] 템플릿 메서드 패턴 - 백엔드 처리 골격을 강제하는 가장 오래되고 가장 위험한 패턴
    • [초안] 대규모 트래픽 중 무중단 마이그레이션 — Feature Flag + Shadow Mode 실전
  • database 페이지로 이동
    • mysql 페이지로 이동
    • opensearch 페이지로 이동
    • redis 페이지로 이동
    • 김영한의-실전-데이터베이스-설계 페이지로 이동
    • 커넥션 풀 크기는 얼마나 조정해야 할까?
    • 인덱스 - DB 성능 최적화의 핵심
    • [초안] JPA N+1과 커머스 조회 모델: 주문/메뉴/쿠폰 도메인에서 살아남기
    • [초안] MyBatis 기본기 — XML Mapper, resultMap, 동적 SQL, 운영 패턴 정리
    • [초안] MyBatis와 JPA/Hibernate 트레이드오프 — 레거시 백엔드를 다루는 시니어 관점
    • 역정규화 (Denormalization)
    • 데이터 베이스 정규화
  • devops 페이지로 이동
    • docker 페이지로 이동
    • k8s 페이지로 이동
    • k8s-in-action 페이지로 이동
    • observability 페이지로 이동
    • [초안] 커머스/F&B 채널 장애 첫 5분과 관측성 기본기
    • Envoy Proxy
    • [초안] F&B / e-Commerce 운영 장애 대응과 모니터링 — 백엔드 관점 정리
    • Graceful Shutdown
  • finance 페이지로 이동
    • industry-cycle 페이지로 이동
    • investing 페이지로 이동
    • stock-notes 페이지로 이동
  • http 페이지로 이동
    • HTTP Connection Pool
  • interview 페이지로 이동
    • [초안] AI 서비스 팀 경험 기반 시니어 백엔드 면접 질문 뱅크 — Spring Batch RAG / gRPC graceful shutdown / 전략 패턴 / 12일 AI 웹툰 MVP
    • [초안] CJ푸드빌 디지털 채널 Back-end 개발자 직무 분석
    • [초안] CJ푸드빌 디지털 채널 Back-end 면접 답변집 — 슬롯 도메인 경험을 커머스/F&B 설계로 번역하기
    • [초안] F&B / e-Commerce 운영 모니터링과 장애 대응 인터뷰 정리
    • Observability — 면접 답변 프레임
    • [초안] 시니어 Java 백엔드 면접 마스터 플레이북 — 김병태
    • [초안] NSC 슬롯팀 경험 기반 질문 은행 — 도메인 모델링·동시성·성능·AI 협업
  • java 페이지로 이동
    • concurrency 페이지로 이동
    • jdbc 페이지로 이동
    • opentelemetry 페이지로 이동
    • spring 페이지로 이동
    • spring-batch 페이지로 이동
    • 더_자바_코드를_조작하는_다양한_방법 페이지로 이동
    • [초안] Java 동시성 락 정리 — 커머스 메뉴/프로모션 정책 캐시 갱신 관점
    • [초안] JVM 튜닝 실전: 메모리 구조부터 Virtual Threads, GC 튜닝, 프로파일링까지
    • Java의 로깅 환경
    • MDC (Mapped Diagnostic Context)
    • Java StampedLock — 읽기 폭주에도 쓰기가 밀리지 않는 락
    • Virtual Thread와 Project Loom
  • javascript 페이지로 이동
    • typescript 페이지로 이동
    • AbortController
    • Async Iterator와 제너레이터
    • CommonJS와 ECMAScript Modules
    • 제너레이터(Generator)
    • Http Client
    • Node 백엔드 운영 패턴 — Streams 백프레셔, pipe/pipeline, 멱등성 vs 분산 락
    • Node.js
    • npm vs pnpm — 어떤 기준으로 선택했나
    • `setImmediate()`
  • kafka 페이지로 이동
    • [초안] Kafka 기본 개념 — 토픽, 파티션, 오프셋, 복제
    • Kafka를 사용하여 **데이터 정합성**은 어떻게 유지해야 할까?
    • [초안] Kafka 실전 설계: 파티션 전략, 컨슈머 그룹, 전달 보장, 재시도, 순서 보장 트레이드오프
    • 메시지 전송 신뢰성
  • linux 페이지로 이동
    • fsync — 리눅스 파일 동기화 시스템 콜
    • tmux — Terminal Multiplexer
  • network 페이지로 이동
    • L2(스위치)와 L3(라우터)의 역할 차이
    • L4와 VIP(Virtual IP Address)
    • IP Subnet
  • rabbitmq 페이지로 이동
    • [초안] RabbitMQ Basics — 실전 백엔드 관점에서 정리하는 메시지 브로커 기본기
    • [초안] RabbitMQ vs Kafka — 백엔드 메시징 선택 기준과 실전 운영 관점
  • security 페이지로 이동
    • [초안] 시니어 백엔드를 위한 보안 / 인증 스터디 팩 — Spring Security, JWT, OAuth2, OWASP Top 10
  • task 페이지로 이동
    • ai-service-team 페이지로 이동
    • nsc-slot 페이지로 이동
    • sb-dev-team 페이지로 이동
    • the-future-company 페이지로 이동
  • testing 페이지로 이동
    • [초안] 시니어 Java 백엔드를 위한 테스트 전략 완전 정리 — 피라미드부터 TestContainers, 마이크로벤치, Contract까지
  • travel 페이지로 이동
    • 오사카 3박 4일 일정표: 우메다 쇼핑, USJ, 난바·도톤보리, 오사카성
  • web 페이지로 이동
    • [초안] HTTP / Cookie / Session / Token 인증 기본기 — 레거시 JSP와 모바일 API가 공존하는 백엔드 관점
FOS-BLOG · FOOTERall systems normal·v0.1 · 2026.04.27·seoul, kr
Ffos-blog/study

개발 학습 기록을 정리하는 블로그입니다. 공부하면서 기록하고, 기록하면서 다시 배웁니다.

visitors
01site
  • Home↗
  • Posts↗
  • Categories↗
  • About↗
02policy
  • 소개/about
  • 개인정보처리방침/privacy
  • 연락처/contact
03categories
  • AI↗
  • Algorithm↗
  • DB↗
  • DevOps↗
  • Java/Spring↗
  • JS/TS↗
  • React↗
  • Next.js↗
  • System↗
04connect
  • GitHub@jon890↗
  • Source repositoryjon890/fos-study↗
  • RSS feed/rss.xml↗
  • Newsletter매주 1 회 · 한 편의 글→
© 2026 FOS Study. All posts MIT-licensed.
built with·Next.js·Tailwind v4·Geist·Pretendard·oklch
fos-blog/task/첫 슬롯을 만들며 시작된 1년의 아키텍처 정…
system

첫 슬롯을 만들며 시작된 1년의 아키텍처 정리 — SpinOperationHandler와 static 해체, 그리고 남은 과제

진행 기간: 2024.06 2025.11 슬롯팀에 합류해 첫 슬롯(Slot 21 — 클러스터 + 텀블링 + 머지)을 맡으면서 마주친 코드베이스는 테스트를 붙이기가 매우 어려운 상태였다. 작은 단위로 TDD 형태로 접근해보려 했지만, 로직이 강결합되어 있고 스프링 컴포넌트를 static으로 호출하는 구조가 도처에 깔려 있어 곧 벽에 부딪혔다. "이걸 한 번에...

2026.04.19·8 min read·64 views

진행 기간: 2024.06 ~ 2025.11

슬롯팀에 합류해 첫 슬롯(Slot 21 — 클러스터 + 텀블링 + 머지)을 맡으면서 마주친 코드베이스는 테스트를 붙이기가 매우 어려운 상태였다. 작은 단위로 TDD 형태로 접근해보려 했지만, 로직이 강결합되어 있고 스프링 컴포넌트를 static으로 호출하는 구조가 도처에 깔려 있어 곧 벽에 부딪혔다.

"이걸 한 번에 갈아엎는 건 불가능하다. 작은 영역부터 조금씩 풀자"는 결심으로 1년 반에 걸쳐 진행한 점진적 정리 기록이다. 한 번에 해결된 건 하나도 없고, 작은 PR을 수십 번 쌓아 조금씩 움직였다. 그 과정에서 중간에 시도한 구조가 한계를 드러낸 순간도 있었고, 지금까지도 풀지 못한 문제가 있다.


첫 슬롯에서 마주친 네 개의 벽

코드를 파악하면서 TDD로 방어막을 치려 했는데 매번 같은 패턴으로 막혔다.

벽 1 — 거대한 SpinResultParameter. 스핀 요청은 이 객체 하나에 모든 맥락이 실려 서비스 레이어로 전달된다. 지금도 436줄이다. 신규 슬롯 하나를 만들려면 이 객체를 이해해야 하는데 필드가 너무 많아 "어디까지 필수고 어디까지 선택인지" 파악이 어려웠다.

벽 2 — 혼동되는 네이밍. SpinResult는 결과 객체로 의미가 명확한데, SpinResultParameter는 스핀을 시작할 때 던지는 파라미터다. "스핀 결과에 대한 파라미터?"라는 해석이 먼저 떠오른다. 더 난감한 건 SpinParameter라는 별도 클래스가 따로 존재한다는 점이다. 두 이름이 공존하면 어느 게 어느 맥락에 쓰이는지 코드를 열기 전에는 구분이 안 된다.

벽 3 — static 강결합. SlotStaticDataLoader.getSlotProduct(slotId) 같은 static 호출이 서비스/컴포넌트/인터페이스 default 메서드까지 퍼져 있었다. 스프링 빈으로 동작하는 컴포넌트들도 어느 지점에서는 static을 찔렀고, 인터페이스의 default 메서드가 내부에서 다른 정적 자원을 참조하는 패턴도 많았다. Mock이 안 되니 테스트도 안 된다. 모킹하려면 PowerMock 같은 방향으로 가야 하는데 그건 다른 종류의 기술 부채를 들여오는 일이었다.

벽 4 — 카피된 3개 PlayService. 스핀 요청 처리 서비스는 NormalPlayService, TutorialPlayService, CheatPlayService 3종으로 분리되어 있었는데, 내부 흐름이 거의 같은 카피였고 아주 작은 분기만 달랐다. 자주 읽지 않는 사람은 세 서비스의 차이를 잡아내기 어려웠고, 한쪽에 버그를 고치면 다른 쪽에 그대로 남아 있었다. 사이드이펙트에 매우 취약한 구조였다.

인사이트. 테스트 작성이 어렵다는 건 "이 코드가 잘 설계되지 않았다"는 가장 조용한 신호다. static/강결합/거대 객체는 각각으론 견딜 만 해 보여도, 이 셋이 동시에 있으면 테스트 불가능 지점이 교집합으로 만들어진다. 첫 슬롯 작업에서 이걸 체감하고부터 큰 리팩터링을 한 번에 하려는 유혹을 버렸다.


변곡점: 스핀 로직 템플릿화 (#5717, 2024.12)

첫 번째로 손에 잡은 건 카피된 PlayService였다. 튜토리얼 스핀을 계속 건드리다 보니 Normal과 중복된 흐름이 더 선명하게 눈에 띄었다.

Before — 카피된 3개 PlayService

plaintext
NormalPlayService                TutorialPlayService              CheatPlayService
 ├─ 요청 검증                      ├─ 요청 검증                       ├─ 요청 검증
 ├─ 유저 정보 조회                  ├─ 유저 정보 조회                   ├─ 유저 정보 조회
 ├─ 스핀 파라미터 조립               ├─ 스핀 파라미터 조립 (시나리오 분기)  ├─ 스핀 파라미터 조립 (치트 분기)
 ├─ 스핀 실행                      ├─ 스핀 실행 (시나리오 결과 주입)      ├─ 스핀 실행 (치트 결과 주입)
 ├─ 후처리 / 로그                   ├─ 후처리 / 로그                   ├─ 후처리 / 로그
 └─ 응답 변환                      └─ 응답 변환                      └─ 응답 변환

흐름이 거의 같고, 각 단계에 슬쩍 다른 분기가 들어가 있다. 이 구조에서 생긴 문제들:

  • 한쪽을 고치면 다른 두 곳에 같은 변경을 반영해야 함
  • 어떤 단계가 공통이고 어떤 단계가 분기인지 코드만 보고는 알기 어려움
  • 리뷰어가 "이 변경이 Tutorial/Cheat에도 반영됐나?"를 매번 확인해야 함

After — SpinOperationHandler + AbstractPlayService

템플릿 메서드 패턴으로 공통 흐름을 AbstractPlayService로 올리고, 각 PlayService가 특수 동작만 hook으로 주입하게 바꿨다.

plaintext
AbstractPlayService (공통 흐름)
 ├─ 요청 검증
 ├─ handler.onStart()                 ← hook
 ├─ handler.onLoadLastSpinResult()    ← hook
 ├─ handler.onLoadUserInfo()          ← hook
 ├─ handler.validateAdditional()      ← hook
 ├─ handler.makeReelCategory()        ← 필수 구현
 ├─ handler.prepareSpinResultParameter() ← hook
 ├─ handler.makeSpinResult()          ← hook (default: slotService.makeSpinResult)
 ├─ handler.onFinish()                ← 필수 구현
 └─ handler.makeUserInfoResponse()    ← 필수 구현
       ↑
   NormalPlayService / TutorialPlayService / CheatPlayService
   각각 SpinOperationHandler 구현체를 제공. 필요한 hook만 오버라이드

SpinOperationHandler 인터페이스 자체는 9개 메서드 — 대부분 default 구현을 제공하고, 각 PlayService는 자기한테 필요한 것만 오버라이드한다. 예를 들어 Tutorial은 makeSpinResult hook을 덮어 "시나리오 결과"를 반환하도록, Cheat는 prepareSpinResultParameter를 덮어 치트 플래그를 삽입한다.

java
// 개념 설명용 의사코드 — 실제 인터페이스 일부
public interface SpinOperationHandler {
    default void onStart(...) {}
    void onLoadLastSpinResult(...);
    default void validateAdditional(...) {}
    ReelCategory makeReelCategory(SpinResultParameter param, SlotService slotService);
    default SpinResult makeSpinResult(SlotService slotService, SpinResultParameter param) {
        return slotService.makeSpinResult(param);   // 기본은 그냥 위임
    }
    // ... 9개 hook
}

변경량은 +490 / -437. 새 파일이 생긴 게 아니라 공통 흐름을 끌어올린 만큼 각 PlayService에서 같은 양이 빠진 것이다.

얻은 것: 세 PlayService의 흐름이 한 곳에서 읽힌다. 새 분기가 필요하면 hook 하나만 오버라이드한다. 리뷰어가 "세 곳에 반영됐나"를 체크할 필요가 없어졌다.

얻지 못한 것: 테스트 방어막은 여전히 없었다. 흐름은 정리됐지만 SlotStaticDataLoader static 호출, SpinResultParameter 거대 객체, 컴포넌트간 강결합은 그대로 남아 있어서 단위 테스트 작성은 여전히 막혔다. 눈으로 보는 구조가 좋아졌을 뿐이라는 걸 인정해야 했다.


꾸준한 static 해체 — 작은 영역부터

템플릿화로 흐름을 정리하고 나서 본격적으로 static을 걷어내기 시작했다. 한 번에는 불가능했다. PR마다 하나의 영역씩 풀었다.

시점PR푼 영역
2024.10#5560유효성 검증을 SpinValidator 컴포넌트로 분리 (첫 컴포넌트 추출)
2025.04#6041ThreadLocalRandom을 Random 컴포넌트로 추상화
2025.07#7320AbstractPlayService의 응답 변환 로직을 ResponseMapper 유틸로 분리
2025.08#7338SlotStaticDataLoader.getSlotProduct() static 메서드 제거 → 빈 주입으로 전환
2025.08#7483StaticDataLoader.refreshAll() 중 NPE 현상 방지 (StampedLock 도입)
2025.08#7491Alias 테이블 init/refresh 시 IN 절로 일괄 조회
2025.09#7513SlotService 인터페이스의 default 구현을 BaseSlotService 추상 클래스로 이동
2025.10#7619JackpotService가 SlotStaticLoader를 주입받아 사용하도록 변경

SlotStaticDataLoader 여정만 따로 보면

가장 강결합이 심했던 SlotStaticDataLoader의 변화가 이 중 핵심이다.

  1. #5425(2024.10) — 애플리케이션 기동 전에 로더가 수행되도록 순서만 정리. static 호출 구조는 그대로 유지. 당시엔 근본 문제를 건드릴 여유가 없었다.
  2. #7338(2025.8) — static 메서드를 빈 메서드로 전환. 이 PR이 static 해체의 실질적 시작점. 같은 PR에서 테스트 정리도 함께 했다. static일 때는 Mock이 안 됐지만 빈이 되자 @Autowired로 주입받아 테스트에서 행동을 대체할 수 있게 됐다.
  3. #7483(2025.8) — 빈 전환 후 드러난 새 문제. refreshAll() 중 clear + 재로드 도중 다른 스레드가 조회하면 NPE가 났다. StampedLock으로 리로드 구간을 보호(상세는 슬롯 엔진 추상화).
  4. #7491(2025.8) — Alias 테이블을 게임별로 쿼리하던 걸 IN 절로 일괄화. 빈으로 전환하면서 호출 경로가 명확해지자 최적화 포인트도 보였다.
  5. #7619(2025.10) — 마지막 남은 JackpotService까지 SlotStaticLoader 주입으로 전환. 이 시점에서 프로덕션 코드 기준 SlotStaticDataLoader.<static> 호출이 사라졌다.

인사이트. static 제거는 단일 PR이 아니라 "드러나지 않은 의존 그래프를 한 노드씩 잘라내는" 작업이었다. 한 곳을 잘라내면 그걸 호출하던 다음 지점이 보였고, 그 지점을 고치면 또 다음이 보였다. 중간에 #7483 같은 부수 효과(락이 필요해짐)도 같이 처리해야 했다. 결국 시간이 해결해주는 게 아니라, 시간과 함께 작은 PR을 쌓아야 해결되는 성격의 작업이었다.

테스트 인프라 자체의 변화(단위 → 통합, Extension 기반, 치트 데이터)는 별도 글 슬롯 테스트 공통 템플릿에서 다뤘다. 이 글은 프로덕션 코드의 강결합 해체에 집중했고, 두 흐름이 합쳐져 2025.10 즈음에는 "새 슬롯을 만들 때 통합 테스트로 실제 동작을 검증할 수 있다"는 상태에 도달했다.


여전히 남은 것 — SpinResultParameter 436줄

벽 1과 벽 2는 아직 풀지 못했다.

네이밍 재정립 — 미완

SpinParameter와 SpinResultParameter가 현재도 공존한다. 둘의 역할을 구분하는 관례가 있지만, 이름만 보면 여전히 서로를 뒤바꿀 수 있을 것처럼 읽힌다. "재정립이 필요하다"고 판단한 뒤 구체 작업으로 옮기지 못한 건, 두 이름이 프로젝트 전반의 메서드 시그니처에 퍼져 있어 이름 바꾸기 작업 자체가 다른 리팩터링과 엉켜 들어갈 위험이 컸기 때문이다. 지금도 아쉬운 부분이다.

거대 파라미터 객체 쪼개기 — 배제

SpinResultParameter는 436줄이다. 안에는 잭팟 관련 필드처럼 잭팟을 쓰지 않는 슬롯에는 불필요한 필드도 포함되어 있다. 모든 슬롯의 스핀 요청이 같은 객체를 통과하니, 잭팟 없는 슬롯도 잭팟 필드를 끼고 흘러간다.

이걸 쪼개려면 makeSpinResult(SpinResultParameter) 시그니처를 건드려야 하는데, 이 메서드가 서비스·템플릿·hook·테스트의 거의 모든 경로를 통과한다. 시그니처 변경이 도미노로 번지면서 PR 하나로 끝낼 수 없는 규모가 된다. 쪼개기 이득 대비 전파 비용이 너무 커서 배제했다. 지금도 이 판단이 옳았다고 생각하지만, "언젠가는 풀어야 할 부채"로 남겨뒀다.


협업은 의견을 구하고 머지하는 방식이었다

팀은 4명이었고, 선배 개발자 한 명이 있었다. 다만 구조 개선에 적극 관심을 보이는 팀원은 많지 않아, 위 리팩터링 대부분은 내가 먼저 제안하고 의견을 구한 뒤 머지하는 방식으로 진행됐다. 큰 변경을 한 번에 제안하면 리뷰 부담이 커서 거부감이 생기니, 작은 단위로 쪼개서 한 번에 하나씩 머지하는 걸 의도적으로 지켰다.

이 과정에서 의식적으로 지킨 세 가지 원칙이 있다.

  • PR 하나에 한 가지 주제만. "static 제거 + 테스트 정리"를 섞어 올릴 때도 리뷰어가 섞어 보지 않도록 PR 설명에서 의도를 분명히 구분했다.
  • Before/After를 PR 설명에 직접 그렸다. 코드 diff만 보면 "왜 이렇게 바꿨나"가 안 보이는 케이스가 많다. 흐름도나 짧은 표를 넣어 의도를 먼저 보여주고 코드를 보도록 유도했다.
  • 한 번에 원하는 종착지로 가지 않았다. 예를 들어 SlotStaticDataLoader도 한 PR에서 static 제거 + 락 도입 + Alias 일괄 조회를 전부 시도하고 싶었지만, 네 개의 PR로 쪼갰다. 각 단계가 실제로 작동하는지 리뷰하기 쉬워졌다.

지금 보면

이 여정 전체를 돌아보면 "큰 PR로 한 번에 갈아엎는 것"이 답이 아니었던 영역이었다. 초기엔 "전체 리팩터링 계획서"를 쓰고 싶은 유혹이 있었지만, 팀 분위기와 일정 안에서 그건 가능하지 않았다. 대신 **"코드를 건드릴 때마다 한 층씩 벗기는 보이 스카우트 룰"**에 가까운 접근이 실제로 잘 먹혔다. 1년에 걸쳐 누적된 작은 PR들이 결국 "static이 제거된 빈 주입 구조 + 통합 테스트 가능한 인프라"를 만들어냈다.

다르게 갔으면 좋았을 것:

  • 네이밍 재정립을 조금 더 일찍 시도했어야 한다. 시그니처 전파를 무서워하지 말고, 이름만 바꾸는 PR을 한 번 올리고 리뷰어들과 합의했다면 지금도 남은 혼동을 줄일 수 있었다.
  • SpinResultParameter 쪼개기의 "시작점"만이라도 만들어둘 수 있었다. 예를 들어 잭팟 필드만 별도 객체로 분리하는 식으로. 전체 도미노를 다 치지 않아도 "이 경계는 이미 나눠져 있다"는 시그널은 남길 수 있었다.

반대로 잘 했다 싶은 건 "한 번에 가지 않겠다"는 결정을 일관되게 지킨 것이다. 템플릿화 PR(#5717)도 한 번에 끝내고 싶은 유혹이 있었지만, 그 전에 SpinValidator 분리(#5560)와 바이피처 검증 개선(#5696)을 먼저 머지해 코드를 익숙하게 만든 뒤에야 템플릿화를 올렸다. 그 순서가 없었으면 템플릿화 PR 리뷰가 훨씬 까다로웠을 것이다.


관련 문서

  • Slot 21 — 클러스터 + 텀블링 + 머지 슬롯 구현기 — 이 여정의 시작이 된 첫 슬롯
  • 슬롯 테스트 공통 템플릿 — 테스트 인프라 진화(단위 → 통합) 허브
  • 슬롯 엔진 추상화 — StampedLock 기반 정적 데이터 리로드 등 후속 아키텍처 정리
on this page
  • 01첫 슬롯에서 마주친 네 개의 벽
  • 02변곡점: 스핀 로직 템플릿화 (#5717, 2024.12)
  • Before — 카피된 3개 PlayService
  • After — SpinOperationHandler + AbstractPlayService
  • 03꾸준한 static 해체 — 작은 영역부터
  • SlotStaticDataLoader 여정만 따로 보면
  • 04여전히 남은 것 — SpinResultParameter 436줄
  • 네이밍 재정립 — 미완
  • 거대 파라미터 객체 쪼개기 — 배제
  • 05협업은 의견을 구하고 머지하는 방식이었다
  • 06지금 보면
  • 07관련 문서

댓글 (0)