목표
- 객체와 테이블 연관관계의 차이를 이해
- 객체의 참조와 테이블의 외래 키를 매핑
- 용어 이해
- 방향 (Direction) : 단방향, 양방향
- 다중성 (Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:N) 이해
- 연관관계의 주인 (Owner) : 객체 양방향 연관관계는 관리 주인이 필요
예제 시나리오
- 회원과 팀이 있다.
- 회원은 하나의 팀에만 소속될 수 있다.
- 회원과 팀은 다대일 관계다.
객체를 테이블에 맞추어 모델링 (연관관계가 없는 객체)
객체를 테이블에 맞추어 모델링 (참조 대신에 외래 키를 그대로 사용)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
@Column(name = "USERNAME")
private String name;
@Column(name = "TEAM_ID")
private Long teamId;
...
}
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
...
}
test code
//팀 저장
Team team = new Team();
team.setName("TeamA");
em.persist(team);
//회원 저장
Member member = new Member();
member.setName("member1");
member.setTeamId(team.getId());
em.persist(member);
결과
테이블에 잘 저장되고 Join도 잘 동작하는 것을 볼 수 있습니다.
객체를 테이블에 맞추어 모델링 (식별자로 다시 조회, 객체 지향적인 방법이 아니다.)
이전에 보았던 것처럼 DB를 통해서 계속해서 끄집어 내야한다.
객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
- 테이블은 외래키로 조인을 사용해서 연관된 테이블을 찾는다.
- 객체는 참조를 사용해서 연관된 객체를 찾는다.
- 테이블과 객체 사이에는 이러한 간격이 존재한다.
단방향 연관관계
객체 지향 모델링 (객체 연관관계 사용)
변경된 코드
//@Column(name = "TEAM_ID")
//private Long teamId;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
- @ManyToOne
- Member N : Team 1 이기 때문에 Member 객체에선 Team과 연관되는 컬럼에는 본인이 Many 팀이 One이라는 어노테이션을 추가해주어야 합니다. (연관관계 사용할 때 이러한 일대일, 다대일, 일대다, 다대다 관계의 어노테이션을 추가해야함)
- @JoinColumn
- 조인해야하는 컬럼이 어떤 컬럼인지 명시해주는 어노테이션입니다. DB에 실제 존재하는 값을 찾아서 매핑합니다.
이렇게 관계와 join 컬럼만 추가해주면 끝입니다.
객체 지향 모델링
조회
//조회
Member findMember = em.find(Member.class, member.getId());
//참조를 사용해서 연관관계 조회
Team findTeam = findMember.getTeam();
Member 객체를 가져온 뒤, getTeam() 메소드만 호출하면 원하는 연관된 Team 객체를 받아올 수 있습니다. (다시 DB를 조회할 필요가 없음)
양방향 연관관계와 연관관계의 주인
양방향 매핑
이전엔 멤버 -> 팀 으로만 가능했다면 양방향 연관관계는 멤버 <-> 팀 으로 이동이 가능하게 매핑하는 것 입니다.
코드
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
Team 객체에 해당 컬럼만 추가해주면 됩니다.
OneToMany는 일대다 관계라는 것을 프로그램에게 알려 주는 것이고, mappedBy는 Member객체에 있는 team이라는 변수와 매핑이 된다는 것을 나타내는 것 입니다.
테스트
//조회
Team findTeam = em.find(Team.class, team.getId());
int memberSize = findTeam.getMembers().size(); //역방향 조회
위의 테스트를 해보았는데 문제가 발생했다.
기존 데이터를 em.flush()를 통해 DB에 저장한 뒤, 탐색하면 정상적으로 데이터를 가져올 수 있을 거라고 생각했지만 그렇지 못했습니다.
em.flush후 em.clear까지 해줘야지 정상적으로 데이터를 갖고올 수 있었습니다.
em.flush후 em.clear를 해야하는 이유는? em.flush를 하면 DB에 데이터가 저장이 됩니다. 그렇기 때문에 DB에 데이터를 가져올 것이라고 생각했지만, 영속성 컨텍스트에 가져오려는 데이터가 있기때문에 DB에 저장된 데이터가 아닌 영속성 컨텍스트에 있는 데이터를 가져옵니다. 여기서 문제점은 영속성 컨텍스트에 있는 Team 테이블에 members 컬럼에는 아무것도 없습니다. DB에서 조회를 하여 가져올 때는 @OneToMany를 통해 DB에서 연관된 테이블을 같이 가져오지만, 영속성 컨텍스트에 있는 Team 테이블은 그렇지 않기 때문입니다. 그렇기 때문에 em.clear를 통해 영속성 컨텍스트를 초기화 시켜준 뒤, 원하는 객체를 DB로부터 받아올 수 있도록 해줘야합니다.
연관관계의 주인과 mappedBy
- mappedBy = JPA의 멘탈붕괴 난이도
- mappedBy는 처음에 이해하기 어렵다.
- 객체와 테이블간에 연관관계를 맺는 차이를 이해해야 한다.
객체와 테이블이 관계를 맺는 차이
- 객체 연관관계 = 2개
- 회원 -> 팀 연관관계 단방향 1개
- 팀 -> 회원 연관관계 단방향 1개
- 테이블 연관관계 = 1개
- 회원 <-> 팀 연관관계 양방향 1개
테이블의 경우 fk 값 하나로 테이블의 연관관계가 끝이 납니다. 하지만 객체의 경우 참조가 객체마다 있어야합니다.
사실상 객체의 양방향 관계는 서로 다른 단방향 관계 2개입니다.
한명의 멤버가 팀을 바꾸려고 할 때 테이블에서는 fk 값만 바꿔주면 끝이납니다. 하지만, 객체의 경우 Member에 있는 team을 바꿔야할 지, Team에 있는 members를 바꿔야할지 딜레마에 빠지기 때문입니다.
그렇기때문에 객체를 매핑할 때 테이블이 fk 값하나로 연관관계를 맺는 것처럼 객체의 참조도 하나일 필요가 있습니다.
그래서 결론은 "객체도 둘 중 하나로 외래 키를 관리해야 한다"로 됩니다. 이 개념이 바로 연관관계의 주인입니다.
연관관계의 주인
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정
- 연관관계의 주인만이 외래 키를 관리 (등록, 수정)
- 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 속성 사용 X
- 주인이 아니면 mappedBy 속성으로 주인 지정
이전에 짰던 코드를 비유하면 연관관계의 주인은 Member의 team입니다.
누구를 주인으로?
- 외래 키가 있는 곳을 주인으로 정해라
- 여기서는 Member.team이 연관관계의 주인
외래 키가 있는 곳을 주인으로 정하는 이유는 아래와 같다.
- 예를들어, Team.members를 외래키로 정했다고 가정해보자. members의 값을 변경하게되면 연관된 다른 테이블의 업데이트 쿼리도 나가게되는데 이렇게되면 헷갈리게 된다.
- 성능 이슈
양방향 연관관계와 연관관계의 주인 - 주의점, 정리
양방향 매핑시 가장 많이 하는 실수 (연관관계의 주인에 값을 입력하지 않음)
실제 코드만 보면 아무런 문제가 없어보이는 코드입니다. member 객체를 만들었고, team에 member를 추가해주었습니다. 한번 더 기억을 더듬어 보자면 저희는 이전에 mappedBy로 연관관계의 주인을 Member로 설정했습니다. 또 등록, 수정이 member를 통해서만 이루어진다고 말했었습니다. 하지만, 해당 코드에서 등록을 연관관계의 주인이 아닌 역방향으로 했습니다. 결과는 어떨까요??
위를 보다시피 매핑이 정상적으로 이루어지지 않은 모습을 볼 수 있습니다.
다시 한번 기억합시다! 연관관계 주인에 설정을 해야합니다.
그렇다면 연관관계의 주인에만 설정을 하는 것이 맞는 걸까요?? 이것도 무언가 객체지향스럽지 못합니다. 이유는 데이터를 넣지도 않았는데 이미 데이터는 들어가있기 때문입니다. 이것은 아래와 같은 두가지에서 문제가 생깁니다.
- em.clear()를 하지 않은 경우 (위에 해당의 문제는 설명한 바 있습니다. 간단하게 설명하자면 영속성 컨텍스트에서 데이터를 가져오기 때문입니다. 이해되지 않는 분들은 em.clear를 하고 안하고의 차이를 주석처리해보며 직접 느껴보시길 추천합니다.)
- 테스트케이스를 작성할 때, member.getTeam은 되지만 team.getMembers는 동작하지 않음.
위의 이유로 "양쪽 다 데이터를 세팅하는 것이 맞는 방법이다"라고합니다.
양방향 연관관계 주의 - 실습
- 순수 객체 상태를 고려해서 항상 양쪽에 값을 설정하자
- 연관관계 편의 메소드를 생성하자
- 양방향 매핑시에 무한 루프를 조심하자
- 예) toString(). lombok, JSON 생성 라이브러리
연관관계 편의 메소드
연관관계 편의 메소드를 생성해 한번의 메소드로 양쪽에 값을 설정해줍니다. 위 코드는 setTeam -> changeTeam을 통해 양쪽의 연관관계를 설정해주는 코드입니다. 양쪽 연관관계를 맺는 방법은 다양한 방법이 있으니 편한 방법대로 해주시면됩니다.
양방향 매핑시 무한 루프
양방향 연관관계에서 두 객체 모두 toString을 생성한 뒤, 한쪽 객체의 toString을 사용하는 경우 member <-> team을 계속해서 부르기 때문에 stackoverflow가 발생하게 됩니다. (toString하는 경우 한쪽 방향만 이용하던가, 양방향 연관하는 컬럼의 경우 제외해주어야 합니다.) 특히, lombok으로 toString 쓰는 경우 보이지 않기때문에 조심해야합니다.
양방향 매핑 정리
- 단방향 매핑만으로도 이미 연관관계 매핑은 완료
- 양방향 매핑은 반대 방향으로 조회(객체 그래프 탐색) 기능이 추가된 것 뿐
- JPQL에서 역방향으로 탐색할 일이 많음
- 단방향 매핑을 잘 하고 양방향은 필요할 때 추가해도 됨 (테이블에 영향을 주지 않음)
김영한님의 추천으로 단방향 매핑으로 설계를 완료하고 필요에 의해서 탐색하는 경우에만 조회용으로만 양방향으로 사용하는 것을 권장한다고 합니다.
연관관계 매핑 시작
테이블 구조
객체 구조
출처
'spring > 인프런 강의 정리' 카테고리의 다른 글
[JPA 기본편] 7. 고급 매핑 (0) | 2022.03.11 |
---|---|
[JPA 기본편] 6. 다양한 연관관계 매핑 (0) | 2022.03.09 |
[JPA 기본편] 4. 엔티티 매핑 (0) | 2022.03.05 |
[JPA 기본편] 3. 영속성 관리 - 내부 동작 방법 (0) | 2022.03.05 |
[JPA 기본편] 2. JPA 시작하기 (0) | 2022.02.21 |