복합키란 데이터베이스 테이블의 행을 고유하게 식별하기 위해 두 개 이상의 컬럼을 조합해서 만든 기본키를 말한다.
즉 우리가 일반적으로 말하는 Primary Key의 애트리뷰트가 두 개 이상인 것을 가리킨다.
다음은 시즌 별로 어느 한 축구 팀의 축구 선수의 스탯을 기록하는 테이블이다.
CREATE TABLE season_player_stat (
season CHAR(7) NOT NULL,
player_id BIGINT NOT NULL,
goals INTEGER NOT NULL DEFAULT 0,
assists INTEGER NOT NULL DEFAULT 0,
minutes INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (season, player_id)
);
나의 경우, 맨체스터 유나이티드라는 축구 팀을 좋아하므로 이에 비유해보자면
위와 같은 튜플이 season_player_stat 테이블에 존재할 수 있을 것이다.
이때 이 테이블에서 하나의 행을 유일하게 식별하기 위해 season과 player_id가 pk(Primary Key)로 사용된다.
pk의 애트리뷰트가 2개이므로 pk(season, player_id)는 복합키이다.
방법부터 말하자면, @EmbeddedId 와 @IdClass 어노테이션을 활용하면 된다.
@EmbeddedId는 복합 키를 하나의 값 타입 객체로 간주하여 엔티티에 내장(embed)시키는 방식이다.
@Embeddable를 이용해 복합 키를 정의하는 별도의 클래스를 생성한다. 이 클래스는 Serializable 인터페이스를 구현해야 한다.
1️⃣ 키 클래스 정의
@Embeddable
public class SeasonPlayerId implements Serializable {
@Column(name = "season", length = 7, nullable = false)
private String season;
@Column(name = "player_id", nullable = false)
private Long playerId;
protected SeasonPlayerId() {} // JPA용 기본 생성자
public SeasonPlayerId(String season, Long playerId) {
this.season = season;
this.playerId = playerId;
}
public String getSeason() { return season; }
public Long getPlayerId() { return playerId; }
// equals/hashCode 반드시 구현해야 한다. (영속성 컨텍스트 키로 사용됨)
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SeasonPlayerId that)) return false;
return Objects.equals(season, that.season) &&
Objects.equals(playerId, that.playerId);
}
@Override public int hashCode() {
return Objects.hash(season, playerId);
}
}
2️⃣ 엔티티 정의
@Entity
@Table(name = "season_player_stat")
public class SeasonPlayerStat {
@EmbeddedId
private SeasonPlayerId id;
@Column(nullable = false)
private int goals;
@Column(nullable = false)
private int assists;
@Column(nullable = false)
private int minutes;
protected SeasonPlayerStat() {}
public SeasonPlayerStat(SeasonPlayerId id, int goals, int assists, int minutes) {
this.id = id;
this.goals = goals;
this.assists = assists;
this.minutes = minutes;
}
public SeasonPlayerId getId() { return id; }
public int getGoals() { return goals; }
public int getAssists() { return assists; }
public int getMinutes() { return minutes; }
public String season() { return id.getSeason(); }
public Long playerId() { return id.getPlayerId(); }
}
3️⃣ 리포지토리 및 활용 예시
public interface SeasonPlayerStatRepository extends JpaRepository<SeasonPlayerStat, SeasonPlayerId> {
...
}
// 저장
SeasonPlayerId key = new SeasonPlayerId("2024-25", 8L);
SeasonPlayerStat stat = new SeasonPlayerStat(key, 10, 15, 3200);
repo.save(stat);
// 조회
repo.findById(new SeasonPlayerId("2024-25", 8L))
.ifPresent(s -> System.out.println(s.getGoals()));
@IdClass는 복합 키를 기본키 클래스로 지정하고, 엔티티 클래스에서 해당 키 클래스의 필드들을 직접 매핑하는 방식이다. @EmbeddedId 방식과 마찬가지로 Serializable 인터페이스를 구현한 클래스를 정의한다.
1️⃣ 키 클래스 정의
public class SeasonPlayerId implements Serializable {
private String season;
private Long playerId;
public SeasonPlayerId() {}
public SeasonPlayerId(String season, Long playerId) {
this.season = season;
this.playerId = playerId;
}
// equals & hashCode
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof SeasonPlayerId that)) return false;
return Objects.equals(season, that.season) &&
Objects.equals(playerId, that.playerId);
}
@Override public int hashCode() {
return Objects.hash(season, playerId);
}
}
2️⃣ 엔티티 정의
@Entity
@Table(name = "season_player_stat")
@IdClass(SeasonPlayerId.class)
public class SeasonPlayerStat {
@Id
@Column(name = "season", length = 7, nullable = false)
private String season;
@Id
@Column(name = "player_id", nullable = false)
private Long playerId;
@Column(nullable = false)
private int goals;
@Column(nullable = false)
private int assists;
@Column(nullable = false)
private int minutes;
protected SeasonPlayerStat() {}
public SeasonPlayerStat(String season, Long playerId, int goals, int assists, int minutes) {
this.season = season;
this.playerId = playerId;
this.goals = goals;
this.assists = assists;
this.minutes = minutes;
}
public String getSeason() { return season; }
public Long getPlayerId() { return playerId; }
public int getGoals() { return goals; }
public int getAssists() { return assists; }
public int getMinutes() { return minutes; }
}
3️⃣ 리포지토리 및 활용 예시
public interface SeasonPlayerStatRepository extends JpaRepository<SeasonPlayerStat, SeasonPlayerId> {
...
}
// 저장
SeasonPlayerStat stat = new SeasonPlayerStat("2024-25", 10L, 7, 6, 2400);
repo.save(stat);
// 조회
repo.findById(new SeasonPlayerId("2024-25", 10L))
.ifPresent(s -> System.out.println(s.getAssists()));
JPA로 복합키를 다루는 방식은 @EmbeddedId와 @IdClass이 있으며 구현 방식에 차이가 있음을 위에서 보았다.
@EmbeddedId는 복합키를 하나의 값 타입 객체로 묶어 도메인 개념을 강화할 수 있다. @IdClass는 복합키 필드를 엔티티에서 직접 선언하므로 직관적이고 단순하다.
팀의 컨벤션에 맞게 적절한 방식을 선택하면 좋을 것 같다.