![[스프링부트 #16] JPA 연관관계 성능 튜닝과 Dirty Checking 원리](https://image.inblog.dev?url=https%3A%2F%2Finblog.ai%2Fapi%2Fog-custom%3Ftitle%3D%255B%25EC%258A%25A4%25ED%2594%2584%25EB%25A7%2581%25EB%25B6%2580%25ED%258A%25B8%2B%252316%255D%2BJPA%2B%25EC%2597%25B0%25EA%25B4%2580%25EA%25B4%2580%25EA%25B3%2584%2B%25EC%2584%25B1%25EB%258A%25A5%2B%25ED%258A%259C%25EB%258B%259D%25EA%25B3%25BC%2BDirty%2BChecking%2B%25EC%259B%2590%25EB%25A6%25AC%26tag%3DTemplate%2B1%26description%3D%26template%3D3%26backgroundImage%3Dhttps%253A%252F%252Fsource.inblog.dev%252Fog_image%252Fdefault.png%26bgStartColor%3D%252323ec86%26bgEndColor%3D%252323ec86%26textColor%3D%2523000000%26tagColor%3D%2523000000%26descriptionColor%3D%2523000000%26logoUrl%3D%26blogTitle%3DGyeongwon%2527s%2Bblog&w=2048&q=75)
오늘은 JPA에서 연관관계를 가진 엔티티를 조회할 때 성능 저하를 막는 방법과,엔티티 수정 시 Dirty Checking(더티 체킹)이 어떻게 동작하는지 정리합니다.여기에 인증·권한 체크와 같은 부가 로직 분리 팁도 함께 다룹니다.
1. 연관관계와 성능 문제
JPA에서
@ManyToOne
, @OneToMany
같은 연관관계를 매핑하면,연관된 엔티티를 불러올 때 로딩 전략에 따라 쿼리 수가 크게 달라집니다.
특히
findAll()
처럼 컬렉션을 조회할 때 EAGER 로딩을 사용하면 다음과 같은 일이 발생합니다.- Board 목록을 불러올 때마다, 각 Board마다 User를 추가로 조회 → N+1 문제
- 결국 SQL이 N+1번 실행되어 성능 급격히 저하
원칙
- EAGER 로딩 금지
- 항상 LAZY 로딩으로 설정 후, 필요한 경우 fetch join 사용
2. 성능 개선 방법
연관 엔티티를 한 번에 가져오려면 크게 두 가지 방법이 있습니다.
- JOIN(fetch join)
- 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 줄이기가 훨씬 중요한 경우가 많습니다.

흐름 정리
findById(id)
- JPA가 DB에서
Board
엔티티를 조회해서 **영속성 컨텍스트(1차 캐시)**에 올림. - 이 시점의
findboard
객체는 영속 상태.
findboard.update(...)
- 영속 상태의 엔티티 필드 값을 변경.
- JPA는 "변경 전 스냅샷"과 "현재 값"을 비교하기 위해 이 변경을 트래킹.
- 트랜잭션 종료 시점 (
@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

이 메서드는 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