본문 바로가기
Back-end/JPA 개념

8. 양방향 연관 관계의 기본 개념

by devraphy 2022. 3. 31.

0. 개요

- 이번 포스팅은 양방향 연관 관계에 대해 알아보도록 하자.

 

1. 양방향 연관 관계

- 단방향 연관 관계에서 사용했던 예시를 확장하여 양방향 연관 관계로 만들어보자.

 

a) 관계 예시

- 구현하려는 양방향 연관 관계는 아래의 테이블과 같다.

 

- 이전 포스팅에서 단방향 연관 관계를 설명할 때의 구조와 동일한 테이블 구조다.

- 분명 양방향 연관 관계를 구현한다고 했으나 왜 동일한 테이블을 사용할까? 

 

- 그 이유는 RDB는 FK를 이용한 Join으로 양방향 연관 관계를 형성한다.

- 즉, RDB는 양방향 연관 관계를 기본으로 한다.

 

- 다만 객체의 관점에서 보았을 때 연관 관계의 방향성이 유의미하다. 

 

b) 양방향 연관 관계란?

- 위의 테이블 구조를 예시로 생각해보자. 

 

- Player 테이블의 FK를 이용하여 Team 테이블과 Join을 할 수 있다.

- 반대로 Team 테이블의 PK를 이용하여 Player 테이블과 Join을 할 수 있다.

 

- 즉, team_id 필드를 이용하여 선수 또는 팀을 검색할 수 있다.

- 이처럼 RDB는 FK를 이용한 양방향 데이터 검색이 가능하다.

 

- 그러나 객체(Entity)에서는 이것이 불가능하다. 

- 객체 간 양방향 검색이 가능하려면, 상대 객체를 참조하는 필드를 양쪽 모두가 가져야 한다.

 

2. 양방향 연관 관계 구현

a) Player Entity

- Player 객체는 다음과 같다.

- 하나의 teamId는 다수의 player를 가지므로 @ManyToOne을 명시한다.  

- 그러므로 Many는 Player 객체, One은 Team 객체가 된다. 

@Entity
public class Player {

    @Id @GeneratedValue
    @Column(name = "PLAYER_ID")
    private Long id;

    @Column(name = "PLAYER_NAME")
    private String name;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team teamId;
    
    // Getter, Setter 생략
}

 

b) Team Entity

- Team 객체는 다음과 같다.

@Entity
public class Team {

    @Id
    @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    @Column(name = "TEAM_NAME")
    private String name;

    @OneToMany(mappedBy = "teamId")
    private List<Player> players = new ArrayList<>();
    
    // Getter, Setter 생략
}

 

- 하나의 Team은 다수의 Player를 가지므로 @OneToMany를 명시한다.

 

- mappedBy 옵션은 관계를 형성하는 객체의 필드 중 어떤 필드를 기준으로 검색을 수행하는지 명시한다.

- 즉, 상대 객체의 필드 중 FK 역할을 수행하는 필드의 이름을 명시한다. 

- 그러므로 Player Entity의 teamId 필드를 명시한다.

 

- mappedBy에 관한 내용은 아래에서 자세히 언급할 예정이다. 

 

c) 실행 결과

- 다음과 같은 코드를 실행한다.

- 아래의 코드를 실행하면 2명의 선수 이름이 콘솔에 출력되어야 한다.

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("ManUnited");
            em.persist(team);

            Player player1 = new Player();
            player1.setName("JI-SUNG PARK");
            player1.setTeamId(team);
            em.persist(player1);

            Player player2 = new Player();
            player2.setName("Cristiano Ronaldo");
            player2.setTeamId(team);
            em.persist(player2);

            em.flush();
            em.clear();

            Player findPlayer = em.find(Player.class, player1.getId());

            List<Player> players = findPlayer.getTeamId().getPlayers();
            for(Player p: players) {
                System.out.println("player = " + p.getName());
            }

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

 

- 콘솔에 결과가 잘 출력되는 것을 확인할 수 있다. 

 

 

- 위에 빨간 줄이 쳐진 부분이 양방향 데이터 검색을 확인할 수 있는 부분이다. 

- player로 team을 검색하고, 다시 player를 검색하였다.

 

3. 연관 관계의 주인

- 위에서 mappedBy 옵션을 사용하여 연관 관계의 기준이 되는 필드를 명시했다.

- mappedBy의 역할과 기능을 이해하려면 연관 관계의 주인에 대한 이해가 필요하다.

- 이에 대해서 알아보도록 하자.

 

a) 객체와 테이블이 형성하는 관계의 차이

- 테이블은 FK를 이용하여 연관 관계를 형성한다.

- FK를 이용한 Join으로 양방향 데이터 검색이 가능하다. 

- 즉, 테이블은 FK 하나로 양방향 연관 관계를 구축한다.

 

- 객체는 참조 필드를 이용하여 연관 관계를 형성한다.

- 즉, 2개의 단방향 연관 관계를 형성하여 하나의 양방향 연관 관계를 구축한다.

- 결론적으로 서로 다른 단방향 연관 관계 2개를 양방향이라고 부를 뿐이다. 

 

b) 객체의 양방향 관계에서 오는 문제점

- RDB의 경우 FK가 Join의 기준점이 된다.

- 그러므로 FK를 통해 양방향 검색이 가능하다. 

 

- 그러나 객체는 서로 다른 단방향 관계 2개를 구축하여 양방향 관계를 구축한다.

- 그렇다면 기준이 되는 관계가 무엇일까?

 

- 여기서 기준이 되는 관계란 다음과 같은 상황을 의미한다. 

  → Player 객체의 teamId 값을 기준으로 Team 객체의 players 값이 바뀌는 것일까?

  → Team 객체의 players 값을 기준으로 Player 객체의 teamId 값이 바뀌는 것일까?

 

- 객체의 입장에서 보면 무엇이 기준이라고 명확하게 말할 수 없는 상황이다. 

 

- 그러므로 기준이 되는 필드를 명시하는데, 이를 연관 관계의 주인이라고 표현한다.

- 즉, FK를 명시하는 것이다.

 

c) 양방향 연관 관계 매핑 규칙

- 양방향 연관 관계를 형성할 때에는 다음의 규칙을 기반한다.

 

   1. 2개의 단방향 관계 중 하나를 연관 관계의 주인으로 지정한다.

   2. 연관 관계의 주인이 FK의 등록 및 수정을 관리한다.

   3. 주인이 아닌 경우, 읽기(= 조회)만 가능하다.

   4. 주인은 mappedBy 옵션을 사용하지 않는다.

   5. 주인이 아닌 경우, mappedBy 옵션을 사용하여 관계의 주인을 명시한다.

 

 

d) 어떤 필드가 주인이 되는가?

- 테이블 구조를 기반으로, FK가 존재하는 테이블을 구현한 Entity가 관계의 주인이다.

- 아래의 그림을 살펴보자. 

 

- 위의 그림은 DB 테이블의 구조를 표현한 것이다.

- Player 테이블의 team_id 필드가 FK다. 

- 그러므로 Player Entity에서 team_id에 해당하는 필드가 연관 관계의 주인이 된다. 

 

- 규칙에 의해 Player Entity의 teamId 필드가 연관 관계의 주인이다.

- 그러므로 주인이 아닌 Team Entity에서 mappedBy 옵션을 사용하여 주인을 명시한다. 

 

4. 양방향 매핑의 핵심 정리

a) 단방향 매핑만으로 충분하다.

- 단방향 매핑만으로도 연관 관계는 충분히 구현된다.

- 그러므로 최초에 백엔드를 설계할 때에는 양방향을 구현하지 말 것을 권장한다.

- 최초에는 단방향 매핑 만으로 모든 연관 관계는 충분히 구현되기 때문이다.

 

b) 양방향 매핑이 필요한 이유

- 양방향 매핑이 필요한 경우는 단방향 매핑된 관계를 역방향으로 조회할 때다.

- 개발을 하다보면 단방향으로 매핑된 연관 관계를 역순으로 조회해야 하는 경우가 발생한다.

- 이처럼 필요한 경우에 한하여, 양방향 매핑을 추가적으로 구현하는 것을 권장한다.

 

c) 왜 단방향 매핑만으로 충분한가?

- 무조건 FK를 가진 테이블이라고 해서 양방향 매핑을 구현할 필요가 없다.

- 데이터를 조회할 때, 테이블을 Join하는 방향이 일방적이라면 단방향 매핑으로 충분하기 때문이다.

- 그러므로 위에서 설명한 것처럼, 역방향 조회가 필요할 때 양방향 매핑을 구현하자.

 

d) 연관 관계 주인을 정하는 기준

- 연관 관계의 주인을 설정할 때에는 비즈니스 로직을 기준으로 생각하면 안된다.

- 반드시 FK를 가진 테이블을 구현한 Entity가 연관 관계의 주인이 된다.

- 이는 규칙처럼 생각하고 이행하자.  

 

 

 

댓글