Spring/jpa

[JPA] 단방향 연관관계, 양방향 연관관계

라임온조 2023. 2. 13. 16:10

1. 방향의 의미

1) 단방향

회원과 팀이 관계가 있을 때 회원 -> 팀, 팀 -> 회원 둘 중 한 쪽만 참조하는 것

2) 양방향

회원과 팀이 관계가 있을 때 회원 -> 팀, 팀 -> 회원 모두 서로 참조하는 것

3) 특징

  • 객체 관계에만 존재하고 테이블 관계는 항상 양방향

2. 객체의 연관관계와 테이블의 연관관계 차이

1) 객체의 연관관계

특징

  • 참조로 연관관계를 맺는다
  • 연관된 데이터를 조회할 때 참조를 사용한다
  • 참조를 통한 연관관계는 언제나 단방향이다
  • 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 하지만 이것은 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.

예시

Member Team
id id
Team team name
username  

회원은 member.getTeam()을 통해 team을 참조, 팀은 team.getMember() 불가(해당 필드를 가지고 있지 않아서)

 

2) 테이블 연관관계

특징

  • 외래 키로 연관관계를 맺는다
  • 연관된 데이터를 조회할 때 테이블은 조인을 사용한다
  • 외래 키를 사용하는 테이블의 연관관계는 양방향이다, 한 테이블에 있는 외래키로 모든 방향에서 조인할 수 있다.

예시

Member Team
member_id(pk) team_id(pk)
team_id(fk) name
username  

select *

from Member m

join team t on m.team_id = t.team_id

 

select *

from Team t

join Member M on t.team_id = m.team_id

 

위 처럼, 외래 키 하나로 양방향 조회가 가능

 

전제조건: 회원 한 명은 하나의 팀에 소속 가능 -> 회원과 팀은 다대일, 팀과 회원은 일대다

3. 단방향 연관관계

1) 개념

객체에서 두 개의 엔티티가 있을 때, 한 엔티티는 다른 엔티티를 참조하지만, 다른 엔티티는 참조를 하지 않는 경우를 의미함

2) 방법

  • @ManyToOne 어노테이션 사용
  • @JoinColumn 어노테이션 사용
@Entity
public class Member{
	
    @Id
    @Column(name = "member_id")
    private String id;
    
    @Column(name = "username")
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
    
}
@Entity
public class Team{
	
    @Id
    @Column(name = "team_id")
    private String id;
    
    @Column(name = "name");
    private String name;
}

3) 특징

  • Member에서는 객체 그래프 이용해서 team 접근 가능
  • team에서는 member 접근 불가능

 

4. 양방향 연관관계

1) 개념

두 엔티티가 있을 때 한 엔티티에서 다른 엔티티를 참조하고, 다른 엔티티에서도 다른 엔티티를 참조하는 관계

2) 특징

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 완료되었다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.

3) 방법

  • @ManyToOne 어노테이션 사용
  • @JoinColumn 어노테이션 사용
  • @OneToMany 어노테이션 사용
@Entity
@Getter
@Setter
@Table(name = "team")
@AllArgsConstructor
@NoArgsConstructor
public class Team {

    @Id
    @Column(name = "team_id")
    private String id;

    @Column(name = "name")
    private String name;
    
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
}
package com.study.jpa.jpa_study.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;


import javax.persistence.*;

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "member")
public class Member {

    @Id
    @Column(name = "member_id")
    private String id;

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

    @ManyToOne
    @JoinColumn(name = "team_id")
    private Team team;
}

4) 연관관계의 주인

개념

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

 

특징

  • 연관관계의 주인만이 데베 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다.
  • 주인이 아닌 쪽은 읽기만 할 수 있다.
  • 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정한다.
  • 일대다, 다대일 관계에서는 항상 다 쪽이 외래 키를 가진다. 따라서 다 쪽이 연관관계의 주인이 되고, @ManyToOne에는 mappedBy 속성이 없다.

방법

  • 주인이 아닌 쪽에 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정한다.
  • 4-2에 있는 코드를 통해 설명하자면 Member 클래스에 있는 team 필드가 연관관계의 주인이고, Team 클래스에 있는 members는 연관관계의 주인이 아니다. 따라서 members 필드에는 mappedBy로 연관관계의 주인인 team을 적어야 한다.

5) 양방향 연관관계의 주의점

연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것

  • 데베에 외래 키 값이 정상적으로 저장되지 않는 문제를 야기한다

 

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전

  • 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제 발생 가능
    • 예를 들어 JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 했을 때 문제 발생 가능
  • 이를 편하게 하기 위해 연관관계 편의 메소드를 사용한다
    • 연관관계의 주인을 가지고 있는 엔티티의 set메소드를 수정
Team team1 = new Team();
team1.setId("team1");
team1.setName("팀1");

Member member1 = new Member();
member1.setId("member1");
member1.setUsername("회원1");

Member member2 = new Member();
member2.setId("member2");
member2.setUsername("회원2");

// 연관관계 설정
// member -> team
member1.setTeam(team1);
member2.setTeam(team1);
// team -> member
team1.getMembers().add(member1);
team1.getMembers().add(member2);
public void setTeam(Team team){
    if(this.team != null){ // 기존에 존재하던 연관관계를 삭제하는 코드
        this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}

무한 루프에 빠지지 않게 조심해야 한다

  • Member에서도 team을 호출하고, Team에서도 member를 호출하다가 발생하기 쉽다
  • json으로 변환할 때 발생하기 쉽다
    • json 라이브러리는 무한루프에 빠지지 않도록 하는 어노테이션이나 기능을 제공한다