복합 키 (@EmbeddedId vs @IdClass)

co-deok·2025년 8월 13일
post-thumbnail

복합키란?

복합키란 데이터베이스 테이블의 행을 고유하게 식별하기 위해 두 개 이상의 컬럼을 조합해서 만든 기본키를 말한다.
즉 우리가 일반적으로 말하는 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)
);

나의 경우, 맨체스터 유나이티드라는 축구 팀을 좋아하므로 이에 비유해보자면

  • 2024-25 시즌 / Bruno Fernandes(id는 8) / 10골 / 25어시스트 / 3555분
  • 2023-24 시즌 / Bruno Fernandes(id는 8) / 7골 / 12어시스트 / 2755분

위와 같은 튜플이 season_player_stat 테이블에 존재할 수 있을 것이다.
이때 이 테이블에서 하나의 행을 유일하게 식별하기 위해 seasonplayer_id가 pk(Primary Key)로 사용된다.
pk의 애트리뷰트가 2개이므로 pk(season, player_id)복합키이다.

그렇다면 JPA에서 복합키 매핑은 어떻게 구현할까?

방법부터 말하자면, @EmbeddedId @IdClass 어노테이션을 활용하면 된다.


@EmbeddedId

@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

@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는 복합키 필드를 엔티티에서 직접 선언하므로 직관적이고 단순하다.
팀의 컨벤션에 맞게 적절한 방식을 선택하면 좋을 것 같다.

profile
😎 Backend Developer with Spring Boot & Django

0개의 댓글