자바 진영 ORM 기술 표준. hibernate를 비롯한 다른 ORM 프레임워크의 기능을 추상화하여 만든 인터페이스, ORM 표준 기술 명세이다.
기존 애플리케이션 개발 방식에서는 DB와 통신하기 위해서 데이터 접근 계층에 많은 SQL 관련 코드를 작성해야 했다. 쿼리를 일일이 작성하는 것은 물론이고, 요구사항이 바뀌는 경우 DTO부터 SQL 쿼리를 모두 수정해야 해서 유지보수도 힘들었다. 애플리케이션 개발은 점점 데이터 코드 작성에 의존적이게 되었다. 실제로 나도 myBatis로 개발을 할 때 로직보다 SQL을 짜는 데에 더 많은 시간을 보내면서 '이게 개발인가?' 싶은 생각도 들었었다.
이러한 문제는 객체 지향적인 개발 언어와 테이블의 관계로 구성된 데이터베이스의 근본적인 지향점이 다르기 때문에 발생했다. 객체 지향 언어에서는 상속, 추상, 다형성 등 객체 간의 복잡성을 통해 데이터를 관리한다. 반면 관계형 데이터베이스는 이러한 개념을 거의 사용하지 않으며, 데이터 자체에 중점을 두고 구조화되어 있다.
자바에서는 상속을 통해서 객체 간의 관계를 표현한다. 하지만 DB에 저장될 때 이를 구현하려면 번거롭다. 예를 들어 Cuisine이라는 슈퍼 클래스가 있고 서브 클래스 Chinese, Korean, Japanese가 각각 이를 상속한다고 해보자. 기존의 JDBC template으로 Chinese 객체를 저장한다고 하면, Chinese의 내용과 Cuinese을 따로 SQL로 저장해야 할 것이다. 조회하는 것은 더 귀찮다. 저장한 Chinese 객체를 찾으려면 Cuisine과 Chinese 테이블을 조인해서 조회한 결과를 Chinese 객체를 반환하는 코드를 짜야할 것이다.
자바와 RDBMS에서 모델링의 가장 큰 차이점은 객체는 참조를 사용하지만 RDBMS는 외래키를 사용한다는 점이다. 외래키로는 관련된 테이블을 모두 조회할 수 있지만, 자바에서는 참조를 가진 쪽만 데이터에 접근할 수 있다. 객체도 테이블처럼 cuisineId와 같이 외래키와 동일한 필드를 객체 내에 직접 저장하는 방법을 생각해볼 수 있지만, cuinse의 정보를 불러올 때 결국 chinese.getCuisineId()
를 사용하고 다시 해당 id로 DB에 접근해야 하므로 Cuisine과 Chinese 간의 관계를 제대로 표현할 수 없는, 객체지향적이지 못한 코드를 써야한다.
Chinese를 조회하는 코드를 보면 아래와 같다.
public class Chinese extends Cuisine{
String id;
String cuisineId;
String ingredient;
}
String cuisineId = chinese.getCuisineId();
Cuinise cuisine = dao.selectCuisine(cuisineId);
좀 더 객체 지향적으로 모델링을 한다고 하면 아래와 같이 관계된 객체의 참조값을 조회하면 될 것이다.
Cuisine cuisine = dao.selectCuisine(chinese.getCuisine());
JPA를 통해 엔티티 간의 관계를 설정해놓고, 아래와 같이 수행하면 chinese에 cuisine으로 접근할 수 있는 참조값을 외래키로 변환하여 적절하게 insert 쿼리를 작성하여 저장해준다.
chinese.setCuisine(cuisine);
jpa.persist(chinese);
Chinese chinese = jpa.find(Chinese.class, chineseId);
Cuisine cuisine = chinese.getCuisine();
객체 간의 관계를 통해 표현되는 네트워크, 관계도. 객체 참조를 통해 객체를 찾는 것을 객체 그래프 탐색이라고 한다. 예를 들어 Member, Team, Order, OrderItem 등 여러 객체가 관계를 맺고 있을 때,
member.getOrder().getOrderItem()...
이와 같은 방식으로 객체 그래프를 탐색할 수 있다.
SQL을 사용하여 직접 객체 그래프를 탐색하게 되면 탐색할 범위를 정해놔야 한다. 위 코드를 쿼리로 보면 아래와 같이 짜야 한다.
SELECT member.*, order.*, orderItem.*
FROM member m
JOIN order o ON o.id = m.order_id
....
이와 같은 상황에서 검색할 컬럼이 하나 더 있거나, order_id를 통해 또 다른 테이블의 정보를 얻어오고자 한다면 쿼리 일부와 dao 일부를 모두 건드려야 할 것이다.
따라서 아래와 같은 코드가 있다고 할 때
Member member = dao.find(memberId);
member.getOrder();
member.getOrder().getOrderItem();
JPA에서는 객체를 사용하는 시점에 적절히 쿼리를 만들어 수행해준다. 그래서 따로 조인 쿼리를 짜지 않더라도 관계 설정이 되어 있는 여러 엔티티에서 다른 엔티티를 호출하는 메서드를 사용하면, 해당 엔티티의 정보를 객체로 반환 받을 수 있다. 따라서 연관관계를 맺어놓은 객체들을 마음껏 탐색할 수 있게 된다. 이것이 가능한 것은 프록시 객체를 이용한 지연 로딩 기능이 있기 때문인데, 이는 뒤에서 천천히 설명한다.
같은 조건으로 데이터를 검색했을 떄, 기존의 JDBC에서는 동일성을 보장하기 위해서는 대부분의 객체마다 equals()와 hashcode()를 오버라이딩해서 사용하여야 했다.
public Member getMember(String memberId){
String sql = "SELECT * FROM MEMBERS WHERE MEMBER_ID = ?";
/..../
return new Member(....);
}
String memberId = "100";
Member m1 = dao.getMember("100");
Member m2 = dao.getMember("100");
하지만 JPA는 같은 트랜잭션에서 같은 객체가 조회되는 것을 보장한다.
String memberId = "100";
Member m1 = jpa.find(Member.class, memberId);
Member m2 = jpa.find(Member.class, memberId);
위와 같은 코드에서 m1==m2는 true를 리턴한다. 이는 JPA에서 사용하는 영속성 컨텍스트를 통해 가능한 것인데, 자세한 것은 이 다음 장에서부터 확인할 수 있다.