6장. 다양한 연관관계 매핑

이윤설·2024년 6월 2일

규칙

어떠한 관계든지, FK는 항상 참조하는 테이블에 위치한다.
단, 일대다 단방향 관계는 예외다.

6.1 다대일

다대일 관계의 반대 방향은 항상 일대다 관계고, 일대다 관계의 반대 방향은 항상 다대일 관계다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽이다.

6.1.1 다대일 단방향(N:1)

@Entity
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;
    
    private String username;
    
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team;

}
@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

}

회원은 Member.team으로 팀 엔티티를 참조할 수 있다.
반대로 팀에는 회원을 참조하는 필드가 없으므로
회원과 팀은 다대일 단방향 연관관계다.

@JoinColumn(name = "TEAM_ID")의 의미는, Member.team 필드를 TEAM_ID 외래 키와 매핑한다는 의미이다. 따라서 Member.team 필드로 회원 테이블의 TEAM_ID 외래 키를 관리한다.

6.1.2 다대일 양방향(N:1, 1:N)


실선이 연관관계의 주인이고, 점선은 연관관계의 주인이 아니다.
따라서 Member.team이 연관관계의 주인이고, Team.members는 연관관계의 주인이 아니다.


@Entity
public class Member {

    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String username;

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

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

        if (!team.getMembers().contains(this)) {
            team.getMembers().add(this);
        }
    }
@Entity
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<Member>();

    public void addMember(Member member) {
        this.members.add(member);
        if (member.getTeam() != this) {
            member.setTeam(this);
        }
    }
}
  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인이다.
    따라서 Member.team이 연관관계의 주인이다.
    연관관계의 주인의 역할: 외래 키를 관리한다.
    연관관계의 주인이 아닌 것의 역할: JPQL이나 그래프 탐색에 사용

  • 양방향 연관관계는 항상 서로를 참조해야한다.
    어느 한 쪽만 참조하면 양방향 연관관계가 성립하지 않는다.
    항상 서로 참조하게 하려면 연관관계 편의 메소드(setter)를 작성하는 것이 좋다.
    양쪽 둘 다 작성하면 무한루프가 발생하기 때문에 둘 중 하나만 호출하면 된다.

6.2 일대다

일대다 관계는 다대일 관계의 반대 방향이다. 엔티티를 하나 이상 참조할 수 있으므로 Collection, List, Set, Map 을 사용한다.

6.2.1 일대다 단방향 (1:N)

하나의 야구팀은 여러 야구선수들을 참조할 수 있다. 이러한 경우를 일대다 관계라고 한다. 그리고 야구팀은 야구선수들을 참조하지만 반대로 회원을 야구팀을 참조하지 않으면 둘의 관계는 단방향이다.

주의할 점이 하나있는데, 일대다 단방향 매핑에서는 참조되는 테이블에 FK가 있다.
왜냐하면 일대다 관계에서 외래 키는 항상 다쪽 테이블에 있다.
하지만 다 쪽인 Member 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없다.
대신에 반대쪽인 Team 엔티티에만 참조 필드인 members가 있다.
따라서 반대편 테이블이 외래 키를 관리하는 것이다.

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

        private String name;

        @OneToMany
        @JoinColumn(name = "TEAM_ID") // Member 테이블의 TEAM_ID로 설정 (FK)
        private List<Member> members = new ArrayList<Member>();
    }
}
@Entity
public class Member {
    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String username;
}

일대다 단방향 관계를 매핑할 때는, 무조건 @JoinColumn을 명시해야 한다.
그렇지 않으면 JPA는 연결테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑한다.

😥 일대다 단방향 매핑의 단점
매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 것이 단점이다.
본인 테이블에 외래 키가 있으면 엔티티의 저장과 연관관계 처리를 INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 한다.

😀 결론: 다대일 양방향 매핑을 사용하자!
일대다 매핑은, 위 단점 때문도 있지만, 관리도 부담스럽다.
두 매핑의 테이블 모양은 완전히 같으므로 엔티티만 약간 수정하면 된다.

정리

  1. 일반적으로 FK는 "참조하는" 테이블에 위치한다.
    예를 들어 주문-주문상품 관계에서 주문상품 테이블에 주문의 PK를 FK로 가진다.

  2. 하지만 "일대다 관계"에서는 예외적으로 "다" 쪽 테이블에 "1" 쪽의 PK를 FK로 가진다.
    예를 들어 부서(1)-직원(N) 관계에서 직원 테이블에 부서의 PK를 FK로 가진다.

이렇게 하는 이유는 다음과 같다.
a. 데이터 무결성 보장: "다" 쪽에 FK를 두어 참조 관계를 정의하고 데이터 무결성을 지킨다.
b. 중복 데이터 방지: "1" 쪽에 "다" 쪽 데이터를 중복 저장하면 문제가 발생하다.
c. 조인 성능 향상: "다" 쪽 행 수가 많으므로 인덱스 활용에 유리하다.

즉, 일대다 관계에서는 "다" 쪽 테이블이 "참조되는" 테이블이지만, 모델링 규칙에 따라 예외적으로 FK를 갖게 되는 것이다.

-> 자세한 내용은 따로 작성한 게시글을 참고하기.

6.2.2 일대다 양방향 (1:N, N:1)

이런 매핑은 공식적으로 존재하지 않는다.
양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없기 때문이다.
왜냐하면 관계형 db 특성상 일대다, 다대일 관계는 항상 다 쪽에 외래 키가 있다. 따라서 @OneToMany, @ManyToOne 둘 중에 연관관계의 주인은 항상 다 쪽인 @ManyToOne을 사용한 곳이다. 그러한 이유로 @ManyToOne에는 mappedBy 속성이 없다.

아무튼 읽기전용으로 매핑을 추가하면 되는데, 그냥 존재하지 않는다고 알아두자.

일대일 (1:1)

회원은 하나의 사물함만 사용하고 사물함도 하나의 회원에 의해서만 사용된다.
이러한 관계를 일대일 관계라고 부른다.

  • 일대일 관계는 그 반대도 일대일 관계다.
  • 테이블 관계에서 일대다, 다대일은 항상 다(N)쪽이 외래 키를 가진다. 반면에 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느곳이나 외래 키를 가질 수 있다.
  • 따라서 일대일 관계는 주 테이블이나 대상 테이블 중에 누가 외래 키를 가질지 선택해야 한다.
  • 주 테이블에 외래 키: 주 테이블이 외래키를 갖고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다.
  • 대상 테이블에 외래 키: 전통적인 DB 개발자들은 보통 대상 테이블에 외래 키를 두는 것을 선호한다. 이 방법의 장점은 테이블 관계를 일대일에서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있다.

6.3.1 주 테이블에 외래키

일대일 관계를 구성할 때 객체지향 개발자들은 주 테이블에 외래 키가 있는 것을 선호한다. JPA도 주 테이블에 외래 키가 있으면 좀 더 편하게 매핑할 수 있다.

일대일 단방향


MEMBER가 주 테이블이고 LOCKER는 대상 테이블이다.
회원은 하나의 Locker만 가지고, Locker는 하나의 회원에 의해서만 사용된다.
@OneToOne 어노테이션을 사용하고, 이 관계는 다대일 단방향(@ManyToOne)과 거의 비슷하다.

일대일 양방향

양방향이므로 연관관계의 주인을 정해야 한다.
MEMBER 테이블이 외래 키를 가지고 있으므로 Member.locker가 연관관계의 주인이다.
따라서 Locker.member는 mappedBy를 선언해서 연관관계의 주인이 아니라고 선언했다.

위 그림을 보면 객체 연관관계에서는 Locker 객체는 member 필드가 있지만,
테이블 연관관계에서는 Locker 테이블이 member에 대한 필드가 아예 없다.
JPA를 사용할 때는 객체 간의 양방향 참조를 구현하기 위해 서로에 대한 참조를 포함해야 한다. 즉, Member 객체는 Locker 객체에 대한 참조를 갖고, Locker 객체도 Member 객체에 대한 참조를 갖는다.

그러나 데이터베이스 관점에서는, 양방향 참조를 구현하기 위해 두 테이블 간에 두 개의 외래 키(FK)를 설정할 필요가 없다.
양방향 참조는 실제로 데이터베이스 쿼리를 통해 구현되며, 한 테이블에만 외래 키를 두어 관계를 정의한다.
이 경우 Member 테이블이 주 테이블이 된다. Member 테이블에 Locker를 참조하는 LOCKER_ID 외래 키를 두어 Member와 Locker 사이의 일대일 관계를 정의하는 것이다.

cf. DB는 단방향, 양방향 참조 모두 SQL 쿼리가 동일하다.

// JPA 일방향 참조
class Member {
    Long id;
    Locker locker;
    String username;
}

class Locker {
    Long id;
    String name;
}


// JPA 양방향 참조
class Member {
    Long id;
    Locker locker;
    String username;
}

class Locker {
    Long id;
    String name;
    Member member;
}

JPA는 일방향/양방향 참조여부에 따라 코드가 달라진다.
하지만 SQL 쿼리문은 모두 동일하다.
JPA와는 다르게 데이터베이스에서 일방향과 양방향 참조는 주로 이론적이고 설계적인 개념일 뿐이다.
실제 데이터베이스에서의 참조 구현 방식은 외래키를 통해 이루어지며, 일방향과 양방향 참조 모두 외래키를 기반으로 하기 때문에 SQL 쿼리는 동일하다.

// Member에서 Locker로 접근 (일방향)
// Member에서 Locker로 접근 (양방향)
// Locker에서 Member로 접근 (양방향)
SELECT * FROM Member m
JOIN Locker l ON m.Locker_id = l.id
WHERE m.id = 1;

cf. A가 B를 참조한다는 것은 무엇을 뜻하고 pk,fk는 어떻게 이루어질까?

데이터베이스에서 A 테이블이 B 테이블의 데이터를 참조하는 관계를 의미한다.
A 테이블에 B 테이블의 기본 키(PK)를 외래 키(FK)로 저장한다.

A 테이블 (참조하는 테이블)
A_ID (PK)
A_NAME
B_ID (FK)

B 테이블 (참조되는 테이블)
B_ID (PK)
B_NAME

6.3.2 대상 테이블에 외래키 (중요)

일대일 단방향


대상 테이블에 외래 키가 있는 단방향 관계는 JPA가 지원하지 않는다.
더군다나 이런 모양으로 매핑할 수도 없다.

일반적인 RDB 설계에서 FK는 관계를 맺고자 하는 두 테이블 중 '참조하는' 테이블에 위치한다. 이는 '참조된' 테이블의 PK를 가리키기 위함이다.
예를 들어 Member 테이블이 Locker를 참조할 때, 관계는 다음과 같이 이루어진다.

  • Member 테이블: locker_id(FK)
  • Locker 테이블: locker_id(PK)

이러한 구조에서 Locker 테이블에 Member 테이블을 참조하게 하는 것은 (=외래 키를 두는 것은) 일반적인 관계 설정에 어긋난다. 왜냐하면, 외래키는 '참조되는' 테이블의 기본키를 '참조하는' 테이블에서 가리키는 역할을 하기 때문이다. 그래서 대상 테이블에 외래키 - 일대일 단방향 매핑은 불가능 하다는 것이다.

따라서 단방향 관계를 Locker에서 Member 방향으로 수정하거나, 양방향 관계로 만들고 Locker를 연관관계의 주인으로 설정해야 한다.

일대일 양방향

@Entity
public class Locker {
    @Id
    @GeneratedValue
    @Column(name = "LOCKER_ID")
    private Long id;

    private String name;

    @OneToOne
    @JoinColumn(name = "MEMBER_ID")
    private Member member;
}

@Entity
public class Member {
    @Id
    @GeneratedValue
    @Column(name = "MEMBER_ID")
    private Long id;

    private String username;

    @OneToOne(mappedBy = "member")
    private Locker locker;

}

일대일 매핑에서 대상 테이블에 외래키를 두고싶으면 이렇게 양방향으로 매핑한다.
주 엔티티인 Member 엔티티 대신 대상 엔티티인 Locker를 연관관계의 주인으로 만들어서 Locker 테이블의 외래 키를 관리하도록 했다.
왜냐하면 외래키인 memberID를 Locker가 소유하고 있기 때문이다.
<- 양방향 관계에서 연관관계의 주인은 항상 외래키를 들고 있는 엔티티가 된다.

6.4 다대다

profile
화려한 외면이 아닌 단단한 내면

0개의 댓글