[DynamoDB] 2. 관계형 사고방식에서 벗어나기

조성우·2025년 9월 1일

DynamoDB

목록 보기
2/4
post-thumbnail

NoSQL 데이터 모델링을 처음 접할 때 가장 큰 장벽은 기존의 관계형 DB에서 익혔던 사고방식이다. 관계형 DB가 데이터의 중복을 없애고 정규화를 통해 저장 공간을 최적화하는 데 집중했다면, DynamoDB는 전혀 다른 목표를 추구한다!


1. NoSQL 데이터 모델링의 변화

RDBMS의 시대에는 스토리지와 컴퓨팅 자원 모두가 비쌌다. 따라서 데이터를 여러 테이블로 나누고 중복을 최소화하는 정규화(Normalization)는 당연한 선택이었다. 이는 저장 공간을 아끼는 데 효과적이었지만, 데이터를 조회할 때마다 여러 테이블을 연결하는 복잡한 JOIN 연산을 요구했다.

하지만 시대가 변했다. 스토리지 비용은 급격히 저렴해진 반면, 대규모 트래픽을 처리하기 위한 컴퓨팅 성능은 여전히 핵심적인 비용 요소이다. NoSQL은 이러한 변화에 발맞춰 탄생했다. NoSQL의 핵심 철학은 스토리지 최적화가 아닌 컴퓨팅 성능 최적화이다.

이를 위해 NoSQL은 비정규화(Denormalization)를 적극적으로 수용한다. 즉, 데이터의 중복을 허용하더라도 JOIN과 같은 비싼 연산을 런타임에 수행하지 않도록 데이터를 설계하는 것이다. 이는 "함께 접근해야 하는 데이터는 함께 저장되어야 한다"는 원칙으로 요약된다.


2. 데이터 관계 새롭게 이해하기

"NoSQL은 비관계형 데이터베이스"라는 말은 가장 큰 오해 중 하나이다. 현실 세계의 모든 데이터는 본질적으로 관계적이다. 온라인 쇼핑몰의 고객과 주문, 주문과 상품처럼 데이터는 서로 연결되어 있다. 중요한 것은 데이터베이스 기술이 아니라 그 관계를 '어떻게' 표현하고 다루느냐의 차이이다.

RDBMS가 외래 키(Foreign Key)와 JOIN을 사용해 여러 테이블에 흩어져 있는 관계를 런타임에 조합한다면, DynamoDB는 단일 테이블 내에서 관계를 명시적으로 모델링한다.

여기서 핵심적인 역할을 하는 것이 파티션 키(PK)정렬 키(SK)이다.

예를 들어, 특정 주문(o#12345)과 관련된 모든 정보를 가져오는 경우를 생각해보자.

  • PK: o#12345
  • SK:
    • p#<product_id> (상품 정보)
    • i#<invoice_id> (송장 정보)
    • c#<customer_id> (고객 정보)

이처럼 동일한 파티션 키 아래에 정렬 키를 사용해 다양한 유형의 관련 데이터를 함께 저장한다. 이렇게 하면 o#12345라는 주문 ID 하나만으로 JOIN 없이 단 한 번의 쿼리를 통해 주문과 관련된 모든 정보를 효율적으로 가져올 수 있다.
(o#12345에 해당하는 데이터 집합을 Item Collection이라고도 함)

단일 요청으로 모든 정보 조회해보기

예를 들어, 특정 주문(o#12345)에 포함된 모든 제품을 조회하고 싶다면, DynamoDB의 Query API와 begins_with() 연산자를 사용하여 단일 요청으로 효율적으로 데이터를 가져올 수 있습니다.

  • 특정 주문의 모든 제품 가져오기: PK = "o#12345", SK begins_with("p#")
  • 특정 주문의 송장 가져오기: PK = "o#12345", SK begins_with("i#")
  • 특정 주문의 모든 세부 정보(고객 포함) 가져오기: PK = "o#12345", SK begins_with("c#")

이는 관계를 부정하는 것이 아니라, 오히려 애플리케이션의 접근 패턴에 맞게 관계를 미리 구성(pre-build)하는, 더 적극적인 관계 모델링 방식이라고 볼 수도 있다!


3. 비정규화

비정규화는 단순히 데이터를 복사해서 여러 곳에 저장하는 것이 아니다. 이는 런타임 JOIN 연산을 제거하여 예측 가능한 낮은 지연 시간을 확보하기 위한 전략적인 선택이다. 대규모 서비스에서 JOIN 연산은 데이터베이스에 엄청난 부하를 주며, 이는 곧 시스템 전체의 성능 저하, 교착 상태(deadlock), 심지어 서비스 장애로 이어질 수 있다.

비정규화를 통해 데이터를 중복 저장하면, 애플리케이션이 필요한 모든 데이터를 단일 요청으로 읽을 수 있다. 이는 특히 읽기 작업이 쓰기 작업보다 압도적으로 많은 대부분의 서비스에서 엄청난 이점을 제공한다. 페이지 로딩 시간을 밀리초 단위로 단축하는 것이 비즈니스의 성패를 가르는 오늘날, 이러한 성능 최적화는 선택이 아닌 필수이다.

물론 데이터 중복은 데이터 일관성에 대한 우려를 낳는다. 원본 데이터가 변경될 때 모든 복사본을 어떻게 업데이트할 것인가? DynamoDB는 이러한 문제를 해결하기 위해 트랜잭션 API를 제공한다. 이를 통해 여러 항목에 대한 변경 작업을 "전부 성공 또는 전부 실패(all-or-nothing)"로 묶어 원자적으로 처리할 수 있으므로, 데이터 정합성을 보장할 수 있다.


4. 단일 테이블 설계 vs. 다중 테이블 설계

RDBMS에 익숙한 개발자에게 가장 낯선 개념 중 하나는 바로 단일 테이블 설계(Single-Table Design)라고 한다. 이는 애플리케이션의 모든 엔티티(고객, 주문, 상품 등)를 단 하나의 테이블에 저장하는 방식이다. DynamoDB에서는 데이터 모델링을 시작할 때 거의 항상 단일 테이블 설계를 우선적으로 고려하는 것이 좋다.

단일 테이블 설계의 장점

  • 성능: 관련 데이터를 물리적으로 한곳에 모아 JOIN 없이 한 번의 I/O로 조회하므로 가장 빠르다.
  • 비용 효율성: 특히 프로비저닝 모드에서 여러 테이블에 걸쳐 오토스케일링 버퍼를 따로 두는 비효율을 줄여 비용을 절감할 수 있다.
  • 확장성: PK, SK, GSI(글로벌 보조 인덱스) 등을 일반적인 이름으로 설계하면, 나중에 새로운 기능이나 엔티티가 추가되어도 스키마 변경 없이 유연하게 확장할 수 있다.

다중 테이블 설계가 더 나은 경우?

하지만 단일 테이블 설계가 만능은 아니다. 다음과 같은 상황에서는 다중 테이블로 분리하는 것이 더 합리적일 수 있다.

  • 서로 다른 접근 패턴: 사용자 프로필 데이터와 실시간 채팅 메시지처럼 데이터의 접근 빈도와 생명주기가 완전히 다른 경우, 테이블을 분리하는 것이 관리와 비용 측면에서 유리하다.
  • 백업 및 복원: 거대한 채팅 메시지 데이터와 중요한 사용자 프로필이 한 테이블에 있다면, 프로필 데이터만 복원하고 싶을 때 불필요하게 전체 테이블을 복원해야 해 비용과 시간이 낭비된다.
  • 비용 최적화: 자주 접근하지 않는 데이터(예: 오래된 사용자 프로필)를 별도의 테이블로 분리하고 DynamoDB의 Standard-IA 테이블 클래스를 적용하면 스토리지 비용을 크게 절감할 수 있다.

Standard-IA 테이블 클래스: Amazon DynamoDB에서 자주 액세스하지 않는 데이터에 대해 스토리지 비용을 절감하는 테이블 클래스이다.


5. 데이터 분해 (Breaking down)

NoSQL 모델링은 단순히 모든 속성을 하나의 거대한 JSON 객체로 저장하는 것을 의미하지 않는다. 오히려 데이터를 더 작고 논리적인 단위로 분해하여 효율성과 유연성을 극대화하는 수직 파티셔닝(Vertical Partitioning) 전략을 사용한다.

이는 앞서 설명한 PK와 SK를 활용한 관계 모델링의 심화 버전이다. 예를 들어, 한 명의 사용자에 대한 모든 데이터를 하나의 큰 항목으로 만드는 대신, 다음과 같이 동일한 PK 아래에 여러 항목으로 분해할 수 있다.

  • PK: u#<user_id>, SK: profile (기본 프로필 정보)
  • PK: u#<user_id>, SK: stats (게임 통계)
  • PK: u#<user_id>, SK: inventory (보유 아이템 목록)

이렇게 데이터를 분해하면, 애플리케이션은 필요한 데이터 조각만 정확히 가져올 수 있다. 프로필 페이지만 필요할 때는 profile 항목만, 게임 통계가 필요할 때는 stats 항목만 읽으면 된다. 이는 불필요한 데이터 전송을 줄여 I/O, 비용, 지연 시간을 모두 최적화하는 효과적인 방법이다.

또한 이 기법은 DynamoDB의 개별 항목 크기 제한인 400KB를 극복(?)하는 데도 유용하다. 만약 400KB를 초과하는 Large Object(LOB)를 저장해야 한다면, 데이터를 여러 조각으로 분해하여 저장하거나, 객체 자체는 Amazon S3와 같은 객체 스토리지에 저장하고 DynamoDB에는 해당 객체를 가리키는 참조나 메타데이터만 저장하는 것이다.

0개의 댓글