첫번째 트러블슈팅
[Error] 일정 관리 트러블슈팅 3 - URI 설계 (User)
이전 트러블슈팅 [Error] 일정 관리 트러블 슈팅 1 - 제네릭제네릭이 개념중에 가장 코드 적용으로 바로 이어지기 힘든 부분이었다. 필수과제 구현까지 총 3번의 제네릭 활용 리팩토링을 해보았는
devz0.tistory.com
문제
제네릭이 문법적으로 가장 시간이 많이들어갔다면 양방향 단방향 연관 관계설정은 생각을 정리하느라 시간이 많이 소요됐다.
리팩토링을 진행하며 User, Schedule, Comment 세 도메인 간 연관관계를 다시 보던 중, “단방향으로 유지할지, 양방향으로 전환할지” 판단이 애매했다. 특히, 단건 조회에서 일정과 댓글을 함께 보여주는 엔드포인트 설계를 하며 “일정 단건 조회시 댓글목록을 항상 참조하는데 Schedule ↔ Comment를 양방향으로 바꿔야 하나?”라는 고민이 있었다.
⚠️ 이전입문과제는 일정과 댓글을 양방향 매핑으로 관계를 설정했다. 뭔가를 알고 했다기 보다 강의대로 진행하다보니 그대로 따라갔다. 단순히 데이터가 함께 필요하다는 이유로 양방향 매핑을 하면 다음과 같은 일이 발생한다.
- @JsonIgnore 등 직렬화 이슈 발생 -> 내가 저번입문과제에 겪었던 문제!
- 그대로 JSON으로 직렬화할 경우, 스프링의 Jackson이 두 엔티티를 서로 무한히 순환하며 직렬화하려고 시도
- 순환참조 위험 (Schedule → Comment → Schedule) -> 내가 저번입문과제에 겪었던 문제!
- 양방향 매핑을 걸면 객체 그래프가 서로 참조 하면서 StackOverflowError 또는 Infinite recursion 예외가 발생
- fetch join 과 Pagination 충돌
- 한 페이지에 10개의 일정만 보여줘야 하는데 실제 쿼리는 JOIN된 댓글 row 수만큼 곱해져서 페이지네이션이 무의미해지는 상황 발생
- 도메인 간 의존성이 뒤얽혀 테스트 및 유지보수 어려움
- 양방향을 무분별하게 설정하면 도메인 간의 경계가 모호해진다.
예를 들어 Schedule이 Comment를 알고, Comment가 다시 Schedule을 알면, 어느 쪽이 데이터를 주도하는지 헷갈리게 된다. - 도메인은 책임 단위로 분리돼야 하는데, 양방향이 많아지면 결국 모든 게 연결된 거대한 그래프가 되어버린다.
- 양방향을 무분별하게 설정하면 도메인 간의 경계가 모호해진다.
해결 과정
각각의 트레이드 오프를 항목별로 나누고 이건 절대 안된다는 기준을 세워 검토했다.
1. 탐색의 방향과 주체
- 일정 단건 조회 시 댓글 목록도 함께 본다 → 주체는 Schedule
- 댓글이 어떤 일정에 속했는지 알고 싶다 → 주체는 Comment
👉🏻 탐색 방향이 한쪽(Comment)에서만 필요하면 단방향 유지
2. 쿼리와 N+1
양방향일 때는 다건 조회시 Lazy 로딩으로 N+1 문제가 발생한다
단방향에서는
commentRepository.findAllByScheduleId(scheduleId);
이건 JPA가 날리는 N+1이 아니라 내가 의도한 1+1 쿼리다.
3. CASCADE
양방향 + cascade = REMOVE | ALL
- JPA가 부모-자식 생명주기를 하나로 묶어서 관리
- DB에서는 삭제됐는데, 1차 캐시에 여전히 남아 있는 상태 가능
- cascade 범위가 넓으면 예기치 않게 여러 엔티티가 함께 삭제될 위험
단방향 + @OnDelete(action = OnDeleteAction.CASCADE)
- DB가 자식 삭제를 담당 → 쿼리 단순성 높음
- JPA cascade로 인한 예기치 않은 전파 제거
- 단, JPA는 DB 내부 cascade를 “모르기 때문에”
같은 트랜잭션 내에서 캐시된 엔티티는 삭제되지 않은 것처럼 보일 수 있다.
두 방식 모두 비슷한 증상을 보이지만, 원인은 다르다.
- 양방향: JPA가 캐시를 비우지 않음
- 단방향: DB가 알아서 삭제했지만 JPA는 그걸 모름
4. 페이징 충돌
- 양방향
@OneToMany(fetch = LAZY) 관계를 fetch join 시 페이징 깨짐 + row 중복 - 단방향
- 다건 조회 (GET /schedules) → 댓글 필요 없음
- 단건 조회 (GET /schedules/{id}) → commentRepository.findAllByScheduleId() 호출
4. 결론
이번 결정의 핵심 기준은 예상한 대로 동작하는 코드 였다.
페이지가 깨지거나 내가 작성하지 않은 쿼리로 문제가 생기는 것들을 허용하지 않았다.
| 단방향의 단점 | 양방향의 단점 | 기준 | 내 선택 | |
| 페이징 / fetch join | fetch join 직접 안 쓰고 별도 쿼리 작성 필요 | fetch join 시 row 중복 + pagination 깨짐 | 페이징이 깨지는 건 절대 안 돼 | 단방향 |
| 쿼리 실행 흐름 | 탐색이 불편해 쿼리 1번 더 써야 함 (commentRepository.findAllByScheduleId()) | JPA가 예상치 못한 Lazy 로딩 쿼리를 N+1로 터뜨릴 수 있음 | 내가 날리지 않은 쿼리가 나가는 건 절대 안 돼 | 단방향 |
| 직렬화 (JSON 응답 안정성) | 별도 조합 DTO 필요 | 순환참조 (Schedule→Comment→Schedule) → StackOverflowError | 응답에서 무한루프는 절대 안 돼 | 단방향 |
배운점
이전에 구글링할때 연관관계 설정은 탐색 주체와 조회 빈도로 결정한다고 보았다. 그러다 보니 양방향도 맞는것같고 단방향도 맞는것 같고 각각의 트레이드오프로 내가 결정을 내리지 못했다.
양방향/단방향의 선택은 어떤것이 일어나서는 안되는가를 기준으로 삼아보자.
- 단방향은 코드가 조금 길어지더라도 쿼리를 조금 더 직접 작성해야 하지만, 그만큼 예측 가능한 동작과 안정적인 구조를 얻는다.
- 양방향은 객채 탐색이 쉬워지지만 잘못 설정하면 페이지 깨짐 / 무한 루프 / 의도치 않은 쿼리가 생길 수 있다.
- 설계의 기준은 문법이 아니라 내가 감당할 수 있는 리스크의 범위였다.
이번 과제에서 가장 의미있는 수확이 있었던 부분이다! 튜터님의 피드백으로 기준을 세우면서 이전처럼 불안하게 선택하는 대신,
지금은 확신을 가지고 구조를 설계할 수 있게 되었다. 물론 아직 완벽하진 않지만..ㅋㅎ 방향은 잡았다!
'BootCamp' 카테고리의 다른 글
| [Error] 일정 관리 트러블슈팅 1 - URI 설계 (User) (0) | 2025.11.15 |
|---|---|
| [Error] 일정 트러블 슈팅 (1) | 2025.11.05 |
| [내배캠] 한달 회고 (0) | 2025.10.31 |
| [Error] 키오스크 트러블 슈팅 3 - 장바구니 구조 설계 (0) | 2025.10.28 |
| [Error] 키오스크 트러블 슈팅 2 -책임 분리 (0) | 2025.10.27 |