EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
Member member = new Member();
em.persist(member);
tx.commit();
} catch (Exception e) {
tx.rollback();
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(String username);
}
@Entity
public class Post {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Post post;
}
@Service
@Transactional
public class MemberService {
private final MemberRepository memberRepository;
public void register(Member member) {
memberRepository.save(member);
// 트랜잭션 내에서 영속성 컨텍스트 관리
// 변경 감지(Dirty Checking) 동작
}
}
무한순환참조는 양방향 연관관계를 가진 엔티티들이 서로를 계속해서 참조하면서 발생하는 문제입니다. 이 과정을 자세히 살펴보겠습니다.
@Entity
public class Post {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "post")
private List<Comment> comments = new ArrayList<>();
}
@Entity
public class Comment {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Post post;
}
JSON 직렬화 시작
Post 객체 직렬화 시도
↓
comments 리스트 직렬화 시도
↓
각 Comment 객체 직렬화 시도
↓
Comment 내부의 post 참조 직렬화 시도
↓
다시 Post 객체 직렬화 시도
↓
무한 반복...
메모리 스택 과정
convertToJson(post) {
jsonObject = new JsonObject()
jsonObject.add("id", post.getId())
jsonObject.add("comments", convertToJson(post.getComments()))
// comments를 변환하는 과정에서
// 다시 post를 참조하게 되어 무한 루프 발생
}
{
"id": 1,
"comments": [
{
"id": 1,
"post": {
"id": 1,
"comments": [
{
"id": 1,
"post": {
// 무한 반복...
}
}
]
}
}
]
}
트랜잭션은 영속성 컨텍스트의 생명주기를 관리합니다. 이 과정을 상세히 살펴보겠습니다.
@Service
public class PostService {
// 트랜잭션 없는 경우
public void processWithoutTransaction(Long postId) {
Post post = postRepository.findById(postId).get();
// 이 시점에서 영속성 컨텍스트가 종료됨
post.getComments().size(); // LazyInitializationException 발생
}
// 트랜잭션이 있는 경우
@Transactional
public void processWithTransaction(Long postId) {
Post post = postRepository.findById(postId).get();
// 트랜잭션 범위 내에서 영속성 컨텍스트 유지
post.getComments().size(); // 정상 동작
}
}
@Entity
public class Post {
@OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
private List<Comment> comments;
public int getCommentsCount() {
// 트랜잭션 없이는 LazyInitializationException 발생
return comments.size();
}
}
@Service
public class PostService {
@Transactional
public void updateTitle(Long postId, String newTitle) {
Post post = postRepository.findById(postId).get();
post.setTitle(newTitle);
// 트랜잭션 종료 시점에 변경 감지 동작
// 별도의 save() 호출 불필요
}
}
@Service
public class PostService {
@Transactional
public void addComment(Long postId, String content) {
Post post = postRepository.findById(postId).get();
Comment comment = new Comment(content);
post.addComment(comment); // 연관관계 설정
// 트랜잭션 범위에서 영속성 전이 동작
}
}
public void processWithoutTransaction() {
EntityManager em = emf.createEntityManager();
Post post = em.find(Post.class, 1L);
em.close(); // 영속성 컨텍스트 종료
// 이 시점에서 post는 준영속 상태
List<Comment> comments = post.getComments(); // 예외 발생
}
@Transactional
public void processWithTransaction() {
// 트랜잭션 시작 - 영속성 컨텍스트 생성
Post post = em.find(Post.class, 1L);
List<Comment> comments = post.getComments(); // 정상 동작
comments.forEach(comment -> {
// 지연 로딩 정상 작동
System.out.println(comment.getContent());
});
// 트랜잭션 종료 - 변경사항 저장
}
@Getter
public class PostDto {
private final Long id;
private final String title;
private final List<CommentDto> comments;
public PostDto(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.comments = post.getComments().stream()
.map(CommentDto::new)
.collect(Collectors.toList());
}
}
@Getter
public class CommentDto {
private final Long id;
private final String content;
// post 참조 제외
public CommentDto(Comment comment) {
this.id = comment.getId();
this.content = comment.getContent();
}
}
@Service
@Transactional
public class PostService {
private final PostRepository postRepository;
@Transactional(readOnly = true)
public PostDto getPost(Long id) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Post not found"));
return new PostDto(post);
}
public void updatePost(Long id, PostUpdateDto updateDto) {
Post post = postRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("Post not found"));
post.update(updateDto);
// 변경 감지 동작
}
}
@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
// N+1 문제 해결을 위한 페치 조인
@Query("SELECT p FROM Post p LEFT JOIN FETCH p.comments WHERE p.id = :id")
Optional<Post> findByIdWithComments(@Param("id") Long id);
}
무한순환참조와 트랜잭션 관리는 JPA 사용 시 반드시 이해하고 있어야 하는 핵심 개념입니다. 무한순환참조는 엔티티 간의 양방향 관계에서 발생하며, DTO 패턴을 통해 효과적으로 해결할 수 있습니다. 트랜잭션은 영속성 컨텍스트의 생명주기를 관리하며, 지연 로딩과 변경 감지 등 JPA의 핵심 기능들이 정상적으로 동작하기 위해 필수적입니다. 이러한 개념들을 제대로 이해하고 적용함으로써, 안정적이고 유지보수하기 좋은 애플리케이션을 개발할 수 있습니다.