개요
기존 포스팅 최하단에 비고로 테이블 중심 설계가 아닌 객체 중심으로 설계할 경우 발생할 수 있는 문제에 대해 살짝 언급했었습니다. (https://jaimemin.tistory.com/1899)
이번 게시글에서는 연관관계 매핑에 대해 간단하게 알아보고 왜 테이블 중심으로 설계해야 하는지 이해해보겠습니다.
1. 객체 중심으로 모델링할 경우 문제점
- 객체를 테이블에 맞추어 모델링할 경우 참조 대신 외래 키 즉, Foreign Key를 그대로 사용
- 이럴 경우 외래 키 식별자를 직접 다루는 것이 문제
- 식별자를 직접 다루기 때문에 식별자로 다시 조회하는 상황이 발생하는데, 이런 방식은 객체지향적이지 않음
- 정리를 하자면, 객체를 테이블에 맞추어 데이터 중심으로 모델링할 경우 협력 관계를 만들어낼 수 없음
- 테이블은 Foreign Key로 Join을 통해 연관된 테이블을 찾아내고 객체는 참조를 통해 연관된 객체를 찾기 때문에 이러한 문제점이 발생
예제: 직원을 나타내는 클래스가 있고 해당 직원은 어떤 기업의 소속
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Builder | |
public class Employee { | |
@Id | |
@GeneratedValue | |
@Column(name = "EMPLOYEE_ID") | |
private Long id; | |
@Column | |
private String name; | |
@Column(name = "COMPANY_ID") | |
private Long companyId; // Company를 참조하는 것이 아닌 외래 키를 그대로 사용 | |
} | |
@Entity | |
@Builder | |
public class Company { | |
@Id | |
@GeneratedValue | |
@Column(name = "COMPANY_ID") | |
private Long id; | |
private String name; | |
} |
기업과 직원을 등록하고 다시 조회하는 코드 (연관관계 X)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("example"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Company company = Company.builder() | |
.name("exampleCompany") | |
.build(); | |
entityManager.persist(company); | |
Employee employee = Employee.builder() | |
.name("employee1") | |
.companyId(company.getId()) | |
.build(); | |
entityManager.persist(employee); | |
// 조회 | |
Employee foundEmployee = entityManager.find(Employee.class, employee.getId()); | |
// 연관관계 X | |
Company foundCompany = entityManager.find(Company.class, foundEmployee.getCompanyId()); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} | |
} |
2. 단방향 연관관계 (객체 연관관계 사용)
- 기존 예제에서 사용한 직원, 회사 엔티티에 연관관계를 입혀보면
- 회사에는 많은 직원들이 있고 대부분의 회사들은 겸업을 금지하므로 직원 : 회사 = N : 1
- 기존에는 Foreign Key를 그대로 사용하는 것이 문제였으므로 이번에는 객체의 참조를 이용하고 이에 맞는 JPA 어노테이션을 적용 (@ManyToOne, @JoinColumn)
- 연관관계를 적용함에 따라 직원 객체로부터 바로 기업을 찾아낼 수 있는 것을 확인할 수 있음
예제: 직원을 나타내는 클래스가 있고 해당 직원은 어떤 기업의 소속
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Builder | |
public class Employee { | |
@Id | |
@GeneratedValue | |
@Column(name = "EMPLOYEE_ID") | |
private Long id; | |
@Column | |
private String name; | |
@ManyToOne // 직원 : 기업 = N : 1 | |
@JoinColumn(name = "COMPANY_ID") // Foreign Key | |
private Company company; | |
} | |
@Entity | |
@Builder | |
public class Company { | |
@Id | |
@GeneratedValue | |
@Column(name = "COMPANY_ID") | |
private Long id; | |
private String name; | |
} |
기업과 직원을 등록하고 다시 조회하는 코드 (연관관계 O)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("example"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Company company = Company.builder() | |
.name("exampleCompany") | |
.build(); | |
entityManager.persist(company); | |
Employee employee = Employee.builder() | |
.name("employee1") | |
.company(company) | |
.build(); | |
entityManager.persist(employee); | |
// 조회 | |
Employee foundEmployee = entityManager.find(Employee.class, employee.getId()); | |
// 연관관계 | |
Company foundCompany = foundEmployee.getCompany(); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} | |
} |
3. 양방향 연관관계
- 단방향 연관관계 예제에서는 기업 엔티티를 통해 소속된 직원들을 찾을 수 없었지만
- 양방향 연관관계를 적용할 경우 직원 엔티티를 통해 기업을 찾을 수도 있고 적절한 JPA 어노테이션을 통해 기업 엔티티를 통해 소속된 직원들을 찾을 수 있음 (@OneToMany, )
예제: 직원을 나타내는 클래스가 있고 해당 직원은 어떤 기업의 소속
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Entity | |
@Builder | |
public class Employee { | |
@Id | |
@GeneratedValue | |
@Column(name = "EMPLOYEE_ID") | |
private Long id; | |
@Column | |
private String name; | |
@ManyToOne // 직원 : 기업 = | |
@JoinColumn(name = "COMPANY_ID") // Foreign Key | |
private Company company; | |
} | |
@Entity | |
@Builder | |
public class Company { | |
@Id | |
@GeneratedValue | |
@Column(name = "COMPANY_ID") | |
private Long id; | |
private String name; | |
@OneToMany(mappedBy = "company") | |
List<Employee> employees = new ArrayList<>(); // NPE 방지를 위해 미리 생성 | |
} |
기업과 직원을 등록하고 다시 조회하는 코드 (연관관계 O)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("example"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Company company = Company.builder() | |
.name("exampleCompany") | |
.build(); | |
entityManager.persist(company); | |
Employee employee = Employee.builder() | |
.name("employee1") | |
.company(company) | |
.build(); | |
entityManager.persist(employee); | |
// 조회 | |
Employee foundEmployee = entityManager.find(Employee.class, employee.getId()); | |
// 연관관계 | |
Company foundCompany = foundEmployee.getCompany(); | |
// 반대방향 조회 | |
List<Employee> employees = foundCompany.getEmployees(); | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} | |
} |
3.1 양방향 연관관계 = 단방향 연관관계 * 2
- 객체와 테이블 간에 연관관계를 맺는 차이를 이해하는 것이 핵심
- 단방향 연관관계의 경우 연관관계가 2개 (직원 > 기업, 기업 > 직원)
- 양방향 연관관계의 경우 논리적으로는 연관관계가 1개 (직원 <-> 기업)
- 하지만, 사실 양방향 관계는 서로 다른 단방향 관계가 2개인 상태
- 따라서 객체를 양방향으로 참조하기 위해서는 단방향 연관관계를 2개 만들어야 함
3.2 테이블의 양방향 연관관계
- 테이블은 외래 키, Foreign Key 하나를 통해 두 테이블의 연관관계를 맺음
- 앞선 예제에서는 COMPANY 테이블의 COMPANY_ID 외래 키 하나를 통해 양방향 연관관계를 가짐
SELECT *
FROM EMPLOYEE
INNER JOIN COMPANY
ON EMPLOYEE.COMPANY_ID = COMPANY.COMPANY_ID
3.3 양방향 연관관계의 주인과 mappedBy 속성
- 앞선 예제에서 외래 키인 COMPANY_ID를 통해 양방향 연관관계를 가지므로 DB 입장에서는 Employee 내 Foreign Key만 관리해주면 됨
- Employee 내 외래 키를 바꾸면 다른 기업을 참조
- DB 입장에서는 양방향 연관관계 중 하나의 객체를 주인으로 지정하여 외래 키를 등록 및 수정하게 하는 것이 관리 측면에서 효율적
- 주인이 아닌 쪽은 등록 및 수정이 불가하고 오로지 읽기만 가능
- 읽기만 가능한 객체는 mappedBy 속성을 사용하고 등록 및 수정이 가능한 주인은 mappedBy 속성을 사용하지 않음
- 정리를 하자면, 외래 키가 있는 객체 즉, Many 쪽을 무조건 연관관계의 주인으로 정하자
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택 X
- 위 에제에서는 직원이 Many이므로 직원인 Employee 객체가 양방향 연관관계의 주인
- 이론상 연관관계의 주인 쪽에만 연관관계를 맺은 객체를 넣어주면 되지만 순수한 객체 관계를 고려했을 때는 양쪽 객체에 모두 값을 입력해주는 것이 맞음 (테스트 케이스 작성할 때 훨씬 수월함)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public static void main(String[] args) { | |
EntityManagerFactory entityManagerFactory | |
= Persistence.createEntityManagerFactory("example"); | |
EntityManager entityManager = entityManagerFactory.createEntityManager(); | |
EntityTransaction transaction = entityManager.getTransaction(); | |
transaction.begin(); | |
try { | |
Company company = Company.builder() | |
.name("exampleCompany") | |
.build(); | |
entityManager.persist(company); | |
Employee employee = Employee.builder() | |
.name("employee1") | |
.build(); | |
// 연관관계 주인이 아닌 쪽에도 객체 설정 | |
company.getEmployees().add(employee); | |
// 연관관계 주인에 객체 설정 | |
employee.setCompany(company); | |
entityManager.persist(employee); | |
// ... | |
transaction.commit(); | |
} catch (Exception e) { | |
transaction.rollback(); | |
} finally { | |
entityManager.close(); | |
} | |
entityManagerFactory.close(); | |
} | |
} |
* 양방향 매핑 시 toString(), lombok, JSON 생성 라이브러리를 사용할 경우 무한 루프가 발생할 수 있으므로 주의
비고
- 앞서 JSON 생성 라이브러리를 사용할 경우 양방향 관계에서 무한 루프가 발생할 수 있다고 언급했지만 Controller에서 Entity를 직접 반환하지 않을 경우 문제 될 게 없음
- Controller에서 Entity를 직접 반환할 경우 무한루프 위험뿐만 아니라 필드 추가/삭제 시 API 스펙이 바뀔 수 있으므로 절대 Entity를 그대로 반환 X
- 추천하는 방법은 별도 DTO를 생성해서 Entity를 API 스펙에 맞는 DTO로 변환 후 Controller에서 반환하는 방법
- 양방향 매핑에 대해 알아봤지만 사실 단방향 매핑으로도 이미 연관관계는 완성된 상태
- 우선, 단방향 매핑을 기준으로 DB를 설계한 이후
- 단순 조회 기능을 목적으로 필요시 양방향 연관관계를 맺는 것을 추천
- 현업에서는 JPQL 쿼리가 너무 복잡해질 경우 양방향 연관관계를 맺는 경우가 많음
출처
자바 ORM 표준 JPA 프로그래밍 - 기본편 (김영한 강사님)
반응형
'DB > JPA' 카테고리의 다른 글
[JPA] 상속관계 매핑 (2) | 2021.09.07 |
---|---|
[JPA] 다양한 연관관계 매핑 (0) | 2021.08.31 |
[JPA] Entity 매핑 정리 (0) | 2021.08.23 |
[JPA] PersistenceContext 간단 정리 (0) | 2021.08.20 |
JPA를 학습해야하는 이유 (1) | 2021.08.16 |