F-LAB JAVA · 7주차 · Phase 4 · JPA 엔티티 매핑
🏷️ Phase 4 시작 — 객체를 JPA 세계로
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
@Entity 는 이 클래스가 JPA 가 관리하는 영속성 객체 (엔티티) 임을 선언하는 어노테이션으로 (1) @Entity 어노테이션 (2) 기본 생성자 필수 (JPA 가 리플렉션으로 객체 생성) (3) final 클래스 금지 (JPA 가 프록시를 만들어야) 의 3가지 조건을 충족해야 하며, DTO 와 달리 영속성 컨텍스트가 관리하고 변경 감지 (Dirty Checking) 의 대상이 된다.
@Entity 는 자바 클래스를 JPA 가 관리하는 영속성 객체 (엔티티) 로 선언하는 어노테이션이다.
이 어노테이션이 붙으면 JPA 가 해당 클래스를 DB 의 테이블과 매핑 하며 영속성 컨텍스트에서 관리한다.
엔티티의 3가지 조건은 — (1)@Entity어노테이션 (필수), (2) 기본 생성자 (no-arg constructor) 필수 (JPA 가 리플렉션으로 객체 생성 하기 위해 — 파라미터 없는 생성자 호출 후 setter 또는 리플렉션 필드 접근), (3) final 클래스 금지 (JPA 가 Lazy Loading 등을 위해 프록시 (자식 클래스) 를 만드는데 final 은 상속 불가).
DTO 와 결정적 차이는 — DTO 는 단순 데이터 전달용 객체로 영속성 X, Entity 는 영속성 컨텍스트가 관리하고 변경 감지 (Dirty Checking) 의 대상 이라entity.setStatus(...)만으로 트랜잭션 commit 시 자동 UPDATE 된다.
@Table(name = "...")은 매핑 테이블명 명시이며, 생략하면 Spring Boot 의 명명 전략에 따라 클래스명을 snake_case 로 변환한 테이블에 매핑된다 (Shipment → shipments).
@Entity = 시민 등록증:
비유:
- 자바 객체 = 사람
- JPA 영속성 컨텍스트 = 국가
- @Entity = 시민 등록
DTO (방문자):
- 데이터 전달만
- 일시적
- 국가가 관리 X
- 떠나면 끝
Entity (영속 시민):
- 등록 (@Entity)
- 국가가 관리
- 변경 추적 (Dirty Checking)
- 데이터 영구 보관 (DB)
3가지 조건:
1. 등록증 (@Entity 어노테이션)
2. 신원 확인 (기본 생성자 필수)
- JPA 가 객체 만들 때 필요
3. 가족 등록 (final X)
- JPA 가 프록시 만들어야
@Table:
- 등록할 지역 (테이블) 명시
- 생략 시 자동 (이름 규칙)
라이프사이클:
- 비영속 (new): 시민 X (자바 객체만)
- 영속 (managed): 시민 등록
- 준영속 (detached): 시민권 일시 정지
- 삭제 (removed): 사망 신고
ILIC:
- Shipment, Customer, Booking 모두 엔티티
- 102 테이블 × @Entity
→ @Entity = JPA 관리 대상 선언, 3가지 조건, DTO 와 결정적 차이.
1. @Entity 정의
2. 엔티티 의미
3. 3가지 조건
4. 기본 생성자 필수
5. final 클래스 X (프록시)
6. DTO vs Entity
7. @Table
8. 이름 명시 vs 자동
9. 엔티티 라이프사이클
@Entity:
자바 클래스를:
- JPA 가 관리하는 객체로 선언
- DB 테이블과 매핑
- 영속성 컨텍스트 관리 대상
→ JPA 의 시작점
import jakarta.persistence.Entity;
@Entity // 클래스 레벨
public class Shipment {
// ...
}
// 패키지:
// jakarta.persistence.Entity (Spring Boot 3+)
// 또는 javax.persistence.Entity (Spring Boot 2)
효과:
JPA 인식:
- 영속성 컨텍스트 관리
- SQL 자동 생성 (find, persist 등)
- 1차 캐시 대상
- Dirty Checking 대상
→ 단순 객체 → 엔티티
// @Entity 적용 (ILIC)
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
@Entity
public class Shipment {
@Id
private Long id;
private String blNo;
private String status;
// ... 필드들
// 기본 생성자 (필수)
public Shipment() {}
// getter / setter
}
// → 이제 JPA 가 Shipment 인식
// → shipmentRepository.findById(1L) 가능
// → 자동 SQL 생성
@Entity 어노테이션의 의미는?
답:
1. 선언:
위치:
패키지:
효과:
엔티티 (Entity):
"@Entity 가 붙은 객체":
- 영속성 컨텍스트가 관리
- DB 행과 1:1 매핑
- 식별자 (PK) 보유
도메인 객체:
엔티티 = 비즈니스 도메인의 핵심 객체:
- 주문, 상품, 회원, 배송
- 의미 있는 단위
- 식별 (id) 가능
→ DDD 의 Entity 와 유사 (정확히 같진 않음)
영속성 (Persistence):
엔티티의 특성:
- DB 에 영구 저장
- JVM 종료 후에도
- 다시 가져올 수 있음
영속성 컨텍스트:
- 엔티티를 관리하는 1차 캐시
- 트랜잭션 범위
| 항목 | 일반 객체 | 엔티티 |
|---|---|---|
| @Entity | X | O |
| DB 매핑 | X | O |
| 영속성 컨텍스트 | X | O |
| 식별자 (PK) | X | O |
| 변경 감지 | X | O |
| 생명 주기 | new/GC | 비영속/영속/준영속/삭제 |
// ILIC 의 엔티티들 (102 테이블 × @Entity)
@Entity
@Table(name = "shipments")
public class Shipment { /* ... */ }
@Entity
@Table(name = "customers")
public class Customer { /* ... */ }
@Entity
@Table(name = "bookings")
public class Booking { /* ... */ }
@Entity
@Table(name = "ports")
public class Port { /* ... */ }
@Entity
@Table(name = "freights")
public class Freight { /* ... */ }
// ... 102 개
// 각 엔티티:
// - 도메인 의미
// - DB 테이블 매핑
// - 영속성 관리
엔티티 (Entity) 의 정의는?
답:
1. 엔티티:
도메인:
영속성:
차이:
엔티티의 3가지 조건:
1. @Entity 어노테이션 (필수)
2. 기본 생성자 (no-arg) 필수
- JPA 가 리플렉션으로 객체 생성
3. final 클래스 / final 메서드 X
- JPA 가 프록시 (자식 클래스) 만들어야
- final 은 상속 불가
→ 빠뜨리면 동작 X
+ 권장 조건:
4. @Id 필드 (PK)
- 모든 엔티티 식별자 필요
5. 기본 생성자 protected 이상
- public 권장
- private 안 됨
6. equals / hashCode 정의 (선택)
- id 기반 비교
- 컬렉션 사용 시
누락 시 에러:
@Entity 없음:
- JPA 인식 X
- Repository 매핑 실패
기본 생성자 없음:
- InstantiationException
- HibernateException
final 클래스:
- 프록시 생성 실패
- Lazy Loading 시 에러
import jakarta.persistence.*;
@Entity
@Table(name = "shipments")
public class Shipment {
@Id
@GeneratedValue
private Long id;
private String blNo;
private String status;
// ① 기본 생성자 (필수) — protected 권장
protected Shipment() {}
// ② 커스텀 생성자 (선택)
public Shipment(String blNo, String status) {
this.blNo = blNo;
this.status = status;
}
// ③ getter
public Long getId() { return id; }
public String getBlNo() { return blNo; }
public String getStatus() { return status; }
// setter (의도된 변경만)
public void setStatus(String status) {
this.status = status;
}
// ④ equals / hashCode (id 기반)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Shipment)) return false;
Shipment that = (Shipment) o;
return id != null && id.equals(that.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
// ⑤ final 아님 (프록시 가능)
// ILIC 의 엔티티 표준 (Lombok 활용)
import lombok.*;
import jakarta.persistence.*;
@Entity
@Table(name = "shipments")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본 생성자
@AllArgsConstructor
@Builder
public class Shipment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "bl_no", length = 50, unique = true)
private String blNo;
@Column(length = 20)
private String status;
@Column(precision = 10, scale = 2)
private BigDecimal weight;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;
// 비즈니스 메서드
public void markAsShipped() {
if (!"BOOKED".equals(this.status)) {
throw new IllegalStateException();
}
this.status = "SHIPPED";
}
}
// → @Entity + @NoArgsConstructor (기본 생성자)
// → final X (Lombok 도 final 안 만듦)
// → 3가지 조건 충족
class Customer {}
엔티티의 3가지 조건은?
답:
1. @Entity:
기본 생성자:
final X:
+ @Id:
기본 생성자 (no-arg constructor):
필요 이유:
- JPA / Hibernate 가 리플렉션으로 객체 생성
- newInstance() 호출 (기본 생성자)
- 그 후 필드 값 채움 (리플렉션)
→ 없으면 객체 생성 불가
// JPA 내부 (개념적 의사 코드)
Class<Shipment> clazz = Shipment.class;
Shipment instance = clazz.getDeclaredConstructor().newInstance();
// ↑ 기본 생성자 필요
// 필드 채우기 (리플렉션)
Field idField = clazz.getDeclaredField("id");
idField.setAccessible(true);
idField.set(instance, 1L);
// ... 모든 필드
// → 기본 생성자 필수
class Shipment {}
누락 시 에러:
No default constructor for entity:
com.ilic.Shipment
→ HibernateException
→ 객체 생성 실패
→ 런타임 에러
// 접근 제어자
public Shipment() {} // ✅ 가장 안전
protected Shipment() {} // ✅ 권장 (외부 호출 방지)
Shipment() {} // ⚠️ package-private (괜찮음)
private Shipment() {} // ❌ JPA 가 접근 못 함
// 권장:
// - protected (외부에서 무분별 사용 방지)
// - 필요 시 Builder 또는 정적 팩토리 메서드
// Lombok 으로 간편하게
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Shipment {
// 자동 생성:
// protected Shipment() {}
// public Shipment(필드 모두) {}
}
// 또는 @Data, @Builder 등 다른 어노테이션
class Shipment {}
// ILIC 의 기본 생성자 패턴
@Entity
@Table(name = "shipments")
@NoArgsConstructor(access = AccessLevel.PROTECTED) // ① 기본 생성자 (JPA)
@AllArgsConstructor(access = AccessLevel.PRIVATE) // ② 전체 생성자 (Builder)
@Builder
@Getter
public class Shipment {
@Id @GeneratedValue
private Long id;
private String blNo;
private String status;
// 정적 팩토리 (도메인 의미)
public static Shipment create(String blNo) {
return Shipment.builder()
.blNo(blNo)
.status("BOOKED")
.build();
}
// 비즈니스 메서드
public void markAsShipped() {
this.status = "SHIPPED";
}
}
// 사용
// 외부 코드:
// new Shipment() ← ❌ protected, 외부 X
// Shipment.create("BL001") ← ✅ 정적 팩토리
// JPA:
// 리플렉션으로 protected Shipment() 호출 (가능)
// 그 후 필드 채움
기본 생성자 필수인 이유는?
답:
1. 이유:
동작:
접근 제어:
Lombok:
JPA 가 프록시 만드는 이유:
1. Lazy Loading:
- @ManyToOne(fetch = LAZY)
- 실제 접근 시점에 SQL
- 프록시 객체 먼저 (값 X)
2. 변경 감지 (일부):
- 객체 수정 감지
- 트랜잭션 commit 시 UPDATE
→ 프록시 필요
프록시의 동작:
엔티티 클래스 (Shipment):
↑ 상속
Hibernate 가 동적 생성 자식 (Shipment$$ProxyXX):
- Shipment 의 메서드 오버라이드
- getId() 호출 시: 캐시 사용
- 다른 메서드 호출 시: SQL 실행 + 위임
→ Hibernate 가 런타임에 자식 만듦
final 의 문제:
final class Shipment { // ❌
// ...
}
Hibernate:
- Shipment 의 자식 만들려고 함
- 하지만 final → 상속 불가
- 프록시 생성 실패
- 에러
// final 메서드도 안 됨
@Entity
public class Shipment {
@Id
private Long id;
public final Long getId() { // ❌
return id;
}
// 프록시가 오버라이드 못 함
// Lazy Loading 등 동작 X
}
// 권장:
// - 클래스 final X
// - 메서드 final X
// - 또는 Lombok @Getter (final 안 붙임)
프록시 생성 라이브러리:
Hibernate 가 사용:
- CGLIB (또는 ByteBuddy)
- 런타임 바이트코드 생성
- 자식 클래스 (Subclass) 생성
→ 부모 (엔티티) 의 final 은 X
// Lazy Loading 동작 (final X 일 때)
@Entity
public class Shipment {
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer; // 프록시
}
Shipment s = repo.findById(1L).orElseThrow();
// s.customer 는 프록시 객체 (실제 Customer X)
// 클래스: Customer$$ProxyXX
s.getCustomer().getName(); // 이때 SQL 실행 (실제 로딩)
// Customer 가 final 이면?
// → 프록시 생성 실패
// → LazyInitializationException 같은 문제
class Customer {}
class Shipment { Customer getCustomer() { return null; } }
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }
// ILIC 의 엔티티 (final 절대 X)
@Entity
@Table(name = "shipments")
public class Shipment { // final X
// 필드 / 메서드 모두 final X
}
@Entity
@Table(name = "customers")
public class Customer { // final X
// ...
}
// Lazy Loading 동작:
@Entity
public class Shipment {
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer; // Customer$$ProxyXX
}
Shipment s = repo.findById(1L).orElseThrow();
s.getCustomer().getName(); // 이 시점 SQL
// → final 없이 모든 엔티티 정상 동작
// → 만약 final 이면 모든 ILIC 동작 멈춤
ShipmentRepository repo;
class ShipmentRepository {
java.util.Optional<Shipment> findById(Long id) { return null; }
}
final 클래스 X 인 이유 (프록시) 는?
답:
1. 프록시:
목적:
final:
CGLIB:
DTO (Data Transfer Object):
데이터 전달 전용 객체:
- 계층 간 (Controller ↔ Service)
- 외부 API 응답
- 비즈니스 로직 X
- 영속성 X
Entity:
영속성 객체:
- DB 와 매핑
- 영속성 컨텍스트 관리
- 도메인 로직 가능
- 변경 감지
| 항목 | DTO | Entity |
|---|---|---|
| 목적 | 데이터 전달 | 영속성 + 도메인 |
| @Entity | X | O |
| DB 매핑 | X | O |
| 영속성 컨텍스트 | X | O |
| 비즈니스 로직 | X (보통) | O |
| 변경 감지 | X | O |
| Lifecycle | 단순 | 4가지 상태 |
| 변경 가능성 | 자유 | 신중 |
분리의 가치:
Entity 만 사용 시:
- 노출 위험 (필드 모두)
- 변경 시 영향 (API 까지)
- 보안 위험 (id 등)
Entity + DTO:
- 계층 분리 (SoC)
- 안전 (필드 노출 제한)
- 변경 격리
→ 5주차 SoC 원칙
// DTO 정의
public class ShipmentDto {
private Long id;
private String blNo;
private String customerName;
public ShipmentDto(Shipment s) {
this.id = s.getId();
this.blNo = s.getBlNo();
this.customerName = s.getCustomer().getName();
}
}
// 변환
ShipmentDto dto = new ShipmentDto(shipment);
// 또는 MapStruct (자동 매핑)
class Shipment {
Long getId() { return null; }
String getBlNo() { return null; }
Customer getCustomer() { return null; }
}
class Customer { String getName() { return null; } }
Shipment shipment;
// Projection (자동 DTO)
public interface ShipmentProjection {
Long getId();
String getBlNo();
String getCustomerName();
}
// Repository
public interface ShipmentRepository extends JpaRepository<Shipment, Long> {
List<ShipmentProjection> findByStatus(String status);
}
// → Entity 가 아닌 Projection 반환
// → 필요 컬럼만
class Shipment {}
interface JpaRepository<T, ID> {}
// ILIC 의 Entity + DTO 분리
// Entity (영속성)
@Entity
@Table(name = "shipments")
public class Shipment {
@Id @GeneratedValue
private Long id;
private String blNo;
private String status;
private BigDecimal weight;
@ManyToOne
private Customer customer;
// 비즈니스 메서드
public void markAsShipped() { this.status = "SHIPPED"; }
}
// DTO (API 응답)
public record ShipmentResponseDto(
Long id,
String blNo,
String status,
BigDecimal weight,
String customerName
) {
public static ShipmentResponseDto from(Shipment s) {
return new ShipmentResponseDto(
s.getId(),
s.getBlNo(),
s.getStatus(),
s.getWeight(),
s.getCustomer().getName()
);
}
}
// DTO (요청)
public record ShipmentCreateRequest(
@NotBlank String blNo,
@NotNull BigDecimal weight
) {
public Shipment toEntity(Customer customer) {
return Shipment.builder()
.blNo(blNo)
.weight(weight)
.customer(customer)
.status("BOOKED")
.build();
}
}
// Controller
@RestController
public class ShipmentController {
@GetMapping("/shipments/{id}")
public ShipmentResponseDto get(@PathVariable Long id) {
Shipment s = service.get(id);
return ShipmentResponseDto.from(s);
}
}
// → Entity (내부) / DTO (외부) 분리
// → SoC, 안전
class Customer { String getName() { return null; } }
class Shipment {
Long getId() { return null; }
String getBlNo() { return null; }
String getStatus() { return null; }
java.math.BigDecimal getWeight() { return null; }
Customer getCustomer() { return null; }
static Builder builder() { return null; }
static class Builder {
Builder blNo(String s) { return this; }
Builder weight(java.math.BigDecimal w) { return this; }
Builder customer(Customer c) { return this; }
Builder status(String s) { return this; }
Shipment build() { return null; }
}
}
class ShipmentService { Shipment get(Long id) { return null; } }
@interface NotBlank {}
@interface NotNull {}
DTO 와 Entity 의 차이는?
답:
1. DTO:
Entity:
차이:
권장:
@Table:
엔티티가 매핑될 테이블 명시:
- 테이블명 (name)
- 스키마 / 카탈로그
- 유니크 제약
- 인덱스
import jakarta.persistence.*;
@Entity
@Table(
name = "shipments", // 테이블명
schema = "ilic_db", // 스키마 (선택)
uniqueConstraints = {
@UniqueConstraint(name = "uk_bl_no",
columnNames = "bl_no")
},
indexes = {
@Index(name = "idx_status", columnList = "status"),
@Index(name = "idx_customer", columnList = "customer_id")
}
)
public class Shipment {
// ...
}
@Table 옵션:
name: 테이블명
schema: 스키마
catalog: 카탈로그
uniqueConstraints: 복합 unique
indexes: 인덱스 (DDL 생성 시)
# application.yml
spring:
jpa:
hibernate:
ddl-auto: update # 또는 create / validate
ddl-auto:
none: 아무것도 X (운영)
validate: 매핑 검증만
update: 변경 사항 반영 (개발)
create: 매번 새로 (테스트)
create-drop: 시작 시 create, 종료 시 drop
@Table 의 indexes / uniqueConstraints:
- DDL 생성 시 반영
- 운영은 보통 Flyway / Liquibase
// ILIC 의 @Table 활용
@Entity
@Table(
name = "shipments",
indexes = {
@Index(name = "idx_shipments_status", columnList = "status"),
@Index(name = "idx_shipments_customer", columnList = "customer_id"),
@Index(name = "idx_shipments_created", columnList = "created_at")
},
uniqueConstraints = {
@UniqueConstraint(name = "uk_shipments_bl_no", columnNames = "bl_no")
}
)
public class Shipment {
// ...
}
// 효과:
// - DDL 자동 생성 (개발)
// - 인덱스 명시 (성능)
// - UNIQUE 제약 (데이터 무결성)
// ILIC 운영:
// - Flyway / Liquibase 로 마이그레이션
// - @Table 의 메타정보는 참고
// - 실제 DDL 은 별도 관리
@Table 어노테이션은?
답:
1. 목적:
옵션:
DDL:
운영:
자동 이름 변환:
Spring Boot 의 기본 명명 전략:
- SpringPhysicalNamingStrategy
변환:
- Shipment → shipments (snake_case, 복수)
- 사실은 (Hibernate 기본):
- Shipment → shipment
Spring Boot 의 추가:
- 단수 → 그대로 (Shipment → shipment)
- 또는 명시
// @Table 생략 시
@Entity
public class Shipment {
// 자동: shipment (단수)
}
// 명시
@Entity
@Table(name = "shipments")
public class Shipment {
// 명시: shipments (복수)
}
// 권장: 명시
camelCase → snake_case:
자바 필드:
- blNo
- createdAt
- shipperName
DB 컬럼 (자동):
- bl_no
- created_at
- shipper_name
→ Spring Boot 자동
# application.yml
spring:
jpa:
hibernate:
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# 기본: SpringPhysicalNamingStrategy
# 또는 커스텀
명시의 가치:
@Table(name = ...) 권장:
- 명확성
- 변경 안전
- 자동 규칙 의존 X
@Column 도 마찬가지
→ 보통 명시
// ILIC 의 명명 (명시 vs 자동)
// 명시 (권장)
@Entity
@Table(name = "shipments")
public class Shipment {
@Column(name = "bl_no", length = 50)
private String blNo;
@Column(name = "created_at")
private LocalDateTime createdAt;
}
// 자동 (Spring Boot 기본 명명)
@Entity
public class Shipment {
private String blNo; // bl_no (자동)
private LocalDateTime createdAt; // created_at (자동)
}
// ILIC 표준:
// - @Table(name = ...) 명시 (테이블명)
// - @Column 은 필요 시만 명시
// (자동 변환 잘 되는 경우 생략)
// - 특수 케이스 (이름 다름) 만 명시
이름 명시 vs 자동은?
답:
1. 자동:
변환:
권장:
@Column:
엔티티 4가지 상태:
1. 비영속 (new / transient):
- new Shipment() 직후
- 영속성 컨텍스트 X
- DB X
2. 영속 (managed / persistent):
- em.persist() 후
- 영속성 컨텍스트 관리
- Dirty Checking 대상
3. 준영속 (detached):
- em.detach() 또는 close 후
- 영속성 컨텍스트 X
- 변경 감지 X
4. 삭제 (removed):
- em.remove() 후
- 영속성 컨텍스트에 삭제 표시
- flush 시 DELETE
상태 전이:
new Shipment()
↓ (객체 생성)
비영속 (new)
↓ em.persist()
영속 (managed)
↓ em.detach() / em.clear()
준영속 (detached)
↓ em.merge()
영속 (managed)
↓ em.remove()
삭제 (removed)
↓ flush
DELETE 실행
// 1. 비영속
Shipment s = new Shipment();
s.setBlNo("BL001");
// 2. 영속
em.persist(s);
// 영속성 컨텍스트 등록
// INSERT 는 아직 X (flush 시)
s.setStatus("SHIPPED");
// 영속이므로 변경 감지 자동
// commit 시 UPDATE
// 3. 준영속
em.detach(s);
// 또는 em.clear() (모두)
// 또는 트랜잭션 종료
s.setStatus("CANCELLED");
// 변경 감지 X (DB 반영 X)
// 4. 다시 영속
Shipment merged = em.merge(s);
// 새 영속 객체 반환
// 5. 삭제
em.remove(merged);
// 영속성 컨텍스트에 삭제 표시
// flush 시 DELETE
class Shipment {
void setBlNo(String s) {}
void setStatus(String s) {}
}
class EntityManager {
void persist(Object o) {}
void detach(Object o) {}
void clear() {}
<T> T merge(T t) { return null; }
void remove(Object o) {}
}
EntityManager em;
Spring Data JPA 의 추상화:
save() = persist 또는 merge
- id null = persist (new)
- id 있음 = merge (또는 그냥 영속)
findById() = find
- 영속 상태로 반환
delete() = remove
→ 라이프사이클 추상화
// ILIC 의 엔티티 라이프사이클
@Service
@Transactional
public class ShipmentService {
@Autowired ShipmentRepository repo;
public Shipment create(ShipmentCreateRequest req) {
// 1. 비영속
Shipment s = Shipment.builder()
.blNo(req.blNo())
.build();
// 2. 영속
return repo.save(s); // INSERT
}
public void updateStatus(Long id, String newStatus) {
// 영속 상태 (find)
Shipment s = repo.findById(id).orElseThrow();
// 변경 감지 (자동 UPDATE)
s.setStatus(newStatus);
// 메서드 종료 → 트랜잭션 commit → UPDATE 자동
}
public void cancel(Long id) {
Shipment s = repo.findById(id).orElseThrow();
// 삭제
repo.delete(s); // DELETE (flush 시)
}
}
// → 영속성 컨텍스트가 라이프사이클 관리
// → 개발자는 객체 작업만
class Shipment {
void setStatus(String s) {}
static Builder builder() { return null; }
static class Builder {
Builder blNo(String s) { return this; }
Shipment build() { return null; }
}
}
record ShipmentCreateRequest(String blNo) {}
ShipmentRepository repo;
interface ShipmentRepository {
Shipment save(Shipment s);
java.util.Optional<Shipment> findById(Long id);
void delete(Shipment s);
}
| Q | 핵심 답변 |
|---|---|
| @Entity? | JPA 관리 선언 |
| 엔티티? | 영속성 객체 |
| 3가지 조건? | @Entity + no-arg + final X |
| 기본 생성자? | 리플렉션 |
| final X? | 프록시 |
| DTO vs Entity? | 전달 vs 영속성 |
| @Table? | 테이블 매핑 |
| 자동 명명? | snake_case |
| 라이프사이클? | 4가지 상태 |
| Lazy Loading? | 프록시 |
답:
답:
답:
답:
답:
1. @Entity = JPA 관리 선언
2. 3가지 조건
@Entity 어노테이션, (2) 기본 생성자 필수 (리플렉션 객체 생성), (3) final 클래스 / 메서드 X (프록시 자식 생성)3. DTO vs Entity
이번 Unit에서 엔티티의 개념을 봤다면, 다음은 @Id (PK 매핑).
🏷️ Phase 4 — JPA 엔티티 매핑
✅ Unit 4.1 @Entity 와 엔티티 개념 ← 여기
⏭ Unit 4.2 @Id 와 PK 매핑
⏭ Unit 4.3 @GeneratedValue 전략 ★깊이
⏭ Unit 4.4 @Column 과 컬럼 매핑
⏭ Unit 4.5 자동 매핑 규칙 (camelCase ↔ snake_case)
🗂️ Part A — 데이터 모델링과 ORM
✅ Phase 1 (5)
✅ Phase 2 (2)
✅ Phase 3 (4)
🏷️ Phase 4 (1/5)
총: 12/24 Unit (50%!)
🏷️ Phase 4 시작 — JPA 엔티티 매핑