이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- ORM은 무엇을 줄여주고 무엇을 없애지 못하는가?
- Repository 메서드가 짧아도 DB 비용이 큰 이유는 무엇인가?
- JPA를 쓰는 백엔드 개발자가 SQL과 실행 계획을 봐야 하는 이유는 무엇인가?
개요
ORM은 객체와 관계형 DB 사이의 매핑을 도와준다. 반복적인 CRUD, row-to-object 변환, 변경 감지, 연관관계 관리를 줄여준다. 하지만 SQL 실행 비용, 인덱스, 트랜잭션, 락, DB 한계는 사라지지 않는다.
JPA/Hibernate를 쓰는 백엔드 개발자가 DB를 모르면 코드가 짧아질수록 문제가 더 늦게 드러난다. 실제로 DB에 어떤 SQL이 몇 번 나가는지 확인해야 한다.
원리
다음 Repository 메서드는 간단하다.
List<Order> findByMemberIdOrderByCreatedAtDesc(Long memberId);그러나 DB에는 이런 SQL이 나갈 수 있다.
SELECT o.id, o.member_id, o.status, o.created_at
FROM orders o
WHERE o.member_id = ?
ORDER BY o.created_at DESC;데이터가 많다면 이 쿼리에는 인덱스와 limit이 필요하다.
CREATE INDEX idx_orders_member_created_id
ON orders (member_id, created_at DESC, id DESC);ORM 메서드 이름은 SQL 비용을 숨길 뿐 없애지 않는다.
목록 API라면 여기서 끝나지 않는다. Pageable을 붙이면 데이터 조회 SQL뿐 아니라 count SQL이 추가될 수 있고, 정렬 조건이 안정적이지 않으면 페이지가 흔들릴 수 있다.
Page<Order> findByMemberId(Long memberId, Pageable pageable);SELECT count(*)
FROM orders o
WHERE o.member_id = ?;데이터 조회는 빠른데 count가 느린 API도 많다. 화면이 전체 페이지 수를 필요로 하지 않으면 Slice나 cursor pagination이 더 적합할 수 있다.
ORM이 줄여주는 것
ORM은 다음을 줄여준다.
- 반복적인 JDBC 코드
- ResultSet 매핑
- insert/update SQL 작성
- 객체 변경 추적
- 1차 캐시와 동일성 보장
- 연관관계 표현
- DB 방언 차이 일부
이 장점은 매우 크다. 문제는 ORM이 DB 설계를 대신하지 않는다는 점이다.
ORM이 없애지 못하는 것
ORM은 다음을 없애지 못한다.
- 인덱스 설계
- 실행 계획
- N+1
- 락과 격리 수준
- 커넥션 풀
- 트랜잭션 경계
- 대량 처리 비용
- schema migration
예를 들어 order.getItems()는 코드상 필드 접근처럼 보이지만, lazy loading이면 DB query가 나갈 수 있다.
for (Order order : orders) {
int itemCount = order.getItems().size();
}이 코드는 주문 수만큼 추가 SQL을 만들 수 있다.
SQL 로그 보기
개발과 학습에서는 SQL 로그를 반드시 확인한다.
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.orm.jdbc.bind=trace운영에서는 SQL 로그가 너무 많고 민감정보를 포함할 수 있으므로 APM, slow query, query sampling, pg_stat_statements 같은 도구를 활용한다.
학습 단계에서는 API 하나를 호출한 뒤 다음을 기록해보면 좋다.
- 총 SQL 실행 횟수
- 가장 오래 걸린 SQL
- 같은 형태로 반복된 SQL
- count 쿼리 존재 여부
- 반환 row 수와 스캔 row 수
- 사용된 인덱스와 정렬 방식
예를 들어 주문 목록 API를 만들었다면 “Repository 메서드가 있다”가 아니라 “orders(member_id, created_at desc, id desc) 인덱스로 20건을 읽고, count는 별도 요구가 없어서 수행하지 않는다”까지 설명할 수 있어야 한다.
ORM 관측 루틴
개발 환경에서는 Hibernate 통계나 SQL 포맷팅을 켜서 감각을 잡을 수 있다.
spring.jpa.properties.hibernate.generate_statistics=true
logging.level.org.hibernate.stat=debug
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.orm.jdbc.bind=trace운영에서는 bind 값 전체를 남기면 개인정보와 로그 비용 문제가 생길 수 있다. 대신 APM span, datasource proxy, Micrometer metric, slow query log, DB별 statement 통계를 조합한다.
HTTP /members/{id}/orders
query.count = 3
query.max = 42ms
query.fingerprint = select ... from orders where member_id = ? order by ...
rows.returned = 20
pool.acquire.p95 = 4ms이런 관측 정보가 있어야 ORM 문제가 “느낌”이 아니라 증거로 다뤄진다.
실전 팁
- Repository 메서드를 만들면 실제 SQL을 한 번은 확인한다.
- 목록 조회에는 limit, 정렬 안정성, 인덱스를 함께 설계한다.
- 엔티티 그래프를 API 응답 모양과 혼동하지 않는다.
- 복잡한 조회는 DTO projection이나 명시적 query를 검토한다.
- SQL과 실행 계획을 PR 설명에 포함하면 리뷰 품질이 올라간다.
- count가 필요한 화면인지 먼저 확인한다. 필요 없다면
Page보다Slice나 cursor가 더 단순하고 안전할 수 있다. - 운영 장애 대응을 위해 API 이름, Repository 메서드, SQL fingerprint를 연결할 수 있게 로그나 APM 태그를 설계한다.
위험 신호!
- JPA를 쓰니 SQL을 몰라도 된다고 말한다.
- Repository 메서드 이름이 길어지고 조건이 복잡해졌는데 실행 계획을 보지 않는다.
- API 응답 DTO를 엔티티 그대로 노출한다.
- Lazy loading이 언제 SQL을 실행하는지 모른다.
- 운영 slow query의 출처 Repository를 찾지 못한다.
- 쿼리 수는 줄였지만 조인 row 수와 메모리 사용량이 늘어난 것을 보지 않는다.
- 운영에서는 SQL 로그를 못 켜는데 대체 관측 수단도 없다.
확인 질문
- ORM이 SQL을 없애지 않는다는 말의 의미는 무엇인가?
- 개발자가 직접 SQL을 덜 쓸 뿐, DB에는 여전히 SQL이 실행되고 그 비용과 실행 계획은 그대로 존재한다는 뜻이다.
- Repository 메서드가 짧아도 성능 문제가 생길 수 있는 이유는 무엇인가?
- 생성된 SQL이 많은 행을 읽거나 정렬하고, 인덱스를 활용하지 못하거나 N+1을 만들 수 있기 때문이다.
- JPA를 쓰는 개발자가 실행 계획을 봐야 하는 이유는 무엇인가?
- ORM이 만든 SQL도 DB Optimizer가 실행하므로 인덱스, 조인, 정렬 비용 문제를 확인해야 하기 때문이다.
- 운영에서 ORM SQL 문제를 추적하려면 무엇을 연결해야 하는가?
- API, Repository 메서드, SQL fingerprint, query count, slow query, 커넥션 풀 지표를 연결해야 원인을 좁힐 수 있다.