Entity와 테이블 매핑의 기본 어노테이션, 기본키 전략, 다양한 타입 매핑을 학습합니다.
클래스를 JPA가 관리하는 Entity로 지정합니다. 이 어노테이션이 붙은 클래스는 JPA가 테이블과 매핑하여 관리합니다.
기본 생성자 필수 (public 또는 protected)
JPA가 리플렉션으로 객체를 생성하기 때문
final 클래스, enum, interface, inner 클래스 사용 불가
프록시 생성을 위해 상속 가능해야 함
저장할 필드에 final 사용 불가
값 변경이 가능해야 함
// name 속성: JPQL에서 사용할 엔티티 이름 지정
// 기본값은 클래스 이름
@Entity(name = "Product") // JPQL: SELECT p FROM Product p
public class Product { ... }
// 같은 이름의 클래스가 다른 패키지에 있을 때 구분용
@Entity(name = "AdminProduct")
public class Product { ... } // com.example.admin.ProductEntity와 매핑할 테이블을 지정합니다. 생략하면 Entity 이름을 테이블 이름으로 사용합니다.
| 속성 | 설명 | 기본값 |
|---|---|---|
| name | 매핑할 테이블 이름 | Entity 이름 |
| catalog | 데이터베이스 catalog 매핑 | - |
| schema | 데이터베이스 schema 매핑 | - |
| uniqueConstraints | DDL 생성 시 유니크 제약조건 | - |
| indexes | DDL 생성 시 인덱스 | - |
PHP 시스템의 기존 테이블 이름이 Java 네이밍 컨벤션과 다를 때:
// 기존 테이블: tbl_products (레거시 네이밍)
@Entity
@Table(
name = "tbl_products",
indexes = {
@Index(name = "idx_product_name", columnList = "name"),
@Index(name = "idx_product_category", columnList = "category_id")
},
uniqueConstraints = {
@UniqueConstraint(name = "uk_product_sku", columnNames = {"sku"})
}
)
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
@Column(name = "product_name", nullable = false)
private String name;
// ...
}Spring Boot에서 기본적으로 camelCase를 snake_case로 변환합니다.
# application.yml
spring:
jpa:
hibernate:
naming:
# 논리적 이름 전략 (Entity → 논리명)
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
# 물리적 이름 전략 (논리명 → 실제 테이블/컬럼명)
physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
# 결과:
# productName → product_name
# OrderItem → order_item기존 DB 사용 시: @Table(name="..."), @Column(name="...")으로 명시적 지정 권장
모든 Entity는 반드시 식별자(@Id)를 가져야 합니다. 기본키 생성 전략은 데이터베이스와 성능에 큰 영향을 미치므로 신중하게 선택해야 합니다.
데이터베이스에 위임 (MySQL AUTO_INCREMENT, PostgreSQL SERIAL)
@GeneratedValue(strategy = GenerationType.IDENTITY)데이터베이스 시퀀스 사용 (Oracle, PostgreSQL, H2)
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "product_seq")
@SequenceGenerator(name = "product_seq",
sequenceName = "product_sequence",
allocationSize = 50)키 생성 전용 테이블 사용 (모든 DB 지원)
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "product_table_gen")
@TableGenerator(name = "product_table_gen",
table = "sequences",
pkColumnValue = "product_seq",
allocationSize = 50)JPA 구현체가 자동 선택 (Hibernate는 DB 방언에 따라 결정)
@GeneratedValue(strategy = GenerationType.AUTO)MySQL 사용 시 권장 전략
MySQL은 SEQUENCE를 지원하지 않으므로 IDENTITY를 사용합니다. 단, IDENTITY는 persist() 시점에 즉시 INSERT가 실행되어 쓰기 지연이 동작하지 않습니다.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// persist() 호출 시 즉시 INSERT 실행
// → ID 값을 얻기 위해 DB 접근 필요
}// IDENTITY 전략 동작 과정
@Transactional
public void createProducts() {
Product p1 = new Product("상품1");
Product p2 = new Product("상품2");
Product p3 = new Product("상품3");
// IDENTITY: persist() 시점에 즉시 INSERT
em.persist(p1); // INSERT 실행 → p1.id = 1
em.persist(p2); // INSERT 실행 → p2.id = 2
em.persist(p3); // INSERT 실행 → p3.id = 3
// 이미 3번의 INSERT가 실행됨
// 쓰기 지연 불가능
}
// SEQUENCE 전략이라면?
@Transactional
public void createProductsWithSequence() {
Product p1 = new Product("상품1");
Product p2 = new Product("상품2");
Product p3 = new Product("상품3");
// SEQUENCE: persist() 시점에 시퀀스만 조회
em.persist(p1); // SELECT nextval('seq') → p1.id = 1
em.persist(p2); // 캐시된 시퀀스 사용 → p2.id = 2
em.persist(p3); // 캐시된 시퀀스 사용 → p3.id = 3
// 커밋 시점에 배치 INSERT 가능
// INSERT INTO products VALUES (1, ...), (2, ...), (3, ...)
}@IdClass 또는 @EmbeddedId 사용. @EmbeddedId가 더 객체지향적입니다.
// 방법 1: @IdClass
public class OrderItemId implements Serializable {
private Long orderId;
private Long productId;
// 기본 생성자, equals(), hashCode() 필수
@Override
public boolean equals(Object o) { ... }
@Override
public int hashCode() { ... }
}
@Entity
@IdClass(OrderItemId.class)
public class OrderItem {
@Id
@Column(name = "order_id")
private Long orderId;
@Id
@Column(name = "product_id")
private Long productId;
private int quantity;
}
// 방법 2: @EmbeddedId (권장)
@Embeddable
public class OrderItemId implements Serializable {
@Column(name = "order_id")
private Long orderId;
@Column(name = "product_id")
private Long productId;
// equals(), hashCode() 필수
}
@Entity
public class OrderItem {
@EmbeddedId
private OrderItemId id;
private int quantity;
// 조회 시
// OrderItem item = em.find(OrderItem.class, new OrderItemId(1L, 100L));
}권장: 가능하면 복합키 대신 대리키(Surrogate Key) 사용. 복합키는 비즈니스 로직 변경에 취약합니다.
분산 시스템이나 보안이 중요한 경우 UUID 사용을 고려할 수 있습니다.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
// 또는 직접 생성
@Id
@Column(length = 36)
private String id;
@PrePersist
public void generateId() {
if (id == null) {
id = UUID.randomUUID().toString();
}
}
}장점
단점
| 속성 | 설명 | 기본값 |
|---|---|---|
| name | 컬럼 이름 | 필드 이름 |
| nullable | null 허용 여부 (DDL) | true |
| unique | 유니크 제약조건 (DDL) | false |
| length | 문자 길이 (String만, DDL) | 255 |
| precision | 소수점 포함 전체 자릿수 (BigDecimal) | 0 |
| scale | 소수점 자릿수 (BigDecimal) | 0 |
| insertable | INSERT 시 포함 여부 | true |
| updatable | UPDATE 시 포함 여부 | true |
| columnDefinition | DDL 직접 지정 | - |
@Entity
@Table(name = "products")
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// 컬럼명 지정 + NOT NULL + 길이 제한
@Column(name = "product_name", nullable = false, length = 200)
private String name;
// 유니크 제약조건
@Column(nullable = false, unique = true, length = 50)
private String sku; // Stock Keeping Unit
// 금액: precision(전체 자릿수), scale(소수점 자릿수)
// DECIMAL(10,2) → 최대 99999999.99
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "discount_price", precision = 10, scale = 2)
private BigDecimal discountPrice;
@Column(nullable = false)
private Integer stockQuantity = 0;
// 긴 텍스트
@Column(length = 4000)
private String description;
// 읽기 전용 컬럼 (DB에서 자동 생성되는 값)
@Column(name = "created_at", insertable = false, updatable = false)
private LocalDateTime createdAt;
// DDL 직접 지정 (DB 특화 타입)
@Column(columnDefinition = "TEXT")
private String detailHtml;
// JSON 컬럼 (MySQL 5.7+)
@Column(columnDefinition = "JSON")
private String metadata;
}@Entity
public class Product {
// 생성 시간: INSERT만 가능, UPDATE 불가
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
// DB 트리거로 자동 생성되는 값: INSERT/UPDATE 모두 제외
@Column(name = "auto_generated_code", insertable = false, updatable = false)
private String autoGeneratedCode;
// 읽기 전용 (다른 테이블과 조인된 뷰에서 사용)
@Column(name = "category_name", insertable = false, updatable = false)
private String categoryName;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
}DDL 생성 속성은 스키마 자동 생성 시에만 적용
DDL 생성 시에만 사용
ddl-auto=none이면 무시됨
런타임에 항상 적용
실제 SQL 생성에 영향
@Column을 생략하면 필드명이 컬럼명으로 사용됩니다. 기본 타입은 자동으로 적절한 DB 타입으로 매핑됩니다.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// @Column 생략 가능
private String name; // VARCHAR(255)
private Integer quantity; // INT
private BigDecimal price; // DECIMAL(19,2)
private Boolean active; // TINYINT(1) / BOOLEAN
private LocalDateTime createdAt; // DATETIME / TIMESTAMP
// 단, 기존 DB 컬럼명이 다르면 @Column(name="...") 필수
}| Java 타입 | MySQL | PostgreSQL | 비고 |
|---|---|---|---|
| String | VARCHAR(255) | VARCHAR(255) | length로 조절 |
| Integer/int | INT | INTEGER | -2^31 ~ 2^31-1 |
| Long/long | BIGINT | BIGINT | ID에 권장 |
| Double/double | DOUBLE | DOUBLE PRECISION | 금액에 사용 금지 |
| BigDecimal | DECIMAL | NUMERIC | 금액에 권장 |
| Boolean/boolean | TINYINT(1) | BOOLEAN | 0/1 또는 true/false |
| LocalDate | DATE | DATE | 날짜만 |
| LocalDateTime | DATETIME | TIMESTAMP | 날짜+시간 |
| byte[] | BLOB | BYTEA | 바이너리 |
Wrapper vs Primitive 타입
Wrapper (Integer, Long, Boolean)
일반적으로 권장
Primitive (int, long, boolean)
0과 null 구분 불가
@Entity
public class Product {
// 금액은 반드시 BigDecimal 사용
// Double/Float는 부동소수점 오차 발생!
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price; // DECIMAL(10,2): 최대 99,999,999.99
@Column(precision = 10, scale = 2)
private BigDecimal discountPrice;
// 비율 (할인율 등)
@Column(precision = 5, scale = 4)
private BigDecimal discountRate; // 0.0000 ~ 9.9999 (최대 999.99%)
}
// 사용 예시
Product product = new Product();
product.setPrice(new BigDecimal("15000.00"));
product.setDiscountRate(new BigDecimal("0.1500")); // 15%
// 계산
BigDecimal finalPrice = product.getPrice()
.multiply(BigDecimal.ONE.subtract(product.getDiscountRate()))
.setScale(2, RoundingMode.HALF_UP); // 12750.00@Entity
public class Product {
// MySQL: TINYINT(1) - 0 또는 1로 저장
// PostgreSQL: BOOLEAN - true/false로 저장
@Column(nullable = false)
private Boolean active = true; // Wrapper 권장
// 기존 DB가 Y/N 문자로 저장하는 경우
@Column(name = "is_active", length = 1)
@Convert(converter = BooleanToYNConverter.class)
private Boolean active;
}
// Y/N 변환 컨버터
@Converter
public class BooleanToYNConverter implements AttributeConverter<Boolean, String> {
@Override
public String convertToDatabaseColumn(Boolean attribute) {
return (attribute != null && attribute) ? "Y" : "N";
}
@Override
public Boolean convertToEntityAttribute(String dbData) {
return "Y".equals(dbData);
}
}이커머스에서 자주 사용하는 타입 매핑
@Entity
public class Product {
@Id
private Long id; // BIGINT
private String name; // VARCHAR(255)
private String sku; // VARCHAR(50)
private BigDecimal price; // DECIMAL(10,2)
private Integer stockQuantity; // INT
private Boolean active; // TINYINT(1)
private LocalDateTime createdAt; // DATETIME
}
@Entity
public class Order {
@Id
private Long id; // BIGINT
private BigDecimal totalAmount; // DECIMAL(12,2)
private LocalDate orderDate; // DATE
private LocalDateTime paidAt; // DATETIME (nullable)
}Enum 순서(0, 1, 2...)를 저장
@Enumerated(EnumType.ORDINAL)Enum 순서 변경 시 데이터 의미가 바뀜!
Enum 이름(문자열)을 저장
@Enumerated(EnumType.STRING)Enum 순서 변경해도 안전
ORDINAL의 위험성
// 초기 Enum
public enum OrderStatus {
PENDING, // 0
PAID, // 1
SHIPPED // 2
}
// DB에 저장된 값: 0, 1, 2
// 나중에 CANCELLED 추가
public enum OrderStatus {
PENDING, // 0
CANCELLED, // 1 ← 새로 추가!
PAID, // 2 ← 기존 1이 2로 변경
SHIPPED // 3 ← 기존 2가 3으로 변경
}
// 결과: 기존 PAID(1) 데이터가 CANCELLED로 해석됨!// 주문 상태 Enum
public enum OrderStatus {
PENDING, // 결제 대기
PAID, // 결제 완료
PREPARING, // 상품 준비중
SHIPPED, // 배송중
DELIVERED, // 배송 완료
CANCELLED, // 취소됨
REFUNDED // 환불됨
}
// 상품 상태 Enum
public enum ProductStatus {
DRAFT, // 임시저장
ACTIVE, // 판매중
OUT_OF_STOCK, // 품절
DISCONTINUED // 단종
}
// 결제 수단 Enum
public enum PaymentMethod {
CREDIT_CARD,
BANK_TRANSFER,
VIRTUAL_ACCOUNT,
KAKAO_PAY,
NAVER_PAY,
TOSS
}
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private OrderStatus status = OrderStatus.PENDING;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private PaymentMethod paymentMethod;
}
@Entity
public class Product {
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ProductStatus status = ProductStatus.DRAFT;
}public enum OrderStatus {
PENDING("결제 대기", false),
PAID("결제 완료", false),
PREPARING("상품 준비중", false),
SHIPPED("배송중", false),
DELIVERED("배송 완료", true),
CANCELLED("취소됨", true),
REFUNDED("환불됨", true);
private final String description;
private final boolean finalStatus; // 최종 상태 여부
OrderStatus(String description, boolean finalStatus) {
this.description = description;
this.finalStatus = finalStatus;
}
public String getDescription() {
return description;
}
public boolean isFinalStatus() {
return finalStatus;
}
// 다음 가능한 상태 반환
public List<OrderStatus> getNextPossibleStatuses() {
return switch (this) {
case PENDING -> List.of(PAID, CANCELLED);
case PAID -> List.of(PREPARING, REFUNDED);
case PREPARING -> List.of(SHIPPED);
case SHIPPED -> List.of(DELIVERED);
default -> List.of();
};
}
}
// 사용
Order order = orderRepository.findById(orderId).orElseThrow();
if (order.getStatus().isFinalStatus()) {
throw new IllegalStateException("이미 완료된 주문입니다.");
}기존 DB가 숫자 코드(10, 20, 30...)나 문자 코드(P, A, C...)를 사용하는 경우:
// 기존 DB: status 컬럼에 10, 20, 30 저장
public enum OrderStatus {
PENDING(10),
PAID(20),
SHIPPED(30),
DELIVERED(40);
private final int code;
OrderStatus(int code) {
this.code = code;
}
public int getCode() {
return code;
}
public static OrderStatus fromCode(int code) {
return Arrays.stream(values())
.filter(s -> s.code == code)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unknown code: " + code));
}
}
@Converter(autoApply = true) // 모든 OrderStatus에 자동 적용
public class OrderStatusConverter implements AttributeConverter<OrderStatus, Integer> {
@Override
public Integer convertToDatabaseColumn(OrderStatus status) {
return status != null ? status.getCode() : null;
}
@Override
public OrderStatus convertToEntityAttribute(Integer code) {
return code != null ? OrderStatus.fromCode(code) : null;
}
}
@Entity
public class Order {
// @Convert 생략 가능 (autoApply = true)
@Column(name = "status")
private OrderStatus status; // DB에는 10, 20, 30으로 저장
}Hibernate 5.x 이상에서는 Java 8 날짜/시간 타입을 자동 매핑합니다. @Temporal 불필요.
@Entity
public class Order {
// 날짜만 (DATE)
private LocalDate orderDate;
// 날짜 + 시간 (DATETIME/TIMESTAMP)
private LocalDateTime createdAt;
// 시간만 (TIME) - 잘 사용 안함
private LocalTime deliveryTime;
// 타임존 포함 (TIMESTAMP WITH TIME ZONE)
// PostgreSQL 지원, MySQL은 제한적
private ZonedDateTime scheduledAt;
// Instant (UTC 기준 TIMESTAMP)
// 서버 간 시간 동기화에 유용
private Instant processedAt;
// 기간 (Period, Duration)은 직접 매핑 안됨
// → String이나 Long으로 변환 필요
}| 용도 | Java 타입 | DB 타입 |
|---|---|---|
| 생년월일, 주문일 | LocalDate | DATE |
| 생성일시, 수정일시 | LocalDateTime | DATETIME |
| 글로벌 서비스 시간 | Instant | TIMESTAMP |
| 예약 시간 (타임존 필요) | ZonedDateTime | TIMESTAMP WITH TZ |
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@Column(updatable = false)
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// INSERT 전 호출
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
// UPDATE 전 호출
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}// 1. 설정 클래스에 @EnableJpaAuditing 추가
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
// 2. 공통 BaseEntity 생성
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
// 작성자/수정자도 추가 가능
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
// 3. Entity에서 상속
@Entity
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// createdAt, updatedAt 자동 관리
}
// 4. 작성자 정보 제공 (선택)
@Configuration
public class AuditorConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getName);
}
}레거시 Date 타입 (비권장)
기존 코드에서 java.util.Date를 사용하는 경우 @Temporal 필요:
// java.util.Date 사용 시 (비권장)
@Temporal(TemporalType.DATE) // DATE만
@Temporal(TemporalType.TIME) // TIME만
@Temporal(TemporalType.TIMESTAMP) // DATETIME
// 권장: Java 8 API로 마이그레이션
// Date → LocalDate 또는 LocalDateTime타임존 처리 전략
방법 1: UTC로 통일 (권장)
서버/DB는 UTC로 저장, 클라이언트에서 로컬 시간으로 변환
방법 2: 서버 타임존 사용
단일 지역 서비스에서 서버 타임존(Asia/Seoul) 사용
# application.yml - JVM 타임존 설정
spring:
jpa:
properties:
hibernate:
jdbc:
time_zone: UTC # 또는 Asia/Seoul@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// CLOB: 대용량 문자 데이터
// String, char[] → CLOB/TEXT
@Lob
private String detailDescription;
// BLOB: 대용량 바이너리 데이터
// byte[] → BLOB/BYTEA
@Lob
private byte[] image;
// 또는 columnDefinition 사용
@Column(columnDefinition = "TEXT")
private String htmlContent;
@Column(columnDefinition = "MEDIUMTEXT") // MySQL: 최대 16MB
private String longContent;
}
// MySQL LOB 타입 크기
// TINYTEXT: 255 bytes
// TEXT: 65KB
// MEDIUMTEXT: 16MB
// LONGTEXT: 4GBDB에 저장하지 않을 필드에 사용. 계산된 값이나 임시 데이터에 활용.
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private BigDecimal price;
private BigDecimal discountRate;
// DB에 저장하지 않음 - 계산된 값
@Transient
private BigDecimal finalPrice;
public BigDecimal getFinalPrice() {
if (discountRate != null && discountRate.compareTo(BigDecimal.ZERO) > 0) {
return price.multiply(BigDecimal.ONE.subtract(discountRate));
}
return price;
}
// 임시 플래그
@Transient
private boolean selected;
// 캐시된 계산 결과
@Transient
private Integer totalReviewCount;
}
// 또는 transient 키워드 사용 (Java 기본)
@Entity
public class Product {
private transient String tempData; // 직렬화/JPA 매핑 모두 제외
}관련 있는 필드들을 하나의 값 객체로 묶어 재사용성과 응집도를 높입니다.
// 값 타입 정의
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Address {
@Column(length = 10)
private String zipCode;
@Column(length = 100)
private String city;
@Column(length = 100)
private String street;
@Column(length = 100)
private String detail;
public Address(String zipCode, String city, String street, String detail) {
this.zipCode = zipCode;
this.city = city;
this.street = street;
this.detail = detail;
}
// 값 타입은 불변으로 설계
public Address changeDetail(String newDetail) {
return new Address(this.zipCode, this.city, this.street, newDetail);
}
}
// 금액 값 타입
@Embeddable
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Money {
@Column(precision = 12, scale = 2)
private BigDecimal amount;
@Column(length = 3)
private String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
public static Money krw(BigDecimal amount) {
return new Money(amount, "KRW");
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("통화가 다릅니다");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
// 임베디드 타입 사용
@Embedded
private Address address;
// 같은 타입을 여러 번 사용할 때 컬럼명 재정의
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "zipCode", column = @Column(name = "work_zip_code")),
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
@AttributeOverride(name = "detail", column = @Column(name = "work_detail"))
})
private Address workAddress;
}
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "amount", column = @Column(name = "total_amount")),
@AttributeOverride(name = "currency", column = @Column(name = "currency"))
})
private Money totalPrice;
@Embedded
private Address shippingAddress;
}
// 생성되는 테이블
// customers: id, name, zip_code, city, street, detail,
// work_zip_code, work_city, work_street, work_detail
// orders: id, total_amount, currency, zip_code, city, street, detail임베디드 타입의 장점
기존 PHP 시스템의 테이블 구조를 가정하고, JPA Entity로 매핑해봅니다.
-- 기존 PHP 시스템의 테이블
CREATE TABLE tbl_products (
product_id BIGINT AUTO_INCREMENT PRIMARY KEY,
product_name VARCHAR(200) NOT NULL,
sku VARCHAR(50) NOT NULL UNIQUE,
price DECIMAL(10,2) NOT NULL,
discount_price DECIMAL(10,2),
stock_qty INT DEFAULT 0,
status VARCHAR(20) DEFAULT 'DRAFT',
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE tbl_categories (
category_id BIGINT AUTO_INCREMENT PRIMARY KEY,
category_name VARCHAR(100) NOT NULL,
parent_id BIGINT,
sort_order INT DEFAULT 0,
is_active TINYINT(1) DEFAULT 1
);
CREATE TABLE tbl_customers (
customer_id BIGINT AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(50) NOT NULL,
phone VARCHAR(20),
zip_code VARCHAR(10),
city VARCHAR(50),
street VARCHAR(100),
address_detail VARCHAR(100),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);// BaseEntity - 공통 필드
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
// Product Entity
@Entity
@Table(name = "tbl_products")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "product_id")
private Long id;
@Column(name = "product_name", nullable = false, length = 200)
private String name;
@Column(nullable = false, unique = true, length = 50)
private String sku;
@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;
@Column(name = "discount_price", precision = 10, scale = 2)
private BigDecimal discountPrice;
@Column(name = "stock_qty", nullable = false)
private Integer stockQuantity = 0;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 20)
private ProductStatus status = ProductStatus.DRAFT;
@Lob
private String description;
// 생성 메서드
public static Product create(String name, String sku, BigDecimal price) {
Product product = new Product();
product.name = name;
product.sku = sku;
product.price = price;
product.status = ProductStatus.DRAFT;
return product;
}
// 비즈니스 메서드
public void activate() {
this.status = ProductStatus.ACTIVE;
}
public void updatePrice(BigDecimal price, BigDecimal discountPrice) {
this.price = price;
this.discountPrice = discountPrice;
}
}
// Category Entity
@Entity
@Table(name = "tbl_categories")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "category_id")
private Long id;
@Column(name = "category_name", nullable = false, length = 100)
private String name;
@Column(name = "parent_id")
private Long parentId; // 다음 세션에서 연관관계로 변경
@Column(name = "sort_order")
private Integer sortOrder = 0;
@Column(name = "is_active", nullable = false)
private Boolean active = true;
}
// Customer Entity with Embedded Address
@Entity
@Table(name = "tbl_customers")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Customer extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "customer_id")
private Long id;
@Column(nullable = false, unique = true, length = 100)
private String email;
@Column(nullable = false, length = 50)
private String name;
@Column(length = 20)
private String phone;
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "zipCode", column = @Column(name = "zip_code")),
@AttributeOverride(name = "city", column = @Column(name = "city")),
@AttributeOverride(name = "street", column = @Column(name = "street")),
@AttributeOverride(name = "detail", column = @Column(name = "address_detail"))
})
private Address address;
}@Entity는 기본 생성자 필수
protected 접근 제한자로 외부 생성 방지
@Table로 기존 테이블명 매핑
레거시 DB의 네이밍 컨벤션 대응
MySQL은 IDENTITY 전략 사용
AUTO_INCREMENT 활용
@Enumerated는 반드시 STRING 사용
ORDINAL은 데이터 무결성 위험
Java 8 날짜 타입은 자동 매핑
@Temporal 불필요
@Embedded로 값 타입 재사용
Address, Money 등 응집도 높은 설계
다음 세션에서는 Entity 간의 관계를 매핑하는 방법을 학습합니다.
@ManyToOne, @OneToMany
다대일, 일대다 연관관계
단방향 vs 양방향
연관관계 방향 설계
연관관계의 주인 (mappedBy)
외래키 관리 주체 결정
@JoinColumn
외래키 컬럼 매핑