
Spring 프로젝트를 진행할 때마다 Entity 설계에서 다음과 같은 질문이 떠오릅니다:
mappedBy는 어디에 쓰고 주체가 무엇인가?@Builder로 설정하는 게 맞을까?@CreatedDate, @LastModifiedDate는 왜 적용이 안 될까?이번 글에서는 그 헷갈림을 확실히 정리해보고자 합니다. 예시는 K-FreeMarket 프로젝트의 User와 CartItem 관계(1:N)를 기준으로 설명합니다.
일반적으로 Entity는 domain.entity 하위에 두며, 도메인 기반 구조를 사용하면 다음과 같이 나눌 수 있습니다.
src/
└── main/
└── java/
└── com.kfreemarket.reemarket_server
└── domain
└── user
└── entity
@Getter
@Entity
@Table(name = "user")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
| 어노테이션 | 설명 |
|---|---|
@Entity | JPA에서 이 클래스가 테이블과 매핑된다는 의미 |
@Table(name = "user") | 테이블 이름 지정 (예약어인 user는 사용 주의) |
@NoArgsConstructor(PROTECTED) | JPA가 리플렉션으로 객체 생성을 위해 필요. 외부 생성을 막기 위해 PROTECTED 사용 |
@Getter | 모든 필드에 Getter 생성 |
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id", nullable = false)
private Long id;
⚠️ email을 PK로 쓸 수는 있지만 실무에서는 id를 보조키로 두는 경우가 많습니다. email은 변경될 수 있고, 연관 관계의 FK로 쓰기에는 성능과 정합성 측면에서 Long id가 더 안정적입니다.
CartItem에서만 User를 참조하고, User는 CartItem을 모른다고 가정합니다.
/entity/CartItem
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@JoinColumn: 명시하지 않으면 중간 조인 테이블 생성됨fetch = FetchType.LAZY: 성능상 유리, 지연 로딩User → CartItem, CartItem → User로 서로 참조합니다.
/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는 "상대 엔티티 안의 필드명"이라는 것. 클래스명이 아니라는 점에 주의하세요.
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;
여기서 중요한 점:
✅ 따라서 mappedBy는 "연관 관계의 주인을 명시하는 것"이 아니라, 비주인이 주인을 가리키는 방법입니다.
JPA에서 연관 관계를 설정하면 객체 간 탐색이 자연스러워지고, 개발자는 객체지향 방식으로 데이터를 다룰 수 있습니다.
User user = cartItem.getUser(); // CartItem → User 탐색
List<CartItem> cartItems = user.getCartItems(); // User → CartItem 탐색
이러한 객체 탐색은 SQL JOIN으로 구현되지만, 우리는 객체의 메서드를 통해 직관적으로 다룰 수 있습니다.
또한 연관 관계를 설정하면 다음과 같은 이점이 있습니다:
@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;
}
이 어노테이션들이 작동하려면 해당 엔티티에 @EntityListeners(AuditingEntityListener.class)가 선언되어 있어야 합니다.
@EntityListeners(AuditingEntityListener.class)
public class User { ... }
그리고 Spring Boot 설정에 아래를 반드시 추가해야 작동합니다.
@EnableJpaAuditing
일반적으로 @SpringBootApplication 위에 선언하거나, 별도 @Configuration 클래스에 둡니다.
[참고]