일대일 관계는 양쪽 엔티티가 서로 하나의 엔티티만을 참조하는 관계를 의미 한다.
일반적인 다대일(N:1) 관계에서는 외래 키가 주로 다(N)쪽 테이블에 위치하지만, 일대일(1:1) 관계에서는 주 테이블과 대상 테이블 어느쪽이든 상관 없이 외래 키를 가질 수 있다.
따라서 주 테이블과 대상 테이블 중 어느 쪽에 외래 키를 둘지 선택해야 한다.
위 그림을 살펴보면, 1명의 회원은 1개의 사물함만 사용 가능하다. 반대로 1개의 사물함은 1명의 회원만 소유할 수 있다. 이것이 일대일 관계인 것이다.
여기서는 사물함보다는 회원에 접근하는 빈도수가 높은 것으로 간주하여 주 테이블을 회원(MEMBER), 대상 테이블을 사물함(LOCKER)으로 설정했다.
먼저, 주 테이블에 외래 키가 있는 경우를 살펴보자.
그림에서 볼 수 있듯, 연관관계의 주인인 객체의 테이블과 외래키가 위치한 테이블이 같다. 이 경우 연관관계 매핑은 어렵지 않으며, 다대일(@ManyToOne
) 단방향 매핑과 유사하다.
@Entity
public class Member {
@Id
@Column(name = "member_id")
private Long id;
private String name;
@OneToOne
@JoinColumn(name = "locker_id")
private Locker locker;
}
Locker 참조 객체를 저장할 수 있는 필드를 하나 생성해주고, @OneToOne
어노테이션을 설정해주어 일대일 연관관계임을 JPA에게 알려주었다. 또한 @JoinColumn
어노테이션으로 locker_id
라는 외래키로 조인할 것임을 명시했다.
주 테이블에 외래 키가 있는 경우는 그렇게 어렵지 않다. 연관관계를 관리하는 주체가 테이블과 객체가 동일하기 때문이다. 너무나 자연스럽고 당연하여 헷갈리지 않는다.
그렇다면 상대 테이블에 외래키가 있는 경우는 어떨까?
그림에서 볼 수 있듯, 연관관계의 주인인 객체의 테이블과 외래키가 위치한 테이블이 같지 않다.
먼저, DB 관점에서 살펴보자. 두 테이블을 조인하기 위해서는 외래 키로 조인을 해야 한다.
SELCT *
FROM MEMBER m
JOIN LOCKER l ON l.MEMBER_ID = m.MEMBER_ID
위 쿼리문을 실행하면 문제 없이 잘 실행된다. 우리도 직관적으로 생각하기에, LOCKER 테이블에 존재하는 외래 키와 MEMBER 테이블의 기본 키를 조인하는 방식이 맞다고 판단하게 된다.
@Entity
public class Member {
@Id
@Column(name = "member_id")
private Long id;
private String name;
// 잘못된 연관관계 설정
@OneToOne
@JoinColumn(name = "member_id")
private Locker locker;
}
그래서 이어서 엔티티를 설정해본다. "조인할 컬럼명을 MEMBER 테이블의 PK랑 동일하게 설정하면, LOCKER 테이블에 있는 member_id가 MEMBER 테이블의 PK와 조인되겠지?" 라는 기대와 함께 말이다.
하지만 안타깝게도, 그렇게는 우리가 원하는 방식으로 연관관계를 설정할 수 없다. 왜 그럴까?
기본적으로 JPA는 @JoinColumn
으로 명시한 컬럼을 기준으로 조인 조건을 내 테이블의 FK = 상대 테이블의 PK로 만들어 낸다. 다시 말하지만, "내 테이블의 FK"를 가지고 조인을 한다는 소리다. 이 말인즉, 내 테이블에 외래 키가 존재해야 한단 소리다.
근데 현재 외래 키가 어디에 존재하냐? 바로 상대 테이블이다. 즉, 내 테이블에는 외래 키가 없단 소리다. 그러니 JPA가 있지도 않은 내 테이블의 외래 키를 찾을 수 있을리가 만무하다.
즉, 위 소스코드에서 설정한 연관관계를 JPA 입장에서 해석하자면 다음과 같다.
"MEMBER 테이블에 member_id라는 외래키 컬럼이 있나 보네? 이걸로 LOCKER 테이블의 locker_id랑 조인하면 되겠네~"
그래서 JPA는 다음과 같은 SQL을 생성하려고 시도한다.
SELECT *
FROM MEMBER m
JOIN LOCKER l ON m.member_id = l.locker_id;
하지만 실제로는 MEMBER 테이블에 member_id와 같은 외래 키 컬럼이 존재하지 않는다. 결국 JPA는 잘못된 조인 조건을 바탕으로 쿼리를 생성하게 되고, 이는 실행 오류로 이어지게 된다.
그래서 상대 테이블에 외래 키가 있는 경우는 일대일 단방향 매핑을 할 수 없는 것이다.
그렇다면 이제는 다음과 같은 의문이 생길 수도 있다.
일대다 단방향 연관관계 매핑을 공부했다면, 연관관계의 주인이 되는 테이블에 외래 키가 없음에도 불구하고 단방향 연관관계 매핑이 가능하단 사실을 알 것이다. 어떻게 가능한 것일까?
하나의 팀에 여러명의 회원이 속할 수 있다는 요구사항을 전제로, 여기서는 다(1)쪽인 Team을 연관관계의 주인으로 설정했다.
@Entity
public class Team {
@Id
private Long id;
private String name;
@OneToMany
@JoinColumn(name = "team_id")
private List<Member> members = new ArrayList<>();
}
앞서 언급했듯이, @JoinColumn
은 기본적으로 "내 테이블에 있는 외래 키"를 기준으로 조인을 수행한다. 하지만 @OneToMany
에 @JoinColumn
을 함께 사용하는 경우에는 조금 다르다. 이 조합을 사용하면 JPA는 대상 테이블(N)에 @JoinColumn
에 명시된 이름의 외래 키 컬럼을 생성하려고 한다.
즉, 주 테이블(1)에서 외래 키의 위치를 상대 테이블에 만들도록 강제로 지정하는 셈이다.
이는 JPA에서 @OneToMany
관계에서만 예외적으로 외래 키를 상대 테이블에 생성할 수 있도록 허용한 경우이기 때문에 가능한 것이다.
JPA에서 단방향 매핑을 할 때, 외래 키는 내 테이블에 있어야 한다는 점을 꼭 기억해야 한다.
만약 외래 키가 상대 테이블에 있다면? JPA 입장에서는 조인 조건을 만들 수가 없어서 단방향 매핑을 할 수 없다. 뭐랄까.. "너한테 외래 키가 없는데 어떻게 대상 테이블과 조인하란 소리임?" 이런 느낌?
다만 @OneToMany
는 JPA가 특별히 예외를 둔 케이스라서, 상대 테이블에 외래 키를 만들어주는 방식으로 처리해준다. 그래서 단방향 매핑이 가능했던 거고.
결국 외래 키가 실제로 어디에 있는지, 그리고 누가 연관관계 주인이 누구인지를 제대로 이해하고 있어야 헷갈리지 않는 것이다.