ORM vs SQL Mapper vs JDBC를 통해 JPA가 무엇인지 알게되었다. 이게 실제 코드를 통해 어떻게 사용하는지 확인해보자. 해당 글은 흔히 사용하는 Hibernate를 기준으로 설명할 것이다.

엔티티 매핑

@Entity
@Table(name = "station")
public class Station {

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

    @Column(name = "name", nullable = false)
    private String name;

    protected Station() {
    }

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

    public Station(String name) {
        this(null, name);
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void changeName(String name) {
        this.name = name;
    }
}

위 코드가 내가 처음으로 만들어 본 엔티티 매핑 코드다. 이 코드에 관련 있는 개념을 하나씩 정리해본다.

  • @Entity
    • 해당 클래스가 테이블과 매핑할 엔티티를 나타낸다는 어노테이션이다. Hibernate의 경우는 Java Reflection을 사용해서 객체를 생성하기 때문에 인자가 없는 생성자가 필요하다. 인자가 없는 생성자는 private일 수 있지만, 런타임 프록시 생성과 효율적인 데이터 검색을 위해서는 public 또는 protected가 되어야한다.
    • name이라는 속성이 있다. 기본값은 클래스 이름을 그대로 사용한다. 다른 패키지에 이름이 같은 엔티티 클래스가 있다면 이름을 지정해서 충돌하지 않도록 해야된다.
  • @Table
    • 엔티티와 매핑할 테이블을 지정하는 어노테이션이다.
    • 아래와 같은 속성이 있다.

      속성 기능 기본값
      name 매핑할 테이블 이름 엔티티 이름을 사용한다.
      catalog catalog 기능이 있는 데이터베이스에서 catalog를 매핑한다.  
      schema schema 기능이 있는 데이터베이스에서 schema를 매핑한다.  
      uniqueConstaraints DDL 생성 시에 유니크 제약조건을 만든다. 2개 이상의 복잡한 유니크 제약 조건도 만들 수 있다. 참고로 이 기능은 스키마 자동 생성 기능을 사용해서 DDL을 만들 때만 사용된다.  
  • @Id
    • 해당 필드가 엔티티의 기본 키로 매핑하는 어노테이션이다.
  • @GeneratedValue
    • 기본 키 자동 생성 전략을 지정하는 어노테이션이다. 데이터베이스마다 기본 키를 생성하는 방식이 서로 다르므로 이 문제를 해결하기위해 이 어노테이션이 필요하다.
    • IDENTITY
      • 기본 키 생성을 데이터베이스에 위임하는 전략이다. 주로 MySQL, PostgreSQL, SQL Server, DB2에서 사용한다. 예를 들어 MySQL의 AUTO_INCREMENT 기능은 데이터베이스가 기본 키를 자동으로 생성해준다. 이 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달된다. 따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다.
    • SEQUENCE
      • 데이터베이스 시퀀스를 사용해서 기본 키를 생성한다. 데이터베이스 시퀀스란 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트다. 이 전략은 시퀀스를 지원하는 오라클, PostgreSQL, DB2, H2 데이터베이스에서 사용할 수 있다. 이 전략은 IDENTITY와 다르게 em.persist()가 호출될 때 먼저 데이터베이스 시퀀스를 사용해 식별자를 조회한다. 조회한 식별자를 엔티티에 할당한 후에 엔티티를 영속성 컨텍스트에 저장한다. 이후에 트랜잭션을 커밋해서 플러시가 일어나면 엔티티를 데이터베이스에 저장한다. 따라서 SEQUNCE는 쓰기 지연이 동작한다.
    • TABLE
      • 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다. 이 전략은 테이블을 사용하므로 모든 데이터베이스에 적용할 수 있다. 이 전략은 값을 조회하면서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용한다. 따라서 SEQUNCE 전략과 비교해서 데이터베이스와 한 번 더 통신하는 단점이 있다.
    • AUTO
      • 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUNCE, TABLE 전략 중에 하나를 자동으로 선택한다. 하지만 Hibernate 버전마다 자동 선택되는 전략이 달라지는 문제가 있으므로 유의해서 써야된다. 관련글
  • @Column
    • 객체 필드를 테이블 컬럼에 매핑하는 어노테이션이다. @Column 어노테이션을 생략하면 대부분 @Column 속성의 기본값이 적용된다.
    • name 속성으로 필드와 매핑할 테이블의 컬럼의 이름을 지정할 수 있다. 기본값은 객체의 필드 이름이다.
    • nullable 속성으로 null 값의 허용 여부를 설정한다. false로 설정하면 DDL 생성 시에 not null 제약조건이 붙는다. 기본값은 true다.
    • 만약 객체 필드가 자바 기본 타입일 때는 @Column 어노테이션을 생략시 not null 제약조건이 붙는다.

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

JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원한다. 위에 설명한 어노테이션 기반으로 클래스의 매핑 정보를 보고 어떤 테이블이 어떤 컬럼을 사용하는지 알아낸다. 스키마 자동 생성 기능을 사용하기 위해서는 persistence.xml에 다음 속성을 추가하면 된다.

<propertry name="hibernate.hbm2ddl.auto" value="create" />

Spring Boot의 경우에는 properties 파일로 스키마 자동 생성을 할 수 있다. 아래의 프로퍼티는 단순히 hibernate.hbm2ddl.auto의 숏컷이다.

spring.jpa.hibernate.ddl-auto=create

추가적으로 persistence.xml에 해당 프로퍼티를 추가하면 콘솔에 실행되는 DDL을 출력할 수 있다.

<property name="hibernate.show_sql" value="true"/>

Spring Boot의 경우에는 properties 파일에 다음을 입력해주면 된다.

spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true

hibernate.hbm2ddl.auto의 속성은 다음이 있다.

옵션 설명
create 기존 테이블을 삭제하고 새로 생성한다. DROP + CREATE
create-drop create 속성에 추가로 애플리케이션을 종료할 때 생성한 DDL을 제거한다. DROP + CREATE + DROP
update 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 변경 사항만 수정한다.
validate 데이터베이스 테이블과 엔티티 매핑정보를 비교해서 차이가 있으면 경고를 남기고 애플리케이션을 실행하지 않는다. 이 설정은 DDL을 수정하지 않는다.
none 자동 생성 기능을 사용하지 않는다.

스키마 자동 생성을 사용해도 될까?

운영 서버에 이 기능을 사용하면 운영 중인 데이터 베이스의 테이블이나 컬럼을 삭제할 수도 있으니 유의해야된다. 개발 환경에 따른 추천 전략은 다음과 같다.

  • 개발 초기 단계는 create 또는 update
  • 초기화 상태로 자동화된 테스트를 진행하는 개발 환경과 CI 서버는 create 또는 create-drop
  • 테스트 서버는 update 또는 validate
  • 스테이징과 운영 서버는 validate 또는 none

영속성 관리

지금까지 엔티티와 테이블을 매핑하는 설계 부분을 정리했다. 이번에는 매핑한 엔티티를 실제 어떻게 사용하는지 확인해본다.

  • 엔티티 메니저: 엔티티를 저장하고, 수정하고, 삭제하고, 조회하는 등 엔티티와 관련된 모든 일을 처리한다. 말그대로 엔티티를 관리하는 관리자다.
  • 엔티티 매니저 팩토리: 하나의 데이터베이스에 접근하기 위해 EntityManagerFactory를 하나 생성한다. 즉, 여러 개의 데이터베이스에 접근하기 위해서는 여러 개의 EntityManagerFactory를 생성하게된다. EntityManagerFactory에서 여러 EntityManager를 생성할 수 있다. EntityManagerFactory는 스레드 세이프하지만, EntityManager는 스레드 세이프하지 않기 때문에 스레드 간에 절대 공유하면 안 된다. jpa01
  • 영속성 컨텍스트: 엔티티를 영구 저장하는 환경이다. 엔티티 매니저로 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다. 영속성 컨텍스트 내에서 엔티티 인스턴스와 그의 라이프 사이클을 관리한다. 영속성 컨텍스트의 스코프는 트랜잭션 단위다. persist() 메소드를 호출하게 되면 엔티티 매니저를 사용해서 엔티티를 영속성 컨텍스트에 저장하게된다.

엔티티의 생명주기

엔티티에는 4가지 상태가 존재한다.

  • 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
  • 영속(managed): 영속성 컨텍스트에 저장된 상태
  • 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
  • 삭제(removed): 삭제된 상태

AG7Vf

영속성 컨텍스트의 특징

  • 영속성 컨텍스트는 엔티티를 식별자 값(@Id로 테이블의 기본 키와 매핑한 값)으로 구분한다. 따라서 영속 상태는 식별자 값이 있어야 한다.
  • JPA는 보통 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 새로 저장된 엔티티를 데이터 베이스에 반영하는데 이것을 flush라고 한다.
  • 영속성 컨텍스트가 엔티티를 관리하면 다음과 같은 장점이 있다.
    • 1차 캐시
    • 동일성 보장
    • 트랜잭션을 지원하는 쓰기 지연
    • 변경 감지
    • 지연 로딩

예시를 통한 영속성 컨텍스트 이해

영속성 컨텍스트는 내부에 캐시를 가지고 있는데 이것을 1차 캐시라 한다. 1차 캐시의 키는 식별자 값이다. 따라서 아래의 코드를 실행하면 아래의 그림과 같은 상태가 된다.

Member member = new Member();
member.setId("member1");
member.setUsername("회원1");

em.persist(member);

image

1차 캐시

만약 em.find(Member.class, "member1")을 호출하면 1차 캐시에 인스턴스가 있기 때문에, 데이터베이스를 조회하지 않고 메모리에 있는 1차 캐시에서 엔티티를 조회한다. em.find(Member.class, "member2")같이 1차 캐시에 없는 경우는 데이터베이스를 조회해서 엔티티를 생성 한 후, 1차 캐시에 저장한 뒤에 영속 상태의 엔티티를 반환한다.

image (1)

image (2)

동일성 보장

아래의 코드를 실행하면 true가 출력된다. 즉, 영속성 컨텍스트는 엔티티의 동일성을 보장해준다. 영속성 컨텍스트는 1차 캐시에 있는 같은 엔티티 인스턴스를 반환하기 때문이다.

Member a = em.find(Member.class, "member1");
Member b = em.find(Member.class, "member2");

System.out.println(a == b);

트랜잭션을 지원하는 쓰기 지연

엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 차곡차곡 모아둔다. 그리고 트랜잭션을 커밋할 때 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기 지연이라 한다.

EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
// 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
transaction.begin();

em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.

// 커밋하는 순간 데이터베이스에 INSERT SQL을 보낸다.
transaction.commit();

image (3)

2.

image (4)

3.

image (5)

변경 감지

엔티티에 변경사항이 memberA.setUsername("hi")같은 메서드 호출로 인해 엔티티에 변경사항이 있으면 데이터베이스에 자동으로 반영하는 기능이다. 이게 가능한 이유는 영속성 컨텍스트를 보관할 때, 최초 상태를 복사해서 저장해두기 때문이다. 이것을 스냅샷이라 한다. 플러시 시점에 스냅샷과 엔티티를 비교해서 변경된 엔티티를 찾는다.

image (6)

  1. 트랜잭션을 커밋하면 엔티티 매니저 내부에 먼저 플러시가 호출된다.
  2. 엔티티와 스냅샷을 비교해 변경된 엔티티를 찾는다.
  3. 변경된 엔티티가 있으면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 보낸다.
  4. 쓰기 지연 저장소의 SQL을 데이터베이스에 보낸다.
  5. 데이터베이스 트랜잭션을 커밋한다.

변경 감지는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용된다.

지연 로딩

지연로딩은 객체를 실제 사용할 때까지 데이터 로딩을 미루는 기능이다.

Member member = em.find(Member.class, "member1"); // 1
Team team = member.getTeam(); // 2
team.getName(); // 3
  1. 회원만 조회하고 팀은 조회하지 않는다. 대신 team 멤버변수에 프록시 객체를 넣어둔다.
    SELECT * FROM MEMBER
    WHERE MEMBER_ID = 'member1'
    
  2. team 객체는 프록시 객체다.
  3. 실제로 데이터가 필요한 순간이 되어서야 데이터베이스를 조회해서 프록시 객체를 초기화한다.
    SELECT * FROM TEAM
    WHERE TEAM_ID = 'team1'
    

참고 자료

https://docs.jboss.org/hibernate/orm/5.5/quickstart/html_single/#hibernate-gsg-tutorial-basic-entity

https://jojoldu.tistory.com/295

https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.data

https://drynod.github.io/jpa/2020/11/02/jpa.html

https://newbedev.com/hibernate-updatable-false-uuid-field-is-updated

자바 ORM 표준 JPA 프로그래밍(김영한)

댓글남기기