개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

기초개념 › JPA

엔티티

엔티티는 무엇이고, 일반 클래스와 무엇이 다른가

엔티티는 데이터베이스 테이블과 짝이 되는 자바 클래스이고, 그 객체 하나가 테이블의 한 행과 짝이 됩니다. 코드로 보면 class로 선언한 평범한 자바 클래스인데, 일반 클래스와 다른 곳은 두 군데입니다. 클래스 위의 @Entity는 이 클래스의 객체를 테이블에 저장한다는 표시이고, 필드 위의 @Id는 그 필드가 기본 키(행을 구별하는 열)라는 표시입니다. 이 둘이 있으면 JPA(여기서는 Hibernate)가 클래스의 필드를 보고 테이블의 열과 짝을 짓고, em.persist로 객체를 넘겨 두면 커밋할 때 INSERT 문을 대신 만들어 실행합니다. 우리가 SQL을 쓰지 않습니다. 여기서 em은 JPA에 저장을 시키는 EntityManager 객체입니다. 이 실습은 두 표시를 하나씩 지워 가며 같은 저장 코드를 실행해, 테이블이 만들어지는지와 INSERT 문이 나가는지를 봅니다.

실행으로 대조 · Hibernate 7.4.5와 H2 2.4.240에서 여덟 가지 조합을 소스째 컴파일해 JPA를 실제로 시작하고 저장해 대조

이 실습에 나오는 개념

엔티티
데이터베이스 테이블의 행에 대응하는 자바 객체다. @Entity를 붙인 클래스로 정의하며 식별자(@Id)를 가진다. JPA는 엔티티의 상태 변화를 추적해 그에 맞는 SQL을 실행한다.
@Entity
@Entity는 클래스 위에 붙여 "이 클래스의 객체는 데이터베이스 테이블의 한 행으로 저장된다"고 표시하는 애너테이션이다. JPA 명세는 엔티티 클래스가 이 애너테이션을 갖거나 XML 설정 파일에 엔티티로 선언되어야 한다고 정하며, 클래스는 인자 없는 생성자(public 또는 protected)를 가져야 하고 final이어서는 안 된다. 이름을 따로 적지 않으면 클래스 이름이 엔티티 이름이 되고, 테이블 이름도 기본으로 이 이름을 따른다. @Entity가 없는 클래스는 JPA에 등록해도 엔티티로 받아들여지지 않으므로, 그 클래스의 객체를 em.persist로 넘기면 오류가 난다.
@Id
@Id는 엔티티의 필드(또는 getter) 위에 붙여 "이 값이 기본 키"라고 표시하는 애너테이션이다. JPA 명세는 모든 엔티티에 기본 키가 있어야 한다고 정하며, 한 필드로 이루어진 단순한 기본 키는 그 필드에 @Id를 붙여 선언한다. 기본 키는 엔티티의 객체 하나를 구별하는 값이므로, 객체를 저장한 뒤에는 바꾸지 않아야 한다. @Id를 필드에 붙이면 JPA가 필드로 값을 읽고 쓰고(필드 접근), getter에 붙이면 getter로 읽고 쓴다(프로퍼티 접근). @Entity가 붙은 클래스에 @Id가 없으면 JPA는 시작할 때 오류를 내고 멈춘다.
JPA, Hibernate, Spring Data JPA
JPA는 자바의 ORM 표준 명세로, 어떤 기능을 어떻게 사용할지 정한 규칙(인터페이스)이다. Hibernate는 그 명세를 실제로 구현한 라이브러리다. Spring Data JPA는 JPA를 더 쉽게 쓰도록 리포지토리 인터페이스를 제공하는 스프링의 모듈이며 내부적으로 JPA(Hibernate)를 사용한다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. 프로그램이 시작되면서 JPA(여기서는 Hibernate)에 Member 클래스를 알려 주고, JPA가 이 클래스를 읽습니다. 이 등록하는 줄과 DB에 연결하는 설정은 화면에서 줄였습니다. 우리가 쓴 코드 가운데 필드를 열에 짝짓는 줄은 없습니다. 짝짓기는 JPA가 클래스의 표시를 보고 합니다.
  2. JPA가 Member 클래스 위에서 @Entity를 찾습니다. 있으므로 저장할 클래스로 받아들입니다. 이어서 필드를 읽어, @Id가 붙은 id를 기본 키로 삼고 나머지 필드(name)를 기본 키가 아닌 열로 삼습니다.
  3. JPA가 이 짝짓기대로 H2 DB에 테이블을 만듭니다. 테이블 이름은 클래스 이름을 따라 Member이고, 열은 필드마다 하나씩 id, name입니다. 이 실습은 JPA가 시작할 때 테이블을 만들도록 설정했고(hibernate.hbm2ddl.auto=create-drop), 우리는 CREATE TABLE도 쓰지 않았습니다. 아직 행은 하나도 없습니다.
  4. em.getTransaction().begin(): 저장 작업을 시작합니다. 트랜잭션은 작업을 한 묶음으로 처리하는 단위이고, 자세한 것은 따로 다룹니다. 아직 INSERT 문은 나가지 않았습니다.
  5. new Member(1L, "김철수"): Member 클래스로 객체를 만듭니다. 이 객체는 아직 평범한 자바 객체이고, JPA는 이 객체를 모릅니다.
  6. em.persist(...): JPA가 이 객체를 넘겨받습니다. 이 줄이 끝난 시점에 나간 INSERT 문은 아직 0번입니다. 객체를 넘겼다고 바로 DB에 쓰이는 것은 아닙니다. INSERT가 언제 나가는지는 "영속성 컨텍스트" 실습에서 다룹니다.
  7. em.getTransaction().commit(): JPA가 INSERT 문을 만들어 DB에 보냅니다. 이 실행에서 Hibernate 7.4.5가 만든 문장은 "insert into Member (name,id) values (?,?)"입니다. 각 ?에는 앞의 열 이름 순서대로 객체의 필드 값이 들어갑니다. 우리가 쓴 코드에는 INSERT라는 글자가 없습니다. 문장의 글자는 Hibernate 버전에 따라 다를 수 있습니다.
  8. Member 테이블에 행이 하나 생겼습니다. id는 1, name은 김철수입니다. 객체 하나가 테이블의 한 행이 되었습니다.

실행 결과

코드

// 표시: 이 클래스의 객체는 테이블의 한 행으로 저장된다
@Entity
public class Member {
    // 표시: 이 필드가 기본 키다
    @Id
    private Long id;
    private String name;

    protected Member() { }

    public Member(Long id, String name) {
        this.id = id;
        this.name = name;
    }
}

// 저장하는 쪽
em.getTransaction().begin();
em.persist(new Member(1L, "김철수"));
em.getTransaction().commit();

이 모형의 전제

Hibernate 7.4.5와 H2 2.4.240(메모리 DB)에서 실행해 대조했습니다. 스프링은 쓰지 않고 Hibernate를 코드로 직접 시작했습니다(persistence.xml 없이 Configuration에 클래스를 등록). 여덟 가지 조합을 모두, 화면의 Member 소스와 같은 글(package와 import 줄만 더함)을 컴파일하고 자바 프로세스를 새로 띄워 확인했습니다. @Entity가 없는 조합에서도 같은 방식으로 Member를 등록했습니다. 화면의 "저장하는 쪽" 세 줄은 실제로는 main 안의 코드이고, EntityManager(em)를 얻는 줄, 예외를 잡아 기록하는 줄, 측정하는 줄은 화면에서 줄였습니다. 대조한 것은 JPA가 시작하다가 실패하는지와 예외의 종류, 시작 직후와 실행 뒤 H2에 있는 테이블과 열(INFORMATION_SCHEMA.COLUMNS), StatementInspector로 잡은 INSERT 문의 수와 글(persist 줄이 끝난 때와 commit 줄이 끝난 때), 실행 뒤 테이블의 행 수와 내용입니다. 테이블 이름은 H2가 MEMBER로, 열 이름은 ID, NAME, EMAIL로 저장하는데, 화면에는 읽기 쉽게 테이블 이름은 첫 글자만 대문자로, 열 이름은 소문자로 적었습니다. 열은 필드를 적은 순서대로 만들어지지 않았습니다(이 실행에서는 id, email, name 순서). 순서를 정하는 규칙은 확인하지 않았습니다. 문서가 정한 것은 Jakarta Persistence 3.2 명세 2.1절(엔티티 클래스는 @Entity를 갖고 public 또는 protected의 인자 없는 생성자를 가져야 하며 final이 아니어야 한다)과 2.4절(모든 엔티티에는 기본 키가 있어야 하고 단순한 기본 키는 필드에 @Id를 붙여 선언한다), Hibernate ORM 7.0 사용자 가이드 3.4절(Entity types)의 3.4.1, 3.4.3, 3.4.5, 3.4.6항(테이블 이름은 기본으로 엔티티 이름을 따르고, 필드에 @Id를 붙이면 필드 접근이다)입니다. 이 문서들은 WebFetch로 열었고, 돌려받은 것은 원문이 아니라 요약이어서 문장을 하나하나 원문과 대조하지는 못했습니다. 명세는 앞부분만(2.1절과 2.4절이 있는 앞 10만 자) 읽었습니다. 아래는 문서가 정한 것이 아니라 이 버전에서 관찰한 동작입니다. @Entity가 없는 클래스를 등록하면 오류 없이 시작을 마치고(JPA가 아는 엔티티는 없음) em.persist에서 IllegalArgumentException이 나며, 이때 @Id가 있어도 결과가 같습니다. @Entity는 있고 @Id가 없으면 시작할 때 AnnotationException이 나서 테이블이 만들어지지 않습니다(Hibernate 7.0 사용자 가이드 3.4.5항은 식별자를 생략하는 것이 예전에는 허용됐으나 폐지 예정이라고 하는데, 7.4.5에서는 오류였습니다). 테이블은 persist나 commit 때가 아니라 JPA가 시작할 때 만들어지며(hibernate.hbm2ddl.auto=create-drop 설정), INSERT 문은 persist 줄이 아니라 commit 줄에서 나갔습니다. 문장은 insert into Member (name,id) values (?,?) 모양이고 Hibernate 버전에 따라 글자가 다를 수 있으며, 그 INSERT가 persist 때 나가는지 commit 때 나가는지 같은 세부는 "영속성 컨텍스트" 실습에서 다룹니다. 화면의 코드에서 줄인 것은 package와 import 줄이고, 필드는 id, name, email만 다뤘습니다. 이름과 열의 짝을 바꾸는 @Table, @Column, 기본 키 값을 자동으로 만드는 @GeneratedValue, 접근자 메서드, 엔티티 클래스의 나머지 조건(final이 아니어야 한다 등)은 다루지 않았습니다. MySQL에서의 실행 대조는 하지 않았습니다.

앞 실습: 테스트 코드 · 다음 실습: 영속성 컨텍스트

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요