본문 바로가기
Backend/JPA

[JPA] 영속성 컨텍스트 1차 캐시의 주의점

by Everyday Sustler 2023. 6. 6.
반응형

영속성 컨텍스트(Persistence Context)는 논리상 어플리케이션과 데이터베이스 사이에 위치한 메모리 캐시입니다.

 

쉽게 풀자면 데이터베이스로부터 가져와 fetched 상태인 모든 엔티티 인스턴스의 집합인 1차 캐시라고 설명할 수 있습니다.

 

트랜잭션을 통한 객체는 모두 1차 캐시에서 관리된다고 생각하면 될 것 같습니다.

 

Persistence Context Lifecycle

 

그래서 해당 객체에 변경이 없다면 데이터를 여러 번 요청하더라도 데이터베이스까지 쿼리는 처음 한 번만 발생합니다.

 

여기서 예상하지 못 한 실수가 발생할 수 있습니다.

 

영속성 컨텍스트는 식별자를 기준으로 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);
}

 

어떤 방법이 좋을까요?

 

트랜잭션 역할 할당 관점과 책에서 추천하는 양방향 관계 관리 사상을 종합하면 두 방법을 모두 병행해 사용하는 것을 추천합니다.

 

번거롭겠지만 안전하고 예상 가능한 범위 안에서 동작하는 소프트웨어를 위해 두 가지 방법을 모두 적용하는 것이 좋겠습니다.

반응형