JPA 핵심 개념 정리(feat.트랜잭션, 무한순환참조)

Kim jisu·2025년 2월 4일

spring

목록 보기
4/5

엔티티 관계, 트랜잭션, 무한순환참조

1. JPA와 Spring Data JPA

JPA (Java Persistence API)

  • 자바 ORM 기술에 대한 표준 명세
  • EntityManager를 통한 데이터베이스 작업
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();
}

Spring Data JPA

  • JPA를 더 쉽게 사용하게 해주는 프레임워크
  • Repository 인터페이스 제공
public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByUsername(String username);
}

2. 엔티티 관계 매핑

일대다 관계 예시

@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;
}

주의사항

  • 양방향 관계 시 연관관계 주인 설정
  • 지연 로딩 사용 권장
  • 무한순환참조 주의

3. 트랜잭션 관리

@Transactional의 중요성

@Service
@Transactional
public class MemberService {
    private final MemberRepository memberRepository;
    
    public void register(Member member) {
        memberRepository.save(member);
        // 트랜잭션 내에서 영속성 컨텍스트 관리
        // 변경 감지(Dirty Checking) 동작
    }
}

트랜잭션 특성

  • 원자성 (Atomicity)
  • 일관성 (Consistency)
  • 격리성 (Isolation)
  • 지속성 (Durability)

실무 적용 가이드

  1. 엔티티 설계 원칙
  • 단방향 관계를 우선 고려
  • 지연 로딩을 기본으로 사용
  • 성능 최적화가 필요한 경우 페치 조인 활용
  1. 트랜잭션 관리
  • 서비스 계층에서 트랜잭션 관리
  • 트랜잭션 전파 속성 적절히 활용
  • 예외 처리 전략 수립
  1. API 설계
  • DTO 사용을 기본으로
  • 엔티티 직접 노출 지양
  • API 스펙에 맞는 별도 DTO 설계
  1. 성능 최적화
  • N+1 문제 해결을 위한 페치 조인 활용
  • 적절한 인덱스 설계
  • 캐시 활용 고려

JPA 심화: 무한순환참조와 트랜잭션의 이해

1. 무한순환참조(Infinite Recursion)의 이해

발생 원리

무한순환참조는 양방향 연관관계를 가진 엔티티들이 서로를 계속해서 참조하면서 발생하는 문제입니다. 이 과정을 자세히 살펴보겠습니다.

@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;
}

순환참조 발생 과정

  1. JSON 직렬화 시작

    Post 객체 직렬화 시도
    ↓
    comments 리스트 직렬화 시도
    ↓
    각 Comment 객체 직렬화 시도
    ↓
    Comment 내부의 post 참조 직렬화 시도
    ↓
    다시 Post 객체 직렬화 시도
    ↓
    무한 반복...
  2. 메모리 스택 과정

convertToJson(post) {
    jsonObject = new JsonObject()
    jsonObject.add("id", post.getId())
    jsonObject.add("comments", convertToJson(post.getComments()))
    // comments를 변환하는 과정에서
    // 다시 post를 참조하게 되어 무한 루프 발생
}

실제 JSON 변환 결과

{
  "id": 1,
  "comments": [
    {
      "id": 1,
      "post": {
        "id": 1,
        "comments": [
          {
            "id": 1,
            "post": {
              // 무한 반복...
            }
          }
        ]
      }
    }
  ]
}

2. 트랜잭션의 필요성

영속성 컨텍스트 생명주기

트랜잭션은 영속성 컨텍스트의 생명주기를 관리합니다. 이 과정을 상세히 살펴보겠습니다.

@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(); // 정상 동작
    }
}

트랜잭션이 필요한 상황들

  1. 지연 로딩
@Entity
public class Post {
    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY)
    private List<Comment> comments;
    
    public int getCommentsCount() {
        // 트랜잭션 없이는 LazyInitializationException 발생
        return comments.size();
    }
}
  1. 변경 감지(Dirty Checking)
@Service
public class PostService {
    @Transactional
    public void updateTitle(Long postId, String newTitle) {
        Post post = postRepository.findById(postId).get();
        post.setTitle(newTitle);
        // 트랜잭션 종료 시점에 변경 감지 동작
        // 별도의 save() 호출 불필요
    }
}
  1. 연관관계 처리
@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); // 연관관계 설정
        // 트랜잭션 범위에서 영속성 전이 동작
    }
}

실제 동작 과정 분석

  1. 트랜잭션 없는 경우의 문제점
public void processWithoutTransaction() {
    EntityManager em = emf.createEntityManager();
    Post post = em.find(Post.class, 1L);
    em.close();  // 영속성 컨텍스트 종료
    
    // 이 시점에서 post는 준영속 상태
    List<Comment> comments = post.getComments(); // 예외 발생
}
  1. 트랜잭션이 있는 경우의 동작
@Transactional
public void processWithTransaction() {
    // 트랜잭션 시작 - 영속성 컨텍스트 생성
    Post post = em.find(Post.class, 1L);
    List<Comment> comments = post.getComments(); // 정상 동작
    comments.forEach(comment -> {
        // 지연 로딩 정상 작동
        System.out.println(comment.getContent());
    });
    // 트랜잭션 종료 - 변경사항 저장
}

3. 해결 방안 및 실무 적용

무한순환참조 해결

  1. DTO 패턴 (권장)
@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();
    }
}

트랜잭션 관리 전략

  1. 서비스 계층 트랜잭션 관리
@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);
        // 변경 감지 동작
    }
}
  1. 성능 최적화
@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의 핵심 기능들이 정상적으로 동작하기 위해 필수적입니다. 이러한 개념들을 제대로 이해하고 적용함으로써, 안정적이고 유지보수하기 좋은 애플리케이션을 개발할 수 있습니다.

profile
Dreamer

0개의 댓글