유랑하는 나그네의 갱생 기록

だけど素敵な明日を願っている -HANABI, Mr.children-

Devlog/SpringBoot

HOW적 사고에서 벗어나기 - Why? (의도가 있는 설계법)

Madirony 2026. 2. 13. 13:45
반응형

Intro

설계 문서는 작업 지시서가 아니다

과거 프로젝트를 진행하며 작성했던 설계 문서는 단순 기능 구현 목록에 가까웠습니다. 비즈니스의 복잡성을 어떻게 해결할 것인지에 대한 고민보다는, "이 필드는 VARCHAR(100)으로 할지 1000으로 할지", "API 경로는 /users인지 /members인지"와 같은 구현 상세(HOW)에만 집중했습니다.

 

 

API 명세서에 왜 Socket 라우팅까지?

심지어 통신 패러다임이 완전히 다른 웹소켓(WebSocket)의 Pub/Sub 라우팅 주소까지 REST API 명세서 표에 포함하기도 했습니다. 프로토콜의 특성이나 아키텍처적 의도(WHY)는 고려하지 않은 채, 기존 문서 양식의 빈칸을 채우는 데 급급했던 결과입니다.

 

이러한 방식은 유지보수 과정에서 큰 비용을 발생시켰습니다. 개발 중 변경 사항이 생기면 문서를 일일이 찾아 수정해야 했고, 업데이트가 늦어지면 동료들이 이전 문서를 보고 잘못된 방향으로 개발할 위험이 있었습니다. 구현 상세만 빼곡히 적어놓은 문서는 코드가 한 줄만 수정되어도 금세 신뢰할 수 없는 문서(Outdated Document)가 되었습니다.

 

이 경험을 토대로, 설계 문서의 역할을 다음과 같이 다시 정의했습니다.

 

설계 문서는 구현 방법(HOW)이 아니라,
의사결정의 근거(WHY)를 기록하는 저장소다.

 

이러한 깨달음을 바탕으로, 이번 이커머스 시스템 설계에서는 관성적인 기술 도입을 지양하고 '우리 시스템에 왜 이것이 필요한가?'를 가장 먼저 고민했습니다. 본론에서 그 과정에서 도출된 두 가지 핵심 의사결정(Decision)을 공유하고자 합니다.

 


본론

이전에 TDD를 통해 강결합(Tight Coupling)이 초래하는 유지보수의 어려움을 경험했습니다. 그래서 이번에는 시스템 간의 '격리(Isolation)'와 '독립성 보장'을 최우선 목표로 두었습니다.

 

Decision 1: 물리적 FK(Foreign Key) 제약조건 제거

[AS-IS: 관성적인 생각]

일반적으로 RDBMS를 설계할 때 데이터 무결성을 위해 외래키(Foreign Key)는 필수라고 여겨집니다. 초기 설계에서도 Brand와 Product 테이블의 정합성을 DB에 위임하기 위해 당연히 FK 제약조건을 설정하려 했습니다.

 

[Deep Dive: 대용량 환경에서의 FK 리스크]

하지만 확장성(Scalability)과 성능(Performance) 관점에서 검토했을 때, 물리적 FK는 잠재적인 장애 포인트가 될 수 있었습니다.

 

  1. 쓰기 지연과 데드락 (Locking & Deadlock): FK가 걸려있으면 자식 테이블(Product)에 데이터를 삽입할 때 부모 테이블(Brand)의 락(Lock)을 확인하거나 조회를 수행합니다. 특히 ON DELETE CASCADE가 설정된 상태에서 부모 데이터를 삭제하면, 연관된 수만 개의 자식 데이터를 지우기 위해 긴 트랜잭션이 점유되고 이는 데드락을 유발할 수 있습니다.
  2. 샤딩 및 MSA 전환의 한계: 데이터 증가로 인해 DB를 분할(Sharding)하거나 서비스 도메인을 분리(MSA)할 때 물리적 FK는 아키텍처 확장을 가로막는 기술 부채가 됩니다.

Reference: 대규모 시스템에서는 성능과 확장성을 위해 물리적 FK를 배제하는 사례가 많습니다. GitHub Engineering 팀은 대용량 데이터베이스를 파티셔닝하는 과정에서 성능 오버헤드와 무중단 스키마 마이그레이션을 위해 외래키 제약조건을 사용하지 않는 아키텍처를 채택했습니다.
(참고 1: Partitioning GitHub’s relational databases to handle scale)
(참고 2: GitHub Engineer's Thoughts on Foreign Keys (gh-ost Issue #331))

 

[TO-BE: 논리적 연결 (Logical FK)]

ERD & DDR

멘토링 피드백과 위의 근거들을 종합하여 물리적 제약조건을 제거했습니다.

  • 결정: DDL에서 FOREIGN KEY 구문을 삭제하고, 애플리케이션 레벨(Service Layer)에서 식별자(ID) 유효성을 검증한다.
  • Trade-off 관리: 제약조건 제거로 인한 '고아 데이터' 발생 위험은 Soft Delete 전략과 트랜잭션 경계 설정을 통해 방어한다.

 


 

Decision 2: 데이터 중복을 허용하기 (Snapshot Pattern)

[AS-IS: 관성적인 생각]

"데이터베이스 정규화(Normalization) 원칙에 따라 중복 데이터는 제거해야 해." 이 원칙에 따라 초기에는 주문 상세(OrderItem) 테이블이 상품(Product) 테이블의 PK만 참조하도록(JOIN) 설계했습니다.

 

[Deep Dive: 시간축에 따른 데이터의 의미 변화]

하지만 커머스 도메인에서 시간은 매우 중요한 변수입니다.

  • 상품(Product): 현재 판매 중인 상태 (가변, Mutable)
  • 주문(Order): 과거에 체결된 계약 (불변, Immutable)

 

만약 상품 가격이 10,000원에서 20,000원으로 인상되었을 때, JOIN을 통해 과거 데이터를 조회하면 어제 10,000원에 결제한 고객의 영수증에도 20,000원으로 표기되는 데이터 왜곡이 발생합니다.

Reference: 마틴 파울러(Martin Fowler)는 이러한 상황을 'Temporal Pattern' 혹은 'Audit Log' 개념으로 설명합니다. 특정 이벤트가 발생한 시점의 상태를 캡처해두지 않으면 비즈니스 무결성이 훼손될 수 있습니다.
(참고: Martin Fowler - Temporal Patterns)

 

[TO-BE: 스냅샷 전략 (Snapshot Strategy)]

시퀀스 다이어그램의 일부 (주문 생성 시점의 데이터를 복제하여 저장하는 스냅샷 패턴의 시퀀스 흐름)

요구사항을 분석하는 과정에서 "왜 주문 도메인에 스냅샷이 필요한가?"에 대한 명확한 비즈니스적 해답을 찾을 수 있었습니다. 이에 따라 데이터 정규화(Normalization) 원칙을 고수하기보다는, 역정규화(Denormalization)를 감수하더라도 스냅샷 패턴을 도입하는 것이 아키텍처 관점에서 더 타당하다고 결론 내렸습니다.

 

  • 결정: 주문 생성 시점의 상품명, 가격, 옵션 스펙을 OrderItem 테이블에 값(Value)으로 복사하여 저장한다.
  • Trade-off 관리: 스토리지 공간의 추가 소모(역정규화)를 감수하는 대신, 주문 데이터의 완전한 불변성(Immutability)과 조회 성능 향상을 얻었다.

 


 

번외로..) 레거시는 "생각하지 않은 빚(Debt)"이다

이번 설계를 진행하며 가장 많이 되뇌었던 문장은 다음과 같습니다.

"시스템은 거짓말을 하지 않습니다.
조직이 결정을 얼마나 자주 미루는지, 불편함을 얼마나 오래 견디는지 그대로 남깁니다."
(출처: 김형근 님 링크드인 - "레거시는 코드 문제가 아니라 조직 문제입니다")

 

과거 제가 작성했던 '일단 돌아가는 코드'와 '칸 채우기 식 문서'는 "왜 이렇게 해야 하는가?"에 대한 판단을 미룬 '사고(Thought)의 부채'였습니다.

 

이제 기능 설계나 코드 작성을 시작하기 전, 스스로에게 두 가지 질문을 던지려 합니다.

  • "이 구현(HOW)은 어떤 문제(WHY)를 해결하는가?"
  • "이 결정으로 인해 우리가 감수해야 할 리스크(Trade-off)는 무엇인가?"

 

아직 완벽한 아키텍처를 설계하지는 못하더라도, 최소한 '근거 없이 미룬 결정'을 남에게 부채로 남기지는 않아야겠습니다.


P.S.

예전 설계 문서를 다시 보니, 명확한 근거 없이 '보안'이라는 명목하에 내부 PK와 외부 UUID를 변환하는 복잡한 구조를 썼던 프로젝트가 있었습니다. 이번 설계에서는 도메인의 특성을 먼저 고민했습니다. 쿠폰처럼 악의적인 유저의 예측을 방어해야 하는 경우가 아니라면, DB 인덱싱 성능에 유리한 Long (Auto Increment)을 사용하는 것이 현재 아키텍처에서 합리적인 판단이라고 생각했습니다.

 

향후 프로젝트를 대용량 분산 환경으로 고도화하게 된다면, 성능과 보안을 모두 챙길 수 있는 TSID나 Snowflake 같은 식별자 생성 전략도 다시 한번 깊게 파헤쳐 보려 합니다.

 

 

반응형
TOP