KeepHunDev
JPA 양방향 연관관계에 대한 고민 본문
마감기한이 얼마 남지 않아 급급하게 팀원들의 코드 리뷰를 자세하게 하지 않다가, 멘토링 삭제 기능(소프트 딜리트)을 확장함에 있어 문제점을 발견하게 되었고, JPA 연관관계에 대한 생각 정리를 하게 되었습니다.
요구사항
- 사용자는 여러 멘토에게 멘토링을 신청할 수 있다.
유연한 비즈니스를 대처할 수 있을까?
멘토링의 정보는 비즈니스적 로그가 될 가능성이 높습니다. 하지만 마감된 신청, 거절된 신청 어떤 정보를 로깅할지는 pm과 경영의 결정에 따라 변경 가능성이 매우 높습니다. 만약 CasecadeType.ALL, 고아 객체 정리로 DB의 무결성을 타이트하게 잡는다면, 서비스를 운영하면서 정보를 삭제하는데 제약 조건을 지켜가면서 정보를 다뤄야 합니다. 따라서 유연한 설계라고 하기 어렵다고 판단하였습니다.
문제의 코드
@Entity
public class Mentor {
@Id @GeneratedValue
private Long id;
private String nickname;
@OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Mentoring> mentorings = new ArrayList<>();
public void addMentoring(Mentoring mentoring) {
this.mentorings.add(mentoring);
mentoring.setMentor(this);
}
}
@Entity
public class Mentoring {
@Id @GeneratedValue
private Long id;
private LocalDateTime appliedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mentor_id")
private Mentor mentor;
public void setMentor(Mentor mentor) {
this.mentor = mentor;
}
}
위 코드는 JPA 입문서에서도 소개하는 표준적인 양방향 연관관계 매핑입니다. Mentor는 자신에게 신청된 Mentoring 목록을 List로 관리하고, addMentoring이라는 연관관계 편의 메서드로 정보의 후속처리를 하고 있습니다. 하지만 이 코드에는 많은 문제점이 있습니다.
1. 숨겨진 성능 저하 (Hidden Performance Degradation)
“10월 5일 이후의 신청에서 이름이 김철수인 사람을 조회한다”라는 요구사항을 처리하는 쿼리를 구성하면 FetchType.LAZY를 설정하더라도, mentoring row를 가지고 와서 getMentor를 호출하여 N+1을 유발할 수 있습니다. 물론 엔티티 그래프나 BatchSize로 처리할 수 있지만, 팀원들 또는 나의 휴먼 에러를 야기할 수 있습니다. 따라서 명시적인 쿼리 작성 유도를 할 수 없습니다.
2. 불분명한 책임 (Unclear Responsibility)
public MentoringResponse createMentoring(CreateMentoringRequest request, Long memberId) {
Mentor mentor = mentorRepository.findById(request.mentorId())
.orElseThrow(() -> new CustomRuntimeException(MentorErrorCode.MENTOR_NOT_FOUND));
...
final Mentoring mentoring = Mentoring.of(..., mentor, ...);
mentor.addMentoring(mentoring)
return MentoringResponse.from(mentoringRepository.save(mentoring));
}
public class Mentor {
...
@OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Mentoring> mentorings = new ArrayList<>();
public void addMentoring(Mentoring mentoring){
mentorings.add(mentoring);
}
public void deleteMentoring(Mentoring mentoring){
mentorings.remove(mentoring);
}
}
Mentoring을 생성하는 책임이 MentoringService에 있는 것 같지만, 실제로는 Mentor 엔티티를 거쳐야만 완전한 저장이 이루어집니다. Mentor 엔티티는 본인의 프로필 정보를 관리하는 핵심 책임 외에, Mentoring의 생명주기를 관리하는 책임까지 떠안게 되었습니다. 이는 단일 책임 원칙(SRP)에 위배됩니다.
추가적으로 멘토링에서 멘토링 시간 변경 요구사항에는 mentor.changeMentoringTime(...)을 만들어야 할까요?
해결책 : OneToMany 제거하기
@Entity
public class Mentor {
// ...
// @OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL)
// private List<Mentoring> mentorings = new ArrayList<>(); // <- 삭제
}
@Entity
public class Mentoring {
// ...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "mentor_id") // 연관관계의 유일한 주인
private Mentor mentor;
}
연관관계의 주인인 Many 쪽만 참조를 유지하고, One 쪽의 컬렉션을 제거한다.
결과
public MentoringResponse createMentoring(CreateMentoringRequest request, Long memberId) {
Mentor mentor = mentorRepository.findById(request.mentorId())
.orElseThrow(() -> new CustomRuntimeException(MentorErrorCode.MENTOR_NOT_FOUND));
...
final Mentoring mentoring = Mentoring.of(..., mentor, ...);
// mentor.addMentoring(mentoring) 삭제
return MentoringResponse.from(mentoringRepository.save(mentoring));
}
public class Mentor {
...
@OneToMany(mappedBy = "mentor", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private List<Mentoring> mentorings = new ArrayList<>();
// addMentoring 삭제
// deleteMentoring 삭제
}
이로 인해 연관관계 편의 메서드들은 Mentor class에서 빠지게 되고, MentoringService 에서 편의 메서드 로직도 빠지게 됩니다. 또한 mentor.getMentorings()처럼 객체 그래프 탐색을 통해 자식 컬렉션을 바로 가져올 수는 없습니다. 하지만 이는 mentoringRepository.findByMentorId(...)라는 명시적인 호출로 대체할 수 있으며, 오히려 언제 쿼리가 실행되는지 명확하게 알 수 있습니다.
결론
JPA의 양방향 @OneToMany는 객체지향적인 모델링을 지원하는 강력한 기능이지만, 실무에서는 결합도, 성능, 책임 분리 측면에서 예상치 못한 문제를 일으킬 수 있다고 생각합니다. 객체 그래프 탐색의 편리함 대신 단방향 연관관계를 기본으로 하고, 필요한 데이터는 Repository를 통해 명시적으로 조회하는 방식이 훨씬 더 안정적이고 유지 보수하기 쉬운 애플리케이션을 만든다는 것을 이번 경험을 통해 느꼈습니다
마치며
이 글이 엔티티 설계를 고민하는 많은 개발자분들께 실질적인 도움이 되기를 바랍니다. 특히 저처럼 학생 및 신입 개발자분들께 저의 고민과 경험이 좋은 참고 자료가 되었으면 하는 마음입니다.
다음 포스팅에서는 JPA의 ResultSet 매핑 최적화 원리를 정리해 보고, 이를 바탕으로 '어떤 비즈니스 시나리오에서 양방향 관계가 단방향보다 더 나은 성능을 보이는가'에 대한 저만의 이론을 정립해 나갈 것입니다.
감사합니다.
'회고록' 카테고리의 다른 글
| 소프트 스킬이 향상됐다고 느꼈던 순간 (2) | 2025.09.13 |
|---|---|
| 텍스트 파일 조인의 성능 병목 분석 및 개선기 (0) | 2025.09.12 |