Spring Entity 설계할 때마다 헷갈리는 것들 정리

wjd15sheep·2025년 6월 18일
0

Spring Boot

목록 보기
13/19
post-thumbnail

Spring 프로젝트를 진행할 때마다 Entity 설계에서 다음과 같은 질문이 떠오릅니다:

  • mappedBy는 어디에 쓰고 주체가 무엇인가?
  • 생성자는 @Builder로 설정하는 게 맞을까?
  • 관계는 단방향으로도 충분하지 않을까?
  • @CreatedDate, @LastModifiedDate는 왜 적용이 안 될까?

이번 글에서는 그 헷갈림을 확실히 정리해보고자 합니다. 예시는 K-FreeMarket 프로젝트의 UserCartItem 관계(1:N)를 기준으로 설명합니다.

📦 Entity의 위치

일반적으로 Entity는 domain.entity 하위에 두며, 도메인 기반 구조를 사용하면 다음과 같이 나눌 수 있습니다.

src/
└── main/
    └── java/
        └── com.kfreemarket.reemarket_server
            └── domain
                └── user
                    └── entity

🧱 기본 어노테이션 설명

@Getter
@Entity
@Table(name = "user")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
어노테이션설명
@EntityJPA에서 이 클래스가 테이블과 매핑된다는 의미
@Table(name = "user")테이블 이름 지정 (예약어인 user는 사용 주의)
@NoArgsConstructor(PROTECTED)JPA가 리플렉션으로 객체 생성을 위해 필요. 외부 생성을 막기 위해 PROTECTED 사용
@Getter모든 필드에 Getter 생성

🆔 PK 설정

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id", nullable = false)
private Long id;
  • @Id : PK 설정
  • @GeneratedValue: DB에서 자동 증가
  • @Column: 컬럼 속성 정의

⚠️ email을 PK로 쓸 수는 있지만 실무에서는 id를 보조키로 두는 경우가 많습니다. email은 변경될 수 있고, 연관 관계의 FK로 쓰기에는 성능과 정합성 측면에서 Long id가 더 안정적입니다.


🔗 관계 설정: 단방향 vs 양방향

단방향

CartItem에서만 User를 참조하고, UserCartItem을 모른다고 가정합니다.

/entity/CartItem
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
  • @JoinColumn: 명시하지 않으면 중간 조인 테이블 생성됨
  • fetch = FetchType.LAZY: 성능상 유리, 지연 로딩

양방향

UserCartItem, CartItemUser로 서로 참조합니다.

/entity/User

@OneToMany(mappedBy = "user", fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private List<CartItem> cartItems;


/entity/CartItem

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
  • mappedBy: 연관관계의 주인이 아닌 쪽에 설정
  • 주인은 실제로 FK를 가지고 있는 쪽 (즉 @JoinColumn을 가진 엔티티)
  • cascade = ALL: 부모 삭제 시 자식도 삭제

⚠️ 중요한 점은 mappedBy는 "상대 엔티티 안의 필드명"이라는 것. 클래스명이 아니라는 점에 주의하세요.

🎯 mappedBy 설정 시, 연관 관계의 주인은 누구인가?

Spring JPA에서는 연관 관계의 주인(Owner)이 FK(외래키)를 실제로 관리하는 쪽입니다.
다시 말해, @JoinColumn이 선언된 엔티티가 주인입니다.

예시로 정리

// 주인: CartItem
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;

// 비주인: User
@OneToMany(mappedBy = "user")
private List<CartItem> cartItems;

여기서 중요한 점:

  • CartItem이 주인입니다. 왜냐하면 user_id라는 FK를 실제로 DB에 가집니다.
  • User는 비주인이고, mappedBy = "user"에서 "user"는 CartItem 안에 선언된 필드명을 의미합니다.

✅ 따라서 mappedBy는 "연관 관계의 주인을 명시하는 것"이 아니라, 비주인이 주인을 가리키는 방법입니다.

❓ 연관 관계를 왜 맺어야 할까?

JPA에서 연관 관계를 설정하면 객체 간 탐색이 자연스러워지고, 개발자는 객체지향 방식으로 데이터를 다룰 수 있습니다.


User user = cartItem.getUser(); // CartItem → User 탐색
List<CartItem> cartItems = user.getCartItems(); // User → CartItem 탐색

이러한 객체 탐색은 SQL JOIN으로 구현되지만, 우리는 객체의 메서드를 통해 직관적으로 다룰 수 있습니다.

또한 연관 관계를 설정하면 다음과 같은 이점이 있습니다:

  • Cascade 처리: 부모 엔티티 삭제 시 자식도 자동 삭제 가능
  • 지연 로딩 설정으로 성능 최적화 가능
  • 영속성 전이(Cascade)와 고아 객체 제거 등의 고급 기능 사용 가능

📌 Builder 사용 여부

@Builder
public User(String username, String mobile_number, String address, UserRole userRole) {
    this.userName = username;
    this.mobile_number = mobile_number;
    this.address = address;
    this.userRole = userRole;
}
  • @Builder: 객체 생성 시 가독성을 높이고 생성자 파라미터 순서 실수를 줄여줍니다.
  • 그러나 불변 객체가 아닐 경우 주의, 값 변경이 많다면 Setter 또는 변경 메서드가 더 적합합니다.

🕒 @CreatedDate / @LastModifiedDate 주의

이 어노테이션들이 작동하려면 해당 엔티티에 @EntityListeners(AuditingEntityListener.class)가 선언되어 있어야 합니다.

@EntityListeners(AuditingEntityListener.class)
public class User { ... }

그리고 Spring Boot 설정에 아래를 반드시 추가해야 작동합니다.

@EnableJpaAuditing

일반적으로 @SpringBootApplication 위에 선언하거나, 별도 @Configuration 클래스에 둡니다.


[참고]

profile
성장 위해 노력하는 웹 개발자 주니어

0개의 댓글