출처 - 인프런 김영한님 자바 ORM 표준 JPA 프로그래밍 - 기본편
인강의 내용이 있기 때문에 출처를 밝힙니다.
코드스쿼드 마지막 프로젝트를 진행한다고 학습을 미루다 2주만에 다시 복습을 시작했다 !
기록 시작 ~
연관관계 매핑에 대해
JPA 를 사용해서 관련있는 엔티티들끼리 연관관계를 매핑에 사용하게 된다.
연관관계가 필요한 이유에 대해 먼저 간단하게 알아보자.
1. DB 테이블에 맞춘 설계 방식 ?
아래와 같은 회원, 팀 엔티티가 있다고 하자.
회원은 자신이 몇 번 팀에 속해져있는지 알기 위해, 팀의 PK 를 자신의 FK 로 가지고 있는 상황이다.
1. 회원 엔티티
@Entity
public class Member {
@Id
@GeneratedValue
private Long id;
@Column
private String name;
@Column(name = "team_id")
private Long teamId;
}
2. 팀 엔티티
@Entity
public class Team {
@Id
@GeneratedValue
private Long id;
private String name;
}
여기서 팀과 회원을 저장해보자.
팀을 먼저 생성하고 회원을 생성하면서 회원의 FK 값인 팀 아이디 값에 미리 생성한 팀의 PK 값을 넣어준다.
그리고 영속화 하면 끝.
Team team = new Team();
team.setName("teamA");
// Team 을 영속화 하면서 PK 값 자동 생성
em.persist(team);
Member member = new Member();
member.setName("memberA");
member.setTeamId(team.getId());
em.persist(member);
여기까지는 문제가 없다고 생각할 수 있지만, 엔티티에 FK 값이 직접 들어가있다면 조회 시 굉장히 불편하다.
회원이 속한 팀을 찾는다고 하면, 회원을 먼저 찾고, 그 회원의 FK 을 가지고 팀을 찾아내야 하기 때문이다.
Member findMember = em.find(Member.class, member.getId());
Team findTeam = em.find(Team.class, findMember.getTeamId());
2. 테이블에 맞춘 설계에서 왜 이런 문제가 발생할까
간단하게 말하면, 객체 그래프 탐색이 가능하지 않아 생기는 문제이다.
즉, 테이블 간의 연관관계는 존재하지만 객체끼리의 어떠한 참조가 존재하지 않기 때문인데, 위 예제의 엔티티 설계 스타일에서는 객체
자체를 가져와 참조 값을 사용하는게 아닌, 객체의 FK 값만을 가져오기 때문에 객체 그래프 탐색이 불가한 것이다.
단방향 연관관계
앞서 살펴봤듯 참조하거나 관련있는 객체끼리는 연관관계를 설정해야하는데 JPA 에서는 단방향, 양방향 연관관계가 존재한다.
먼저 단방향 연관관계부터 알아보자.
단방향이란 말 그대로 한 쪽 엔티티에만 연관관계를 심어주는 매핑 방법이다.
예제를 통해 어떻게 설정하는지, 또 DB 테이블 위주 설계와 어떤 점이 다른지 알아보자.
1. 단방향 연관관계 적용 예제
이전의 회원 엔티티에서는 팀의 FK 값을 직접적으로 지니고 있었는데, 단방향 연관관계를 적용해 팀 엔티티 자체를 지니도록 변경했다.
회원과 팀은 다대일 관계이므로 @ManyToOne 을 사용했고, @JoinColumn 의 name 속성을 사용해 회원 엔티티에서 참조하는
팀의 FK 컬럼 명을 정의해줬다. @JoinColumn 은 본인이 FK 를 관리한다고 생각하면 된다.다만 여기서 주의할 점이 있는데 ..
예전에 @JoinColumn 의 name 속성은 팀 엔티티의 PK 값 컬럼명 (그러니까 팀 테이블에 자동으로 생성되는 컬럼명)과 동일하게
설정해야 한다고 생각했었는데, 그게 아니라 단순히 회원 엔티티의 FK 값 컬럼명을 지정해주는 것이였다.
즉, name 을 team_id 로 설정하든 뭐로 설정하든 상관이 없다.
만약 name 을 team_id 로 설정하면, 회원 엔티티에서 FK 를 관리하며 해당 FK 컬럼 명을 team_id 로 한다 가 되는 것.
@JoinColumn 의 name 속성이 단순히 컬럼명을 지정해주는 거라면, 회원 쪽은 어떻게 팀과 연관관계를 맺고 있다는 것을 알 수 있을까 ?
엔티티끼리 조인을 하기 위해선 대상 테이블의 컬럼이 있어야하는데, 이게 바로 referencedColumnName속성이다.
멀쩡하게 사용할 수 있는 속성이 있는데 사용하지 않는 이유는, referencedColumnName 을 생략하면 대상 테이블의 PK로 자동 지정
되기 때문이다. 즉, Member.teamId(FK) -> Team.teamId(PK) 를 조인하며 알아서 연관관계가 처리된다.
1. 이전 회원 엔티티 코드
... 중략
@Column(name = "team_id")
private Long teamId;
... 중략
-----
2. 단방향 연관관계를 적용한 회원 엔티티 코드
... 중략
@ManyToOne
@JoinColumn(name = "team_id, referencedNameColumn = "생략")
private Team team;
... 중략
2. DB 테이블에 맞춘 기존의 설계 방식과의 차이점
대상 엔티티를 그대로 참조하고 있기 때문에 조회 시 객체 그래프 탐색이 가능하다 !
회원이 지닌 FK 값을 찾아 팀을 찾는게 아닌, 회원에서 바로 팀을 조회하는 것이 가능한 것.
Team team = new Team();
team.setName("teamB");
em.persist(team);
Member member = new Member();
member.setName("memberB");
member.setTeam(team);
em.persist(member);
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
양방향 연관관계
단방향 연관관계를 살펴봤으니, 양방향 연관관계에 대해 알아보자.
현재 단방향 연관관계애서는 회원에 팀이 존재하기 때문에 회원에서 팀을 조회할 수는 있지만 그 역은 불가능한 상태다.
팀에서 또한 회원을 조회하고 싶다면 팀 엔티티에도 회원을 추가해주면 되는데, 이게 바로 양방향 연관관계인 것.
1. 양방향 연관관계 적용 예제
팀 관점에서 회원은 일대다 관계이기 때문에 @ManyToOne 의 반대인 @OneToMany 를 사용한다.
또한 팀은 여러 회원을 가지기 때문에 List<> 의 반환타입을 갖고 관례에 따라 ArrayList 타입으로 초기화해준다.
1. 팀 엔티티에서 양방향 연관관계 적용
... 중략
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
... 중략
2. mappedBy 는 왜 사용 ?
두 엔티티가 양방향 관계를 맺는다면, 연관 관계의 주인을 지정해야하는데, 그 때 사용하는 옵션이 mappedBy 옵션이다.
쉽게 말해 FK 값을 비롯한 테이블의 컬럼을 저장, 수정 삭제 권한을 갖는 주인 엔티티를 지정하는 옵션이라고 할 수 있다.
그냥 매핑만 하면 될 텐데 .. 왜 굳이 연관 관계의 주인을 지정해줘야 할까 ? 이 부분에 있어서 처음에는 정말 이해할 수 없었다.
요점은 FK 가 있는 곳을 주인으로 정하고 주인이 아닌 곳에서 mappedBy 속성을 사용해 명시해야 한다.
조금 더 자세히 알아보자.
3. 왜 연관관계 주인을 지정해야 하나 ?
이걸 이해하기 위해선 우선 엔티티끼리 관계를 맺는 방법, 테이블끼리 관계를 맺는 방법에 대한 차이를 알아야한다.
우선 엔티티의 양방향 연관관계은 서로 참조하는 단방향 연관관계 2개로 이뤄진다.
하지만 테이블의 연관관계에서는 FK 값 하나로 서로를 참조할 수 있기 때문에 하나의 양방향 연관관계 1개로 이뤄진다.
즉, 엔티티는 양 쪽 모두에서 FK 값을 관리할 수 있다. 그럼 의문점이 생기게 되는데 ..
객체의 참조는 둘인데, FK 값은 하나기 때문에 어느 쪽에서 FK 를 관리할 것인가 ? 에 대한 의문이다.
회원을 수정하는 상황이 있을 때, 회원 쪽에서 직접 수정해야할까, 팀 쪽에서 회원 리스트를 가져와 수정해야할까 ?
객체 입장에선 두 방법 다 상관없는 방법이다. DB 입장에서도 엔티티 참조가 뭐든 FK 값만 잘 업데이트 되면 그만이고.
하지만 직접 DB 와 소통하는 JPA 입장에서는 혼란이 있을 수 있기 때문에 두 엔티티 사이의 연관관계 주인을 명확하게 명시해서
회원에서 팀을 생성, 수정, 삭제 할 때만 FK 값을 업데이트 하겠다 라는 룰을 정한 것이다.
4. 양방향 매핑 규칙
가. 객체의 두 관계 중 FK 값이 존재하는 쪽을 연관관계의 주인으로 지정한다.
나. 연관관계의 주인만이 생성, 수정, 삭제의 권한을 가진다.
다. 주인이 아닌 쪽은 조회만 가능하다.
라. 주인은 mappedBy 를 사용하지 않고, 주인이 아닌 쪽에서 mappedBy 를 사용해 주인이 아님을 명시한다.
5. 주인이 아닌 곳에서 값을 생성, 수정, 삭제를 할 수 있을까 ?
아래 예제의 결론으로 회원 테이블 FK 값에는 NULL 이 들어간다.
일대다 또는 다대일의 관계에서 FK 는 (다) 쪽에 존재하는데, 지금 예제에서 회원이 (다) 이기 때문에 FK 를 가지고 있고
양방향 매핑 규칙에서도 언급했듯 연관관계의 주인이 된다. 아래 예제 코드를 간단히 해설해보자면,
가. 회원을 생성해 영속성 컨텍스트 1차 캐시에 등록
나. 팀을 생성하고 팀이 가지고 있는 회원 리스트에 앞서 생성한 회원을 삽입
다. 팀을 영속성 컨텍스트 1차 캐시에 등록
이게 당연히 NULL 일 수 밖에 없는 이유가 팀이 생성된 후 영속성 컨텍스트 1차 캐시에 등록되면서 PK 값이 나오고 이 PK 값을 회원의
FK 값에 등록해야 하는데 이 과정이 빠져있기 때문이다.
당연히 FK 값을 가지고 있는 회원 쪽에서 FK 값 셋팅이 안됐기 때문에 회원 테이블에 값이 들어갈리가 없다.
Member member = new Member();
member.setName("memberA");
em.persist(member);
Team team = new Team();
team.setName("teamA");
team.getMembers().add(member);
em.persist(team);
6. 연관관계의 주인 쪽에서 값을 셋팅한 예제
그렇다면 FK 를 관리하는 연관관계 주인 쪽에서 값을 셋팅해보자.
하지만 이렇게 수정해도 문제가 있는데 바로 팀의 회원 리스트에 아무것도 없다는 것이다. 이것도 어찌보면 당연한게 회원에는 팀을
저장했지만 반대로 팀에서는 회원 리스트에 아무것도 저장하는 로직이 없기 때문이다.
Team team = new Team();
team.setTeam("teamA");
em.persist(team);
Member member = new Member();
member.setName("memberA");
// JPA 에서 자동으로 회원의 FK 값을 업데이트 한다.
member.setTeam(team);
em.persist(member);
7. 순방향, 역방향 값을 모두 셋팅한 예제
주인이 아닌 곳에서는 조회만 할 수 있다더니 예제 코드에 떡하니 team.getMembers().add(member); 코드가 적혀져있다.
DB 만 생각했을 땐 FK 값을 잘 업데이트하면 그만이라 연관관계 주인 쪽에만 값을 넣어도 상관없지만, 객체 입장은 좀 다르다.
사실 요 부분은 정확하게 이해를 못했다. 서로 참조하는 값 쪽에 모두 값이 잘 들어가 있어야 순수한 객체 상태를 유지한다 라고 강의에
나와있긴 한데 ,, 좀 더 공부해야봐야 할 것 같다. 일단 이렇게 팀의 회원 리스트에도 값을 넣어주면 찍었을 때 값이 잘 나오긴 한다.
Team team = new Team();
team.setName("TeamA");
// 1차 캐시에 Team 틍록
// 현재 Team 의 Member List 는 값이 없는 상태
em.persist(team);
Member member = new Member();
member.setName("memberA");
member.setTeam(team);
// 1차 캐시에 Member 등록
// 위 로직으로 인해 Member 의 Team 은 값이 있는 상태
em.persist(member);
// 순수 객체 상태를 위해 역방향에도 값을 셋팅해줘야 한다.
team.getMembers().add(member);
System.out.println(Arrays.toString(team.getMembers());
8. 연관관계 편의 메서드 사용
순수 객체 관점에서 양쪽의 값 모두를 입력하는게 안전하다고 했다. 하지만 개발자는 기계가 아닌지라 실수를 하기 마련.
따라서 member.setTeam(team), team.getMembers().add(member) 부분을 한 번에 처리할 수 있도록 메서드를 만들어 사용하면
실수도 줄일 수 있고 편하게 사용할 수 있다.
메서드는 양 쪽 어디서든 만들어도 상관없다. 상황에 따라 어디 만들지 고려하자.
가. 회원 쪽에서 편의 메서드 생성
자신이 참조하고 있는 팀에 받아온 팀을 넣어주고, 받아온 팀 쪽에 존재하는 회원 리스트에 자신을 넣어준다.
나. 팀 쪽에서 편의 메서드 생성
가져온 회원 쪽에서 참조하고 있는 팀에 가져온 회원을 넣어주고, 이렇게 셋팅된 멤버를 팀이 참조하고 있는 회원 리스트에 추가한다.
Member Entity 에서 편의 메서드를 만든다면 ?
... 중략
// 꼭 Setter 가 아니여도 된다.
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
... 중략
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("MemberA");
member.setTeam(team);
em.persist(member);
-----------
Team Entity 에서 편의 메서드를 만든다면 ?
... 중략
public void setMember(Member member) {
member.setTeam(this);
members.add(member);
}
... 중략
Team team = new Team();
team.setName("TeamA");
em.persist(team);
Member member = new Member();
member.setName("MemberA");
em.persist(member);
team.setMember(member);
9. 양방향 연관관계 사용 시 주의점
양방향 연관관계를 사용할 때는 객체가 객체를 참조하고 있기 때문에 항상 무한루프를 주의해야 한다.
대표적으로 toString(), Lombok, JSON 생성 라이브러리에서 무한루프가 발생할 수 있다. (StackOverflow)
toString 을 사용할 때는 그대로 사용하지 말고 객체 부분은 빼고 사용해야 무한루프를 막을 수 있고, JSON 무한루프 같은 경우는 엔티티 자체를 컨트롤러에서 Response 해줄 때 무한루프가 발생하는데 이는 DTO 를 사용하면 막을 수 있다.
무한루프를 막는 것도 막는거지만 엔티티 자체를 컨트롤러에서 반환하면 안된다 .. 꼭 DTO 로 변환해 반환하도록 하자.
10. 양방향 연관관계 정리
가. 단방향 매핑으로도 모든 설계를 마칠 수 있다. 역방향 조회가 필요한 경우에만 양방향 매핑을 고려하도록 하자.
나. 양방향 연관관계를 건다고 DB 테이블에 영향을 주는건 아니다.
다. 연관관계의 주인은 꼭 외래 키 위치를 기준으로 정해야 한다.
'JPA' 카테고리의 다른 글
[인강 복습] JPA 기본편 다양한 연관관계 매핑 (#5) (0) | 2022.07.06 |
---|---|
[인강 복습] JPA 기본편 요구사항 분석과 기본 매핑 까지 (#3) (0) | 2022.06.16 |
[인강 복습] JPA 기본편 영속 컨텍스트 까지 (#2) (0) | 2022.06.12 |
[인강 복습] JPA 기본편 JPA 시작하기 까지 (#1) (0) | 2022.06.10 |