Skip to content

Latest commit

 

History

History
608 lines (548 loc) · 27.1 KB

[민하] 5장.md

File metadata and controls

608 lines (548 loc) · 27.1 KB

연관관계 매핑 기초

Introduction

  • 연관관계

    • 객체 : 참조(즉, 주소)를 이용해 관계를 맺음
    • 테이블 : 외래 키를 이용해 관계를 맺음
      but, 참조와 외래 키는 완전히 다른 특징을 가짐!!!!!

    → 이번 시간에는 객체의 참조와 테이블의 외래 키를 매핑하는 방법을 배울 것이다

  • 💛핵심 키워드💛

    • 회원과 팀이 관계가 있다고 하자
    • 방향(Direction)
      • 단방향 관계 : 회원→팀 또는 팀→회원 둘 중 한 쪽만 참조하는 것
      • 양방향 관계 : 회원→팀, 팀→회원 양쪽 모두 서로 참조하는 것
      • 방향은 객체관계에만 존재하고, 테이블 관계는 항상 양방향이다
    • 다중성(Multiplicity)
      • 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M)가 존재한다
      • 예를 들어 한 팀에 여러 회원이 소속될 수 있으므로 팀과 회원은 1:N 관계이다
    • 연관관계의 주인(Owner)
      • 양방향 매핑시 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리하는 연관관계의 주인을 정해야 한다

Body

1. 단방향 연관관계(N:1)

  • 회원과 팀이 있다
  • 회원은 하나의 팀에만 소속될 수 있다
  • 회원과 팀은 N:1 관계이다
    image
    위의 그림을 분석해보자
  1. 객체 연관관계
  • 회원 객체와 팀 객체는 단방향 관계이다
  • Member.team 필드를 통해 회원은 팀을 알 수 있지만, 반대로 팀은 회원을 알 수 없다
    • 예를 들어 member→team의 조회는 member.getTeam()으로 가능하지만 team→member를 접근하는 필드는 없다
  1. 테이블 연관관계
  • 회원 테이블과 팀 테이블은 양방향 관계이다
  • 회원 테이블의 TEAM_ID 외래 키를 통해 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다
    • 예를 들어 MEMBER 테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAM과 TEAM JOIN MEMBER 둘 다 가능하다
  • 회원과 팀을 조인하는 SQL
    SELECT *
    FROM MEMBER M
    JOIN TEAM T ON M.TEAM_ID = T.ID
  • 팀과 회원을 조인하는 SQL
    SELECT *
    FROM TEAM T
    JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
  1. 객체 연관관계 VS 테이블 연관관계 비교
    비교 객체 연관관계 테이블 연관관계
    연관 맺는 방법 참조(주소) 외래 키
    방향 단방향 양방향
    비고 서로 다른 단방향 관계 2개를 통해 양쪽에서 서로 참조 가능(양방향 연관관계) -

1.1 순수한 객체 연관관계

JPA를 사용하지 않은 순수한 회원과 팀 클래스의 코드

public class Member {
    private String id;
    private String username;

    private Team team; //팀의 참조를 보관

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

    //Getter, Setter ...
}
public class Team {
    private String id;
    private String name;

    //Getter, Setter ...
}
public static void main(String[] args) {
    //생성자(id, 이름)
    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");
    Team team1 = new Team("team1", "팀1");

    member1.setTeam(team1);
    member2.setTeam(team1);

    //회원1이 속한 팀1을 조회
    Team findTeam = member1.getTeam();
}

image
위의 그림을 보았을 때(발캡처 죄삼다) 회원1과 회원2는 팀1에 소속해있고, 코드에서 회원1이 속한 팀1을 조회할 수 있다.
이처럼 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라고 한다.

1.2 테이블 연관관계

데이터베이스 테이블의 회원과 팀의 관계
(1) 회원 테이블과 팀 테이블을 생성하고, 추가로 회원 테이블의 TEAM_ID에 외래 키 제약조건을 설정하였음

CREATE TABLE MEMBER (
    MEMBER_ID VARCHAR(255) NOT NULL,
    TEAM_ID VARCHAR(255),
    USERNAME VARCHAR(255),
    PRIMARY KEY (MEMBER_ID)
)

CREATE TABLE TEAM (
    TEAM_ID VARCHAR(255) NOT NULL,
    NAME VARCHAR(255),
    PRIMARY KEY (TEAM_ID)
)

ALTER TABLE MEMBER ADD CONSTRAINT FK_MEMBER_TEAM (
    FOREIGN KEY (TEAM_ID)
    REFERENCES TEAM
)

(2) 회원1과 회원2를 팀1에 소속시킴

INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1','팀1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member1','team1','회원1');
INSERT INTO MEMBER(MEMBER_ID, TEAM_ID, USERNAME) VALUES('member2','team1','회원2');

(3) 회원1이 소속된 팀을 조회

SELECT T.*
FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.ID
WHERE M.MEMBER_ID = 'member1'

(1)~(3)의 과정으로 데이터베이스는 외래 키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라고 한다.

1.3 객체 관계 매핑

1.1과 1.2에서는 객체와 테이블 각각에서의 연관관계를 알아보았고 이제 JPA를 사용해서 둘을 매핑해보자
image
객체 연관관계는 회원 객체의 Member.team 필드를 사용하였고, 테이블 연관관계는 회원 테이블의 MEMBER.TEAM_ID 외래 키 컬럼을 사용하였다.

@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;

    //Getter, Setter ...
}

회원 객체의 Member.team과 회원 테이블의 MEMBER.TEAM_ID를 매핑하는 것이 연관관계 매핑이다. 아래의 코드가 연관관계 매핑 코드이다.

@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
  • @ManyToOne : 회원과 팀이 N:1 관계라는 매핑 정보다.연관관계를 매핑할 때 다중성을 나타내는 annotation은 필수로 사용해야 한다.
    속성 기능 기본값
    optional false로 설정하면 연관된 엔티티가 항상 있어야한다 true
    fetch 글로벌 페치 전략 설정 FetchType.EAGER / FetchType.LAZY
    cascade 영속성 전이 기능 사용 -
    targetEntity 연관된 엔티티의 타입 정보를 설정(거의 사용X) -
    image
  • @JoinColumn(name="TEAM_ID") : 외래 키를 매핑할 때 사용한다. name 속성에는 매핑할 외래 키 이름을 지정한다. 생략 가능하다. 생략시 기본 전략(필드명+_+참조하는 테이블의 컬럼명)을 사용한다.
    속성 기능 기본값
    name 매핑할 외래키 이름 필드명+_+참조하는 테이블의 기본키 컬럼명
    referencedColumnName 외래키가 참조하는 대상 테이블의 컬럼명 참조하는 테이블의 기본키 컬럼명
    foreignKey 외래키 제약조건을 직접 지정 가능 -
    unique, nullable, insertable, updatable, columnDefinition, table @Column의 속성과 같다 -
    image

2. 연관관계 사용

연관관계를 매핑한 엔티티를 어떻게 사용하는지 알아보자

2.1 저장

회원과 팀을 저장하는 코드

//주의!!! : JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태여야 한다

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); //저장
}

회원1과 회원2를 저장하는 부분에서 회원 엔티티는 팀 엔티티를 참조하고 저장했다. JPA는 참조한 팀의 식별자(Team.id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다. 이 때 실행된 SQL은 아래와 같다. 여기서 회원 테이블의 외래 키 값으로 참조한 팀의 식별자 값인 team1이 입력된 것을 확인할 수 있다.

INSERT INTO TEAM(TEAM_ID, NAME) VALUES('team1', '팀1')
INSERT INTO MEMBER(MEMBER_ID, NAME, TEAM_ID) VALUES('member1', '회원1', 'team1')
INSERT INTO MEMBER(MEMBER_ID, NAME, TEAM_ID) VALUES('member2', '회원2', 'team1')
SELECT M.MEMBER_ID, M.NAME, M.TEAM_ID, T.NAME AS TEAM_NAME
FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

위의 SELECT 문을 이용하여 조회를 하면 아래와 같은 결과가 나온다.

MEMBER_ID NAME TEAM_ID TEAM_NAME
member1 회원1 team1 팀1
member2 회원2 team1 팀1

2.2 조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 객체 그래프 탐색(객체 연관관계를 사용한 조회)과 객체지향 쿼리 사용(JPQL) 2가지가 있다.

  1. 객체 그래프 탐색(객체 연관관계를 사용한 조회)
    member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.

    Member member = em.find(Member.class, "member1");
    Team team = member.getTeam(); //객체 그래프 탐색
    System.out.println("팀 이름 = " + team.getName());
    
    //출력 결과 : 팀 이름 = 팀1
  2. 객체지향 쿼리 사용(JPQL)
    회원을 대상으로 조회하는데 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색 조건으로 사용해야 한다. 연관된 테이블을 조인해서 검색 조건을 사용하면 된다.

    private static void queryLogicJoin(EntityManager em) {
        String jpql = "select m from Member m join m.team t where " + "t.name=:teamName";
    
        List<Member> resultList = em.createQuery(jpql, Member.class)
            .setParameter("teamName", "팀1");
            .getResultList();
    
        for (Member member : resultList) {
            System.out.println("[query] member.username=" + member.getUserName());
        }
    }
    
    //결과 : [query] member.username=회원1
    //결과 : [query] member.username=회원2
    • from Member m join m.team t : 회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해서 Member와 Team을 조인했다.
    • where t.name=:teamName : t.name을 검색조건으로 사용해서 팀1에 속한 회원만 검색한다. :로 시작하는 것은 파라미터를 바인딩받는 문법이다.
    SELECT M.*
    FROM
        MEMBER MEMBER INNER JOIN TEAM TEAM ON MEMBER.TEAM_ID = TEAM1_.ID
    WHERE
        TEAM1_.NAME='팀1'

    위의 JPQL을 실행했을 때 실행되는 SQL은 위와 같다. 실행된 SQL와 JPQL을 비교하면 JPQL은 객체(엔티티)를 대상으로 하고 SQL보다 간결하다. (JPQL 관련 내용은 10장에서 나와요)

2.3 수정

팀1 소속이던 회원을 새로운 팀2에 소속하도록 수정

private static void updateRelation(EntityManager em) {

    //새로운 팀2
    Team team2 = new Team("team2", "팀2");
    em.persist(team2);

    //회원1에 새로운 팀2 설정
    Member member = em.find(Member.class, "member1");
    member.setTeam(team2);
}

실행되는 수정 SQL은 다음과 같다

UPDATE MEMBER
SET TEAM_ID='team2', ...
WHERE ID='member1'

수정은 em.update() 같은 메소드가 없다. 단순히 불러온 엔티티의 값만 변경해두면 트랜잭션을 커밋할 때 플러시가 일어나면서 변경 감지 기능이 작동한다. 그리고 변경사항을 데이터베이스에 자동으로 반영한다.
이것은 연관관계를 수정할 때도 같은데, 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리한다.

2.4 연관관계 제거

회원1을 팀에 소속하지 않도록 변경

private static void deleteRelation(EntityManager em){
    Member member1 = em.find(Member.class, "member1");
    member1.setTeam(null); //연관관계 제거
}

연관관계를 null로 설정했다. 이 때 실행되는 연관관계 SQL은 아래와 같다.

UPDATE MEMBER
SET TEAM_ID=null, ...
WHERE ID='member1'

2.5 연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생한다.

member1.setTeam(null); //회원1 연관관계 제거
member2.setTeam(null); //회원2 연관관계 제거
em.remove(team); //팀 삭제

3. 양방향 연관관계

위에서는 회원에서 팀으로만 접근하는 다대일 단방향 매핑을 알아보았다. 이번에는 반대 방향인 팀에서 회원으로 접근하는 관계를 추가하여, 팀과 회원이 서로 접근할 수 있도록 양방향 연관관계로 매핑해보자.

  1. 객체 연관관계
    image
  • 회원에서 팀은 N:1 관계(회원→팀, Member.team)
  • 팀에서 회원은 1:N 관계(팀→회원, Team.members)
  • 1:N 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션(List, Collection, Set, Map 등)을 사용해야 한다. Team.members를 List 컬렉션으로 추가했다.
  1. 테이블 연관관계
    image
  • 데이터베이스 테이블은 외래 키 하나만으로 양방향 조회가 가능하므로 처음부터 양방향 관계이기 때문에 추가할 내용은 없다.
  • 즉 TEAM_ID 외래 키 하나만으로 MEMBER JOIN TEAM과 TEAM JOIN MEMBER가 모두 가능하다.

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 ...
}
  • 팀과 회원은 일대다 관계이므로 팀 엔티티에 컬렉션인 List members를 추가하였다.
  • 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.
    • mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다. 자세한건 연관관계의 주인에서..

3.2 일대다 컬렉션 조회

양방향 매핑이 완료되었기 때문에 이제 팀에서 회원 컬렉션으로 객체 그래프를 탐색할 수 있다. 이를 이용해 팀1에 소속된 회원을 찾아 출력해보자.

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

4. 연관관계의 주인

mappedBy가 필요한 이유

  • 객체 연관관계
    • 회원→팀 연관관계 1개 (단방향)
    • 팀→회원 연관관계 1개 (단방향)
    • 단방향 2개를 로직으로 잘 묶어서 양방향처럼 보이게 함, 양방향은 아님
  • 테이블 연관관계
    • 회원↔팀 연관관계 1개 (양방향)
    • 외래키로 양방향에서 서로 조인 가능
  • 엔티티 단방향 vs 양방향
    • 단방향 : 참조를 하나만 사용하여 이 참조로 외래키를 관리하면 됨
    • 양방향 : 객체의 참조는 둘인데 외래키는 하나여서 어떤 관계를 사용해서 외래키를 관리할 지 알 수 없음
    • 따라서 양방향에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 한다.

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

  • 양방향 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.
  • 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있고, 주인이 아닌 쪽은 읽기만 할 수 있다.
  • 어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.
    • 주인은 mappedBy 속성을 사용하지 않는다.
    • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.
  • 연관관계의 주인 정하는 방법
    image
    //회원→팀(Member.team) 방향
    class Member {
        @ManyToOne
        @JoinColumn(name="TEAM_ID)
        private Team team;
        ...
    }
    //팀→회원(Team.members) 방향
    class Team {
        @OneToMany
        private List<Member> members = new ArrayList<Member>();
        ...
    }
    • 연관관계의 주인 정하는 것 : 외래 키 관리자를 선택하는 것, 여기서는 MEMBER 테이블의 TEAM_ID 외래 키를 관리할 관리자를 선택
    • 회원 엔티티의 Member.team을 주인으로 선택 : 자기 테이블에 있는 외래 키를 관리하면 된다
    • 팀 엔티티의 Team.members를 주인으로 선택 : Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리해야할 외래 키는 MEMBER 테이블에 있어 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 함

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

  • 연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
  • 여기서는 MEMBER 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
  • 주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해 주인이 아님을 설정하고, mappedBy 속성의 값으로는 연관관계의 주인인 Member 엔티티의 team 필드를 주면 된다.
    class Team {
        @OneToMany(mappedBy="team") //MappedBy 속성의 값은 연관관계의 주인인 Member.team
        private List<Member> members = new ArrayList<Member>();
    }
    image
  • 정리 : 연관관계의 주인(외래 키가 있는 곳)만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다. 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지는 못한다.
  • 참고 : 데이터베이스 테이블의 N:1, 1:N 관계에서는 항상 N 쪽이 외래 키를 가진다. N 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy를 설정할 수 없다. 따라서 @ManyToOne에는 mappedBy 속성이 없다.

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);
}

위의 예제에서 팀1을 저장하고 회원1, 회원2에 연관관계의 주인인 Member.team 필드를 통해서 회원과 팀의 연관관계를 설정하고 저장했다. 데이터베이스에서 회원 테이블을 조회하면 결과는 아래와 같다.

MEMBER_ID USERNAME TEAM_ID
member1 회원1 team1
member2 회원2 team1

TEAM_ID 외래키에 팀의 기본키 값이 저장되어 있다. 양방향 연관관계는 주인이 외래키를 관리해서 주인이 아닌 방향은 값을 설정하지 않아도 DB에 외래키 값이 정상 입력된다.

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");
    //주인이 아닌 곳(Team.members)만 연관관계 설정
    team1.getMembers().add(member1);
    team1.getMembers().add(member2);

    em.persist(team1);
}

회원1, 회원2를 저장하고 팀의 컬렉션에 담은 후 팀을 저장했다. 데이터베이스에서 회원 테이블을 조회한 결과는 아래와 같다.

MEMBER_ID USERNAME TEAM_ID
member1 회원1 null
member2 회원2 null

TEAM_ID에 null이 입력되어 있는 이유는 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다.
연관관계의 주인만이 외래 키의 값을 변경할 수 있다. 위의 예제에서는 연관관계의 주인인 Member.team에 아무 값도 입력하지 않기 때문에 TEAM_ID 외래 키의 값이 null이 저장된다.

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→team1

    List<Member> members = test1.getMembers();
    System.out.println("members.size = " + members.size());
}

//결과 : members.size = 0

위의 코드는 JPA를 사용하지 않는 순수한 객체이고 연관관계의 주인에만 값을 저장하고 반대 방향은 설정하지 않아서 팀1에 소속된 회원 수를 출력했을 때 0이 나온다. 따라서 이 경우 아래의 코드처럼 수정하여 양방향에서 모두 연관관계를 설정해야 한다.

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→team1
    team1.getMembers().add(member2); //연관관계 설정 team1→member2

    List<Member> members = test1.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→team1
    team1.getMembers().add(member2); //연관관계 설정 team1→member2
    em.persist(member2);
}

결론 : 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어주자!!

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);
    }
}
public void testORM_양방향_리팩토링() {
    //팀1
    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);
}

이렇게 한 번에 양방향 관계를 설정하는 메소드를 연관관계 편의 메소드라 한다.

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

앞선 코드의 문제점

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

image
왼쪽 그림은 member1.setTeam(teamA)를 호출한 직후 객체 연관관계이고, 오른쪽 그림은 member1.setTeam(teamB)를 호출한 직후 객체 연관관계이다.
문제점 : teamB로 변경할 때 teamA→member1 관계를 제거하지 않았다. 연관관계 변경 시 기존의 연관관계를 삭제해야 한다.

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

Conclusion

  • 단방향 매핑과 비교해서 양방향 매핑은 복잡하다
    • 단뱡향 매핑은 언제나 연관관계의 주인이지만, 양방향 매핑은 연관관계의 주인을 설정해줘야 한다.
      • 단방향 매핑에 주인이 아닌 연관관계를 하나 추가한 것과 같다.
      • 연관관계의 주인 반대편은 mappedBy로 주인을 지정해야 하고, 읽기만 가능하다.
    • 두 개의 단방향 연관관계를 양방향으로 만들기 위해 로직도 잘 관리해야 한다.
    • 양방향의 장점 : 연관관계의 주인 반대방향으로 객체 그래프 탐색 기능이 추가되었다.