JPA 연관관계 편의 메서드의 위치에 대한 내 생각
kindof
·2023. 3. 15. 22:25
1. 문제
JPA에서 연관관계 편의 메서드는 양방향 연관관계를 한 번에 설정하기 위해 작성하는 메서드 입니다.
이 때, 통상적으로 엔티티 A(Many), B(One)가 서로 양방향 관계일 때 연관관계 편의 메서드는 총 3가지 케이스로 작성할 수 있습니다.
- [1] A에 작성하고, B까지 관리한다.
- [2] B에 작성하고, A까지 관리한다.
- [3] A, B 각각에 대해 작성한다.
그런데 여기서 [3]번 방식은 둘 중의 한 곳에서라도 메서드 호출을 빼먹으면 논리적인 문제가 생기고 코드의 관리 측면도 복잡해져서 지양하는 패턴입니다.
그러면 [1], [2] 케이스 중에 어디에 연관관계 편의 메서드를 정의하는 게 좋을까요?
이번 글에서는 이 질문에 대한 제 생각을 기록해보려고 합니다.
2. 단순한 ManyToOne 양방향 관계
아래와 같이 두 엔티티가 있다고 해보겠습니다.
[Pin.java]
@Entity
@Table(name = "pin")
public class Pin extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@OneToMany(mappedBy = "pin", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<PinTag> pinTags = new ArrayList<>();
...
}
[Picture.java]
@Entity
@Table(name = "picture")
public class Picture extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Pin pin;
}
여기서 연관관계 편의 메서드는 Pin 엔티티에 있어야 할까요? Picture 엔티티에 있어야 할까요?
사실 두 곳 어디에 둬도 상관 없습니다. 하지만, 저는 Pin 엔티티에 두는 쪽을 컨벤션으로 지향합니다.
그 이유는 비즈니스 로직 관점에서 Pin 엔티티가 Picture를 활용하지, Picture 엔티티가 Pin을 활용하지 않기 때문입니다. 실제로 코드를 작성하면 Pin과 관련된 코드들에서 Picture를 저장/업데이트/삭제하지, Picture 쪽에서는 Pin을 거의 손대지 않기 때문입니다.
뿐만 아니라, Cascade 옵션을 통해 부모가 자식의 생명주기를 관리한다는 측면을 생각해보면 Pin 입장에서 자신의 Picture 리스트를 관리하는 것이 그 의도를 분명히 드러낼 수 있기도 합니다.
따라서, 아래와 같이 Pin 엔티티에 연관관계 편의 메서드를 작성합니다.
@Entity
@Table(name = "pin")
public class Pin extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@OneToMany(mappedBy = "pin", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<PinTag> pinTags = new ArrayList<>();
####### 연관관계 편의 메서드 #######
public void addPicture(Picture picture) {
if(!getPictures().contains(picture)){
getPictures().add(picture);
}
picture.setPin(this);
}
public void removePicture(Picture picture) {
this.getPictures().remove(picture);
picture.setPin(null);
}
}
3. ManyToMany 관계에서는 어떻게 할까?
ManyToMany 관계를 풀어내기 위해서는 두 엔티티를 연결하는 연결 엔티티를 사용합니다.
아래 엔티티 관계는 한 유저가 여러 써클에 가입할 수 있고, 한 써클도 여러 유저를 갖을 수 있다는 요구사항에서 도출되었습니다.
그리고 Users - UserCircle, Circle - UserCircle에 대해 각각 양방향 맵핑을 적용했습니다.
[Users.java]
@Entity
@Table(name = "users")
public class Users extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<UserCircle> userCircleList = new ArrayList<>();
...
}
[Circle.java]
@Entity
@Table(name = "circle")
public class Circle extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@OneToMany(mappedBy = "circle", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<UserCircle> userCircleList = new ArrayList<>();
...
}
[UserCircle.java]
@Entity
@Table(name = "user_circle")
public class UserCircle extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Users user;
@ManyToOne(fetch = FetchType.LAZY)
private Circle circle;
...
}
자, 그러면 이 상황에서도 Users, Circle 엔티티 쪽에 연관관계 편의 메서드를 작성하는 게 좋을까요?
제 생각은 '아니오'입니다. 이 경우에는 UserCircle 쪽에 연관관계 편의 메서드를 작성하는 게 더 좋다고 생각합니다.
그 이유는 아래 테스트 코드를 통해 살펴보겠습니다. Users, Circle에 연관관계 편의 메서드(addUserCircle)가 위치한다고 가정합니다.
이 테스트 코드는 실패합니다. Circle에 작성한 연관관계 편의 메서드를 호출했지만, Users 쪽에 작성한 연관관계 편의 메서드를 호출하지 않았기 때문입니다.
이 테스트 코드가 성공하기 위해서는 u1.addUserCircle(uc1); 이라는 메서드를 추가로 호출했어야 합니다.
이 상황은 맨 처음 도입부에서 말했던 양쪽에 연관관계 편의 메서드를 쓰는 [3]번 상황을 지양했던 이유와 비슷해집니다.
따라서 ManyToMany 관계를 풀어내기 위한 연결 엔티티에 세 개의 엔티티를 관리하는 연관관계 편의 메서드를 만드는 게 좋아 보입니다.
@Entity
@Table(name = "user_circle")
public class UserCircle extends BaseTimeEntity {
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Users user;
@ManyToOne(fetch = FetchType.LAZY)
private Circle circle;
####### 연관관계 편의 메서드 #######
public void setUserAndCircle(Users user, Circle circle) {
addUserCircleForUser(user);
addUserCircleForCircle(circle);
setUser(user);
setCircle(circle);
}
private void addUserCircleForUser(Users user) {
if(!user.getUserCircleList().contains(this)){
user.getUserCircleList().add(this);
}
}
private void setUser(Users user) {
this.user = user;
}
private void addUserCircleForCircle(Circle circle) {
if(!circle.getUserCircleList().contains(this)){
circle.getUserCircleList().add(this);
}
}
private void setCircle(Circle circle) {
this.circle = circle;
}
}
그리고 아래와 같이 테스트 코드를 작성하면, 한 줄의 연관관계 편의 메서드 호출로 테스트가 성공합니다. 그리고 파라미터를 Users, Circle을 필수로 받도록 함으로써 컴파일 시점에 잘못된 로직 사용을 방지할 수 있게 됩니다.
4. 정리
지금까지 말한 내용을 정리하면 크게 두 가지입니다.
- ManyToOne 관계의 두 엔티티가 양방향 맵핑일 때는 부모 쪽에 연관관계 편의 메서드를 작성한다.
- ManyToMany 관계를 풀어내기 위해 중간 연결 엔티티를 두고 양방향 맵핑을 한다면 연관관계 편의 메서드를 작성한다.
연관관계 편의 메서드 작성을 하는 방식은 사람마다 다를 것 같고, 제가 생각한 방식이 정답은 아니니 참고정도만 해보면 좋을 것 같습니다.
'Spring & Springboot' 카테고리의 다른 글
@SpringBootApplication과 AutoConfiguration 들여다보기 (0) | 2023.10.07 |
---|---|
단위 테스트에서 @InjectMocks, @Mock VS @MockBean 이해하기 (0) | 2023.04.15 |
[Spring] @CacheEvict 없이 @Cacheable을 쓸 때 생길 수 있는 문제 (0) | 2023.02.22 |
@DataJpaTest의 동작 방식과 몇 가지 주의사항 (0) | 2023.02.12 |
Spring Bean VS StaticClass (0) | 2022.12.10 |