2025.03.27
Entity 클래스 간의 관계를 정의하는 것
DB에서는 연관관계를 한쪽에서만 관리
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.2">
<!-- persistence-unit 이름은 JPA 설정 시 참조되는 식별자 -->
<persistence-unit name="퍼시스턴스유닛이름">
<!-- 관리할 엔티티 클래스 등록 -->
<class>엔티티패키지경로.엔티티클래스1</class>
<class>엔티티패키지경로.엔티티클래스2</class>
<properties>
<!-- JDBC DB 연결 설정 -->
<property name="jakarta.persistence.jdbc.driver" value="Jcom.mysql.cj.jdbc.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/DB이름"/>
<property name="jakarta.persistence.jdbc.user" value="DB사용자ID"/>
<property name="jakarta.persistence.jdbc.password" value="DB비밀번호"/>
<!-- SQL 로그 출력 설정 -->
<!-- 실행 SQL 출력 -->
<property name="hibernate.show_sql" value="true"/>
<!-- SQL 보기 좋게 정렬 출력 -->
<property name="hibernate.format_sql" value="true"/>
<!-- DDL 생성 전략 설정 -->
<property name="hibernate.hbm2ddl.auto" value="none"/>
<!--
개발 환경에서 주로 사용하는 값
- create : 실행 시 기존 테이블 DROP 후 재생성
- create-drop : 실행 시 생성, 종료 시 DROP
- update : 변경된 부분만 반영 (보존된 데이터와 함께)
운영 환경에서 주로 사용하는 값
- validate : 테이블/컬럼 유효성 검사만 (DDL 생성 X)
- none : 아무 작업도 하지 않음 (기본값),
DB 테이블과 일치하게 작성
-->
</properties>
</persistence-unit>
</persistence>
@Entity(name = "상위엔티티논리이름")
<!-- 실제 DB 테이블 이름 -->
@Table(name = "상위테이블명")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class 상위엔티티클래스명 {
@Id
<!-- 기본키(PK) 컬럼 지정 -->
@Column(name = "기본키컬럼명")
private 기본키자료형 기본키필드;
<!-- 일반 정보 컬럼 -->
@Column(name = "일반컬럼명1")
private 자료형 필드1;
<!-- 외래키 컬럼 (단순 필드로만 작성된 경우) -->
<!-- 상위 엔티티는 외래키를 가질 필요 없이 그 자체로 참조 대상,
일반적으로 하위 엔티티에서 외래키를 소유하지만,
상위 엔티티 구조 이해를 돕기 위해 명시적으로 표현 -->
@Column(name = "외래키컬럼명")
private 자료형 외래키필드;
}
@Entity(name = "하위엔티티논리이름")
@Table(name = "하위테이블명")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class 하위엔티티클래스명 {
@Id
@Column(name = "기본키컬럼명")
private 기본키자료형 기본키필드;
@Column(name = "일반컬럼1")
private 자료형 일반필드1;
@Column(name = "일반컬럼2")
private 자료형 일반필드2;
<!-- CascadeType.PERSIST : 하위 엔티티를 persist()할 때,
연관된 상위 엔티티도 자동으로 persist() 되도록 하는 설정 -->
@ManyToOne(cascade = CascadeType.PERSIST)
<!-- 상위 테이블의 PK를 참조하는 외래키 -->
@JoinColumn(name = "외래키컬럼명")
private 상위엔티티클래스명 상위엔티티변수;
@Column(name = "일반컬럼3")
private 자료형 일반필드3;
}
JoinColumn 어노테이션에서 사용할 수 있는 속성
- 'name' : 참조하는 테이블의 컬럼명을 지정한다. <!-- 이 부분만 많이 쓰고 아래는 잘 쓰지 않는다. -->
- 'referencedColumnName' : 참조되는 테이블의 컬럼명을 지정한다.
- 'nullable' : 참조하는 테이블의 컬럼에 null 값을 허용할지 지정한다.
- 'unique' : 참조하는 테이블의 컬럼에 유일성 제약조건을 추가할지 지정한다.
- 'insertable' : 새로운 엔티티가 저장될 때, 이 참조 컬럼이 SQL INSERT에 포함될지 지정한다.
- 'updatable' : 엔티티가 업데이트될때, 이 참조 컬럼이 SQL UPDATE에 포함될지 지정한다.
- 'columnDefinition' : 이 참조 컬럼에 대한 SQL DDL을 직접 지정한다.
- 'table' : 참조하는 테이블의 이름을 지정한다.
- 'foreginKey' : 참조하는 테이블에 생성될 외래 키에 대한 추가 정보를 지정한다.
ManyToOne 어노테이션에서 사용할 수 있는 속성
<!-- cascade, fetch 중요 -->
- 'cascade' : 연관된 엔티티에 대한 영속성 전이를 설정한다.
트랜젝션 내에서 하위/상위 엔티티를 함께 처리할 때 필요
- 'fetch' : 연관된 엔티티를 로딩하는 전략 설정(EAGER : 즉시 로딩, LAZY : 지연 로딩)
DB에서 데이터를 가져오는 시점을 제어
@OneToMany(fetch = FetchType.LAZY)
@ManyToMany -- 지연로딩이 default
@ManyToOne(fetch = FetchType.EAGER)
@OneToOne -- 이른로딩이 default
- 'optional' : 연관된 엔티티가 필수인지 선택인지를 설정한다.
public class ManyToOneAssociationTests {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
@BeforeAll
public static void initFactory() {
<!-- persistence.xml의 persistence-unit 이름 사용 -->
entityManagerFactory = Persistence.createEntityManagerFactory("퍼시스턴스유닛이름");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
<!-- 여러 테스트 -->
}
@Test
@DisplayName("다대일 연관관계 객체 그래프 탐색을 이용한 조회 테스트")
public void 연관관계조회테스트() {
<!-- given : 조회할 엔티티의 기본키 값 -->
기본키자료형 기본키변수 = 기본키값;
<!-- when : 연관관계가 설정된 엔티티 조회 후, 연관된 상위 엔티티 접근 -->
하위엔티티클래스명 조회된하위엔티티변수 = entityManager.find(하위엔티티클래스명.class, 기본키변수);
상위엔티티클래스명 연관된상위엔티티변수 = 조회된하위엔티티변수.get상위엔티티변수();
<!-- then : 연관 엔티티가 정상적으로 조회되었는지 확인 -->
assertNotNull(연관된상위엔티티변수);
System.out.println("연관된상위엔티티변수 = " + 연관된상위엔티티변수);
}
@Test
@DisplayName("다대일 연관관계 객체지향쿼리(JPQL) 사용한 연관 엔티티 필드 조회 테스트")
public void 연관관계JPQL조회테스트() {
<!-- given : JPQL 작성 (엔티티 기준으로 작성) -->
String jpql변수 = "select 상위엔티티별칭.조회필드 from 하위엔티티명 하위엔티티별칭 " +
"join 하위엔티티별칭.상위엔티티필드명 상위엔티티별칭 " +
"where 하위엔티티별칭.기본키필드명 = :값";
<!-- when : 쿼리 실행 후 단일 결과 조회 -->
반환자료형 결과변수 = entityManager
.createQuery(jpql, 반환자료형.class)
.getSingleResult();
<!-- then : 결과 검증 -->
assertNotNull(결과변수);
System.out.println("조회된결과 = " + 결과변수);
}
@Test
@DisplayName("다대일 연관관계 엔티티 삽입 테스트")
public void manyToOneInsertTest() {
<!-- given : 하위 엔티티 및 상위 엔티티 객체 생성 및 설정 -->
하위엔티티클래스 하위엔티티변수 = new 하위엔티티클래스();
하위엔티티변수.set기본키(기본키값);
하위엔티티변수.set필드1("특정값1");
하위엔티티변수.set필드2(특정값2);
상위엔티티클래스 상위엔티티변수 = new 상위엔티티클래스();
상위엔티티변수.set기본키(상위기본키값);
상위엔티티변수.set필드1("특정값3");
상위엔티티변수.set필드2(참조코드값);
<!-- 연관관계 설정 -->
하위엔티티변수.set하위의상위연관관계필드(상위엔티티변수);
하위엔티티변수.set추가필드("추가값");
<!-- when : 트랜잭션 시작 → 영속화 → 커밋 -->
EntityTransaction transaction변수 = entityManager.getTransaction();
transaction변수.begin();
<!-- CascadeType.PERSIST 설정 시 상위도 함께 저장됨 -->
entityManager.persist(하위엔티티변수);
transaction변수.commit();
<!-- then : 저장된 객체 조회 및 검증 -->
하위엔티티클래스 조회결과변수 = entityManager.find(하위엔티티클래스.class, 기본키값);
assertEquals(기본키값, 조회결과변수.get기본키());
assertEquals(상위기본키값, 조회결과변수.get하위의상위연관관계필드().get기본키());
}
@Entity(name = "상위엔티티논리이름")
<!-- 실제 DB 테이블 이름 -->
@Table(name = "상위테이블명")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class 상위엔티티클래스명 {
@Id
@Column(name = "기본키_컬럼명")
private 기본키자료형 기본키필드;
@Column(name = "일반컬럼명1")
private 자료형 필드1;
@Column(name = "일반컬럼명2")
private 자료형 필드2;
@OneToMany(cascade = CascadeType.PERSIST, fetch = FetchType.LAZY)
@JoinColumn(name = "외래키컬럼명") // 하위 엔티티에서 외래키로 참조
private List<하위엔티티클래스명> 하위엔티티컬렉션변수;
}
@Entity(name = "하위엔티티논리이름")
@Table(name = "하위테이블명")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class 하위엔티티클래스명 {
@Id
@Column(name = "기본키_컬럼명")
private 기본키자료형 기본키필드;
@Column(name = "일반컬럼명1")
private 자료형 필드1;
@Column(name = "일반컬럼명2")
private 자료형 필드2;
<!-- 상위 엔티티의 PK를 참조하는 외래키 -->
@Column(name = "외래키컬럼명")
private 자료형 외래키필드;
@Column(name = "일반컬럼명3")
private 자료형 필드3;
}
public class OneToManyAssociationTests {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
@BeforeAll
public static void initFactory() {
<!-- persistence.xml의 persistence-unit 이름 사용 -->
entityManagerFactory = Persistence.createEntityManagerFactory("퍼시스턴스유닛이름");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
<!-- 여러 테스트 -->
}
@Test
@DisplayName("일대다 연관관계 객체 그래프 탐색 조회 테스트")
public void oneToManyLazyLoadTest() {
<!-- given : 조회할 상위 엔티티의 기본키 값 설정 -->
기본키자료형 상위기본키 = 특정값;
<!-- when : 상위 엔티티 조회
(연관된 하위 엔티티는 아직 조회되지 않음 - 지연로딩) -->
상위엔티티클래스 상위엔티티변수 = entityManager.find(상위엔티티클래스.class, 상위기본키);
<!-- then : 상위 엔티티가 null이 아님을 검증 -->
assertNotNull(상위엔티티변수);
<!-- 연관된 하위 엔티티 출력 -->
→ 이 시점에 하위 테이블을 조회하는 SQL 쿼리가 실행됨
System.out.println(상위엔티티변수);
}
@Test
@DisplayName("일대다 연관관계 객체 삽입 테스트")
public void oneToManyInsertTest() {
<!-- given : 상위 엔티티 및 하위 엔티티 리스트 생성 -->
상위엔티티클래스 상위엔티티변수 = new 상위엔티티클래스();
상위엔티티변수.set기본키(상위기본키값);
상위엔티티변수.set필드1("상위값1");
<!-- 참조값이 없는 경우 -->
상위엔티티변수.set필드2(null);
List<하위엔티티클래스> 하위엔티티리스트변수 = new ArrayList<>();
하위엔티티클래스 하위엔티티변수 = new 하위엔티티클래스();
하위엔티티변수.set기본키(하위기본키값);
하위엔티티변수.set필드1("하위값1");
하위엔티티변수.set필드2(하위값2);
하위엔티티변수.set필드3("하위값3");
<!-- 외래키 필드 수동 설정 -->
하위엔티티변수.set외래키필드(상위엔티티변수.get기본키());
<!-- 하위엔티티변수들을 리스트안에 넣는다 -->
하위엔티티리스트변수.add(하위엔티티변수);
상위엔티티변수.set하위엔티티리스트변수(하위엔티티리스트변수);
<!-- when : 트랜잭션 시작
→ 상위 엔티티만 영속화
(CascadeType.PERSIST 적용 시 하위도 자동 영속화) -->
EntityTransaction transaction변수 = entityManager.getTransaction();
transaction변수.begin();
entityManager.persist(상위엔티티변수);
transaction변수.commit();
<!-- then : 저장 확인 -->
상위엔티티클래스 조회결과변수 = entityManager.find(상위엔티티클래스.class, 상위기본키값);
System.out.println(조회결과변수);
}
양방향 연관 관계는 반대 방향으로도 접근하여 객체 그래프 탐색을 할 일이 많은 경우에만 사용
DB -> 외래키 하나로 양방향 조회 가능(JOIN으로 자동처리 가능)
객체 -> 서로 다른 두 단방향 참조를 합쳐서 양방향
주인 : 외래키 가진 엔티티 클래스(@JoinColumn), DB에 외래키 반영됨
비주인 : mappedBy로 주인을 참조만 함 (읽기 전용)
@Entity(name = "주인엔티티논리이름")
@Table(name = "주인테이블이름")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
<!-- 순환참조 방지(양방향일 때 필수) -->
@ToString(exclude = "비주인을참조하는필드명")
public class 주인엔티티클래스 {
@Id
@Column(name = "기본키컬럼명")
private 자료형 기본키필드명;
@Column(name = "일반컬럼명1")
private 자료형 필드명1;
@Column(name = "일반컬럼명2")
private 자료형 필드명2;
<!-- 연관관계 주인 쪽
외래키를 직접 소유하는 Many 쪽이 연관관계의 주인
JoinColumn을 통해 외래키 지정 -->
@ManyToOne
@JoinColumn(name = "외래키컬럼명") // 실제 DB 컬럼명
private 비주인엔티티클래스 비주인필드명;
@Column(name = "일반컬럼명3")
private 자료형 일반컬럼명3;
}
@Entity(name = "비주인엔티티논리이름")
@Table(name = "비주인테이블이름")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class 비주인엔티티클래스 {
@Id
@Column(name = "기본키컬럼명")
private 기본키자료형 기본키필드명;
@Column(name = "일반컬럼명1")
private 자료형 필드명1;
@Column(name = "일반컬럼명2")
private 자료형 필드명2;
<!-- mappedBy = "주인엔티티에서 비주인을 참조하고 있는 필드명"
연관관계의 주인이 아님 (읽기 전용)
실제 외래키는 주인 엔티티에 존재
주인이 아닌쪽은 mappedBy로 소유자 지정만 한다 -->
@OneToMany(mappedBy = "비주인을참조하는필드명")
private List<주인엔티티클래스> 주인엔티티리스트;
}
public class BiDirectionTests {
private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;
@BeforeAll
public static void initFactory() {
<!-- persistence.xml의 persistence-unit 이름 사용 -->
entityManagerFactory = Persistence.createEntityManagerFactory("퍼시스턴스유닛이름");
}
@BeforeEach
public void initManager() {
entityManager = entityManagerFactory.createEntityManager();
}
@AfterAll
public static void closeFactory() {
entityManagerFactory.close();
}
@AfterEach
public void closeManager() {
entityManager.close();
}
<!-- 여러 테스트 -->
}
@Test
@DisplayName("양방향 연관관계 매핑 조회 테스트")
public void bidirectionMappingTest() {
<!-- given : 조회할 주인/비주인 엔티티의 식별자 값 -->
기본키자료형 주인엔티티기본키 = 특정값;
기본키자료형 비주인엔티티기본키 = 특정값;
<!-- when -->
<!-- 주인 쪽(@ManyToOne)은 처음 조회 시 JOIN 쿼리로 연관 객체까지 함께 로딩됨 (즉시 로딩) -->
주인엔티티클래스 주인엔티티변수 = entityManager.find(주인엔티티클래스.class, 주인엔티티기본키);
<!-- 비주인 쪽(@OneToMany)은 처음에는 연관된 엔티티를 불러오지 않음 (지연 로딩) -->
비주인엔티티클래스 비주인엔티티변수 = entityManager.find(비주인엔티티클래스.class, 비주인엔티티기본키);
<!-- then -->
<!-- 양방향 연관관계에서 toString()은 순환참조 StackOverflow 위험 → exclude 설정 권장 -->
System.out.println(주인엔티티변수);
System.out.println(비주인엔티티변수);
<!-- 필요 시 연관 엔티티를 사용하는 순간, 지연로딩 쿼리 발생 -->
비주인엔티티변수.get주인엔티티리스트().forEach(System.out::println);
}
양방향 연관관계 toString() 오버라이딩 / lombok 사용 시 주의점 정리
@OneToMany 연관관계에서 지연로딩 쿼리 발생 이유
@Test
@DisplayName("주인 엔티티를 통한 연관 객체 삽입 테스트")
public void 양방향연관관계_주인엔티티삽입_테스트() {
<!-- given : 주인 엔티티(하위 엔티티) 생성 및 연관된 비주인 엔티티(상위 엔티티) 설정 -->
주인엔티티클래스 주인엔티티변수 = new 주인엔티티클래스();
주인엔티티변수.set기본키(주인기본키값);
주인엔티티변수.set필드명1("특정값1");
주인엔티티변수.set필드명2(특정값2);
<!-- 연관관계 주입 (연관된 비주인 엔티티는 DB에서 조회해오는 방식) -->
주인엔티티변수.set비주인엔티티변수(
entityManager.find(비주인엔티티클래스.class, 비주인기본키값)
);
<!-- when : 트랜잭션 시작 및 주인 엔티티 영속화 -->
EntityTransaction transaction변수 = entityManager.getTransaction();
transaction변수.begin();
entityManager.persist(주인엔티티변수);
transaction변수.commit();
<!-- then : 저장된 엔티티 조회 및 검증 -->
주인엔티티클래스 조회결과변수 = entityManager.find(주인엔티티클래스.class, 주인엔티티.get기본키());
assertEquals(주인엔티티변수.get기본키(), 조회결과변수.get기본키());
System.out.println(조회결과변수);
}
@Test
@DisplayName("양방향 연관관계 비주인 엔티티 삽입 테스트")
public void insertWithNonOwningEntity() {
<!-- given : 비주인 엔티티 인스턴스 생성 및 설정 -->
비주인엔티티클래스 비주인엔티티변수 = new 비주인엔티티클래스();
비주인엔티티변수.set기본키(기본키값);
비주인엔티티변수.set필드1("특정값1");
비주인엔티티변수.set필드2(null); // 상위 참조값이 없을 경우
<!-- when : 트랜잭션 시작 → 비주인 엔티티만 영속화 -->
EntityTransaction transaction변수 = entityManager.getTransaction();
transaction변수.begin();
entityManager.persist(비주인엔티티변수);
transaction변수.commit();
<!-- then : 저장된 비주인 엔티티를 다시 조회하여 검증 -->
비주인엔티티클래스 조회결과변수 = entityManager.find(비주인엔티티클래스.class, 기본키값);
assertEquals(비주인엔티티변수.get기본키(), 조회결과변수.get기본키());
System.out.println(조회결과변수);
}
기본 원칙 OneToMany vs ManyToOne