모델링으로 배우는 정규화 / 비정규화

soyeon·2023년 4월 29일
post-thumbnail

정규화 / 비정규화

정규화

  • 중복을 제거하고 한 곳에서 관리
  • 데이터 정합성 유지가 쉬움
  • 읽기 시 참조 발생

비정규화

  • 중복을 허용
  • 데이터 정합성 유지가 어려움
  • 참조없이 읽기 가능

테이블 설계관점에서 조회와 쓰기 사이의 트레이드 오프

실습

간단한 SNS 서비스를 구현하면서 정규화와 비정규화를 해보자.
깃참고

요구사항

  • 회원정보 관리
    • 이메일, 닉네임, 생년월일을 입력받아 저장한다.
    • 닉네임은 10자를 초과할 수 없다.
    • 회원은 닉네임을 변경할 수 있다.
      • 회원의 닉네임 변경이력을 조회 할 수 있어야한다.
  • 팔로우 기능
    • 누가 누구를 팔로우했는지 저장한다.

일단 멤버의 가입, 닉네임 변경에 대한 로직은 간단히 작성한 후 회원의 닉네임 변경이력을 조회 할 수 있어야한다. 부분을 진행하도록 하겠다.

히스토리성 데이터는 정규화 대상이 아니다.

요구사항에 따라 데이터의 최신성을 보장해야하는 데이터인지 과거의 내역을 보장해야하는 데이터인지를 고려해야한다.

그러므로 현재의 경우 MemberNicknameHistory의 nickname과 Member의 nickname은 둘 다 존재해야하는 경우이다.
처음에 가입 시 MemberNicknameHistory에 첫 닉네임과 함께 같이 저장하고 변경 시에 새로운 닉네임을 받아 같이 저장해주는 로직으로 작성했다.

MemberNicknameHistory entity 정의하기

package com.example.twittermysql.domain.member.entity;

import java.time.LocalDateTime;
import java.util.Objects;
import lombok.Builder;
import lombok.Getter;

@Getter
public class MemberNicknameHistory {

    final private Long id;

    final private Long memberId;

    final private String nickname;

    final private LocalDateTime createdAt;

    @Builder
    public MemberNicknameHistory(Long id, Long memberId, String nickname, LocalDateTime createdAt) {
        this.id = id;
        this.memberId = Objects.requireNonNull(memberId);
        this.nickname = Objects.requireNonNull(nickname);
        this.createdAt = createdAt == null ? LocalDateTime.now() : createdAt;
    }
}

정규화한 경우

follow는 데이터의 최신성을 보장해야하는 경우다.

follow에서 member를 조인하게 되면 follow 서비스에 멤버가 침투하게 될것이다...
그러면 두 도메인 간에 엄청난 결합이 이뤄진다.
초반부터 큰 결합을 가지는 경우는 유연성이 좋지 않은 서비스가 될 가능성이 높다.
리팩토링도 힘들어진다.
초반에 결합을 낮추면서 개발을 하는 것이 좋다.

follow는 중복을 제거하는 것이 비즈니스 요구사항에 더 적합하다.

Follow entity 정의하기

package com.example.twittermysql.domain.follow.entity;

import java.time.LocalDateTime;
import java.util.Objects;
import lombok.Builder;
import lombok.Getter;

@Getter
public class Follow {

    final private Long id;

    final private Long fromMemberId;

    final private Long toMemberId;

    final private LocalDateTime createdAt;

    @Builder
    public Follow(Long id, Long fromMemberId, Long toMemberId, LocalDateTime createdAt) {
        this.id = id;
        this.fromMemberId = Objects.requireNonNull(fromMemberId);
        this.toMemberId = Objects.requireNonNull(toMemberId);
        this.createdAt = createdAt == null ? LocalDateTime.now() : createdAt;
    }
}

정리

중복된 데이터면 반드시 정규화를 해야하나?

  • 중복 데이터면 기계적으로 정규화하는 것은 NO!
  • 정규화도 비용이다. 읽기 비용과 쓰기 비용의 트레이드오프..

정규화시 고려해야할 점

  • 데이터의 최신성을 얼마나 보장해야하는가?
  • 히스토리성 데이터는 정규화 대상이 아니다.
  • 데이터의 변경 주기와 조회 주기
    - 자주 변경되는 데이터일수록 쓰기에 이점을 가져가도록 한다.
  • 객체(테이블) 탐색 깊이

    위 A에서 D 객체까지의 탐색 깊이를 가진 경우가 있다.
    원래는 A -> B -> C -> D 이렇게 가졌다면 B에서 D를 참조할 수 있게 변경하여 탐색 깊이를 줄일 수 있다.
    하지만 읽기와 쓰기의 트레이드 오프가 발생하는데 C의 D 참조가 변경될 경우 B의 D참조도 같이 변경해야한다는 경우이다.

정규화를 하기로 했다면 읽기시 데이터를 어떻게 가져올 것인가?

  • 조인?! 고려할 사항이 많다...
    • 다른 최적화 기법을 이용 시 제한이 생기거나 더 많은 리소스가 들 수 있다.
    • 서로 다른 테이블의 결합도를 높인다..
  • 조회시 성능이 좋은 별도의 데이터베이스나 캐싱등 다양한 최적화기법을 사용
  • 읽기 쿼리를 한번 더 실행 => 이는 큰 부담이 아닌 경우도 있다.
profile
사부작 사부작

0개의 댓글