문제 발생

운영 중인 SQL Server 데이터베이스에서 디스크 사용량 경고가 발생했다.

또한 서비스 장애와 속도가 느려지는 현상들이 발생했다.

확인 결과 트랜잭션 로그(LDF) 파일이 약 500GB까지 증가하였으며, 로그 사용률 또한 거의 100%에 근접한 상태였다.

일반적으로 트랜잭션 로그가 급격히 증가하는 경우에는 다음과 같은 원인을 먼저 의심하게 된다.

  • 장시간 종료되지 않는 트랜잭션
  • 대량 데이터 처리 작업
  • 로그 백업 미수행
  • Replication 또는 CDC 관련 이슈

이번 사례 역시 이러한 관점에서 원인 분석을 진행하였다.


1차 확인 - 장기 트랜잭션 확인

가장 먼저 SQL Server의 활성 트랜잭션 상태를 확인하였다.

DBCC OPENTRAN;

그러나 예상과 달리 장시간 유지 중인 사용자 트랜잭션은 확인되지 않았다.

반면 데이터베이스 상태를 조회한 결과 다음과 같은 정보가 확인되었다.

SELECT
    name,
    recovery_model_desc,
    log_reuse_wait_desc
FROM sys.databases;

결과

log_reuse_wait_desc = ACTIVE_TRANSACTION

SQL Server는 여전히 활성 트랜잭션 때문에 로그를 재사용할 수 없다고 판단하고 있었다.


2차 확인 - CDC 상태 점검

해당 데이터베이스는 AWS DMS CDC(Change Data Capture)를 이용하여 타 시스템으로 데이터를 동기화하고 있었다.

먼저 CDC 관련 Job 상태를 확인하였다.

EXEC sys.sp_cdc_help_jobs;

확인 결과

  • Capture Job 정상
  • Cleanup Job 정상

으로 확인되었다.

추가로 CDC Log Scan 상태도 점검하였다.

SELECT *
FROM sys.dm_cdc_log_scan_sessions;

결과는 모두 정상 종료(Done) 상태였으며 에러도 발견되지 않았다.

즉, 분석 시점 기준으로는 CDC 자체가 중단되거나 오류 상태는 아니었다.


3차 확인 - 이상 세션 발견

활성 세션을 조사하던 중 특이한 세션을 발견하였다.

SELECT
    session_id,
    program_name,
    status,
    open_transaction_count
FROM sys.dm_exec_sessions;

확인 결과 일부 세션에서 다음과 같은 상태가 확인되었다.

program_name = replctl
status = sleeping
open_transaction_count = 1

특히 해당 세션은 장시간 Sleeping 상태였음에도 트랜잭션을 보유하고 있었다.

일반적인 사용자 세션이라기보다 Replication 또는 CDC 관련 내부 세션으로 판단되었다.


결정적 단서

문제가 되는 세션의 마지막 실행 SQL을 확인하였다.

DBCC INPUTBUFFER(<session_id>);

확인 결과 다음과 같은 SQL이 조회되었다.

BEGIN TRANSACTION

UPDATE dbo.awsdms_truncation_safeguard
SET ...

여기서 AWS DMS가 사용하는 내부 테이블인 awsdms_truncation_safeguard가 확인되었다.


awsdms_truncation_safeguard란?

AWS DMS는 CDC 작업 수행 시 트랜잭션 로그가 조기에 삭제되지 않도록 내부적으로 관리 테이블을 사용한다.

그중 하나가 awsdms_truncation_safeguard 이다.

DMS는 이 테이블을 통해 CDC 작업에 필요한 로그 범위를 보호하며, 정상적으로 종료될 경우 관련 트랜잭션도 함께 정리된다.


추정되는 원인

조사 과정에서 확인된 내용은 다음과 같다.

  • AWS DMS 관련 세션 존재
  • 해당 세션이 Sleeping 상태에서 트랜잭션 보유
  • awsdms_truncation_safeguard 관련 트랜잭션 확인
  • SQL Server 상태는 ACTIVE_TRANSACTION
  • LDF 사용률 99% 이상

따라서 다음과 같은 시나리오를 의심할 수 있었다.

AWS DMS CDC 작업 수행
        ↓
트랜잭션 생성
        ↓
DMS 작업 중단 또는 비정상 종료
        ↓
트랜잭션 미정리
        ↓
ACTIVE_TRANSACTION 상태 유지
        ↓
로그 절단(Log Truncation) 불가
        ↓
LDF 파일 지속 증가

다만 DMS 작업이 실제로 어떤 시점에 중단되었는지, 그리고 해당 트랜잭션이 직접적으로 로그 증가를 유발했는지에 대해서는 추가 검증이 필요하다.

따라서 위 내용은 분석 과정에서 도출된 정황이며, 확정된 원인으로 단정할 수는 없다.


분석 과정에서 얻은 교훈

이번 사례를 통해 확인한 점은 다음과 같다.

 

1. ACTIVE_TRANSACTION은 반드시 사용자 트랜잭션만 의미하지 않는다

SQL Server 내부 세션이나 CDC, Replication 관련 세션도 로그 재사용을 막을 수 있다.

 

2. CDC Job이 정상이라고 해서 문제가 없는 것은 아니다

Capture Job과 Cleanup Job이 모두 정상 상태였음에도 내부 세션에 의해 로그 관리 문제가 발생할 수 있다.

 

3. 로그 파일 증가 시 원인부터 확인해야 한다

LDF 파일이 증가했다고 해서 무조건 Shrink를 수행하기보다는 다음 항목을 먼저 확인하는 것이 중요하다.

  • log_reuse_wait_desc
  • CDC 상태
  • Replication 상태
  • 활성 세션
  • 열린 트랜잭션
  • DMS 관련 세션

원인을 제거하지 않은 상태에서 로그 파일만 축소할 경우 동일한 문제가 반복될 수 있다.


마무리

이번 사례는 AWS DMS CDC 운영 환경에서 발생할 수 있는 로그 관리 이슈를 분석한 사례이다.

분석 결과 AWS DMS 관련 세션이 비정상적으로 트랜잭션을 유지하고 있었던 정황이 확인되었으며, 이것이 SQL Server의 ACTIVE_TRANSACTION 상태와 로그 증가 현상에 영향을 주었을 가능성을 확인할 수 있었다.

실제 운영 환경에서는 DMS 상태뿐 아니라 SQL Server 내부의 CDC 및 Replication 관련 세션까지 함께 모니터링하는 것이 중요하다.

 

 

데이터베이스 개념적 설계를 처음 시작할 때 대부분의 개발자는 “엔티티부터 찾아야지”라고 생각한다.

하지만 실제로 설계를 진행해보면 엔티티보다 더 자주 막히는 지점이 있다.

 

바로 관계(Relationship), 그리고 그 관계를 어떻게 표현해야 하는지에 대한 문제다.

이때 반드시 이해하고 넘어가야 할 개념이 관계차수(Relationship Cardinality) 이다.

관계차수를 이해하지 못한 상태에서 개념적 설계를 시작하면 ERD는 그럴듯해 보일 수 있지만,

서비스 흐름을 제대로 담지 못하는 구조가 되기 쉽다.

 


관계차수란 무엇인가?

 

관계차수는 다음 질문에 대한 답이다.

“한 엔터티의 한 행은 다른 엔터티와 몇 개의 관계를 가질 수 있는가?

 

여기서 중요한 포인트는 두 가지다.

  1. 관계가 반드시 필요한가, 없어도 되는가?
  2. 관계가 하나만 가능한가, 여러 개가 가능한가?

 

이 두 질문의 조합이 바로 관계차수다.

즉, 관계차수는 단순히 “1:N” 같은 숫자 표현이 아니라 업무 규칙을 데이터 구조로 표현한 결과라고 볼 수 있다.

 


 

관계차수가 왜 중요한가?

 

개념적 설계의 목적은 테이블을 예쁘게 나누는 것이 아니다.

그 목적은 다음과 같다.

  • 서비스가 어떻게 흘러가는지
  • 데이터가 언제 생성되고, 변경되고, 사라지는지
  • 어떤 관계는 필수이고, 어떤 관계는 선택인지

이 흐름을 데이터 구조로 정확히 옮기는 것이다.

 

관계차수를 잘못 이해하면 다음 문제가 발생한다.

  • 반드시 필요한 관계를 선택 관계로 만들어 버리거나
  • 하나만 존재해야 할 관계를 여러 개 허용해 버리거나
  • 중간 테이블이 필요한 구조를 단순 연결로 처리하거나
  • 예외 상황을 감당하지 못하는 모델이 된다

 

즉, 관계차수는 설계의 방향을 결정하는 기준선이다.

 


 

Crow’s Foot 표기법이란 무엇인가?

 

Crow’s Foot(까마귀발) 표기법은

관계차수를 시각적으로 표현하기 위한 ERD 표기 방식이다.

 

이 표기법의 핵심은 단순하다.

 

참여 여부(필수 / 선택)다중성(1 / N)한 번에 표현한다.

 

Crow’s Foot 표기법은 기호를 외우기보다는 읽는 기준을 이해하는 것이 훨씬 중요하다.

 


 

Crow’s Foot 관계선, 어떻게 읽어야 하는가?

 

Crow’s Foot 관계선은 항상 엔터티 기준으로 읽는다.

 

즉, 다음 문장으로 해석한다.

“이 엔터티의 한 행은 상대 엔터티와 몇 개의 관계를 가질 수 있는가?”

관계선에는 두 가지 정보가 담겨 있다.

 

1.  참여 여부 (Participation)

  • ○ : 관계가 없어도 된다 (선택)
  • | : 관계가 반드시 있어야 한다 (필수)

2. 다중성 (Multiplicity)

  • | : 최대 1개
  • < : 여러 개 가능

이 두 기호의 조합으로 모든 관계차수가 표현된다.

 


 

아래는 이전 글의 흐름을 유지하면서

「Crow’s Foot로 표현되는 네 가지 관계차수」 부분만을 더 깊이·더 실무적으로 확장한 설명입니다.

블로그 본문에 그대로 끼워 넣어도 자연스럽도록 문단형으로 작성했습니다.

 


 

Crow’s Foot로 표현되는 네 가지 관계차수

Crow’s Foot 표기법에서 관계차수는 단순히 “1:N”을 표현하기 위한 기호가 아니다.

 

이 네 가지 관계차수는 서비스의 업무 규칙, 데이터의 생명주기, 예외 상황까지 포함한 설계 의사결정의 결과를 나타낸다.

따라서 각각을 정확히 이해하지 못하면 개념적 설계는 겉모양만 남고, 실제 흐름은 반영하지 못하게 된다.

 


 

1) 선택적 다수 (0 : N) — 가장 유연하지만 가장 많이 쓰이는 관계

 

의미 요약

  • 관계가 없어도 된다
  • 관계가 생기면 여러 개까지 가능
관계차수 Crow’s Foot 표현 ERD Cloud 표현

0 : N

○<

 

 

업무 규칙 관점에서의 의미

이 관계는 “아직 발생하지 않았을 수도 있는 사건”이나 “선택적으로 발생하는 활동”을 표현할 때 사용된다.

 

즉, 이 엔터티는 혼자 존재할 수 있으며, 상대 엔터티는 필요할 때만 연결된다.

 

개념적 설계에서의 중요 포인트

  • 데이터가 반드시 생성되는 흐름인지,
  • 아니면 사용자의 선택이나 특정 조건에 따라 생성되는지를 구분해야 한다.
  • 이 관계를 필수로 잘못 잡으면
  • “존재하지 않아도 되는 데이터”를 억지로 만들어야 하는 구조가 된다.

 

대표적인 사고 질문

  • “이 데이터는 없어도 시스템이 정상 동작하는가?”
  • “사용자가 아무 행동도 하지 않아도 이 엔터티는 존재하는가?”

 


 

2) 선택적 단일 (0 : 1) — 옵션성 관계를 표현하는 핵심 차수

 

의미 요약

  • 관계가 없을 수도 있다
  • 관계가 있더라도 최대 하나만 가능
관계차수 Crow’s Foot 표현 ERD Cloud 표현

0 : 1

○|

 

 

업무 규칙 관점에서의 의미

이 관계는 ‘있을 수도 있는 보조 정보’를 표현할 때 사용된다.

핵심은 “이 관계는 본질적인 구성 요소는 아니다”라는 점이다.

 

개념적 설계에서의 중요 포인트

  • 이 관계를 1:1로 잡아버리면 모든 엔터티가 반드시 상대 데이터를 가져야 하는 구조가 된다.
  • 반대로 0:N으로 잡으면 “여러 개가 존재하면 안 되는 정보”가 중복 저장될 수 있다.

 

대표적인 사고 질문

  • “이 정보는 없어도 본 엔터티가 성립하는가?”
  • “이 정보가 여러 개 존재하면 의미가 깨지지 않는가?”

 


 

3) 필수 다수 (1 : N) — 구조적 완전성을 강제하는 관계

 

의미 요약

  • 관계가 반드시 하나 이상 필요
  • 여러 개가 존재할 수 있음
관계차수 Crow’s Foot 표현 ERD Cloud 표현

1 : N 

|<

 

 

업무 규칙 관점에서의 의미

이 관계는 ‘구성 요소 없이는 성립할 수 없는 상위 개념’을 표현한다.

 

즉, 이 엔터티는 상대 엔터티가 최소 하나 이상 존재해야만 의미를 가진다.

 

개념적 설계에서의 중요 포인트

  • 이 관계를 선택 관계로 잘못 잡으면 “내용 없는 껍데기 데이터”가 만들어질 수 있다.
  • 반대로 반드시 필요한 구성 요소를 명확히 강제함으로써 데이터 무결성을 구조적으로 확보할 수 있다.

 

대표적인 사고 질문

  • “이 엔터티는 혼자 존재할 수 있는가?”
  • “이 데이터가 없다면 상위 개념이 의미를 가지는가?”

 


 

4) 필수 단일 (1 : 1) — 가장 강력한 제약을 가지는 관계

 

의미 요약

  • 관계가 반드시 존재해야 함
  • 정확히 하나만 존재 가능
관계차수 Crow’s Foot 표현 ERD Cloud 표현

1 : 1

||

 

 

업무 규칙 관점에서의 의미

이 관계는 두 엔터티가 논리적으로 분리되어 있을 뿐, 생명주기가 거의 동일할 때 사용된다.

즉, “둘 중 하나만 존재하는 상황은 허용되지 않는다”는 의미다.

 

개념적 설계에서의 중요 포인트

  • 이 관계는 생각보다 드물다.
  • 무분별하게 사용하면 구조를 지나치게 경직시킨다.
  • 보안, 책임 분리, 확장성 같은 명확한 이유가 있을 때만 사용해야 한다.

 

대표적인 사고 질문

  • “이 두 엔터티는 생성·삭제 시점이 항상 같은가?”
  • “하나만 존재하는 상태가 허용되는가?”

 


 

네 가지 관계차수의 핵심 차이 요약 (개념적 관점)

관계차수설계 관점 핵심

0 : N 선택적 활동, 발생하지 않을 수도 있는 흐름
0 : 1 옵션성 정보, 있어도 되고 없어도 되는 관계
1 : N 필수 구성 요소, 구조적 완전성 강제
1 : 1 생명주기 동일, 강한 결합 관계

 

 

그렇다면 ERD Cloud의 아래 표현들은 어떤 의미일까?

“Optional, one + many”와 “Optional, many”는 의미상 동일하며 모두 0 : N이다.
차이는 개념이 아니라 ‘표현의 정확성’에 있다.
설계에서는 항상 결과인 관계차수(0 : N)로 정리해야 한다.”
Optional one + many 없거나 한개 또는 여러개
null many 여러개

null one 한개

 

 


 

 

점선과 실선은 무엇을 의미하는가?

 

Crow’s Foot 관계선에는 점선실선이 존재한다.

이 차이는 관계차수와는 다른 질문에 대한 답이다.

“이 관계가 엔터티의 ‘식별’까지 의존하는가?”

 


 

실선 관계 (식별 관계, Identifying Relationship)

  • 자식 엔터티는 부모 엔터티 없이는 존재할 수 없음
  • 부모의 PK가 자식의 PK 일부가 됨
  • 자식의 정체성이 부모에 의존

 

즉, “이 엔터티는 저 엔터티 없이는 개념적으로도 존재할 수 없다.”

 

이 관계는 매우 강한 결합을 의미한다.

 


 

점선 관계 (비식별 관계, Non-Identifying Relationship)

  • 자식 엔터티는 독립적인 PK를 가짐
  • 부모의 PK는 FK일 뿐, 식별자는 아님
  • 관계는 논리적 연결일 뿐 존재 자체를 결정하지 않음

 

즉, “이 엔터티는 저 엔터티와 연결될 수는 있지만, 없어도 존재 자체는 성립한다.”

 


 

관계차수와 점선/실선의 관계

아주 중요한 점이 있다.

점선 = Optional, 실선 = Mandatory       ❌ 아니다.

 

이 둘은 완전히 다른 차원의 개념이다.

 

구분의미

관계차수 몇 개까지 가능한가
Optional / Mandatory 관계 참여 여부
점선 / 실선 식별 의존성 여부

 

즉, Mandatory + 점선, Mandatory + 실선 모두 가능하다.

 


 

개념적 설계에서의 판단 순서

개념적 설계를 할 때는 반드시 다음 순서로 사고해야 한다.

  1. 이 관계는 있어도 되는가, 반드시 필요한가?
  2. 관계가 있다면 하나인가, 여러 개인가?
  3. 이 엔터티는 상대 엔터티 없이는 존재할 수 있는가?
  4. 식별자가 상대 엔터티에 의존하는가?

 

이 질문들의 답이 관계차수와 점선/실선을 결정한다.

 


 

관계차수를 정확히 이해해야 하는 이유

 

관계차수는 단순한 표기 규칙이 아니라 서비스 규칙을 데이터 구조로 고정시키는 장치다.

  • 잘못된 관계차수 → 잘못된 데이터 구조
  • 잘못된 데이터 구조 → 예외 처리 증가
  • 예외 처리 증가 → 설계 붕괴

 

그래서 개념적 설계에서는 엔티티를 찾는 것만큼이나 관계차수를 정확히 정의하는 작업이 중요하다.

 


 

관계차수는 서비스 흐름에서 결정된다

 

관계차수는 책에서 외워서 적용하는 개념이 아니다.

반드시 서비스 흐름을 따라가며 결정해야 한다.

 

다음 질문을 던져보면 자연스럽게 답이 나온다.

  • 이 데이터는 생성되지 않아도 되는가?
  • 반드시 존재해야 하는 시점이 있는가?
  • 동시에 여러 개가 존재할 수 있는가?
  • 과거 데이터는 보존되어야 하는가?

 

이 질문의 답이 곧 관계차수가 된다.

 

즉, 관계차수는 업무 규칙을 데이터 구조로 번역한 결과물이다.

 


 

관계차수를 이해하면 개념적 설계가 쉬워진다

 

관계차수를 이해하고 나면 개념적 설계의 다음 단계들이 훨씬 명확해진다.

  • 엔터티를 찾을 때
    → 독립된 대상인지, 관계의 결과인지 판단할 수 있고
  • 속성을 정의할 때
    → 어떤 엔터티에 속해야 자연스러운지 판단할 수 있으며
  • 관계를 정의할 때
    → 중간 엔터티가 필요한 구조인지 즉시 파악할 수 있다

 

결국 관계차수는 개념적 설계 전체를 관통하는 핵심 개념이다.

 


 

왜 이 개념들이 먼저 필요한가

 

개념적 설계는 ERD를 그리는 기술이 아니라 서비스를 구조적으로 이해하는 사고 과정이다.

  • 관계차수는 업무 규칙을 고정하고 Crow’s Foot는 이를 시각화하며 점선과 실선은 엔터티의 존재와 식별 의존성을 명확히 한다

 

이 개념들을 이해한 상태에서 설계를 시작하면 엔티티를 찾고, 속성을 정의하고, 관계를 나누는 과정이 훨씬 논리적이고 안정적으로 이어진다.

 

 

 

— “값 하나 추가하는데, 정말 운영에 영향이 있을까?”

 

이전 글에서 PostgreSQL ENUM을 운영 DB에서 조심스럽게 사용하는 이유로

변경 시 리스크를 언급했다.

 

그중 가장 많이 나오는 반론은 이것이다.

 

“요즘 PostgreSQL에서는 ENUM 값 추가 시 잠금 거의 안 걸리지 않나?”

 

그래서 실제로 확인해 보기로 했다.

ENUM 변경 시 PostgreSQL은 정말 안전한가?

이번 글은 그에 대한 간단하지만 현실적인 테스트 결과다.

 


테스트 목적

  • ENUM 타입에 새로운 값을 추가할 때
  • 실제 테이블 사용 중인 상황에서
  • 읽기/쓰기 쿼리에 영향이 있는지
  • 잠금(lock)이 발생하는지

를 확인한다.

 


테스트 환경

  • PostgreSQL 13.x
  • ENUM 타입을 사용하는 테이블 존재
  • 동시에 트랜잭션이 수행 중인 상황 가정

 


 

테스트용 ENUM 및 테이블 생성

CREATE TYPE order_status AS ENUM ('pending', 'paid', 'done');

CREATE TABLE orders (
    id bigint PRIMARY KEY,
    status order_status
);

INSERT INTO orders VALUES (1, 'pending');

 


 

시나리오 1: ENUM 변경 중 SELECT는 가능한가?

 

세션 1: ENUM 값 추가

ALTER TYPE order_status ADD VALUE 'cancelled';

 

세션 2: 동시에 SELECT 수행

SELECT * FROM orders;

 

결과

  • SELECT는 정상 수행
  • 읽기 쿼리는 블로킹되지 않음

 

-> 읽기 관점에서는 큰 문제 없어 보인다

 


 

시나리오 2: ENUM 변경 중 INSERT는 가능한가?

 

세션 2: INSERT 수행

INSERT INTO orders VALUES (2, 'paid');

 

결과

  • INSERT가 대기 상태에 들어감
  • ENUM 변경이 끝난 후에야 실행됨

-> 쓰기 작업은 ENUM 변경 완료까지 블로킹됨

 

이 부분이 운영 관점에서 가장 중요한 포인트다.

 


 

시나리오 3: 기존 ENUM 값으로 INSERT 시에도 잠기는가?

INSERT INTO orders VALUES (3, 'pending');

 

결과

  • 마찬가지로 대기
  • 새로 추가한 값과 무관하게 ENUM 타입 자체가 잠김

 

-> “기존 값이니까 괜찮겠지”는 통하지 않는다

 


 

왜 이런 일이 발생하는가?

 

PostgreSQL에서 ENUM 값 추가는 내부적으로 다음을 수행한다.

  • ENUM 타입 메타데이터 변경
  • 해당 타입을 사용하는 객체 보호
  • 타입 수준의 잠금 획득

 

이 과정에서 "SELECT는 허용" "INSERT / UPDATE는 대기" 하게 된다.

 

공식 문서에서도 ENUM 변경은 “동시성에 영향을 줄 수 있다”고 간접적으로 언급되어 있다

(정확한 잠금 범위는 버전별로 다를 수 있음 — 확실하지 않음).

 


 

“그럼 잠깐이면 괜찮은 거 아닌가?”라는 질문에 대해

이론적으로는 맞다.

하지만 운영 환경에서는

  • 배포 중 예상보다 오래 걸리는 DDL
  • 다른 마이그레이션과 겹침
  • 커넥션 대기 증가
  • API 타임아웃 발생

같은 연쇄적인 문제로 이어질 수 있다.

 


 

코드 테이블 방식과 비교하면?

항목ENUM코드 테이블

값 추가 DDL DML
잠금 리스크 있음 거의 없음
롤백 어려움 가능
배포 영향 작음

 

이 차이 때문에 나는 운영 DB의 상태값에는 ENUM을 피하게 되었다.

 


테스트를 통해 얻은 결론

  • ENUM 값 추가 시 쓰기 작업은 실제로 블로킹된다.
  • 영향 범위는 작을 수 있지만 운영 환경에서는 “작은 잠금”도 장애가 될 수 있다.

 

따라서

ENUM 변경은 “안전하다”기보다는 “조심스럽게 다뤄야 하는 작업”에 가깝다

 


 

그럼에도 ENUM을 쓸 수 있는 경우

 

다시 한 번 정리하면

  • 값 변경 가능성이 거의 없고
  • 운영 중 쓰기 트래픽이 적으며
  • 배포 윈도우가 명확한 경우

 

라면 ENUM은 여전히 좋은 선택이다.

 

하지만 비즈니스 상태값이라면

나는 이 테스트 이후에도 생각이 바뀌지 않았다.

 


 

마무리하며

이번 테스트는 PostgreSQL ENUM의 단점을 과장하기 위한 것이 아니라 운영 리스크를 수치가 아닌 행동으로 확인하기 위한 것이었다.

 

ENUM은 잘 만든 기능이다.

다만, 운영 환경에서는 그 “변경 비용”을 반드시 고려해야 한다.

 


질문

  • 실제 운영 DB에서 ENUM 변경을 해본 경험이 있는가?
  • 잠금이나 장애로 이어진 사례가 있었는가?
  • PostgreSQL 버전에 따라 다른 결과를 본 적은 있는가?

 

경험 공유는 언제나 환영이다.

이 주제는 문서보다 현장 경험이 더 중요하다.

 

이전에 PostgreSQL ENUM 타입의 개념과 장단점에 대해 정리한 글을 작성한 적이 있다.

ENUM이 무엇인지, 어떤 경우에 유용한지, 그리고 성능과 저장 공간 측면에서 어떤 이점이 있는지를 중심으로 다뤘다.

 

요약하면 이전 글의 결론은 이것이었다.

 

ENUM은 값의 범위가 잘 정의되어 있고, 변경 가능성이 낮은 경우에만 사용하는 것이 좋다.

 

이 말 자체는 틀리지 않다. 하지만 그 글을 다시 읽어보면서 한 가지 아쉬운 점이 남았다.

 

“그래서 실제 운영 DB에서는 쓰라는 건지, 말라는 건지?”

 

이번 글은 그 질문에 대한 나의 명확한 답이다.

 


 

결론부터 말하면 나는 운영 DB에서는 ENUM을 거의 쓰지 않는다

 

이전 글에서는 장단점을 비교하며 비교적 중립적인 톤을 유지했다.

하지만 운영 환경에서 PostgreSQL을 사용해 본 이후의 개인적인 결론은 분명하다.

운영 DB, 특히 비즈니스 상태값에는 ENUM을 거의 사용하지 않는다.

 

이 판단은 ENUM의 기능이 부족해서가 아니라, 운영과 변경이라는 현실적인 문제 때문이다.

 


 

ENUM의 진짜 문제는 “유연성 부족”이라는 한 단어로 설명되지 않는다

 

ENUM의 단점으로 가장 많이 언급되는 것은 다음이다.

  • 새로운 값을 추가하려면 타입을 변경해야 한다

하지만 이 설명은 너무 추상적이다. 운영 관점에서의 문제는 훨씬 구체적이다.

 

운영 환경에서 마주치는 현실

  • ENUM 변경이 포함된 배포
  • DDL 수행 중 잠금 가능성
  • 배포 실패 시 롤백 불가
  • ENUM 값 추가 순서에 따른 예상치 못한 영향
ALTER TYPE order_status ADD VALUE 'refunded';

이 한 줄은 단순한 데이터 변경이 아니라 스키마 변경이며 되돌리기 어려운 작업이다.

 

값 하나 추가하는데 왜 이렇게 많은 것을 고려해야 할까?

 


 

이전 글에서 말하지 못했던 부분...  “변경은 반드시 온다”

 

이전 글에서는 “변경 가능성이 낮은 경우”라는 전제를 두었다.

하지만 운영 환경에서 느낀 점은 이렇다.

 

비즈니스 상태값은 거의 예외 없이 변경된다.
  • 처음엔 pending, done만 있던 상태 이후 cancelled 나중엔 refunded, partially_refunded 그리고 또 새로운 요구사항//

이건 설계가 잘못돼서가 아니라 비즈니스가 살아있기 때문이다.

 


 

그래서 선택한 대안.. 코드 테이블

 

이 지점에서 나는 ENUM 대신 다른 방식을 선택했다.

 

  • 상태값을 관리하는 코드 테이블

 

이 방식의 장점은 명확하다.

  • 값 추가가 DML 수준
  • 배포와 데이터 변경 분리 가능
  • 롤백 가능
  • 운영 리스크 감소

 

성능 차이에 대한 질문이 나올 수 있다.

하지만 실제 운영에서 ENUM과 코드 테이블의 성능 차이를 체감한 적은 없다

(확실하지 않음: 초대규모 트래픽 환경에서는 다를 수 있다).

 


 

그렇다면 ENUM은 언제 쓰는가?

 

이전 글에서 말한 것처럼, ENUM이 어울리는 경우도 분명히 있다.

  • 값의 의미가 절대 바뀌지 않는 경우
  • 내부 로직용 고정 값
  • 시스템 레벨의 분류 값

 

예를 들면

  • 요일
  • 내부 플래그
  • 프로토콜 수준의 고정 상태

 

하지만 비즈니스 상태값이라면? 나는 여전히 ENUM을 선택하지 않는다.

 


 

이전 글과 이번 글의 결론을 합치면

  • ENUM은 기술적으로 깔끔하고 성능도 좋다
  • 하지만 운영과 변경을 고려하면 부담이 크다
  • 그래서 나는 운영 DB에서는 거의 사용하지 않는다

 

이전 글이 “ENUM은 이런 특징이 있다”였다면, 이번 글은 “그래서 나는 이렇게 사용한다”에 가깝다.

 


 

데이터베이스 설계의 첫 번째 단계는 개념적 설계이다.

이 단계는 기술보다 업무 이해가 중요한 단계이며, 데이터베이스 구조의 방향을 결정하는 가장 본질적인 과정이다.

 

개념적 설계를 제대로 수행하려면 다음 세 가지 질문에 자연스럽게 답할 수 있어야 한다.

  1. 무엇을 다루는가? → 엔티티 찾기
  2. 그것은 어떤 특징을 가지는가? → 속성 도출
  3. 서로 어떻게 연결되는가? → 관계 정의

 

이 세 단계만 명확히 정리해도 데이터베이스 설계의 절반 이상은 성공한 것이나 마찬가지다.

 


 

엔티티 찾기 — “우리는 어떤 대상을 관리하는가?”

 

엔티티(Entity)는 서비스에서 독립적으로 존재하며 저장해야 하는 대상을 의미한다.

엔티티를 찾기 위해서는 SQL이나 테이블 구조를 먼저 떠올리기보다 업무 흐름을 면밀히 관찰하는 것이 더 효과적이다.

 

엔티티를 찾을 때 자주 사용하는 기준

  • 서비스에서 중요한 명사 는 무엇인가?
  • 업무 흐름에서 역할 을 가진 존재는 무엇인가?
  • 시간이 지나도 기록으로 남아야 하는 정보는 무엇인가?
  • 다른 데이터와 관계를 맺으며 의미를 갖는 대상은 무엇인가?

예: 업체 기반 상품 서비스에서의 엔티티 도출

서비스 흐름을 기준으로 엔티티를 도출하면 다음과 같이 정리할 수 있다.

  • 업체 : 상품을 공급하는 주체이며, 고유한 정보를 가진다.
  • 상품 : 업체가 제공하며 서비스에 등록되는 독립된 아이템이다.
  • 회원 : 서비스를 이용하는 고객
  • 주문 : 사용자와 상품 사이에서 발생하는 비즈니스 이벤트.
  • 주문상품 : 한 주문에 포함된 개별 상품 정보.
  • 결제 : 주문과 연결된 금전적 트랜잭션.
  • 배송 : 주문 이후에 발생하는 별도 흐름.

 

이처럼 엔티티는 도메인을 깊이 이해할수록 더 정확하게 정의된다.

 


 

속성 도출 — “각 엔티티가 무엇을 가지고 있어야 하는가?”

 

엔티티를 찾았다면, 이제 그 엔티티가 어떤 정보를 포함해야 하는지 생각해보아야 한다.

이는 테이블 컬럼을 바로 정하는 것이 아니라, 실제 업무에서 해당 대상이 어떤 정보로 구성되는지를 파악하는 과정이다.

 

속성을 도출할 때 고려해야 하는 요소

  • 이 대상은 반드시 가져야 하는 고유 정보가 있는가?
  • 시간이 지나며 변하는 정보는 무엇인가?
  • 다른 엔티티와 연결되기 위해 필요한 정보는 무엇인가?
  • 이벤트(주문, 결제 등)가 발생할 때 어떤 정보가 기록되어야 하는가?

예: 업체 기반 서비스에서의 속성 도출

업체

  • 업체명
  • 사업자 정보
  • 연락처
  • 정산 관련 정보

 

상품

  • 상품명
  • 업체 ID(업체와의 연결)
  • 가격
  • 등록일
  • 현재 판매 상태

 

주문

  • 주문번호
  • 회원 ID
  • 주문 상태
  • 주문 시점
  • 총액

 

주문상품

  • 주문 ID
  • 상품 ID
  • 주문 당시 상품 가격
  • 수량

 

속성을 도출할 때 특히 중요한 것은 어떤 값은 시간이 지나도 변하면 안 된다”는 점이다.

 

예: 주문 당시 상품 가격은 원래 상품 가격과 달라야 한다

→ 이것은 개념적 설계 단계에서 반드시 고려해야 하는 중요한 속성 기준이다.

 


 

관계 정의 — “엔티티들이 어떻게 연결되는가?”

 

관계를 정의하는 목적은 단순히 선을 그리는 것이 아니라, 업무 흐름이 데이터로 어떻게 표현되는지 규칙을 정하는 것이다.

 

 

관계 정의 시 고려할 질문

  • A가 B를 포함하는가? (1:N)
  • A와 B가 서로 여러 개를 가질 수 있는가? (M:N → 중간 엔티티 필요)
  • 사건(Event)은 어떤 엔티티들 사이에서 발생하는가?
  • 이 관계가 시간에 따라 변하는가?

예: 업체 기반 서비스의 관계 정의

  • 업체 1:N 상품 → 한 업체는 여러 상품을 가짐
  • 회원 1:N 주문 → 한 회원은 여러 차례 주문 가능
  • 주문 1:N 주문상품 → 주문 하나에 여러 개의 상품이 포함
  • 상품 1:N 주문상품 → 하나의 상품은 여러 주문에서 등장 가능
  • 주문 1:1 또는 1:N 결제 → 서비스 정책에 따라 달라지는 부분

 

이 관계 정의 과정에서 드러나는 것이 바로 “흐름의 본질”이다.

관계를 그림으로 표현하는 과정에서 업무 흐름의 예외 사항이나 놓친 정보들이 자연스럽게 발견된다.

 


 

개념 설계의 핵심은 “서비스를 깊이 이해하는 것”

 

개념적 설계는 SQL을 잘하는 것보다 서비스가 어떻게 동작하는지, 어떤 예외가 있고 어떤 데이터가 중요한지 이해하는 능력에 더 의존한다.

 

다음과 같은 질문을 던지면서 설계를 깊게 할수록 더 탄탄한 데이터 모델이 나온다.

  • 이 엔티티는 정말 독립적으로 존재할 수 있는가?
  • 흐름에서 발생하는 모든 상태를 반영했는가?
  • 예외 상황을 감당할 수 있는 구조인가?
  • 시간이 지나도 기존 데이터가 망가지지 않는가?
  • 데이터가 변할 때 어떤 관계가 영향을 받는가?

 

이 과정을 충실히 거치면 논리적 설계와 물리적 설계 단계가 훨씬 명확하고 쉬워진다.

 


 

데이터베이스 설계의 3단계 로드맵 — 구조를 만드는 방법을 이해하기

 

앞선 글에서 데이터베이스 설계의 중요성을 다뤘다면, 이번 글에서는 “그럼 실제로 어떻게 설계해야 하는가?”라는 질문에 답해보려고 한다.

 

설계는 단순히 테이블 몇 개를 만드는 작업이 아니라,

서비스가 어떻게 흘러가고 어떤 일을 처리해야 하는지를 구조적으로 정리하는 과정이다.

그래서 올바른 설계를 위해서는 기술 지식뿐 아니라 업무 흐름(Business Flow)을 깊게 이해하는 능력이 함께 요구된다.

 

설계는 보통 개념적 설계 → 논리적 설계 → 물리적 설계라는 3단계를 따른다.

이 단계들은 서로 분리된 과정이 아니라, 서비스를 더 명확하게 이해하고 구조화하기 위한 사고의 흐름에 가깝다.

 


 

1단계 : 개념적 설계 — “무엇을 다루고 어떤 일이 일어나는가?”

 

개념적 설계는 서비스의 실제 업무 흐름을 이해하는 것에서 시작된다.

기술적 결정보다 비즈니스가 어떤 방식으로 움직이는지를 파악하는 단계다.

 

이 단계에서 개발자가 집중해야 하는 질문은 다음과 같다.

  • 서비스에서 다루는 핵심 대상은 무엇인가?
  • 각 대상은 어떤 속성을 가지는가?
  • 대상들은 어떤 흐름에 따라 변화하는가?
  • 예외 상황은 어떤 것이 있으며, 어디에서 발생하는가?

 

여기서 도출되는 대상(엔티티)은 서비스마다 다르다.

예를 들어 나의 서비스 모델에서는 상품은 ‘도매업체 또는 공급업체 단위’로 관리되는 구조이므로, 다음과 같은 이해가 필요하다.

 

 

개념적 이해의 예 

  • 주문은 여러 상태를 가진다.상태가 명확하지 않으면 이후 테이블 설계도 흐트러진다.
    • 예: 주문 접수 → 결제 완료 → 출고 준비 → 배송 중 → 배송 완료
  • 상품은 ‘판매자 개인’이 아닌 ‘업체 단위’로 취급된다.특정 공급업체가 제공하고 관리하는 단일 상품 흐름을 따른다.
    • 즉, 하나의 상품은 여러 개인 판매자와 연결되는 구조가 아니라,
  • 업체는 여러 상품을 보유할 수 있다.“업체(1) → 상품(N)” 형태로 흐른다.
    • 따라서 관계는 개인 판매자 기반이 아닌
  • 상품 정보는 시간이 지나며 변할 수 있다.이때 기존 주문의 가격은 어떤 기준으로 유지되어야 하는가?
    • 예: 업체가 가격을 변경할 수 있다

이처럼 도메인을 올바르게 이해하고 표현하는 것

개념적 설계의 출발점이다.

 


 

서비스 흐름을 왜 이해해야 하는가?

 

데이터 설계는 표만 그리는 것이 아니다.

서비스가 어떤 순서로 움직이는지, 어떤 상황에서 예외가 발생하는지, 기능이 어떤 데이터를 생성·변경·조회하는지를 알아야만 테이블 구조가 제대로 만들어진다.

 

예를 들어

  • 주문이 생성된 직후 바로 결제가 완료되는가?
  • 결제가 실패하면 주문은 어떤 상태가 되어야 하는가?
  • 고객이 주소를 변경하면 기존 주문에 영향을 주는가?
  • 상품 가격이 변할 때 기존 주문은 어떻게 처리해야 하는가?

 

이런 흐름 기반 질문이 없다면 테이블 구조는 현실을 따라가지 못하고 조금만 복잡한 기능이 들어와도 틀어지기 시작한다.

서비스 흐름을 이해해야 다음을 판단할 수 있다.

  • 어떤 데이터가 반드시 분리되어야 하는지
  • 어떤 데이터는 이력으로 저장해야 하는지
  • 상태가 시간에 따라 어떻게 변화하는지
  • 예상 가능한 예외 상황이 어떤 것인지

 

즉, 개념 설계의 핵심은 도메인 이해도이며, 그 도메인 이해도는 ‘서비스가 어떻게 흘러가는지’를 파악하는 데서 시작된다.

 


 

2단계 : 논리적 설계 — “업무 개념을 데이터베이스 규칙에 맞게 정리하기”

 

개념적 설계로 큰 그림이 잡혔다면, 이제 그것을 관계형 데이터베이스의 구조에 맞게 다듬는 단계가 논리적 설계다.

 

이 단계에서 이루어지는 작업은 다음과 같다.

테이블 정의

개념 단계에서 찾은 대상들을 테이블로 정식화한다.

 

속성(컬럼) 설계

각 대상이 어떤 정보를 가져야 하는지 구체적으로 정의한다.

 

정규화

중복을 줄이고 데이터 무결성을 유지하기 위해 테이블을 적절히 분리한다.

 

 관계(PK, FK) 설계

개념 단계의 관계를 실제 데이터베이스 구조로 옮긴다.

 

논리 설계는 개발 언어나 DBMS와 무관하게 적용되는 설계 이론에 기반한 표준 구조화 단계이다.

 


 

왜 논리적 설계가 중요한가?

 

개념 설계는 비즈니스를 설명하는 언어라면, 논리 설계는 이를 데이터 세계의 규칙으로 변환하는 과정이다.

 

이 단계가 탄탄해야

  • 데이터 중복이 줄어들고
  • 이상 현상이 방지되며
  • 기능 변경에도 구조가 흔들리지 않으며
  • 성능 튜닝의 기반이 마련된다

즉, 논리 설계는 정확성과 안정성을 확보하는 핵심 단계다.

 


 

3단계 : 물리적 설계 — “실제 운영 환경에 맞게 최적화하기”

 

마지막 단계는 논리 모델을 실제 데이터베이스에 맞게 구체화하는 과정이다.

 

 컬럼 타입 결정

정수, 실수, 문자, 날짜 등 정확한 타입을 선택한다.

 

인덱스 전략

조회 빈도나 패턴에 따라 인덱스를 설계한다.

 

제약조건 설정

NOT NULL, UNIQUE, CHECK 등으로 데이터 유효성을 강제한다.

 

스토리지 엔진 및 성능 요소 고려

환경에 따라 선택지가 달라진다.

 

 

실제 CREATE TABLE 스크립트 구성

이제 논리적 설계가 현실이 되는 단계다.

 


 

결국, 설계 3단계는 “서비스를 깊이 이해하기 위한 사고의 단계”이다

 

많은 개발자들은 “설계 3단계”를 단순히 암기해야 하는 이론처럼 여긴다.

하지만 실제로 이 세 단계는 다음을 위한 사고의 순서이다.

 

 

1)  이 서비스는 무엇을 다루는가?

(개념)

 

2) 그것을 어떻게 구조화하면 안정적일까?

(논리)

 

3) 그 구조를 실제 환경에서 어떻게 구현할까?

(물리)

 

이 세 단계는 표면적으로 나뉘어 있는 것처럼 보이지만 결국 하나의 목적을 향한다.

 

서비스를 정확하게 이해하고, 그 흐름을 데이터 구조로 완전하게 표현하기 위한 과정이다.

 


 

“개념적 설계를 실제로 어떻게 시작할까?”

 

다음 글에서는 본격적으로 개념적 설계(Conceptual Modeling)를 시작하는 방법을 다룬다.

  • 엔티티를 찾는 기준
  • 속성을 정의하는 방식
  • 관계를 구분하는 방법
  • 서비스 흐름을 통해 설계를 강화하는 팁

 

설계는 결국 업무를 얼마나 정확히 이해했는가에서 출발한다.

이제 그 첫걸음을 함께 밟아보자.

 


 

데이터베이스 설계의 중요성 — 시스템의 뼈대를 세우는 과정

 

요즘 개발 환경을 보면 흥미로운 변화가 하나 있다.

프레임워크는 점점 더 발전하고, 서버 개발은 이전보다 훨씬 손쉬워졌지만,

그에 비해 데이터베이스 설계에 대한 관심이 줄어든 미들급 개발자들이 눈에 띄게 늘고 있다.

 

초급 개발자가 데이터 쿼리에 익숙하지 않은 것은 자연스러운 일이다.

하지만 미들급 이상의 개발자라면 데이터베이스의 구조적 중요성을 반드시 이해하고 있어야 한다.

서비스가 커지고 복잡해질수록, 데이터베이스는 단순 저장소가 아니라

비즈니스의 흐름과 시스템의 방향성을 결정하는 기반이 되기 때문이다.

 

특히 하나의 데이터베이스를 단기간만 운영하는 것과

5년 이상 꾸준히 확장·변경하며 유지해본 경험의 차이는 매우 크다.

빠른 오픈만을 목표로 만든 구조는 몇 달 동안은 잘 버티는 것처럼 보인다.

하지만 시간이 지나면 본래 설계의 빈틈이 그대로 기술 부채로 돌아와,

어느 순간 팀 전체가 “왜 이렇게까지 느려졌지?”, “왜 단순 기능인데 손댈 곳이 이렇게 많지?”와 같은 질문을 반복하게 된다.

 

반대로 장기적인 관점에서 데이터를 어떻게 보관하고,

어떤 변화가 생길 때 어디까지 영향을 미칠지 고민하며 설계한 데이터베이스는 시간이 지날수록 그 가치가 드러난다.

오래된 구조를 수십 번 개조하면서 얻은 경험은, 개발자의 시야를 한 단계 더 확장시키는 중요한 자산이 된다.

 

즉, 데이터베이스 설계는 단순한 기술 중 하나가 아니라

개발자의 경력 수준을 결정짓는 성숙도의 지표라고 해도 과언이 아니다.

 


 

데이터 무결성을 지키는 첫 번째 방어선

 

데이터베이스 설계의 핵심 목표 중 하나는 데이터의 일관성과 정확성을 보장하는 것이다.

잘못된 구조는 데이터가 서로 다른 위치에서 중복되거나 불필요하게 복제되도록 만들고, 그 결과 어느 데이터가 최신인지 구분하기 어려워진다.

 

정확한 설계는 다음을 보장한다.

  • 동일한 데이터가 여러 곳에 중복 저장되지 않도록 하고
  • 한 지점에서 변경하면 전체 시스템에서 일관되게 반영되며
  • 잘못된 갱신이나 누락을 막을 수 있는 구조를 제공한다

 

잘된 설계는 데이터 무결성 자체를 구조적으로 보호하는 장치이며, 이는 모든 기능의 신뢰성을 지탱하는 기반이 된다.

 


 

성능을 유지하는 가장 근본적인 요소

 

데이터베이스의 속도는 단순히 하드웨어나 인덱스 튜닝만으로 결정되지 않는다.

애초에 어떻게 테이블을 나누었는지, 어떤 관계로 모델링했는지, 데이터가 어떤 흐름을 따라 저장되도록 설계했는지가 성능을 좌우하는 근본 요소다.

 

좋지 않은 설계는 시간이 지날수록 다음과 같은 문제를 낳는다.

  • 테이블이 비대해져 조회 비용 증가
  • 관계가 뒤엉켜 필요 이상의 연산 발생
  • 인덱스가 제대로 활용되지 않는 구조적 한계

 

반면, 올바른 설계는 데이터가 아무리 쌓여도 성능 저하를 최소화하고,

조회·갱신·검색이 효율적으로 실행되도록 보장한다.

 

즉, 설계는 성능 최적화의 출발점이자 필수 조건이다.

 


 

변경과 확장에 강한 구조를 만든다

 

서비스가 성장하면 필연적으로 새로운 요구사항이 생긴다.

컬럼이 추가될 수도 있고, 새로운 기능이 들어오거나, 저장해야 할 정보가 확장될 수도 있다.

 

탄탄한 설계는 이런 변화를 자연스럽게 받아들일 수 있는 구조를 제공한다.

  • 특정 기능 추가가 전체 테이블 구조를 뒤흔들지 않고
  • 새로운 데이터 유형이 들어와도 기존 모델을 파괴하지 않으며
  • 기존 데이터와 새로운 데이터가 충돌하지 않는 형태로 구성된다

 

즉, 설계를 잘하면 작은 수정으로 큰 기능을 구현할 수 있고, 설계를 잘못하면 작은 요구에도 전체 시스템을 손봐야 하는 상황이 반복된다.

 

이 차이는 장기적으로 프로젝트 속도, 비용, 품질 모두에 큰 영향을 준다.

 


 

시스템 전체 품질을 좌우하는 중심축

 

데이터베이스는 애플리케이션 코드, API, 배치 작업, 분석 시스템 등 온갖 기능과 서비스의 중심에 있다.

 

데이터베이스 설계를 단단히 해두면

  • 애플리케이션 로직이 단순해지고
  • 중복 처리 코드가 사라지고
  • 예기치 않은 사이드 이펙트가 감소하며
  • 시스템 전체가 안정적으로 동작할 수 있다

 

반대로 설계를 소홀히 하면, 서버와 코드가 아무리 좋아도 데이터가 발목을 잡는 구조적 한계를 피할 수 없다.

 


 

설계는 비용이 아니라 예방이며 투자이다

 

좋은 설계를 위해 초기 시간을 투자하는 것은 단기적으로는 느려 보일 수 있다.

하지만 이 과정 덕분에 미래의 수많은 장애·수정 비용·성능 문제를 막을 수 있다.

 

정리하면 설계는

  • 문제를 미리 차단하는 예방책이며
  • 시스템의 수명을 늘리는 전략적 투자이며
  • 개발 속도를 장기적으로 높이는 도구

 

시간이 지날수록 이 가치는 더욱 크게 드러난다.

 


 

데이터베이스 설계는 

 

데이터베이스를 어떻게 설계하느냐는 서비스가 얼마나 성장할 수 있는지, 얼마나 안정적일 수 있는지,

그리고 개발팀이 얼마나 빠르게 움직일 수 있는지를 결정한다.

 

정확한 설계는 단순히 “잘 짜여진 테이블”을 의미하는 것이 아니다.

데이터를 중심으로 한 전체 시스템의 구조적 질서를 확립하는 일이다.

 

설계를 잘하면 서비스는 탄탄하고 유연해진다.

설계를 소홀히 하면 기술 부채는 쌓이고 시스템은 점점 느려지고 무거워진다.

 

결국 데이터베이스 설계는 단순한 문서 작업이 아니라 미래의 개발 속도와 시스템 안정성을 보장하는 전략적 선택이다.

 


 

최근 개발자와 논의하면서 “상품 수정 이력을 어떤 방식으로 관리해야 하는가”라는 주제가 있었다.

커머스에서 상품 데이터는 단순한 정보가 아니라 매출, 정산, CS, 분쟁 처리, 법적 책임까지 직결되는 핵심 데이터다.

따라서 “누가 언제 수정했는가”를 넘어서 그 당시의 상품 상태를 정확히 복원할 수 있는 이력 관리가 필수적이다.

 

이번 글은 그러한 논의를 기반으로, JSON 스냅샷 기반 이력 관리 방식과 테이블 기반 이력 관리(Envers 포함)의 장단점을 다시 정리하고, 커머스 환경에서 어떤 방향이 현실적인지 기술적 조언 관점에서 요약한 내용이다.


1. 기존 방식의 한계 — 왜 논의가 시작되었나

 

현재 시스템은 “수정자 / 수정 시점” 정도만 남기고 있어, 과거 특정 시점의 상품 데이터 전체를 재구성하는 것이 사실상 불가능하다.

 

문제는 상품 도메인이 매우 복잡하고 테이블도 20개 넘게 분리되어 있어, 임의 시점의 데이터를 조회하려면 복잡한 Join을 수행해야 하고 이는 성능적으로 취약하다.

이 한계를 해결하기 위해 JSON 스냅샷 방식이 제안되었다.

 


 

2. JSON 스냅샷 방식 — 매력적이지만 구조적 약점도 분명하다

장점

  • 한 번에 전체 상태를 저장하므로 조회가 매우 빠름
  • 구조 변경 시 DB 스키마 변경 없이 JSON 구조만 관리하면 됨
  • PostgreSQL JSON 기능으로 부분 검색도 가능
  • 조회가 압도적으로 많은 상품 도메인에서는 큰 이점

 

그러나 치명적 단점도 있다

  1. 무엇이 어떻게 변경됐는지 필드 단위로 알 수 없음
  2. → CS, 정산, 분쟁 대응, 운영 관점에서 매우 치명적
  3. 스키마 제어 불가 → 데이터 품질 저하 위험
  4. → 오타·누락·잘못된 타입이 그대로 저장됨
  5. 도메인 변경 시 과거 JSON이 호환되지 않음
  6. → 시간이 지나면 “당시 상태를 정확히 복원”하기 어려워짐
  7. 전체 스냅샷 저장으로 스토리지 비용 증가
  8. → 초기엔 적어 보여도 장기적으로는 커질 수 있음
  9. 정규화된 구조가 아니므로 분석·집계·통계가 비효율적

 

스냅샷 방식은 어디까지나 조회 최적화용 캐시 개념에 가깝다.

 


 

3. 테이블 기반 이력 — 정확하지만 구현 비용이 높다

Envers 등 ORM 기반 이력 시스템을 쓰더라도, 서브 도메인(가격, 옵션 등)이 수정되면 상품 테이블에는 이력이 남지 않는다.

즉, “상품 단위”로 이력을 남기기 위해서는 결국 별도 구현이 필요하다.

또한 20개 넘는 테이블에 선분 이력을 모두 남기는 방식은

구멍 없는 이력 유지가 어렵고, 복원 시 무거운 조회 성능 문제가 발생할 수 있다.

 


 

4. 현실적인 결론 — 하이브리드 접근이 가장 적절하다

논의의 결론은 단순하다.

JSON 스냅샷은 조회 최적화를 위해 사용

정확한 변경 이력은 테이블 기반으로 관리

▶ 두 방식을 조합한 하이브리드 구조가 가장 안정적

 

왜 하이브리드인가?

  • JSON 스냅샷 → 특정 시점을 빠르게 복원하는 용도
  • 테이블 기반 이력 → 법적 근거, 정산, CS, 운영 내역 등 “증적 자료”로 사용
  • 도메인 변경에도 비교적 안정적으로 대응 가능

 

커머스 상품은 시간이 지나도 “과거 상태를 정확히 복원”할 수 있어야 하고, 이는 JSON 스냅샷만으로는 결코 해결되지 않는다.

 


 

5. 정리

이 논의는 단순 기술 선택이 아니라, 커머스 시스템이 반드시 가져야 하는 데이터 신뢰성과 책임성에 관한 문제다.

JSON 스냅샷은 분명 매력적인 도구이며, 조회 성능 최적화 관점에서는 탁월한 선택일 수 있다.

 

하지만 커머스의 특성상, 정확한 이력 관리·데이터 무결성·법적 증빙·도메인 호환성을 모두 충족시키려면

스냅샷 단독 방식은 부족하며, 테이블 기반 이력을 병행하는 구조가 가장 현실적이고 안정적이다.

 


 

+ Recent posts