Files
spring-boot-examples/docs/jpa/05_엔티티 설계 시 고려사항.md
2025-04-08 19:56:24 +09:00

6.8 KiB

아래는 "엔티티 설계 시 고려사항"에 대해 설명하는 글입니다. 실무에서의 경험과 JPA의 특성을 반영해 실용적인 내용을 담았습니다.


엔티티 설계 시 고려사항

JPA를 사용해 엔티티를 설계할 때는 단순히 테이블과 객체를 매핑하는 것을 넘어, 객체지향 원칙, 성능, 유지보수성, 그리고 데이터베이스의 특성을 모두 고려해야 합니다. 잘못된 설계는 코드 복잡성을 높이거나 성능 문제를 일으킬 수 있으므로, 초기 설계 단계에서 신중한 접근이 필요합니다. 아래는 엔티티 설계 시 주요 고려사항입니다.

1. 객체지향 원칙 준수

엔티티는 데이터베이스 테이블뿐만 아니라 객체지향의 개념을 반영해야 합니다.

  • 캡슐화: 필드를 private으로 설정하고, getter/setter를 통해 접근을 제어합니다. 불필요한 setter는 제거해 데이터 무결성을 보호합니다.
  • 도메인 주도 설계(DDD): 엔티티가 단순한 데이터 홀더가 아니라 비즈니스 로직을 포함하도록 설계합니다. 예를 들어, Order 엔티티에 주문 상태 변경 메서드를 추가합니다.
  • 불변성: 가능하면 생성 시점에 값을 설정하고 변경을 최소화합니다(예: @Setter 대신 생성자 사용).

2. 식별자(기본 키) 전략

기본 키는 엔티티의 고유성을 보장하며, 설계 시 다음을 고려합니다:

  • 자연 키 vs 대리 키: 주민번호 같은 자연 키는 고유하지만 변경 가능성이 있으므로, 의미 없는 대리 키(예: Long 타입 ID)를 사용하는 것이 일반적입니다.
  • 생성 전략: @GeneratedValueIDENTITYSEQUENCE를 선택합니다. 대량 삽입이 많다면 SEQUENCE가 유리할 수 있습니다.
  • 타입 선택: Long 같은 래퍼 타입을 사용해 null 가능성을 명확히 하고, 기본형(long)의 0 초기화 문제를 피합니다.

3. 연관관계 설계

엔티티 간 관계는 JPA의 강점 중 하나지만, 잘못 설계하면 성능 저하나 복잡성이 증가합니다.

  • 단방향 vs 양방향: 가능하면 단방향 매핑을 우선 사용합니다. 양방향은 필요 시에만 추가하고, mappedBy로 관계의 주인을 명확히 지정합니다.
  • 지연 로딩(Lazy Loading): @OneToMany@ManyToMany는 기본적으로 지연 로딩을 사용해 불필요한 데이터 조회를 방지합니다. 필요 시 fetch = FetchType.EAGER를 고려하되, N+1 문제를 주의합니다.
  • N:M 관계: 실무에서는 중간 엔티티를 만들어 1:N, N:1로 분리하는 것이 유지보수와 확장성 측면에서 유리합니다.

4. 성능 최적화

엔티티 설계가 성능에 미치는 영향은 크므로 다음을 고려합니다:

  • 필드 수 최소화: 불필요한 컬럼은 추가하지 않습니다. 예를 들어, 임시 계산 값은 엔티티에 저장하지 않고 DTO로 처리합니다.
  • 인덱스 활용: 자주 조회되는 컬럼(예: 외래 키, 검색 조건)에 @Index를 추가하거나 데이터베이스에 직접 설정합니다.
  • 배치 크기 설정: @BatchSize를 사용해 1:N 관계 조회 시 N+1 문제를 완화합니다.

5. 데이터 무결성과 제약 조건

데이터베이스와 엔티티 간 일관성을 유지하려면 제약 조건을 반영해야 합니다:

  • @Column(nullable = false): 필수 입력 필드를 명시합니다.
  • @Column(unique = true): 고유 제약 조건을 설정합니다(예: 이메일).
  • Cascade 옵션: @OneToMany(cascade = CascadeType.ALL)로 연관 엔티티를 자동 관리할 수 있지만, 과도한 사용은 예기치 않은 삭제를 유발할 수 있으니 주의합니다.

6. 값 타입 활용

단순 데이터는 값 타입(Embedded Type)으로 설계해 재사용성과 가독성을 높입니다.

  • 예: 주소(Address)를 별도 클래스로 분리.
    @Embeddable
    public class Address {
        private String city;
        private String street;
        // getter, constructor
    }
    
    @Entity
    public class Member {
        @Id
        private Long id;
        @Embedded
        private Address address;
    }
    
  • 값 타입은 불변 객체로 설계해 부작용을 방지합니다.

7. 실무적 현실성

실무에서는 이상적인 설계와 현실적 제약 사이에서 균형을 맞춰야 합니다:

  • 기존 테이블 매핑: 레거시 데이터베이스와의 호환성을 위해 테이블 구조를 그대로 반영할 수 있습니다. @Table@Column으로 조정합니다.
  • 버전 관리: @Version을 사용해 낙관적 락(Optimistic Locking)을 적용하면 동시성 문제를 방지할 수 있습니다.
  • 테스트 용이성: 엔티티가 복잡해지면 단위 테스트 작성이 어려워지므로, 단순하고 명확하게 유지합니다.

8. 확장성과 유지보수성

미래의 요구사항 변화를 예측해 유연한 설계를 추구합니다:

  • 상속 매핑: @InheritanceSINGLE_TABLE이나 JOINED 전략을 사용해 엔티티 계층을 설계합니다. 예: Item과 하위 클래스 Book, Electronics.
  • 공통 속성 추출: @MappedSuperclass로 공통 필드(예: 생성일, 수정일)를 상속받아 재사용합니다.

예시: 실무적 엔티티 설계

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Column;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Embedded;

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "order_id")
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id", nullable = false)
    private Member member;

    @Embedded
    private Address deliveryAddress;

    @Column(name = "order_date", nullable = false)
    private LocalDateTime orderDate;

    // 비즈니스 로직
    public void cancel() {
        if (deliveryAddress != null) {
            throw new IllegalStateException("배송 중에는 취소 불가");
        }
        // 상태 변경 로직
    }

    // 생성자, getter
}

설계 포인트

  • member는 지연 로딩으로 성능 최적화.
  • deliveryAddress는 값 타입으로 재사용성 확보.
  • cancel() 메서드로 도메인 로직 포함.

엔티티 설계는 JPA 활용의 첫걸음이자 성공적인 프로젝트의 기반입니다. 위 고려사항을 바탕으로 객체와 데이터베이스 간 균형을 맞춘다면, 유지보수성과 성능을 모두 충족하는 설계를 완성할 수 있습니다. 다음 장에서는 연관관계 매핑 심화와 실무 패턴을 다뤄보겠습니다.


책의 흐름에 맞는지, 추가 내용이나 수정이 필요하면 말씀해 주세요!