카카오 테크 캠퍼스 프로젝트를 개발하는 중 생긴 일이다.
나는 DB 설계대로 엔티티 개발을 하고 있었다.
이 때 산책과 채팅방 엔티티를 OneToOne으로 연관 짓고자 하였는데, 산책에 채팅방 외래키를 두어 단방향으로 연관 짓고자 하였다.
아래는 엔티티 코드들이다.
산책 Entity
/**
* Walk(산책) 엔티티
*
* @author Kevin
* @version 1.0
*/
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Walk {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@ManyToOne(fetch = FetchType.LAZY)
private Member walker;
@ManyToOne(fetch = FetchType.LAZY)
private Member master;
@OneToOne(fetch = FetchType.LAZY)
private ChatRoom chatRoom;
@Enumerated(value = EnumType.STRING)
private WalkStatus walkStatus;
private LocalDateTime startTime;
private LocalDateTime endTime;
}
채팅방 엔티티
@Entity
@Getter
@NoArgsConstructor
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "CHATROOM_ID")
private long id;
}
위와 같이 코드를 작성하고, 테스트할 때 외래키 제약 조건 에러가 발생하였다.
인터넷에 검색해보니 대상 엔티티만이 주 엔티티의 외래키를 가지고 있는 단방향 OneToOne 관계는 불가능하다는 말을 들었다.
도대체 주 엔티티와 대상 테이블은 무엇이고, 왜 단방향 OneToOne 관계는 불가능하다는 것일까? 자세히 알아보자.
쉽게 예를 들어서 회원과 프로필 도메인이 있다고 하자.
회원은 하나의 프로필만 사용 가능하고, 프로필도 하나의 회원에 매칭이 가능하다.
이 때 회원은 주 테이블
이 되고, 프로필은 대상 테이블
이 된다.
이 때 회원이 주 테이블이 될 수 있는 근거는 사용자 테이블이 최소한 먼저 생겨야 프로필 테이블도 생길 수 있기 때문이다.
즉 주 테이블은 1:1 관계에서 반드시 먼저 생겨야 하는 테이블이라고 나는 이해했다.
일대일 관계
에 대해서 좀 더 살펴보자.
일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느 곳이나 외래키를 가질 수 있다.
테이블은 주 테이블이든 대상 테이블이든 외래키 하나만 있으면 양쪽으로 조회할 수 있기에. 누가 외래키를 가질지 선택해야한다.
→ 이는 주 객체가 대상 객체를 참조하는 것처럼 객체지향과 같은 개념으로 설계를 할 수 있다. 이 방법의 장점은 주 테이블이 외래키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있다는 것이다.
이 말을 좀 더 풀어서 설명하면, 회원 테이블에 프로필 외래키가 있다면, 회원 테이블만 확인해도 프로필 테이블과 연관관계가 있는지를 확인할 수 있다는 것이다.
이 때 프로필 테이블은 반드시 회원 테이블이 존재해야 존재할 수 있으므로 프로필 테이블을 확인하지 않아도 테이블이 있음을 확신할 수 있다.
반면 프로필 테이블에 회원 외래키가 있다면 회원 테이블만을 확인할 때 프로필 테이블이 존재함을 확신할 수 없다. 프로필 테이블 없이 회원 테이블만 존재하는 경우가 있기 때문이다.
아래는 주 테이블에 대상 외래키가 있을 때 단방향
코드이다.
Member(주 테이블)
@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
@OneToOne
@JoinColumn(name = "PROFILE_ID")
private Profile profile;
...
}
Profile(주 테이블)
@Entity
@Getter
@NoArgsConstructor
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "PROFILE_ID")
private long id;
...
}
주 테이블에 대상 외래키가 있을 때 양방향
코드이다.
Member(주 테이블)
@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
@OneToOne
@JoinColumn(name = "PROFILE_ID")
private Profile profile;
...
}
Profile(주 테이블)
@Entity
@Getter
@NoArgsConstructor
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "PROFILE_ID")
private long id;
@OneToOne(mappedBy = "profile")
private Member member;
...
}
이 경우에는 Member 테이블이 Profile 외래키를 가지고 있으므로, Member.profile이 연관 관계의 주인이다.
mappedBy는 JPA에게 “나 연관관계 주인 아니에요~!! 제 주인의 이름은 이래요~!!” 라고 말하는 코드라고 생각하자.
→ 먼저 대상 테이블에 외래 키가 있는 단방향 관계는 JPA는 지원하지 않는다. 그렇기에 양방향 관계로 만들고 대상 테이블을 연관관계의 주인으로 설정해야 한다.
Member(주 테이블)
@Entity
@Getter
@NoArgsConstructor
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "MEMBER_ID")
private long id;
@OneToOne(mappedBy = "member")
private Profile profile;
...
}
Profile(주 테이블)
@Entity
@Getter
@NoArgsConstructor
public class Profile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "PROFILE_ID")
private long id;
@OneToOne
@JoinColumn(name = "MEMBER_ID")
private Member member;
...
}
나는 우리 프로젝트의 비즈니스 로직 Flow 상 채팅방 생성후 산책 엔티티가 생성 될 때까지의 텀이 길수도, 짧을 수도 있는 불확실성이 다분하다고 판단했다.
채팅방에서 최종적으로 견주가 아르바이트 생에게 산책을 허락해줄 때가 되어서야 산책 엔티티 생성 시점이 된다.
그렇기에 아래와 같은 이유들로 해당 결정을 하게 되었다.
최종 코드는 아래와 같다.
산책 Entity
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class Walk {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
@ManyToOne(fetch = FetchType.LAZY)
private Member walker;
@ManyToOne(fetch = FetchType.LAZY)
private Member master;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "CHATROOM_ID")
private ChatRoom chatRoom;
@Enumerated(value = EnumType.STRING)
private WalkStatus walkStatus;
private LocalDateTime startTime;
private LocalDateTime endTime;
}
채팅방 엔티티
@Entity
@Getter
@NoArgsConstructor
public class ChatRoom {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "CHATROOM_ID")
private long id;
@OneToOne(mappedBy = "chatRoom")
private Walk walk;
}