김영한 님의 자바 ORM 표준 JPA 프로그래밍 강좌를 듣고 정리한 내용입니다.
ORM이란?
- Object Relational Mapping(객체 관계 매핑)
- 객체는 객체대로 설계한다.
- 관계형 데이터베이스는 관계형 데이터베이스대로 설계한다.
- ORM 프레임워크가 중간에서 매핑한다.
- 대중적인 언어에는 대부분 ORM 기술이 존재한다.
JPA의 동작
JPA가 JAVA 애플리케이션에서 어떤 순서로 동작하는지 간단하게 알아볼 수 있다.
개발자가 JPA에게 명령을 내리면, JPA는 JDBC API를 사용해서 명령에 알맞은 SQL을 DB로 보내고 결과를 받는다.
객체 저장
- JPA에게 member 객체를 넘긴다. (Entity Object)
- JPA가 다음 작업을 수행한다.
- Entity 분석
- INSERT 쿼리 생성
- JPA는 결국 JDBC API를 사용한다.
- 패러다임 불일치 해결
객체 조회
- JPA의 find(id) 기능을 호출한다.
- JPA가 다음 작업을 수행한다.
- SELECT 쿼리 생성
- JDBC API 사용
- ResultSet 매핑(객체의 필드 하나하나에 DB에서 가져온 값을 매핑해준다.)
- 패러다임 불일치 해결
- JPA는 ResultSet을 Entity Object로 리턴한다.
JPA를 사용하는 이유
1. SQL 중심적인 개발에서 객체 중심으로 개발
2. 생산성 - JPA와 CRUD
이미 모든 CRUD기능이 JPA에 만들어져 있다.
그냥 호출해서 사용하면 된다.
저장 : jpa.persist(member)
조회 : Member member = jpa.find(memberId)
수정 : member.setName("변경할 이름")
-> 수정은 특히 재미있다. member.setName("이름") 을 호출하면 알아서 이름이 바뀌어서 DB 업데이트 쿼리가 날아간다. JPA를 사용한다는 것은 마치 자바 컬렉션에 객체를 push, pop 하는것과 같이 만들기 위해 사용하는 것이다.
삭제 : jpa.remove(member)
3. 유지보수
기존 : 필드 변경시 모든 SQL 수정
기존 SQL
INSERT INTO MEMBER(MEMBER_ID, NAME) VALUES
SELECT MEMBER_ID, NAME FROM MEMBER M
...
만약 MEMBER에 TEL 필드가 추가된다면?
INSERT INTO MEMBER(MEMBER_ID, NAME, TEL) VALUES
SELECT MEMBER_ID, NAME, TEL FROM MEMBER M
...
모든 SQL에 TEL을 추가해야 할 것이다.
JPA : 필드만 추가하면 된다. SQL은 JPA가 처리한다.
public class Member {
private String memberId;
private String name;
private String tel;
...
}
4. 패러다임의 불일치 해결
1. JPA와 상속
JAVA에서 Entity가 객체 상속관계를 가지고, RDB에서 Table 슈퍼타입 서브타입 관계를 가진다.
이때 개발자가 Album 객체를 DB에 저장하고 싶다면?
jpa.persist(album);
개발자는 단 한줄만 작성하면 된다.
나머지는 JPA가 처리한다.
INSERT INTO ITEM ...
INSERT INTO ALBUM ...
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
2. JPA와 연관관계
연관 관계 저장
member는 하나의 team을 갖는 관계이다. 하나의 team은 여러 멤버를 가질 수 있다.
member.setTeam(team);
jpa.persist(member);
3. JPA와 객체 그래프 탐색
객체 그래프 탐색
Member member = jpa.find(Member.class, memberId);
Team team = member.getTeam();
마치 Java 컬렉션에 member 객체가 있었던 것처럼 객체를 가져올 수 있다.
4. JPA와 비교하기
String memberId = "100";
Member member1 = jpa.find(Member.class, memberId);
Member member2 = jpa.find(Member.class, memberId);
member1 == member2; //같다.
동일한 트랜잭션에서 조회한 엔티티는 같음을 보장한다. (== 비교 사용)
5. 성능
1. 1차 캐시와 동일성 보장
- 같은 트랜잭션 안에서는 같은 엔티티를 반환한다 - 약간의 조회 성능 향상
- DB Isolation Level이 Read Commit이어도 애플리케이션에서 Repeatable Read를 보장한다.
String memberId = "100";
Member m1 = jpa.find(Member.class, memberId); //SQL
Member m2 = jpa.find(Member.class, memberId); //캐시
println(m1 == m2) //true
SQL은 1번만 실행된다.
2. 트랜잭션을 지원하는 쓰기 지연(transactional write-behind)
트랜잭션을 커밋할 때까지 SQL을 모은다.
그리고 JDBC BATCH SQL 기능을 사용해서 한번에 SQL을 전송한다.
transaction.begin(); // [트랜잭션] 시작
em.persist(memberA);
em.persist(memberB);
em.persist(memberC);
//여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
//커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // [트랜잭션] 커밋
3. 지연 로딩(Lazy Loading)
지연 로딩 : 객체가 실제 사용될 때 로딩 -> LAZY
즉시 로딩 : JOIN SQL로 한번에 연관된 객체까지 미리 조회 -> EAGER
더 알아보기!
즉시 로딩은 JPQL에서 N+1 문제를 일으킨다.
"select m from Member m"
위 SQL은 Member만 SELECT 하게 된다.
MEMBER를 쭉 다 가져와서 보니까 Member 엔티티의 Team의 fetchType이 EAGER였다.
- LAZY면 프록시(가짜 객체)를 넣으면 되겠지만, EAGER는 반환하는 시점에 다 조회가 되어 있어야 한다.
- 따라서, Member를 다 가져오고 나서, 그 Member와 연관된 Team을 다시 다 가져온다.
따라서 Member들을 모두 조회해서 가져왔지만 각 Member들의 Team이 비어있기 때문에 Team을 채우기 위해 각 Member마다 Team을 가져오는 쿼리를 날려서 가져온다.
Member의 수가 10000명 이라면? 최소 10000개의 쿼리가 날아갈 것이다..
N + 1 문제의 의미는
위 설명처럼 member 정보를 가져오기 위한 쿼리를 1개 날렸는데, 그것 때문에 추가 쿼리가 N개 나간다는 의미이다.