위 테이블이 있다고 하자.
테이블 연관관계만 보고 클래스를 작성하면 아래와 같이 될 것이다.
@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;
...
}
참조를 만들지 않고 외래키인 teamId를 그대로 작성한 것이다.
이 클래스에서 어떻게 Member와 Team의 협력을 만들수 있을까?
외래키인 teamId는 그저 Long인 숫자일 뿐이다.
아마 Member의 teamId를 가지고 Team를 전부 뒤져야 할 것이다.
이처럼 객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.
테이블은 외래 키로 조인을 사용해서 연관된 테이블을 찾는다.
객체는 참조를 사용해서 연관된 객체를 찾는다.
그래서 연관관계를 매핑해주기 위해 참조에 사용되는 키에 어노테이션을 붙혀준다.
테이블의 연관관계에 따라
@ManyToOne, @OneToMany, @ManyToMany, @OneToOne
의 어노테이션을 알맞게 붙혀준다.
위와 같이 매핑된다.
양방향이라고 거창한 것은 아니다.
위는 Member -> Team 으로 단방향 연관관계만 매핑되어 있다.
Member에서 Team의 참조를 저장하고 있기 때문에 Member에서 Team 으로 조회가 가능하지만, Team에서 Member의 정보는 아직 알 수 없다.
이를 위해 Team -> Member로 단방향 연관관계를 설정해주면 양방향 연관관계가 되는 것이다.
Team과 Member는 일대다 관계로 Many인 관계와 매핑하려면 여러개가 들어올수 있도록 컬렉션으로 받아야 한다.
하지만 이렇게 양방향 연관관계를 설정하면 문제가 발생할 수 있다.
엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다. 반면에 데이터베이스 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나다. 따라서 둘 사이에 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리하도록 해야한다.
이 것을 연관관계의 주인이라하고, mappedBy
속성을 통해 설정해준다.
이를 통해 설정된 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있다.
연관관계의 주인은 테이블에 외래 키가 있는 곳으로 정해야 한다. 여기서는 회원 테이블이 외래 키를 가지고 있으므로 Member.team이 주인이 된다.
주인이 아닌 Team.members에는 mappedBy="team"
속성을 사용해서 주인이 아님을 설정해야 한다.
여기서 mappedBy의 값으로 사용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말한다.
데이터베이스 테이블의 다대일, 일대다 관계에서는 항상 다 쪽이 외래 키를 가진다. 다 쪽인 @ManyToOne은 항상 연관관계의 주인이 되므로 mappedBy
를 설정할 수 없다. 그래서 @ManyToOne에는 mappedBy
속성이 없다.
Member member1 = new Member("member1", "회원1");
em.persist(member1);
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 를 생성하고 team1에 더해주었다. DB에 저장되었을까?
외래 키 TEAM_ID에 team1이 아닌 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문이다.
연관관계의 주인인 Member.team에 아무 값도 입력하지 않았기 때문에, TEAM_ID 외래 키의 값도 null이 저장된다.
객체 관점에서 양쪽 방향에 모두 값을 입력해주는 것이 안전하다. 양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있다.
안전한 관계 설정을 위해서는 아래와 같이 양쪽에 모두 값을 입력해줘야 한다.
public void test(){
Team team1 = new Team("team1", "팀1");
em.persist(team1);
Member member1 = new Member("member1", "회원1");
member1.setTeam(team1);
team1.getMembers().add(member1);
em.persist(member1);
Member member2 = new Member("member2", "회원2");
member2.setTeam(team2);
team1.getMembers().add(member2);
em.persist(member2);
}
하지만 member.setTeam(team)과 team.getMembers().add(member)를 각각 호출하다 보면 실수로 빼먹을 수도 있기 때문에 김영한 강사님께서 아래와 같은 방법을 추천하셨다.
public class Member{
private Team team;
public void setTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
// ...
}
setTeam()
메서드를 리팩토링해 한쪽을 설정해줄때 자동으로 같이 되게 하는 것이다.
다대일 단방향의 경우 가장 흔하게 사용되는 연관관계이다.
다대일의 반대는 일대다로 양방향 매핑 시 반대 테이블에 일대다 매핑을 해준다.
위에서 보았듯 외래 키가 있는쪽이 연관관계의 주인이 된다.
양쪽을 서로 참조가 필요할 경우 다대일 양방향 매핑을 해준다.
일대다 단방향 매핑을 하게되면, 일(1)의 테이블이 연관관계의 주인이 된다.
하지만 외래키는 다(N) 테이블이 가지고 있다.
이러한 구조때문에 @JoinColumn
을 사용하지 않으면, 조인 테이블을 생성하게 된다.
그리고 Update 쿼리를 실행하게 되기때문에, 차라리 다대일 양방향 매핑이 효율적이다.
일대다 양방향은 다대일 양방향 매핑의 반대 개념으로 공식적으로 있는 개념은 아니다.
@JoinColumn(insertable = false, updatable = false)
속성을 이용해 읽기 전용 필드를 양방향 처럼 사용할 수 있다.
일대일의 반대는 일대일 일 것이다.
주 테이블이나 대상 테이블 중에 어디서 외래키를 보관하게 할 것인지 선택해야한다.
외래 키에 유니크 제약조건을 추가시켜 주는 것이 좋다.
일대일 단반향은 다대일의 단방향 매핑과 유사하다.
양방향의 경우 외래 키를 가지고있는 테이블을 연관관계의 주인으로 설정해준다.
관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다.
그래서 연결 테이블을 생성해 일대다 + 다대일 관계로 풀어서 표현하게 된다.
하지만 연결 테이블이 단순히 연결만 하고 끝나지 않는다. 조인 테이블 자체에 주문시간, 수량 같은 추가 데이터가 많이 들어갈 수 있다.
하지만, 매핑 정보만 넣는 것이 가능하고, 추가 정보를 넣는 것 자체가 불가능하다.
그리고 중간 테이블이 숨겨져 있기 때문에 예상하지 못하는 쿼리들이 나간다.