연관관계 트러블슈팅

이재서·2026년 7월 9일

양방향 연관관계 CASCADE 저장 시 자식 엔티티 불완전 조회 오류

1. 배경

  • Schedule(일정)과 Comment(댓글)은 1:N 관계이다.
  • Schedule을 통해 Comment를 저장하기 위해 cascade=cascadeType.ALL 설정을 사용했다.

2. 문제 상황

Schedule 객체의 comments 리스트에 새로운 Comment 객체를 추가하고 Dirt Check를 통해 DB에 저장했다. 저장 후 정상적으로 저장됐음을 알리기 위해 Comment 객체를 CommentResponse라는 DTO를 통해 클라이언트에 전달하고자 했으나, comments 리스트에서 해당 객체를 가져왔을 때 id같은 GeneratedValue가 비어있는 것을 확인했다.

//Schedule 객체를 가져온다.
Schedule schedule = scheduleRepository.findById(scheduleId);

//부모 객체의 컬렉션에만 추가 후 Drity Check를 통해 저장
Comment comment = Comment.create(userId, schedule, "댓글 내용");
schedule.getComments().add(comment);

//이후 Comment 객체를 사용하려고 보면 comment의 id같은 값이 없다.
System.out.println(comment.getId());

3. 원인 분석

  • 쿼리 커밋 시점: 해당 메서드 내에서 Comment 객체를 불러오면 Dirty Check를 통해서 저장이 진행되지 않은 시점이기 때문에 GeneratedValue인 id 값은 아직 생성되지 않은 시점이다. 해당 시점에서 Comment를 반환하려했기 때문에 문제가 발생한다.

4. 해결 방안

이를 해결하기 위해 CommentService에서 Comment 객체를 생성, 저장하여 Comment 객체를 반환하는 메서드를 작성하는 방법을 떠올려 봤다. 하지만 이미 CommentService에는 Schedule 객체의 존재 여부 확인을 위해서 ScheduleService가 필요했기 때문에 ScheduleService를 의존성 주입으로 받고 있는 상태였다. 해당 상황에서 ScheduleService에서 CommentService의 의존성을 주입받게 된다면 순환 참조 오류가 생길 것이다.

두 번째 방안은 ScheduleService는 Comment 객체의 생성이나 저장에 관여하지 않고, 연관된 Schedule 객체에 Comment 객체를 전달하여 comments 컬렉션에 추가만 하는 것이다. 해당 방식은 Comment 객체의 생성 등에 관한 책임을 온전히 CommentService가 가져가게 되고 연관관계를 잃어버리지 않게끔 할 수 있으며, 의존성 방향도 바뀌거나 추가되는 일이 없다. 그래서 해당 방식을 적용했다.

Schedule 엔티티 수정(연관관계 편의 메서드 추가)

@Entity
public Schedule {
	//기존 코드
    
    public void addComment(Comment comment) {
    	this.comments.add(comment);
        //Comment 객체의 Schedule 객체와의 연관관계 추가
        comment.setSchedule(this);
    }
}

비즈니스 로직 수정

@Service
public CommentService {
	private final CommentRepository commentRepository;
    private final ScheduleService scheduleService;
    private final CommentMapper commentMapper;
    
    public Comment createComment(CommentCreateRequest createRequest, Long userId, Long scheduleId) {
    	User user = userService.getUserById(userId);
        Schedule schedule = scheduleService.getScheduleById(scheduleId);
        
        Comment toSave = Comment.create(user, createRequest.content());
        
        //객체의 참조를 전달하면 save로 인해 객체가 변경되는 것까지 반영된다.
        schedule.addComment(comment);
        
        return commentMapper.toCommentRepsonse(commentRepository.save(toSave));
    }
}

5. 결과 및 회고

  • 결과: 연관관계 편의 메서드를 통해 메모리 상에도 Schedule과 Comment가 서로를 참조하게 되었으며, Comment를 생성함과 동시에 완전한 객체에서 DTO를 통해 반환할 수 있게 되었고, Comment 객체의 생성에 관한 책임을 온전히 CommentService라는 도메인의 하위 서비스에 남길 수 있다.
  • 회고: JPA를 사용할 때는 RDB 관점에서 확인한 관계뿐만 아니라, 영속성 컨텍스트 상에서 객체가 어떤 상태로 존재하고 있는지를 생각하면서 코드를 작성해야 한다. JPA 상의 관계라는 것은 객체의 참조에 관한 것이라는 것을 염두에 두고, 양방향 관계에서는 양쪽 모두에 참조를 전달할 수 있도록 하는 연관관계 편의 메서드를 작성해야 한다.
profile
아무거나 쓰는 중

0개의 댓글