회원 테이블에 대한 CRUD 기능을 개발한다고 가정
class Member {
private String memberId;
private String name
}
관계형 데이터베이스는 데이터 중심적이고 집합적인 사고를 요구한다. 또한, 데이터베이스는 객체지향의 추상화, 상속, 다형성 같은 개념이 없다. 이러한 패러다임의 불일치를 개발자가 중간에서 해결해야 한다.
객체는 참조를 사용해서 연관관계를 맺고, 데이터베이스는 외래키를 통해 연관관계를 맺게 된다.
객체는 참조가 있는 방향으로만 조회가 가능하지만, 테이블은 외래 키 하나로
Member join Team
, Team join Member
둘다 가능하다.
객체 모델은 외래 키가 필요 없는 대신 참조 값이 필요하고 테이블은 참조 값이 필요 없고 외래 키가 필요한데 이를 개발자가 중간에서 변환시켜줘야 한다.
하지만, JPA를 사용한다면 개발자는 객체간의 관계를 설정하고 객체를 저장하기만 하면된다.
JPA가 알아서 참조 값을 외래키로 변환해 적절한 쿼리를 DB로 전달한다.
참조를 사용해서 연관된 객체를 찾는 것
객체는 자유롭게 객체 그래프를 탐색할 수 있다.
// 회원이 소속된 팀
Team team = member.getTeam();
// 회원 주문의 주문 아이템
OrderItem orderItem = member.getOrder().getOrderItem();
하지만 SQL을 직접 다루면 처음 짠 SQL에 따라 객체 그래프 탐색이 어디까지 가능한지 정해지게 된다.
이렇게 되면, 개발자는 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수 없게 된다. 결국 또 SQL문을 일일이 확인해야 한다.
하지만, JPA를 사용하면 연관된 객체를 사용한 시점에 적절한 쿼리를 날려준다.
hashCode()
)equals()
)//MemberDAO에 memberId를 통해 멤버를 반환하는 쿼리문이 있다 가정
Member member1 = memberDAO.getMember(memberId);
Member member2 = memberDAO.getMember(memberId);
member1
과 member2
는 객체의 동일성 비교에서 실패한다. (둘 다 새롭게 생성된 객체이므로)
JPA를 사용하면, 한 트랜잭션내에서 같은 로우에서 객체를 여러번 조회해도 동일성을 보장해준다.
Java Persistence API - 자바 진영의 ORM 기술 표준이다.
자바 애플리케이션과 JDBC 사이에서 동작한다.
객체를 자바 컬렉션에 저장하듯이 ORM 프레임워크에 저장하면 ORM 프레임워크는 적절한 쿼리를 만들어서 JDBC를 통해 DB에 전달된다. 개발자는 매핑 방법만 ORM 프레임워크에게 알려주면 된다.
자바 진영에는 이러한 ORM 프레임워크가 다양한데, 그 중 하이버네이트 프레임워크를 가장 많이 사용
하이버네이트는 거의 모든 패러다임 불일치 문제를 해결해준다.
JPA는 인터페이스로 자바 ORM 기술에 대한 API 표준 명세이다.
따라서, JPA를 사용하려면 인터페이스 구현체가 있어야하고 그것이 하이버네이트와 같은 프레임워크이다.
이러한 JPA라는 표준 덕분에 특정 구현 기술에 대한 의존도가 줄어들고 다른 기술로 유연하게 이동할 수 있다.
JDBC API를 통해 쿼리를 직접 작성했다면, 한 트랜잭션내에서 DB와 두 번 통신 했을 것이다.
JPA를 사용하면 조회 쿼리를 DB에 한번에 보내고, 조회한 회원 객체는 재사용된다.
String memberId = "abc";
Member member1 = jpa.find(memberId);
Member member2 = jpa.find(memberId);
엔티티를 영구 저장하는 환경
엔티티 매니저를 통해 엔티티를 저장하거나 조회하면, 엔티티 매니저는 영속성 컨텍스트에 보관하고 관리한다.
em.persist(member);
: 엔티티 매니저를 통해 엔티티를 영속성 컨텍스트에 저장
em.detach(member)
, em.clear()
, em.close()
em.remove(member)
영속성 컨텍스트 내부에는 key로 식별자값, value로 인스턴스를 가지는 Map이 있고 이를 1차 캐시라고 부른다.
영속성 컨텍스트에 데이터를 저장하고 조회하는 모든 기준은 식별자 값 즉, 데이터베이스 기본 키 값이다.
em.find(엔티티 클래스, 식별자 값);
으로 엔티티를 조회할 수 있다.
1차 캐시에 엔티티가 있으면 1차 캐시에서 바로 반환
1차 캐시에 엔티티가 없으면 DB에서 엔티티를 조회한 후 엔티티를 1차 캐시에 저장해 영속 상태로 만들어 반환한다. 이렇게 되면 이후 해당 엔티티를 조회할 때 1차 캐시에서 불러올 수 있으므로 성능이 향상된다.
//해당 인스턴스가 영속성 컨텍스트에 관리되는 영속 상태라면 1차 캐시에 있는 같은 인스턴스를 반환한다.
Member a = em.find(Member.class, 1);
Member b = em.find(Member.class, 1);
엔티티를 영속화할 때 엔티티 매니저는 1차 캐시에 엔티티를 저장하고, 쓰기 지연 저장소에 Insert 쿼리를 모아둔다. 이때, 엔티티 매니저는 트랜잭션이 커밋되기 직전까지 쓰기 지연 저장소에 Insert
쿼리를 모아둔다.
마지막으로, 트랜잭션이 커밋되면 엔티티 매니저는 flush
를 통해 쓰기 지연 저장소에 저장된 쿼리를 DB로 보낸다. 이렇게 영속성 컨텍스트의 변경 내용을 DB에 동기화한 후에 실제 DB 트랜잭션을 커밋하게 된다.
쿼리를 한번에 보냄으로써 성능을 최적화할 수 있다.
트랜잭션 커밋이 발생하면 우선 flush
를 호출해서 데이터베이스와의 동기화 작업을 진행한다.
이때, 1차 캐시에 저장된 엔티티 인스턴스와 스냅샷을 비교한다. (스냅샷에는 영속성 컨텍스트 저장 시점의 최초 상태가 저장되어 있다.) 다른 부분에 대한 update
쿼리를 만들어 쓰기 지연 저장소에 저장한다. 쓰기 지연 저장소의 쿼리를 db로 보내고 최종적으로 트랜잭션 커밋이 종료된다. 이를 변경감지라고 한다.
당연한 얘기지만, 변경감지는 영속상태의 엔티티에만 적용된다.
그리고, 변경감지를 통해 update 쿼리가 생성되고 db로 전송될 때, 쿼리문을 보면 사실상 모든 필드에 업데이트가 적용된 것을 확인할 수 있다. 이렇게 하면 수정 쿼리가 항상 동일하므로 재사용이 가능하다.
만약, @DynamicUpdate
어노테이션을 엔티티 클래스에 적용한다면 수정된 데이터만 사용해서 Update
쿼리를 전송하는 것이 가능하다. ( 보통 칼럼이 30개 이상이면 동적 수정 쿼리를 사용하는 것이 더 빠르다고 한다.)
Member memberA = em.find(Member.class, "memberA");
em.remove(memberA);
엔티티 등록과 마찬가지로 즉시 삭제되는 것이 아닌 쓰기 지연 저장소에 삭제 쿼리가 저장되었다가 커밋이 발생하면 flush
를 호출해서 db와 동기화되면서 쿼리가 db로 전송된다.
영속성 컨텍스트의 변경 내용을 DB에 동기화하는 작업
em.flush()
로 직접 호출영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 상태
영속성 컨텍스트가 지원하는 어떤 기능도 동작하지 않는다.
em.detach(entity)
em.clear()
em.close()
준영속 상태의 엔티티를 영속 상태로 변경하려면 병합을 사용할 수 있다. (em.merge(entity)
)
준영속 상태의 엔티티를 받아서 그 정보로 새로운 영속 상태의 엔티티를 반환한다.
(출처 : 자바 ORM 표준 JPA 프로그래밍)병합은 파라미터로 넘어온 엔티티의 식별자 값으로 조회해서 없으면 새로 생성해서 병합한다.
병합을 사용하면 파라미터로 넘어온 엔티티의 모든 필드 값으로 속성이 변경된다. 따라서 병합 시 값이 없으면 null
로 업데이트 할 위험도 있다. (실무에서는 잘 사용 안함)