엔티티-테이블 매핑 기본

한 줄 정의

Entity는 JPA가 관리하는 객체로, 데이터베이스 테이블과 1:1로 매핑되어 영속성 컨텍스트의 관리 대상이 된다.

실무에서 왜 중요한가

Entity 매핑이 잘못되면 다음 문제가 생긴다.

  • 기본 생성자가 없어서 JPA가 객체를 생성하지 못한다.
  • @Column 설정이 누락되어 DDL 자동 생성 시 의도와 다른 스키마가 만들어진다.
  • @Id 생성 전략을 잘못 선택해서 배치 insert 성능이 나빠진다.
  • @Enumerated(ORDINAL)을 써서 enum 순서 변경 시 데이터가 깨진다.
  • Entity를 API 응답으로 직접 반환해서 순환 참조, 민감 정보 노출 문제가 생긴다.

기본 매핑 규칙

@Entity
@Table(name = "users")
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    @Column(nullable = false, length = 50)
    private String name;
 
    @Column(nullable = false, unique = true)
    private String email;
 
    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private UserStatus status;
 
    @Column(updatable = false)
    private LocalDateTime createdAt;
 
    private LocalDateTime updatedAt;
 
    protected User() {
    }
 
    // 생성 메서드, getter 등
}

Entity 필수 조건

조건이유
@Entity 어노테이션JPA 관리 대상으로 등록
@Id 필드영속성 컨텍스트에서 식별자로 사용
기본 생성자 (protected 이상)JPA가 리플렉션으로 객체를 생성
final class가 아닐 것프록시 생성을 위해 상속 가능해야 함

@Id 생성 전략

전략동작실무 포인트
IDENTITYDB의 auto_increment 사용MySQL에서 주로 사용, insert 후 id를 알 수 있어 batch insert 불가
SEQUENCEDB 시퀀스 사용PostgreSQL/Oracle에서 주로 사용, allocationSize로 성능 조절
TABLE별도 테이블로 시퀀스 흉내성능이 나빠서 실무에서 거의 사용하지 않음
AUTODB 방언에 따라 자동 선택예측이 어려워 명시적 전략이 안전

MySQL 환경에서는 IDENTITY가 일반적이다. 단, IDENTITY는 insert 시점에 DB에서 id를 받아와야 하므로 JDBC batch insert가 동작하지 않는다.

@Column 실무 설정

@Column(nullable = false, length = 100)  // NOT NULL, VARCHAR(100)
private String name;
 
@Column(unique = true)                    // UNIQUE 제약조건
private String email;
 
@Column(updatable = false)                // UPDATE 시 제외
private LocalDateTime createdAt;
 
@Column(insertable = false, updatable = false)  // 읽기 전용
private LocalDateTime dbCreatedAt;

@Column을 생략하면 JPA가 필드명을 그대로 컬럼명으로 사용한다. Spring Boot의 기본 네이밍 전략은 camelCase를 snake_case로 변환한다 (userNameuser_name).

Enum 매핑

// 위험: 순서 기반 저장
@Enumerated(EnumType.ORDINAL)
private UserStatus status; // 0, 1, 2...
 
// 안전: 이름 기반 저장
@Enumerated(EnumType.STRING)
private UserStatus status; // "ACTIVE", "INACTIVE"...

ORDINAL은 enum 상수의 선언 순서가 바뀌면 기존 데이터와 매핑이 깨진다. 반드시 STRING을 사용해야 한다.

날짜/시간 매핑

Java 8+ LocalDateTime, LocalDate는 별도 어노테이션 없이 매핑된다.

private LocalDateTime createdAt;
private LocalDate birthDate;

@CreatedDate, @LastModifiedDate를 사용하면 자동으로 생성/수정 시간을 관리할 수 있다. 단, Configuration 클래스에 @EnableJpaAuditing을 추가해야 동작한다.

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity {
 
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
 
    @LastModifiedDate
    private LocalDateTime updatedAt;
}
@Configuration
@EnableJpaAuditing
public class JpaConfig { }

자주 나는 실수

  • 기본 생성자를 만들지 않아서 InstantiationException이 발생한다.
  • @Enumerated의 기본값이 ORDINAL인 것을 모르고 사용한다.
  • Entity를 API 응답으로 직접 반환한다.
  • @Column(nullable = false)를 DB 제약조건으로 착각하고, DDL 자동 생성을 안 쓰면 아무 효과 없다고 생각한다.
  • @GeneratedValue 전략을 이해하지 않고 기본값을 사용한다.
  • @Id 타입을 primitive(long)로 써서 null 체크가 안 된다.

핵심 요약

JPA Entity는 @Entity, @Id, 기본 생성자가 필수이며, 테이블과 1:1로 매핑됩니다.

@Id 생성 전략은 MySQL에서는 IDENTITY가 일반적이지만, batch insert가 안 되는 제약이 있습니다. enum은 반드시 @Enumerated(STRING)을 사용해야 순서 변경에 안전합니다.

Entity를 API 응답으로 직접 반환하면 순환 참조, 민감 정보 노출, lazy loading 문제가 생기므로 DTO로 변환해야 합니다.

꼬리 질문

점검 퀴즈

아래 문항은 개념을 실제로 설명할 수 있는지 점검하기 위한 것이다. 선택지를 누르면 정답 여부와 이유가 표시된다.

객관식Entity에 기본 생성자가 필요한 이유는?

OXEnum을 ORDINAL로 저장하면 enum 순서 변경 시 기존 데이터의 의미가 바뀔 수 있다.

객관식@Id 타입을 primitive long보다 Long으로 쓰는 것이 일반적인 이유는?

관련 문서