프로젝트 진행 중 프로젝트 모집 게시글의 전체 리스트를 보여주는 API에서 N+1 문제가 발생하여 원인과 해결한 과정을 작성하게 되었다.
먼저 N+1이 무엇인지 알아보자
N+1 문제는 ORM 기술에서 특정 객체를 대상으로 수행한 쿼리가 해당 객체가 가지고 있는 연관관계 또한 조회하게 되면서 N번의 추가적인 쿼리가 발생하는 문제이다.
N+1문제가 발생하는 근본적인 원인은 관계형 데이터베이스와 객체지향 언어간 차이로 인해 발생한다. 객체는 연관관계를 통해 하위 객체를 가지고 있으면 메모리 내에서 연관 객체에 접근할 수 있지만 RDB의 경우 Select 쿼리를 통해서만 조회할 수 있기 때문이다.
데이터를 조회할 때 발생하며 아래 두가지 상황에서 발생한다.


projectskill 조회 이후 skill에 대한 조회를 N번을 하는 쿼리를 발견하였다.
public List<ProjectSkill> findProjectSkillsInProjectIds(List<Long> projectIds) {
return jpaQueryFactory
.selectFrom(projectSkill)
.leftJoin(projectSkill.skill)
.where(projectSkill.project.id.in(projectIds))
.fetch();
}
위 코드는 전체 프로젝트를 불러오는 과정에서 각 프로젝트의 skill들을 불러오기 위해 작성한 코드이다.
그러나 Skill 엔티티가 Lazy 로딩 방식으로 설정되어 있어 Skill 엔티티는 프록시 객체로 유지된다. 이후 스킬 객체를 사용하기 위해 호출할 때마다 데이터베이스에서 추가 쿼리가 실행되어 N+1 문제가 발생하였다.
public List<ProjectSkill> findProjectSkillsInProjectIds(List<Long> projectIds) {
return jpaQueryFactory
.selectFrom(projectSkill)
.leftJoin(projectSkill.skill).fetchJoin()
.where(projectSkill.project.id.in(projectIds))
.fetch();
}
ProjectSkill을 조회할 때 Skill 데이터를 Fetch Join을 사용하여 하위 객체까지 한 번에 로드하도록 변경하였다. Fetch Join은 연관된 엔티티를 한 번의 쿼리로 로드하는 JPA 기능으로, Lazy 로딩으로 인한 추가적인 데이터베이스 쿼리 실행(N+1 문제)을 방지할 수 있다. 이를 통해 반환된 ProjectSkill 리스트에서 연관된 Skill 데이터는 프록시가 아닌 실제 엔티티로 초기화된 상태로 포함된다.