JPA에서의 연관관계에 대한 고민
서론
JPA를 사용한 프로젝트에서 양방향 연관관계의 편리함과 동시에 발생하는 여러 문제들에 직면했습니다. 비록 프로젝트 규모는 작았지만, 연관관계가 많아지면서 발생한 복잡성과 순환 참조 문제는 실무의 더 큰 프로젝트에서 어떻게 확대될지 고민하게 했습니다. 이 포스트는 그 과정에서 겪었던 어려움, 그리고 이를 어떻게 극복했는지에 대한 회고입니다.
양방향 연관관계의 도입
처음에는 모든 연관관계를 양방향으로 구성했습니다. 이유는 간단했습니다: 데이터를 조회하고 관리하는 것이 편리했기 때문입니다. 하지만 이 선택이 가져온 복잡성과 순환 참조 문제는 예상치 못했던 도전이었습니다.
//수정 전 USER 엔티티
@Entity
@Table(name = "users")
@Getter
@Setter
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private UserBaseInfo userBaseInfo;
@Enumerated(EnumType.STRING)
private Role role;
@JsonManagedReference
@OneToMany(mappedBy = "organizer", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ExerciseEvent> organizedEvents = new ArrayList<>();
@JsonManagedReference("user-registration")
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Registration> registrations = new ArrayList<>();
@JsonManagedReference("user-review")
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews = new ArrayList<>();
@JsonManagedReference
@OneToMany(mappedBy = "participant", fetch = FetchType.LAZY)
private List<ChatRoom> participatingChatRooms = new ArrayList<>();
@JsonManagedReference
@OneToMany(mappedBy = "creator", fetch = FetchType.LAZY)
private List<ChatRoom> createdChatRooms = new ArrayList<>();
@JsonManagedReference("user-message")
@OneToMany(mappedBy = "sender", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ChatMessage> messages = new ArrayList<>();
//수정 전 이벤트 엔티티
@Entity
@Getter
@Table(name = "exercise_events")
public class ExerciseEvent extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JsonManagedReference
@OneToMany(mappedBy = "exerciseEvent",cascade = CascadeType.REMOVE,orphanRemoval = true)
private List<Registration> registrations = new ArrayList<>();
@JsonManagedReference
@OneToMany(mappedBy = "exerciseEvent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Review> reviews = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@JsonBackReference
private User organizer;
@JsonManagedReference
@OneToMany(mappedBy = "exerciseEvent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ChatRoom> chatRooms = new ArrayList<>();
@Embedded
private EventDetail eventDetail;
@Embedded
private RecruitmentPolicy registrationPolicy;
@Embedded
private Location location;
@Enumerated(EnumType.STRING)
private Category category;
양방향 연관관계의 문제점
순환 참조 문제와 초기 대응
엔티티를 생성하고 관계를 맺는 과정에서 JSON 직렬화 시 순환 참조 문제가 발생했습니다. 임시 해결책으로 @JsonManagedReference와 @JsonBackReference 어노테이션을 사용했지만, 이는 근본적인 해결책이 아니었습니다.
복잡성 증가에 대한 고민
프로젝트가 진행될수록, 여러 테이블과 엔티티 간의 양방향 연관관계는 코드의 복잡성만을 증가시켰습니다. 특히, 작은 프로젝트 임에도 불구하고 user 테이블의 연관관계가 6개나 들어가 있는걸 확인할 수 있는데 실무에서 여러개의 데이터베이스 수십개의 테이블과 컬럼들이라고 생각할때 코드에 복잡성이 증가한다고 생각했습니다.
단방향 연관관계로의 전환 고려
고민 끝에, 양방향 연관관계를 단방향으로 전환하기로 결정했습니다. @OneToMany 단방향 매핑 대신 @ManyToOne을 주로 사용하기로 했습니다. 그 이유는 다음과 같습니다:
- 일대다 단방향 매핑의 단점:
- 엔티티가 관리하는 외래키가 실제로는 다른 테이블에 있게 됩니다.
- 연관관계를 관리하기 위해 추가로 UPDATE SQL이 실행되어 성능 문제가 발생할 수 있습니다.
이러한 문제를 고려하여, 더 효율적인 데이터 관리와 성능을 위해 다대일(@ManyToOne) 연관관계로 구조를 재설계했습니다.
단방향 연관관계로의 전환: 해결과 교훈
//변경 후 USER 엔티티
@Entity
@Table(name = "users")
@Getter
@Setter
public class User extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Embedded
private UserBaseInfo userBaseInfo;
@Enumerated(EnumType.STRING)
private Role role;
//변경 후 Event 엔티티
@Entity
@Getter
@Table(name = "exercise_events")
public class ExerciseEvent extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private User organizer;
@Embedded
private EventDetail eventDetail;
@Embedded
private RecruitmentPolicy registrationPolicy;
@Embedded
private Location location;
@Enumerated(EnumType.STRING)
private Category category;
순환 참조 문제의 해결
단방향 연관관계로 전환하며 가장 큰 성과 중 하나는 순환 참조 문제의 근본적 해결이었습니다. @JsonManagedReference와 @JsonBackReference를 사용한 임시 방편 대신, 연관관계의 방향을 명확히 함으로써 JSON 직렬화 시 발생하던 순환 참조 문제를 해결할 수 있었습니다.
코드의 가독성 향상
변경 전과 후의 User 엔티티를 비교해보면, 구조의 변화가 가독성을 크게 향상시켰음을 알 수 있습니다. 단방향 연관관계를 적용함으로써 엔티티 간의 관계가 더 명확해지고, 이해하기 쉬워졌습니다. 이러한 구조적 명확성은 유지보수 시간을 단축시키고, 코드의 질을 높이는 데 기여했습니다.
테스트 용이성
단방향 연관관계로의 전환은 테스트 코드 작성에도 긍정적인 영향을 미쳤습니다. 복잡한 연관관계를 가진 엔티티보다 단방향 연관관계를 가진 엔티티를 테스트하는 것이 훨씬 수월했으며, 데이터 상태의 일관성을 보장하기가 더 쉬워졌습니다.
N+1 문제에 대한 걱정 감소
단방향 연관관계를 적용한 후, N+1 문제에 대한 우려도 크게 줄었습니다. 복잡한 연관관계에서는 각 연관관계를 처리하기 위해 추가적인 fetch 전략을 고민해야 했지만, 단방향 연관관계에서는 이러한 문제를 상대적으로 덜 걱정하며 데이터 접근 전략을 설계할 수 있었습니다. 이는 성능 최적화 측면에서도 유리한 점이 많았습니다.
결론: 단방향 연관관계, 복잡성 해결의 열쇠
프로젝트를 진행하며 JPA의 양방향 연관관계의 편리함과 함께, 이로 인해 발생하는 순환 참조와 같은 복잡성을 직접 경험했습니다. 이 과정에서 얻은 깨달음은 엔티티 간의 라이프 사이클이 밀접하게 연결되어 있지 않을 때, 양방향 연관관계는 불필요한 복잡성을 초래할 수 있다는 점을 배웠습니다.
단방향 연관관계로의 전환은 순환 참조 문제를 근본적으로 해결하고, 코드의 가독성과 유지보수성을 향상시켰으며, 더욱 간결하고 명확한 데이터 모델을 구축할 수 있게 해주었습니다. 물론, 이 과정에서 추가적인 코드 작성이 필요하게 되었고, 일정한 불편함이 수반되었지만, 장기적인 관점에서 보았을 때 프로젝트의 건강을 위한 가치 있는 결정이었습니다.
'프로젝트(개인)' 카테고리의 다른 글
| 애플리케이션 배포 시 마주친 문제와 해결책 (0) | 2024.03.27 |
|---|---|
| [Spring , react] 실시간 채팅 구현하기 (1) | 2024.03.19 |
| JWT 인증과 Session 인증의 복합 사용! (1) | 2024.02.10 |
| Spring boot와 JPA , react 를 이용한 Google Oauth2 구현 (0) | 2024.01.30 |