백엔드/JPA

[JPA] 연관관계 주인이 필요한 이유

happy_life 2022. 8. 11. 11:55

연관관계의 주인이 필요한 이유는 무엇일까요? 객체 지향의 패러다임과 데이터베이스의 패러다임에 차이가 있기 때문입니다. 단방향과 양방향 매핑과 관련해 각 패러다임의 차이를 이해하면 연관관계의 주인이 필요한 이유를 알 수 있습니다.

 

 

목차

1. 단방향과 양방향

2. 연관관계의 주인

3. 주의사항

 

 

JPA 단방향과 양방향

데이터베이스 테이블은 외래 키 하나로 양쪽 테이블 조인이 가능합니다. 하지만 객체는 참조용 필드가 있는 객체만 다른 객체를 참조하는 것이 가능합니다. 따라서 두 객체 사이에 하나의 객체만 참조용 필드를 갖고 참조하면 단방향, 두 객체 모두가 각각 참조용 필드를 갖고 참조하면 양방향 관계라고 합니다.

실제로 양뱡향 관계라는 것은 두 객체가 단방향 참조를 각각 가져 양방향 관계처럼 사용한다는 의미입니다. JPA를 사용해 데이터베이스와 패러다임을 맞추기 위해서는 객체들이 단방향 관계를 가질지 양방향 관계를 가질지 선택해야 합니다.

 

 

** 양방향 관계만 사용하면 안되는 이유

굳이 단방향, 양방향을 나누지 말고, 양방향으로 모두 나눠버리면 편하지 않을까 싶습니다. 하지만 객체 입장에서 양방향 매핑을 하면 오히려 복잡해질 수 있습니다. 예를 들어 사용자 Entity는 다른 엔티티들과 연관관계를 갖습니다. 이 경우 모두 양방향 관계를 설정하면 사용자 Entity가 엄청나게 복잡해질 것입니다. 그래서 기본적으로 단방향을 기본으로 하되, 나중에 객체 탐색이 필요한 경우 양방향으로 추가해주면 좋습니다.

 

 

 

JPA 연관관계의 주인

연관관계의 주인을 지정한다는 것은, 두 단방향관계 (A->B, B->A)를 맺을 때, 이 중 제어의 권한(데이터 조회, 저장, 수정, 삭제)를 갖는 실질적인 관계가 무엇인지 JPA에게 알리는 것입니다. 따라서 연관관계의 주인은 연관 관계를 갖는 두 객체 사이에서 조회, 저장, 수정, 삭제를 할 수 있지만, 그 외에는 조회만 가능하다. 연관관계의 주인이 아닌 객체는 mappedBy 속성을 사용해 주인을 지정해주면 됩니다.

 

* 외래 키가 있는 곳이 연관 관계의 주인

 

 

연관관계의 주인을 지정해야 하는 이유

1. 패러다임의 차이

예를 들어 (Member, Team)이 있고 양방향 연관 관계를 갖는다고 해보자. 이 상황에서 Member를 다른 Team으로 수정하려고 할 때 Member 객체에서 setTeam() 같은 메서드로 수정해야하는지, Team 객체에서 getMemberList()로 List형식으로 들어와있는 member List 객체를 꺼내 수정해야하는지의 문제가 발생한다. 객체 패러다임에서는 두 방식 다 옳지만, 데이터베이스의 패러다임을 적용하려고 존재하는 JPA 입장에서 혼란스러워진다는 것이다.

즉, 연관관계의 주인이 있어야 객체 패러다임에서의 양방향 관계가 데이터베이스 패러다임에서 연관관계가 하나임을 보장할 수 있게 된다.

 

2. 영속성 컨텍스트 변경감지 기준

예를들어 member.setTeam(), team.getMemberlist().addMember() 등을 모두 허용하게 되면 영속성 컨텍스트가 너무 복잡해지게 된다. 경우에 따라 하나의 수정이지만, update 쿼리가 두번 나가게 될 수도 있고(양쪽 연관관계를 모두 업데이트 해버리는 경우), 이를 최적화하기 위한 추가적인 연산도 많아진다. 또한 양쪽 연관관계가 모순되는 경우 어떻게 처리할지에 대한 규약도 훨씬 복잡해진다.(team.memberLis에는 member1이 있는데, member1.setTeam(null)인 경우에 어떤 쪽을 따라야하는가?)

 

 

주의사항

1. 양방향 매핑 시 양쪽에 값을 다 세팅해주어야 한다.

코드예제

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");

        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();

        try {
            //저장

            Team team = new Team();
            team.setName("TeamA");
            em.persist(team);

            Member member = new Member();
            member.setName("member1");
            member.setTeam(team);
            em.persist(member);


            Team findTeam = em.find(Team.class, team.getId()); // 1차 캐시
           List<Member> members = findTeam.getMembers();
            for (Member member1 : members) {
                /**
                 * 출력되지 않음
                 */
                System.out.println("member.getName() = " + member1.getName());
            }

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        }finally {
            em.close();
        }
        emf.close();
    }
}

 

위와 같은 경우 members 컬렉션엔 값이 들어가있지 않다. 디비에서 값을 꺼낸 객체가 아니기 때문이다. (SELECT 쿼리가 나가지 않는다.) flush(), clear()를 중간에 삽입해주면 모르겠지만, 이를 매번 하는 것은 실수하기 쉽다. 따라서 연관관계의 주인이 아닌 것도 객체의 관점에서 코드를 추가해주어야 한다.

 

team.getMembers().add(member);

 

하지만 위와같은 경우 매번 양방향의 코드를 추가하기 힘들다. 따라서 아래와 같이 연관관계 편의 메서드를 활용하는 것이 좋다.

연관관계 편의 메서드

 

 

 

2. 양방향 매핑 시 무한루프를 조심하자

ex) toString(), Lombok 라이브러리, Json 생성 라이브러리