본문 바로가기
Spring/JPA

[JPA] 복합키(Composite Key) 엔티티

by J4J 2021. 3. 24.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 JPA 복합키(Composite Key) 엔티티에 대해 적어보는 시간을 가져보려고 합니다.

 

들어가기에 앞서 이전에 작성된 포스팅을 읽고 오시면 이해가 더 잘되실 겁니다.

 

2021.03.21 - [Spring/JPA] - [JPA] JPA 환경설정

2021.03.22 - [Spring/JPA] - [JPA] JPA Repository설정 및 CRUD

2021.03.23 - [Spring/JPA] - [JPA] 조건절(where) 메서드

 

 

 

앞서 포스팅을 읽어보신 분들이라면 기본키가 1개일 때는 문제없이 JPA를 활용할 수 있는 환경을 구성하셨을 겁니다.

 

그리고 본인만의 개성이 드러난 프로젝트를 구성하려고 하는데 기본키가 2개 이상인 경우 JpaRepository의 제네릭에 무엇을 넣어야 될지 고민하게 되며 막히신 분들이 있을 겁니다.

 

네... 제 얘기입니다...

 

그래서 복합키로 구성되어있는 테이블에서는 JPA를 어떻게 사용해야 되는지에 대해 설명드리겠습니다.

 

지금까지 주제는 학교로 했으나 테이블을 변경해야 되기 때문에 학생이라는 주제를 가지고 프로젝트를 구성해보겠습니다.

 

 

반응형

 

 

방법 1: @IdClass

 

[ 0. MySQL에 학생 테이블 생성 및 데이터 생성 ]

 

create table student (
    student_id varchar(30),
    name varchar(30),
    school_id int,
    score int,
    primary key(student_id, name)
);

insert into student values ('1234', '김고기', 3, 89);
insert into student values ('2345', '정덮밥', 1, 89);
insert into student values ('3456', '박찌개', 2, 89);
insert into student values ('4567', '문초밥', 3, 89);
insert into student values ('5678', '이족발', 4, 89);
insert into student values ('6789', '진짬뽕', 2, 89);

 

 

[ 1. 학생 테이블의 복합키를 담고 있는 식별자 클래스 StudentID 생성 ]

 

package com.spring.jpa.dto;

import lombok.NoArgsConstructor;

import java.io.Serializable;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentID implements Serializable {
	private String studentId;
	private String name;
}

 

 

@IdClass를 사용하기 위한 식별자 클래스를 생성할 때는 다음과 같은 조건이 만족해야 됩니다.

 

  • 식별자 클래스의 변수명과 엔티티에서 사용되는 변수명이 동일
  • 디폴트 생성자가 존재 (위의 코드는 Lombok의 @NoArgsConstructor어노테이션 추가로 자동 생성)
  • 식별자 클래스의 접근 지정자는 public
  • Serializable을 상속
  • equals, hashCode 구현 (위의 코드는 Lombok의 @Data어노테이션 추가로 자동 생성)

 

 

 

[ 2. 엔티티 클래스 Student 생성 ]

 

package com.spring.jpa.dto;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "student")
@IdClass(StudentID.class)
public class Student implements Serializable {
	@Id
	@Column(name = "student_id")
	private String studentId;
	
	@Id
	private String name;
	
	@Column(name = "school_id")
	private int schoolId;
	private int score;
}

 

 

엔티티 클래스에는 @IdClass라는 어노테이션을 이용하여 식별자 클래스를 매핑해줘야 됩니다.

 

그리고 위에서 말씀드린 것처럼 식별자 클래스의 변수명과 동일해야 합니다.

 

 

 

 

[ 3. Repository 생성 ]

 

package com.spring.jpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.spring.jpa.dto.Student;
import com.spring.jpa.dto.StudentID;

public interface StudentRepository extends JpaRepository<Student, StudentID> { // 제네릭 타입: <엔티티 클래스, 엔티티 클래스의 기본키>

}

 

 

상속받는 JpaRepository의 제네릭 타입에는 엔티티 클래스와 엔티티 클래스의 기본키가 들어가면 됩니다.

 

이전 포스팅에서도 그랬듯이 기본키가 하나일 때는 기본키의 타입을 넣어주면 되지만 복합키 같은 경우는 식별자 클래스를 기본키가 들어가야 되는 부분에 넣어주시면 됩니다.

 

 

[ 4. 단위 테스트 ]

 

정상적으로 실행되는지 확인하기 위해 단위 테스트 코드를 작성해보겠습니다.

 

package com.spring.jpa;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import com.spring.jpa.config.RootContext;
import com.spring.jpa.repository.StudentRepository;

import lombok.extern.slf4j.Slf4j;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class JPATest {
	
	@Autowired
	StudentRepository studentRepository;
	
	@Test
	public void studentTest() {
		log.info(studentRepository.findAll().toString());
	}
}

 

 

위와 같이 코드를 작성한 뒤 테스트를 진행해보면 다음과 같이 출력이 정상적으로 되는 것을 확인할 수 있습니다.

 

INFO : org.springframework.test.context.support.DefaultTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener]
INFO : org.springframework.test.context.support.DefaultTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@4d41cee, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@3712b94, org.springframework.test.context.support.DependencyInjectionTestExecutionListener@2833cc44, org.springframework.test.context.support.DirtiesContextTestExecutionListener@33f88ab, org.springframework.test.context.transaction.TransactionalTestExecutionListener@27a8c74e, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@2d8f65a4, org.springframework.test.context.event.EventPublishingTestExecutionListener@1b68ddbd]
INFO : org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
INFO : org.springframework.data.repository.config.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 104ms. Found 2 JPA repository interfaces.
INFO : org.hibernate.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: jpa-mysql]
INFO : org.hibernate.Version - HHH000412: Hibernate Core {5.4.10.Final}
INFO : org.hibernate.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
INFO : org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
INFO : org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
INFO : org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'jpa-mysql'
Hibernate: 
    /* select
        generatedAlias0 
    from
        Student as generatedAlias0 */ select
            student0_.name as name1_1_,
            student0_.student_id as student_2_1_,
            student0_.school_id as school_i3_1_,
            student0_.score as score4_1_ 
        from
            student student0_
INFO : com.spring.jpa.JPATest - [Student(studentId=1234, name=김고기, schoolId=3, score=89), Student(studentId=2345, name=정덮밥, schoolId=1, score=78), Student(studentId=3456, name=박찌개, schoolId=2, score=82), Student(studentId=4567, name=문초밥, schoolId=3, score=99), Student(studentId=5678, name=이족발, schoolId=4, score=73), Student(studentId=6789, name=진짬뽕, schoolId=2, score=85)]
INFO : org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'jpa-mysql'

 

 

 

 

방법 2: EmbeddedId

 

[ 1. 학생 테이블의 복합키를 담고 있는 식별자 클래스 StudentID 생성 ]

 

package com.spring.jpa.dto;

import lombok.NoArgsConstructor;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Embeddable;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class StudentID implements Serializable {
	@Column(name = "student_id")
	private String studentId;
	
	private String name;
}

 

 

@EmbeddedId를 사용하기 위한 식별자 클래스를 생성할 때는 다음과 같은 조건이 만족해야 됩니다.

 

  • 식별자 클래스에 @Embeddable어노테이션 추가
  • 디폴트 생성자가 존재 (위의 코드는 Lombok의 @NoArgsConstructor어노테이션 추가로 자동 생성)
  • 식별자 클래스의 접근 지정자는 public
  • Serializable을 상속
  • equals, hashCode 구현 (위의 코드는 Lombok의 @Data어노테이션 추가로 자동 생성)
  • 컬럼명과 변수명이 다를 경우 @Column어노테이션 사용

 

 

 

[ 2. 엔티티 클래스 Student 생성 ]

 

package com.spring.jpa.dto;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.Table;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "student")
public class Student implements Serializable {
	
	@EmbeddedId
	private StudentID stduentID;
	
	@Column(name = "school_id")
	private int schoolId;
	private int score;
}

 

 

엔티티 클래스에서 식별자 클래스를 매핑하기 위해 사용되는 것은 @EmbeddedId 어노테이션입니다.

 

위와 같이 식별자 클래스를 변수로 선언한 뒤 해당 변수에 어노테이션만 추가해주면 됩니다.

 

 

 

[ 3. Repository 생성 ]

 

package com.spring.jpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.spring.jpa.dto.Student;
import com.spring.jpa.dto.StudentID;

public interface StudentRepository extends JpaRepository<Student, StudentID> { // 제네릭 타입: <엔티티 클래스, 엔티티 클래스의 기본키>

}

 

 

Repository는 방법 1의 @IdClass사용과 동일합니다.

 

JpaRepository의 제네릭 타입에 엔티티 클래스와 식별자 클래스를 각각 넣어주면 됩니다.

 

 

 

[ 4. 단위 테스트 ]

 

package com.spring.jpa;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import com.spring.jpa.config.RootContext;
import com.spring.jpa.repository.StudentRepository;

import lombok.extern.slf4j.Slf4j;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RootContext.class)
@Slf4j
public class JPATest {
	
	@Autowired
	StudentRepository studentRepository;
	
	@Test
	public void studentTest() {
		log.info(studentRepository.findAll().toString());
	}
}

 

 

위와 같이 단위 테스트 코드를 작성하고 실행하면 아래와 같은 결과가 출력되는 것을 확인할 수 있습니다.

 

INFO : org.springframework.test.context.support.DefaultTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener]
INFO : org.springframework.test.context.support.DefaultTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@4d41cee, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@3712b94, org.springframework.test.context.support.DependencyInjectionTestExecutionListener@2833cc44, org.springframework.test.context.support.DirtiesContextTestExecutionListener@33f88ab, org.springframework.test.context.transaction.TransactionalTestExecutionListener@27a8c74e, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener@2d8f65a4, org.springframework.test.context.event.EventPublishingTestExecutionListener@1b68ddbd]
INFO : org.springframework.data.repository.config.RepositoryConfigurationDelegate - Bootstrapping Spring Data JPA repositories in DEFAULT mode.
INFO : org.springframework.data.repository.config.RepositoryConfigurationDelegate - Finished Spring Data repository scanning in 105ms. Found 2 JPA repository interfaces.
INFO : org.hibernate.jpa.internal.util.LogHelper - HHH000204: Processing PersistenceUnitInfo [name: jpa-mysql]
INFO : org.hibernate.Version - HHH000412: Hibernate Core {5.4.10.Final}
INFO : org.hibernate.annotations.common.Version - HCANN000001: Hibernate Commons Annotations {5.1.0.Final}
INFO : org.hibernate.dialect.Dialect - HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
INFO : org.hibernate.engine.transaction.jta.platform.internal.JtaPlatformInitiator - HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
INFO : org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean - Initialized JPA EntityManagerFactory for persistence unit 'jpa-mysql'
Hibernate: 
    /* select
        generatedAlias0 
    from
        Student as generatedAlias0 */ select
            student0_.name as name1_1_,
            student0_.student_id as student_2_1_,
            student0_.school_id as school_i3_1_,
            student0_.score as score4_1_ 
        from
            student student0_
INFO : com.spring.jpa.JPATest - [Student(stduentID=StudentID(studentId=1234, name=김고기), schoolId=3, score=89), Student(stduentID=StudentID(studentId=2345, name=정덮밥), schoolId=1, score=78), Student(stduentID=StudentID(studentId=3456, name=박찌개), schoolId=2, score=82), Student(stduentID=StudentID(studentId=4567, name=문초밥), schoolId=3, score=99), Student(stduentID=StudentID(studentId=5678, name=이족발), schoolId=4, score=73), Student(stduentID=StudentID(studentId=6789, name=진짬뽕), schoolId=2, score=85)]
INFO : org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean - Closing JPA EntityManagerFactory for persistence unit 'jpa-mysql'

 

 

 

 

@IdClass vs @EmbeddedId

 

두 방식의 차이점은 단순하게 "엔티티 클래스를 작성할 때 귀찮기 vs 변수를 사용할 때 귀찮기"라고 생각합니다.

 

@IdClass 같은 경우는 식별자 클래스에 있는 모든 변수들을 그대로 엔티티 클래스에 작성해야 되기 때문에 엔티티 클래스를 작성할 때 @EmbeddedId보다 다소 시간이 소모됩니다.

 

하지만 생성된 클래스들을 사용할 때 @IdClass는 단순하게 student.getName()이라고 하면 될 것을 @EmbeddedId는 student.getStudentID().getName()과 같이 코드가 길어지게 됩니다.

 

또한 JPQL쿼리를 작성할 때도 식별자 클래스까지 넣어야 되기 때문에 길이가 길어지게 됩니다.

 

결론적으로 취향대로 사용하시면 됩니다.

 

 

 

 

참조

 

JPA 고급매핑 (2)

 

 

 

 

이상으로 JPA 복합키(Composite Key) 엔티티에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

728x90
반응형

'Spring > JPA' 카테고리의 다른 글

[JPA] 연관관계 매핑 (양방향)  (0) 2021.03.28
[JPA] 연관관계 매핑 (단방향)  (0) 2021.03.26
[JPA] 조건절(where) 메서드  (0) 2021.03.23
[JPA] JPA Repository설정 및 CRUD  (0) 2021.03.22
[JPA] JPA 환경설정  (0) 2021.03.21

댓글