자바 ORM 표준 JPA 프로그래밍 - (5) 연관관계 매핑 기초

 Date: 2022-03-08

✓︎ 단방향 연관관계
✓︎ 연관관계 사용
✓︎ 양방향 연관관계
✓︎ 연관관계의 주인
✓︎ 양방향 연관관계 저장
✓︎ 양방향 연관관계의 주의점

핵심 키워드

  • 방향 : 단방향, 양방향이 있다.
  • 한 쪽이 어느 한 쪽만 참조하는 → 단방향 관계
  • 양쪽 모두 서로를 참조하는 것 → 양방향 관계
  • 방향은 객체관계에만 존재하고 테이블 관계는 항상 양방향이다.
  • 다중성 : 다대일(N:1), 일대다(1:N), 다대다(N:N) 다중성이 있다.
  • 관계의 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

1. 단방향 연관관계

  • 다대일(N:1) 단방향 관계
    • 회원(Member) - 팀(Team)
    • 회원은 하나의 팀에만 소속될 수 있다. → 다대일(N:1) 관계
    • 회원 객체는 Member.team 필드로 팀 객체와 연관관계를 맺는다. 반대로 팀은 회원을 알 수 없다.
    • 회원 테이블은 TEAM_ID 외래키로 팀 테이블과 연관관계를 맺는다.

    참조를 통한 연관관계는 언제나 단방향이다.
    테이블을 통한 연관관계는 언제나 양방향이다.
    그렇다면 객체 간의 양방향 관계는?
    객체 간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다.
    (정확히 이야기하면 이것은 양방향 관계가 아니라 서로 다른 단반향 관계 2개이다.)

객체 연관관계 vs 테이블 연관관계 정리

  • 참조를 사용하는 객체의 연관관계는 단방향이다.
    A → B (a.b)

  • 외래 키를 사용하는 테이블의 연관관계는 양방향이다.
    A JOIN B 가 가능하면 반대로 B JOIN A 도 가능

  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
    A → B (a.b)
    B → A (b.a)

1) 순수한 객체 연관관계

  • 객체 탐색 그래프: 객체는 참조를 사용해서 연관관계를 탐색할 수 있다.
    Team team = memer1.getTeam();
    

2) 테이블 연관관계

  • 데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라 한다.
    SELECT T.*
    FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
    WHERE M.MEMBER_ID = 'memeber1'
    

3) 객체 관계 매핑

  • JPA를 통한 연관 관계 매핑
    • 객체 연관관계 : 회원 객체의 Member.team 필드를 사용
    • 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    

4) @JoinColumn

  • @JoinColumn 을 생략하면 외래 키를 찾을 때 기본 전략을 사용한다.
    • 기본 잔략 : 필드명 + _ + 참조하는 테이블의 컬럼명

5) @ManyToOne

  • 다대일(@ManyToOne)과 비슷한 일대일(@OneToOne) 관계도 있다. 단방향 관계를 매핑할 때 둘 중 어떤 것을 사용해야 할지는 반대편 관계에 달려 있다.
    • 일대다(1:N) ↔︎ 다대일(N:1) / 일대일(1:1) ↔ 일대일(1:1)

2. 연관관계 사용

1) 저장

  • 회원 엔티티가 팀 엔티티를 참조하여 저장한다.
    void testSave() {
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);  //연관된 엔티티는 영속 상태여야 함
      
        Member member1 = new Member("member1", "회원1");
        member1.setTeam(team1);  //연관관계 설정 (member1 -> team1)
        em.persist(member1);
    }
    

📍 JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다.

2) 조회

  • 연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지이다.
    • 객체 그래프 탐색(객체 연관관계를 사용한 조회)
        Member member = em.find(Member.class, "member1");
        Team team = member.getTeam();
      
    • 객체지향 쿼리(JPAL) 사용
        String jpql = "select m from Member m join m.team t where t.name = :teamName";
        List<Member> result = em.createQuery(jpql, Member.class)
                            .setParameter("teamName", "팀1")
                            .getResultList();
      

3) 수정

  • 단순히 불러온 엔티티의 값만 변경하면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. em.update() 와 같은 메소드는 없다.

    Member member = em.find(Member.class, "member1");
    member.setTeam(team2);
    

4) 연관관계 제거

  • 연관관계를 null 로 설정한다.
    Member member = em.find(Member.class, "member1");
    member.setTeam(null);  //연관관계 제거
    

5) 연관된 엔티티 삭제

  • 연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다.
    membe1.setTeam(null);  //연관관계 제거
    em.remove(team1);  //팀 삭제
    

3. 양방향 연관관계

1) 양방향 연관관계 매핑

  • 일대다(1:N) 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.
    • Mamber.clss
      @ManyToOne
      @JoinColumn(name = "TEAM_ID")
      private Team team;
      
    • Team.class
      @OneToMany(mappedBy = "team")
      private List<Member> members = new ArrayList<>();
      

2) 일대다 컬렉션 조회

  • 객체 그래프 탐색을 사용
    Team team = em.find(Team.class, "team1");
    List<Member> members = team.getMembers(); //(팀 -> 회원)
    

4. 연관관계의 주인

  • 엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다.
  • JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

1) 양방향 매핑의 규칙 : 연관관계의 주인

  • 양방향 연관관계 매핑 시 연관관계의 주인을 정해야 한다.
    • 연관관계의 주인은 데이터베이스 연관관계와 매핑되고 외래키를 관리(등록, 수정, 삭제) 할 수 있다.
    • 연관관계의 주인이 아닌 쪽은 읽기만 할 수 있다.
  • 연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.

2) 연관관계의 주인은 외래 키가 있는 곳

  • 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.

    📍 데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne 에는 mappedBy 속성이 없다.

5. 양방향 연관관계 저장

  • 연관관계의 주인이 아닌 곳에 입력된 값은 외래 키에 영향을 주지 않는다.

    team.getMember().add(member);  //무시(연관관계의 주인 아님)
    member.setTeam(team);  //연관관계 설정(연관관계의 주인)
    

6. 양방향 연관관계의 주의점

  • 양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.

1) 순수한 객체까지 고려한 양방향 연관관계

  • 그렇다면 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까?
    객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전하다.
  • 객체를 고려한 양방향 연관관계
    Team team1 = new Team("team1", "팀1");
    em.persist(team);
      
    Member member1 = new Member("member1", "회원1");
      
    //양방향 연관관계 설정
    member1.setTeam(team1);
    team1.getMembers().add(member1);
    em.persist(member1);
    

2) 연관관계 편의 메소드

  • 양방향 연관관계는 결국 양쪽 다 신경을 써야 하는데 연관관계를 설정하는 메소드를 각각 호출하다보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.
  • 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라고 한다.
    public void setTeam(Team team) {
       this.team = team;
       team.getMembers().add(this);
    }
    

3) 연관관계 편의 메소드 작성 시 주의사항

  • 연관관계를 변경할 때 이전에 설정한 연관관계가 제거되지 않기 때문에 이전의 연관관계를 삭제하는 코드를 추가해야 한다.
    public void setTeam(Team team) {
       //기존 팀과 관계를 제거
       if (this.team != null) {
       		this.team.getMembers().remove(this);
       }
       this.team = team;
       team.getMembers().add(this);
    }
    

    객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.

7. 정리

- 단방향

  • 연관관계가 하나인 단방향 매핑은 언제나 연관관계의 주인이다.
  • 단방향은 항상 외래 키가 있는 곳을 기준으로 매핑하면 된다.

- 양방향

  • 단방향 매핑과 비교해서 양방향 매핑은 복잡하다. 연관관계의 주인도 정해야 하고, 두 개의 단방향 연관관계를 양방향으로 만들기 위해 로직도 잘 관리해야 한다.
  • 양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.
  • 연관관계 주인의 반대편은 mappedBy로 주인을 지정해야 한다.
  • 양방향 매핑 시에는 무한 루프에 빠지지 않게 조심해야 한다.
    • 예를 들어, Member.toString()에서 getTeam()을 호출하고, Team.toString()에서 getMember()를 호출하면 무한 루프에 빠질 수 있다.
    • 이런 문제는 엔티티를 JSON으로 변환할 때 자주 발생한다. JSON 라이브러리들은 보통 무한루프에 빠지지 않도록 하는 어노테이션이나 기능을 제공한다.
    • Lombok 라이브러리를 사용할 때도 자주 발생한다.