2025. 11. 17. 15:47ㆍJava/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 초급 → 중급으로 넘어가는 중요한 단계였다.
복합키는 연관관계 매핑에서도 자주 등장하기 때문에, 이번 실습의 개념을 정확히 이해한 뒤 다음 단계로 넘어가면 훨씬 수월하다.
'Java > JPA' 카테고리의 다른 글
| [LG U+ 유레카 3기] Spring Data JPA CRUD + Lombok + 패턴 실습 정리 (0) | 2025.11.25 |
|---|---|
| [LG U+ 유레카 3기] JPA N+1 · Fetch Join · JPQL Join (0) | 2025.11.20 |
| [LG U+ 유레카 3기]순수JPA 실습 — JPQL (Java Persistence Query Language) (0) | 2025.10.20 |
| [LG U+ 유레카 3기]순수 JPA 실습 엔티티 생명주기 (persist / find / merge / detach / remove) (0) | 2025.10.20 |
| [LG U+ 유레카 3기]JPA 영속성 컨텍스트와 1차 캐시 실습 및 정리 (0) | 2025.10.20 |