5장 연관관계 매핑 기초

sua·2023년 5월 10일
0

엔티티들은 대부분 다른 엔티티와 연관관계가 있다.
but, 객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래키를 사용해서 관계를 맺는다. -> 이 둘은 완전히 다른 특징을 갖는다.
객체 관계 매핑(ORM)에서 가장 어려운 부분이 바로 객체 연관관계와 테이블 연관관계를 매핑하는 일이다.
객체의 참조와 테이블의 외래 키를 매핑하는 것이 이장의 목표다.

방향은 객체관계에만 존재하고, 테이블 관계는 항상 양방향이다.
객체를 양방향 연관관계로 만들면 연관관계의 주인을 정해야 한다.

5.1 단방향 연관관계

연관관계 중에선 다대일(N:1) 단방향 관계를 가장 먼저 이해해야 한다.
지금부터 회원과 팀의 관계를 통해 다대일 단뱡항 관계를 알아보자.

  • 회원과 팀이 있다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.

객체 연관관계

  • 회원 객체는 Member.team 필드(멤버변수)로 팀 객체와 연관관계를 맺는다.
  • 회원 객체와 팀 객체는 단뱡항 관계다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다. 예를 들어 member.getTeam()으로 팀을 조회 가능하지만 반대 방향은 접근하는 필드는 없다.

테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
  • 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다. 예를 들어 MEMBER 테이블의 TEAM_ID외래키 하나로 MEMBER JOIN TEAMTEAM JOIN MEMBER 둘 다 가능하다.

외래 키 하나로 어떻게 양방향으로 조인하는지 알아보자.
아래 코드는 회원과 팀을 조인하는 SQL이다.

SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

다음 코드는 반대인 팀과 회원을 조인하는 SQL이다.

SELECT *
FROM TEAM TT
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

객체 연관관계와 테이블 연관관계의 가장 큰 차이
객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 한다. 결국 연관관계를 하나 더 만들어야 한다. -> 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 한다.
but, 정확히 말하면 이건 서로 다른 단방향 관계 2개다.
반면에 테이블은 외래키 하나로 양방향으로 조인할 수 있다.

다음은 단방향 연관관계다.

class A {
	B b;
}
class B {}

다음은 양방향 연관관계다.

class A {
	B b;
}
class B {
	A a;
}

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

  • 객체는 참조(주소)로 연관관계를 맺는다.
  • 테이블은 외래키로 연관관계를 맺는다.
  • 참조를 사용하는 객체의 연관관계는 단방향이다.
  • 외래키를 사용하는 테이블의 연관관계는 양방향이다.
  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.

5.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 ...
}

아래 코드를 실행해서 회원1과 회원2를 팀1에 소속시키자.

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

    Team findTeam = member1.getTeam();
}

회원1과 회원2를 팀1에 소속했고 회원1이 소속한 팀1을 getTeam()으로 조회할 수 있다.
이처럼 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색 이라 한다.


5.1.2 테이블 연관관계

아래 코드는 회원 테이블과 팀 테이블의 DDL이다. 추가로 회원 테이블의 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 VARACHAR(255) NOT NULL,
    NAME VARCHAR(255),
    PRIMARY KEY (TEAM_ID)
)

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

다음 sql을 실행해서 회원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');

다음 sql을 실행해서 회원1이 소속된 팀을 조회해보자.

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

이처럼 데이터베이스는 외래키를 사용해서 연관관계를 탐색할 수 있는데 이것을 조인이라고 한다.


5.1.3 객체 관계 매핑

JPA를 사용해서 둘을 매핑해보자.

  • 객체 연관관계 : 회원 객체의 Member.team 필드 사용
  • 테이블 연관관계 : 회원 테이블의 MEMBER.TEAM_ID 외래키 컬럼을 사용

Member.team과 MEMBER.TEAM_ID를 매핑하는 것이 연관관계 매핑이다.

@Entity
@Table(name="MEMBER", uniqueConstraints = {@UniqueConstraint( // 추가
        name = "NAME_AGE_UNIQUE",
        columnNames = {"NAME", "AGE"} )})
public class Member {
    @Id
    @Column(name = "ID")
    private String id; // 아이디

    @Column(name = "NAME", nullable = false, length = 10) // 추가
    private String username; // 이름
    
    // 연관관계 매핑
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;
}

회원 엔티티를 매핑하고, 팀 엔티티를 매핑했다.
연관관계 매핑 코드를 분석해보자.

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

회원 엔티티에 있는 연관관계 매핑 부분인데 연관관계를 매핑하기 위한 새로운 어노테이션들이 있다.

  • @ManyToOne : 이름 그대로 다대일 관계라는 매핑 정보다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 한다.
  • @JoinColumn(name = "TEAM_ID") : 조인 컬럼은 외래키를 매핑할 때 사용한다. name 속성에는 매핑할 외래키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID외래키로 연관관계를 맺으므로 이 값을 지정하면 된다.

5.1.4 @JoinColumn 주요 속성

속성기능기본값
name매핑할 외래키 이름필드명 + _ + 참조할 테이블의 기본키 컬럼명
referencedColumnName외래키가 참조하는 대상 테이블의 컬럼명참조하는 테이블의 기본키 컬럼명
foreignKey(DDL)외래키 제약조건을 직접 지정할 수 있다. 이 속성은 테이블을 생성할 때만 사용한다.
unique, nullable, insertable, updatable, columnDefinition, table@Column의 속성과 같다.

5.1.5 @ManyToOne 속성

이 어노테이션은 다대일 관계에서 사용한다.

속성기능기본값
optionalfalse로 설정하면 연관된 엔티티가 항상 있어야 한다.true
fetch글로벌 페치 전략을 설정한다.@ManyToOne=FetchType.EAGER, @OneToMany=FetchType.LAZY
cascade영속성 전이 기능을 사용한다.
targetEntity연관된 엔티티의 타입 정보를 설정한다. 이 기능은 거의 사용하지 x, 컬렉션을 사용해도 제네릭으로 타입 정보를 알 수 있다.

다음 코드는 targetEntity 속성의 사용 예다.

@OneToMany
private List<Member> members; // 제네릭으로 타입 정보를 알 수 있다.

@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없다.

5.2 연관관계 사용

5.2.1 저장

연관관계를 매핑한 엔티티를 어떻게 저장하는지 보자.

public void testSave() {
    // 팀1 저장
    Team team1 = new Team("team1", "팀1");
    empersist(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);
}

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

이 코드를 보자.

member1.setTeam(team1); // 연관관계 설정 member1 -> team1
em.persist(member1);

회원 엔티티는 팀 엔티티를 참조하고 저장했다. JPA는 참조한 팀의 식별자(Team.id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성한다. 이때 실행된 SQL은 다음과 같다.

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')

5.2.2 조회

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

  • 객체 그래프 탐색(객체 연관관계를 사용한 조회)
  • 객체지향 쿼리 사용 JPQL

방금 저장한 대로 회원1, 회원2가 팀1에 소속해 있다고 가정하자.

객체 그래프 탐색

member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있다.

Member member = em.find(Member.class, "member1");
Team team = member.getTeam(); // 객체 그래프 탐색
System.out.println("팀 이름 = " + team.getTeam());
// 출력 결과 : 팀 이름 = 팀1

이렇게 객체를 통해 연관된 엔티티를 조회하는 것을 객체 그래프 탐색이라고 한다.


객체지향 쿼리 사용

회원을 대상으로 조회하는데 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색조건으로 사용해야 한다. SQL은 연관된 테이블을 조인해서 검색조건을 사용하면 된다. JPQL도 조인을 지원한다.
팀1에 소속된 모든 회원을 조회하는 JPQL을 보자.

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

JPQL의 from Member m join m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드(m.team)을 통해서 MemberTeam을 조인했다. 그리고 where 절을 보면 조인한 t.name을 검색조건으로 사용해서 팀1에 속한 회원만 검색했다.
아래 JPQL을 보자. 참고로 :teamName과 같이 :로 시작하는 것은 파라미터를 바인딩받는 문법이다.

select m from Member m join m.team t 
where t.name=:teamName

이때 실행되는 SQL은 다음과 같다.

SELECT M.* FROM MEMBER MEMBER
INNER JOIN
    TEAM TEAM ON MEMBER.TEAM_ID = TEAM1_.ID
WHERE
    TEAM1_.NAME='팀1'

실행된 SQL과 JPQL을 비교하면 JPQL은 객체(엔티티)를 대상으로 하고 SQL 보다 간결하다.


5.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가 자동으로 처리한다.


5.2.4 연관관계 제거

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

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

실행되는 연관관계 제거 SQL은 다음과 같다.

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

5.2.5 연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 한다. 그렇지 않으면 외래키 제약조건으로 인해 데이터베이스에서 오류가 발생한다.
팀1에는 회원1과 회원2가 소속되어 있다. -> 팀1을 삭제하려면 연관관계를 먼저 끊어야 한다.

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

5.3 양방향 연관관계

이제 팀에서 회원으로 접근하는 관계를 추가하자. 그래서 회원에서 팀으로 접근하고 반대 방향인 팀에서도 회원으로 접근할 수 있도록 양방향 연관관계 로 매핑해보자.

회원과 팀은 다대일 관계다. 반대로 팀에서 회원은 일대다 관계다. 일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다.
Team.membersList 컬렉션으로 추가했다.
객체 연관관계를 정리하면 다음과 같다.

  • 회원 -> 팀 (Member.team)
  • 팀 -> 회원 (Team.members)

데이터베이스 ㅌ이블은 외래키 하나로 양방향으로 조회할 수 있다. -> 데이터베이스에 추가할 내용은 전혀 없다.


5.3.1 양방향 연관관계 매핑

먼저 회원 엔티티를 보자.

@Entity
@Table(name="MEMBER", uniqueConstraints = {@UniqueConstraint( // 추가
        name = "NAME_AGE_UNIQUE",
        columnNames = {"NAME", "AGE"} )})
public class Member {
    @Id
    @Column(name = "ID")
    private String id; // 아이디

    @Column(name = "NAME", nullable = false, length = 10) // 추가
    private String username; // 이름

    // 연관관계 매핑
    @ManyToOne
    @JoinColumn(name="TEAM_ID")
    private Team team;
    
    // 연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
    }
	...
}

다음으로 팀 엔티티를 보자.

@Entity
public class Team {
    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;
    
    //==추가==//
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
}

팀과 회원은 일대다 관계이므로 팀 엔티티에 컬렉션인 List<Member> members를 추가했다. 그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다. -> 반대쪽 매핑이 Member.team이므로 team을 값으로 주었다.
이것으로 양방향 매핑을 완료했고 이제 팀에서 회원 컬렉션으로 객체 그래프 탐색할 수 있다.


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 연관관계의 주인

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

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

양방향 연관관계 매핑 시 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.

  • 주인은 mappedBy 속성을 사용하지 않는다.
  • 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 한다.

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


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

연관관계의 주인은 테이블에 외래키가 있는 곳으로 정해야 한다. 여기서는 회원 테이블이 외래키를 가지고 있으므로 Member.team이 주인이 된다. 주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 설정한다.


5.5 양방향 연관관계 저장

양방향 연관관계를 사용해서 팀1, 회원1, 회원2를 저장해보자.

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 필드를 통해서 회원과 팀의 연관관계를 설정하고 저장했다.
참고로 이 코드는 단방향 연관관계에서 살펴본 예제의 회원과 팀을 저장하는 코드와 완전히 같다.

양방향 연관관계는 연관관계의 주인이 외래키를 관리하기 때문에 주인이 아닌 방향은 값을 설정하지 않아도 데이터베이스에 외래키 값이 정상 입력된다.
즉,

tea1m.getMembers().adD(member1);

이런 코드는 없다는 뜻이다.


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

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 것이다.
데이터베이스에 외래키 값이 정상적으로 저장되지 않으면 이를 의심해야 한다.

주인이 아닌 곳에만 값을 설정하면 어떻게 되는지 코드로 알아보자.

public void testSaveNowOnwer() {
    // 회원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);
}

회원1, 회원2를 저장하고 팀의 컬렉션에 담은 후에 팀을 저장했다. 데이터베이스에서 회워 테이블을 조회하게 되면 외래키 TEAM_ID에 team1이 아닌 null 값이 입력된다.
이는 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다.


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

연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 되는가? -> 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.

JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정해보자. ORM은 객체와 관계형 데이터베이스 둘다 중요하다. 데이터베이스 뿐만 아니라 객체도 함께 고려해야 한다. 아래 코드를 보자.

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
    em.persist(member2);

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

// 결과: members.size = 0

해당 코드는 JPA를 사용하지 않는 순수한 객체다. Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았다. 팀에 소속된 회원이 몇명인지 출력해보면 결과는 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 = team1.getMembers();
    System.out.println("members.size = " + members.size());
}

// 결과: members.size = 2

양쪽 모두 관계를 설정했고, 결과도 기대했던 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);
}

양쪽에 연관관계를 설정했다. 따라서 순수한 객체 상태에서도 동작하며 테이블의 외래키도 정상 입력 된다.

  • Member.team : 연관관계의 주인, 이 값으로 외래키를 관리한다.
  • Team.members: 연관관계의 주인이 아니다. 따라서 자장 시에 사용되지 않는다.

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


5.6.2 연관관계 편의 메소드

양방향 연관관계는 결국 양쪽 다 신경 써야 한다.
memgber.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() 메소드 하나로 양방향 관계를 모두 설정하도록 변경했다.

연관관계를 설정하는 부분을 수정한 양방향 리팩토링 전체코드를 살펴보자.

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); // 1
member1.setTeam(teamB); // 2
Member findMember = teamA.getMember(); // member1이 여전히 조회된다.

이 시나리오를 그림으로 분석해보자. 먼저 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);
}

해당 코드는 객체에서 서로 다른 단방향 연관관계 2개를 양방향인 것 처럼 보이게 하려고 얼마나 많은 고민과 수고가 필요한지 보여준다.
객체에서 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 한다.


5.7 정리

양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐이다.

member.getTeam(); // 회원 -> 팀
team.getMembers(); // 팀 -> 회원 (양방향 매핑으로 추가된 기능)

주인의 반대편은 mappedBy로 주인을 지정해야 한다. 그리고 주인의 반대편은 단순히 보여주는 일(객체 그래프 탐색)만 할 수 있다.

양방향 매핑은 복잡하다. 비즈니스 로직의 필요에 따라 다르겠지만 우선 단방향 매핑을 사용하고 반대 방향으로 객체 그래프 탐색 기능(JPQL 쿼리 탐색 포함)이 필요할 때 양방향을 사용하도록 코드를 추가해도 된다.

연관관계의 주인은 외래키의 위치와 관련해서 정해야지 비즈니스 중요도로 접근하면 안된다.

profile
가보자고

0개의 댓글