⭐️ Today's summary
오늘은 엔티티 관계 맵핑에 대해서 더 자세하게 알아보았다.
우선 @Check 어노테이션 사용하는 방식보다 Enum으로 대체하였고, Default값 또한
DB에서 생성하지 않고 엔티티가 영속성 컨텍스트에 등록될때 값이 초기화 되게 바꾸었다.
그리고 제일 중요한 4가지를 배웠다.
가능하면 다대일(N:1) 중심으로 관계를 설계한다.
일대다(1:N)는 사용을 자제하고, 역방향 조회만 필요할 때 사용한다.
일대일(1:1)은 외래키 위치와 로딩 전략을 신중히 결정한다.
다대다(N:M)는 절대 직접 매핑하지 않고, 반드시 중간 엔티티로 처리한다.
⭐️ Problem
Enum을 처음 배우기도 했고, 관계맵핑이 아직 많이 미숙하기 때문에 아래에 다루면서 정리 해 보겠다.
⭐️ Try
[ Member.java ]
package com.kh.jpa.entity;
import com.kh.jpa.eums.CommonEnums;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.DynamicInsert;
import org.hibernate.annotations.DynamicUpdate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Getter
@AllArgsConstructor
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA 스펙상 필수 + 외부 생성 방지
@Entity
@DynamicInsert // insert시에 NULL이 아닌 필드만 쿼리에 포함, default값 활용
@DynamicUpdate // 변경된 필드만 update문에 포함
public class Member {
@Id
@Column(name = "USER_ID", length = 30)
private String userId;
@Column(name = "USER_PWD", nullable = false, length = 100)
private String userPw;
@Column(name = "USER_NAME", nullable = false, length = 15)
private String userName;
@Column(name = "EMAIL", length = 254)
private String email;
//@Check(constraints = "gender IN ('M', 'F')")
@Enumerated(EnumType.STRING) // EnumType이 String인데 저 Enum타입을 사용 하겠다.
@Column(name = "GENDER", length = 1)
private Gender gender;
@Column(name = "AGE")
private Integer age;
@Column(name = "PHONE", length = 13)
private String phone;
@Column(length = 100)
private String address;
@Column(name = "ENROLL_DATE")
private LocalDateTime enrollDate;
@Column(name = "MODIFY_DATE")
private LocalDateTime modifyDate;
@Column(name = "STATUS", length = 1, nullable = false)
@Enumerated(EnumType.STRING)
private CommonEnums.Status status;
// Profile과 1:1 관계 맵핑
// 1:1 관계에서는 주테이블에 외래키를 두는것이 일반적
// 그리고 unique 제약조건을 주자
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PROFILE_ID", unique = true)
private Profile profile;
//---------------------------------------------------------------
// enum 타입
public enum Gender {
M, F,
}
// 엔티티가 영속성 컨텍스트에 저장되기 전(em.persist())에 실행되는 메서드
// 초기설정을 해두는 용도로 사용
@PrePersist
public void prePersist() {
this.enrollDate = LocalDateTime.now();
this.modifyDate = LocalDateTime.now();
if (this.status == null) {
this.status = CommonEnums.Status.Y;
}
}
@PreUpdate
public void preUpdate() {
this.modifyDate = LocalDateTime.now();
}
}
어제와 다른 점은 enum을 추가하여 @Check 제약조건을 대신하는것였다.
enum이란 본인이 사용할 값을 정해놓고 사용하는 자바 유형(type)이다.
Gender처럼 같은 클래스에 enum타입을 정의해서 사용할 수 있다.
@Enumerated(EnumType.STRING) 어노테이션을 사용해 // "M" 또는 "F"로 저장하겠다고 알려주는 것이다.
[ CommonEnums.java ]
package com.kh.jpa.eums;
public class CommonEnums {
public enum Status {
Y, N,
}
}
위 코드처럼 Class로 분리해서 Status를 가져올 수 있다. 그러면 Status는 "Y" 또는 "N"을 가져올 수 있다는 것이다.
그 다음은 @PrePersist 어노테이션이다.
이 어노테이션은 엔티티가 영속성 컨텍스트에 저장되기 전(em.persist())에 실행되는 메서드이다.
초기 설정을 해두는 용도로 사용되며 사용법은
@PrePersist 어노테이션을 설정하고, 메서드를 정의하고 그 안에 this에 값을 넣어주면된다.
@PreUpdate는 업데이트시에 변경될값을 의미한다.
이제 맵핑에 대해서 보겠다.
🔥 1 : 1 맵핑
// Profile과 1:1 관계 맵핑
// 1:1 관계에서는 주테이블에 외래키를 두는것이 일반적
// 그리고 unique 제약조건을 주자
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "PROFILE_ID", unique = true)
private Profile profile;
이 코드에서 1:1 관계 맵핑을 다루고 있는데, 1:1 맵핑에서는 누가 외래키를 가지고 있냐가 중요하다.
일반적으로는 주테이블 (자주 사용되는 테이블)이 외래키를 가지고 있는것이 일반적이다.
그리고 1:1 관계이기때문에 unique 제약조건을 줘야된다 ‼️
🔥 N : 1 맵핑
package com.kh.jpa.entity;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDateTime;
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA 스펙상 필수 + 외부 생성 방지
@Entity
public class Notice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "NOTICE_NO")
private Long noticeNo;
@Column(name = "NOTICE_TITLE", nullable = false, length = 30)
private String noticeTitle;
// ManyToOne에선 Many에 외래키를 주는것(주인 설정)이 일반적.
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "NOTICE_WRITER", nullable = false)
private Member member;
@Column(name = "NOTICE_CONTENT", length = 200, nullable = false)
private String noticeContent;
@Column(name = "CREATE_DATE")
private LocalDateTime createDate;
@PrePersist
protected void onCreate() {
this.createDate = LocalDateTime.now();
}
}
여러 엔티티중에 Notice.java를 예시로 들겠다.
N : 1 관계 ( 한 Member는 여러개의 Notice를 작성할 수 있다) 에서는 @ManyToOne 어노테이션을 통해 관계를
맵핑하고, @JoinColumn()을 사용하여 name을 작성해주는것이 좋다.
또한 Many인쪽에 외래키를 설정해주는것이 일반적이고, 이것은 단방향 맵핑만 한 상태이다.
이말은 Notice로만 Member 엔티티를 조회할 수 있는 것이다. 반대로 Member 엔티티가 Notice 엔티티를 조회
하고 싶으면 Member 엔티티에 mappedBy 설정을해주고 아래처럼 컬럼을 설정해줘야한다.
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL)
private List<Notice> noticeList = new ArrayList<>();
Member 엔티티 관점에서는 1:N 관계이고, mappedBy를 설정해야한다.
mappedBy를 통해 외래키의 주인은 Notice엔티티의 member 필드라는것을 알려주는것이다.
이를 통해 Member 엔티티는 연관관계 비주인. 즉, 읽기 전용이라는 말이다.
이처럼 N : 1 관계 말고 1:N 관계도 있지만 위에서 설명했던
"일대일(1:1)은 외래키 위치와 로딩 전략을 신중히 결정한다" 이 말이 중요하다.
- 외래키는 항상 "다(N)" 쪽에 있기 때문에, "일(1)" 쪽에서 관계를 관리하기가 힘들다.
- 추가적인 UPDATE 쿼리 발생 → 성능 저하
- 권장: 다대일(N:1) 매핑 후, 필요 시 양방향매핑으로 컬렉션 조회
이러한 이유때문에 1:N 관계는 가급적 사용하지 않는것이 좋다.
🔥 N : M 맵핑
package com.kh.jpa.entity;
import jakarta.persistence.*;
import lombok.*;
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA 스펙상 필수 + 외부 생성 방지
@Entity
@Table(name = "BOARD_TAG")
public class BoardTag {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "BOARD_TAG_ID")
private Long boardTagId;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "BOARD_NO", nullable = false)
private Board board;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "TAG_ID", nullable = false)
private Tag tag;
}
위 코드는 N:M 맵핑을 다루고 있다.
다대다 관계는 @ManyToMany를 사용하지않고 중간 엔티티를 생성하는것이 좋다.
데이터 베이스 테이블에 중개 테이블을 생각하고 N:1 관계를 작성해주고, 기본키를 만들어주면 된다.
이를 통해서 맵핑에 대해서 어제보단 조금 나아졌지만, 더 많이 써보고 이게 왜 이렇게 써야하는지를 알게 된다면
더 쉬워질 것같다.
'Weekly TIL' 카테고리의 다른 글
Weekly TIL - Day 33 (0) | 2025.05.17 |
---|---|
Weekly TIL - Day 32 (2) | 2025.05.16 |
Weekly TIL - Day 30 (3) | 2025.05.14 |
Weekly TIL - Day 29 (0) | 2025.05.13 |
Weekly TIL - Day 28 (0) | 2025.05.12 |