[JPA] Chapter 5. 연관관계 매핑 기초 2 - 양방향 연관관계

joyful·2021년 7월 18일
0

JPA

목록 보기
9/18

들어가기 앞서

이 글은 김영한 님의 저서 「자바 ORM 표준 JPA 프로그래밍」을 학습한 내용을 정리한 글입니다. 모든 출처는 해당 저서에 있습니다.


5.3 양방향 연관관계

  • 객체 연관관계

    • 회원 → 팀(Member.team) : 다대일(N:1) 관계
    • 팀 → 회원(Team.members) : 일대다(1:N) 관계
      • 여러 건과 연관관계 맺기 가능 → 컬렉션 사용
  • 테이블 연관관계

    • 외래 키(TEAM_ID) 하나로 양방향 조회 가능 → 처음부터 양방향 관계
      • 회원 → 팀(MEMBER JOIN TEAM)
      • 팀 → 회원(TEAM JOIN MEMBER)

5.3.1 양방향 연관관계 매핑

💻 회원 엔티티

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;
    
    //연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
    }
    
    //Getter, Setter ...
}

💻 팀 엔티티

@Entity
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;
    
    private String name;
    
    //==추가==//
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();
    
    //Getter, Setter ...
}
  • @OneToMany
    • 일대다(1:N) 관계를 나타내는 매핑 정보
    • 속성설명기본값
      mappedBy◾ 연관관계의 주인 필드 선택
      ◾ 값으로 반대쪽 매핑의 필드 이름 제공
      fetch글로벌 패치 전략 설정FetchType.LAZY
      cascade속성 전이 기능 사용
      targetEntity◾ 연관된 엔티티의 타입 정보 설정
      ◾ 거의 사용하지 않음
      ◾ 컬렉션 사용해도 제네릭으로 타입 정보 알 수 있음

5.3.2 일대다 컬렉션 조회

💻 일대다 방향으로 객체 그래프 탐색

public void biDirection() {

    Team team = em.find(Team.class, "team1");
    List<Member> members = team.getMembers();  //팀 → 회원
    					       //객체 그래프 탐색
                               
    for(Member member : members) {
        System.out.println("member.username = " + member.getUsername());
    }
}

//==결과==//
//member.username = 회원1
//member.username = 회원2


5.4 연관관계의 주인

  • 객체 연관관계

    • 객체에는 양방향 연관관계 존재 x
      → 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어 양방향인 것 처럼 보이게 함
      ex) 회원 → 팀(단방향), 팀 → 회원(단방향)
  • 테이블 연관관계

    • 데이터베이스 테이블은 외래 키 하나로 양쪽 조인 가능
      외래 키 하나만으로 양방향 연관관계 맺음
      ex) 회원 ↔ 팀(양방향)

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

✅ 문제

엔티티를 양방향 연관관계로 설정 시 객체의 참조는 둘인데 외래 키는 하나
→ 둘 사이에 차이 발생

✅ 해결 방안

JPA에서 연관관계의 주인(Owner)을 지정

💡 연관관계의 주인(Owner)

연관관계를 맺고 있는 두 객체 중 테이블의 외래 키를 관리하는 객체

주인주인x
기능◾ 데이터베이스 연관관계와 매핑
◾ 외래 키 관리(등록, 수정, 삭제)
읽기만 가능
mappedBy 사용XO

✅ 결정

💡 연관관계의 주인 결정 = 외래 키 관리자 선택

💻 회원 → 팀(Member.team) 방향

class Member {
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    ...
}
  • 자기 테이블의 외래 키 관리

💻 팀 → 회원(Team.members) 방향

class Team {
    @OneToMany
    private List<Member> members = new ArrayList<Member>();
  • 물리적으로 전혀 다른 테이블의 외래 키 관리

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

💻 연관관계 주인 설정

class Team {

    @OneToMany(mappedBy="team")  //MappedBy 속성의 값은
    				 //연관관계의 주인인 Member.team
    private List<Member> members = new ArrayList<Member>();
}

💻 다이어그램

  • 데이터베이스 테이블의 다대일(N:1), 일대다(1:N) 관계에서는 항상 다(N)쪽이 외래 키를 가진다.
  • 다(N) 쪽인 @ManyToOne은 항상 연관관계의 주인이 됨
    mappedBy 설정 불가(mappedBy 속성이 없는 이유)


5.5 양방향 연관관계 저장

💻 양방향 연관관계 저장

public void testSave() {

    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    //회원1 저장
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1);  //연관관계 설정 member1 → team1
    em.persist(member1);
    
    //회원2 저장
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1);  //연관관계 설정 member2 → team1
    em.persist(member2);
}

💻 회원 테이블 조회

SELECT * FROM MEMBER;

💻 결과

MEMBER_IDUSERNAMETEAM_ID
member1회원1team1
member2회원2team1
  • TEAM_ID 외래 키에 팀의 기본 키 값 저장된 상태
  • 양방향 연관관계는 연관관계의 주인이 외래 키 관리
    → 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래 키 값 정상 입력 됨
  • 주인이 아닌 곳에 이벽된 값은 외래 키에 영향 x
  • Member.team
    • 연관관계의 주인
    • 엔티티 매니저가 이곳에 입력된 값 사용하여 외래 키 관리


5.6 양방향 연관관계의 주의점

양방향 연관관계 설정 후 연관관계의 주인이 아닌 곳에만 값을 입력하지 않도록 주의한다.

💻 예시

public void testSaveNonOwner() {

    //회원1 저장
    Member member1 = new Member("member1", "회원1");
    em.persist(member1);
    
    //회원2 저장
    Member member2 = new Member("member2", "회원2");
    em.persist(member2);
    
    Team team1 = new Team("team1", "팀1");
    //주인이 아닌 곳만 연관관계 설정
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);
    
    em.persist(team1);
}

💻 회원 테이블 조회

SELECT * FROM MEMBER;

💻 결과

MEMBER_IDUSERNAMETEAM_ID
member1회원1null
member2회원2null
  • 연관관계의 주인이 아닌 Team.member에만 값을 저장
  • 연관관계의 주인인 Member.team에 아무 값도 입력하지 않음

💡 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.


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

💻 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 → team2
    
    //팀에 소속된 회원 수 출력
    List<Member> members = team1.getMembers();
    System.out.println("members.size = " + members.size());
}

//결과 : members.size = 0
  • Member.team에만 연관관계 설정
//회원 → 팀
member1.setTeam(team1);
member2.setTeam(team1);
  • 반대 방향에도 연관관계 설정 필요
team1.getMembers().add(member1);  //팀 → 회원

💻 양방향 모두 관계 설정

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
    team1.getMembers().add(member1); //연관관계 설정 team1 → member1
    
    member2.setTeam(team1);	     //연관관계 설정 member2 → team2
    team1.getMembers().add(member2); //연관관계 설정 team1 → member2
    
    List<Member> members = team1.getMembers();
    System.out.println("members.size = " + members.size());
}

//결과: members.size = 2

💻 JPA로 코드 완성

public void testORM_양방향() {

    //팀1 저장
    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    Member member1 = new Member("member1", "회원1");
    
    //양방향 연관관계 설정
    member1.setTeam(team1);	     //연관관계 설정 member1 → team1
    team1.getMembers().add(member1); //연관관계 설정 team1 → member1
    em.persist(member1);
    
    Member member2 = new Member("member2", "회원2");
    
    //양방향 연관관계 설정
    member2.setTeam(team1);	     //연관관계 설정 member2 → team2
    team1.getMembers().add(member2); //연관관계 설정 team1 → member2
    em.persist(member2);
}
  • Member.team
    • 연관관계의 주인
    • 외래 키 관리
  • Team.members
    • 연관관계의 주인 x
    • 저장 시 사용 x

💡 주의 사항

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

∴ 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주는 것이 좋다.


5.6.2 연관관계 편의 메소드

한 번에 양방향 관계를 설정하는 메소드


✅ 개별 호출시 문제점

member.setTeam(team);
team.getMembers().add(member);
  • 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있음

✅ 코드 리팩토링

💻 Member 클래스의 setTeam() 메소드 수정

public class Member {

    private Team team;
    
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }
    ...
}
  • setTeam() 메소드 하나로 양방향 관계 모두 설정하도록 변경

💻 연관관계 설정하는 부분 수정

//연관관계 설정
member1.setTeam(team1);
member2.setTeam(team1);

//==기존 코드 삭제 시작==//
//teamA.getMembers().add(member1);  //팀1 → 회원1;
//teamA.getMembers().add(member2);  //팀1 → 회원2;
//==기존 코드 삭제 종료==//

✅ 양방향 리팩토링 전체 코드

public void testORM_양방향_리팩토링() {

    Team team1 = new Team("team1", "팀1");
    em.persist(team1);
    
    Member member1 = new Member("member1", "회원1");
    member1.setTeam(team1);  //양방향 설정
    em.persist(member1);
    
    Member member2 = new Member("member2", "회원2");
    member2.setTeam(team1);  //양방향 설정
    em.persist(member2);
}

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

✅ 문제점

setTeam() 메소드에는 버그가 존재한다.

member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember();  //member1이 여전히 조회된다.
  1. member1.setTeam(teamA) 호출 직후

  2. member1.setTeam(teamB) 호출 직후

    • teamB로 변경 시 teamA → member1 관계 제거 x

💡 참고

  1. teamA → member1 관계가 제거되지 않아도 데이터베이스 외래 키 변경에는 문제 x
    → 관계를 설정한 Team.members가 연관관계의 주인이 아니기 때문
  2. 연관관계의 주인인 Member.team의 참조를 member1 → teamB로 변경
    → 데이터베이스의 외래 키가 teamB 정상 참조
  3. 새로운 영속성 컨텍스트에서 teamA를 조회하여 teamA.getMembers() 호출
    => 데이터베이스 외래 키의 관계 끊어진 상태 → 조회 x
  4. 관계 변경 후 영속성 컨텍스트 유지 상태에서 teamAgetMembers() 호출
    member1 반환
    변경된 연관관계관계제거하는 것이 안전함

✅ 해결 방안

연관관계 변경 시 기존에 참조하고 있는 객체 존재한다면, 기존에 참조하고 있던 객체와의 연관관계를 삭제하는 코드를 추가해야 한다.

public void setTeam(Team team) {

    //기존 팀과 관계를 제거
    if(this.team != null) {
        this.team.getMembers().remove(this);
    }
    
    this.team = team;
    team.getMembers().add(this);
}


5.7 정리

✅ 단방향

  • 단방향 매핑은 언제나 연관관계의 주인이다.
  • 단뱡향 매핑만으로 테이블과 객체의 연관관계 매핑이 가능하다.

✅ 양방향

  • 양방향은 반대방향으로 객체 그래프 탐색 기능이 추가된다.
  • 양방향은 주인이 아닌 연관관계를 하나 추가한 것이다.
    • 주인의 반대편은 mappedBy로 주인을 지정해야 한다.
    • 주인의 반대편은 단순히 보여주는 일(객체 그래프 탐색)만 가능하다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.

✅ 연관관계의 주인을 정하는 기준

  • 연관관계의 주인 = 외래 키 관리자
  • 기준 : 외래 키의 위치
    • 단방향 → 외래 키가 있는 곳
    • 양방향 → 외래 키가 있는 다(N) 쪽

🔥 주의 사항

  1. 양방향 매핑 시 무한 루프에 주의할 것

    • Lombok이 자동으로 생성하는 toString()을 사용하지 않도록 한다.

      MembertoString() 호출
      TeamtoString()membersmembertoString() 호출
      ∴ 무한 루프 생성 및 스택오버플로우 발생

    • JSON 생성 라이브러리

      • 양방향 관계의 엔티티를 JSON으로 시리얼라이즈 하는 순간 무한루프 발생
      • 엔티티를 JSON으로 변환 시 무한루프 발생
        → 컨트롤러에서 엔티티를 직접 반환하지 말고, DTO로 변환해서 반환하도록 한다.
  2. 연관관계의 주인으로 일대다(1:N)도 선택 가능
    → 성능과 관리 측면에서 권장하지 않는다.
    ex) 팀 엔티티의 Team.members




📖 참고

profile
기쁘게 코딩하고 싶은 백엔드 개발자

0개의 댓글