Weekly TIL - Day 31

2025. 5. 15. 10:23·Weekly TIL

⭐️ 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
'Weekly TIL' 카테고리의 다른 글
  • Weekly TIL - Day 33
  • Weekly TIL - Day 32
  • Weekly TIL - Day 30
  • Weekly TIL - Day 29
KoesJin
KoesJin
hEELo
  • KoesJin
    Seok DevLog
    KoesJin
  • 전체
    오늘
    어제
    • 분류 전체보기 (109)
      • Back End (31)
        • DataBase (15)
        • JAVA (12)
        • JDBC (4)
      • Front End (9)
        • HTML5 & CSS (3)
        • Java Script (6)
        • REACT (0)
      • Server (9)
        • JSP - TomCat - Servlet (7)
        • Spring Boot (2)
      • GitHub (1)
      • IT 지식 (기술면접 대비) (20)
      • Weekly TIL (39)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 글쓰기
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    DAO
    순서에 대하여
    weekly til - day 43
    select
    INNER JOIN
    weekly til - day 40
    GC
    weekly til - day 41
    where
    dml
    DDL
    order by
    css
    weekly til - day 39
    from
    commit
    View
    MVC 패턴
    weekly til - day 38
    exception
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
KoesJin
Weekly TIL - Day 31
상단으로

티스토리툴바