본문 바로가기

백엔드/Spring

[자바 ORM 표준 JPA 프로그래밍 - 기본편] 04. 엔티티 매핑

엔티티 매핑


객체와 테이블 매핑: @Entity, @Table

@Entity

  • @Entity가 붙은 클래스는 JPA가 관리한다. 
  • 주의
    • 기본 생성자 필수
    • 저장할 필드에 final 사용 안됨 
  • 속성: name
    • JPA에서 사용할 엔티티 이름을 지정한다.
    • 기본값은 클래스 이름을 그대로 사용하는 것이다. 가급적 기본값을 쓴다. 

@Table

엔티티와 매핑할 테이블을 지정한다.

속성 기능 사용 예시
name 테이블의 이름을 지정 @Table(name = "users")
schema 테이블이 위치할 데이터베이스 스키마를 지정 @Table(name = "users", schema = "public")
catalog 테이블이 위치할 데이터베이스 카탈로그를 지정 @Table(name = "users", catalog = "mycatalog")
uniqueConstraints 테이블 수준에서 유니크 제약 조건을 설정. 하나 또는 여러 열에 대한 유니크 조건을 정의 가능 @Table(name = "users", uniqueConstraints = {@UniqueConstraint(columnNames = {"email"})})

 

필드와 컬럼 매핑: @Column

기본 키 매핑: @Id

연관관계 매핑: @ManyToOne, @JoinColumn

 

데이터베이스 스키마 자동 생성


DDL(Data Definition Language, 데이터 정의 언어)은 데이터베이스 스키마를 생성하고, 수정하며, 삭제할 수 있는 SQL 명령어 집합을 의미한다.

Hibernate는 JPA의 구현체 중 하나로 엔티티만 등록해놓으면 DDL을 자동 생성하여 테이블을 생성하거나 수정해주는 ddl-auto라는 설정이 있다. 이는 굉장히 편리하면서도 위험한 옵션을 가진다.

none 자동 스키마 생성 및 업데이트를 실행하지 않는다. 설정을 생략하면 none이 기본값이다.
validate 시작할 때 데이터베이스 스키마가 현재 JPA/Hibernate 엔티티 매핑과 일치하는지 검증한다. 
update 데이터베이스 스키마를 현재 JPA/Hibernate 엔티티 매핑에 맞게 업데이트한다. 새로운 엔티티나 칼럼이 매핑된 경우에는 해당 스키마를 추가하지만, 데이터 손실은 발생하지 않는다.
create 기존 테이블을 삭제 후 다시 생성한다.
create-drop create와 같으나, 애플리케이션 종료 시점에 테이블을 drop한다.

 

  • 운영 장비에는 절대 create, create-drop, update를 사용하면 안된다.
  • 개발 초기 단계: create 또는 update
  • 테스트 서버: update 또는 validate
  • 스테이징, 운영 서버: validate 또는 none

운영서버나 테스트 서버에서 ddl-auto 옵션을 잘못 설정했다가는 지금까지 저장됐던 모든 데이터가 날라갈...테이블이 drop돼버릴 위험이 있기에 조심해야한다.

DDL 생성 기능

@Column(unique=true, length = 10, name = "userName")
  • 데이터베이스 테이블에 생성될 컬럼의 이름을 "userName"으로 지정
  • 컬럼의 최대 길이를 10으로 제한
  • 해당 컬럼에 대해 유니크 제약 조건을 추가

이렇게 여러 조건을 줄 수 있다. 엔티티 코드만 보고 여러 정보를 파악할 수 있기에 @Column을 사용하여 제약 조건을 명시하는 것이 편리할 수 있다.

이런 조건들은 런타임에 영향을 주지 않고 DDL생성에만 영향을 준다.

 

추가적인 필드와 컬럼 매핑


@Enumerated(EnumType.STRING) 
private RoleType roleType;

Java Enum 타입을 데이터베이스에 저장할 때 문자열로 저장하도록 설정한다.

EnumType.ORDINAL 옵션도 존재하는데, 이는 enum의 '순서'를 숫자로서 저장하기때문에 중간에 enum 클래스의 내용이 바뀌는 순간 DB에 저장된 내용이 다 꼬여버린다. >> 사용 금지

@Temporal(TemporalType.TIMESTAMP) 
private Date createdDate;

Java에서 날짜와 시간을 다루는 방식은 크게 두 가지가 있다.

  • java.util.Date 클래스를 사용
  • Java 8부터 도입된 java.time 패키지(예: LocalDate, LocalDateTime)를 사용

java.util.Date

  • Date: java.util 패키지에 있는 Date 클래스는 날짜와 시간을 밀리초 단위로 표현한다. 1970년 1월 1일 00:00:00 GMT 이후 지난 밀리초를 나타내고 날짜와 시간을 모두 포함할 수 있지만 표현에 한계가 있다.

java.time.LocalDate & java.time.LocalDateTime

  • LocalDate: java.time 패키지에 있는 LocalDate 클래스는 날짜(년, 월, 일)만을 표현
  • LocalDateTime: LocalDateTime 클래스는 날짜와 시간을 모두 포함하지만, LocalDate와 마찬가지로 시간대 정보는 포함하지 않음
더보기

LocalDateTime 클래스가 날짜와 시간 정보를 포함하면서도 "시간대" 정보가 없다는 것은, 이 클래스가 특정 지역의 시간대를 반영하지 않고 순수하게 연, 월, 일, 시, 분, 초만을 나타내는 '지역화되지 않은' 시간을 표현한다는 의미이다.

@Temporal

  • JPA에서는 java.util.Date와 java.util.Calendar의 날짜/시간 값을 데이터베이스에 매핑할 때 사용하는 어노테이션
  • java.time 패키지의 클래스들은 @Temporal을 필요로 하지 않음
  • @Temporal 어노테이션의 3가지 속성
    • TemporalType.DATE: 날짜만 (년, 월, 일).
    • TemporalType.TIME: 시간만 (시, 분, 초, 나노초).
    • TemporalType.TIMESTAMP: 날짜와 시간 모두 (년, 월, 일, 시, 분, 초, 나노초)
@Lob private 
String description;
  • 데이터베이스 BLOB, CLOB 타입과 매핑된다.
  • 매핑하는 필드 타입이 문자면 CLOB, 나머지는 BLOB 매핑
@Transient
private int temp;  // 이 필드는 데이터베이스에 저장되지 않는다.
  • 필드 매핑X, 데이터베이스에 조회X, 저장X
  • 주로 메모리상에서만 임시로 어떤 값을 보관하고 싶을 때 사용한다.

 

기본 키 매핑 방법


  • 직접 할당: @Id만 사용
  • 자동 생성(@GeneratedValue)
    • IDENTITY: 데이터베이스에 위임, MYSQL
    • SEQUENCE: 데이터베이스 시퀀스 오브젝트 사용, ORACLE
      @SequenceGenerator 필요
    • TABLE: 키 생성용 테이블 사용, 모든 DB에서 사용
      @TableGenerator 필요
    • AUTO: 방언에 따라 자동 지정, 기본값

GenerationType.IDENTITY

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

 

따로 Id값을 지정하지 않아도

 

이렇게 ID값이 차례로 저장된 것을 알 수 있다. 

더보기

빠르게 정리해보는 영속성 컨텍스트

//비영속
Member member1 = new Member();
member1.setUsername("member1");

//영속
em.persist(member1);

tx.commit();
  • Member member = new Member(): Member객제 생성, 비영속 상태. 영속성 컨텍스트와 아무런 관련 없음
  • em.persist(member1): member1 객체를 영속성 컨텍스트에 관리하도록 함. 이 시점에서 member1은 영속 상태가 되며, 1차 캐시에 저장, 쓰기 지연 SQL 저장소에 SQL문이 생성되어 저장
  • tx.commit(): JPA는 트랜잭션 내에 쓰기 지연 SQL 저장소에 모인 SQL 문을 데이터베이스에 전송
  • 데이터베이스가 자동으로 ID를 생성하기 때문에 개발자가 직접 ID값을 설정하면 안된다.
  • 데이터베이스 삽입 시 null로 취급되어 데이터베이스가 자동으로 ID를 할당하는 것
  • JPA에서는 엔티티가 영속 상태가 되려면 PK값이 필요하다.
    • 따라서 GenerationType.IDENTITY 전략을 사용하면 엔티티 매니저의 persist메소드를 호출할 때 (em.persist) 바로 데이터베이스에 INSERT 쿼리가 발생한다. ID를 즉시 확정직시 위한 처리이다.

일반적으로 JPA에서는 트랜잭션이 커밋되는 시점에 SQL 쿼리가 데이터베이스로 전송된다. 그러나 GenerationType.IDENTITY 전략을 사용할 경우, persist 메소드 호출 시점에 바로 INSERT 쿼리가 데이터베이스로 전송한다. 이는 ID 값을 즉시 생성하고 영속성 컨텍스트에서 엔티티를 올바르게 관리하기 위함이다.

GenerationType.SEQUENCE

  • 엔티티를 영속화하기 전에 먼저 데이터베이스의 시퀀스를 사용해 ID값을 얻는다.
  • persist 메소드 호출 전에 발생하며, 엔티티의 ID필드는 null이 아닌 시퀀스에서 생성된 값으로 초기화된다.
    • 얻어진 시퀀스 값으로 ID가 설정된 후, 해당 엔티티가 영속성 컨텍스트에 저장된다.
  • 일반적인 경우처럼, 트랜잭션이 커밋되는 지점까지 쿼리를 쌓아둔 후, 커밋 시 일괄적으로 데이터베이스에 전송한다.

INSERT쿼리 없이 SELECT만트로 ID값을 가져온 것을 알 수 있다.

 

실전예제 -1. 요구사항 분석과 기본 매핑


요구사항에 따라 테이블과 엔티티를 만들어주었다.

근데 해당 엔티티는 그저 테이블의 속성과 1대1 매핑해주는 것 밖에 안된다.

@Entity
@Table(name = "ORDERS")
public class Order {

    @Id @GeneratedValue
    @Column(name = "ORDER_ID")
    private Long id;

    @Column(name = "MEMBER_ID")
    private Long memberId;
    private LocalDateTime orderDate;

    @Enumerated(EnumType.STRING)
    private OrderStatus status;
}

이러한 설계는 객체지향스럽지 못한데,

Order order = em.find(Order.class, 1L);
Long memberId = order.getMemberId();

Member member = em.find(Member.class, memberId);
Member findMember = order.getMember();

예를 들어 order에서 member을 찾을 때 id값을 이용해 여러번 조회하는 로직이 필요하기 때문이다.

객체보다는 관계형 db에 맞춘 설계인 것이다. 

데이터 중심 설계의 문제점

  • 테이블의 외래키를 객체에 그대로 가져왔다.
  • 객체 그래프 탐색이 불가능하다.
  • 참조가 없으므로 UML(다이어그램)도 잘못되었다.

 

다음 시간에 연관관계 매핑을 통해 해당 문제를 해결해보자.