6.8 KiB
아래는 "엔티티 설계 시 고려사항"에 대해 설명하는 글입니다. 실무에서의 경험과 JPA의 특성을 반영해 실용적인 내용을 담았습니다.
엔티티 설계 시 고려사항
JPA를 사용해 엔티티를 설계할 때는 단순히 테이블과 객체를 매핑하는 것을 넘어, 객체지향 원칙, 성능, 유지보수성, 그리고 데이터베이스의 특성을 모두 고려해야 합니다. 잘못된 설계는 코드 복잡성을 높이거나 성능 문제를 일으킬 수 있으므로, 초기 설계 단계에서 신중한 접근이 필요합니다. 아래는 엔티티 설계 시 주요 고려사항입니다.
1. 객체지향 원칙 준수
엔티티는 데이터베이스 테이블뿐만 아니라 객체지향의 개념을 반영해야 합니다.
- 캡슐화: 필드를
private으로 설정하고, getter/setter를 통해 접근을 제어합니다. 불필요한 setter는 제거해 데이터 무결성을 보호합니다. - 도메인 주도 설계(DDD): 엔티티가 단순한 데이터 홀더가 아니라 비즈니스 로직을 포함하도록 설계합니다. 예를 들어,
Order엔티티에 주문 상태 변경 메서드를 추가합니다. - 불변성: 가능하면 생성 시점에 값을 설정하고 변경을 최소화합니다(예:
@Setter대신 생성자 사용).
2. 식별자(기본 키) 전략
기본 키는 엔티티의 고유성을 보장하며, 설계 시 다음을 고려합니다:
- 자연 키 vs 대리 키: 주민번호 같은 자연 키는 고유하지만 변경 가능성이 있으므로, 의미 없는 대리 키(예:
Long타입 ID)를 사용하는 것이 일반적입니다. - 생성 전략:
@GeneratedValue로IDENTITY나SEQUENCE를 선택합니다. 대량 삽입이 많다면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. 확장성과 유지보수성
미래의 요구사항 변화를 예측해 유연한 설계를 추구합니다:
- 상속 매핑:
@Inheritance로SINGLE_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 활용의 첫걸음이자 성공적인 프로젝트의 기반입니다. 위 고려사항을 바탕으로 객체와 데이터베이스 간 균형을 맞춘다면, 유지보수성과 성능을 모두 충족하는 설계를 완성할 수 있습니다. 다음 장에서는 연관관계 매핑 심화와 실무 패턴을 다뤄보겠습니다.
책의 흐름에 맞는지, 추가 내용이나 수정이 필요하면 말씀해 주세요!