공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
단방향 연관관계
객체와 테이블 연관관계의 차이를 이해해 보자.
객체를 테이블에 맞추어 모델링하는 예
(참조 대신에 외래 키를 그대로 사용한다.)
@Entity
public class Team {
@Id @GeneratedValue
@Column(name ="TEAM_ID")
private Long id;
private String anme;
}
- TEAM_ID가 PK다.
@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;
}
- Team의 PK를 참조한다.
- Member 테이블에 TEAM_ID값을 그대로 생성한다. (문제점)
Member객체를 만들고 team을 저장해 보자
try {
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeamId(team.getId());
em.persist(member);
tx.commit();
}
- Team의 PK값인 TEAM_ID가 1이고 member는 FK로 TEAM_ID 1을 가진다.
- TEAM_ID로 조인을 할 수도 있다.
외래키 식별자를 직접 다루면 조회할 때도 문제점이 있다.
Member findMember = em.find(Member.class, member.getTeamId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);
- 멤버가 어느 팀 소속인지 알고 싶다고 가정했을 때, 우선 em.find로 멤버 클래스를 가져온다.
- 그럼 Team을 바로 못 가져오기 때문에 findTeamId를 먼저 가져온다.
- 그리고 findTeamId로 Team을 가져온다.
이런 식으로 정보가 필요할 때마다 연관 관계가 없기 때문에 매번 멤버에서 조회를 먼저 해야 한다.
객체를 테이블에 맞춰 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
테이블은 외래 키로 조인을 해서 연관된 테이블을 찾고, 객체는 참조를 사용해서 연관된 객체를 찾는다. 이것이 가장 큰 간격이다.
단방향 연관관계
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
- 이렇게 맵핑하면 Team의 Id값이 아닌 참조값을 가져온다.
- 하나의 팀에 여러 멤버가 소속되니까 맴버가 N 팀이 1이다. 따라서 @ManyToOne를 적어준다.
- 그리고 조인되는 컬럼을 @JoinColumn으로 명시해 준다.
그러면 아래와 같은 관계가 된다.
객체 지향 모델링(ORM 매핑)
이 상태에서 테스트를 해보자.
//팀
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//맴버
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
- member.setTeam(team)으로 바로 넣으면 JPA에서 알아서 PK를 꺼내고 값을 인서트 할 때 FK을 사용한다.
- 조회할 때도 findMember.getTeamId() 대신에 바로 findMember.getTeam()으로 조회하면 된다.
여기서 select 쿼리가 없는 이유는 영속성 컨텍스트에 1차 캐시에서 값을 가져오기 때문이다.
DB에 select 하는 쿼리를 보고 싶을 때는 em.flush(); em.clear(); 를 적어주면 된다.
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();
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("findTeam = " + findTeam.getName());
tx.commit();
- flush()로 영속성 컨텍스트에 있는 값을 DB로 보내서 싱크를 맞춘 후
- clear()로 영속성 컨텍스트를 초기화시킨다.
위 테스트에서 알 수 있듯이 JPA에서는 객체의 참조와 테이블의 외래키를 맵핑해서 연관관계를 만들 수 있다.
양방향 연관관계와 연관관계의 주인1 - 기본
JPA의 포인터라고 불리는 양방향 연관관계를 알아보자.
테이블은 단방향 매핑에서 봤던 연관관계와 똑같다. 왜냐하면 DB에서는 FK만 있으면 조인해서 양쪽 모두 접근 가능하기 때문이다.
ex) select m.member_id, t.team_id
from member m join team t
on m.team_id = t.team_id;
하지만 객체는 Team에서 Member에 접근하려면 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<>();
//생략
}
- Team에 @OneToMany로 연관관계 매핑을 한다.
Member findMember = em.find(Member.class, member.getId());
List<Member> members = findMember.getTeam().getMembers();
for (Member m : members) {
System.out.println("m = " + m.getUsername());
}
여기서 이런 의문이 생길 수 있다.
member에서는 @JoinColumn(name = "TEAM_ID")으로 컬럼만 적었는데
team에서는 @OneToMany(mappedBy = "team")으로 team 전체를 적네? 왜 다른 걸까?
mappedBy(객체와 테이블 간에 연관관계를 맺는 차이)를 이해해야 한다.
객체와 테이블이 관계를 맺는 차이
- 객체 연관관계 = 2개
ㄴ 회원 -> 팀 연관관계 1개 (단방향)
ㄴ 팀 -> 회원 연관관계 1개 (단방향)
- 테이블 연관관계 = 1개
ㄴ 회원 <--> 팀의 연관관계 1개 (양방향)
객체의 양방향 관계
- 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
- 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 한다.
- A -> B (a.get(B))
- B -> A (b.get(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(FK) 값은 Member 객체도 참조하고, Team 객체도 참조한다.
- 이때, FK를 Member의 team값을 변경했을 때 update 해야 할지 Team의 members값을 변경했을 때 update 해야 할지 딜레마가 생긴다.
- 테이블 입장에서는 TEAM_ID 하나만 관리하지만 객체는 그렇지 않다. 주인을 정해야 한다.
연관관계의 주인(Owner)
<양방향 매핑 규칙>
- 객체의 두 관계 중 하나를 연관관계 주인으로 지정한다.
- 연관관계의 주인만 외래 키를 관리한다.(등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능하다.
- 주인은 mappedBy 속상 사용할 수 없다.
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
누구를 주인으로?
- 비즈니스 로직을 기준으로 연관관계의 주인을 선택하면 안 된다.
- 외래키가 있는 곳을 주인으로 정한다.
- 여기서는 Member.team이 연관관계 주인이다.
양방향 연관관계와 연관관계의 주인2 - 주의점, 정리
양방향 매핑 시 가장 많이 하는 실수
Member member = new Member();
member.setUsername("member1");
em.persist(member);
Team team = new Team();
team.setName("TeamA");
team.getMembers().add(member); //역방향(주인이 아닌 방향)만 연관관계 설정
em.persist(team);
- INSERT 쿼리가 2번 나갔다.
- DB에서 MEMBER의 TEAM_ID가 NULL이다.
- Team은 주인이 아니기 때문에 읽기만 가능한데 team 객체를 이용해서 값을 변경하니까 MEMBER 테이블에 변화가 없는 것이다.
- 주인인 Member에서 데이터를 변경해야 한다.
Team team = new Team();
team.setName("TeamA");
//team.getMembers().add(member); //역방향(주인이 아닌 방향)만 연관관계 설정
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setTeam(team);
em.persist(member);
- 이제 MEMBER의 TEAM_ID가 1로 세팅된 것을 확인할 수 있다.
그럼 Team은 주인이 아니기 때문에 위의 주석된 코드를 적지 않아도 될까?
그건 아니다. 양방향 연관관계일 때 순수한 객체 관계를 고려하면 항상 양쪽 다 값을 입력해야 한다.
객체지향적으로 코드를 짜야한다.
만약 생략하면 2가지 문제점이 있다.
1. flush(), clear()를 생략한 경우
//저장
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()); // 1차 캐시에 존재
List<Member> members = findTeam.getMembers();
System.out.println("=============");
for (Member m : members) {
System.out.println("m = " + m.getUsername());
}
System.out.println("=============");
tx.commit();
- 위 코드에서 findTeam은 1차 캐시에서 값을 찾아온다.
- 그런데 아직 team은 name("TeamA")에 대한 정보 말고는 업데이트된 정보가 없다.
- 그래서 team.getId를 해도 아무 값도 안 나오는 것이다.
따라서 양쪽 다 업데이트해주어야 한다.
2. 테스트 케이스를 작성하는 경우
- 테스트 케이스는 JPA 없이도 동작하도록 순수한 자바 코드로 작성하는데 양방향으로 업데이트하지 않으면 NULL값이 나올 수 있다.
양방향 연관관계 주의 - 실습
1. 연관관계 편의 메서드를 생성한다.
public class JpaMain {
public static void main(String[] args) {
// 생략
try {
//저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.changeTeam(team); // 여기서 member의 team에 데이터를 저장할 때 양방향 세팅
em.persist(member);
@Entity
public class Member {
//생략
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this); //Team의 List<Member>에 데이터 저장
}
}
- this는 Member 자기 자신
@Entity
public class Team {
// 생략
public List<Member> getMembers() {
return members;
}
}
- 양방향 연관관계를 모두 적어주는 것은 귀찮기도 하고 실수할 가능성이 있다.
- 그래서 team.getMembers(). add(this)로 team의 값이 들어올 때 양방향으로 값을 설정할 수 있다.
- 관례적으로 쓰이는 Getter, Setter 말고 로직이 들어가는 경우에는 메서드에 이름을 정해주는 것이 좋다.
연관관계 편의 메서드는 Member나 Team 중 어디에 만들어도 상관없다.
2. 양방향 매핑 시에 무한 루프를 조심하자.
@Entity
public class Member {
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", team=" + team +
'}';
}
}
================================================
@Entity
public class Team {
@Override
public String toString() {
return "Team{" +
"id=" + id +
", name='" + name + '\'' +
", members=" + members +
'}';
}
}
- 위 코드에서 System.out.println("members = " + findTeam); 이렇게 작성을 하면 findTeam 객체를 문자열로 변환한다.
- 그때 해당 객체의 클래스인 Team 클래스의 toString 메서드는 members 리스트를 출력하는데 members 리스트에는 Member 객체가 포함되어 있다.
- Member 클래스의 toString 메서드는 team 필드를 포함하여 출력한다. team필드는 다시 Team 객체를 가리키고 있고 다시 Team의 toString() 메서드를 호출한다.
- 이런 식으로 메서드의 무한 재귀호출 때문에 스택오버플로우가 발생한다.
- lombok을 사용할 때도 toString()은 웬만하면 쓰지 말자.
- JSON 생성 라이브러리도 컨트롤러에서 엔티티를 바로 반영하면 제이슨으로 바꿀 때 무한루프에 빠진다.
ㄴ DTO로 변환해서 반환하는 것을 추천한다.
정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료된다.
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가되는 것뿐이다.
- 실무에서는 JPQL에서 역방향으로 탐색할 일이 많다.
- 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 된다. (테이블에 영향을 주지 않는다.)
단방향 매핑으로 설계를 끝내려고 해 보자.
실전 예제 2 - 연관관계 매핑 시작
- 테이블 구조는 이전과 같다.
- 참조를 사용하도록 변경한다.
- Member와 Order에서 연관관계 주인은 외래키가 있는 Order가 된다.
@Entity
@Table(name = "ORDERS")
public class Order {
//생략
// @Column(name = "MEMBER_ID")
// private Long memberId;
@ManyToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
}
- 기존 코드는 주석처리하고 연관관계 설정을 한다.
@Entity
public class OrderItem {
//생략
// @Column(name = "ORDER_ID")
// private Long orderId;
// @Column(name = "ITEM_ID")
// private Long itemId;
@ManyToOne
@JoinColumn("ORDER_ID")
private Order order;
@ManyToOne
@JoinColumn("ITEM_ID")
private Item item;
}
- ORDER_ITEM 입장에서는 ORDERS와 ITEM의 외래키를 모두 가지기 때문에 둘 다 설정해 준다.
- 이렇게 외래키 값을 그대로 가지는 것이 아니라 객체를 가지도록 설계한다.
- 나중에 필요하면 OrderItem 객체를 조회해서 getOrder, getId로 값을 가져올 수 있다.
@Entity
public class Member {
//생략
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
}
- Member에서 양방향 매핑을 한다.
@Entity
@Table(name = "ORDERS")
public class Order {
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItems = new ArrayList<>();
}
- OrderItem과 양방향 매핑을 한다.
주문이 들어왔을 때를 가정해 보자.
(Order와 OrderItem은 현재 양방향 연관관계이기 때문에 연관관계 편의 메서드를 만든다.)
@Entity
@Table(name = "ORDERS")
public class Order {
//생략
public void addOrderItem(OrderItem orderItem) {
orderItems.add(orderItem);
orderItem.setOrder(this);
}
}
- Order에 연관관계편의 메서드를 만든다.
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 {
Order order = new Order();
order.addOrderItem(new OrderItem());
tx.commit();
} catch (Exception e) {
tx.rollback();
} finally {
em.close();
}
emf.close();
}
}
- Main 클래스에서 연관관계편의 메서드를 호출한다.
그런데 Order에서 orderItems List가 꼭 필요한 것만은 아니다.
public class JpaMain {
public static void main(String[] args) {
try {
Order order = new Order();
em.persist(order);
OrderItem orderItem = new OrderItem();
orderItem.setOrder(order);
em.persist(orderItem);
}
}
}
- 이렇게 양방향 연관관계가 아니어도 애플리케이션 개발하는데 아무 문제가 없다.
단방향 연관관계만으로도 대부분의 개발을 수행할 수 있다.
그러나 양방향 연관관계를 사용하는 주된 이유는 개발의 편의성과 JPQL 작성 시 복잡성을 줄이기 위해서이다.
보통은 단방향 연관관계를 주로 사용하고 개발의 편리성을 위해 양방향 연관관계를 사용할 때도 있다고 알아두자.
'BackEnd > JPA' 카테고리의 다른 글
[JPA] 고급 매핑 (0) | 2024.07.15 |
---|---|
[JPA] 다양한 연관관계 매핑 (0) | 2024.07.14 |
[JPA] 엔티티 매핑 (0) | 2024.07.12 |
[JPA] 영속성 관리 - 내부 동작 방식 (1) | 2024.07.12 |
[JPA] JPA 시작하기 (0) | 2024.07.11 |