연관 관계에 대한 예를 들기 위해서, 하나의 예를 들어보자.
1) 회원과 팀이 있는데,
2) 회원은 하나의 팀에만 소속될 수 있고,
3) 회원과 팀은 N:1관계이다.
연관 관계가 없는 객체를 테이블에 맞춰 모델링을 하면 아래와 같이 표현할 수 있다.
이것을 소스코드로 표현하면 아래와 같다. (Getter and Setter 생략)
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;
}
객체를 테이블에 맞추어 모델링한 것을 아래와 같이 표현할 수 있다.
(외래 키 식별자를 직접 다루어 표현)
JpaMain.java
public class JpaMain {
public static void main(String[] args) {
// src/main/resources/META-INF/persistence.xml
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);
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
// code
emf.close();
}
}
조회하여 얻은 결과로 다시 조회하는 것은 객체지향적인 방법이 아니다!
JpaMain.java
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());
//연관관계가 없음
Team findTeam = em.find(Team.class, team.getId());
tx.commit();
}
객체를 테이블에 맞춰 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
- 테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이런 큰 간격이 있다.
단방향 연관관계
Member 객체는 Team 객체를 참조하고,
Member 테이블은 Team 테이블의 기본키인 TEAM_ID를 외래키로 참조하고 있다.
코드로 표현하면 아래와 같다. (Getter and Setter 생략)
@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
@JoinColumn(name = "TEAM_ID")
private Team team;
}
ORM(Object Relational Mapping) 매핑을 표현하면 다음과 같다.
실제로 MEMBER 테이블은 TEAM의 정보를 다 가지는 것은 아니다.
그저 TEAM의 기본 키인 TEAM_ID만 외래키로 가지고 있을 뿐이다.
그런데 Member 객체는 Team 클래스를 참조하고 있다.
연관관계를 저장하는 것을 코드로 표현하려면 아래와 같이 해야한다.
JpaMain.java
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();
tx.commit();
}
member 객체가 team 객체 자체를 가져야 한다.
기존의 한 멤버에게 새로운 팀을 부여하려면 member.setTeam(teamB) 를 해주면 된다.
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원1에 새로운 팀B 설정
member.setTeam(teamB);
양방향 매핑
양방향으로 매핑은 위에서 소스에 더하여
Team에서도 Member에 대한 정보를 알 수 있어야 한다.
테이블은 변하지 않았다.
하지만 Team 에서는 컬렉션 타입 List Members 가 추가되었다.
Team 객체에서도 Member 정보를 참조하게 하면 양방향으로 연관관계를 갖는다.
Member 엔티티는 단방향과 동일하다.
Member.java
package hellojpa;
import javax.persistence.*;
import java.time.LocalDate;
import java.util.Date;
@Entity
public class Member {
@Id @GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String userName;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
public Team getTeam() {
return team;
}
public Member() {}
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
Team 엔티티는 Member List 컬렉션을 추가한다
Team.java
package hellojpa;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@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<>(); // 초기화 해주는게 관행
public List<Member> getMembers() {
return members;
}
public void setMembers(List<Member> members) {
this.members = members;
}
public Team() {}
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 void addMember(Member member) {
member.setTeam(this);
members.add(member);
}
}
양방향 매핑을 해줘서, 서로 반대방향으로도 객체 그래프를 탐색할 수 있게 해줘야 한다.
JpaMain.java
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); //역방향 조회
엔티티(객체) 간 연관관계를 맺어주면, 어디가 Main으로 되어야할까?
외래키를 가지고 있는 테이블의 엔티티가 Main이 되는 것이 맞다.
즉, Member 클래스와 Team 클래스의 연관관계 주인은 Member가 되어야 한다.
Team 클래스 내에서 정의된 List<Member> members 는 @OneToMany(mappedBy = "team")을 붙여줘야
어느 변수에 매핑되는지 명시되기 때문에 꼭 필수로 mappedBy를 써줘야 한다.
객체와 테이블의 관계의 갯수는 객체는 단방향으로 2개, 테이블은 양방향으로 1개이다.
- 객체 연관관계 == 2개
- 회원 → 팀 연관관계 1개(단방향)
- 팀 → 회원 연관관계 1개(단방향)
- 테이블 연관관계 == 1개
- 회원 ↔ 팀의 연관관계 1개(양방향)
객체의 양방향 관계
- 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개이다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
- A → B (a.getB())
class A {
B b;
}
- B → A (b.getA())
class B {
A a;
}
테이블의 양방향 연관관계
- 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
- 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
MEMBER의 TEAM_ID든, TEAM의 TEAM_ID 든, 외래 키 관리를 해야하는데,
테이블 중에서 두 테이블 중 외래키를 가진 곳의 테이블을 기준으로
외래 키를 참조하는 객체를 기준으로 잡으면 된다.
연관관계의 주인(Owner)
양방향 매핑 규칙
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리(등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 속성을 사용하지 않는다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
그래서, 외래 키가 있는 곳을 주인으로 정하면 된다. (Member)
양방향 매핑 시 가장 많이 하는 실수는 연관관계의 주인에 값을 입력하지 않는 것이다.
JpaMain.java
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
///// 여기에 member.setTeam(team); 을 해줘야 했다.
//역방향(주인이 아닌 방향)만 연관관계 설정
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);
양방향 연관관계 주의사항
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정해야한다.
- 연관관계 편의 메소드를 생성하자 (Team 클래스 addMember )
- 양방향 매핑시에 무한루프를 조심하자
- 예) toString(), lombok, JSON 생성 라이프러리
Team 클래스에서 List<Member>를 참조하고,
Member 클래스에서 Team을 참조하고 있기 때문에
JpaMain.java 에서
System.out.println("member : " + member.toString()); 을 하면
Member 클래스 안에 team이 있기 때문에 Team을 호출하고,
Team 안에 List<Member> members 가 있기 때문에
Member 클래스를 또 호출하고,
Member 클래스 안의 Team, Team 안의 Member를 계속 호출해서
오버플로우가 발생한다.
실제 운영에서 이와 같은 일이 발생하면 시스템 장애가 발생하게 된다.
그래서 toString() 사용시에는 양방향 참조하는 부분에 대해서는 확인하고 사용할 필요가 있다.
양방향 매핑 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
- JPQL에서 역방향으로 탐색할 일이 많음
- 단방향 매핑을 잘해야 하고, 양방향은 필요할 때 추가해도 된다.(테이블에 영향을 주지 않는다)
연관관계의 주인을 정하는 기준은
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안된다.
- 연관관계의 주인은 외래 키의 위치를 기준으로 정해야 한다.
'자바 > JPA' 카테고리의 다른 글
[JPA/Java] 고급 매핑 (상속관계 매핑<조인, 단일 테이블>, @MappedSuperclass) (0) | 2023.02.21 |
---|---|
[JPA/JAVA] 다양한 연관관계 매핑 (N:1, 1:N, 1:1, N:M) (0) | 2023.02.20 |
[JPA/Java] 엔티티 매핑 (객체, 테이블, 필드, 컬럼, 기본 키 등) (0) | 2023.01.22 |
[JPA/JAVA] 플러시 & 준영속 상태 (2) | 2023.01.12 |
[JPA/JAVA] 영속성 컨텍스트 (0) | 2023.01.11 |
댓글