• 방향 : [단뱡향, 양방향]
    • 단방향 : 회원 -> 팀 또는 팀 -> 회원 둘 중 한쪽만 참조하는 것
    • 양방향: 회원 -> 팀, 팀 -> 회원 양쪽 모두 참조하는 것
  • 다중성 : [N:1, 1:N, 1:1, N:N]
  • 연관관계 주인 : 객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

1. 단방향 연관관계

다대일 단방향 연관관계
  • 객체 연관관계
    • Member.team 가능, 반대로 팀은 회원을 알 수 없음 (단방향)
  • 테이블 연관관계
    • team_id 외래키로 연관관계를 맺음
    • 둘 다 가능 (양방향)
      select * from Member M join Team T on M.....  
      select * from Team T join Member M on M.....  
      
  • 객체 연관관계와 테이블 연관관계의 가장 큰 차이
    • 참조를 통한 연관관계는 항상 단방향임. 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 함. => 양방향 연관관계
// 단방향
class A {
B b; }
  
class B {}
// 양방향
class A {
B b; }

class B {
A a; }
  • 객체 연관관계 vs 테이블 연관관계 정리
    • 객체 : 참조로 연관관계
      • 객체 그래프 탐색 : 참조를 사용하여 연관관계 탐색
    • 테이블 : 외래키로 연관관계

    이 둘은 비슷해보이지만 매우 다른 특징을 가짐.

순수한 객체 연관관계

@Setter
@Getter
@Entity
class Mamber{
    private String id;
    private String username;
    private Team team;
}

@Setter
@Getter
@Entity
class Team{
    private String id;
    private String name;
}

psvm{
    Team team = new Team("SKU", "서경 ");

    Member member1 = new Member("member1", "멤버1");
    Member member2 = new Member("member1", "멤버1");

    member1.setTeam(team);
    member2.setTeam(team);
    
    // 객체 그래프 탐색
    Team team1 = member1.getTeam();
}

객체 관계 매핑

public class Member {
// ...

@ManyToOne
@JoinColumn(name="Team_ID")
private Team team; 
// ... 
}

public class Team {
@Id 
@Column(name="Team_ID") // 테이블 연관관계 : Team_ID 외래키 사용 
private String id; 
// ...
}
  • @ManyToOne
    • 다대일 관계 매핑 정보. 회원과 팀은 다대일 관계다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
  • @JoinColumn(name=”Team_ID”)
    • 조인컬럼은 외래키를 매핑할 때 사용. name 속성에는 매핑할 외래키 이름을 지정한다. (생략 가능)

@JoinColumn

외래키를 매핑할 때 사용함.

  • 주요속성
@JoingColumn의 주요 속성

@ManyToOne

다대일 관계에서 사용함.

  • 주요속성
@ManyToOne 속성 .

2. 연관관계 사용

연관관계를 등록, 수정, 삭제, 조회하는 예제를 통해 연관관계를 어떻게 사용하는지 알아보자.

저장

psvm{
    Team team = new Team("SKU", "서경");
    em.persist(team);

    Member member1 = new Member("member1", "멤버1");
    Member member2 = new Member("member2", "멤버2");

    member1.setTeam(team);
    member2.setTeam(team);

    em.persist(member1);
    em.persist(member2);
}

이렇게 하면 아래와 같이 저장됨.

INSERT INTO TEAM VALUES("SKU", "서경");

INSERT INTO MEMBER VALUES("member1", "멤버1", "SKU");
INSERT INTO MEMBER VALUES("member2", "멤버2", "SKU");

조회

연관관계가 있는 엔티티를 조회하는 방법 2가지

  • 객체 그래프 탐색
    psvm{
      Member givenMember = em.find(Member.class, "member1");
      Team givenTeam = givenMember.getTeam();
    }
    
  • 객체지향 쿼리 사용(JPQL)
    select m from Member m join m.team t 
    where t.name=:teamName // ':teamName' : 파라미터 바인딩 문법
    

수정

psvm{
    Team team = new Team("SKU_new", "서경_new");

    Member givenMember = em.find(Member.class, "member1");
    givenMember.setTeam(team);
}
  • 변경 감지 -> update 필요 없음.

연관관계 제거

psvm{
    Member givenMember = em.find(Member.class, "member1");
    givenMember.setTeam(null);
}

연관된 엔티티 삭제

팀에 소속되어 있는 회원들의 연관관계를 먼저 끊어야 함.

psvm{
    member1.setTeam(null);
    member2.setTeam(null);

    em.remove(team);
}

3. 양방향 연관관계

현재: 단방향 관계 (Member -> Team)

  • Team -> Member 를 추가하면 양방향 관계 성립
양방향 객체 연관관계
  • 회원 -> 팀(Member.team)
  • 팀 -> 회원(Team.members)
    • jpa는 list, collection,set,map 등 다양한 컬렉션을 지원함

팀과 회원은 일대다 관계 => list members 추가!

@Setter
@Entity
class Team{
    private String id;
    private String name;

    //==추가==//
    @OneToMany(mappedBy = "team")
    private List<Member> memberList;
}

psvm{
    Team givenTeam = em.find(Team.class, "team");
    
    List<Member> givenMembers = givenTeam.getMembers();

    // do something...
}

mappedBy는 무엇일까?

4. 연관관계의 주인

엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐.
반면에, 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인 가능. (양방향)

  • 테이블 연관관계
    • 회원, 팀의 연관관계 1개(양방향)
  • 객체 연관관계
    • 회원 -> 팀 연관관계 1개(단방향)
    • 팀 -> 회원 연관관계 1개(단방향)
      엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래키는나하나이다. 따라서 둘 사이에 차이가 발생 -> 연관관계 주인을 정해야함! ==> mappedBy!!!

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

연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리할 수 있음.
반면에, 주인이 아닌 쪽은 읽기만 할 수 있음.

  • 주인은 mappedBy 속성을 사용X
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야함
    Q. 그렇다면 어떤 것을 연관관계의 주인으로 정해야 할까?
연관관계 주인 선택

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것.

  • Member.team 주인 -> 자기 테이블에 있는 외래키를 관리하면 됨.
  • Team.members 주인 -> 물리적으로 전혀 다른 테이블의 외래키를 관리해야 함.(member테이블에 관리해야 할 외래키가 있기 때문)

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

회원 테이블이 외래키를 갖고 있음 -> Member.team이 주인이 됨.

따라서 Team.members에는 mappedBy=”team”으로 주인이 아님을 설정함.

5. 양방향 연관관계 저장

psvm{
    team.getMembers().add(member1); // 무시됨
    team.getMembers().add(member2); // 무시됨

    member.setTeam(team); // 설정됨
}
  • 위의 코드에서 team.getMembers.add(member1)이 무시되는 이유는 team.members가 연관관계의 주인이 아니기 때문.
  • Member.team이 주인 -> member.setTeam(team)은 정상 입력됨.

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

흔히 하는 실수 : 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것.

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

객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다. JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있기 때문.

public void test순수한객체_양방향() {
    //팀1    
    Team team1 = new Team("team1", "팀1");  
    Member member1 = new Member("member1", "회원1");    
    Member member2 = new Member("member2","회원2");    
    member1.setTeam(team1); //연관관계 설정 member1 -> team1    
    member2.setTeam(team1); //연관관계 설정 member2 -> team1    
    List<Member> members = team1.getMembers();    
    System.out.println("members.size = " + members.size());
}

//결과: members.size = 0

연관관계 편의 메소드

아래처럼 리팩토링 해보자.

public class Member {

private Team team;

public void setTeam(Team team) {
  this.team = team;
  team.getMembers().add(this);
}
  // ...

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

member1.setTeam(teamA); // 1
member1.setTeam(teamB); // 2
Member findMember = teamA.getMember();  // member1이 여전히 조회된다.
  • teamB로 변경할 때 teamA -> member1 관계를 제거하지 않았다.
  • 아래 그림과 같은 문제 발생
삭제되지 않은 관계
  • 연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 한다.
// 수정된 편의 메소드 
public void setTeam(Team team) {

    // 기존 팀과 관계를 제거
    if(this.team != null) {
        this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}
  • 삭제되지 않은 관계2에서 teamA -> member1 관계가 제거되지 않아도 데이터베이스 외래 키를 변경하는 데는 문제가 없다.
  • teamA -> member1 관계를 설정한 Team.members는 연관관계의 주인이 아니기 때문이다.
  • 연관관계의 주인인 Member.team의 참조를 member1 -> teamB로 변경했으므로 데이터베이스에 외래 키는 teamB를 참조하도록 정상 반영 된다.
  • 그리고 그 이후에 새로운 영속성 컨텍스트에서 teamA를 조회해서 teamA.getMembers()를 호출하면 데이터베이스 외래 키에는 관계가 끊어져 있으므로 아무것도 조회되지 않는다.
  • 문제는 관계를 변경하고 영속성 컨텍스트가 아직 살아있는 상태에서 teamA의 getMembers()를 호출하면 member1이 반환된다는 점이다. 따라서 변경된 연관관계는 앞서 설명한 것처럼 관계를 제거하는 것이 안전하다.

댓글남기기