데이터베이스에서 데이터를 관리하려면 SQL을 사용해야 한다. Java로 작성한 애플리케이션은 JDBC API를 사용해 SQL을 데이터베이스에 전달한다.
SQL을 직접 다루면 개발자가 DAO(Data Access Object)를 정의할 때 객체지향 애플리케이션과 데이터베이스 사이에서 SQL과 JDBC API를 사용해 SQL 쿼리를 하드코딩하고, 반환된 ResultSet 객체에 대해 직접 모델 객체로의 변환 작업을 수행한다. 이 과정에서 SQL과 JDBC API 코드의 중복이 매우 심하게 발생한다.
요구사항이 변경되어 데이터베이스 테이블에 필드가 하나 추가될 때 그로 인해 파생되는 SQL 수정 작업이 매우 많다. 또한 테이블 간의 연관 관계가 추가되었을 때 연관된 테이블의 객체를 조회(member.getTeam())했을 때의 결과가 SQL 쿼리에 의존하기 때문에 DAO를 직접 확인해 어떤 SQL인지 확인해 보아야 한다. 이런 경우에 개발자는 Member, Team 등 엔터티를 신뢰하고 사용할 수 없게 되고 그렇기 때문에 매번 DAO 코드를 분석하며 어떤 쿼리가 실행되는지 파악해야 한다.
즉, 애플리케이션에서 SQL을 직접 다룰 때는 다음과 같은 문제점이 발생한다.
JPA는 다음과 같은 CRUD API를 제공한다.
jpa.persist(member);jpa.find(Member.class, memberId);member.setName("new name");member.getTeam();개발자는 JPA API만 사용하면 되고, SQL 쿼리는 JPA가 자동적으로 생성한다.
객체지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 메커니즘을 제공한다. 그렇기 때문에 현대적인 애플리케이션들은 대부분 객체지향 언어로 개발한다.
비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다. 하지만 객체로 모델링된 도메인 모델이 다른 클래스를 상속받거나 참조하고 있으면 영속성 계층에 저장할 때 복잡성으로 인한 문제가 발생한다.
Java는 이런 문제까지 고려해서 객체를 파일로 저장하는 직렬화 기능과 저장된 파일을 객체로 복구하는 역직렬화 기능을 지원한다. 하지만 이 방식은 직렬화된 객체를 검색하는 것이 어렵기 때문에 보통 영속성 계층은 파일 시스템이 아닌 데이터베이스와 상호작용하게 된다.
하지만 객체와 관계형 데이터베이스는 지향성이 다르므로 둘의 기능과 표현 방법이 다르다. 이를 객체와 관계형 데이터베이스의 패러다임 불일치 문제라고 한다. 따라서 객체 구조를 테이블에 저장하는 데는 한계가 있다.

객체는 상속 기능을 갖고 있지만 테이블은 상속 기능을 지원하지 않는다. 일부 DBMS는 상속 기능을 지원하지만 객체의 상속 기능과는 다른 것이다.

그나마 데이터베이스 모델링 시 슈퍼타입-서브타입 관계를 사용하면 객체 상속과 가장 유사한 형태로 테이블 설계가 가능하다. 하지만 이를 Java 애플리케이션에서 구현하고자 한다면 객체를 분리하는 것과 더불어 삽입 쿼리를 매번 두 개씩 작성하고, 조회 시에도 조인 결과로 반환된 ResultSet 객체를 종합해서 저장해야 할 것이다.
JPA는 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결한다. 개발자는 자바 컬렉션에 객체를 저장하듯이 JPA를 사용해 객체를 저장하면 된다.
삽입 시에는 jpa.persist(album);과 같이 코드를 작성하면 JPA는 다음과 같은 SQL 쿼리를 생성하여 객체를 두 테이블에 나누어 저장한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
조회 시에는 Album album = jpa.find(Album.class, albumId);와 같이 코드를 작성하면 JPA는 다음과 같은 쿼리를 생성하여 조인 결과를 객체로 매핑한다.
SELECT I.*, A.*
FROM ITEM I
JOIN ALBUM A ON I.ITEM_ID = A.ITEM_ID

객체는 참조를 사용해 다른 객체와 연관 관계를 가지고, 참조에 접근해서 연관된 객체를 조회한다. 반면 테이블은 외래 키를 사용해서 다른 테이블과 연관 관계를 가지고, 조인을 사용해서 연관된 테이블을 조회한다.
객체는 참조가 있는 방향으로만 조회할 수 있다. 즉, 위와 같은 연관 관계에서 member.getTeam()과 같은 연산은 허용되지만, team.getMember()와 같은 연산은 허용되지 않는다. 그러나 테이블은 외래 키와 조인을 사용해 양방향으로 조회가 가능하다.
패러다임 불일치를 해결하기 위해 Member 클래스에 테이블의 TEAM_ID와 같은 타입의 teamId 필드를 정의하여 테이블 구조와 일치시키면 간단한 삽입 및 조회 쿼리는 작성하기 쉬워질지 몰라도 member.getTeam() 연산으로 Team 객체를 즉시 불러오는 일은 불가능하게 된다. 이러한 방식을 따르면 객체지향의 장점을 잃어버리게 된다.
패러다임 불일치를 해결하기 위해 Member 클래스에 Team 타입 필드를 정의하면 반대로 간단한 삽입 및 조회 쿼리가 까다로워지게 된다. 객체 모델은 외래 키가 필요하지 않고 테이블은 참조가 필요하지 않기 때문에 개발자가 중간에서 변환을 통해 중재해야 한다.
JPA는 member.setTeam(team);, jpa.persist(member); 같은 코드로 패러다임의 불일치 문제를 매우 쉽게 해결해 준다. JPA가 team 참조를 외래 키로 변환해 적절한 INSERT 쿼리를 데이터베이스에 전달한다. 또한 객체를 조회할 때 외래 키를 참조로 변환하는 일도 Team team = member.getTeam(); 같은 코드로 해결해 준다.
위와 같은 문제들은 SQL을 직접 다루더라도 많은 코드를 작성하면 어느 정도 극복할 수 있는 문제들이었다.

객체에서 연관 관계를 조회할 때는 member.getTeam()과 같이 참조를 사용해 연관된 객체의 참조를 찾으면 되는데, 이를 객체 그래프 탐색이라 한다. 위 그림과 같은 객체 그래프에서 Member 객체로부터 Item 객체를 찾으려면 member.getOrder().getOrderItem().getItem()과 같은 코드를 작성할 수 있다.
SELECT M.*, T.*
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
그런데 만약 데이터베이스에서 Member 객체를 조회할 때 위와 같은 쿼리로 조회했다면 객체 그래프에서 Member로부터 Team으로 이어지는 탐색밖에 할 수 없게 된다. member.getOrder()의 결과가 null일 것이다. 객체는 객체 그래프를 자유롭게 탐색할 수 있어야 하지만 SQL에 의존하게 된다.
SQL을 직접 다루면 처음 실행하는 SQL 쿼리에 따라 객체 그래프의 탐색 범위가 결정된다. 객체 그래프를 탐색하기 전 DAO 코드의 SQL 쿼리문을 직접 확인해야 한다. 이는 엔터티가 SQL 쿼리에 논리적으로 종속되어서 발생하는 문제이다.
JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 이 기능은 실제 객체를 사용하는 시점까지 데이터베이스 조회를 미룬다고 하여 지연 로딩(Lazy loading)이라 한다. 이를 통해 객체는 JPA를 신뢰하고 객체 그래프를 자유롭게 탐색할 수 있다. 사용자가 getOrder() 메서드에 별도의 코드를 추가하지 않아도 JPA가 지연 로딩을 투명(transparent)하게 처리한다.
그런데 만약 비즈니스 로직에서 Member 객체를 사용할 때마다 Order 객체도 함께 사용한다면 Member 객체를 조회하는 시점에 조인을 사용해 Member, Order 테이블을 함께 조회하는 것이 효과적이다. JPA는 이를 위해 즉시 로딩(Eager loading)을 지원한다. 이는 Member 객체를 조회할 때 연관된 Order 객체도 조인을 사용해 즉시 조회하는 것을 의미한다.
데이터베이스는 기본 키의 값으로 각각의 행(row, record)을 구분한다. 반면 객체는 동일성(identity, ==) 비교와 동등성(equality, equals()) 비교라는 두 가지 비교 방법이 있다. 따라서 데이터베이스의 같은 행을 조회했더라도 다른 인스턴스에 저장되어 유효한 동일성 비교가 불가능할 수 있다. 패러다임 불일치를 해결하기 위해 같은 행이라면 같은 주소의 인스턴스로 매핑하는 메커니즘을 구현하기는 쉽지 않다.
JPA는 같은 트랜잭션 내에서 같은 객체가 조회되는 것을 보장하는 1차 캐시 메커니즘으로 유효한 동일성 비교를 보장한다. 그러나 객체 비교는 분산 환경이나 트랜잭션이 다른 상황까지 고려하면 더 복잡해진다.

JPA(Java Persistence API)는 Java 진영의 ORM 기술 표준으로, 애플리케이션과 JDBC API 사이에서 인터페이스 역할을 한다.
ORM(Object-Relational Mapping)은 객체와 관계형 데이터베이스를 매핑한다는 뜻이다. ORM 프레임워크는 개발자에게는 컬렉션에 객체를 저장하듯이 API를 호출하게 하고, 객체와 테이블을 매핑하여 적절한 SQL 쿼리를 생성함으로써 패러다임 불일치 문제를 개발자 대신 해결해준다.

jpa.persist(member); 코드는 위와 같이 동작한다.

jpa.find(memberId); 코드는 위와 같이 동작한다.
ORM 프레임워크는 단순히 SQL을 자동 생성할 뿐만 아니라 다양한 패러다임의 불일치 문제들을 해결하여 정교한 객체 모델링을 돕는다. 덕분에 개발자는 데이터 중심적인 관계형 데이터베이스를 사용하면서도 객체지향적인 애플리케이션 개발에 집중할 수 있다.
Java 진영에는 거의 대부분의 패러다임 불일치 문제를 해결해 주는 성숙한 ORM 프레임워크인 하이버네이트(Hibernate)가 존재한다.

과거 Java 진영은 Enterprise Java Beans(EJB)라는 기술 표준을 만들고 그 안에 엔터티 빈이라는 ORM 기술을 포함했다. 하지만 복잡성과 낮은 기술 성숙도, 그리고 자바 엔터프라이즈(J2EE) 애플리케이션 서버에서만 동작한다는 낮은 호환성 등의 문제를 갖고 있었다.
이때 EJB의 단점을 극복한 하이버네이트(hibernate.org) 오픈소스 ORM 프레임워크가 등장했고, 결국 EJB 3.0에서 하이버네이트를 기반으로 JPA라는 새로운 Java ORM 기술 표준이 만들어졌다.
JPA는 Java ORM 기술에 대한 API 표준 명세(인터페이스 집합)이다. 따라서 JPA를 사용하려면 JPA의 계약을 구현한 ORM 프레임워크를 선택해야 한다. 현재 JPA 2.1을 구현한 ORM 프레임워크는 하이버네이트, EclipseLink, DataNucleus가 있다. 이 중 하이버네이트가 가장 대중적이다. JPA 표준은 일반적이고 공통적인 기능의 모음이기 때문에 표준을 먼저 이해하고 필요에 따라 개별 구현체가 제공하는 고유 기능을 알아가면 된다.
JPA 버전별 특징은 다음과 같다.