[스프링부트 #11] JPA ORM의 본질과 fetch join 본질

ORM, fetchjoin의 본질을 따져가며 공부하였다.
도경원's avatar
Aug 14, 2025
[스프링부트 #11] JPA ORM의 본질과 fetch join 본질

1. ORM이 진짜 필요한 순간

많은 개발자들이 JPA를 사용하면서도 그 강력한 기능을 100% 다 활용하지는 않을 것이다. 특히, 조회만 할 거라면 굳이 ORM이 필요할까 ?
단순 조회(read-only)만 한다면 native query + DTO 면 대다수의 상황에서 충분할 것이다. 하지만, 상태를 변경(write/update)하거나 객체 관계를 조립하려면 ORM이 반드시 필요하다.

2. 객체지향적으로 설계된 엔티티

Board 엔티티 설계

notion image
userId 같은 외래 키 숫자가 아니라, User 객체 자체를 참조한다.
이것이 바로 ORM의 세계라고 할 수 있다. 테이블이 아닌 객체의 관계를 코드로 나타낸다.
이후 로직에서 board.getUser().getUsername() 같은 자연스러운 객체 탐색이 가능하다. (약간 Board 객체안에 User 객체를 넣어버린 느낌)

3. 수동 매핑 VS ORM 매핑 비교

수동 매핑 방식

notion image
방식은 native SQL, 객체는 수동으로 new, 상태 추적은 불가능하고, 유지보수성은 낮다.
그래도 쿼리 제어 자유도는 높고 목적은 복잡한 쿼리 + DTO 이다.
위의 메서드는 JPA의 껍데기만 쓰는 방식이고, 진짜 ORM이라고 보기 힘들다. 그렇지만, 수동 매핑을 해보면서 Hiberate등의 편리한 기능에 감사한 마음을 가질 수 있지 않을까 ?

ORM 방식

notion image
방식은 JPQL + fetch join, 객체 생성은 JPA가 자동 조립, 상태 추적 가능하고, 유지보수성은 높다(레고같음)
쿼리 제어는 객체 중심으로 제어하고 목적은 객체 간 관계 유지이다.

4. fetch join의 본질과 효용

지연 로딩(LAZY)의 기본 동작

Board board = boardRepository.findById(1); String username = board.getUser().getUsername(); // 여기서 추가 쿼리 발생!
→ N + 1 문제 발생 가능

fetch join으로 해결

@Query("select b from Board b join fetch b.user where b.id = :id") Board findByIdWithUser(@Param("id") int id);
→ 쿼리 1번으로 Board + User 조립 완료
→ 이후에도 getUser() 시 추가 쿼리 없음
JPA에서의 fetch join 의미
“연관된 엔티티까지 한 번에 가져오라” 라는 명령이라고 볼 수 있다.
일반 join은 단순히 SQL에서 데이터를 매칭만 하고, 실제로 연관 엔티티를 영속성 컨텍스트에 로딩하지 않을 수도 있다. 하지만 fetchjoin은 매칭과 동시에 연관 객체까지 즉시 로딩한다는 차이가 있다.
 
Share article

Gyeongwon's blog