inblog logo
|
Gyeongwon's blog
    Springboot

    [스프링부트 #16] JPA 연관관계 성능 튜닝과 Dirty Checking 원리

    도경원's avatar
    도경원
    Aug 25, 2025
    [스프링부트 #16] JPA 연관관계 성능 튜닝과 Dirty Checking 원리
    Contents
    1. 연관관계와 성능 문제2. 성능 개선 방법3. Dirty Checking(더티 체킹)과 flush4. 인증·권한 체크5. 부가 로직 분리6. Board.java의 Setter 7. Java HttpSession과 getAttribute() 이해하기오늘 내용 한 장 요약
    오늘은 JPA에서 연관관계를 가진 엔티티를 조회할 때 성능 저하를 막는 방법과,
    엔티티 수정 시 Dirty Checking(더티 체킹)이 어떻게 동작하는지 정리합니다.
    여기에 인증·권한 체크와 같은 부가 로직 분리 팁도 함께 다룹니다.

    1. 연관관계와 성능 문제

    JPA에서 @ManyToOne, @OneToMany 같은 연관관계를 매핑하면,
    연관된 엔티티를 불러올 때 로딩 전략에 따라 쿼리 수가 크게 달라집니다.
    특히 findAll()처럼 컬렉션을 조회할 때 EAGER 로딩을 사용하면 다음과 같은 일이 발생합니다.
    • Board 목록을 불러올 때마다, 각 Board마다 User를 추가로 조회 → N+1 문제
    • 결국 SQL이 N+1번 실행되어 성능 급격히 저하

    원칙

    • EAGER 로딩 금지
    • 항상 LAZY 로딩으로 설정 후, 필요한 경우 fetch join 사용

    2. 성능 개선 방법

    연관 엔티티를 한 번에 가져오려면 크게 두 가지 방법이 있습니다.
    1. JOIN(fetch join)
    1. IN query
    이번 예제에서는 fetch join을 사용했습니다.
    public Board findByIdJoinUser(int id) { Query query = em.createQuery( "select b from Board b join fetch b.user where b.id = :id", Board.class ); query.setParameter("id", id); return query.getSingleResult(); }
    이렇게 하면 board와 user를 한 번의 쿼리로 가져와
    board.getUser().getUsername() 호출 시 추가 쿼리가 발생하지 않습니다.

    3. Dirty Checking(더티 체킹)과 flush

    동작 원리

    JPA에서는 영속 상태의 엔티티가 변경되면 자동으로 변경 내용을 감지하여(DB 반영 준비)
    트랜잭션 종료 시점에 UPDATE 쿼리를 실행합니다. 이 과정을 Dirty Checking이라 합니다.

    flush 시점

    • em.flush() 직접 호출
    • 트랜잭션 종료 시점(@Transactional 메서드 종료)
    • JPQL/SQL 직접 실행 시
    flush는 버퍼에 모아둔 SQL을 DB로 보내는 과정입니다.

    예제

    @Test public void update_test() { Board findBoard = boardJpaRepository.findById(1); // 1) 조회 → PC에 저장 findBoard.update("title change", "content change"); // 2) 상태 변경 // 3) flush 시점에 update SQL 실행 }
    이 방식의 장점은 여러 UPDATE를 모아서 한 번에 반영 → 디스크 I/O 최소화입니다.
    성능 최적화에서 알고리즘 연산 개선보다 이런 I/O 줄이기가 훨씬 중요한 경우가 많습니다.
    notion image

    흐름 정리

    1. findById(id)
        • JPA가 DB에서 Board 엔티티를 조회해서 **영속성 컨텍스트(1차 캐시)**에 올림.
        • 이 시점의 findboard 객체는 영속 상태.
    1. findboard.update(...)
        • 영속 상태의 엔티티 필드 값을 변경.
        • JPA는 "변경 전 스냅샷"과 "현재 값"을 비교하기 위해 이 변경을 트래킹.
    1. 트랜잭션 종료 시점 (@Transactional 종료)
        • JPA가 flush() 실행.
        • 변경 감지(Dirty Checking)로, 바뀐 필드만 UPDATE 쿼리 생성.
        • 쿼리 전송 후 커밋.

    왜 더티 체킹인가?

    • update(...)는 DB에 직접 UPDATE 쿼리를 날리는 게 아니라, 엔티티 객체의 상태만 변경.
    • JPA가 알아서 트랜잭션 끝날 때 변경된 부분을 찾아서 UPDATE SQL로 만들어 실행.
    • 즉, SQL을 내가 안 써도 JPA가 변경 감지를 해줌.

    4. 인증·권한 체크

    브라우저로부터 받은 데이터는 절대 신뢰하지 말 것!

    예시: 세션 로그인 여부 확인

    User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { throw new RuntimeException("로그인 하세요"); }
    • 인증 실패: 401 Unauthorized
    • 권한 실패: 403 Forbidden
      • (예: 내가 쓴 글이 아닌데 삭제 시도)

    5. 부가 로직 분리

    • 인증, 권한 체크, 로깅 등은 부가 로직
    • 핵심 비즈니스 로직과 분리해야 유지보수가 쉬움
    • 나중에 Interceptor, Filter, AOP로 빼서 관리 가능

    6. Board.java의 Setter

    notion image
    이 메서드는 Board 객체의 제목과 내용을 한 번에 바꾸는 기능이다. 즉, title 필드와 content 필드를 동시에 수정한다.
    update(title, content)는 게시글의 제목·내용을 수정하는 도메인 전용 메서드로, @Setter 남발을 피하고, 의미 있는 이름으로 객체의 상태 변경을 안전하게 관리하기 위해 만든 것이다.

    7. Java HttpSession과 getAttribute() 이해하기

    User sessionUser = (User) session.getAttribute("sessionUser");
    User sessionUser = (User) session.getAttribute("sessionUser");는 서버가 브라우저별로 제공하는 HttpSession에서 "sessionUser"라는 이름으로 저장된 로그인 사용자 객체를 꺼내와 User 타입으로 변환해 변수에 담는 코드다. 즉, 로그인 시 setAttribute로 보관한 유저 정보를 이후 요청에서 다시 꺼내 쓰는 과정이라고 볼 수 있다.

    오늘 내용 한 장 요약

    • EAGER 금지, fetch join으로 N+1 방지
    • Dirty Checking은 변경 내용을 모아 한 번에 반영
    • flush 시점: 직접 호출 / 트랜잭션 종료 / 쿼리 실행 시
    • 성능 최적화에서 I/O 줄이기가 핵심
    • 인증(401) / 권한(403) 구분
    • 부가 로직은 핵심 로직과 분리
    JPA를 잘 쓴다는 건 단순히 동작시키는 것보다 성능·보안·구조까지 고려하는 것입니다.
    이번 내용은 JPA 튜닝의 기초지만, 실제 서비스에서도 필수적으로 적용해야 하는 부분입니다.
     
    Share article
    Contents
    1. 연관관계와 성능 문제2. 성능 개선 방법3. Dirty Checking(더티 체킹)과 flush4. 인증·권한 체크5. 부가 로직 분리6. Board.java의 Setter 7. Java HttpSession과 getAttribute() 이해하기오늘 내용 한 장 요약

    Gyeongwon's blog

    RSS·Powered by Inblog