[LG U+ 유레카 3기] 순수 JPA 복합키 실습(@IdClass & @EmbeddedId)

2025. 11. 17. 15:47Java/JPA

오늘 오후 실습에서는 JPA에서 자주 등장하는 난제 중 하나인 복합키(Composite Key) 를 두 가지 방식으로 직접 다뤘다.
즉, 하나의 테이블(엔티티)이 두 개 이상의 컬럼으로 Primary Key를 구성하는 상황을 실제 코드로 구현하여, Hibernate가 어떤 방식으로 매핑하고 테이블을 생성하는지 확인하는 실습이었다.

Spring JPA와 가장 크게 다른 점

  • EntityManagerFactory(EFM)를 개발자가 직접 생성
  • HibernatePersistenceProvider() 를 직접 사용
  • 트랜잭션을 begin() / commit() 으로 직접 제어
  • @Service, @Repository, @Transactional 등의 Spring 전용 어노테이션 없음
  • application.properties / yml 설정 없음
  • 스프링 컨테이너 없이 순수 자바 코드만으로 구동
  • 순수 JPA VS Spring Data JPA

❶ 오늘 실습에서 구현한 시나리오

학생, 상품 등 여러 시스템에서 “단일 ID가 아닌 여러 필드 조합으로 식별”해야 하는 경우가 존재한다.
예를 들어:

  • 학생 : 학년 + 반 + 번호
  • 상품 : code + number

이런 경우 하나의 컬럼으로 PK를 둘 수 없기 때문에 복합키를 사용하게 된다. JPA는 이를 처리하는 방식으로 @IdClass 방식@EmbeddedId 방식 두 가지를 제공한다.


❷ @IdClass 방식 — 엔티티에 @Id 여러 개 + 키 클래스 따로

@IdClass 방식은 엔티티에 @Id를 여러 개 선언하고, 그 조합을 나타내는 키 클래스를 별도로 만드는 구조다.

📌 ProductId (복합키 클래스)


public class ProductKey implements Serializable {

    private static final long serialVersionUID = 1L;

    private String code;
    private int number;

    public ProductKey() {}

    @Override
    public int hashCode() {
        return Objects.hash(code, number);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        ProductKey other = (ProductKey) obj;
        return Objects.equals(code, other.code) && number == other.number;
    }
}

📌 Product 엔티티


@Entity
@IdClass(ProductKey.class)
public class Product {

    @Id
    private String code;

    @Id
    private int number;

    private String color;

    public Product() {}
}

@Id 두 개를 그대로 엔티티에 놓고, 외부 클래스로 복합키를 정의한다. 코드가 길어지고 PK 필드가 엔티티에 그대로 노출된다는 특징이 있다.


❸ @EmbeddedId 방식 — 복합키 전체를 객체로 묶어 사용하는 방식

두 번째 방식은 복합키 자체를 하나의 객체로 묶어 엔티티에 “하나의 필드로” 넣는 방식이다.
현업에서 훨씬 많이 쓰이는 방식이다.

📌 StudentKey (복합키 객체)


@Embeddable
public class StudentKey implements Serializable {

    private static final long serialVersionUID = 1L;

    private String code;
    private int number;

    @Override
    public int hashCode() {
        return Objects.hash(code, number);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null) return false;
        if (getClass() != obj.getClass()) return false;
        StudentKey other = (StudentKey) obj;
        return Objects.equals(code, other.code) && number == other.number;
    }
}

📌 Student 엔티티


@Entity
public class Student {

    @EmbeddedId
    private StudentKey id;

    private String name;

    public void setId(StudentKey id){ this.id = id; }
}

엔티티가 훨씬 깔끔해지고, 복합키 필드가 객체 하나로 묶여서 구조가 정리된다.


❹ Hibernate PersistenceProvider로 직접 EMF 구성

Spring 없이 순수 JPA/Hibernate 환경에서 EMF를 직접 구성하는 실습도 진행했다.


Map<String, String> props = new HashMap<>();
props.put("hibernate.hbm2ddl.auto", "update");
props.put("hibernate.show_sql", "true");

EntityManagerFactory emf =
    new HibernatePersistenceProvider()
        .createContainerEntityManagerFactory(new MyPersistenceUnitInfo(), props);

EntityManager em = emf.createEntityManager();

em.getTransaction().begin();

Product p = new Product();
p.setCode("uplus");
p.setNumber(1);
p.setColor("blue");

em.persist(p);

em.getTransaction().commit();

핵심 동작 흐름

  • EntityManagerFactory 직접 생성
  • EntityManager 획득
  • 트랜잭션 begin → persist → commit
  • commit 시점에서 실제 DB INSERT 발생


❺ 두 방식(@IdClass vs @EmbeddedId) 비교

@IdClass @EmbeddedId
엔티티에 @Id 여러 개 필요 엔티티가 깔끔함 — PK 객체 하나만 있음
코드 길고 가독성 떨어짐 현업에서 많이 사용
레거시 DB 호환성 좋음 객체 지향적

❻ @EmbeddedId 방식 실제 실습 — 등록 및 조회

Student 엔티티를 @EmbeddedId 로 등록한 후, 실제로 insert 및 find 를 수행해보는 중요한 실습이었다.


// EmbeddedId 등록
StudentKey key = new StudentKey();
key.setCode("uplus");
key.setNumber(1);

Student s = new Student();
s.setId(key);
s.setName("홍길동");

em.persist(s);

// 조회
StudentKey key2 = new StudentKey();
key2.setCode("uplus");
key2.setNumber(1);

Student result = em.find(Student.class, key2);
System.out.println(result);

Hibernate 는 StudentKey 내부 필드를 PK 컬럼으로 자동 매핑하여 SQL 을 생성한다.


❼ MyPersistenceUnitInfo — 순수 JPA 환경 설정


@Override
public DataSource getJtaDataSource() {
    HikariDataSource dataSource = new HikariDataSource();
    dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/jpa_basic_5");
    dataSource.setUsername("root");
    dataSource.setPassword("root");
    return dataSource;
}

@Override
public List<String> getManagedClassNames() {
    return List.of("entity.Employee", "entity.Product", "entity.Student");
}

Spring 부트처럼 자동 설정이 있는 것이 아니라, 엔티티 목록, 데이터소스, 트랜잭션 방식 등을 모두 개발자가 직접 구성해야 한다.
이 점이 “순수 JPA 실습”의 핵심이다.


❽ 이번 실습에서 배운 핵심 교훈

  • JPA 복합키는 @IdClass 와 @EmbeddedId 두 방식이 존재
  • EmbeddedId 방식이 절대적으로 더 깔끔하고 유지보수성이 좋음
  • 리플렉션 때문에 기본 생성자가 반드시 필요
  • equals & hashCode 필수 구현
  • 순수 JPA에서는 모든 환경 설정을 직접 구성해야 함

❾ 마무리

오늘 실습은 JPA 초급 → 중급으로 넘어가는 중요한 단계였다.
복합키는 연관관계 매핑에서도 자주 등장하기 때문에, 이번 실습의 개념을 정확히 이해한 뒤 다음 단계로 넘어가면 훨씬 수월하다.