일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 스프링 데이터 JPA
- Spring Data JPA
- 불변 객체
- 기본문법
- 스프링 mvc
- 임베디드 타입
- 페이징
- JPQL
- JPA
- 값 타입 컬렉션
- API 개발 고급
- 예제 도메인 모델
- JPA 활용 2
- QueryDSL
- 김영한
- Bean Validation
- 실무활용
- 타임리프
- 벌크 연산
- 검증 애노테이션
- 타임리프 문법
- JPA 활용2
- 컬렉션 조회 최적화
- 로그인
- 스프링
- 일론머스크
- jpa 활용
- 스프링MVC
- 트위터
- 프로젝트 환경설정
- Today
- Total
RE-Heat 개발자 일지
[JPA] [5] 연관관계 매핑 기초 본문
https://www.inflearn.com/course/ORM-JPA-Basic
인프런 김영한 님의 강의를 듣고 작성한 글입니다.
목표
- 객체와 테이블 연관관계의 차이를 이해
- 객체의 참조와 테이블의 외래 키를 매핑
- 용어 이해
- 방향(Direction): 단방향, 양방향
- 다중성(Multiplicity): 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
- 연관관계의 주인(Owner): 객체 양방향 연관관계는 관리 주인 이 필요
[1] 단방향 연관관계
■ 연관관계가 필요한 이유
"객체 지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다"
조영호(객체지향의 사실과 오해)
객체를 테이블에 맞춰 모델링하면 객체지향과는 거리가 먼 설계가 된다. 그래서 객체 지향 모델링을 위해 생긴 개념이 객체 연관관계다.
■ 예제 시나리오[공통]
회원과 팀이 있다.
회원은 하나의 팀에만 소속될 수 있다.
회원과 팀은 다대일 관계다.
① 객체를 테이블에 맞춰 모델링(연관관계X)
Member
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
//... getter, setter
}
참조 대신 테이블의 외래키 값을 가지고 있다.
Team
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
...
}
JpaMain
▶ 저장
// 팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
// 회원 저장
Member member = new Memeber();
member.setName("member1");
member.setTeamId(team.getId()); // 외래키 식별자를 직접 추가
em.persist(member);
member.setTeamId(team.getId())는 객체지향적인 방법이 아니다.(객체지향적인 방법은 member.setTeam(team))
▶ 조회
Member findMember = em.find(Member.class, member.getId());
Long findTeamId = findMember.getTeamId();
Team findTeam = em.find(Team.class, findTeamId);
연관관계가 없으므로 조회를 두 번 해야 한다. 이는 객체지향적인 방법이 아니다.
따라서 객체를 테이블에 맞춰 모델링하면 협력 관계를 만들 수 없다.
테이블 : 외래 키로 조인해서 연관 테이블을 찾는다.
객체 : 참조를 사용해 연관 객체를 찾는다.
=> 테이블 중심 설계론 객체 그래프 탐색이 불가능
② 단방향 연관관계 매핑 이론
객체 지향 모델링(객체 연관관계 사용)
Member에 teamId가 아닌 Team의 참조값을 가져온다.
객체 연관관계를 사용해 객체의 참조(Team team)와 테이블의 외래키(TEAM_ID)를 매핑한다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
private int age;
// @Column(name = "TEAM_ID")
// private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID") // 매핑
private Team team;
...
}
목표 : 외래키 대신 Team 객체를 넣고 TEAM_ID를 매핑한다..
1. 연관관계를 맺는다(Member N : 1 Team)
@ManyToOne으로 연관관계를 설정한다.
2. 조인할 컬럼을 명시한다.
@JoinColumn(name ="TEAM_ID")
참조값 team이 Team 테이블의 "TEAM_ID"란 외래 키와 매핑된다.
객체 지향 모델링(ORM 매핑)
JpaMain
▶ 저장
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeam(team); //단방향 연관관계 설정, 참조 저장
em.persist(member);
1. member.setTeamId(team.getId()); => member.setTeam(team);으로 변화
▶ 조회
// 조회
Member findMember = em.find(Member.class, member.getId());
// 참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
SELECT 쿼리문을 확인하면 JPA가 조인을 해서 Member와 Team을 함께 가져오는 것을 알 수 있다.
▶ 연관관계 수정
// 새로운 팀B
Team teamB = new Team();
teamB.setName("TeamB");
em.persist(teamB);
// 회원1에 새로운 팀B 설정
member.setTeam(teamB);
setTeam()으로 기존 팀을 새로운 팀으로 바꾸면 연관관계도 변경된다.
Update Query에서 FK가 새로 업데이트된다.
[2] 양방향 연관관계와 연관관계의 주인 1 - 기본
목표 : 객체는 참조, 테이블은 외래 키를 이용한 JOIN을 쓴다. 이 차이를 이해해야 한다.
■ 양방향 매핑
1. 테이블의 연관관계는 바뀐 게 없다. JOIN을 통해 양쪽에서 접근할 수 있으므로 사실상 양방향이다.
2. 객체는 참조로 연관관계를 설정하기 때문에 단방향, 양방향 구분이 있다.
따라서 Team → Member로 가려면 member에 참조를 넣어줘야 한다.
여기선 List Members를 가지고 있도록 설계했다.
Member - 단방향과 동일
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
private int age;
@ManyToOne // N:1
@JoinColumn(name = "TEAM_ID")
private Team team;
...
}
Team - 양방향 적용
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // 1:N
private List<Member> members = new ArrayList<Member>(); // 초기화 (관례) - NullPointerException 방지
...
}
1. 연관관계를 맺는다.
@OneToMany 일대다 관계 설정
2. mappedBy로 team과 연관돼 있다는 것을 명시한다.
이제 반대방향으로도 객체 그래프를 탐색할 수 있다.
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); //역방향 조회
■ 연관관계 주인과 mappedBy
객체와 테이블 간에 연관관계를 맺는 차이를 이해해야 한다.
관계를 맺을 때 객체와 테이블의 차이
① 객체 연관관계 : 단방향 2개
- 객체 연관관계 = 2개
- 회원 -> 팀 연관관계 1개(단방향)
- 팀 -> 회원 연관관계 1개(단방향)
1. 객체의 연관관계는 사실상 양방향이 아니라 서로 다른 단방향 관계 2개다.
2. 객체를 양방향으로 참조하려면 단방향 연관관계 2개를 만들어야 한다.
• A → B (a.getB())
class A {
B b;
}
• B -> A (b.getA())
class B {
A a;
}
② 테이블의 연관관계 : 1개
- 테이블 연관관계 = 1개
- 회원 ↔ 팀 연관관계 1개 (양방향, 사실 방향이 없는 것)
- 외래키 값 하나로 테이블을 조인하면 양쪽의 연관관계가 끝이 난다.
1. 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리한다.
2. MEMBER.TEAM_ID 외래 키(FK) 하나로 양방향 연관관계를 가진다. (양쪽에서 서로 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
■ 둘 중 하나로 외래 키를 관리해야 한다.
1. Member도 Team을 가지고 있고 Team도 Member 참조값을 가지고 있다.
2. 이럴 땐 둘 중 어떤 것과 테이블 외래키를 매핑해야 할까?
3. 그래서 양방향 연관관계에선 외래 키 관리 주체를 명확하게 하기 위해 연관관계의 주인이라는 개념이 생겼다.
■ 연관관계의 주인(Owner)
양방향 매핑 규칙
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
- 연관관계의 주인만 외래 키를 관리한다.
- 주인만 DB에 접근하여 값을 등록, 수정, 변경할 수 있다.
- 주인이 아닌 객체가 값을 변경하더라도 DB에는 영향이 없다.
- 주인이 아닌 쪽은 읽기(SELECT)만 가능하다.
- List Members는 가짜 매핑
누구를 주인으로 해야 할까?
- 외래 키가 있는 곳을 주인으로 정해라.
- DB에서는 N에 해당하는 테이블이 FK를 가지고 있다. (N쪽이 연관관계의 주인)
- 즉, @JoinColumn이 있는 쪽이 주인이다.
- 다시 말해 mappedBy가 있는 쪽은 주인이 아니다.
외래 키가 있는 쪽을 연관관계 주인으로 정하는 이유는 무엇인가?
1] Team에 있는 members를 바꿨는데, (Team은 읽기 전용이므로) JPA에선 Member의 update의 쿼리가 날아간다. 이러면 개발자 입장에선 상당히 헷갈릴 수밖에 없다.
2] 성능 이슈 : Member는 insert 쿼리 한 방에 해결 가능. 반면 Team은 insert 쿼리, Member update 쿼리 두 번 날아간다.
영한님 : 연관관계의 주인은 비즈니스적으론 의미가 없다. 자동차와 바퀴가 있다면 자동차가 중요하지만, 연관관계의 주인은 바퀴다.
[3] 양방향 연관관계와 연관관계의 주인 2 - 주의점, 정리
■ 양방향 매핑 시 가장 많이 하는 실수 - 연관관계의 주인에 값을 입력하지 않음
JpaMain
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("member1");
//역방향(주인이 아닌 방향)만 연관관계 설정
team.getMembers().add(member);
em.persist(member);
JPA에서 mappedBy가 있는 쪽(주인 X)은 update, insert 쿼리 날릴 때 고려하지 않는다.
따라서 연관관계의 주인만 외래키 값을 등록할 수 있다.
■ 양방향 매핑 시 연관관계의 주인에 값을 입력
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);
■ 사실 순수한 객체 관계를 고려하면 양쪽 다 값을 입력해야 한다.
이유
① 1차 캐시 문제
- 위의 코드에서 team도 영속성 컨텍스트에 들어가 있다.
- 연관관계의 주인에만 값 설정해 주면 영속성 컨텍스트에 있는 team 객체의 members 컬렉션은 여전히 비어있는 상태다. (물론 커밋시점에 연관관계의 주인에 의해서 DB에는 업데이트 쿼리 날아간다.)
- 따라서 트랜잭션 안에서 영속성 컨텍스트 flush하고 clear 되기 전에 해당 컬렉션 조회한다면 정상적인 결과가 출력되지 않는다.
② 테스트 케이스 작성 문제
- 또 하나의 문제점은 나중에 테스트 케이스 작성할 때이다.
- 테스트 케이스 작성 시 JPA 없이도 동작하게 순수 자바 코드로도 작성한다.
- 그런 케이스에도 대비하기 위해서는 양쪽 다 입력해 주는 것이 맞다.
결론 : 양방향 연관관계 매핑에선 순수 객체 상태를 고려해 양쪽에 값을 설정하자.
■ Tip
연관관계 편의 메소드를 생성하면 헷갈리지 않게 설정할 수 있다.
양방향 매핑 시 무한 루프에 주의해야 한다.
예시) : toString(), lombok, JSON 생성 라이브러리
해결책
1. Lombok의 @ToString은 웬만하면 안 쓰는 게 루프를 막을 수 있다.
2. JSON 생성 라이브러리 : 컨트롤러에서 엔티티를 절대 반환하지 마라. 값만 넘기는 간단한 dto로 반환하는 게 무한 루프를 방지할 수 있다.
■ 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것뿐이다.
- 생각보다 실무에서는 JPQL에서 역방향으로 탐색할 일이 많음
- 단방향 매핑을 잘하고 양방향 매핑을 필요할 때 추가해도 된다.
(관계형 DB 테이블에 영향을 주지 않는다.)
연관관계 주인 정하는 기준
- 비즈니스 로직을 기준으로 연관관계 주인을 선택하면 안 된다.
- 연관관계의 주인은 외래키의 위치를 기준으로 정해야 한다.
[4] 실전예제 2 - 연관관계 매핑 시작
실습 내역
'백엔드 > JPA' 카테고리의 다른 글
[JPA] [7] 고급 매핑 (0) | 2023.08.11 |
---|---|
[JPA] [6] 다양한 연관관계 매핑 (0) | 2023.08.10 |
[JPA] [4] 엔티티 매핑(하편) - 기본키 매핑 (0) | 2023.08.06 |
[JPA] [4] 엔티티 매핑(상편) (0) | 2023.08.05 |
[JPA] [3] 영속성 관리 - 내부 동작 방식 (0) | 2023.08.04 |