프로젝트/아카이뷰
[Spring boot] JPA(Java Persistence API)
cks._.hong
2024. 1. 13. 19:38
JPA(Java Persistence API) 왜 궁금했을까❓
SSAFY 1학기 프로젝트인 Share Your Trip을 할 때, MyBatis를 사용해서 DB에 Query를 날리곤 했는데 타입이나 변수명 등 Java 객체와 불일치 하는 경우가 많아 오류가 많이 발생했다. 꼼꼼하게 확인하며 개발을 하면 문제가 없을 것이지만 짧은 시간안에 많은 작업들을 하다보니 놓치는 부분이 생기기 마련이였다. 이러한 부분을 보완할 수 있는 ORM 기술이 있다는 것을 확인하였고 그 중 JPA를 학습해보려고 한다.
JPA란 ❓
JPA는 자바 진영에서 ORM 기술 표준으로 사용되는 인터페이스 모음을 뜻한다.
실제적으로 구현된 것이 아닌 구현된 클래스와 매핑을 해주기 위해 사용되는 라이브러리이다.
JPA를 구현한 오픈 소스로는 Hibernate가 있다.
ORM(Object-Relational Mapping)
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 저장소에 쌓인다.
- transaction.commit()이 실행되면 쓰기 지연 SQL 저장소에 쌓여있던 쿼리 명령어가 DB에 전달된다. (flush)
- DB에서 전달받은 쿼리를 실행하고 결과를 저장한다. (commit)
쓰기 지연을 사용하는 이유가 뭘까 ❓
- 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 저장소에 저장
- 이후 쓰기 지연 SQL 저장소에 있는 쿼리를 DB로 전달