오늘은 너무 유명한 N+1 문제와 이와 관련한 EntityManager 의 getReference 라는 메서드를 이야기 하려고 한다.
사실 N+1 이 발생하는 원인이나 해결책은 간단한데, 조금 애매한 부분이 있어서 혼란을 겪었다.
N+1 문제
N+1 문제는 자바 ORM 에서 객체 내부의 연관된 객체에 접근하려고 할때 발생한다. DB에서 객체를 로드해 올때 객체의 필드에 다른 엔티티가 포함되어 있는 경우, ORM 은 해당 엔티티의 정보를 전부 가지고 오지 않는다. (select query 를 생각해보면 당연하다. 연관된 다른 테이블의 자료는 조회해오지 않는다.) 그 대신 객체 행세(?)를 할 수 있는 프록시 객체(위임 객체)를 넣어주는데, 이 프록시 객체는 껍데기만 가지고 있을 뿐이고 변수에 데이터를 가지고 있지 않는다. 그런데 이 껍데기 뿐인 객체의 알맹이(변수)에 접근을 하게 되면, 그 때 해당 객체를 로드해오는 쿼리가 발생하면서 해당 프록시 객체도 온전히 영속화된 객체가 되는 것이다.
이 경우, 쿼리가 1번만 나가지 않고 추가적으로 N 번 더 나간다고 해서(만약에 연관된 객체가 List 형태라면 size 만큼 나가므로) N+1 문제라고 불리는 것이다.
생각해보면 간단한 문제이긴 한데 부끄럽게도 내가 아주 치명적으로 오해한 부분이 있다.
프록시 객체의 id 값 접근
나는 앞서 이야기한 대로 N+1 문제의 핵심은 "연관된 객체에 접근하는 것" 이라고 인식하였다. 당연히 맞는 말이긴 한데 내가 오해한 것은 getId() 로 해당 객체의 Id 값에 접근하는 것도 N+1 문제를 야기할 것이라고 생각했다는 것이다. 그래서 왜 객체간 연관을 해야하는 지 잘 이해하지 못했다. 그냥 필드에 다른 객체의 id 값(Long) 을 넣는 것이 낫지 않나? 라고 생각했었다.
결론부터 말하자면 잘못된 생각이었다. 프록시 객체는 Id 값을 가지고 있으므로, id 값에 접근해도 추가적인 쿼리는 나가지 않는다. 우리가 Select Query 로 특정 Table 의 자료를 조회할 때 연관된 엔티티의 Id 값을 가지고 오듯이, 해당 프록시 객체에는 Id 값이 담긴다.
getReference
위 고민에 뒤이어 정 반대의 의문이 들었는데, 그러면 특정 객체를 저장할 때 다른 영속화된 객체가 있어야할까? Table 에 자료를 저장할때 Foreign Key 만 가지면 연관관계를 정의할 수 있는데, 왜 굳이 객체가 있어야하지? 라는 의문이었다.
이 또한 굳이 디비에서 객체를 로드해와서 연관시킬 필요가 없다. EntityManager 는 getReference 라는 메서드로 "id 값 만 알면" 프록시 객체를 생성해 주기 때문이다. 프록시 객체는 id 값에만 접근하는 것이 가능하다는 것(추가 쿼리 없이)과 정 반대의 논리다.
추가 궁금증
간단한 내용이라 따로 덧붙일 이야기는 없는데, 만약 getReference 에 엉뚱한 id 값을 넣으면 어떻게 되는가? 가 궁금해서 실험을 해봤는 데
Caused by: org.postgresql.util.PSQLException: ERROR: insert or update on table "chatroom" violates foreign key constraint "fkam0i409hjtorov2sxo4b6emvf"
Detail: Key (user_id)=(100) is not present in table "users".
postgresql 기준 저장을 시도할 때 문제가 된다. 외래 키 보유하고 있는 테이블을 삭제하지 못하는 것과 같은 논리로
저장시 외래키를 확인하고, 없으면 예외를 터트리는 것 같다.