티스토리 뷰
Team을 통해서도 getMemberList()로 특정 팀에 속한 멤버 리스트를 가져오고 싶다.
객체 설계는 위와 같이 Member에서는 Team을 가지고 있고, Team에서는 Members를 가지고 있도록 설계하면 된다.
DB를 보자. DB는 단방향 매핑때와 바뀌는게 없다. 왜냐. 둘을 join 하면 된다. DB는 방향이 없다!
이 두가지가 큰 차이다.
Member 엔티티는 단방향과 동일하다.
public class Member {
private Long id;
name = "USERNAME") (
private String name;
private int age;
name = "TEAM_ID") (
private Team team;
...
}Team 엔티티는 컬렉션을 추가해주면 된다.
팀의 입장에서 바라보는일대다, @OneToMany 어노테이션을 설정하고,
mappedBy로 team과 연관이 있는 것을 알려준다.
컬렉션을 매핑한다. 관례로 ArrayList로 초기화 한다. NPE 방지.
public class Team {
private Long id;
private String name;
mappedBy = "team") (
private List<Member> members = new ArrayList<Member>();
...
}반대 방향으로 객체 그래프 탐색할 수 있다.
//팀 조회
Team findTeam = em.find(Team.class, team.getId());
// 역방향으로 멤버들 조회
int memberSize = findTeam.getMembers().size();
계속 강조하지만, 객체는 방향을 가지고 있고, DB는 방향성이 없다. FK선언 하나면 양방향 관계를 맺을 수 있다.
mappedBy = JPA의 멘붕 클래스, mappedBy는 처음에는 이해하기 어렵다.
객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
객체 연관관계
회원 -> 팀 연관관계 1개(단방향)
팀 -> 회원 연관관계 1개(단방향)
객체의 양방향 관계
객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
객체를 양방향으로 참조하려면 억지로 단방향 연관관계를 2개 만들어야 한다.
A -> B(a.getB())
class A {
B b;
}B -> A(b.getA())
class B {
A a;
}테이블 연관관계
회원 <-> 팀의 연관관계 1개(양방향)
테이블의 양방향 연관관계
테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
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
객체로 보자. 멤버에 팀을 넣고, 팀에 멤버를 넣는다. 두군데 모두 넣어 주는데.
둘 중에 뭘로 연관관계를 매핑해야 되나?
멤버의 팀값을 업데이트 할때 FK 업데이트?
팀의 members를 업데이트 했을때 FK 업데이트?
하지만 DB 에서는 객체가 참조를 어떻게 하던, FK값만 업데이트 되면 된다.
단방향 연관관계 매핑에서는 참조와 외래키만 업데이트 하면 됐지만, 양방향 관계 매핑에서는 다르다.
이게 가장 큰 패러다임의 문제이다.
연관관계의 주인(Owner)
패러다임의 문제를 해결하기 위해 만들어진 개념이다.
양방향 매핑 규칙
객체의 두 관계중 하나를 연관관계의 주인으로 지정
연관관계의 주인만이 외래 키를 관리(등록, 수정) 한다. DB에 접근한다.
객체에 둘다 정보를 업데이트 해도, 연관관계의 주인 것만 DB에 영향을 준다.
주인이 아닌쪽은 읽기만 가능
Member에 Team값을 업데이트 하면 mappedBy로 매핑된 Team의 members로 업데이트 된 DB의 멤버들을 읽을 수 있다는 이야기이다.
Member에 Team 정보 update 했는데, Team에서 getMembers로 가져왔는데 반영 안됐네? -> 연관관계 설정 안했구나.. 라고 바로 알 수 있다.
mappedBy 키워드로 연관관계 매핑이 되었다. 라는 것을 의미한다. 그래서 JoinColumn도 없다.
주인은 mappedBy 속성 사용 하지 않고, @ JoinColumn
주인이 아니면 mappedBy 속성으로 주인을 지정한다.
누구를 주인으로 해야 할까?
외래 키가 있는 곳을 주인으로 정해라. @JoinColumn이 있는 쪽.
Team.members를 주인으로 선택했다고 가정하고, members를 업데이트 했다. 결과는? Team의 필드값을 바꿨는데, Member로 업데이트 쿼리가 날라간다. ???
여기서는 Member.team이 연관관계의 주인이다.
멤버와 팀이 다대일 관계인데, DB에서 보면 다쪽이 FK를 가지고 있다. 다쪽이 주인이 된다.
연관관계의 주인은 비즈니스 적으로 중요하지 않다. 단지 FK를 가진 쪽이 주인이 되면 된다.
예를 들면, 자동차와 자동차 바퀴가 있는데 비즈니스적으로는 자동차가 중요하지만, 자동차와 바퀴는 일대다 관계다. 다인 바퀴가 연관관계의 주인이 된다.
이렇게 설계 해야,
뒤에서 나올 성능이슈도 없고,
엔티티랑 테이블이 매핑이 되어있는 테이블에서 FK가 관리가 된다.
모 민족에서 프로젝트 중에, 모두 단방향으로 설계하고 개발했다. 양방향으로 뚫어서 조회가 필요한 경우에 그 때 양방향 매핑 하면 된다. 자바 코드만 수정이 일어나기 때문에 DB에 영향 주지 않는다.
연관관계의 주인에 값을 입력하지 않음
연관관계의 주인을 Member.team으로 잡아놓고, Team의 members에만 연관관계를 설정한다.
주인이 아닌 mappdBy가 설정 되어있는 Team의 members는 조회 권한만 가지고 있고, DB에 영향을 주지 못한다.
따라서, DB에 Member를 조회해보면, TEAM_ID 가 NULL인 상황이 발생한다.
양방향 매핑에 대해서 잘 이해하고 있으면, DB 모델링에 따라서 객체 모델링은, ORM 모델링은 이렇게 엮어 가면 되겠구나 라고 생각하는데 많은 도움이 된다.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
em.persist(member);
//이 작업을 하지 않고,
//member.setTeam(team);
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
tx.commit();
양방향 매핑시 연관관계의 주인에 값을 입력해야 한다.
JPA 입장에서만 보면, 연관관계의 주인인 멤버에다가 팀을 세팅해주면 끝난다. DB에 FK값 세팅 되고 문제가 없다.
굳이 team의 members 컬렉션에 멤버를 새로 넣어주지 않아도, 지연 로딩을 통해서 해당 멤버를 조회해올 수 있기 때문에 (아래의 코드에서는)아무 문제가 없다.
아래의 코드에서는 em.flush(), clear() 하는 순간에 DB에 FK 세팅 된다. 그래서 지연 로딩을 해도 FK로 조인해서 가져올 수 있다.
하지만, em.flush(), clear()가 일어나지 않으면 DB에 쿼리가 안날라가고, FK도 없이 MEMBER가 1차 캐시에만 영속화 되어있는 상태이다. members 조회해봤자 size 0이다.
결론은 진짜 객체 지향적으로 고려하면 항상 양쪽다 값을 넣어 주는 것이 맞다.
추가적으로 JPA 없이 순수 자바 Object로 테스트 케이스가 동작하게끔 테스트 코드를 짤때도 NPE 발생한다. 양쪽다 넣어주자.
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
em.persist(member);
// 연관관계의 주인에 값 설정
member.setTeam(team);
// 역방향 연관관계를 설정하지 않아도, 지연 로딩을 통해서 아래에서 Member에 접근할 수 있다.
//team.getMembers().add(member);
// 이 동작이 수행되지 않으면 FK가 설정되어 있지 않은 1차캐시에만 영속화 된 상태이다. SELECT 쿼리로 조회해봤자 list 사이즈 0이다.
em.flush();
em.clear();
Team findTeam = em.find(Team.class, team.getId());
List<Member> findMembers = findTeam.getMembers();
for (Member m : findMembers) {
// flush, clear가 일어난 후에는 팀의 Members에 넣어주지 않았지만, 조회를 할 수 있음. 이것이 지연로딩
System.out.println(m.getUsername());
}
tx.commit();
순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자.
연관관계를 설정하다 보면 휴먼에러가 생길 여지가 많다. 연관관계 편의 메소드를 생성하는 것을 권장한다.
편의 메소드에서는 주인쪽에서 연관관계의 값을 설정할때, 역방향 값도 함께 설정해준다. 메소드를 원자적으로 사용해서 세트로 움직이면 실수할 일이 없어진다.
혹시 편의 메소드 안에 비즈니스 로직이 들어가는 복잡한 경우. 책을 참조해보자.
class Member {
...
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}양방향 매핑시에 무한 루프를 주의하자
lombok이 자동으로 만드는 toString()을 사용하지 말자.
Member의 toString()
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", team=" + team +
'}';
}Team의 toString()
public String toString() {
return "Team{" +
"id=" + id +
", name='" + name + '\'' +
", members=" + members +
'}';
}Member의 toString()을 호출하는 순간 Team의 toString()의 members가 호출하는 각 member의 toString()때문에 무한루프 생성. 스택오버플로우 발생
JSON 생성 라이브러리
양방향 관계의 엔티티를 JSON으로 시리얼라이즈 하는순간 무한루프에 빠져버린다.
컨트롤러에서는 엔티티를 절대 직접 반환하지 말자.
1 - 엔티티 -> JSON 시 무한루프 걸린다.
2 - 엔티티는 충분히 변경의 여지가 있는데, 엔티티 변경시 API 스펙 자체가 변경된다.
실무에서 웬만하면 DTO로 변환해서 반환하자. 그렇게 하면 JSON 생성 라이브러리 때문에 생기는 문제는 없다.
단방향 매핑만으로도 이미 연관관계 매핑은 끝이다.
설계할 때 객체 입장에서 보면 양방향 매핑은 양쪽다 신경을 써야 하므로 복잡도가 올라간다.
실무에서 JPA 모델링 할 때, 단방향 매핑으로 처음에 설계를 끝내야 한다(객체와 테이블을 매핑하는 것). 일대다에서 다쪽에 단방향 매핑으로 쭉 설계하면 이미 테이블 FK 설정은 끝났다. 거기서 필요할때 양방향 매핑을 추가해서 역방향 조회 기능을 쓰면 된다. 자바 코드에 컬렉션만 추가하면 된다.
양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐이다.
JPQL에서 역방향으로 탐색할 일이 많다. 현업에서 많이 쓴다.
단방향 매핑을 잘 하고, 테이블이 굳어진 뒤에 양방향 매핑은 필요할 때 추가해도 된다. 테이블에 영향주지 않는다.
그동안 학습한 것을 보면, 단방향 매핑에서 양방향 매핑으로 넘어가면서 추가된 것은 팀 객체(엔티티)에 members 컬렉션이 들어간것 뿐이다. 테이블은 똑같다.
연관관계의 주인을 정하는 기준은
비즈니스 로직을 기준으로 주인을 선택하면 안된다.
연관관계 주인은 외래 키의 위치를 기준으로 정해야 한다.
Reference
'ICT Eng > JPA' 카테고리의 다른 글
[JPA] @ManyToOne, 다대일[N:1] 관계 (1) | 2019.08.23 |
---|---|
[JPA] 연관관계 매핑시 고려사항 (0) | 2019.08.23 |
[JPA] 단방향 연관관계 (0) | 2019.08.16 |
[JPA] Spring Data JPA와 QueryDSL 이해, 실무 경험 공유 (18) | 2019.04.30 |
[JPA] 객체지향 쿼리, JPQL (1) | 2019.04.26 |
- Total
- Today
- Yesterday
- AWS
- ORM
- 정렬
- 인프런
- RBT
- 시간복잡도
- Raspberry Pi
- 라즈베리파이
- Spring Boot
- 스프링부트
- JPA
- 알고리즘
- vuejs
- Java
- 자바
- IT융합인력양성사업단
- Spring
- 젠킨스
- 레드블랙트리
- Vue.js
- Wisoft
- springboot
- github
- vuex
- Recursion
- 무선통신소프트웨어연구실
- 순환
- 한밭이글스
- 한밭대학교
- Algorithm
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |