영속성 컨텍스트(Persistence Context)는 논리상 어플리케이션과 데이터베이스 사이에 위치한 메모리 캐시입니다.
쉽게 풀자면 데이터베이스로부터 가져와 fetched 상태인 모든 엔티티 인스턴스의 집합인 1차 캐시라고 설명할 수 있습니다.
트랜잭션을 통한 객체는 모두 1차 캐시에서 관리된다고 생각하면 될 것 같습니다.
그래서 해당 객체에 변경이 없다면 데이터를 여러 번 요청하더라도 데이터베이스까지 쿼리는 처음 한 번만 발생합니다.
여기서 예상하지 못 한 실수가 발생할 수 있습니다.
영속성 컨텍스트는 식별자를 기준으로 1차 캐시 역할을 수행한다
일부 사람들은 @ManyToOne, @OneToMany, mappedBy 를 사용하면 객체 수준에서도 자동으로 관계가 형성된다고 생각합니다.
그러나 이 어노테이션과 규칙은 데이터베이스로부터 객체를 저장하거나 읽어올 때 동작합니다.
JPA에서 관리하는 엔티티 객체는 persist() 메소드는 영속성 컨텍스트에 객체를 등록하는 영속화 과정을 거칩니다.
이 때에는 사용자가 실제로 양쪽 객체에 양방향 매핑을 정의해주어야 합니다.
먼저 아래와 같이 양방향 관계가 정의된 Member, Team 데이터를 저장하는 initSave() 메소드가 있습니다.
public static void initSave(EntityManager em){
// save team1
Team team1 = new Team("team1", "T1");
em.persist(team1);
// save member1
Member member1 = new Member("member1", "m1");
member1.setTeam(team1);
team1.getMembers().add(member1);
em.persist(member1);
// save member2
Member member2 = new Member("member2", "m2");
member2.setTeam(team1);
team1.getMembers().add(member2);
em.persist(member2);
// flush
em.flush();
}
다음으로는 양방향 관계를 조회하는 biDirection() 메소드가 있습니다.
public static void biDirection(EntityManager em){
Team team = em.find(Team.class, "team1");
List<Member> members = team.getMembers();
for(Member member : members){
System.out.println("member.username = " + member.getUsername());
}
}
이를 아래와 같이 한 개의 트랜잭션 안에서 실행하면 어떤 결과가 나올까요?
데이터를 삽입하고, flush() 까지 수행해 데이터베이스에 반영까지 했으니 양방향 관계는 형성되었겠다고 생각할 수 있습니다.
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // transaction begin
initSave(em); // init save
biDirection(em); // check bi-directional
tx.commit(); // transaction commit
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
그러나 이 코드를 실행해보면 예상과는 달리 아무것도 조회되지 않습니다.
반면, 데이터베이스에는 TEAM_ID 값을 통해 외래키가 설정되어있음을 확인할 수 있습니다.
이런 현상이 발생하는 이유는 영속성 컨텍스트는 식별자를 기준으로 1차 캐시 역할 을 수행하기 때문입니다.
해결 방법
biDirection() 메소드에서 team1 값을 ID로 가지는 팀을 요청하는데, 이 객체는 이미 영속성 컨텍스트에 등록되어 있습니다.
그렇기 때문에 List<Member> members = new ArrayList<>(); 로 초기화만 진행되고 값은 텅 빈 기존의 Team 객체를 반환해 줍니다.
정상적으로 연관관계에 있는 값을 조회해오고 싶다면 두 가지 방법이 있습니다.
먼저 조회를 서로 다른 트랜잭션에서 수행하는 것입니다.
그렇게 되면 새로운 영속성 컨텍스트가 만들어지고, 데이터베이스로부터 값을 조회하며 연관관계에 있는 멤버를 함께 가져오게 됩니다.
두 번째로는 객체 레벨에서도 삽입을 명시하는 연관관계 편의 메소드를 정의하는 것입니다.
이미 mappedBy 키워드에 의해 연관관계의 주인을 설정했습니다.
그러나 이는 데이터베이스 값 변경에 영향을 미치는 객체만을 나타낸 것이며, 실제 객체에는 아무 영향을 주지 못 합니다.
따라서 Team을 저장할 때, team.getMembers().add(member); 코드를 삽입해 데이터 변경을 양쪽에 명시적으로 나타냅니다.
public void setTeam(Team team){
if(this.team != null)
this.team.getMembers().remove(this);
this.team = team;
team.getMembers().add(this);
}
어떤 방법이 좋을까요?
트랜잭션 역할 할당 관점과 책에서 추천하는 양방향 관계 관리 사상을 종합하면 두 방법을 모두 병행해 사용하는 것을 추천합니다.
번거롭겠지만 안전하고 예상 가능한 범위 안에서 동작하는 소프트웨어를 위해 두 가지 방법을 모두 적용하는 것이 좋겠습니다.
'Backend > JPA' 카테고리의 다른 글
[JPA] 다대다(N:N) 관계 (0) | 2023.07.03 |
---|---|
[JPA] 프록시 객체의 직관적 이해 (0) | 2023.06.12 |
[JPA] org.hibernate.PersistentObjectException: detached entity passed to persist 에러 (0) | 2023.06.06 |
[JPA] StackOverflowError (0) | 2023.05.27 |
[JPA] could not initialize proxy - no session (0) | 2023.05.27 |