Search
Duplicate

[Spring JPA] 연관관계 맵핑 기초

날짜
2022/04/29 05:17
상태
스프링 JPA 기본
속성 1
Spring
JAVA
JPA
담당자

연관관계 맵핑

목표
객체와 테이블 연관관계의 차이를 이해
객체의 참조와 테이블의 외래 키를 매핑
용어
방향(Direction): 단방향, 양방향
다중성 : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
연관관계의 주인(Onwer): 객체 양방향 연관관계는 관리 주인이 필요

연관관계가 필요한 이유

객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.

예제 시나리오

회원과 팀이 있다.
회원은 하나의 팀에만 소속될 수 있다.
회원과 팀은 다대일 관계다.

단방향 연관관계

1) 객체를 테이블에 맞추어 모델링

2) 객체를 테이블에 맞추어 모델링

package helloJpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import java.util.List; public class JpaMain { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");// 애플리케이션 에서 한개만 만들어 져야된다. EntityManager em = emf.createEntityManager();//하나의 단위를 만들때마다 만들어 줘야된다. EntityTransaction tx = em.getTransaction(); tx.begin(); try { Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); member.setTeamId(team.getId()); em.persist(member); Member findMember = em.find(Member.class, member.getId()); Long findTeamId = findMember.getTeamId(); Team findTeam = em.find(Team.class, findTeamId); tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); } emf.close(); } }
Java
복사
문제 발생
객체를 테이블에 맞춰 모델링을 하다 보니 객체지향적이지 않고 뭔가 이상한 것을 느낄 수 있을 것이다.
바로 TeamId 를 넣었을 때 팀을 세팅하고 값을 가져오는 부분이다.
객체지향이라면 분명 member 안에 Team이 있을 것이다. member.setTeam(team);
조회 시 문제 발생
조회할 때도 멤버의 id값으로 팀의 ID 값을 조회한 후 조회한 ID로 팀이름을 찾아야 된다.
조회할 때마다 DB에서 계속 꺼내는 일이 발생한다. 이는 연관관계가 없기 때문이다.
전혀 객체지향스럽지 못하다.
결론
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조를 사용해서 연관된 객체를 찾는다.
테이블과 객체 사이에는 이런 큰 간격이 있다.

3) 객체지향 모델링

위 멤버 객체를 Meber에게는 Many, Team에게는 One으로 일대다 관계를 맺어주고
조인은 TEAM_ID 칼럼으로 한다고 명시해 줌으로써 연관관계를 매핑해줬다.

3) 객체지향 모델링

package helloJpa; import javax.persistence.*; import java.util.Date; @Entity public class Member { ... // @Column(name = "TEAM_NO")// private Long teamId; @ManyToOne// JPA에게 일대다 다대일 관계인지 알려줘야된다. @JoinColumn(name = "TEAM_ID")// 어떤 컬럼명으로 조인을 할지 알려줘야한다. private Team team; ... }
Kotlin
복사
package helloJpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import java.util.List; public class JpaMain { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");// 애플리케이션 에서 한개만 만들어 져야된다.EntityManager em = emf.createEntityManager();//하나의 단위를 만들때마다 만들어 줘야된다.EntityTransaction tx = em.getTransaction(); tx.begin(); try { // 저장Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); member.setTeam(team); em.persist(member); Member findMember = em.find(Member.class, member.getId()); Team findTeam = findMember.getTeam(); System.out.println("findTeamName = " + findTeam.getName()); tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); } emf.close(); } }
Java
복사

양방향 연관관계와 연관관계의 주인

테이블 연관 관계는 단반향 예제 외 차이가 없다. 테이블은 외래 키로 조인을 하기 때문에
하지만 객체는 멤버에서 팀에게 가는 단방향은 있었지만 팀에서 멤버로 가는 방향은 없었다.
가는 방법을 만들기 위해 팀에 List Members를 추가했다.

연관관계의 주인과 mappedBy

@Entity public class Team { @Id @GeneratedValue @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>(); ... }
Java
복사
package helloJpa; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.Persistence; import java.util.List; public class JpaMain { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");// 애플리케이션 에서 한개만 만들어 져야된다. EntityManager em = emf.createEntityManager();//하나의 단위를 만들때마다 만들어 줘야된다. EntityTransaction tx = em.getTransaction(); tx.begin(); try { // 저장 ... Member findMember = em.find(Member.class, member.getId()); List<Member> members = findMember.getTeam().getMembers(); for (Member member1 : members) { System.out.println(member1.getName()); } tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); } emf.close(); } }
Java
복사
mappedBy = JPA의 멘탈붕괴 난이도
mappedBy는 처음에는 이해하기 어렵다.
객체와 테이블 간에 연관관계를 맺는 차이를 이해해야 한다.

객체와 테이블이 관계를 맺는 차이

객체 연관관계 = 2개
회원 -> 팀 연관관계 1개 (단방향)
팀 -> 회원 연관관계 1개 (단방향)
테이블 연관관계 = 1개
회원 <-> 팀의 연관관계 1개(양방향)
테이블은 외래 키로 연관관계가 끝이 나지만, 객체는 연관관계를 2개를 잡아줘야 되기 때문에 두 개의 차이가 있다.

객체의 양방향 관계

객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
A->B(a.getB())
B->A(b.getA())

테이블의 양방향 연관관계

테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐(양쪽으로 조인할 수 있다.)
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
SQL
복사

둘 중 하나로 외래 키를 관리해야 한다.

Member의 Team 값을 변경해였을 때 Member의 외래 키를 변경해야 될지,
Team의 Members 값을 변경했을 때 Member의 외래 키를 변경해야 될 지, 딜레마가 오게 된다.
하지만 DB기 준으로는 외래 키 값만 변경하면 된다.
그래서 룰이 생긴다. 둘 중에 하나를 주인으로 둬야 된다.

연관관계의 주인(Owner)

양방향 매핑 규칙
객체의 두 관계 중 하나를 연관관계의 주인으로 지정
연관관계의 주인만이 외래 키를 관리(등록, 수정)
주인이 아닌 쪽은 읽기만 가능
주인은 mappedBy 속성 사용 X
주인이 아니면 mappedBy 속성으로 주인 지정

누구를 주인으로?

외래 키가 있는 곳을 주인으로 정해라
여기서 Member.team이 연관관계의 주인

양방향 매핑 시 가장 많이 하는 실수

(연관관계의 주인에 값을 입력하지 않음)
Member member = new Member(); member.setName("member1"); em.persist(member); Team team = new Team(); team.setName("TeamA"); //역방향(주인이 아닌 방향)만 연관관계 설정정 team.getMembers().add(member); em.persist(team);
Java
복사
결론은 연관관계 주인을 통해서 넣어야 된다.
제일 좋은 것은 객체지향처럼 둘 다 넣는 것이 베스트이다.

양방향 연관관계 주의 - 실습

순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.
연관관계 편의 메서드를 생성하자
양방향 매핑 시 무한 루프를 조심하자
예: toString(), lombok, Json 생성 라이브러리
package helloJpa; import javax.persistence.*; import java.util.Date; @Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String name; // @Column(name = "TEAM_NO") // private Long teamId; // JPA에게 일대다 다대일 관계인지 알려줘야된다. @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team; public void setTeam(Team team) { this.team = team; team.getMembers().add(this); } ... }
Java
복사

양방향 매핑 정리

단방향 매핑만으로도 이미 연관관계 매핑은 완료
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것뿐
JPQL에서 역방향으로 탐색할 일이 많음
단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 됨
결론
테이블을 어떻게 연관관계로 맵핑을 하는지 참 궁금었다. 하지만 정말 간단하게 해결하였고, 하지만 사용하는 개념에 대해서는 정확하게 알고 사용해야 되겠다는 생각을 하였다. 잘못 사용하게 되면 정말 헤어 나올 수 없는 오류를 범할 수 도 있다는 생각이 든다.
이 글은 인프런의
제목 : 자바 ORM 표준 JPA 프로그래밍 - 기본 편
강사 : 김영한 님의 동영상을 참조해 만들었습니다.