[자바 ORM 표준 JPA 프로그래밍 - 기본편] 05강. 연관관계 매핑 기초
05강. 연관관계 매핑 기초
1. 연관관계가 필요한 이유
객체를 테이블에 맞추어 모델링했을 때의 문제점
: 객체 간 연관관계 없음
-
참조하지 않고 외래 키를 그대로 사용
// Member.java @Entity public class Member { @Id @GeneratedValue private Long id; @Column(name = "USERNAME") private String name; @Column(name = "TEAM_ID") private Long teamId; … } // Team.java @Entity public class Team { @Id @GeneratedValue private Long id; private String name; … }
// 생략 // 문제점 : 참조가 아니고 외래키 값을 그대로 가지고 있음 // 저장 Team team = new Team(); team.setName("TeamA"); em.persist(team); // 영속 상태가 되려면 ID 값 생김 Member member = new Member(); member.setName("member1"); // member1을 TeamA에 소속시키자 member.setTeamId(team.getId()); // ID값 가져오기 : 영속 상태 될 때 생긴 ID 값을 가져옴 em.persist(member); // 객체면 setTeam이어야 할 것 같은데 .. tx.commit(); // 생략
실행 결과
// 생략 : MEMBER 테이블, TEAM 테이블 CREATE 쿼리 Hibernate: call next value for hibernate_sequence Hibernate: call next value for hibernate_sequence Hibernate: /* insert hellojpa.Team */ insert into Team (name, TEAM_ID) values (?, ?) Hibernate: /* insert hellojpa.Member */ insert into Member (USERNAME, TEAM_ID, MEMBER_ID) values (?, ?, ?)
DB
MEMBER
MEMBER_ID USERNAME TEAM_ID 2 member1 1 TEAM
TEAM_ID NAME 1 TeamA
💡 ID가 TEAM_ID는 1, MEMBER_ID는 2인 이유?
- 두 객체가 같은 Sequence 이용
- 따로 ID 관리하려면 따로 만들고 매핑
-
외래 키 식별자를 직접 다룸
//팀 저장 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()); Team findTeam = em.find(Team.class, team.getId()); // 연관관계가 없기 때문에 계속 다시 쿼리를 보내야 함
-> 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다
- 테이블과 객체의 차이 : 외래키(테이블) vs. 참조(객체)
- 테이블 : 외래 키로 조인 사용해서 연관된 테이블 찾음
- 객체 : 참조 사용해서 연관된 객체를 찾음
2. 단방향 연관관계
객체 지향 모델링
- 객체 연관관계 사용 : Member에서 TeamId가 아니라 Team을 가져오도록 모델링
- 객체의 참조와 테이블의 외래 키를 매핑
-
@ManyToOne
,@OneToMany
@Entity public class Member { @Id @GeneratedValue private Long id; @Column(name = "USERNAME") private String name; private int age; // @Column(name = "TEAM_ID") // private Long teamId; @ManyToOne // Member가 Many, Team이 One (Member : Team = N : 1) @JoinColumn(name = "TEAM_ID") // Join 해야 하는 Column이 뭔지 private Team team; // getter, setter..
JpaMain.java
// 생략 // 저장 Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); // member1을 TeamA에 소속시키자 member.setTeam(team); // team에서 PK값 꺼내서 알아서 FK로 사용 em.persist(member); // 객체면 setTeam이어야 할 것 같은데 .. Member findMember = em.find(Member.class, member.getId()); Team findTeam = findMember.getTeam(); System.out.println("findTeam = " + findTeam.getName()); // findTeam = TeamA tx.commit(); // 생략
-
-
ORM 매핑
-
연관관계 저장
//팀 저장 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();
-
연관관계 수정
// 새로운 팀B Team teamB = new Team(); teamB.setName("TeamB"); em.persist(teamB); // 회원1에 새로운 팀B 설정 member.setTeam(teamB);
- 이와 같이 수정하면 DB에 연관관계가 업데이트 됨
3. 양방향 연관관계와 연관관계의 주인
양방향 매핑
-
지금까지 작성한 코드에서 Member -> Team은 가능하지만 Team -> Member는 불가능
-> 양방향 매핑을 하면 가능해짐
대상 방법 테이블 외래키로 Join 객체 Team -> Member 조회할 수 있도록 Team에 members라는 List 넣어줌 -
엔티티에 필요한 컬렉션을 추가
- Member 엔티티는 단방향과 동일, Team 엔티티는 컬렉션 추가
-> 역방향 조회 가능
@Entity public class Member { @Id @GeneratedValue private Long id; @Column(name = "USERNAME") private String name; private int age; @ManyToOne @JoinColumn(name = "TEAM_ID") private Team team;
@Entity public class Team { @Id @GeneratedValue private Long id; private String name; @OneToMany(mappedBy = "team") List<Member> members = new ArrayList<Member>(); … }
//조회 Team findTeam = em.find(Team.class, team.getId()); int memberSize = findTeam.getMembers().size(); //역방향 조회
연관관계의 주인과 mappedBy
객체와 테이블이 관계를 맺는 차이
대상 | 설명 |
---|---|
객체 | - 연관관계 2개 : 회원 -> 팀 연관관계 1개(단방향) / 팀 -> 회원 연관관계 1개(단방향) -> 양쪽에 참조가 필요함 |
테이블 | - 연관관계 1개 : 회원 <-> 팀의 연관관계 1개(양방향) -> Foreign Key 하나만 있으면 양쪽 모두 조회 가능 |
(1) 객체의 양방향 관계
- 사실 양방향 관계가 아닌 서로 다른 단방향 관계 2개
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 함
- A -> B (a.getB()) / B -> A (b.getA())
class A {
B b;
}
class B {
A a;
}
(2) 테이블의 양방향 관계
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리
- MEMBER.TEAM_ID 외래 키 하나로 양방향 연관관계 가짐 (양쪽으로 Join)
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
연관관계의 주인(Owner)
-
참조 두 가지 중 하나로 외래 키를 관리해야 함 -> 연관관계의 주인!
ex. Team team 또는 List members
양방향 매핑 규칙
- 객체의 두 관계중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리(등록, 수정)
- ⭐️ 주인이 아닌쪽은 읽기만 가능
- 주인은
mappedBy
속성 사용X, 주인이 아니면mappedBy
속성으로 주인 지정
연관관계의 주인 정하기
- 외래 키가 있는 있는 곳
- 1:N 관계에서 N쪽(
@ManyToOne
)
- 1:N 관계에서 N쪽(
ex. 여기서는 Member.team이 연관관계의 주인
🚨 주의 : 양방향 매핑할 때 가장 많이 하는 실수
연관관계의 주인에 값을 입력하지 않음
Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); //역방향(주인이 아닌 방향)만 연관관계 설정 team.getMembers().add(member); em.persist(member);
양방향 매핑시 연관관계의 주인에 값을 입력해야 함 (순수한 객체 관계를 고려하면 항상 양 쪽 다 값을 입력해야 한다.)
Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); team.getMembers().add(member); //연관관계의 주인에 값 설정 member.setTeam(team); //** em.persist(member);
값을 입력하지 않은 경우의 문제점
JpaMain.java
// 생략 Member member = new Member(); member.setName("member1"); em.persist(member); Team team = new Team(); team.setName("TeamA"); team.getMembers().add(member); em.persist(team); em.flush(); // 1차 캐시가 아닌 DB에서 값을 가져오도록 em.clear(); tx.commit(); // 생략
실행 결과
Hibernate: /* insert hellojpa.Member */ insert into Member (USERNAME, TEAM_ID, MEMBER_ID) values (?, ?, ?) Hibernate: /* insert hellojpa.Team */ insert into Team (name, TEAM_ID) values (?, ?)
DB : TEAM_ID가 null
연관관계의 주인이 Team team이기 때문에, team의 members는 읽기만 가능(mappedBy)
-> setTeam 해줘야 함
JpaMain.java
// 생략 Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); member.setTeam(team); em.persist(member); team.getMembers().add(member); em.flush(); em.clear(); tx.commit(); // 생략
실행 결과
Hibernate: /* insert hellojpa.Team */ insert into Team (name, TEAM_ID) values (?, ?) Hibernate: /* insert hellojpa.Member */ insert into Member (USERNAME, TEAM_ID, MEMBER_ID) values (?, ?, ?)
DB : TEAM_ID가 입력되어있음
양방향 매핑할 때는 가급적 양쪽에 다 값을 넣어주자 (실수 방지)
JpaMain.java
// 생략 Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); member.setTeam(team); em.persist(member); //team.getMembers().add(member); // List에 값 세팅 안함 em.flush(); em.clear(); Team findTeam = em.find(Team.class, team.getId()); List<Member> members = findTeam.getMembers(); for (Member m : members) { System.out.println("m = " + m.getName()); } tx.commit(); // 생략
실행 결과 : FK를 이용하여 연관된 값을 가져옴
Hibernate: /* insert hellojpa.Team */ insert into Team (name, TEAM_ID) values (?, ?) Hibernate: /* insert hellojpa.Member */ insert into Member (USERNAME, TEAM_ID, MEMBER_ID) values (?, ?, ?) Hibernate: select team0_.TEAM_ID as TEAM_ID1_1_0_, team0_.name as name2_1_0_ from Team team0_ where team0_.TEAM_ID=? Hibernate: select members0_.TEAM_ID as TEAM_ID3_0_0_, members0_.MEMBER_ID as MEMBER_I1_0_0_, members0_.MEMBER_ID as MEMBER_I1_0_1_, members0_.USERNAME as USERNAME2_0_1_, members0_.TEAM_ID as TEAM_ID3_0_1_ from Member members0_ where members0_.TEAM_ID=? m = member1
- 그런데 List값 세팅하지 않으면 두 가지 문제가 발생
flush, clear 하지 않는 경우 : 1차 캐시에만 있는 값은 가져오지 못함
JpaMain.java
// 생략 Team team = new Team(); team.setName("TeamA"); em.persist(team); Member member = new Member(); member.setName("member1"); member.setTeam(team); em.persist(member); //team.getMembers().add(member); //em.flush(); //em.clear(); Team findTeam = em.find(Team.class, team.getId()); // 1차 캐시에 있음 List<Member> members = findTeam.getMembers(); System.out.println("================="); for (Member m : members) { System.out.println("m = " + m.getName()); } System.out.println("================="); tx.commit(); // 생략
실행결과
================= ================= Hibernate: /* insert hellojpa.Team */ insert into Team (name, TEAM_ID) values (?, ?) Hibernate: /* insert hellojpa.Member */ insert into Member (USERNAME, TEAM_ID, MEMBER_ID) values (?, ?, ?)
- tc 작성할 때 : Jpa 사용하지 않고 Java로만 테스트
양방향 연관관계 주의 사항
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정
-
연관관계 편의 메소드 생성
ex. Team 또는 Member에 연관관계 메서드를 생성
Member.java : Member에 생성한 경우
@Entity public class Member { @Id @GeneratedValue @Column(name = "MEMBER_ID") private Long id; @Column(name = "USERNAME") private String name; @ManyToOne // Member가 Many, Team이 One (Member : Team = N : 1) @JoinColumn(name = "TEAM_ID") // Join 해야 하는 Column이 뭔지 private Team team; // getter, setter public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Team getTeam() { return team; } public void changeTeam(Team team) { // setter를 수정하기보다는 새로 만들기 this.team = team; team.getMembers().add(this); } }
Team.java : Team에 만드는 경우
public void addMember(Member member) { member.setTeam(this); members.add(member); }
- 무한 루프 발생 주의 : toString(), lombok, JSON 생성 라이브러리
-
toString()
Member.java의 toString()
@Override public String toString() { return "Member{" + "id=" + id + ", name='" + name + '\'' + ", team=" + team + // team.toString()을 호출 '}'; }
Team.java의 toString()
@Override public String toString() { return "Team{" + "id=" + id + ", name='" + name + '\'' + ", members=" + members + // 각 member의 toString() 모두 호출 '}'; }
- lombok 라이브러리에서 toString 생성시 주의 -> 가능하면 lombok에서 toString 생성 사용하지 않기
- JSON 생성 라이브러리
- 컨트롤러에서 엔티티를 직접 response로 보내면 엔티티를 json으로 바꿀 때 계속 또 파고 들어가서 장애 발생
- 컨트롤러에서 엔티티 반환하지 말고, 가능하면 DTO로 변환하여 반환하기
-
🚨 컨트롤러에 엔티티 반환하는 경우의 문제점
- 무한 루프 발생
- 엔티티가 변경될 가능성
4. 실전 예제
🚨 주의 사항
- 최대한 단방향으로 작성! 조회의 편의를 위해 필요한 경우 양방향으로
- 모든 것을 양방향 매핑하지 않고, 관심사를 적당히 끊어내는 것이 중요 ex. Member에 Orders List 만들기보다는 Order에서 쿼리로 가져오지
- mappedBy의 주인은 FK쪽
- List는 ArrayList로 초기화(관례)
Leave a comment