ORM과 Database JPA 관점
3줄 요약
- ORM은 SQL을 없애는 기술이 아니라 객체 모델과 관계형 모델 사이의 반복 작업을 줄여주는 도구다.
- JPA/Hibernate를 쓰는 백엔드 개발자는 영속성 컨텍스트, flush, dirty checking, lazy loading, N+1, 트랜잭션 경계를 DB 비용으로 설명할 수 있어야 한다.
- 실무에서는 “엔티티 코드가 예쁜가”보다 “어떤 SQL이 언제 몇 번 나가고, 어떤 인덱스와 락을 사용하는가”가 더 중요할 때가 많다.
핵심 정리
- ORM은 객체 그래프를 DB row로 매핑한다. 이 과정에서 즉시 로딩, 지연 로딩, 변경 감지, flush 시점이 SQL 실행 타이밍을 결정한다.
- 영속성 컨텍스트는 1차 캐시와 변경 추적을 제공한다. 편리하지만 트랜잭션이 길면 관리 객체가 늘고 의도하지 않은 update가 나갈 수 있다.
- N+1은 객체 그래프 탐색이 여러 SQL로 번지는 대표 문제다. fetch join, EntityGraph, batch size, DTO query를 상황에 맞게 선택해야 한다.
@Transactional(readOnly = true)는 읽기 의도를 드러내고 Hibernate flush 최적화와 연결될 수 있다. 그래도 DB 부하가 자동으로 사라지는 것은 아니다.
- Repository 메서드 이름이 간단해도 생성 SQL이 비효율적일 수 있다. 중요한 쿼리는 JPQL, Querydsl, native query, DTO projection까지 검토한다.
- 엔티티 설계는 DB 스키마 설계와 분리해서 볼 수 없다. 연관관계 편의성, cascade, orphanRemoval은 실제 delete/update와 락 범위에 영향을 준다.
- JPA 코드는 반드시 SQL 관측과 함께 읽어야 한다. API별 query count, slow query fingerprint, 실행 계획, row 수, count 쿼리 비용을 같이 봐야 운영 감각이 생긴다.
- Bulk update/delete는 영속성 컨텍스트를 우회한다. 이미 로딩된 엔티티와 DB 상태가 어긋날 수 있으므로 clear, 재조회, 트랜잭션 경계를 명시한다.
- OSIV가 켜져 있으면 Controller나 JSON 직렬화 단계에서도 lazy loading이 가능하다. 편리하지만 connection 점유 시간과 N+1이 숨을 수 있어 API 서버에서는 팀 기준을 정해야 한다.
헷갈리는 지점
- JPA를 쓰면 SQL을 몰라도 된다고 생각하기 쉽다. CRUD 코드가 매우 짧아지기 때문이다.
- 핵심은 JPA가 SQL 작성을 줄여도 SQL 실행 비용은 그대로 DB가 부담한다는 점이다.
- 실제 SQL 로그와 실행 계획을 확인해야 한다.
- Lazy Loading은 항상 성능 최적화라고 오해하기 쉽다. 필요한 때만 가져온다는 설명이 좋아 보이기 때문이다.
- 핵심은 반복 접근이 N+1을 만들 수 있다는 점이다.
- API 응답에 필요한 데이터 모양을 기준으로 fetch 전략을 설계한다.
- Fetch join을 붙이면 N+1은 항상 해결된다고 오해하기 쉽다. 쿼리 수가 줄어드는 것은 눈에 바로 보이기 때문이다.
- 핵심은 컬렉션 fetch join이 row 폭증, 중복 제거, pagination 문제를 만들 수 있다는 점이다.
- 단건 상세, 목록, 컬렉션 포함 여부, 정렬과 limit 위치를 기준으로 선택한다.
- Flush와 commit을 같은 것으로 착각하기 쉽다. 둘 다 트랜잭션 끝 근처에서 일어나는 경우가 많기 때문이다.
- 핵심은 flush는 SQL을 DB에 보내는 것이고 commit은 변경 확정이라는 점이다.
- 제약 조건 위반, 락 획득, bulk update와의 상태 불일치가 flush 시점에 드러날 수 있다.
- Cascade는 편리하니 많이 걸어도 된다고 생각하기 쉽다. 객체 저장이 자동 전파되기 때문이다.
- 핵심은 cascade가 DB 변경 범위를 숨길 수 있다는 점이다.
- 생명주기가 정말 같은 객체 관계에만 제한적으로 사용한다.
확인 질문
- JPA의 N+1 문제는 왜 발생하는가?
- 처음 조회한 엔티티 목록의 연관 객체를 하나씩 접근할 때 각 행마다 추가 SQL이 실행되기 때문이다.
- Flush 시점을 이해해야 하는 이유는 무엇인가?
- SQL이 실제로 DB에 전달되는 시점과 락, 제약 조건 검증, 예외 발생 시점이 flush와 연결되기 때문이다.
- ORM을 쓰더라도 실행 계획을 봐야 하는 이유는 무엇인가?
- ORM이 생성한 SQL도 DB Optimizer가 실행하며, 인덱스 미사용이나 조인 비용 문제는 동일하게 발생하기 때문이다.
- JPA 목록 API를 리뷰할 때 최소한 무엇을 확인해야 하는가?
- 실제 SQL, query count, limit과 count 쿼리, fetch 전략, 정렬 안정성, WHERE/ORDER BY를 지원하는 인덱스를 확인해야 한다.