Entity 간 관계 설정
RDB 스키마에서 각 table 간의 1:1, 1:N, N:N 관계를 매핑하여 정의하듯이
Spring Entity에서도 데이터베이스의 관계형 모델을 표현하기 위해 다양한 relation을 설정할 수 있다.
이를 위해 JPA에서 제공하는 어노테이션인 @OneToMany, @ManyToOne, @ManyToMany, @JoinColumn 등을 사용한다.
@OneToOne
두 Entity가 1:1 관계를 가지는 경우 사용한다.
주로 한 entity가 다른 entity를 소유하는 개념에서 많이 사용된다. (ex. 특정 유저의 계좌 정보)
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Joined Column
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name="account_id", referencedColumnName = "id")
private Account account;
}
@Entity
public class Account {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "balance", nullable = false)
@ColumnDefault("0")
private Long balance;
}
이러한 1:1 매핑에서 종속관계를 적용하기 위하여 주인의 개념인 entity에 종속하는 entity class의 필드를 정의한다.(Account account)
그리고 해당 필드에 @OneToOne, @JoinColumn 어노테이션을 붙여 관계를 설정해준다.
@JoinColumn에서 name은 User table에서 나타낼 column 이름이고, referencedColumnName은 Account table에서 어떤 column을 Foreign Key 대상으로 매핑할 것인지 명시해주는 속성이다.
@OneToMany / @ManyToOne
한 명의 User는 여러개의 Order를 가질 수 있다. 따라서 User와 Order의 관계는 1:N이다.
이를 표현하기 위해 다음 2개의 annotation을 각각 user와 order에 명시한다.
- @OneToMany: 한 Entity가 여러 개의 하위 Entity를 참조할 때 사용한다.
- @ManyToOne: 여러 Entity가 하나의 상위 Entity를 참조할 때 사용한다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Order> orders = new ArrayList<>();
}
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
}
User가 여러개의 Order를 가질 수 있으니 List<Order> type으로 멤버변수를 정의하고 @OneToMany 어노테이션을 붙인다.
Order는 하나의 User에 종속되니 User type으로 멤버변수를 정의하고 @ManyToOne 어노테이션을 붙인다.
@OneToMany 쪽에서는 mappedBy로 종속 entity의 변수명과 연결해주어야 하고,
@ManyToOne 쪽에서는 @JoinColumn을 붙여 user_id라는 컬럼명으로 User에 대한 foreign key를 생성한다.
@ManyToMany
다대다 관계를 표현할 때 사용한다.
한 명의 User는 여러 개의 caln에 가입할 수 있고, 하나의 Clan 또한 여러 명의 user를 멤버로 받을 수 있으니 두 entity의 관계는 N:N이 된다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(
name = "user_clan",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "clan_id")
)
private Set<Clan> clans = new HashSet<>();
}
@Entity
public class Clan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "clans")
private Set<User> users = new HashSet<>();
}
N:N 관계는 사실 중계 테이블을 사이에 두고 생성해야 한다. 따라서 @ManyToMany와 @JoinTable 어노테이션을 함께 사용하여 두 entity의 관계를 정의한다.
@JoinTable annotation 내부에 생성할 중계 테이블의 이름과 연결할 두 entity의 key column을 각각 joinColumns, inverseJoinColumns에 @Joincolumn 형태로 명시하면 된다.
@JoinTable 어노테이션을 사용한 entity 쪽이 joinColumns이고 반대쪽이 inverseJoinColumns이다.
@JoinTable을 사용하여 중계테이블을 생성할 수 있지만, 사실 별도의 Entity로 생성하는 것이 관리하는 측면이 더 편리하다.
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
// @JoinColumn(name="userclan_id", referencedColumnName = "id")
private Set<UserClan> registeredClan;
}
@Entity
@Table(name = "clan")
public class Clan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, unique = true, length = 15)
private String name;
@Column(name = "create_at", nullable = false)
private LocalDate createAt;
@OneToMany(mappedBy = "clan", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserClan> clanMembers;
}
@Entity
@Table(
name = "user_clan",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "clan_id"})
)
public class UserClan {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "clan_id", nullable = false)
private Clan clan;
@Column(name = "role", nullable = false, length = 10)
@Enumerated(EnumType.STRING)
@Builder.Default
private UserRole role = UserRole.MEMBER;
@Column(name = "joined_at", nullable = false)
private LocalDate joinedAt;
}
위 예시처럼 User와 Clan의 N:N 관계를 표현할 중계 테이블 UserClan class를 @Entity로써 생성하면 된다.
UserClan class 내부에는 User와 Clan을 멤버변수로 가지며 각각 @ManyToOne 어노테이션을 사용한다.
User와 Clan은 마찬가지로 내부에 Set<UserClan>을 멤버변수로 가지며 @OneToMany 어노테이션을 사용한다.
이렇게 별도의 클래스로 중계 테이블을 정의하는 것이 추가적인 데이터 컬럼을 생성하고 유지보수 하는데 편리하다.
또한 동일한 User가 동일한 Clan에 가입할 수 없는 제약조건을 걸기 위해 두 개의 foreign key (user_id, clan_id) pair에 대해 unique constraint를 생성할 수 있다. 이러한 경우 @Table 어노테이션 내부 속성에 uniqueConstraint를 정의해주면 된다.
@Table(
name = "user_clan",
uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "clan_id"})
)
Cascade, OrphanRemoval, FetchType
관계 테이블을 설정하는 @OneToMany, @ManyToOne, @ManyToMany 어노테이션을 유심히 봤다면,
cascade, orphanremoval, fetch와 같은 속성이 붙어있는것을 확인했고 이 기능이 무엇인지 고민했을 것이다.
cascade는 부모 entity의 작업이 자식 entity에 전파되도록 설정하는 속성이다.
- CascadeType.ALL: 모든 작업 전파(저장, 삭제)
- CascasdeType.REMOVE: 부모 삭제 시 자식도 삭제
- CascadeType.PERSIST: 부모 저장 시 자식도 삭제.
예를 들어 Account는 User 종속되어있기 때문에 CascadeType.ALL로 설정 시 user가 삭제되면 연결되어있는 account도 함께 삭제된다.
orphanremoval는 true로 할 시 관계가 끊어진 자식 entity를 자동으로 삭제한다. 예를 들어 User에서 더이상 연결된 UserClan을 참조하지 않을 경우 자동으로 UserClan 데이터를 DB에서 삭제한다.
fetch 속성은 관련 데이터의 조회 시점에 대한 속성이다.
예를 들어 User가 100억개의 clan에 가입했고, User를 조회할 때마다 연결된 모든 UserClan 객체를 조회하게 된다면 어마어마한 overloading이 발생할 것이다. 따라서 관련 데이터의 필드값에 실제로 접근할 때 조회하도록 설정하는 것이 Lazy Fetch(지연로딩) 방식이다.
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private Set<UserClan> registeredClan;
하지만 lazy fetch가 항상 좋은 것은 아니다. 관계 데이터를 자주 사용하는 경우에는 즉각적으로 함께 조회하는 것이 유용하다.
N+1 문제는 lazy fetch에서도 발생할 수 있으며 이는 나중에 설명할 repository 부분에서 fetch join이나 EntityGraph로 해결해야한다.
하지만 기본적으로는 FetchType.LAZY를 사용하고, 상황에 따라 EAGER로 전환하는 방식이 실무에서 자주 사용된다.
여기까지가 entity의 기본적인 관계설정을 하는 내용이었다.
본인이 이 내용을 처음 접했을 때는 N:N 관계 설정에서 어디에 어떤 어노테이션을 적용해야 하는지 헷갈렸었다.
하지만 데이터 스키마에서 entity간 종속관계를 잘 생각해보면서 코드를 살펴보면 일련의 규칙이 존재하니 도움이 되길 바랍니다.
'Backend' 카테고리의 다른 글
[Java Spring Boot] @Repository 구현과 다양한 방식의 CRUD 메소드(JpaRepository, JPQL, QueryDSL) (1) | 2025.01.02 |
---|---|
[Java Spring Boot] application.yaml 파일에 datasoruce 설정하기. (spring.jpa, HikariCP) (0) | 2025.01.02 |
[Java Spring Boot] Entity 개념 이해하기 및 기본적인 코드 작성 방법 (0) | 2025.01.02 |
[Java Spring Boot] 프로젝트 구성 패키지 설명 (0) | 2024.12.31 |
[Java Spring Boot] 스프링 프로젝트 시작하기 (0) | 2024.12.31 |