JPA(Java Persistence API)란?
JPA(Java persistence API)는 자바 진영에서 ORM(Object-Relational-Mapping) 기술 표준으로 사용되는 인터페이스의 모음입니다. 그 말은 즉, 실제적으로 구현된것이 아니라 구현된 클래스와 매핑을 해주기 위해 사용되는 프레임워크입니다. JPA를 구현(구현체)한 대표적인 오픈소스로는 Hibernate가 있습니다.
ORM(Object Relational Mapping)이란?
ORM은 Object Relational Mapping의 약자로 객체와 데이터베이스의 관계를 매핑해주는 도구를 말합니다. 프로그래밍 언어의 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해주는 도구이며, 프로그래밍 언어의 객체와 관계형 데이터베이스 사이의 중계자(통역자) 역할을 합니다.
ORM의 장점
- SQL문이 아닌 Method를 통해 DB를 조작할 수 있어, 개발자는 객체 모델을 이용하여 비즈니스 로직을 구성하는데만 집중할 수 있습니다. (내부적으로는 쿼리를 생성하여 DB를 조작함. 하지만 개발자가 이를 신경 쓰지 않아도 됨)
- Query와 같이 필요한 선언문, 할당 등의 부수적인 코드가 줄어들어 각종 객체에 대한 코드를 별도로 작성하여 코드의 가독성을 높입니다.
- 객체지향적인 코드 작성이 가능합니다. 오직 객체지향적 접근만 고려하면 되기때문에 생산성이 증가합니다.
- 매핑하는 정보가 Class로 명시 되었기 때문에 ERD를 보는 의존도를 낮출 수 있고 유지보수 및 리팩토링에 유리합니다.
- 데이터베이스를 교체해야할 일이 있을 경우 쿼리를 수정해야하지만 ORM을 사용하면 쿼리를 수정할 필요가 없습니다. (특정 데이터베이스에 종속되지 않습니다.)
ORM의 단점
- 프로젝트의 규모가 크고 복잡하여 설계가 잘못된 경우, 속도 저하 및 일관성을 무너뜨리는 문제점이 생길 수 있습니다.
- 복잡하고 무거운 Query는 속도를 위해 별도의 튜닝이 필요하기 때문에 결국 SQL문을 써야할 수도 있습니다.
- 학습이 어렵습니다.
JPA를 사용하는 이유
JPA는 반복적인 CRUD SQL을 처리해줍니다. JPA는 매핑된 관계를 이용해서 SQL을 생성하고 실행하는데 개발자는 어떤 SQL이 실행될지 생각만 하면 되고, 예측도 쉽게 할 수 있습니다. 추가적으로 JPA는 네이티브 SQL이란 기능을 제공해주는데 관계 매핑이 어렵거나 성능에 대한 이슈가 우려되는 경우 SQL을 직접 작성하여 사용할 수 있습니다.
JPA를 사용하여 얻을 수 있는 가장 큰 이점은 SQL이 아닌 객체 중심으로 개발할 수 있다는 것 입니다. 이에 따라 당연히 생산성이 좋아지고 유지보수도 수월해집니다. 또한 JPA는 패러다임의 불일치(객체와 DB 테이블의 구조적 차이)도 해결했습니다. 예를 들면 JAVA에서는 부모클래스와 자식클래스의 관계 즉, 상속 관계가 존재하는데 데이터베이스에서는 이러한 객체의 상속관계를 지원하지 않습니다.(상속 기능을 지원하는 DB도 있지만 객체 상속과는 다릅니다.)
JPA의 동작 및 구조
위 그림과 같이 JPA는 JAVA 애플리케이션과 JDBC API 사이에서 동작합니다. 기존에는 개발자가 직접 JDBC API를 사용했다면 JPA에서는 개발자가 JPA를 사용하고 JPA는 JDBC API를 사용하여 DB 작업을 수행합니다.
JPA의 저장(JPA insert)
JPA의 조회(JPA find)
Persistence Context(영속성 콘텍스트)
우선 영속성 콘텍스트의 개념에 대해서 간단히 알아보겠습니다.
영속성 콘텍스트란 JPA의 내부 흐름 또는 동작 원리 그 자체를 의미합니다. 그러므로 하나의 기술이나 기능이라기보다는 논리적인 개념에 가깝습니다. 흔히 영속성 콘텍스트를 설명할 때는 "Entity를 DB에 영구적으로 저장하는 것"으로 표현하지만 영속성 콘텍스트는 단순히 DB에 객체 데이터를 저장하는 것 이상의 의미를 갖습니다.
왜냐하면 DB에 저장하는 행위는 Entity를 영속화 시키기 위한 하나의 방법에 때문입니다. 즉, 개념적으로 접근하자면 영속성 콘텍스트의 진정한 의미는 "Entity를 영속화 시키는 것"입니다.
Entity & Entity Manager
[ Entity ]
Entity는 DB에 존재하는 하나의 Table을 클래스로 구현한 것입니다. 쉽게 말해서, DB Table의 Column과 대응하는 속성을 가진 클래스를 의미합니다. 참고로 Entity 객체를 Entity라고 부릅니다.
[ Entity Manager ]
Entity Manager는 위에 설명한 Entity의 구현체(=객체)의 생명주기(=Lifecycle)를 관리합니다.
Entity Manager는 Entity 객체를 영속성 콘텍스트(Persistence Context)에 저장하여 관리합니다. 즉, Entity와 Persistence Context 사이에 존재하는 역할입니다.
Entity Lifecycle
Spring Bean에도 생명주기가 있듯이 Entity 또한 생명주기를 갖습니다. Entity의 생명주기는 다음과 같습니다.
[ Transient(=new, 비영속 상태) ]
Entity가 영속성 콘텍스트와 전혀 관계가 없는 상태로 Entity가 생성되고 Entity Manager에게 등록되지 않은 상태를 말합니다.
public class Jpa {
public static void main(String args[]) {
User userA = new User();
userA.setId("userA");
userA.setName("김철수");
}
}
[ Managed(= 영속 상태) ]
Entity가 영속성 콘텍스트에 의해 관리되는 상태 즉, Entity가 영속성 콘텍스트에 등록되어 있는 상태를 의미합니다.
public class Jpa {
public static void main(String args[]) {
User userA = new User();
userA.setId("userA");
userA.setName("김철수");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin(); // 트랜잭션 시작
em.persist(userA); // 영속성 컨텍스트에 Entity를 저장 => 영속 상태
}
}
* 영속 상태가 되었다고 DB에 바로 저장되는 것이 아닙니다. DB에 저장되는 시점은 persist()를 commit()한 다음, 해당 Transaction이 온전히 종료됐을때 입니다.
[ Detached(= 준영속 상태) ]
Entity가 영속성 콘텍스트에 저장되었다가 분리된 상태를 말합니다.
public class Jpa {
public static void main(String args[]) {
User userA = new User();
userA.setId("userA");
userA.setName("김철수");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin(); // 트랜잭션 시작
em.persist(userA); // 영속성 컨텍스트에 등록 => 영속 상태
em.detach(userA); // 영속성 컨텍스트에서 분리됨 => 준영속 상태
}
}
[ Removed(= 삭제 상태) ]
DB에 저장되어있는 Entity가 삭제된 상태 즉, DB로부터 삭제된 Entity의 상태를 의미합니다.
Entity Manager의 역할
위에서 Entity Manager는 Entity와 Persistence Context 사이에 존재한다고 이야기했습니다. 그렇다면 왜 Entity Manager는 중간에 위치하는 것일까요?
쉽게 말하면 1차적으로 끝날 과정에 하나의 단계를 추가함으로써 중간 처리단계를 만들 수 있기 때문입니다. 즉, 객체 데이터를 Persistence Context에 바로 저장하명 데이터에 대한 후처리를 진행할 수 없기 때문입니다.
[ 1차 캐시 ]
Entity Manager는 1차 캐시를 가지고 있습니다. Entity Manager를 통해 Entity를 Persistence Context에 등록하면 1차 캐시에 등록 됩니다. 1차 캐시는 다음과 같이 동작합니다.
public class Jpa {
public static void main(String args[]) {
User userA = new User();
userA.setId("userA");
userA.setName("김철수");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(userA);
em.find(userA); // Entity Manager의 1차 캐시에서 Entity를 조회
}
}
Entity Manager를 통해 Entity를 조회하는 경우, DB를 조회하기 전에 1차 캐시를 우선 조회합니다. 이처럼 1차 캐시를 우선 조회하는 경우 응답속도가 훨씬 빠르다는 장점이 있습니다. 만약 1차 캐시에 존재하지 않는 다른 Entity를 조회한다면, 1차 캐시를 조회한 후 DB에 접근하여 조회합니다. 이후 DB에서 반환된 값을 Entity Manager의 1차 캐시에 저장하고 나서 클라이언트에게 반환합니다.
* Entity Manager는 하나의 트랜잭션이 시작하면서 생성되고 종료되면서 삭제됩니다. 그러므로 1차 캐시는 하나의 트랜잭션 안에서만 사용되는 캐시이고 애플리케이션 전체가 공유하는 캐시는 2차 캐시라고 부릅니다.
[ 영속된 Entity의 동일성 보장 ]
Entity Manager는 소속된 트랜잭션과 동일한 Lifecycle을 갖습니다. 그러므로 하나의 트랜잭션 동안 동일한 객체를 여러번 조회하면 이를 같은 값으로 처리합니다.
public class Jpa {
public static void main(String args[]) {
User userA = new User();
userA.setId("userA");
userA.setName("김철수");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(userA);
User a = em.find(userA);
User b = em.find(userA);
System.out.println(a == b); // true 반환
}
}
자바의 관점에서 살펴보면 a와 b는 서로 다른 주소 값을 갖는 객체입니다. 하지만 JPA는 하나의 트랜잭션 안에서 조회되는 동일한 객체에 대해서 같은 객체로 처리합니다. 즉, 동일성이 보장됩니다.
이 동일성 또한 Entity Manager의 1차 캐시 덕분에 가능합니다. 동일한 SQL을 반복해서 수행하면 DB로부터 값을 조회하지 않고, 1차 캐시에서 조회하기 때문에 가능한 것입니다.
[ 쓰기 지연 ]
commit()을 수행하기 전까지 Entity Manager는 SQL을 작성하지도 DB에게 전달하지도 않습니다. 이 기능을 쓰기 지연이라고 부릅니다.
public class Jpa {
public static void main(String args[]) {
User userA = new User();
userA.setId("userA");
userA.setName("김철수");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction(); // 트랜잭션 생성
transaction.begin(); // 트랜잭션 시작
em.persist(userA);
em.persist(userB);
transaction.commit(); // SQL이 DB에게 전달되는 시점
}
}
위의 코드가 진행되는 과정을 설명하면 다음과 갖습니다.
- em.persist(userA)가 실행됩니다.
- INSERT 쿼리가 생성되며 Persistence Context 내부의 쓰기 지연 SQL 저장소에 쌓입니다.
- em.persist(userB)가 실행됩니다.
- INSERT 쿼리가 생성되며 Persistence Context 내부의 쓰기 지연 SQL 저장소에 쌓입니다.
- transaction.commit()이 실행됩니다.
- 쓰기 지연 SQL 저장소에 쌓여있던 쿼리 명령어가 DB에게 전달됩니다. -> flush
- DB에서 전달받은 쿼리 명령어를 실행하고 결과를 저장합니다. -> commit
쓰기 지연 기능을 사용하는 이유는 DB와의 네트워킹 횟수에 있습니다.
실제로 DB에서 한번의 커밋은 하나의 트랜잭션을 의미합니다. 즉, DB의 값이 변경되는 작업의 처리 단위를 의미하는 것 입니다.
쿼리 하나당 한번의 커밋을 수행한다는 것은 10개의 쿼리를 수행하기 위해 10번의 커밋을 필요로한다는 것입니다. 이를 백엔드의 관점에서 보면 10번의 쿼리를 수행하기 위해 10번의 DB통신을 필요로 한다는 것이고, 10개의 쿼리를 처리하기 위해 10개의 트랜잭션(=Entity Manager)를 생성하고 10번의 commit을 수행하는 것 입니다.
만약 1만개의 요청으로 1만개의 쿼리를 수행한다고 하면 DB와 1만번의 통신을 필요로 하지만 쿼리를 10개씩 묶어서 처리한다면 DB와 천번의 통신을 필요로 하게 됩니다. 즉, 네트워킹 횟수가 현저히 줄어들게 되고 이로 인해 서비스 또는 시스템의 부하가 감소하게 됩니다.
이처럼 다수의 쿼리를 묶어서 처리하는 기능으로 JDBC Batch가 있으며, Hibernate에서는 hibernate.jdbc.batch_size 옵션을 이용하여 설정할 수 있습니다.
[ Entity 수정 및 변경 감지(Dirty Checking) ]
public class Jpa {
public static void main(String args[]) {
User userA = new User();
userA.setId("userA");
userA.setName("김철수");
EntityManagerFactory emf = Persistence.createEntityManagerFactory("JPA이름");
EntityManager em = emf.createEntityManager();
EntityTransaction transaction = em.getTransaction();
transaction.begin();
User user1 = em.find(User.class, "userA"); // DB에 저장된 userA를 찾는다.
user1.setName("HELLO");
transaction.commit();
}
}
우선 DB에서 userA라는 값을 가진 객체를 조회합니다. -> em.find(User.class, "userA");
그리고 조회된 객체의 값을 변경 합니다. -> user1.setName("홍길동");
값을 변경한 다음 em.persist(user1); 를 실행해야 되는 것 아닌지 의문이 들수 있지만 그렇지 않습니다.
JPA의 핵심은 DB의 데이터를 객체처럼 다루도록 하는 것 입니다. 즉, 자바 collections에 데이터를 저장하고 수정하는 것처럼 DB의 데이터를 다루도록 합니다.
예를 들어, 배열에 저장된 값을 변경하고 다시 해당 값을 배열에 저장하지 않는 것처럼 말입니다. 배열에 저장된 값을 변경하면 그것을 끝으로 배열은 변경된 값을 유지합니다. 이와 똑같은 원리로 이해하면 됩니다.
위의 코드를 실행하면 JPA 내부적으로 update 쿼리가 생성됩니다.
어떻게 이것이 가능할까요? 결론부터 이야기하자면 1차 캐시를 사용하기 때문입니다.
위에서 DB의 데이터를 조회하는 경우, 조회된 데이터를 1차 캐시에 저장한 후 클라이언트에게 반환한다고 설명했습니다. 그럼 위의 예시 코드에서 "userA"를 조회한 시점에서 DB로부터 반환된 객체의 값이 1차 캐시에 저장됩니다. 해당 객체가 DB로부터 가장 최근에 조회된 값을 1차 캐시의 snapshot 속성에 저장합니다.
transaction.commit()이 호출되면 DB에게 쿼리를 전달하기 전에 JPA 내부에서는 flush()가 호출됩니다. flush()가 호출된 시점에서 user1의 값과 1차 캐시에 저장된 snapshot을 비교하여 데이터의 변경을 감지하는 것입니다. 이 비교 과정에서 데이터의 수정이나 변경이 감지되면 update 쿼리가 생성되고 쓰기 지연 SQL 저장소에 저장됩니다. 이후 쓰기 지연 SQL 저장소에 쌓이 쿼리를 DB에게 전달합니다.
[ flush() ]
flush()는 영속성 콘텍스트의 변경 내용을 DB에 반영하는 것입니다. 즉, 영속성 콘텍스트가 가지고 있는 SQL을 DB에 전달하는 것입니다.
flush() 역할 :
- 변경 감지(Dirty Checking)
- 변경된 Entity의 내용을 쓰기 지연 SQL 저장소에 등록
- 쓰기 지연 SQL 저장소에 등록되어 있는 쿼리를 DB에게 전달
flush() 사용방법 :
- 직접 호출 방법 -> em.flush();
- 트랜잭션 커밋(플러시 자동호출) -> em.commit();
- JPQL 쿼리 실행(플러시 자동호출) -> em.createQuery();
[ 준영속 상태 ]
준영속 상태란 Persistence Context에 의해 관리되던 Entity가 더이상 관리 받지 않는 상태를 말합니다. 즉, Entity가 영속성 콘텍스트로부터 떨어져 나온 분리된 상태입니다.(=detached) 그러므로 준영속 상태의 Entity는 영속성 콘텍스트가 제공하는 기능을 사용할 수 없습니다.
준영속 상태가 되는 방법 :
- 특정 Entity의 상태
- 영속성 콘텍스트 초기화 -> em.clear();
- 영속성 콘텍스트 종료 -> em.close();
반복적인 CRUD SQL을 작성하고 객체를 SQL에 매핑하는데 시간을 보내기에는 우리의 시간이 너무 아깝다. 이미 많은 자바 개발자들이 오랫동안 비슷한 고민을 해왔고 문제를 해결하려고 많은 노력을 기울여왔다. 그리고 그 노력의 결정체가 바로 JPA다. JPA는 표준 명세만 570페이지에 달하고, JPA를 구현한 Hibernate는 이미 10년 이상 지속해서 개발되고 있으며, 핵심 모듈의 코드 수가 이미 십만 라인을 넘어섰다. 귀찮은 문제들은 이제 JPA에게 맡기고 더 좋은 객체 모델링과 더 많은 테스트를 작성하는데 우리의 시간을 보내자. 개발자는 SQL Mapper가 아니다.
- 출처 : 자바 ORM 표준 JPA 프로그래밍 / 저자:김영한 -
reference
'Back-End > Spring' 카테고리의 다른 글
[Spring] JPA Entity Mapping (0) | 2024.07.16 |
---|---|
[Spring] JPA 프로젝트 생성 및 예제 실습 -1- (0) | 2024.07.11 |
[Spring] DispatcherServlet이란? (0) | 2024.06.24 |
[Spring] MVC Pattern (0) | 2024.06.24 |
[Spring] Spring Framework란? (0) | 2024.06.09 |