연관관계 매핑이 왜 필요할까?
우리가 DB 테이블 구조대로 코드를 작성하면 객체지향적이지 않게 된다.
위와 같이 테이블 구조대로 코드를 작성하고, 멤버가 속한 팀을 찾아보자.
DB에서 멤버 찾기 -> 멤버의 팀 아이디 찾기 -> 팀 아이디를 통하여 DB에서 팀 찾기
과정이 이뤄진다.
하지만 객체지향적으로 코드를 작성하고, 멤버가 속한 팀을 찾아보자.
DB에서 멤버 찾기 -> 멤버의 팀 찾기
과정이 이뤄진다.
과정이 단축되는 것을 알 수 있다.
테이블과 객체에서 연관관계를 맺는 데의 차이점이 있다.
테이블은 외래키로 두 테이블이 양방향 관계를 맺게 되고,
객체는 참조를 사용하여 관계(단방향)를 맺는다.
단방향 연관관계
현재 위의 테이블을 보면 회원은 하나의 팀에만 소속될 수 있다.
그래서 Member와 Team은 다대일 관계이다.
단방향 연관관계에서는 객체 연관관계와, 어떤 column과 조인하는지 알려줘야한다.
Member는 현재 Team 객체를 참조하고 있다. 그리고 TEAM_ID를 외래키로 가진다.
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
@ManyToOne
객체 연관관계를 나타내준다.
현재 Member 와 Team은 다대일 관계로 Membe는 '다', Team은 '일' 이므로 @ManyToOne을 쓴다.
@JoinColumn
제일 중요한 점은 어떤 column과 조인하는지 알려줘야한다.
현재 Member에서 외래키를 가지고 있으므로, 어느 외래키로 Team 테이블과 조인을 하는지 알려줘야한다.
@JoinColumn(name = "TEAM_ID")
양방향 연관관계
단뱡항 연관관계에서 Member -> Team에만 접근할 수 있었다.
그렇다면 Team -> Member로 접근하는 방법은 없을까?
있다.
Team -> Member 일대다 관계이다. 즉, 한 Team에 여러 Member가 들어가있다.
따라서 컬렉션을 사용해야 한다.
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
@OneToMany
Team은 '일', Member는 '다' 로 일대다 구조이다. 따라서 @OneToMany를 사용한다.
mappedBy
연관관계를 매핑하면서 어려운 점이 mappedBy라고 한다.
mappedBy를 쉽게 이해하기 위해서는 객체와 테이블 간 연관관계를 맺는 차이를 숙지하고 있어야한다.
객체와 테이블 간 연관관계 차이
객체의 양방향 연관관계는 단방향 연관관계가 2개 있다고 생각하자.
그리고 Member에 Team, Team에는 List<Member> members 참조값이 서로 존재한다.
하지만 테이블은 연관관계 한 개로 양방향 가능하다.
연관관계의 주인은 왜 필요할까?
양방향 연관관계에서 객체를 양방향으로 만들어놨다. 그렇다면 둘 중에 어떤 걸로 매핑해야할까?
Member에 있는 team 값을 바꿨을 때 MEMBER 테이블의 외래키 값이 업데이트 또는
Team에 있는 members 값이 변했을 때 외래키가 업데이트 되야할까?
그래서 룰이 생긴다.
둘 중 하나로 외래 키를 관리해야한다.
Member에 있는 Team 또는 Team에 있는 List members 중에 주인을 정해야한다.
이것이 바로 연관관계의 주인이다.
mappedBy라는 뜻 자체가 ~에 의해 매핑되었다라는 말이다.
그니까 mappedBy가 쓰여진 객체는 주인이 아니고, mappedBy = "OOO" OOO이 주인이다.
여기서 주인이 아닌 객체는 읽기(조회)만 가능하다. 즉, 해당 객체에 값을 넣어도 아무런 변화가 없다.
왜냐?
DB에 값을 넣거나 업데이트 할 때는 주인인 OOO만 참조하기 때문이다.(자주 헷갈리므로 꼭 기억하자!)
양방향 연관관계에서 데이터 저장 시 주의할 점
위에서 말했다시피 양방향 연관관계에서 데이터를 저장할 시,
주인에게 저장해야 DB에 제대로 저장된다고 했다.
위의 사진을 보면 team에 member를 더해도 연관관계가 성립되지 않음을 알 수 있다. (TEAM_ID = null)
TEAM_ID에 1이 되있는 것을 알 수 있다.
연관관계 편의 메서드
위 사진은 양방향 연관관계에서 데이터를 저장하고 조회가 잘 되는 결과를 볼 수 있다.
하지만 연관관계의 주인에만 값을 저장하면 순수한 객체에서 문제가 발생될 수 있기 때문에,
양쪽 방향에 모두 값을 입력해주는게 좋다고 한다.
양족에 넣어주지 않게 되면 2가지 문제가 생길 수 있다.
1. flush, clear가 없을 경우
flush와 clear를 하지 않으면 member와 team은 1차 캐시가 그대로 저장되어 있다.
그래서 쿼리가 나가지 않게 된다
또한, 여기서 중요한 점은 members도 조회되지 않는 점이다.
왜 조회되지 않을까?
findTeam은 1차 캐시에서 그대로 가져온 것이다.
즉, 연관관계를 매핑하지 않고 처음 team을 영속성 컨텍스트에 넣을 때 그 시점의 team이다.
따라서 team은 메모리에서 그대로 가져온, 순수한 객체 상태이기 때문에 컬렉션에도 값이 있지 않게 된다.
2. 양쪽에 값을 넣어줬을 때, //** 중에 한 개를 까먹을 수 있다.
까먹는 것을 방지하기 위해 둘을 하나의 메서드로 만들어 실수를 방지한다.
이것을 연관관계 편의 메서드라 한다.
연관관계 편의 메서드 중 변경 시 주의할 점
연관관계 편의 메서드를 작성하고, 연관관계를 변경할 시 주의할 점이 있다.
member1.setTeam(teamA);
member1.setTeam(teamB);
Member findMember = teamA.getMember();
findMember는 여전히 member1이 조회되는 것을 알 수 있다.
따라서 setTeam을 잘 작성하여 위와 같은 오류가 생기지 않게 해야한다.
양방향 연관관계 시 무한 루프
양방향 연관관계를 사용하다가 무한 루프에 걸릴 수 있다.
아무 생각 없이 양방향 연관관계를 맺고 있는 두 클래스에 toString lombok을 사용하게 되면
아래의 빨간 줄이 계속해서 참조가 되어 무한루프에 빠질 수 있다.
두 번째로 JSON 생성 라이브러리 무한 루프가 있다.
controller에서 entity를 response로 보낼 때, 두 entity가 양방향이면 무한루프가 발생한다.
entity를 JSON으로 바꾸면 team이 있고, team에서 JSON을 또 바꿀 때 members가 있기 때문에 무한루프가 발생한다.
또한, entity가 변경되는 순간에 api 스펙이 바뀌기 때문에 혼란스러워질 수 있다.
따라서 controller에서 entity를 직접 절대 반환하지 않고, DTO를 사용하자.
Reference
- 자바 ORM 표준 JPA 프로그래밍 (김영한 지음)
'Spring 강의 > JPA - 기본편' 카테고리의 다른 글
고급 매핑 (0) | 2021.12.07 |
---|---|
다양한 연관관계 매핑 (0) | 2021.12.02 |
앤티티 매핑 (0) | 2021.11.20 |
영속성 관리 - 내부 동작 방식 (0) | 2021.11.19 |
JPA 시작하기 (0) | 2021.11.17 |