해당 포스팅은 인프런에서 제공하는 김영한 님의 '자바 ORM 표준 JPA 프로그래밍 - 기본편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.
지속 가능한 어플리케이션을 개발하기 위해서는 어플리케이션이 발전하면서 증가하는 복잡성을 얼마나 잘 제어하는가가 중요하다.
이를 위해 객체 지향 프로그래밍은 추상화, 캡슐화, 정보은닉, 상속, 다형성 등 시스템의 복잡성을 제어할 수 있는 다양한 장치들을 제공한다. 그러나 생성된 객체를 관계형 데이터베이스에 저장하기 위해 SQL로 변환하는 반복적인 작업을 하게되며, 필드의 추가/변경 시 관계된 모든 쿼리를 고쳐야 한다는 불편함이 존재한다.
또한, 이러한 번거로움의 문제를 넘어 객체 지향 프로그래밍에서 객체간에 관계를 맺는 방식과 관계형 데이터베이스에서 테이블 간 관계를 맺는 방식의 차이에서 오는 패러다임의 불일치마저 존재한다.
객체는 상속과 참조(a.getB()
)를 통해 연관 관계를 맺지만, 테이블은 외래 키(a.id = b.id
)를 통해 연관 관계를 맺는다.
이와 같은 차이로 인해 결국 객체를 테이블에 맞추어 모델링하게 되는데, 이럴 경우 어떠한 SQL에 의해 객체를 가져왔는지에 따라 그 탐색 범위가 제한되며 이는 곧 해당 Entity에 대한 신뢰 문제로 이어진다. 쿼리의 성능 문제 때문에 모든 데이터를 JOIN해서 가져올 수 도 없다.
SELECT * FROM member -> member.getTeam()
// Team 정보를 JOIN하지 않았기 때문에 알 수 없다.
이 외에도 객체와 테이블의 데이터 타입의 차이나, 데이터 식별 방법(같은 타입의 객체간 비교)에서도 차이가 있기 때문에 SQL 중심의 개발은 여러 문제점과 불편함을 감수해야 한다.
SELECT * FROM member WHERE id=1 -> member;
SELECT * FROM member WHERE id=1 -> member;
// 동일한 두 쿼리를 통해 얻은 데이터로 생성된 객체는 같은 객체인가?
이러한 객체와 데이터베이스 간에 존재하던 패러다임의 불일치를 해결하기 위해 객체 관계 매핑(Object-Relational Mapping, ORM)이 등장하게 된다. ORM을 통해 객체의 설계와 DB의 설계가 분리되며 그 중간에서 ORM 프레임워크가 패러다임의 불일치를 해결해준다.
대부분의 대중적인 프로그래밍 언어에 ORM 기술이 존재하며, Java 진영의 ORM 기술 표준이 바로 JPA(Java Persistence API)이다.
JPA는 어플리케이션과 JDBC 사이에서 동작하며 SQL을 생성해주고, JDBC API를 사용해주며, 객체 매핑을 통해 패러다임의 불일치를 해결해준다. 이를 통해 객체 중심의 개발이 가능해진다.
개발자는 상속과 참조를 통해 객체에 값을 저장하기만 하면 SQL 작성, JOIN을 통한 연관관계 처리를 성능 최적화 까지 고려하여 처리해준다. 덕분에 Entity 계층의 신뢰도가 상승하며, 동일한 트랜잭션 안에서 조회한 객체간의 동일성도 보장해준다.
영속성 컨텍스트 내부에 엔티티를 보관하기 위한 저장소가 존재하며, 이를 1차 캐시라 한다. 1차 캐시는 하나의 트랜잭션과 생명주기가 동일하며, 1차 캐시가 유효한 동안 동일한 엔티티를 보장한다.(DB Isolation Level이 Read Commit일 경우에도 Repeatable Read 보장)
JPA는 트랜잭션을 커밋할 때까지 INSERT SQL을 모아 커밋이 이뤄지는 순간에 JDBC BATCH SQL 기능을 사용하여 한 번에 전송, 네트워크 통신 비용을 줄인다.
지연 로딩은 객체가 실제 사용될 때 로딩되는 것을 의미한다.
Member member = memberDAO.find(memberId); -> SELECT * FROM member
Team team = member.getTeam();
String teamName = team.getName(); -> SELECT * FROM team
즉시 로딩은 JOIN SQL로 한 번에 연관된 객체까지 미리 조회하는 것을 의미한다.
Member member = memberDAO.find(memberId); -> SELECT m.*, t.*
Team team = member.getTeam(); FROM member m
String teamName = team.getName(); JOIN team t...
페이징 처리와 같이 같은 RDBMS라도 벤더마다 기능의 사용법이 다른 경우가 많다. 그렇기에 선택한 데이터베이스 기술에 종속되고, 다른 데이터베이스로 변경하기 어렵다는 문제가 존재한다.
JPA는 어플리케이션과 데이터베이스 사이에 추상화된 데이터 접근 계층을 제공하여 사용하는 데이터베이스 방언으로 쿼리를 작성해준다. 그렇기에 특정 데이터베이스 기술에 종속되지 않은 개발을 할 수 있게해준다.
Entity Manager Factory는 단 하나만 생성하여 어플리케이션 전체에서 공유한다. Entity Manager Factory를 통해 생성한 Entity Manager는 쓰레드간에 공유되어서는 안되며, JPA의 모든 데이터 변경은 트랜잭션 안에서 실행되어야 한다.
JPA는 엔티티 객체를 중심으로 개발할 수 있게해준다. 검색 또한 테이블이 아닌 엔티티 객체를 대상으로 이루어진다. 하지만 복잡한 검색 쿼리의 경우 모든 DB 데이터를 객체로 변환해서 검색할 수 없다는 문제점이 발생하게 되고, 결국 검색 조건이 포함된 SQL이 필요하게 된다.
이 문제를 해결하기 위해 JPA는 SQL을 추상화한 JPQL이라는 객체 지향 쿼리 언어를 제공해준다. JPQL은 SQL과 유사한 문법으로 구성되어 있으며 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 등 자주 쓰이는 SQL 문법들을 지원해준다.
JPQL은 SQL에 의존하지 않고 엔티티 객체를 대상으로 하는 객체지향 쿼리(SQL은 DB의 테이블을 대상으로 한다.)이므로, SQL 방언을 바꿔도 코드 자체를 바꿀 필요가 없다는 장점이 있다.