JPA(Java Persistence API)는 자바 ORM 기술에 대한 API 표준이다.
ORM이란 ‘Object Relational Mapping’의 약자로 객체와 관계형 데이터베이스를 매핑해주는 것을 말한다.
그렇다면 ORM 기술을 왜 나왔는지와 관계형 데이터베이스의 문제점에 대해서 알아보도록 하자.
학생에 관한 데이터를 관리하는 Student 클래스가 있고, 학생 데이터를 관계형 데이터베이스에서 관리하기 위해서 우리는 SQL문을 사용한다. SQL 중심 개발의 문제점은 우리가 흔히 부르는 CRUD(INSERT, DELETE, UPDATE, SELECT)를 작성해서 객체를 관계형 데이터베이스에 넣어주고 가져오는 작업을 하는 것이다. 즉, 자바 객체를 SQL을 통해 데이터베이스에서 관리하게 하고, 데이터베이스에 저장된 데이터를 자바 애플리케이션에서 사용하려면 SQL을 통해 다시 자바 객체로 변환하는 반복적인 작업을 해야 한다. 다시 한번 더 말해, 백엔드 코드에 집중해야 할 개발자가 SQL을 매핑하는 역할을 반복적으로 해야한다는 것이다.
또한 객체와 관계형 데이터베이스의 패러다임이 불일치 한다는 것이 가장 큰 문제점이다. 자바는 객체 지향 패러다임으로 만들어졌고, 관계형 데이터베이스는 데이터를 정규화해서 잘 보관하는 것을 목표로 한다. 객체를 데이터베이스에 넣기 위해서는 SQL문을 통해 변환하여 저장해야 하고, 데이터베이스에서 객체를 다시 꺼내오기 위해서는 복잡한 SQL문을 작성해야 한다. 결국 객체를 데이터 전달용으로 사용할 뿐 객체지향적으로 프로그래밍을 할 수가 없다. 이러한 문제점을 해결하기 위해 등장한 것이 ORM 기술이다.
객체는 객체지향적으로, 데이터베이스는 패러다임에 맞게 잘 정규화하여 설계한다. 그리고 ORM은 중간에서 2개를 매핑해주는 역할을 한다. 이를 통해 개발자는 코드를 조금 더 객체적으로 설계하고 비즈니스 로직에만 집중할 수 있게 된다.
JPA는 위에서 설명한 ORM 기술의 표준 명세로 자바에서 제공하는 API이다. 즉, JPA는 인터페이스고 이를 구현한 대표적인 구현체로는 Hibernate, EclipseLink, DataNucleus, OpenJpa, TopLink 등이 있다. 이 중에 JPA 인터페이스를 구현한 가장 대표적인 오픈소스가 Hibernate(하이버네이트)이다.
애플리케이션 개발을 위해 데이터베이스로 오라클(Oracle)을 사용해 개발했다고 가정해보자. 오라클을 오픈소스인 MariaDB로 변경하려면 데이터베이스마다 쿼리문이 다르기 때문에 쿼리문 전체를 수정해주어야 한다. 따라서 처음 선택한 데이터베이스를 변경하기 어렵다는 것이다. 하지만 JPA는 추상화한 데이터 접근 계층을 제공한다. 설정 파일에 어떤 데이터베이스를 사용할 것인지 알려주면 얼마든지 데이터베이스를 변경할 수 있다.
JPA를 사용하면 데이터베이스 설계 중심의 패러다임을 가지고 객체지향적으로 설계가 가능하다. 이를 통해 조금 더 직관적이고 비즈니스 로직에 집중할 수 있다.
데이터베이스 테이블에 새로운 컬럼이 추가되었을 경우, 해당 테이블의 컬럼을 사용하는 DTO 클래스의 필드도 모두 변경해야 한다. JPA에서는 테이블과 매핑된 클래스에 필드만 추가한다면 쉽게 관리가 가능하다. 또한 SQL문을 직접 작성하지 않고 객체를 사용해 동작하기 때문에 유지보수 측면에서도 좋고 재사용성도 증가한다는 장점이 있다.
통계 처리와 같이 복잡한 쿼리를 사용할 경우에는 SQL문을 사용하는 게 나을 수도 있다. JPA에서는 Native SQL을 통해 기존의 SQL문을 사용할 수도 있지만, 그러면 특정 데이터베이스에 종속된다는 단점이 생긴다. 이를 보완하기 위해서 SQL과 유사한 기술인 JPQL을 지원한다.
객체 간의 매핑을 잘못했을 경우 성능 저하가 발생할 수도 있으며, 자동으로 생성되는 쿼리가 많기 때문에 개발자가 의도하지 않는 쿼리로 인해 성능이 저하되기도 한다.
JPA를 제대로 사용하려면 알아야 할 것들이 많기 때문에 학습하는데 시간이 다소 소요된다. 개인적으로는 관계혀여 데이터베이스를 충분히 알아야 JPA를 사용할 수 있기 때문에 관계형 데이터베이스를 학습한 후에 JPA를 사용하길 권장한다.
엔티티(Entity)란 데이터베이스의 테이블에 대응하는 클래스이다. @Entity라는 어노테이션이 붙은 클래스는 JPA에서 관리하며 엔티티라고 한다. 데이터베이스에 student 테이블을 만들고, 이에 대응되는 student.java 클래스를 만들어서 @Entity 어노테이션을 붙이면 이 클래스가 엔티티가 되는 것이다. 클래스 자체나 생성한 인스턴스도 엔티티라고 부른다.
엔티티 매니저 팩토리(Entity Manager Factory)는 엔티티 매니저 인스턴스를 관리하는 주체이다. 애플리케이션을 실행할 때 한 개만 만들어지며 사용자로부터 요청이 오면 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성한다.
엔티티 매니저(Entity Manager)란 영속성 컨텍스트에 접근해 엔티티에 대한 데이터베이스 작업을 제공한다. 내부적으로 데이터베이스 커넥션을 사용해 데이터베이스에 접근한다. 엔티티 매니저의 몇 가지 메소드를 살펴보자.
JPA를 이해하기 위해서는 영속성 컨텍스트(Persistence Context)를 이해하는 것이 가장 중요하다. 엔티티를 영구 저장하는 환경으로 엔티티 매니저를 통해 영속성 컨텍스트에 접근한다.
JPA는 왜 이렇게 영속성 컨텍스트를 사용해야 할까? 바로 애플리케이션과 데이터베이스 사이에 영속성 컨텍스트라는 중간 계층을 만들었기 때문이다. 이렇게 중간 계층을 만들면 버퍼링, 캐싱 등을 할 수 있는 장점이 있다.
영속성 컨텍스트에는 내부에는 엔티티를 저장하는 저장소인 1차 캐시가 존재하며 Map<KEY, VALUE>로 저장된다. entitymanager.find() 메소드 호출 시 영속성 컨텍스트의 1차 캐시를 조회한다. 엔티티가 존재할 경우 해당 엔티티를 반환하고, 엔티티가 없으면 데이터베이스에서 조회 후 1차 캐시에 저장 및 반환한다. 1차 캐시는 일반적으로 트랜잭션을 시작하고 종료할 때까지만 유효하다.
하나의 트랜잭션에서 같은 키값으로 영속성 컨텍스트에 저장된 엔티티를 조회하면 같은 엔티티 조회를 보장한다. 바로 1차 캐시에 저장된 엔티티를 조회하기 때문에 가능하다.
영속성 컨텍스트는 쓰기 지연 SQL 저장소가 존재한다. entitymanager.persist()를 호출하면 1차 캐시에 저장되는 것과 동시에 쓰기 지연 SQL 저장소에 SQL문이 저장된다. 이렇게 SQL을 쌓아두고 트랜잭션을 커밋하는 시점에 저장된 SQL문들이 flush되면서 데이터베이스에 반영된다. 이렇게 모아서 보내기 때문에 성능상 이점을 볼 수 있는 것이다.
JPA는 1차 캐시에 데이터베이스에서 처음 불러온 엔티티의 스냅샷 값을 갖고 있다. 그리고 1차 캐시에 저장된 엔티티와 스냅샷을 비교 후 변경된 내용이 있다면 UPDATE SQL문을 쓰기 지연 SQL 저장소에 담아둔다. 그리고 데이터베이스에 커밋 시점에 변경 내용을 자동으로 반영한다. 즉, 따라 update문을 호출할 필요가 없다.