7주차 Unit 4.2 — @Id 와 PK 매핑

Psj·2026년 6월 1일

F-lab

목록 보기
225/239

Unit 4.2 — @Id 와 PK 매핑

F-LAB JAVA · 7주차 · Phase 4 · JPA 엔티티 매핑


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • @Id 어노테이션의 의미는?
  • PK 의 역할 (DB / 객체) 은?
  • @Id 필수 인 이유는?
  • @Id 없으면 어떻게 되는가 ?
  • 단일 키 매핑 은?
  • 복합 키 @IdClass 는?
  • 복합 키 @EmbeddedId 는?
  • 자연 키 vs 대리 키 차이는?
  • PK 타입 선택 (Long/UUID/String) 은?

🎯 핵심 한 문장

@Id 는 엔티티의 식별자 (Primary Key) 필드를 표시하는 어노테이션으로 모든 엔티티가 반드시 1개 (또는 복합 키) 가져야 하며 JPA 가 영속성 컨텍스트에서 이 값을 기준으로 객체 동일성 (==) 을 보장하고, 복합 키는 @IdClass 또는 @EmbeddedId 로 표현하며 실무는 자연 키보다 대리 키 (Surrogate Key, 보통 Long auto-increment) 가 압도적이다.
@Id 는 엔티티의 식별자 (Primary Key) 필드를 표시 하는 어노테이션이다.
모든 엔티티는 반드시 @Id 1개 이상 을 가져야 하며 (단일 키 또는 복합 키), 누락 시 매핑 시점에 No identifier specified for entity 예외가 발생한다.
JPA 가 이 PK 값을 기준으로 — (1) DB 의 행 식별 (SELECT WHERE id = ?), (2) 영속성 컨텍스트 1차 캐시 키 (같은 id = 같은 객체, == 보장), (3) 변경 감지 추적 단위 — 의 핵심 정보로 사용한다.
복합 키 (여러 컬럼이 합쳐 PK) 는 — @IdClass (별도 PK 클래스, 엔티티에 PK 필드 그대로) 또는 @EmbeddedId (PK 를 임베디드 객체로 감쌈) 의 두 방식으로 표현한다.
실무에선 대리 키 (Surrogate Key, 의미 없는 자동 생성 PK, 보통 Long auto-increment) 가 압도적 — 자연 키 (Natural Key, 의미 있는 PK 예: 주민번호) 는 변경 가능성·성능 등의 이유로 거의 안 쓰며, ILIC 의 모든 102 테이블도 id BIGINT AUTO_INCREMENT 대리 키 표준이다.

비유 — 주민등록번호 (PK)

@Id = 주민등록번호:

비유:
  - 엔티티 = 사람
  - @Id = 주민등록번호 (PK)
  - 모든 사람 = 유일 번호 보유

PK 의 역할:
  - DB: 행 식별 (특정 사람 찾기)
  - 객체: 같은 번호 = 같은 사람
  - 영속성: 1차 캐시 키

복합 키 = 다중 식별:
  - 학교 + 학년 + 반 + 번호 (학생 식별)
  - 한 필드로 부족 시
  - @IdClass / @EmbeddedId

자연 키 vs 대리 키:
  - 자연 키: 의미 있는 (주민번호, 이메일)
  - 대리 키: 의미 없는 (auto-increment 번호)

실무 = 대리 키:
  - 변경 불가능 (id 1 은 영원히 같은 행)
  - 인덱스 효율
  - 외부 노출 안전

ILIC:
  - 모든 102 테이블 = Long id
  - bl_no 는 UNIQUE 컬럼 (자연 키 후보)
  - 하지만 PK 는 id (대리 키)

→ @Id = PK 매핑, 반드시 1개, 영속성 핵심, 대리 키 압도적.


🧭 9개 섹션 로드맵

1. @Id 정의
2. PK 의 역할
3. @Id 필수 (1개)
4. @Id 없으면 에러
5. 단일 키 매핑
6. 복합 키 @IdClass
7. 복합 키 @EmbeddedId
8. 자연 키 vs 대리 키
9. PK 타입 선택 + equals/hashCode

1️⃣ @Id 정의

1.1 정의

@Id:

  엔티티의 식별자 (Primary Key) 필드:
    - 어노테이션 표시
    - 필드 또는 메서드 위
    - 모든 엔티티 1개 이상 필수

1.2 위치

import jakarta.persistence.Id;

@Entity
public class Shipment {
    
    @Id   // ← 필드 위
    private Long id;
    
    // ...
}

// 또는 메서드 위 (Property Access)
@Entity
public class Shipment {
    private Long id;
    
    @Id
    public Long getId() { return id; }
}

1.3 필드 접근 vs 메서드 접근

접근 방식:

  필드 접근 (Field Access):
    - @Id 가 필드 위
    - JPA 가 리플렉션으로 필드 직접 접근
    - 실무 표준

  메서드 접근 (Property Access):
    - @Id 가 getter 위
    - getter 호출
    - 거의 안 씀

→ 필드 접근 권장 (간결)

1.4 ILIC 의 맥락

// @Id 표준 (ILIC)
@Entity
@Table(name = "shipments")
public class Shipment {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;   // PK
    
    private String blNo;
    private String status;
    // ...
}

// 102 테이블 모두 비슷한 패턴:
// - @Id Long id
// - 자동 생성 (IDENTITY)
// - 필드 접근

class Customer {}

1.5 자기 점검 답변

@Id 어노테이션의 의미는?

:
1. @Id:

  • PK 필드 표시
  1. 위치:

    • 필드 위 (표준)
  2. 필수:

    • 모든 엔티티 1개
  3. 표준:

    • 필드 접근

2️⃣ PK 의 역할

2.1 DB 에서 PK

DB 에서 PK:

  Primary Key:
    - 유일한 식별자
    - NOT NULL
    - UNIQUE
    - 자동 인덱스 (Clustered Index)

  역할:
    - 행 식별
    - 빠른 조회
    - FK 의 참조 대상

2.2 객체에서 식별자

객체에서 식별자:

  자바 객체 식별:
    - == (메모리 주소)
    - equals (내용)

  JPA 엔티티:
    - PK 값으로 식별
    - 같은 PK = 같은 데이터
    - 같은 트랜잭션 내 == (1차 캐시)

2.3 JPA 의 PK 활용

JPA 의 PK 활용:

  1. SELECT (em.find):
     - SELECT * FROM table WHERE id = ?

  2. 영속성 컨텍스트 캐시:
     - id 가 키
     - 같은 id 두 번 find = SQL 1번 + 캐시 1번

  3. UPDATE:
     - WHERE id = ?
     - 정확한 행만

  4. DELETE:
     - WHERE id = ?

2.4 1차 캐시 동작

// 1차 캐시 (영속성 컨텍스트)
@Transactional
public void demo() {
    Shipment s1 = repo.findById(1L).orElseThrow();  // SQL 1번
    Shipment s2 = repo.findById(1L).orElseThrow();  // 캐시 (SQL X)
    
    // 같은 트랜잭션 안에서
    s1 == s2;   // true! (영속성 컨텍스트 1차 캐시)
    s1.equals(s2);   // true
    
    // 다른 트랜잭션이면:
    // s1 == s2;   // false (다른 영속성 컨텍스트)
}
class Shipment {}
ShipmentRepository repo;
interface ShipmentRepository { java.util.Optional<Shipment> findById(Long id); }

2.5 ILIC 의 맥락

// ILIC 의 PK 활용
@Transactional
public void process(Long shipmentId) {
    // 1. PK 로 조회 (영속성 컨텍스트 시작)
    Shipment s = shipmentRepository.findById(shipmentId).orElseThrow();
    
    // 2. 같은 트랜잭션 내 다시 조회 (캐시)
    Shipment same = shipmentRepository.findById(shipmentId).orElseThrow();
    s == same;   // true (같은 객체)
    
    // 3. 수정 (PK 기반)
    s.setStatus("SHIPPED");
    // 트랜잭션 commit 시:
    // UPDATE shipments SET status = ? WHERE id = ?
    //                                        ↑ PK 사용
    
    // 4. 삭제
    shipmentRepository.delete(s);
    // DELETE FROM shipments WHERE id = ?
    //                              ↑ PK 사용
}
class Shipment { void setStatus(String s) {} }
ShipmentRepository shipmentRepository;
interface ShipmentRepository {
    java.util.Optional<Shipment> findById(Long id);
    void delete(Shipment s);
}

2.6 자기 점검 답변

PK 의 역할 (DB / 객체) 은?

:
1. DB:

  • 유일 식별 / 인덱스
  1. 객체:

    • 식별자
  2. JPA:

    • 1차 캐시 키
  3. 활용:

    • SELECT / UPDATE / DELETE

3️⃣ @Id 필수 (1개)

3.1 모든 엔티티 @Id 필수

모든 엔티티 @Id 필수:

  - 단일 키: @Id 1개
  - 복합 키: @Id 여러 (또는 @EmbeddedId)
  - 0개: 금지 (예외)

→ JPA 의 핵심 규칙

3.2 왜 필수인가

왜 필수인가:

  JPA 는 PK 로:
    - 행 식별
    - 1차 캐시 키
    - 변경 감지

  PK 없으면:
    - 식별 불가
    - 캐시 X
    - 어떤 행 UPDATE?

→ 동작 불가

3.3 단일 키 (보통)

// 단일 키 (99% 케이스)
@Entity
public class Shipment {
    @Id
    private Long id;
    // ...
}

// 1개 PK 필드

3.4 복합 키 (드묾)

// 복합 키 (드물게)
@Entity
@IdClass(ShipmentLegId.class)
public class ShipmentLeg {
    @Id
    private Long shipmentId;
    
    @Id
    private int legSequence;
    
    // 두 필드 합쳐 PK
}

public class ShipmentLegId implements Serializable {
    private Long shipmentId;
    private int legSequence;
    
    // equals / hashCode 필수
}

3.5 ILIC 의 맥락

// ILIC = 거의 모두 단일 키
@Entity
public class Shipment {
    @Id
    @GeneratedValue
    private Long id;
}

@Entity
public class Customer {
    @Id
    @GeneratedValue
    private Long id;
}

@Entity
public class Booking {
    @Id
    @GeneratedValue
    private Long id;
}

// 102 테이블 모두 @Id Long id

// 복합 키 예외 (드물게):
// - ShipmentItem 같은 종속 엔티티
// - 또는 history 테이블
// - 그래도 보통 별도 id 추가

3.6 자기 점검 답변

@Id 필수인 이유는?

:
1. 필수:

  • 모든 엔티티
  1. 이유:

    • 식별 / 캐시
  2. 개수:

    • 1개 또는 복합
  3. 표준:

    • 단일 키

4️⃣ @Id 없으면 에러

4.1 에러 발생 시점

에러 발생 시점:

  애플리케이션 시작 시:
    - Spring Boot 가 엔티티 스캔
    - @Entity 인데 @Id 없음
    - 즉시 예외

  → 런타임 X, 시작 시 발견

4.2 에러 메시지

에러 메시지 (예):

  org.hibernate.AnnotationException:
  No identifier specified for entity:
  com.ilic.Shipment

  또는:
  
  Caused by: org.hibernate.MappingException:
  ...
  
→ "PK 가 없습니다"

4.3 동작 안 됨

동작 안 됨:

  - Repository 생성 실패
  - 애플리케이션 시작 실패
  - 빨리 발견 (좋음)

→ 컴파일 시점은 아니지만, 빠른 실패

4.4 예시

// ❌ @Id 없음
@Entity
public class Shipment {
    private Long id;       // @Id 없음
    private String blNo;
}

// 실행 시:
// org.hibernate.AnnotationException: 
// No identifier specified for entity: com.ilic.Shipment
// → 시작 실패

// ✅ @Id 있음
@Entity
public class Shipment {
    @Id
    private Long id;
    private String blNo;
}

4.5 IDE 도움

IDE 도움:

  IntelliJ + JPA Buddy / 또는 자체:
    - @Entity 인데 @Id 없으면 경고
    - 빨간 줄
    - 컴파일 전 발견

→ 개발 시점 발견

4.6 ILIC 의 맥락

// ILIC 의 @Id 누락 방지

// IDE 경고 + 시작 시 에러
// → 102 테이블 모두 @Id 확인

// 또한 베이스 클래스로 보장
@MappedSuperclass
public abstract class BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

@Entity
public class Shipment extends BaseEntity {
    // @Id 상속받음
    // 누락 방지 + 표준화
}

4.7 자기 점검 답변

@Id 없으면 어떻게 되는가?

:
1. 시점:

  • 애플리케이션 시작
  1. 에러:

    • AnnotationException
  2. 결과:

    • 시작 실패
  3. 방지:

    • IDE / 베이스 클래스

5️⃣ 단일 키 매핑

5.1 단일 키 (실무 표준)

단일 키 (실무 표준):

  하나의 컬럼이 PK:
    - 단순
    - 빠름
    - 99% 케이스

  보통:
    - Long (auto-increment)
    - 또는 UUID (분산)

5.2 표준 패턴

// 표준 패턴
@Entity
public class Shipment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // ...
}

// DB:
// CREATE TABLE shipments (
//     id BIGINT AUTO_INCREMENT PRIMARY KEY,
//     ...
// );

5.3 타입 선택

타입사용 케이스장단점
Long일반 (auto-increment)빠름, 작음, 표준
UUID분산 시스템충돌 X, 큼 (128bit)
String비즈니스 키자연 키 시, 보통 X
Integer작은 테이블21억 한계

5.4 wrapper vs primitive

// Long (Wrapper) vs long (primitive)
@Id
private Long id;   // ✅ Wrapper 권장
// 이유:
// - new 직후 null (비영속 상태 구분)
// - long 이면 0 으로 초기화 → 헷갈림

@Id
private long id;   // ❌ 비권장
// long 은 null X
// new 직후 0 → 이미 영속처럼 보임

5.5 @Id + @GeneratedValue

@Id + @GeneratedValue:

  거의 항상 함께:
    @Id
    @GeneratedValue(strategy = ...)
    private Long id;

  GeneratedValue:
    - 자동 생성 전략
    - IDENTITY / SEQUENCE / TABLE / AUTO
    - 다음 Unit 4.3 ★ 깊이

→ 짝꿍

5.6 ILIC 의 맥락

// ILIC 의 단일 키 표준
@Entity
@Table(name = "shipments")
public class Shipment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // ...
}

@Entity
@Table(name = "customers")
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    // ...
}

// 102 테이블 동일
// - @Id Long id
// - IDENTITY (MySQL auto-increment)

// DB:
// CREATE TABLE shipments (
//     id BIGINT AUTO_INCREMENT PRIMARY KEY,
//     ...
// );

// 장점:
// - 단순
// - 일관
// - 빠름
// - 분산 X (단일 DB)

5.7 자기 점검 답변

단일 키 매핑은?

:
1. 단일 키:

  • 99% 표준
  1. 타입:

    • Long (auto-increment)
  2. Wrapper:

    • Long (null 구분)
  3. GeneratedValue:

    • 짝꿍

6️⃣ 복합 키 @IdClass

6.1 복합 키

복합 키:

  여러 컬럼이 합쳐 PK:
    - 단일 컬럼으론 식별 X
    - 두 개 이상의 조합으로

  예:
    - 학생 (학교 + 학번)
    - 주문 상품 (주문ID + 상품ID)

  드물게 사용

6.2 @IdClass 방식

// 1. PK 클래스 (별도)
public class ShipmentLegId implements Serializable {
    private Long shipmentId;
    private int legSequence;
    
    // 기본 생성자, equals, hashCode 필수
    public ShipmentLegId() {}
    
    public ShipmentLegId(Long shipmentId, int legSequence) {
        this.shipmentId = shipmentId;
        this.legSequence = legSequence;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ShipmentLegId)) return false;
        ShipmentLegId that = (ShipmentLegId) o;
        return shipmentId.equals(that.shipmentId) 
            && legSequence == that.legSequence;
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(shipmentId, legSequence);
    }
}

// 2. 엔티티
@Entity
@IdClass(ShipmentLegId.class)
public class ShipmentLeg {
    @Id
    @Column(name = "shipment_id")
    private Long shipmentId;
    
    @Id
    @Column(name = "leg_sequence")
    private int legSequence;
    
    private String portOfLoading;
    // ...
}

6.3 사용

// 조회
ShipmentLegId pk = new ShipmentLegId(1L, 1);
ShipmentLeg leg = em.find(ShipmentLeg.class, pk);

// 또는 Repository
public interface ShipmentLegRepository 
    extends JpaRepository<ShipmentLeg, ShipmentLegId> {
    // 두 번째 타입이 PK 클래스
}
class ShipmentLegId {}
class ShipmentLeg {}
class EntityManager {
    <T> T find(Class<T> c, Object id) { return null; }
}
EntityManager em;
interface JpaRepository<T, ID> {}

6.4 @IdClass 의 특징

@IdClass 의 특징:

  장점:
    - SQL 친화 (필드명 그대로)
    - 단순

  단점:
    - PK 필드 중복 (엔티티 + PK 클래스)
    - equals/hashCode 직접 작성
    - 코드 ↑

6.5 ILIC 의 맥락

// ILIC 의 복합 키 활용 (드물게)

// 시나리오: shipment_status_history
// 같은 shipment 의 여러 상태 변경 이력
// PK: (shipment_id, sequence)

public class ShipmentStatusHistoryId implements Serializable {
    private Long shipmentId;
    private int sequence;
    
    // 기본 생성자, equals, hashCode
    // ... (Lombok @EqualsAndHashCode 가능)
}

@Entity
@Table(name = "shipment_status_history")
@IdClass(ShipmentStatusHistoryId.class)
public class ShipmentStatusHistory {
    @Id
    @Column(name = "shipment_id")
    private Long shipmentId;
    
    @Id
    @Column(name = "sequence")
    private int sequence;
    
    @Column(name = "status")
    private String status;
    
    @Column(name = "changed_at")
    private LocalDateTime changedAt;
}

// 하지만 ILIC 표준은:
// - 별도 id (auto-increment) + index
// - 복합 키 피함

@Entity
@Table(name = "shipment_status_history")
public class ShipmentStatusHistory {
    @Id
    @GeneratedValue
    private Long id;   // 대리 키
    
    private Long shipmentId;
    private int sequence;
    private String status;
    private LocalDateTime changedAt;
    
    // unique 제약 (shipment_id, sequence) 로 보완
}
// → 단순 + 일관

6.6 자기 점검 답변

복합 키 @IdClass 는?

:
1. 복합 키:

  • 여러 컬럼 PK
  1. @IdClass:

    • 별도 PK 클래스
  2. 요구사항:

    • Serializable / equals / hashCode
  3. 단점:

    • PK 필드 중복

7️⃣ 복합 키 @EmbeddedId

7.1 @EmbeddedId 방식

// 1. PK 임베디드 클래스
@Embeddable
public class ShipmentLegId implements Serializable {
    @Column(name = "shipment_id")
    private Long shipmentId;
    
    @Column(name = "leg_sequence")
    private int legSequence;
    
    // 기본 생성자, equals, hashCode 필수
    public ShipmentLegId() {}
    public ShipmentLegId(Long shipmentId, int legSequence) {
        this.shipmentId = shipmentId;
        this.legSequence = legSequence;
    }
    
    // equals, hashCode
}

// 2. 엔티티
@Entity
public class ShipmentLeg {
    @EmbeddedId
    private ShipmentLegId id;   // PK 객체로 임베드
    
    private String portOfLoading;
    // ...
}

7.2 사용

// 조회
ShipmentLegId pk = new ShipmentLegId(1L, 1);
ShipmentLeg leg = em.find(ShipmentLeg.class, pk);

// PK 접근
Long shipmentId = leg.getId().getShipmentId();
int seq = leg.getId().getLegSequence();
class ShipmentLeg {
    ShipmentLegId getId() { return null; }
}
class ShipmentLegId {
    Long getShipmentId() { return null; }
    int getLegSequence() { return 0; }
}
EntityManager em;
class EntityManager {
    <T> T find(Class<T> c, Object id) { return null; }
}

7.3 @IdClass vs @EmbeddedId

항목@IdClass@EmbeddedId
PK 클래스필요필요 (@Embeddable)
엔티티 필드PK 필드 직접PK 객체 (id)
SQL 친화O
객체지향O
코드량더 많음적음

7.4 어떤 걸 사용?

어떤 걸 사용?:

  @EmbeddedId 권장:
    - 더 객체지향
    - PK 클래스 활용
    - JPA 표준

  @IdClass:
    - SQL 친화 (필드명 그대로)
    - 옛 스타일

→ 신규는 @EmbeddedId 권장

7.5 권장: 복합 키 회피

권장: 복합 키 회피

  실무 권장:
    - 복합 키 대신 대리 키 (auto-increment id)
    - UNIQUE 제약 (자연 키 조합)

  이유:
    - 단순 (코드 적음)
    - JPA 연관 매핑 간단
    - 표준화

→ 복합 키 회피

7.6 ILIC 의 맥락

// ILIC 의 복합 키 회피 패턴

// ❌ 복합 키 (피함)
@Entity
public class ShipmentItem {
    @EmbeddedId
    private ShipmentItemId id;   // (shipment_id, item_seq)
    
    private String itemName;
}

// ✅ 대리 키 + UNIQUE (ILIC 표준)
@Entity
@Table(name = "shipment_items",
    uniqueConstraints = @UniqueConstraint(
        name = "uk_shipment_item_seq",
        columnNames = {"shipment_id", "item_sequence"}
    )
)
public class ShipmentItem {
    @Id
    @GeneratedValue
    private Long id;   // 대리 키
    
    @ManyToOne
    @JoinColumn(name = "shipment_id")
    private Shipment shipment;
    
    @Column(name = "item_sequence")
    private int itemSequence;
    
    private String itemName;
}

// 장점:
// - JPA 연관 매핑 간단
// - 외부 노출 안전 (id 만)
// - 102 테이블 일관
class Shipment {}

7.7 자기 점검 답변

복합 키 @EmbeddedId 는?

:
1. @EmbeddedId:

  • PK 임베디드 클래스
  1. @Embeddable:

    • PK 클래스 어노테이션
  2. vs @IdClass:

    • 객체지향
  3. 권장:

    • 회피 (대리 키)

8️⃣ 자연 키 vs 대리 키

8.1 자연 키 (Natural Key)

자연 키 (Natural Key):

  비즈니스 의미 있는 PK:
    - 주민등록번호 (사람)
    - 이메일 (사용자)
    - 사번 (직원)
    - bl_no (배송)

  특징:
    - 의미 있음
    - 외부에서 알 수 있음
    - 변경 가능성

8.2 대리 키 (Surrogate Key)

대리 키 (Surrogate Key):

  의미 없는 자동 생성 PK:
    - auto-increment Long
    - UUID
    - 시퀀스

  특징:
    - 의미 X
    - 자동 생성
    - 변경 불가능 (영원히)

8.3 비교

항목자연 키대리 키
의미있음없음
생성비즈니스 결정자동 (DB)
변경가능 (사고 위험)절대 X
외부 노출위험안전
크기가변고정 (Long 8bytes)
인덱스 효율보통좋음
FK 사용큼 (참조 비용)작음

8.4 자연 키 의 문제

자연 키 의 문제:

  1. 변경 가능성:
     - 이메일 PK → 사용자 이메일 변경 시?
     - FK 모두 변경 (재앙)
     - 또는 변경 불가 (UX ↓)

  2. 외부 노출:
     - URL: /users/john@example.com
     - 보안 / 프라이버시 ↓

  3. 인덱스 비효율:
     - String PK 길음
     - 인덱스 크기 ↑

  4. 외래 키 비용:
     - FK 컬럼 크기 ↑
     - 조인 성능 ↓

8.5 대리 키 의 장점

대리 키 의 장점:

  1. 변경 불가:
     - id 1 = 영원히 같은 행
     - FK 안전

  2. 외부 노출 안전:
     - id 만 노출 (의미 없음)
     - 보안 ↑

  3. 효율:
     - Long 8 bytes
     - 인덱스 작음
     - 빠른 JOIN

  4. 일관:
     - 모든 테이블 같은 패턴

→ 압도적 선호

8.6 자연 키 + 대리 키 (실무)

실무 패턴:

  대리 키 (PK):
    - id BIGINT AUTO_INCREMENT

  자연 키 (UNIQUE):
    - bl_no UNIQUE
    - email UNIQUE
    - business_no UNIQUE

  → 둘 다 활용

8.7 ILIC 의 맥락

-- ILIC 의 표준
CREATE TABLE shipments (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,   -- 대리 키
    bl_no VARCHAR(50) UNIQUE NOT NULL,       -- 자연 키 (UNIQUE)
    customer_id BIGINT,
    status VARCHAR(20),
    -- ...
    FOREIGN KEY (customer_id) REFERENCES customers(id)
);

-- bl_no 는 UNIQUE 이지만 PK X
-- 이유:
-- - bl_no 변경 가능 (오타 수정, 시스템 변경)
-- - PK 면 FK 모두 변경 (재앙)
-- - 대리 키 id 가 PK (안전)
// ILIC 엔티티
@Entity
@Table(name = "shipments",
    uniqueConstraints = @UniqueConstraint(
        name = "uk_shipments_bl_no",
        columnNames = "bl_no"
    )
)
public class Shipment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;   // 대리 키 (PK)
    
    @Column(name = "bl_no", unique = true)
    private String blNo;   // 자연 키 (UNIQUE, PK X)
    
    // ...
}

// 외부 API:
// - /shipments/123 (대리 키 id)
// - 검색은 bl_no 도 가능
// - 하지만 FK 는 id

8.8 자기 점검 답변

자연 키 vs 대리 키 차이는?

:
1. 자연 키:

  • 의미 있음
  1. 대리 키:

    • 자동 생성
  2. 실무:

    • 대리 키 압도적
  3. 혼용:

    • PK = 대리, UNIQUE = 자연

9️⃣ PK 타입 선택 + equals/hashCode

9.1 PK 타입 선택

PK 타입 선택:

  Long (BIGINT) ★★★:
    - 가장 표준
    - 8 bytes
    - 21 quintillion 까지
    - 빠름

  Integer (INT):
    - 21억 한계
    - 작은 테이블만

  UUID (16 bytes):
    - 분산 환경
    - 클라이언트 생성 가능
    - 인덱스 비효율 (랜덤)
    - 또는 ULID

  String:
    - 비즈니스 키 (자연 키)
    - 보통 X

9.2 Long 의 우위

Long 의 우위:

  - MySQL BIGINT 와 매핑
  - auto-increment 표준
  - 작고 빠름
  - 외부 노출 안전
  - JPA 와 잘 맞음

→ 실무 표준

9.3 UUID 사용 케이스

UUID 사용 케이스:

  - 분산 시스템 (여러 DB)
  - 클라이언트가 PK 생성
  - 외부 노출 시 추측 불가

  단점:
    - 16 bytes (크기 2배)
    - 랜덤이라 인덱스 비효율
    - DB 정렬 어색

→ 특수 시나리오

9.4 equals / hashCode

// equals / hashCode (id 기반)
@Entity
public class Shipment {
    @Id
    private Long id;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Shipment)) return false;
        Shipment that = (Shipment) o;
        // id null 체크 (비영속 상태)
        return id != null && id.equals(that.id);
    }
    
    @Override
    public int hashCode() {
        // id 가 변할 수 있으니 클래스 기반 (Hibernate 권장)
        return getClass().hashCode();
    }
}

9.5 왜 이렇게?

왜 이렇게?:

  equals:
    - id 비교 (PK 일치)
    - id null 이면 false (비영속)

  hashCode:
    - id 사용 X (Hibernate 권장)
    - id 가 null → not null 변할 수 있음
    - HashSet 등에서 문제
    - 클래스 hashCode 사용

9.6 Lombok 주의

// ❌ 위험 (Lombok)
@Entity
@EqualsAndHashCode   // 모든 필드 기반!
public class Shipment {
    // 모든 필드로 equals/hashCode
    // 컬렉션, 연관관계 포함 → 문제
}

// ✅ id 만
@Entity
@EqualsAndHashCode(of = "id")
public class Shipment {
    @Id
    private Long id;
}

// 또는 직접 작성 (위 코드)

9.7 ILIC 의 맥락

// ILIC 의 PK 표준
@Entity
@Table(name = "shipments")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Shipment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @EqualsAndHashCode.Include
    private Long id;   // Long, IDENTITY
    
    @Column(name = "bl_no", unique = true)
    private String blNo;
    
    @Column(name = "weight", precision = 10, scale = 2)
    private BigDecimal weight;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private Customer customer;
    
    // ...
}

// ILIC 표준:
// - 모든 PK = Long
// - IDENTITY (MySQL auto-increment)
// - equals/hashCode = id 만
// - Lombok 활용 (안전 옵션)
class Customer {}

9.8 면접 단골 질문 매핑

Q핵심 답변
@Id?PK 필드 표시
PK 역할?식별 / 캐시
필수?모든 엔티티 1개
없으면?시작 실패
단일 키?Long auto-increment
복합 키?@IdClass / @EmbeddedId
자연 vs 대리?대리 표준
Long 권장?안전 / 효율
equals?id 기반
hashCode?클래스 기반

9.9 자기 점검 체크리스트

@Id

  • 정의

PK 역할

  • DB / 객체

필수

  • 1개

없으면

  • 에러

단일 키

  • 표준

@IdClass

  • 복합

@EmbeddedId

  • 복합

자연 vs 대리

  • 대리 표준

타입

  • Long

equals/hashCode

  • id 기반

9.10 추가 심화 질문

Q1: PK 변경 가능?

답:

  • JPA: 거의 불가능 (영속성 컨텍스트 키)
  • DB: 가능하지만 FK 모두 변경
  • 대리 키 = 절대 변경 X

Q2: UUID 인덱스 비효율?

답:

  • 랜덤 값 → B-Tree 분산 ↑
  • ULID (정렬 가능 UUID) 대안
  • MySQL 8 의 UUID_TO_BIN(uuid, 1)

Q3: @Version (낙관적 락) 과 @Id?

답:

  • 별도 (Version 컬럼)
  • @Id 는 식별, @Version 은 동시성
  • 둘 다 필요 시 사용

Q4: PK 노출의 보안 이슈?

답:

  • 순차 id (1, 2, 3...) 노출 위험
  • 다음 데이터 추측 가능
  • UUID 또는 hashids 같은 인코딩

Q5: 마이그레이션 시 PK 처리?

답:

  • 보통 유지
  • 변경 시 FK 모두 변경
  • 신중

🎯 핵심 요약 — 3줄 정리

1. @Id = PK 매핑 필수

  • 모든 엔티티는 @Id 1개 이상 (단일 키 또는 복합)
  • DB 식별 + 영속성 컨텍스트 1차 캐시 키
  • 누락 시 애플리케이션 시작 실패 (AnnotationException)

2. 단일 키 vs 복합 키

  • 단일 키 (99%): Long auto-increment, @Id + @GeneratedValue
  • 복합 키: @IdClass (별도 PK 클래스) 또는 @EmbeddedId (@Embeddable)
  • 실무는 복합 키 회피 (대리 키 + UNIQUE 로 대체)

3. 자연 키 vs 대리 키

  • 자연 키: 의미 있음 (이메일/bl_no), 변경 위험
  • 대리 키: 자동 생성 (auto-increment), 안전
  • 실무 표준: 대리 키 = PK, 자연 키 = UNIQUE
  • equals/hashCode: id 기반 + 클래스 hashCode (Hibernate 권장)

📚 다음으로...

Unit 4.3 — @GeneratedValue 전략 ★ 깊이 파기

이번 Unit에서 @Id 를 봤다면, 다음은 @GeneratedValue (★ 깊이, PK 자동 생성 전략).

  • 4가지 생성 전략 (IDENTITY / SEQUENCE / TABLE / AUTO)
  • 각 전략의 DB 적합성
  • IDENTITY 의 단점 (배치 INSERT 불가)
  • SEQUENCE 가 빠른 이유 (미리 받아둠)

Phase 4 진행 상황

🏷️ Phase 4 — JPA 엔티티 매핑
  ✅ Unit 4.1 @Entity 와 엔티티 개념
  ✅ Unit 4.2 @Id 와 PK 매핑 ← 여기
  ⏭ Unit 4.3 @GeneratedValue 전략 ★깊이
  ⏭ Unit 4.4 @Column 과 컬럼 매핑
  ⏭ Unit 4.5 자동 매핑 규칙

7주차 누적 진행

🗂️ Part A — 데이터 모델링과 ORM
  ✅ Phase 1 (5)
  ✅ Phase 2 (2)
  ✅ Phase 3 (4)
  🏷️ Phase 4 (2/5)

총: 13/24 Unit (54%)

profile
Software Developer

0개의 댓글