문제 상황
프로젝트를 진행 중 게시글(Post)을 저장하는 과정에서 LazyInitializationException이 발생하였다.
이때 프로젝트의 구조는 Member와 Post가 양방향 연관관계를 맺고 있었고 Post의 setMember메서드를 사용하여 연관관계를 유지하고 있었다.
문제의 코드는 아래와 같았다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
@Transactional
public Long write(Post post) {
postRepository.save(post);
return post.getId();
}
}
게시글을 담당하는 PostService의 게시글을 저장하는 write메서드이다.
처음 PostService를 구상할 때는 컨트롤러단에서 Post를 받아오고 이를 Repository의 persist를 통해 저장하면 되겠다고 생각했다.
public void save(Post post) {
em.persist(post);
}
이와 같이 Repository의 save를 호출하면 단순히 persist를 통해 Post를 영속화하는 구조로 작성되었다.
아마 제대로 동작했다면 이후에 트랜잭션을 커밋할 때 쓰기 지연 저장소에 저장된 쿼리가 데이터베이스에 반영되어 게시글이 올바르게 저장되었을 것이다.
하지만 LazyInitializationException이 발생하며 Post는 제대로 저장되지 않았다.
해결
문제는 Post와 Member 사이의 양방향 연관관계와 영속성 관리에 있었다.
간단하게 말해 Post를 DB에 저장하기 위해서는 양방향 연관관계로 존재하는 Member의 값도 변경되어야 하는데
Member가 영속성 컨텍스트에서 관리되고 있지 않아 변경사항이 반영되지 않는 문제가 발생했다.
필요한 부분만 코드를 가져왔다.
@Entity
@Getter
public class Post extends BaseTimeEntity{
@Id
@GeneratedValue
@Column(name = "post_id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
public Post(Member member,String title,String content,String writer){
setMember(member);
this.title = title;
this.content = content;
this.writer = writer;
}
public static Post createPost(Member member,String title,String content,String writer) {
return new Post(member,title,content,writer);
}
//연관 관계 메서드
public void setMember(Member member) {
this.member = member;
member.getPosts().add(this);
}
}
데이터 저장 시 PostForm에서 Post로 변환하는 과정에 정적 팩토리 메서드가 사용된다.
정적 팩토리 메서드(createPost)를 보면 setMember를 호출하고 이때 Member와 Post의 양방향 연관관계를 설정한다.
하지만 생각해보자 현재 Member의 정보가 영속상태가 아니라면 연관관계 메서드의 member.getPosts(). add(this)를 통한 변경은 제대로 반영되지 않을 것이다.
이를 해결하기 위해서는 변경 감지를 사용해야 한다.
이 변경 감지를 사용하기 위해서는 Member를 영속 상태로 만들어야 한다.
이와 같은 이유로 같은 트랜잭션 안에서 Member 정보를 영속 상태로 만들어주면 해결할 수 있을 것이다.
@Transactional
public Long write(Long memberId, String title,String content) {
Member member = memberService.findOne(memberId);
Post post = Post.createPost(member, title, content, member.getNickname());
postRepository.save(post);
return post.getId();
}
write 호출 시 PostForm을 통해 받아온 title, content와 함께 memberId를 같이 받아오도록 변경했다.
이때 memberId로 Member를 검색하여 Member를 영속상태로 만들고 변경 감지 기능을 사용하여 Member와 Post의 양방향 연관관계를 유지하여 해결했다.
'트러블 슈팅' 카테고리의 다른 글
| [트러블 슈팅] MySQL 환경에서 @QueryHint가 무시되어 timeout을 지정할 수 없는 문제 해결 (0) | 2025.07.04 |
|---|---|
| [트러블 슈팅] HikariCP 데드락 발생 원인과 해결 과정 (feat:nGrinder) (1) | 2025.03.22 |
| [트러블 슈팅] JPA 성능 최적화: N+1 문제 해결 및 nGrinder를 사용한 성능 테스트 (0) | 2025.03.15 |
| [트러블 슈팅] JPA 연관 관계와 테스트 코드 : delete 쿼리가 실행되지 않는 현상 해결 (2) | 2025.01.05 |