SSAFY 1학기 프로젝트인 Share Your Trip을 할 때, MyBatis를 사용해서 DB에 Query를 날리곤 했는데 타입이나 변수명 등 Java 객체와 불일치 하는 경우가 많아 오류가 많이 발생했다. 꼼꼼하게 확인하며 개발을 하면 문제가 없을 것이지만 짧은 시간안에 많은 작업들을 하다보니 놓치는 부분이 생기기 마련이였다. 이러한 부분을 보완할 수 있는 ORM 기술이 있다는 것을 확인하였고 그 중 JPA를 학습해보려고 한다.
JPA란 ❓
JPA는 자바 진영에서 ORM 기술 표준으로 사용되는 인터페이스 모음을 뜻한다. 실제적으로 구현된 것이 아닌 구현된 클래스와 매핑을 해주기 위해 사용되는 라이브러리이다. JPA를 구현한 오픈 소스로는 Hibernate가 있다.
ORM(Object-RelationalMapping)
ORM이란 이름에서도 알 수 있듯이 어플리케이션의 객체와 RDB의 테이블과 매핑시켜주는 것이라고 할 수 있다. RDB 테이블에 객체를 영속화 시키는 것이라고도 말할 수 있다.
SQL문을 이용해서 조작하지 않고 Method를 이용하여 DB를 조작함으로써 개발자는 비즈니스 로직에 집중할 수 있다.
매핑 정보가 객체에 명시되어 있어 ERD 의존도를 낮출 수 있어 유지보수와 리팩토링을 손쉽게 도와줄 수 있다.
MyBatis를 사용할 때에 비해 비교적 적은 양의 코드로 가독성을 높일 수 있다.
설계가 잘못될 경우 속도가 저하될 수 있으며 일관성을 무너뜨릴 수도 있다.
동적 쿼리의 경우 별도의 튜닝이 필요하여 결국 SQL 문을 사용해야 할 수도 있다.
왜 JPA를 사용해야 할까 ❓
- JPA는 반복적인 CRUD를 직접 처리하여 개발자가 어떤 SQL을 실행할지만 선택하게하여 생산성을 높인다. - JPA는 네이티브 SQL을 제공하여 관계 매핑이 어렵거나 성능 저하를 개선할 수 있다. - JPA는 패러다임 불일치를 해결하여 연관, 상속 등의 관계에서도 사용할 수 있다.
JPA 구성 요소
Entity Manager Factory - Entity Manager 인스턴스를 관리하는 주체로 사용자 요청 발생 시 1개의 Entity Manger를 생성
Entity Manager - Persist Context에 접근한 뒤, 내부적으로 DB에 연결하고 사용자 요청을 처리한다. 또한, Entity 객체를 Persist Context에 저장하여 관리하는 역할
Entity - DB에 존재하는 하나의 Table의 column과 대응되는 속성을 가진다.
Persist Context - 영속성 컨텍스트로 Entity를 영구적으로 저장한다. DB에 저장하는 행위를 하지만 이는 Entity를 영속화 시키기 위한 수단으로 볼 수 있다.
JPA 동작 원리
사용자 요청이 들어오면 Entity Manager Factory가 Entity Manager를 생성하고 맵핑한다.
Entity Manager는 Persist Context에 접근하여 사용자 요청에 대해 처리한다.
내부적으로 데이터베이스 커넥션을 사용하여 DB에 접근하여 요청을 처리하게 된다.
Entity Lifecycle
비영속 상태(New)
Entity가 Persist Context와 관계 없는 상태
Entity가 생성되었지만 Entity Manager에 의해 등록되지 않은 상태
public class Jpa {
public static void main(String args[]) {
Person chanhong = new Person();
chanhong.setId("chanhong");
chanhong.setName("박찬홍");
}
}
영속 상태(Managed)
Entity가 Persist Context에 등록되어 관리되는 상태
영속 상태가 되었더라도 persist()를 commit하고 transaction이 종료 되었을 때 DB에 반영되게 된다.
public class Jpa {
public static void main(String args[]) {
Person chanhong = new Person();
chanhong.setId("chanhong");
chanhong.setName("박찬홍");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin(); // 트랜잭션 시작
em.persist(chanhong); // 영속성 컨텍스트에 Entity를 저장 => 영속 상태
}
}
준영속 상태(Detached)
Entity가 Persist Context에서 저장되었다가 분리된 상태
public class Jpa {
public static void main(String args[]) {
Person chanhong = new Person();
chanhong.setId("chanhong");
chanhong.setName("박찬홍");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin(); // 트랜잭션 시작
em.persist(chanhong); // 영속성 컨텍스트에 등록 => 영속 상태
em.detach(chanhong); // 영속성 컨텍스트에서 분리됨 => 준영속 상태
}
}
삭제 상태(Removed)
DB에 저장되어 있던 Entity가 삭제된 상태
Entity Manager의 역할
Entity Manager는 Entity와 Persist Context 사이에서 존재하게 되는데 그 이유는 1차적으로 끝나는 단계에 하나의 단계를 추가함으로써 중간 처리 단계를 만들 수 있기 때문이다.
1차 캐시
Entity Manager는 1차 캐시를 가지는데 Entity를 등록하면 우선적으로 1차 캐시에 등록하게 된다.
Entity Manager를 통해 Entity를 조회하는 경우 우선적으로 1차 캐시에서 조회하고 없다면 DB에 접근하여 조회한다.
조회 이후에는 Persist Context에 해당 Entity를 저장하고 Client에게 반환한다.
1차 캐시는 Entity Manager에 존재하므로 요청이 끝나면 사라지게 된다. 어플리케이션 전체가 공유하는 2차 캐시도 존재한다.
public class Jpa {
public static void main(String args[]) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(chanhong);
em.find(chanhong); // Entity Manager의 1차 캐시에서 Entity를 조회
}
}
영속된 Entity의 동일성 보장
Entity Manager는 소속된 Transaction과 동일한 Life Cycle을 가진다.
하나의 트랜잭션 동안 동일한 객체를 여러 번 조회한다면 동일한 값으로 처리하게 된다.
Java의 관점에서는 다른 객체로 인식을 하지만 JPA에서는 동일한 객체로 처리함으로써 동일성이 보장된다.
public class Jpa {
public static void main(String args[]) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(userA);
Person a = em.find(chanhong);
Person b = em.find(chanhong);
System.out.println(a == b); // true 반환
}
}
쓰기 지연
commit을 수행하기전까지 Entity Manager는 SQL을 DB에 반영하지 않는 것을 쓰기 지연이라고 한다.
public class Jpa {
public static void main(String args[]) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction(); // 트랜잭션 생성
transaction.begin(); // 트랜잭션 시작
em.persist(chan9784);
em.persist(hong9764);
transaction.commit(); // SQL이 DB에게 전달되는 시점
}
}
em.persist(chan9784)가 실행되면 INSERT 쿼리가 생성되며 Persist Context 내부의 쓰기 지연 SQL 저장소에 쌓인다.
em.persist(hong9764)가 실행되면 INSERT 쿼리가 생성되며 Persist Context 내부의 쓰기 지연 SQL 저장소에 쌓인다.
- DB에서 하나의 commit은 하나의 transaction을 의미하며 DB의 작업 처리 단위를 뜻한다. - 쿼리 하나당 하나의 commit을 한다는 것은 5개의 쿼리를 수행하기 위해서는 5번의 commit을 해야한다는 말이다. - 백엔드 측면에서 봤을 때, DB와 5번의 커넥션이 이뤄져야 하며 5개의 트랜잭션을 처리하기 위해 5개의 Entity Manager가 생성되야 한다. - 5개의 쿼리를 한 번에 묶어서 DB와 한 번만 통신하여 처리할 수 있다면 네트워킹 횟수가 줄어들 것이고 이는 성능적인 측면에서 효율적일 것이다. - 이러한 기능을 지원하기 위해 JDBC Batch가 존재하며 Hibernate에서는hibernate.jdbc.batch_size 옵션을 통해 설정할 수 있다.
Dirty Checking
public class Jpa {
public static void main(String args[]) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
Person chan9784 = em.find(Person.class, "chan9784"); // DB에 저장된 chan9784를 찾는다.
chan9784.setName("박찬홍");
transaction.commit();
}
}
DB에서 "chan9784" 값을 가지는 객체를 조회하고 1차 캐시에 저장
chan9784 객체의 이름을 "박찬홍"으로 변경
JPA 내부적으로 update 코드가 생성
transaction.commit()을 호출하면 DB에 쿼리를 전달하기 전에 JPA 내부에서는 flush()
이 과정에서 변경 및 수정이 감지되면 update 쿼리가 생성되고 쓰기 지연 SQL 저장소에 저장