Search

JPA와 영속성 관리

생성일
2024/01/25 06:34
태그
spring
Database
상태
Done

JPA

JPA는 Java Persistence API의 약자로, 자바 진영의 ORM 기술 표준을 말한다.
ORM(Object-Relational Mapping) 객체는 객체 지향적으로 설계하고 관계형 데이터베이스는 관계형 데이터베이스대로 설계하면, 객체와 관계형 데이터베이스가 잘 연동이 되지 않는다. 이를 중간에서 매핑하여 객체대로 사용하면서 관계형 데이터베이스를 쉽게 사용하도록 도와주는 기술을 ORM 프레임워크라 한다. 대중적인 언어는 대부분 ORM 기술을 지원한다.
Java에는 EJB와 엔티티 빈이라는 복잡한 기술이 있는데, 이를 간단하게 사용할 수 있도록 만든게 Hibernate이다. 결국 Java에서 Hibernate를 만든 사람을 데려와 JPA라는 자바 표준 ORM을 만들었다. 오픈 소스에서 출발한 기술이기 때문에 굉장히 실용적이고 유연하다.
JPA는 표준 인터페이스로 Hibernate, EclipseLink, DataNucleus 등의 구현체를 두고 있다.
JPA를 사용하면 여러 이점을 얻을 수 있다.
SQL 중심적인 개발을 벗어나 객체 중심적인 개발로 인한 생산성, 유지보수성 향상
상속이나 연관관계, 객체 그래프 탐색, 비교 등 객체와 데이터베이스 간의 페러다임의 불일치 해결
여러 성능 최적화
같은 트랜잭션 내에서 같은 엔티티를 반환(1차 캐시와 동일성 보장)하여 약간의 성능 향상
트랜잭션을 커밋할 때까지 insert SQL을 모아서 JDBC BATCH SQL 기능을 사용해 한번에 SQL 전송하여 처리
객체가 실제 사용될 때 로딩하는 지연 로딩
기존에 DB와 통신하기 위해 SQL에 맞춰 JDBC API를 개발자가 직접 호출했었다면, JPA는 애플리케이션과 JDBC 사이에서 동작하여 코드에 맞춰 JDBC API를 대신 호출해주는 역할을 한다.
JPA를 통해 저장하게되면 JPA가 Entity를 분석 후 Insert 쿼리를 대신 작성하여 JDBC API를 호출하여, 페러다임의 불일치를 해결해준다.
JPA를 통한 조회 역시 마찬가지로, JPA가 select 쿼리를 대신 생성하고 JDBC API를 호출해 객체에 매핑해줌으로써 페러다임의 불일치를 해결한다.

JDBC 및 JPA, Hibernate와의 차이

JDBC는 여러 DB에 접근하기 위해 Java에서 제공하는 API이다.
JDBC 드라이버에서 데이터베이스와 통신을 담당하여, Oracle이나 MS SQL, MySQL 등 다양한 데이터베이스에 맞게 JDBC 드라이버를 구현해두고 제공한다. 이를 통해 개발자는 DB의 종류를 고려하지 않고 Java 코드를 작성할 수 있다.
최근에는 Spring Data JDBC, Spring Data JPA 등의 기술들이 등장하면서 JDBC API를 직접 사용하는 일은 거의 없다.
JDBC API를 통해 데이터베이스와 연결하는 작업은 TCP/IP 커넥션 연결 및 아이디, 패스워드를 통한 내부 인증 등 비용이 많이 드는 작업이기 때문에, 여러 Connection 객체를 미리 생성하여 Connection Pool에 보관하다가 애플리케이션에서 필요할 때 꺼내서 사용한다. 최근에는 HikariCP라는 가벼운 용량과 빠른 속도를 가지는 JDBC Connection Pool 프레임워크가 많이 사용된다.
JPA와 JDBC, Hibernate의 차이는 다음의 처리 흐름을 통해 쉽게 알 수 있다.
JDBC는 Oracle이나 MySQL, MS SQL 등 여러 벤더의 DB에 사용되는 SQL 방언들을 표준 SQL로 통합하여 사용할 수 있도록 지원해주는 기술이다(개별 DB마다 SQL 방언도 지원).
JPA는 Java 객체와 RDB의 관계를 고려하여 JDBC API를 통해 표준 SQL을 직접 작성하지 않도록, JDBC와 Java 사이에서 동작하여 개발자가 객체 지향적으로 개발할 수 있게 도와주는 표준 ORM 기술이다.
JPA가 인터페이스로 여러 구현체들이 구현되어 있는데, Hibernate는 그러한 JPA의 구현체 중 하나이다. Hibernate는 HQL(Hibernate Query Language)이라는 강력한 쿼리 언어를 지원하여, 상속이나 다형성, 관계 등 객체지향의 여러 강점들을 이용할 수 있도록 지원한다.

영속성 관리와 영속성 컨텍스트

우리가 데이터베이스에 접근할 때, 엔티티 매니저 팩토리에서 엔티티 매니저를 생성받아 커넥션 풀에서 커넥션을 받아서 연결 후 데이터베이스에 접근한다.
영속성 컨텍스트란 JPA의 핵심 개념으로, 엔티티를 영구적으로 저장하는 환경이라는 의미이다. 영속성 컨텍스트는 논리적인 개념으로, 엔티티 매니저를 통해 영속성 컨텍스트에 접근할 수 있다.
이러한 영속성 컨텍스트에서 관리되는 엔티티의 생명주기는 다음과 같다.
비영속(new/transient)
영속성 컨텍스트와 전혀 관계가 없는 새로운 엔티티가 생성된 상태
// 객체를 생성한 상태(비영속) Member member = new Member();
Java
복사
객체를 생성만 한 상태에서 해당 객체는 영속성 컨텍스트에서 관리 되고 있지 않는 비영속 상태이다.
영속(managed)
영속성 컨텍스트에 관리되는 상태
// 객체를 생성한 상태(비영속) Member member = new Member(); em.getTransaction().begin(); // 객체를 저장한 상태(영속) em.persist(member);
Java
복사
영속 상태가 된다고 해서 바로 쿼리가 발생하지 않는다. 말 그대로 영속성 컨텍스트 내에 해당 엔티티를 두고 관리할 뿐이다.
준영속(detached)
영속성 컨텍스트에 저장되었다가 분리된 상태
// 회원 엔티티를 영속성 컨텍스트에서 분리(준영속) em.detach(member); // 영속성 컨텍스트를 완전히 초기화 em.clear(); // 영속성 컨텍스트를 종료 em.close();
Java
복사
영속성 컨텍스트가 제공하는 여러 이점들을 사용할 수 없다.
삭제(remvoed)
영속성 컨텍스트에서 삭제된 상태
// 객체를 삭제한 상태(삭제) em.remove(member);
Java
복사
영속성 컨텍스트 내에서 엔티티 자체를 지운 상태이다.
이렇게 영속성 컨텍스트에서 엔티티를 관리하는 이유는 몇 가지 이점을 얻을 수 있기 때문이다.
1차 캐시
영속성 컨텍스트 내에서 엔티티를 1차 캐시에 저장해두고, 조회 시 영속성 컨텍스트에서 관리하는 엔티티라면 1차 캐시에서 조회한다.
영속성 컨텍스트 내에 없는 엔티티라면 데이터베이스에서 조회해 1차 캐시에 저장 후 반환한다.
하지만 이런 1차 캐시는 트랜잭션 내에서만 유지되고, 트랜잭션이 종료되면 해당 영속성 컨텍스트는 날려버리기 때문에 2차 캐시에 비해 별로 큰 이득을 가지지는 못한다.
영속성 엔티티의 동일성(identity) 보장
Member member1 = em.find(Member.class, 101L); Member member2 = em.find(Member.class, 101L); if (member1 == member2) { // 영속성 엔티티의 동일성 보장 ... }
Java
복사
1차 캐시를 통해 REPEATABLE READ 등급의 트랜잭션 격리 수준을 애플리케이션 차원에서 제공
트랜잭션을 지원하는 쓰기 지연 및 지연 로딩(Lazy Loading)
이처럼 em.persist()를 호출한다고 하여 바로 SQL을 날리는 것이 아니라,
트랜잭션이 완료된 시점에 SQL을 통해 쓰기를 수행한다.
변경 감지(Dirty Checking)
JPA는 커밋이나 플러시 시점에 영속성 컨텍스트 내의 엔티티와 스냅샷(값을 읽어온 최초의 시점의 엔티티 상태)을 비교한다. 비교 후 다르다면 자체적으로 update 쿼리를 생성하여 DB에 쿼리를 날리게 된다.
플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영하는 명령으로, 플러시 명령을 받으면 변경 감지(Dirty Checking) 후 수정된 엔티티를 반영하기 위한 SQL을 쓰기 지연 SQL 저장소에 등록하고 이후 데이터베이스에 쿼리를 전송한다. 다시말해, 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스 동기화 하는 작업이다.
플러시는 em.flush()를 통해 직접 호출하는 방법이 있고, 트랜잭션 커밋 혹은 JPQL 쿼리 실행 시 자동으로 플러시가 호출되게 된다.
JPQL 쿼리 시 플러시가 자동으로 호출되는 이유
em.persist(memberA); em.persist(memberB); em.persist(memberC); // JPQL 실행 query = em.createQuery("select m from Member m", Member.class); List<Member> members = query.getResultList();
Java
복사
위와 같은 상황에서 memberA, B, C의 경우에는 영속화만 되어있기 때문에 DB에는 저장되어 있지 않다. 하지만 JPQL은 쿼리를 바로 수행해버리기 때문에, 이처럼 검색이 안될 수 있는 상황을 피하기 위해 플러시를 실행한다.
em.setFlushMode(FlushModeType.COMMIT)으로 커밋할 때만 플러시하도록 설정할 수도 있다. (기본값 : FlushModeType.AUTO - 커밋이나 쿼리를 실행할 때 플러시)

참고