ERD를 설계하고, 엔티티 간에 관계를 매핑할 때 항상 헷갈리는 개념이 있습니다.
일대다, 다대다 이런 관계는 알겠는데, 식별 관계로 할지, 비식별 관계로 할지, 그럼 엔티티 간에 관계는 양방향일지 단방향일지, 그럼 외래 키의 주인은 누구로 할지를 추가적으로 정해야 합니다.
이 개념들이 혼동되지 않도록 다시 정리하고자 합니다.
식별 관계와 비식별 관계
식별 관계 (Identifying Relationship)
식별 관계는 부모 테이블의 기본 키(Primary Key, PK)가 자식 테이블의 기본 키의 일부로 포함되는 관계이다.
즉, 자식 테이블에서 부모 테이블의 기본 키가 자식 테이블의 기본 키에 포함되어, 두 테이블 간의 관계가 강하게 묶여 있다. 이 관계는 자식 테이블의 기본 키가 부모 테이블의 기본 키에 의존적임을 의미한다.
예시
- 부모 테이블: Order (주문)
- order_id (PK)
- 자식 테이블: OrderItem (주문 항목)
- order_id (PK, FK)
- item_id (PK)
OrderItem 테이블에서 기본 키는 (order_id, item_id)로 구성된다. 여기서 order_id는 부모 테이블 Order의 기본 키이자, 자식 테이블 OrderItem의 기본 키의 일부가 된다. 즉, OrderItem 테이블의 기본 키는 부모 테이블의 기본 키와 결합된 식별자이다.
비식별 관계 (Non-Identifying Relationship)
비식별 관계는 부모 테이블의 기본 키가 자식 테이블에 외래 키(Foreign Key, FK)로 포함되지만, 자식 테이블의 기본 키에는 포함되지 않는 관계이다. 자식 테이블은 부모 테이블과 독립적으로 자신의 고유한 기본 키를 가진다. 이 관계는 두 테이블 간의 연결이 있지만, 자식 테이블이 부모 테이블의 기본 키에 의존하지 않는 것을 의미한다.
예시:
- 부모 테이블: User (사용자)
- user_id (PK)
- 자식 테이블: Cart (장바구니)
- cart_id (PK)
- user_id (FK)
Cart 테이블의 기본 키는 cart_id입니다. user_id는 부모 테이블 User의 기본 키로서, Cart 테이블에 외래 키로 포함되지만, 기본 키의 일부가 되지는 않는다. 따라서 Cart 테이블의 기본 키는 user_id와 관계없이 고유하다.
관계의 방향
데이터베이스에서
데이터베이스 테이블에서 관계는 기본적으로 외래 키(Foreign Key)를 통해 구현된다. 외래 키는 한 테이블이 다른 테이블의 기본 키(Primary Key)를 참조하도록 설정함으로써 두 테이블 간의 관계를 정의한다.
이때 데이터베이스에서 관계는 양방향으로 해석된다. 외래 키 관계가 설정되면, 이를 통해 두 테이블 간의 관계가 쿼리에서 자유롭게 탐색될 수 있다. 예를 들어, JOIN 연산을 사용해 어느 방향으로든 데이터를 조회할 수 있다.
위 그림처럼 단순히 겹치는 부분이 있으면 그 부분으로 조회를 하면 되기 때문에, 어느 한쪽에서만 데이터를 조회할 수 있는 경우는 없다.
엔티티에서
엔티티 관계 모델에서는 관계의 방향성이란 개념이 존재한다. 이는 객체 간의 참조가 명시적으로 설정되어야 하기 때문이다. 엔티티는 객체 지향 프로그래밍의 개념을 기반으로 하며, 여기서 객체는 다른 객체를 참조하거나 소유할 수 있지만, 참조가 설정되지 않은 객체를 직접 접근할 수 없다.
예를 들어, 고객(Customer) 엔티티와 음식(Food) 엔티티의 관계를 생각해 보자.
- 단방향 관계: 고객 엔티티가 음식 엔티티를 참조하지 않는 경우, 고객 엔티티에서는 음식에 대한 정보를 직접 조회할 방법이 없다. 음식 정보를 조회하려면 고객 엔티티에서 음식 엔티티에 대한 참조가 필요하다.
- 양방향 관계: 고객 엔티티가 음식 엔티티를 참조하고, 음식 엔티티도 고객 엔티티를 참조하는 경우, 두 엔티티 간의 데이터를 자유롭게 조회할 수 있다. 이는 데이터베이스의 JOIN 연산과 유사한 방식으로 양방향으로 데이터를 탐색할 수 있게 한다.
외래 키의 주인
이러한 관계의 방향 때문에, 어느 엔티티에 다른 엔티티를 참조할 필드를 추가할 것인가 라는 고민이 생긴다. 이를 정하는 외래 키의 주인이라는 개념이 있다.
외래 키의 주인은 외래 키(Foreign Key)를 실제로 소유하고 관리하는 엔티티를 의미한다.
개인적으로 이게 헷갈렸는데, 자기 PK를 준거니까 외래 키의 주인은 이 외래 키를 빌려준 엔티티 아닌가? 싶었다.
하지만 FK를 물려받은 엔티티가 외래 키의 주인이다.
Entity에서 외래 키의 주인은 일반적으로 N(다)의 관계인 Entity이다.
단방향 관계에서 일대다 외래 키의 주인 설정
여기서 1 : N에서 N 관계의 테이블이 외래 키를 가질 수 있기 때문에 외래 키는 N 관계인 users 테이블에 외래 키 칼럼을 만들어 추가하지만, 외래 키의 주인인 음식 Entity를 통해 관리한다.
코드로 표현하면 다음과 같다.
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToMany
@JoinColumn(name = "food_id") // users 테이블에 food_id 컬럼
private List<User> userList = new ArrayList<>();
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
단방향이기 때문에 User는 Food의 PK를 가지고 있지 않다.
참고: 단방향 관계에서는 일대다보다 다대일이 더 일반적이다.
- 단방향 1대다 관계는 양방향 1대다에 비해 덜 효율적일 수 있다. JPA는 Food 엔티티가 어떤 User 엔티티들과 연결되어 있는지 알기 위해 추가적인 SQL 쿼리를 실행해야 할 수 있다.
- 단방향 1대다 관계에서 외래 키를 관리하는 Food 엔티티만 있고, 반대 방향의 참조가 없기 때문에, 데이터 무결성 관리가 어렵고 불필요한 중간 테이블이 생성될 수 있다.
- 따라서 아래처럼 User가 Food를 ManytoOne으로 구현하는 것이 더 일반적이다.
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "food_id") // users 테이블에 외래 키 food_id를 추가
private Food food;
}
양방향 관계에서 일대다 외래 키의 주인 설정
1 대 N 관계에서는 일반적으로 양방향 관계가 존재하지 않다. 따라서 1 대 N 관계에서 양방향 관계를 맺으려면 음식 Entity를 외래 키의 주인으로 정해주기 위해 고객 Entity에서 mappedBy 옵션을 사용해야 한다.
양방향 관계에서 외래 키의 주인을 지정해 줄 때 @OneToMany에 mappedBy 옵션을 사용한다.
mappedBy는 이 옵션 값이 관계의 주인이 아니며, 양방향 관계에서 참조된 필드가 관계의 주인임을 명시한다. 양방향 관계에서 mappedBy 옵션을 생략할 경우 JPA가 외래 키의 주인 Entity를 파악할 수가 없어 의도하지 않은 중간 테이블이 생성되기 때문에 반드시 설정해 주는 게 좋다.
@ManyToOne 어노테이션은 mappedBy 속성을 제공하지 않는다. 이 이유는 @ManyToOne 관계에서 외래 키의 주인은 항상 N 측 엔티티이기 때문이다. 즉, User 엔티티(여기서는 N 측 엔티티)가 Food 엔티티(여기서는 1 측 엔티티)를 참조할 때, 외래 키는 User 엔티티에 위치하게 된다.
@Entity
@Table(name = "food")
public class Food {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
@OneToMany(mappedBy = "food")
private List<User> userList = new ArrayList<>();
}
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "food_id") // User 테이블에 food_id 컬럼 생성
private Food food;
}
위 코드는 Food와 User를 양방향으로 설정하며, Food는 mappedBy 속성을 통해 User가 참조하고 있는 Food 필드명을 넣어준다. 이를 통해 User 엔티티의 food 필드는 외래 키의 주인(관계의 주인)이 아니며, 참조하고 있는 User가 외래키의 주인임을 나타낸다.
@JoinColumn(name = "food_id")은 users 테이블에 food_id라는 외래 키 컬럼을 생성하여, 이 필드가 Food 엔티티를 참조하도록 한다.
외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 사용하지 않아도 default 옵션이 적용되기 때문에 생략이 가능하다. 다만 1 대 N 관계에서 외래 키의 주인 Entity가 @JoinColumn() 애너테이션을 생략한다면 JPA가 외래 키를 저장할 컬럼을 파악할 수가 없어서 의도하지 않은 중간 테이블이 생성된다. 따라서 외래 키의 주인 Entity에서 @JoinColumn() 애너테이션을 활용하시는게 좋다.
정리
식별 관계 (Identifying Relationship)
- 특징: 부모 테이블의 기본 키가 자식 테이블의 기본 키에 포함되어, 자식 테이블의 기본 키가 부모 테이블의 기본 키에 의존적이다.
- 예시: Order(부모)와 OrderItem(자식) 관계에서, OrderItem의 기본 키는 order_id와 item_id의 조합으로, order_id는 부모 테이블의 기본 키이기도 하다.
비식별 관계 (Non-Identifying Relationship)
- 특징: 부모 테이블의 기본 키가 자식 테이블에 외래 키로 포함되지만, 자식 테이블의 기본 키에는 포함되지 않는다. 자식 테이블의 기본 키는 독립적이다.
- 예시: User(부모)와 Cart(자식) 관계에서, Cart의 기본 키는 cart_id이며, user_id는 외래 키로만 존재한다.
관계의 방향
- 데이터베이스: 외래 키를 통해 두 테이블 간의 관계는 양방향으로 해석될 수 있으며, 쿼리에서 자유롭게 탐색할 수 있다.
- 엔티티: 객체 지향 프로그래밍에서는 관계의 방향성이 존재하며, 객체 간의 참조가 명시적으로 설정되어야 한다. 단방향 관계에서는 한쪽에서만 참조가 가능하지만, 양방향 관계에서는 양쪽 모두에서 데이터를 참조할 수 있다.
외래 키의 주인
- 정의: 외래 키를 실제로 소유하고 관리하는 엔티티를 의미한다. 일반적으로 N(다) 측 엔티티가 외래 키의 주인이다.
- 단방향 관계: 외래 키는 N 측 테이블에 생성되며, 이를 통해 부모 테이블(1 측)을 참조한다.
- 양방향 관계: 양방향 관계를 설정하려면 @OneToMany에 mappedBy = "${본인의 엔티티 필드명}" 옵션을 사용하여 외래 키의 주인이 아님을 명시해야 한다. @ManyToOne에서는 외래 키의 주인이 항상 N 측이므로 mappedBy 옵션이 필요하지 않는다.
'Development > Diary' 카테고리의 다른 글
[Diary] Spring ORM에서는 N + 1을 항상 신경쓰자. (0) | 2024.08.30 |
---|---|
[Diary][Spring] Spring Security에서 Role과 Authority의 차이가 뭘까? (0) | 2024.08.29 |
[Diary] @SpringBootApplication의 @ComponentScan 범위 (0) | 2024.08.26 |
[Diary][Spring] Spring Cloud Gateway와 FeignClient를 같이 사용하면 발생할 수 있는 문제 (0) | 2024.08.25 |
[Diary] AWS EC2에서 겪은 포트포워딩 문제: 리눅스 네트워크 인터페이스 eth0와 enX0 (1) | 2024.08.01 |