JPA란?
JPA: Java Persistence API의 약자로 자바 진영의 ORM 표준 기술이다.
또한 JPA는 인터페이스의 모음이다. 이 인터페이스 모음들을 구현한 구현체는
하이버네이트, EclipseLink, DataNucleus 등이 있는데, 구현체로는 거의 하이버네이트를 사용한다.
ORM이란?
Object-relational mapping의 약자이다.
객체는 객체대로 설계하고 관계형 데이터베이스는 관계형 데이터베이스대로 설계하면
ORM 프레임워크는 중간에서 객체와 데이터베이스를 매핑해준다.
JPA는 왜 써야 될까?
SQL 중심적인 개발에서 객체 중심으로 개발이 가능해진다.
SQL은 JPA가 알아서 처리해준다. 우리는 객체만 신경써서 설계하면 된다.
생산성이 높아진다.
// JPA로 CRUD 해보기
jpa.persist(member); //저장
Member member = jpa.find(Member.class, memberId); //조회
member.setName("변경할 이름"); //수정(변경감지)
jpa.remove(member); //삭제
CRUD를 위 코드와 같이 매우 편하게 할 수 있다.
유지보수가 쉬워진다.
public class Member {
private String memberId;
private String name;
//private String tel;
}
Member Table에 id와 name 컬럼만 있었는데, tel 컬럼이 추가됐다고 생각해보자!
JPA가 객체를 보고 SQL을 알아서 매핑해서 처리해주기때문에
우리는 그냥 주석을 지우고 tel만 필드로 추가해주면 된다.
이렇게 우리는 VO와 상황에 따라서 Controller 혹은 View 파일 정도만 수정하면 작업이 끝난다.
즉, 아키텍처가 논리적으로 안정적인 것을 확인할 수 있다.
패러다임의 불일치를 해결해준다.
상속
위와 같은 관계에서 저장을 한다고 생각해보자. SQL 중심의 설계에서는 ITEM과 ALBUM Table에 각각 insert를 해주어야 했다.
하지만 JPA를 이용하면 SQL을 알아서 처리해주므로 전혀 생각할 필요가 없다.
# 개발자가 쓰는 코드
jpa.persist(album);
# JPA가 처리해주는 SQL
insert into item ...
insert into album ...
조회나 수정도 같은 원리로, 상속에서 생기던 쿼리와 코드가 복잡해지는 문제가 해결됐다.
연관관계와 객체 그래프 탐색
// 연관관계 저장
member.setTeam(team);
jpa.persist(member);
// 객체 그래프 탐색
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
연관관계를 저장할 때도 Team 객체에 대한 의존성 설정만 제대로 해주고,
jpa를 통해 저장하면 MEMBER와 TEAM 테이블 모두에 데이터가 제대로 저장된다.
또한 객체 그래프를 탐색할 때도 SQL을 알아서 만들어주는 JPA덕분에 안심하고 연관된 객체를 사용할 수 있다.
즉 엔티티를 신뢰할 수 있게 된다.
비교하기
동일한 트랜잭션에서 조회한 엔티티는 같음을 보장한다.
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
System.out.println(member1==member2); // true
성능 최적화가 가능해진다.
1차 캐시와 동일성 보장
동일한 트랜잭션 안에서는 같은 엔티티를 반환하는데는 약간(?)의 비밀이 있다.
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId); // Select문
Member member2 = jpa.find(Member.class, memberId); // 캐시에서 가져오기
System.out.println(member1==member2); // true
1번째 find의 경우 Select문을 통해 DB와 통신해서 데이터를 가져오는 반면,
2번째 find는 DB와 통신하지 않고 캐시에서 바로 가져오고 이를 통해 동일성이 보장되는 것인데,
여기서 캐시를 사용하므로 성능이 더 좋아진다.(캐시에서 가져오는 것이 DB와 통신하는 것보다 더 빠르다)
트랜잭션을 지원하는 쓰기 지연
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋
insert문을 DB서버와 3번 통신하며 실행시키는 것이 아니라
트랜잭션을 커밋하는 순간 쿼리를 모아서 한 번에 실행시킨다.
(위와 같은 코드에서는 commit하는 순간 INSERT 쿼리 3개가 DB로 날아간다)
지연로딩과 즉시로딩
지연로딩: 객체가 실제 사용될 때 로딩
즉시로딩: JOIN SQL로 한 번에 연관된 객체까지 미리 조회
//지연 로딩
Member member = memberDAO.find(memberId); // SELECT * from member where ...
Team team = member.getTeam();
String teamName = team.getName(); // SELECT * from TEAM where ...
//즉시 로딩
Member member = memberDAO.find(memberId); // SELECT * from MEMBER JOIN TEAM ...
Team team = member.getTeam();
String teamName = team.getName();
통신비용 측면에서 보면 한 번에 조회하는 즉시로딩이, 필요할 때마다 조회하는 지연로딩보다 뛰어나다.
메모리 측면에서 보면 반대로 지연로딩이 즉시로딩보다 뛰어나다.
따라서 상황에 맞게 적절히 사용해야 하는데,
위 코드를 예로 들면 Member와 함께 거의 모든 상황에서 Team이 필요하다면 즉시로딩이 유리하고
어쩌다 한 번씩 Member와 함께 Team이 사용된다면 지연로딩이 유리하다.