본문 바로가기
개발/NHibernate

NHibernate one-to-one Select N+1 문제 해결....

by 그저그런보통사람 2011. 9. 18.
지난 글에 표준 Linq를 제공하는 NHibernate.Query<T>()에서 one-to-one에서 Select N+1 문제를 일으킨다는 내용을 보셨을 겁니다. (최근이죠...)
디밥 (http://debop.egloos.com/) 님께 자문을 구해 몇가지 해결책을 얻어 해결했기에 그 내용을 적어볼까 합니다.

저의 프로젝트 목적의 가장 첫 번째 기준은 "유연성"입니다. 유연성이라는 단어로 여러가지 장점 (확장성, 유지보수성, 테스트 용이성, 기타 등등)을  표현할 수 있겠는데요, 제가 목표로하는 유연성을 위한 구체적인 방법 중 하나는 "분리" 입니다.
구체적인 (코딩, 프레임워크 사용 등) 것 중에 하나인 하이버네이트는 ORM 도구로 데이터베이스의 데이터를 도메인 모델 클래스과 매핑해주는 도구입니다. 즉, 데이터 엑세스 계층 (Data Access Layer) 라는 말인데, 저는 이 구체적인 도구가 비즈니스 계층 (Business Logic Layer)에 노출되길 원하지 않았습니다. 제가 원하는 구체적인 "분리" 중에 하나입니다.
"분리"가 되면 비즈니스에 전혀 노출 되지 않기 때문에 데이터 엑세스 계층에서 하이버네이트를 엔터티 프레임워크 (Entity Framework)로 교체를 한다해도 영향을 주지 않는 장점이 생깁니다. (인터페이스로 시그니처를 선언하고 구현부를 두는 다형성을 이용해서 개발을 해야겠지요. 클래스만으로 구현하면 종속성이 있는 모든 계층이 영향을 받습니다... !!! )

문제는 지난 글에서처럼 Select N+1 문제를 특정 도메인 모델 규칙 (1:1 식별관계)에서 나타났고, 며칠 (솔직히 한달 이상 고민했습니다...;;)을 씨름하다 포기상태에 놓였더랬죠...

표준 Linq를 지원하는 Query<T>() 를 사용하면 one-to-one 에서 무조건 Select N+1 문제가 발생합니다.
이를 해결하려면 Fetch() 메소드를 활용하여 즉시(Eager) 호출을 이용하면 되지만, Count() 메소드를 호출하지 못합니다.
이도저도 못하는 상황에서 디밥님이 제시해주신 방법은 QueryOver()를 사용하는 것이었습니다.
QueryOver는 기존 프로젝션을 위한 Criteria API를 .NET 에서 최근 활용되고 있는 람다식으로 제공하는 API입니다.
Fluent 방식에 익숙하고 람다와 Linq를 활용해 보셨다면 쉽게 접근할 수 있는 방법이며, 상당히 다양한 기능을 제공하고 있습니다. (Query<T>()에 비하면요...)

QueryOver를 사용하니 one-to-one 관계에서 별도의 조작없이 자동으로 left join이 되더군요... 쿨럭....
(디밥님께 정말 감사하게 생각하고 있습니다 ㅜㅜ ;)

다만 QueryOver를 사용하게 되면 발생하는 문제가 제가 원하는 "분리"를 할 수가 없어집니다.
왜냐하면 QueryOver에서 제공하는 람다식은 .NET 표준 람다식이라기 보다 커스터마이징된 람다식이기 때문입니다.
지난 글 댓글로 남겼는데 Contain() 메소드는 "포함하는 것"를 찾아주는 확장 메소드 .NET 제공 메소드입니다.
하지만 QueryOver Where()에 Contain()를 호출하면 예외가 발생합니다. 




아래와 같이 해줘야합니다. 




IsLike() 는 하이버네이트에서 제공하는 메소드입니다. Where() 메소드는 동일하지만 분명 동작하는데는 차이가 있습니다.
그러다보니 비즈니스 계층에서 조건절을 적용하려고 한다면(프로젝트를 물리적으로 분리) 하이버네이트 라이브러리를 참조해야합니다.
제가 위에서 언급했던 비즈니스 계층에 대한 특정 라이브러리 종속성 분리가 어렵게 되버린 것이죠.... 

그럼, 데이터엑세스 계층에서 사용하면 되지 않느냐? 반문하실지 모르겠는데요, 데이터 엑세스 계층에서 저는 Repository 패턴을 사용합니다. Repository 패턴은 In-Memory Database 처럼 접근하는 개념인데, Repository에는 비즈니스 로직이 존재해서는 안된다는 것이 저의 생각입니다. 즉 1억건의 회원 데이터가 있다고 가정하고, 1억건 중에 탈퇴 회원만 조회한다는 비즈니스 요구 사항이 있다면 이 로직은 비즈니스 계층에 있어야 하지 Repository에 있어서는 안된다는 의미입니다.

다만 성능적인 이슈가 발생하는 쿼리에 대해서는 별도로 데이터베이스의 "뷰"라는 개념을 적용하여 유연성을 남겨놓기로 했습니다. 

어째든 QueryOver를 사용하게 되면 조건절이나 페이징 같은 로직이 비즈니스 계층에서 사용되기 위해선 라이브러리 참조가 필수가 되버립니다. 또한 ORM 도구의 변경이 (거의 그럴일은 없겠지만) 발생한다면 비즈니스 계층까지 수정을 해야하는 상황이 발생하기도 하구요...
(이러한 규칙은 제가 나름데로 정한 규칙입니다. 즉, 이것이 정답이다! 라고 내세운 규칙이 아님을 알려드리고 싶습니다. 간혹 현상만 보고 판단하시는 분들이 많아서 ^^;;;)

기본적인 매핑 방법으로 one-to-one을 처리하면 Query<T>()에서 무조건적인 Select N+1이 발생한다고 했었습니다.
이제 그 문제를 해결하는 두번째 방법을 디밥님께서 제안해주셨는데....
바로 관점을 달리하는 것입니다. one-to-one을 처리하는 기본적인 매핑방법에 대한 관점을 다르게 바라보는 것인데요....
one-to-one은 데이터베이스의 1:1 식별관계를 나타냅니다.
이 데이터베이스의 1:1 식별관계의 "식별관계" 관점에서 접근합니다. 식별관계가 무엇인지는 따로 설명하지 않겠습니다. (구글신이 이미 친절하게 답을 준비해놓고 있으니까요 ^^)

그래서 나온 방법이 Join 매핑을 사용하는 것입니다. 


위 매핑의 검은선 박스가 기본적인 one-to-one 방식인데 이 부분을 아래와 같이 변경합니다.


첫번째 방식은 테이블과 도메인 모델을 각각 두고 관계를 형성하는 방식이고, 아래 방법은 테이블은 따로 두지만 도메인 모델은 통합해서 사용하는 방식입니다. 
Optional 이 true면 left outer join이 되며 false 및 별도로 지정하지 않는다면 inner join이 됩니다.

위와 같이 수정하고 다시 Query<T>()로 호출하면 깔끔하게 left outer join이 되어 Select N+1 문제가 해결되었습니다.