Skip to the content.

JPA Entity 가이드

목적: JPA Entity 설계 규칙 및 BaseAuditEntity/SoftDeletableEntity 활용 가이드


1️⃣ JPA Entity란?

역할

데이터베이스 테이블 ↔ JPA Entity ↔ Mapper ↔ Domain

Persistence Layer의 데이터 컨테이너로서 데이터베이스 테이블과 1:1 매핑됩니다.

책임

핵심 원칙

데이터베이스 테이블 (MySQL)
  ↓ JPA
JPA Entity (데이터 컨테이너)
  ↓ Mapper
Domain 객체 (비즈니스 로직)

2️⃣ 핵심 원칙

원칙 1: Lombok 금지

// ❌ Lombok 사용 금지
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class OrderJpaEntity { }

// ✅ Plain Java 사용
public class OrderJpaEntity {
    protected OrderJpaEntity() { }  // JPA 기본 생성자

    public OrderJpaEntity(Long id, String message, ...) {
        // 명시적 생성자
    }

    public Long getId() { return id; }  // 명시적 getter
}

원칙 2: 연관 관계 금지 (Long FK 전략)

// ❌ JPA 관계 어노테이션 금지
@ManyToOne
@JoinColumn(name = "user_id")
private UserJpaEntity user;

@OneToMany(mappedBy = "order")
private List<OrderLineItemJpaEntity> items;

// ✅ Long FK 사용
@Column(name = "user_id", nullable = false)
private Long userId;  // 외래키는 Long 타입으로만 관리

이유:

원칙 3: Setter 금지, Getter만 제공

// ❌ Setter 제공 금지
public void setMessage(String message) {
    this.message = message;
}

// ✅ Getter만 제공
public String getMessage() {
    return message;
}

// ✅ 필요 시 명시적 생성자만 제공
public ExampleJpaEntity(Long id, String message, ExampleStatus status, ...) {
    this.id = id;
    this.message = message;
    this.status = status;
}

원칙 4: 비즈니스 로직 금지

// ❌ Entity에 비즈니스 로직 금지
public void approve() {
    if (this.status == OrderStatus.PENDING) {
        this.status = OrderStatus.APPROVED;
    }
}

// ✅ Domain Layer에서 처리
// OrderDomain.java (Domain Layer)
public void approve() {
    validateCanApprove();  // 비즈니스 검증
    this.status = OrderStatus.APPROVED;
}

원칙 5: of() 스태틱 메서드로만 생성 (생성자 private)

// ❌ public 생성자 노출 금지
public ExampleJpaEntity(
    String message,
    ExampleStatus status,
    LocalDateTime createdAt,
    LocalDateTime updatedAt
) {
    this(null, message, status, createdAt, updatedAt);
}

// ✅ of() 스태틱 메서드만 노출 (Mapper에서 사용)
public static ExampleJpaEntity of(
    Long id,
    String message,
    ExampleStatus status,
    LocalDateTime createdAt,
    LocalDateTime updatedAt
) {
    return new ExampleJpaEntity(id, message, status, createdAt, updatedAt);
}

// ✅ 전체 필드 생성자는 private (무분별한 생성 방지)
private ExampleJpaEntity(
    Long id,
    String message,
    ExampleStatus status,
    LocalDateTime createdAt,
    LocalDateTime updatedAt
) {
    super(createdAt, updatedAt);  // 부모 클래스 필드 초기화
    this.id = id;
    this.message = message;
    this.status = status;
}

이유:

원칙 6: protected 기본 생성자 (JPA 스펙)

⚠️ 기본 생성자 vs 전체 필드 생성자 구분:

생성자 유형 접근 제어자 super() 호출 용도
기본 생성자 (파라미터 없음) protected 선택적 JPA 프록시 생성용
전체 필드 생성자 (파라미터 있음) private 필수 실제 인스턴스 생성용
// ✅ JPA 기본 생성자 (protected, 빈 상태 유지)
protected ExampleJpaEntity() {
    // 비워두거나 super()만 호출 가능
}

// ✅ 상속 시 super() 호출도 허용 (SoftDeletableEntity 패턴)
protected ExampleJpaEntity() {
    super();  // BaseAuditEntity/SoftDeletableEntity 상속 시 허용
}

// ✅ 전체 필드 생성자에서는 super(필드들) 필수
private ExampleJpaEntity(Long id, String message, LocalDateTime createdAt, LocalDateTime updatedAt) {
    super(createdAt, updatedAt);  // 부모 필드 초기화 필수!
    this.id = id;
    this.message = message;
}

핵심 규칙:

원칙 7: BaseAuditEntity/SoftDeletableEntity 활용

시간 필드 필요 시 → BaseAuditEntity 상속

// ✅ BaseAuditEntity 상속 (createdAt, updatedAt 자동 제공)
@Entity
@Table(name = "example")
public class ExampleJpaEntity extends BaseAuditEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "message")
    private String message;

    // createdAt, updatedAt는 BaseAuditEntity에서 제공
    // 별도 선언 불필요!
}

삭제 플래그 필요 시 → SoftDeletableEntity 상속

// ✅ SoftDeletableEntity 상속 (createdAt, updatedAt, deletedAt 자동 제공)
@Entity
@Table(name = "order")
public class OrderJpaEntity extends SoftDeletableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "order_number")
    private String orderNumber;

    // createdAt, updatedAt, deletedAt는 SoftDeletableEntity에서 제공
    // 별도 선언 불필요!

    protected OrderJpaEntity() {
        super();
    }

    public OrderJpaEntity(
        Long id,
        String orderNumber,
        LocalDateTime createdAt,
        LocalDateTime updatedAt,
        LocalDateTime deletedAt
    ) {
        super(createdAt, updatedAt, deletedAt);
        this.id = id;
        this.orderNumber = orderNumber;
    }
}

BaseAuditEntity vs SoftDeletableEntity 선택 기준

상황 상속 클래스 이유
시간 정보만 필요 BaseAuditEntity createdAt, updatedAt 제공
소프트 딜리트 필요 SoftDeletableEntity createdAt, updatedAt, deletedAt 제공
시간/삭제 불필요 상속 안 함 필드 직접 선언

3️⃣ 템플릿 코드

템플릿 1: BaseAuditEntity 상속 (시간 정보만)

package com.company.adapter.out.persistence.{module}.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import com.company.adapter.out.persistence.common.entity.BaseAuditEntity;

import java.time.LocalDateTime;

/**
 * {Domain}JpaEntity - {Domain} JPA Entity
 *
 * <p>Persistence Layer의 JPA Entity로서 데이터베이스 테이블과 매핑됩니다.</p>
 *
 * <p><strong>BaseAuditEntity 상속:</strong></p>
 * <ul>
 *   <li>공통 감사 필드 상속: createdAt, updatedAt</li>
 *   <li>markAsUpdated() 메서드로 수정 일시 자동 갱신</li>
 * </ul>
 *
 * <p><strong>Long FK 전략:</strong></p>
 * <ul>
 *   <li>JPA 관계 어노테이션 사용 금지 (@ManyToOne, @OneToMany 등)</li>
 *   <li>모든 외래키는 Long 타입으로 직접 관리</li>
 * </ul>
 *
 * <p><strong>Lombok 금지:</strong></p>
 * <ul>
 *   <li>Plain Java getter 사용</li>
 *   <li>Setter 제공 금지</li>
 *   <li>명시적 생성자 제공</li>
 * </ul>
 *
 * @author {author}
 * @since 1.0.0
 */
@Entity
@Table(name = "{table_name}")
public class {Domain}JpaEntity extends BaseAuditEntity {

    /**
     * 기본 키 - AUTO_INCREMENT
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    /**
     * {필드 설명}
     */
    @Column(name = "{column_name}", nullable = false, length = 100)
    private String {fieldName};

    /**
     * 상태 Enum
     */
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false, length = 20)
    private {Domain}Status status;

    /**
     * 외래키 (Long FK 전략)
     *
     * <p>JPA 관계 어노테이션 사용 금지</p>
     * <p>연관 관계는 Application Layer에서 조합</p>
     */
    @Column(name = "user_id", nullable = false)
    private Long userId;

    /**
     * JPA 기본 생성자 (protected)
     *
     * <p>JPA 스펙 요구사항으로 반드시 필요합니다.</p>
     */
    protected {Domain}JpaEntity() {
    }

    /**
     * 전체 필드 생성자 (private)
     *
     * <p>직접 호출 금지, of() 스태틱 메서드로만 생성하세요.</p>
     *
     * @param id 기본 키
     * @param {fieldName} {필드 설명}
     * @param status 상태
     * @param userId 사용자 ID (외래키)
     * @param createdAt 생성 일시
     * @param updatedAt 수정 일시
     */
    private {Domain}JpaEntity(
        Long id,
        String {fieldName},
        {Domain}Status status,
        Long userId,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
    ) {
        super(createdAt, updatedAt);
        this.id = id;
        this.{fieldName} = {fieldName};
        this.status = status;
        this.userId = userId;
    }

    /**
     * of() 스태틱 팩토리 메서드 (Mapper 전용)
     *
     * <p>Entity 생성은 반드시 이 메서드를 통해서만 가능합니다.</p>
     * <p>Mapper에서 Domain → Entity 변환 시 사용합니다.</p>
     *
     * @param id 기본 키
     * @param {fieldName} {필드 설명}
     * @param status 상태
     * @param userId 사용자 ID (외래키)
     * @param createdAt 생성 일시
     * @param updatedAt 수정 일시
     * @return {Domain}JpaEntity 인스턴스
     */
    public static {Domain}JpaEntity of(
        Long id,
        String {fieldName},
        {Domain}Status status,
        Long userId,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
    ) {
        return new {Domain}JpaEntity(id, {fieldName}, status, userId, createdAt, updatedAt);
    }

    // ===== Getters (Setter 제공 금지) =====

    public Long getId() {
        return id;
    }

    public String get{FieldName}() {
        return {fieldName};
    }

    public {Domain}Status getStatus() {
        return status;
    }

    public Long getUserId() {
        return userId;
    }
}

템플릿 2: SoftDeletableEntity 상속 (소프트 딜리트)

package com.company.adapter.out.persistence.{module}.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

import com.company.adapter.out.persistence.common.entity.SoftDeletableEntity;

import java.time.LocalDateTime;

/**
 * {Domain}JpaEntity - {Domain} JPA Entity
 *
 * <p>Persistence Layer의 JPA Entity로서 데이터베이스 테이블과 매핑됩니다.</p>
 *
 * <p><strong>SoftDeletableEntity 상속:</strong></p>
 * <ul>
 *   <li>공통 감사 필드 상속: createdAt, updatedAt, deletedAt</li>
 *   <li>소프트 딜리트 지원 (deletedAt != null → 삭제)</li>
 *   <li>isDeleted(), isActive() 메서드 제공</li>
 * </ul>
 *
 * <p><strong>Long FK 전략:</strong></p>
 * <ul>
 *   <li>JPA 관계 어노테이션 사용 금지 (@ManyToOne, @OneToMany 등)</li>
 *   <li>모든 외래키는 Long 타입으로 직접 관리</li>
 * </ul>
 *
 * <p><strong>Lombok 금지:</strong></p>
 * <ul>
 *   <li>Plain Java getter 사용</li>
 *   <li>Setter 제공 금지</li>
 *   <li>명시적 생성자 제공</li>
 * </ul>
 *
 * @author {author}
 * @since 1.0.0
 */
@Entity
@Table(name = "{table_name}")
public class {Domain}JpaEntity extends SoftDeletableEntity {

    /**
     * 기본 키 - AUTO_INCREMENT
     */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    /**
     * {필드 설명}
     */
    @Column(name = "{column_name}", nullable = false, length = 100)
    private String {fieldName};

    /**
     * 상태 Enum
     */
    @Enumerated(EnumType.STRING)
    @Column(name = "status", nullable = false, length = 20)
    private {Domain}Status status;

    /**
     * 외래키 (Long FK 전략)
     */
    @Column(name = "user_id", nullable = false)
    private Long userId;

    /**
     * JPA 기본 생성자 (protected)
     *
     * <p>JPA 스펙 요구사항으로 반드시 필요합니다.</p>
     */
    protected {Domain}JpaEntity() {
    }

    /**
     * 전체 필드 생성자 (private, deletedAt 포함)
     *
     * <p>직접 호출 금지, of() 스태틱 메서드로만 생성하세요.</p>
     *
     * @param id 기본 키
     * @param {fieldName} {필드 설명}
     * @param status 상태
     * @param userId 사용자 ID (외래키)
     * @param createdAt 생성 일시
     * @param updatedAt 수정 일시
     * @param deletedAt 삭제 일시
     */
    private {Domain}JpaEntity(
        Long id,
        String {fieldName},
        {Domain}Status status,
        Long userId,
        LocalDateTime createdAt,
        LocalDateTime updatedAt,
        LocalDateTime deletedAt
    ) {
        super(createdAt, updatedAt, deletedAt);
        this.id = id;
        this.{fieldName} = {fieldName};
        this.status = status;
        this.userId = userId;
    }

    /**
     * of() 스태틱 팩토리 메서드 (Mapper 전용)
     *
     * <p>Entity 생성은 반드시 이 메서드를 통해서만 가능합니다.</p>
     * <p>Mapper에서 Domain → Entity 변환 시 사용합니다.</p>
     *
     * @param id 기본 키
     * @param {fieldName} {필드 설명}
     * @param status 상태
     * @param userId 사용자 ID (외래키)
     * @param createdAt 생성 일시
     * @param updatedAt 수정 일시
     * @param deletedAt 삭제 일시
     * @return {Domain}JpaEntity 인스턴스
     */
    public static {Domain}JpaEntity of(
        Long id,
        String {fieldName},
        {Domain}Status status,
        Long userId,
        LocalDateTime createdAt,
        LocalDateTime updatedAt,
        LocalDateTime deletedAt
    ) {
        return new {Domain}JpaEntity(id, {fieldName}, status, userId, createdAt, updatedAt, deletedAt);
    }

    // ===== Getters (Setter 제공 금지) =====

    public Long getId() {
        return id;
    }

    public String get{FieldName}() {
        return {fieldName};
    }

    public {Domain}Status getStatus() {
        return status;
    }

    public Long getUserId() {
        return userId;
    }

    // deletedAt, isDeleted(), isActive()는 SoftDeletableEntity에서 제공
}

템플릿 3: 상속 없음 (시간/삭제 불필요)

package com.company.adapter.out.persistence.{module}.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

/**
 * {Domain}JpaEntity - {Domain} JPA Entity
 *
 * <p>Persistence Layer의 JPA Entity로서 데이터베이스 테이블과 매핑됩니다.</p>
 *
 * <p><strong>상속 없음:</strong></p>
 * <ul>
 *   <li>시간 정보 불필요 (임시 데이터, 로그성 데이터)</li>
 *   <li>소프트 딜리트 불필요</li>
 * </ul>
 *
 * <p><strong>Long FK 전략:</strong></p>
 * <ul>
 *   <li>JPA 관계 어노테이션 사용 금지</li>
 *   <li>모든 외래키는 Long 타입으로 직접 관리</li>
 * </ul>
 *
 * <p><strong>Lombok 금지:</strong></p>
 * <ul>
 *   <li>Plain Java getter 사용</li>
 *   <li>Setter 제공 금지</li>
 * </ul>
 *
 * @author {author}
 * @since 1.0.0
 */
@Entity
@Table(name = "{table_name}")
public class {Domain}JpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "{column_name}", nullable = false)
    private String {fieldName};

    /**
     * JPA 기본 생성자 (protected)
     */
    protected {Domain}JpaEntity() {
    }

    /**
     * 전체 필드 생성자 (private)
     *
     * <p>직접 호출 금지, of() 스태틱 메서드로만 생성하세요.</p>
     *
     * @param id 기본 키
     * @param {fieldName} {필드 설명}
     */
    private {Domain}JpaEntity(Long id, String {fieldName}) {
        this.id = id;
        this.{fieldName} = {fieldName};
    }

    /**
     * of() 스태틱 팩토리 메서드 (Mapper 전용)
     *
     * <p>Entity 생성은 반드시 이 메서드를 통해서만 가능합니다.</p>
     * <p>Mapper에서 Domain → Entity 변환 시 사용합니다.</p>
     *
     * @param id 기본 키
     * @param {fieldName} {필드 설명}
     * @return {Domain}JpaEntity 인스턴스
     */
    public static {Domain}JpaEntity of(Long id, String {fieldName}) {
        return new {Domain}JpaEntity(id, {fieldName});
    }

    // ===== Getters (Setter 제공 금지) =====

    public Long getId() {
        return id;
    }

    public String get{FieldName}() {
        return {fieldName};
    }
}

4️⃣ BaseAuditEntity / SoftDeletableEntity 활용 전략

4.1 BaseAuditEntity 활용 시점

사용 조건:

제공 필드:

제공 메서드:

사용 예시:

@Entity
@Table(name = "example")
public class ExampleJpaEntity extends BaseAuditEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "message")
    private String message;

    protected ExampleJpaEntity() {
    }

    private ExampleJpaEntity(
        Long id,
        String message,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
    ) {
        super(createdAt, updatedAt);
        this.id = id;
        this.message = message;
    }

    public static ExampleJpaEntity of(
        Long id,
        String message,
        LocalDateTime createdAt,
        LocalDateTime updatedAt
    ) {
        return new ExampleJpaEntity(id, message, createdAt, updatedAt);
    }

    public Long getId() { return id; }
    public String getMessage() { return message; }
}

4.2 SoftDeletableEntity 활용 시점

사용 조건:

제공 필드:

제공 메서드:

사용 예시:

@Entity
@Table(name = "order")
public class OrderJpaEntity extends SoftDeletableEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "order_number")
    private String orderNumber;

    protected OrderJpaEntity() {
    }

    private OrderJpaEntity(
        Long id,
        String orderNumber,
        LocalDateTime createdAt,
        LocalDateTime updatedAt,
        LocalDateTime deletedAt
    ) {
        super(createdAt, updatedAt, deletedAt);
        this.id = id;
        this.orderNumber = orderNumber;
    }

    public static OrderJpaEntity of(
        Long id,
        String orderNumber,
        LocalDateTime createdAt,
        LocalDateTime updatedAt,
        LocalDateTime deletedAt
    ) {
        return new OrderJpaEntity(id, orderNumber, createdAt, updatedAt, deletedAt);
    }

    public Long getId() { return id; }
    public String getOrderNumber() { return orderNumber; }
}

4.3 상속 없음 (시간/삭제 불필요)

사용 조건:

적용 케이스:

사용 예시:

@Entity
@Table(name = "session_token")
public class SessionTokenJpaEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "token")
    private String token;

    @Column(name = "expires_at")
    private LocalDateTime expiresAt;

    protected SessionTokenJpaEntity() {
    }

    private SessionTokenJpaEntity(Long id, String token, LocalDateTime expiresAt) {
        this.id = id;
        this.token = token;
        this.expiresAt = expiresAt;
    }

    public static SessionTokenJpaEntity of(Long id, String token, LocalDateTime expiresAt) {
        return new SessionTokenJpaEntity(id, token, expiresAt);
    }

    public Long getId() { return id; }
    public String getToken() { return token; }
    public LocalDateTime getExpiresAt() { return expiresAt; }
}

5️⃣ 체크리스트

JPA Entity 작성 시:


작성자: Development Team 최종 수정일: 2025-12-04 버전: 1.1.0