김영한님의 자바 ORM 표준 JPA 프로그래밍 강의와 책을 바탕으로 진행되는 스터디입니다
섹션 1. JPA 소개
SQL 중심적인 개발의 문제점
SQL 의존적인 개발을 피하기 어렵다.
- 무한 반복, 지루한 코드
- INSERT… UPDATE… SELECT… DELETE…
- 자바 객체 → SQL, SQL → 자바 객체 무한 반복
- 만약 객체에 필드가 추가되면?
- 해당 객체를 사용하는 모든 CRUD의 SQL을 수정해주어야 한다
패러다임의 불일치
- 객체를 저장하는 다양한 방법이 있지만 최선의 방법은 관계형 데이터베이스
- 개발자가 SQL Mapper가 된다.
- 객체와 관계형 데이터베이스의 차이가 많아 패러다임의 불일치로 오는 비용이 많음.
- 상속
- 객체는 상속 관계를 가지고 있지만 테이블은 상속이 없다.
- 그나마 슈퍼타입 - 서브타입 관계가 유사
- 그러나 슈퍼타입 - 서브타입 관계를 사용하더라도 객체의 저장 시 SQL문을 두 개를 사용해야 함
- 데이터를 조회해서 객체에 저장하려고 하면 JOIN문을 직접 작성해서 객체를 생성해야 함
- 만약 자바 컬렉션에 데이터를 저장한다면 타입 고민 없이 그냥 컬렉션을 사용하면 됨
- 연관관계
- 객체는 참조를 사용하고 테이블은 외래 키를 사용
- 객체는 참조 방향으로만 조회가 가능(단방향)
- 테이블은 외래 키 하나로 양방향 조회가 가능(MEMEBER JOIN TEAM / TEAM JOIN MEMBER)
- 객체를 테이블에 맞춰서 모델링 할 경우 객체지향의 장점을 이용할 수 없음
- 객체의 저장 / 조회는 편리함
- 객체 참조를 활용 불가능
- 객체지향적으로 모델링 할 경우 객체의 조회가 어려움
- JOIN문을 사용해 연관된 필드를 조회하고 이를 매핑해주는 과정이 필요
- 객체 그래프 탐색이 어려움
-
객체 그래프 탐색: 객체에서 참조를 사용해서 연관관계를 탐색
-
SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해짐
-
따라서 SQL문을 직접 보기 전 까지는 객체 그래프 탐색을 신뢰할 수 없음
class MemberService {
...
public void process() {
Member member = memberDAO.find(memberId);
member.getTeam();
member.getOrder().getDelivery();
}
}
-
그렇다고 매번 모든 연관관계를 전부 로딩하기에는 비용이 많이 듦
-
결국 상황에 따라 같은 객체에 대한 조회 쿼리를 여러 개 작성해야 함
- 동일성 비교의 문제
- 같은 SQL문을 사용해서 같은 트랜잭션에서 같은 객체를 조회하더라도 동일성 비교에는 실패함
- 데이터 타입
- 데이터 식별 방법
JPA 소개
JPA란?
- Java Persistence API는 자바 진영의 ORM 표준 기술
- ORM(Object-Relational Mapping)은 객체 - 관계형 데이터베이스 간 매핑을 해주는 프레임워크
- JPA는 애플리케이션과 JDBC 사이에서 동작
- SQL을 직접 작성할 필요 없이 JPA를 통해 객체를 직접 CRUD 할 수 있음
- 패러다임의 불일치 해결
JPA는 자바 ORM 기술에 대한 API 표준 명세
- 쉽게 말하면 인터페이스의 모음
- JPA를 사용하려면 JPA의 구현체 프레임워크(ex 하이버네이트)를 사용해야 함
JPA를 사용해야 하는 이유
- 생산성
- 자바 컬렉션에 객체를 저장하듯이 사용 가능
- SQL문을 작성하고 JDBC API를 사용하는 지루하고 반복적인 일을 JPA가 대신함
- 특히 데이터 수정 시
setter
하나만으로 처리 가능
- 유지보수
- 기존에는 필드 변경시 관련된 모든 SQL을 수정해야 했음
- JPA 사용 시 필드만 추가하면 됨
- 패러다임의 불일치 해결
- 앞서 살펴본 패러다임의 불일치 문제를 해결해줌
- 자바 컬렉션을 사용하듯이 데이터베이스를 사용할 수 있음
- 성능
- 애플리케이션과 데이터베이스 사이에 계층이 하나 더 있으므로 최적화가 가능
- 캐싱, 지연 로딩 / 즉시 로딩 …
섹션 2.
섹션 3. 영속성 관리
영속성 컨텍스트
엔티티 매니저
- 엔티티를 저장하고, 수정하고, 삭제하고, 조회하는 등 엔티티와 관련된 모든 일을 처리(엔티티 관리자)
- 여러 스레드가 동시에 접근하면 동시성 문제 발생
- 데이터베이스 연결이 꼭 필요한 시점까지 커넥션을 얻지 않고 트랜잭션을 시작할 때 커넥션을 획득
엔티티 매니저 팩토리
- 엔티티 매니저를 만드는 공장
- 만드는 비용이 크므로 보통 한 개만 만들어서 애플리케이션 전체에서 공유
- 엔티티 매니저를 만드는 비용은 거의 들지 않음
- 여러 스레드가 동시에 접근해도 안전
영속성 컨텍스트(persistence context)
영속성(persistence)은 데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성을 의미한다.
- 엔티티 매니저를 통해 영속성 컨텍스트에 접근하고 영속성 컨텍스트를 관리 가능
엔티티의 생명 주기
- 비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
- 엔티티 객체를 생성한 뒤 저장하지 않은 순수한 객체 상태
- 영속(managed): 영속성 컨텍스트에 저장된 상태
- 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장 → 영속성 컨텍트스가 관리하는 상태
- 조회 메서드를 사용해 조회한 엔티티도 영속 상태
- 준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
- 영속 상태의 엔티티를 영속성 컨텍스트가 관리하지 않는 상태
em.detach
를 호출하면 준영속 상태
em.close
를 호출해서 영속성 컨텍스트를 닫거나 em.clear
를 호출해서 영속성 컨텍스트를 초기화해도 해당 영속성 컨텍스트가 관리하던 엔티티는 준영속 상태가 됨.
- 삭제(removed): 삭제된 상태
영속성 컨텍스트의 특징
- 영속성 컨텍스트는 엔티티를 식별자 값으로 구분 → 식별자 값 반드시 필요
- JPA는 트랜잭션을 커밋하는 순간 영속성 컨텍스트에 저장된 엔티티를 데이터베이스에 반영 → 플러시(flush)
- 영속성 컨텍스트의 장점
- 1차 캐시
- 동일성 보장
- 트랜잭션을 지원하는 쓰기 지연
- 변경 감지
- 지연 로딩
엔티티 조회
영속성 조회
- 엔티티 내부에는 영속 상태의 엔티티가 저장되는 1차 캐시가 존재
- 쉽게 말해
@Id
로 매핑한 식별자를 key로, 엔티티 인스턴스를 value로 가지는 Map이 존재
em.find
메서드로 조회
- 첫 번째 파라미터는 엔티티 클래스의 타입
- 두 번째 파라미터는 조회할 엔티티의 식별자 값
- 우선 1차 캐시에서 엔티티를 찾고 없으면 데이터베이스에서 조회
- 데이터베이스에서 엔티티를 조회할 때 1차 캐시에 저장한 후 영속 상태의 엔티티 반환
- 이후에 해당 엔티티를 조회하면 1차 캐시에서 조회 가능
- 영속 엔티티의 동일성 보장
em.find
는 반복 호출해도 1차 캐시에 있는 같은 엔티티 인스턴스 반환
- 따라서 두 인스턴스의 동일성 비교(==)는
true
(엔티티의 동일성 보장)
엔티티 등록
em.persist
로 등록
- 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장 x
- 내부 쿼리 저장소에
INSERT SQL
를 모아둠
- 엔티티 영속화 시 1차 캐시에 저장하면서 등록 쿼리 생성
- 트랜잭션을 커밋하면 영속성 컨텍스트를 플러시하고 등록된 데이터를 데이터베이스에 반영
- 쓰기 지연이 가능
- 데이터 저장 즉시 쿼리를 날리지 않고 쿼리를 저장한 뒤 트랜잭션의 마지막에 한번에 쿼리를 보냄
- 성능의 최적화 가능
엔티티 수정
em.update
메서드는 존재하지 않음
- JPA는 엔티티의 변경을 감지
- 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용
- 즉, 준영속과 같은 상태의 엔티티는 값을 변경해도 반영 x
- 엔티티를 영속성 컨텍스트에 보관할 때 최초 상태를 복사해서 저장(스냅샷)
- 플러시 시점에 스냅샷과 엔티티를 비교해 변경된 엔티티를 감지하고 UPDATE 쿼리 발생
- JPA의 기본 전략은 엔티티의 모든 필드를 업데이트
- 데이터 전송량이 증가하지만 수정 쿼리가 항상 같아서 캐싱 가능
- 하이버네이트 확장 기능을 활용해 동적 UPDATE SQL 생성 전략 사용 가능
엔티티 삭제
em.remove
로 삭제
- 삭제하기 전에 먼저
em.find
로 삭제하려는 엔티티를 조회해야 함
- 엔티티 삭제 역시 삭제 쿼리를 쓰기 지연 SQL 저장소에 등록
- 영속성 컨텍스트에서는 바로 제거 됨
- 재사용하지 말고 가비지 컬렉션의 대상이 되도록 두는 것이 좋음
- 플러시 되는 시점에 삭제 쿼리 전송
플러시
- 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영
- 영속성 컨텍스트의 모든 엔티티를 스냅샷과 비교해서 변경 감지
- 수정된 엔티티의 수정 쿼리를 생성해 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송(CRUD 모두)
- 영속성 컨텍스트를 플러시하는 방법
em.flush
직접 호출
- 트랜잭션 커밋 시 자동 호출
- JPQL 쿼리 실행 시 자동 호출
- JPQL은 SQL로 변환되기 때문에, JPQL 실행 이전에 영속성 컨텍스트의 내용을 반영해야 함
- 단,
find
메서드는 플러시 실행하지 않음
- 플러시 모드를 직접 지정 가능
FlushModeType.AUTO
: 커밋이나 쿼리를 실행 시 플러시(기본값)
FlushModeType.COMMIT
: 커밋할 때만 플러시
- 주의) 이름은 플러시지만 영속성 컨텍스트에 보관된 엔티티를 지우는 의미가 아님! 동기화의 의미
준영속
준영속 상태
- 영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것이므로 준영속 상태의 엔티티는 영속성 컨텍스트가 제공하는 기능 사용 불가
em.detach
- 특정 엔티티를 준영속 상태로 만듦
- 메서드가 호출되면 1차 캐시부터 쓰기 지연 SQL 저장소까지 해당 엔티티를 관리하기 위한 모든 정보가 제거
em.clear
- 영속성 컨텍스트를 초기화
- 해당 영속성 컨텍스트가 관리하는 모든 엔티티를 준영속 상태로 만듦
em.close
- 영속성 컨텍스트를 종료
- 해당 영속성 컨텍스트가 관리하는 모든 엔티티를 준영속 상태로 만듦
준영속 상태의 특징
- 거의 비영속 상태에 가까움
- 영속성 컨텍스트가 제공하는 어떤 기능도 동작하지 않음
- 식별자 값을 가지고 있음
- 준영속이 되기 위해서는 한 번 영속상태였어야 하므로 식별자를 가짐
- 지연 로딩 불가능
- 지연 로딩: 실제 객체 대신 프록시 객체를 로딩해두고 해당 객체를 사용할 때 영속성 컨텍스트를 통해 데이터를 불러오는 방법
- 준영속 상태는 영속성 컨텍스트가 관리 x → 지연 로딩 시 문제가 발생
em.merge
- 준영속 상태의 엔티티를 다시 영속 상태로 변경하기 위해 사용
- 준영속 상태의 엔티티를 받아 새로운 영속 상태의 엔티티를 반환
- 기존의 준영속 엔티티를 참조하던 변수는 사용할 필요가 없음
- 준영속 엔티티를 참조하던 변수를 영속 엔티티를 참조하도록 변경하는 것이 안전
- 비영속 엔티티도 영속 상태로 만들 수 있음
- 식별자로 엔티티 조회가 가능하면 불러서 병합
- 조회가 불가능하면 새로 생성해서 병합
- 즉, save or update 기능을 수행