2023. 02.07 13:00 복습 시작
연관관계
엔티티들은 대부분 다른 엔티티와 연관관계가 있다.
객체는 참조(주소)를 사용해서 관계를 맺고 테이블은 외래 키를 사용해서 관계를 맺는다.
이 둘은 완전히 다른 특징을 가지며,
객체 관계 매핑(ORM)에서 가장 어려운 부분이 바로 객체 연관관계와 테이블의 연관 관계를 매핑하는 일이다.
이제 객체의 참조와 테이블의 외래 키를 매핑하는 방법을 알아보겠다.
알아둬야 할 용어들이 있다.
- 방향 : 단방향과, 양방향이 있다.
한쪽만 참조하는 것을 단방향, 양 쪽 모두 서로 참조하는 것을 양방향이라고 한다. - 방향은 객체 관계에만 존재하고, 테이블은 항상 양방향이다.
- 다중성: 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
- 연관관계의 주인 : 객체 양방향 연관관계는 관리 주인이 필요하다.
단방향 연관관계
먼저 다대일 단방향 관계를 공부해보자. 회원과 팀의 예제를 들어보겠다.
- 회원과 팀이 있다.
- 회원은 한 팀에만 소속될 수 있다. 팀은 다수의 회원을 가진다.
- 회원과 팀은 다대일 관계다.
위의 그림을 통해 예시를 더욱 명확하게 이해할 수 있다. 객체 연관관계와 테이블 연관관계에 대해 분석해보자.
객체 연관관계
- 회원 객체는 Member.team필드로 팀 객체와 연관관계를 맺는다.
- 회원 객체와 팀 객체는 단방향 관계다. 회원은 Member.team 필드를 통해서 팀을 알 수 있지만 반대로 팀은 회원을 알 수 없다.
테이블 연관관계
- 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺는다.
- 회원 테이블과 팀 테이블은 양방향 관계다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있다.
이제 JPA를 사용해서 이 둘을 매핑해보자. 아래 코드는 회원 클래스다.
@Entity
public class Member{
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
//연관관계 설정
public void setTeam(Team team){
this.team = team;
}
//...
}
아래 코드는 팀 클래스다.
@Entity
public class Team{
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
//...
}
연관관계 매핑을 위한 새로운 어노테이션이 등장했는데 이에 대해 살펴보자.
- @ManyToOne : 이름 그대로 다대일 관계라는 매핑 정보다. 연관관계를 매핑할 때 이렇게 다중성을 나타내는 어노테이션은 필수다.
- @JoinColumn(name="TEAM_ID") : 조인 컬럼은 외래 키를 매핑할 때 사용한다.
name 소성에는 매핑할 외래 키 이름을 지정한다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값으로 지정.
이 어노테이션은 생략이 가능하다.
양방향 연관관계
이전 공부할 때 사용한 예시는 회원에서 팀으로만 접근하는 다대일 단방향 매핑이었다.
이번에는 회원에서 팀으로 접근 가능하고, 팀에서 회원으로도 접근 가능한 다대일 양방향 매핑에 대해 알아보겠다.
먼저 객체 연관관계를 살펴보자.
- 회원과 팀은 다대일 관계다. 반대로 팀에서 회원은 일대다 관계다.
일대다 관계는 여러 건과 연관관계를 맺을 수 있으므로 컬렉션을 사용해야 한다. Team.members를 List 컬렉션으로 추가했다.
테이블의 관계는 어떨까?
- 데이터베이스 테이블은 외래 키 하나로 양방향으로 조회할 수 있다. 사실 이전 단방향과 차이점이 없다.
이제 양방향 연관 관계를 매핑한 코드를 살펴보겠다.
먼저 회원 엔티티 코드다. 회원 엔티티는 변경한 부분이 없다.
@Entity
public class Member{
@Id
@Column(name = "MEMBER_ID")
private String id;
private String username;
//연관관계 매핑
@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;
//연관관계 설정
public void setTeam(Team team){
this.team = team;
}
//...
}
다음은 팀 엔티티다. 변경된 코드를 확인할 수 있다.
@Entity
public class Team{
@Id
@Column(name = "TEAM_ID")
private String id;
private String name;
//추가된 부분
@OneToMany(mappedBy = "team")
private List<Member> members= new ArrayList<Member>();
//..
}
팀과 회원은 일대다 관계다. 따라서 팀 엔티티에 컬렉션인 List<Member> members를 추가했다.
그리고 일대다 관계를 매핑하기 위해 @OneToMany 매핑 정보를 사용했다.
mappedBy 속성은 양방향 매핑일 때 사용하는데 반대쪽 매핑의 필드 이름을 값으로 주면 된다.
반대쪽 매핑이 Member.team이므로 team을 값으로 주었다.
연관관계의 주인
@OneToMany만 있으면 되지 mappedBy는 왜 필요할까??
사실 객체에는 양방향 연관관계라는 것이 없다.
서로 다른 단방향 연관 관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다.
객체 연관관계는 즉 다음과 같다.
- 회원 -> 팀 연관 관계. 단방향 1개
- 팀 -> 회원 연관 관계. 단방향 1개
테이블 연관관계는 아래와 같다.
- 회원 < - > 팀 연관관계. 양방향 1개
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 둘 사이에서 차이가 발생한다.
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래 키를 관리해야 하는데, 이것을 연관관계의 주인이라 한다.
양방향 연관관계 매핑 시 지켜야 할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 한다.
연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래키를 관리할 수 있다.
반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 된다.
즉 양방향 매핑 관계의 규칙은 아래와 같이 정리할 수 있다.
- 객체의 두 관계 중 하나를 연관관계의 주인으로 지정한다.
- 연관관계의 주인만이 외래 키를 관리
- 주인이 아닌 쪽은 읽기만 가능
- 주인은 mappedBy 속성 사용 불가
- 주인이 아니면 mappedBy 속성으로 주인을 지정한다.
그렇다면 Member.team, Team.members 둘 중 어떤 것을 연관관계의 주인으로 정해야 할까?
연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것이다.
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다.
여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
주인이 아닌 Team.members에는 mappedBy="team" 속성을 사용해서 주인이 아님을 밝힌다.
정리하면 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있다.
주인이 아닌 반대편은 읽기만 가능하고 외래 키를 변경하지는 못한다.
양방향 연관관계의 주의점
양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관 관계의 주인에는 값을 입력하지 않고,
주인이 아닌 곳에만 값을 입력하는 것이다. 아래 코드를 살펴보자.
public void testSaveNonOwner(){
//회원1 저장
Member member1 = new Member("member1", "회원1");
em.persist(member1);
//회원2 저장
Member member2 = new Member("member2", "회원2");
em.persist(member2);
Team team1=new Team("team1", "팀1");
//주인이 아닌 곳에만 연관관계 설정
team1.getMembers().add(member1);
team1.getMembers().add(member2);
em.persist(team1);
}
회원1, 회원2를 저장하고 팀의 컬렉션에 담은 후에 팀을 저장했다. 데이터베이스에 회원 테이블을 조회해보면
외래 키 TEAM_ID에 team1이 아닌 null 값이 입력되어 있는데,
다시 한번 강조하지만 연관관계의 주인만이 외래 키의 값을 변경할 수 있다.
위 코드는 주인인 Member.team에 아무 값도 입력하지 않았다. 따라서 TEAM_ID 외래 키의 값도 null이 저장된다.
그렇다면 정말 연관관계의 주인에만 값을 저장하고 주인이 아닌 곳에는 값을 저장하지 않아도 될까?
사실은 객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 가장 안전하다.
만약 양쪽 모두에 저장하지 않는다면 JPA를 사용하지 않는 순수한 객체 상태에서 문제가 발생할 수 있다.
아래 코드는 위에 연관관계의 주인에 값을 넣지 않은 부분을 수정한 코드다.
public void testSaveNonOwner(){
//팀 1 저장
Team team1=new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
//회원1 저장
member1.setTeam(team1);
team1.getMembers().add(member1);
em.persist(member1);
Member member2 = new Member("member2", "회원2");
//회원2 저장
member2.setTeam(team1);
team1.getMembers().add(member2);
em.persist(member2);
}
위 코드처럼 객체의 양방향 연관관계는 양쪽 모두 관계를 맺어줘야 한다.
연관관계 편의 메서드
양방향 연관관계는 결구 양쪽 다 신경 써야 한다.
위 코드처럼 member.setTeam(team1)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 둘 중 하나만 호출해서 양방향이 깨질 수 있다.
양방향 관계에서 두 코드는 하나인 것처럼 사용하는 것이 안전하다.
그러므로 양쪽 모두 관계를 맺어주는 하나의 메서드를 만들면 편하다. 이를 연관관계 편의 메서드라고 부른다.
Member 클래스를 리팩토링 해보자.
public class Member{
private Team team;
public void setTeam(Team team){
this.team=team;
team.getMembers().add(this);
}
}
이러면 위에 연관관계 값을 넣어주는 코드를 더욱 깔끔하게 바꿀 수 있다.
public void testSave_양뱡향_리팩토링(){
//팀 1 저장
Team team1=new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
Member.setTeam(team1); //양방향 설정
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team1);
em.persist(member2);
}
정리
- 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었다.
- 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가된다.
- JPQL에서 역방향으로 탐색할 일이 많다.
- 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 한다.
- 양방향 매핑 시에 무한 루프를 조심하자. ex toString(), lombok, JSON 생성 라이브러리
- 연관관계를 사용하려면 로직을 정말 견고하게 작성해야 한다.
- 단방향 매핑을 잘하고 양방향은 필요할 때 추가해도 된다.(테이블에 영향을 주지 않기 때문이다.)
- 연관관계의 주인은 외래 키가 있는 곳 즉, 다대일 & 일대다에서는 늘 '다'가 주인이다.
참고자료
자바 ORM 표준 JPA 프로그래밍 - 김영한
자바 ORM 표준 JPA 프로그래밍 강의 - https://www.inflearn.com/course/ORM-JPA-Basic/dashboard
2023. 02.07 14:00 복습 및 정리 마무리
댓글