- [JPA] 6. 연관관계 매핑 주의점2025년 02월 28일 23시 00분 31초에 업로드 된 글입니다.작성자: nickhealthy
이전 포스팅에서 연관관계 매핑에 대해서 자세히 알아보았다. 이번에는 실전 예제를 통해 직접 어떻게 동작하는지 확인해보자.
❌ 잘못된 방법) - 연관관계의 주인이 아닌 가짜 매핑에 데이터를 입력한 경우
[MEMBER2 테이블]
package hellojpa.mapping; import jakarta.persistence.*; @Entity(name = "MEMBER2") public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String username; @ManyToOne // Member와 Team은 N:1 관계 @JoinColumn(name = "TEAM_ID") // Team 엔티티의 TEAM_ID 컬럼과 외래키 관계 private Team team; ... }
[TEAM 테이블]
package hellojpa.mapping; import jakarta.persistence.*; import java.util.ArrayList; import java.util.List; @Entity public class Team { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "TEAM_ID") private Long id; private String name; @OneToMany(mappedBy = "team") // 양방향 연관관계 설정, mappedBy 속성으로 주인을 지정 private List<Member> members = new ArrayList<>(); ... }
[JPA 실행]
package hellojpa.mapping; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.Persistence; /** * JPA 양방향 연관관계와 연관관계의 주인 - 주의점, 정리 */ public class JpaMain2 { public static void main(String[] args) { EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello"); EntityManager em = emf.createEntityManager(); EntityTransaction tx = em.getTransaction(); tx.begin(); try { Member member = new Member(); member.setUsername("MEMBER1"); em.persist(member); Team team = new Team(); team.setName("TeamA"); team.getMembers().add(member); // mappedBy로 설정된 가짜매핑에 데이터를 세팅 em.persist(team); em.flush(); em.clear(); tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); emf.close(); } } }
[실행결과]
연관관계의 주인이 아닌, 연관관계와 매핑된 가짜매핑에(TEAM 클래스의 members 필드) 값을 세팅 후 `em.persist()` 메서드를 호출하였다. INSERT 쿼리는 두번 나갔지만 DB를 확인해보면 값이 저장하지 않은 것을 확인할 수 있다.
JPA 실행 결과 [DB 데이터 확인]
TEAM 테이블에는 데이터가 들어가 있는 것을 확인할 수 있는 반면, MEMBER2 테이블에 TEAM_ID가 세팅되지 않은 것을 확인할 수 있다. 외래키 설정으로 두 테이블에 동일한 데이터가 매핑되어야 하지만, 데이터 정합성에 문제가 발생하였다.
DB 조회 결과 이는 가짜 매핑, 즉, TEAM 컬럼의 mappedBy로 설정된 테이블에 값을 세팅하고, JPA를 수행한 결과이다.
가짜 매핑은 데이터를 UPDATE 하거나 INSERT 하더라도 JPA에서 이를 무시한다. 가짜 매핑은 데이터를 불러와서 읽기 전용으로만 가능하다.
✅ 옳은 방법) - 양방향 매핑 시 연관관계의 주인에 값을 세팅한 경우
package hellojpa.mapping; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.Persistence; /** * JPA 양방향 연관관계와 연관관계의 주인 - 주의점, 정리 */ public class JpaMain2 { 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.setUsername("MEMBER1"); member.setTeam(team); // 연관관계의 주인에 값을 세팅하는 경우 em.persist(member); em.flush(); em.clear(); tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); emf.close(); } } }
[실행결과]
DB 조회 결과 이번에는 데이터가 정상적으로 들어간 것을 확인할 수 있다. 이렇게 연관관계 주인에 값을 세팅해주어야 JPA는 실제로 그 값을 데이터베이스에 정상적으로 반영해준다.
하지만 양방향 매핑 관계에서 양쪽 모두 값을 세팅해주어야 한다.
방금까지 위에서 연관관계의 주인에 값을 세팅해야 한다면서 무슨 소리인가 싶을 수 있다. 그렇지만 아래와 같은 이유들로 양쪽에서 값을 세팅해주는 것이 옳은 방법이다. 이유는 다음과 같이 크게 두 가지로 나뉜다.
- 코드가 객체지향적이지 않으며, 논리도 맞지 않다. 더 큰 문제는 JPA 값을 조회할 때 문제가 데이터가 조회되지 않는 발생한다.(아래 예제 참고)
- 테스트 케이스 작성 시 JPA를 활용하지 않고, 순수한 자바 코드로 작성할 때가 있다. 이럴 땐 MEMBER 객체에서는 조회되지만, TEAM 객체에서는 조회되지 않아 데이터의 정합성이 깨질 수 있다.
예제를 살펴보면서 자세히 알아보자.
❌ 잘못된 방법) - 연관관계 주인에만 설정했을 경우
package hellojpa.mapping; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.Persistence; import java.util.List; /** * JPA 양방향 연관관계와 연관관계의 주인 - 주의점, 정리 */ public class JpaMain2 { 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.setUsername("MEMBER1"); member.setTeam(team); // 연관관계의 주인에 값을 세팅하는 경우 em.persist(member); // 문제가 되는 코드 em.flush(); em.clear(); // 문제가 되는 코드 Team findTeam = em.find(Team.class, team.getId()); List<Member> members = findTeam.getMembers(); System.out.println("============"); for (Member m : members) { // 문제가 되는 코드 System.out.println("member = " + m.getUsername()); } System.out.println("============"); tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); emf.close(); } } }
- members List에 값을 세팅하지 않았는데, List iteration 루프를 돌며 값이 조회된다. 우선 코드가 객체지향적이지도 않고 논리에도 맞지 않다.
- `em.flush()`, `em.clear()` 메서드를 위에서 사용하지 않았다면 값이 조회되지 않는다.
- 왜냐하면 해당 메서드로 데이터베이스에 값을 저장하고, 영속성 컨텍스트를 초기화 시키는 것이기 때문에 `em.find()` 메서드를 통해서 데이터베이스에서 값을 조회해 오기 때문에 값 조회가 가능한 것이다.
- 만약 해당 메서드를 제외하고 코드를 실행하면 값이 단순히 영속성 컨텍스트에서만 저장되어 있기 때문에 List에서 값을 조회할 수 없다. 위의 코드 iterator에서 Member를 루프로 돌면서 값을 조회할 때는 JPA와 관련없이 순수 객체 코드라고 생각하면 이해하기 편하다. 당연히 값을 조회할 수 없는 상태인 것이 맞다. (List에 값을 세팅한 것이 없으므로)
✅ 옳은 방법) - 양방향 매핑 관계에서 관련된 값을 모두 세팅
package hellojpa.mapping; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.Persistence; import java.util.List; /** * JPA 양방향 연관관계와 연관관계의 주인 2 - 주의점, 정리 */ public class JpaMain2 { 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.setUsername("MEMBER1"); member.setTeam(team); // 연관관계의 주인에 값을 세팅하는 경우 em.persist(member); team.getMembers().add(member); // 가짜매핑에도 값을 세팅 Team findTeam = em.find(Team.class, team.getId()); List<Member> members = findTeam.getMembers(); for (Member m : members) { System.out.println("m.getUsername() = " + m.getUsername()); } tx.commit(); } catch (Exception e) { tx.rollback(); } finally { em.close(); emf.close(); } } }
가짜 매핑(`team.getMembers().add(member);`)에도 값을 세팅해주고, `em.flush()`, `em.clear()` 메서드를 삭제했음에도 값이 정상적으로 조회되는 것을 확인할 수 있다. 그리고 당연히 코드 자체에도 논리적이며 객체지향적인 코드가 되었다.
✅ 옳은 방법(추천) - 연관관계의 주인에 편의 메서드 만들기
하지만 사람이기 때문에 값 세팅을 까먹을 수도 있는 즉, 휴먼에러가 발생할 가능성이 높다.
이를 방지하기 위해 편의 메서드를 연관관계의 주인을 가진 엔티티에 만드는 것이 좋다.package hellojpa.mapping; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.Persistence; import java.util.List; /** * JPA 양방향 연관관계와 연관관계의 주인 - 주의점, 정리 */ public class JpaMain2 { 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.setUsername("MEMBER1"); // member.setTeam(team); // 주석 처리 member.changeTeam(team); // 편의 메서드 생성 em.persist(member); ... } } package hellojpa.mapping; import jakarta.persistence.*; // JPA가 관리할 객체 @Entity(name = "MEMBER2") public class Member { ... public void changeTeam(Team team) { this.team = team; team.getMembers().add(this); // 가짜매핑에도 값을 세팅 } }
setTeam 메서드 안에 단순히 가짜 매핑의 값을 세팅하는 로직 하나만 추가하고 setTeam의 이름을 변경하였을 뿐이다.
이렇게 편의 메서드를 생성하면 휴먼 에러를 방지할 수 있고, 메서드의 이름을 단순히 setter로 지정하는 것보다 이해하기도 훨씬 수월한 클린 코드가 된다.정리
- 양방향 매핑 관계에서 값을 세팅해야 하는 곳은 연관관계의 주인이다.
- 가짜 매핑은 UPDATE, INSERT를 수행할 수 없고, 값을 읽을 수만 있다.
- 그렇지만, 논리적 결함(객체지향적인 코드) 및 JPA 사용 시 오류를 방지하기 위해 양방향 매핑 관계에서는 양쪽 모두 값을 세팅하는 것이 옳다.
-> 가능하다면 편의 메서드를 연관관계의 주인 쪽에 하나 생성해서 하나의 메서드에서 처리하도록 관리하자.
다음글이 없습니다.이전글이 없습니다.댓글