JPA
object relational mapping
객체랑 관계형 데이터베이스를 매핑해주는 것
마치 자바 컬렉션에 저장하듯이 해준다.
왜 사용해야 하는가
SQL 중심적인 개발에서 객체 중심 개발을 할 수 있다.
- 생산성
- 유지보수
- 패러다임의 불일치 해결
- 성능
- 표준
- 데이터 접근 추상화와 벤더 독립성
- 표준
메소드
저장: jpa.persist()
조회: jpa.find()
수정: member.setName()
삭제: jpa.remove()
JPA가 패러다임의 불일치를 해결해준다.
1. JPA와 상속(저장)
개발자는 객체인 앨범만 저장하면 됨. jpa가 알아서 쿼리문을 날려준다.
2. JPA 상속(조회)
원래는 테이블이랑 앨범을 조인한다음에 가져와야 하지만, jpa는 알아서 분석 후 받아와준다.
3. 신뢰할 수 있는 엔티티가 되어 계층 신뢰 가능
JPA가 관리하는 것 = 엔티티이므로, 원래는 새로운 인스턴스 생성이므로 == 했을 때 달라야 하지만,
jpa는 이 두개가 같다고 나온다.
동일한 트랜잭션 안에서 조회한 엔티티는 같다.
1. 1차 캐시와 동일성 보장
jpa 는 같은 트랜잭션 안에서는 항상 같은 엔티티를 반환해준다. → 조회 성능 향상
2. 트랜잭션을 지원하는 쓰기 지연 Insert
- 트랜잭션을 커밋할 때까지 insert SQL을 모음.
- JDBC Batch SQL 기능을 사용해 한번에 SQL 전송
어쨌든 트랜잭션 커밋하기 전까지 보내면 되기 때문에 커밋하는 순간 데이터베이스에 insert SQL을 모아서 보낸다.
3. 트랜잭션을 지원하는 쓰기 지연 update
update도 비슷한 성능 이점이 있다.
지연 로딩과 즉시 로딩
지연 로딩
객체가 실제 사용될 때 로딩한다.
EX) 멤버를 조회할 때 팀을 같이 안 쓰고 어쩌다 팀만 조회할 때만 쓰니까 연관된 걸 같이 미리 땡겨오지 말자
Member member = memberDAO.find(memberId);
Team team = member.getTeam();
String teamName = team.getName(); //이떄 팀을 가져옴
즉시 로딩
join SQL로 한 번에 로드할 때 즉시 로드한다.
Hello JPA!
jpa 를 시작해보자.
JPA 구동 방식
JPA에서 중요한것 (트랜잭션 단위)
모든 작업을 트랜잭션 작업 위에서 해야 함.
⇒ 트랜잭션을 생성하지 않고 한다면 작업이 반영되지 않는다.
삽입
package org.example;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 꺼내기
/*
트랜잭션
*/
EntityTransaction tx = em.getTransaction(); //트랜잭션 작업단위 생성
//멤버 저장
tx.begin(); //트랜잭션 시작
Member member = new Member();
member.setId(1L);
member.setName("Hello A");
em.persist(member);
tx.commit(); //트랜잭션 성공 시 커밋
em.close(); //엔티티 매니저 죽이기
emf.close(); //실제 로직이 끝나면 팩토리를 닫아줘야 한다.
}
}
삽입의 결과
실제 DB에 들어간 것을 볼 수 있다.
신기한 점
나는 멤버가 어느 테이블에 저장되어라고 한 적이 없는데 왜 멤버라는 객체에 자동적으로 매핑되었을까?
→ 이름이 자동적으로 매핑되었기 때문이다.
@Column(name = "username")
이었으면 name에 매핑이 되지 않았을 거다..
실수한 점
"Cannot begin Transaction on closed Session/EntityManager"
라는 에러를 마주했다. 내가 코드 중간에 em.close() 라는 걸 써서 엔티티 매니저를 종료했기 때문에 저장하지 못했던 것이다.
Cannot rollback transaction in current status [COMMITTED]
이미 커밋된 트랜잭션을 롤백할 수 없습니다.
이런 오류가 뜨길래 엥 했다.
이유는 바로 내가 tx.commit()을 두번 날렸기 때문이었다
→ 하지만 트랜잭션은 한 번 커밋되면 더 이상 유효하지 않기 때문에 begin()후에 commit()을 두번 쓸 수 없다.
쓰려면 트랜잭션을 두번 사용해야 한다.
tx.begin() //트랜잭션 시작
.. 어쩌구 저쩌구 삽입 내용 ..
tx.commit() //트랜잭션 성공 시 커밋 = 종료
tx.begin() //트랜잭션 시작
Member findMember = em.find(Member.class,1L);
System.out.println("findMember.id: " + findMember.getId());
findMember.setName("Hello B");
tx.commit(); //두번째 트랜잭션 성공 시 커밋
트랜잭션을 try-catch 안에다가 써보자
package org.example;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 꺼내기
EntityTransaction tx = em.getTransaction(); //트랜잭션 작업단위 생성
//멤버 저장
tx.begin(); //트랜잭션 시작
try{
Member member = new Member();
member.setId(1L);
member.setName("Hello A");
em.persist(member);
tx.commit(); //트랜잭션 성공 시 커밋
}catch (Exception e){
tx.rollback(); //트랜잭션 실패 시 롤백
} finally {
em.close(); //엔티티 매니저 죽이기
emf.close(); //실제 로직이 끝나면 팩토리를 닫아줘야 한다.
}
}
}
조회
package org.example;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.EntityTransaction;
import jakarta.persistence.Persistence;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 꺼내기
EntityTransaction tx = em.getTransaction(); //트랜잭션 작업단위 생성
//멤버 저장
tx.begin(); //트랜잭션 시작
try{
Member member = new Member();
member.setId(1L);
member.setName("Hello A");
em.persist(member);
tx.commit(); //트랜잭션 성공 시 커밋
Member findMember = em.find(Member.class,1L);
System.out.println("findMember.id: " + findMember.getId());
}catch (Exception e){
tx.rollback(); //트랜잭션 실패 시 롤백
} finally {
em.close(); //엔티티 매니저 죽이기
emf.close(); //실제 로직이 끝나면 팩토리를 닫아줘야 한다.
}
}
}
결과
업데이트
자바 객체에서 값만 바꿨는데 어떻게 변경이 될까?
JPA을 통해서 엔티티를 가져오면 JPA에서 관리를 한다. JPA가 객체가 변경되었는지 감지하고, 변경되었으면 업데이트 쿼리를 날린다.
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 꺼내기
EntityTransaction tx = em.getTransaction(); //트랜잭션 작업단위 생성
//멤버 저장
tx.begin(); //트랜잭션 시작
try{
Member member = new Member();
member.setId(1L);
member.setName("Hello A");
em.persist(member);
Member findMember = em.find(Member.class,1L);
System.out.println("findMember.id: " + findMember.getId());
findMember.setName("HelloB");
tx.commit(); //트랜잭션 성공 시 커밋
}catch (Exception e){
tx.rollback(); //트랜잭션 실패 시 롤백
} finally {
em.close(); //엔티티 매니저 죽이기
emf.close(); //실제 로직이 끝나면 팩토리를 닫아줘야 한다.
}
}
}
업데이트 결과
근데 신기한게 insert가 나중에 일어나는 건지 아니면 시스템 출력을 먼저 주는건진 모르겠는데 1번이 먼저 출력되었다.. 뭐지?
생각해보니까 내가 실수한거였다 .. ㄱ-
이번엔 그래서 삽입 트랜잭션과 업데이트 트랜잭션을 나눴다.
바꾼 코드
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf
= Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 꺼내기
EntityTransaction tx = em.getTransaction(); //트랜잭션 작업단위 생성
//멤버 저장
tx.begin(); //트랜잭션 시작
try{
Member member = new Member();
member.setId(1L);
member.setName("Hello A");
em.persist(member);
tx.commit();
Member findMember = em.find(Member.class,1L);
System.out.println("findMember.id: " + findMember.getId());
tx.begin();
findMember.setName("HelloB");
tx.commit(); //트랜잭션 성공 시 커밋
}catch (Exception e){
tx.rollback(); //트랜잭션 실패 시 롤백
} finally {
em.close(); //엔티티 매니저 죽이기
emf.close(); //실제 로직이 끝나면 팩토리를 닫아줘야 한다.
}
}
}
그랬더니 내가 생각했던 결과가 맞게 떴다.
여기서 flush와 commit이 란?
영속성 컨텍스트의 변경 사항을 데이터베이스에 반영하는 작업이다.
- flush() 가 끝났다고 해서 트랜잭션이 종료되는 것은 아니다. 계속 유지된다.
- 트랜잭션이 종료되지 않았으므로 변경사항을 다른 트랜잭션에서 볼 수 없다.
↔ commit
트랜잭선 성공 후 트랜잭션 내에서 실행된 모든 변경 사항을 확정한다.
- commit() 호출 후에는 트랜잭션이 종료되고, 데이터베이스에 반영된 최종 변경 사항이 최종적으로 확정된다.
둘 중 장점은?
두번째가 더 명확한 것 같다.
삽입 시점, 업데이트 시점을 명확하게 구분할 수 있기 때문이다.
주의할 점
엔티티 매니저 팩토리는 딱 하나만 생성된다.
웹서버가 올라오는 시점에 딱 하나만 생성되는 것이다.
- 애플리케이션 전체에 공유한다.
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
- 따라서 WAS가 내려갈 때 닫아줘야 한다.
emf.close(); //실제 로직이 끝나면 팩토리를 닫아줘야 한다.
엔티티 매니저는 쓰레드 간의 공유가 불가
쓰레드 간의 공유를 하면 안된다.
- 자원을 사용하고 버려야 함.
EntityManager em = emf.createEntityManager(); // 엔티티 매니저 꺼내기 ....사용... em.close() //버려
모든 JPA의 데이터 변경은 트랜잭션 안에서 실행
JPA에서 데이터 변경 시에 트랜잭션 안에서 반드시 수행해야 한다.
EntityTransaction tx = em.getTransaction(); //트랜잭션 작업단위 생성
tx.begin(); //트랜잭션 시작
tx.commit(); //트랜잭션 성공 및 종료
tx.rollback(); //트랜잭션 성공
JPQL = 객체 지향 SQL
JPQL은 테이블 대상이 아니라 “엔티티” 객체 대상이다.
따라서 아래 코드에서도 Member을 조회한다.
List findMembers = em.createQuery("select m from Member")
.resultList();
장점
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- 만약 H2가 아니라 mysql, oracle 등으로 바꾼다고 해도, JPQL을 쓰면 해당 데이터베이스의 문법으로 자동 변경해준다.
미니 프로젝트 코드 리뷰에서 내가 JPA에 대한 지식이 부족한 것 같다는 피드백을 받았다.
너무 머리가 멍했다..^^ 사실 나 스스로도 코드를 치며 느끼고 있던 부분이었다.
혼자서 자괴감을 느끼다가 김영한 님의 JPA 강의를 정독하기로 결심했다.
일단 내 목표는 이번주 안으로 끝내고 심화 시작하기 인데,, 내가 세운 목표이니 만큼 매일 블로그에 정리하며 열심히 해야겠다.
인강을 들으면서 노션에 적으니까 빠진 부분도 많은 것 같다.
열심히 해야지....
'BACKEND > Spring Boot' 카테고리의 다른 글
REST Template vs WebFlux (0) | 2024.11.05 |
---|---|
[자바 ORM 표준 JPA 프로그래밍 - 기본편] 영속성 컨텍스트 (0) | 2024.10.30 |
Spring Container는 어떻게 생성될까 (0) | 2024.09.22 |
Tomcat은 어떻게 동작하는가 (0) | 2024.09.22 |
테스트 스텁과 목 오브젝트의 차이점 (1) | 2024.09.19 |
안녕하세오 저는 똑똑해지고 싶은 버그 수집가에오
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!