JPA에서 복잡한 쿼리 작성하기

PEPPERMINT100·2022년 6월 12일
2

서론

기존 MyBatis로 작성된 쿼리를 JPA로 옮기던 작업중이었다. MyBatis는 조금 자유롭게 SQL을 사용할 수 있기에 아키텍처를 잘 설계하거나 쿼리 작성에 무게를 두지 않으면 이런저런 쿼리들이 많이 작성되게 되는데

살펴보면 애초에 복잡한 쿼리를 작성할 필요가 없던 경우가 많았어서 가능하면 구조를 들어내면서 복잡한 쿼리 자체를 생성 하지 않는 방향으로 가려고 했다. 그런데 복잡한 쿼리를 간단하게 바꾸려고 하니 리포지토리 레이어 이전의 구조를 들어내야 했고 작업 시간이 너무 많이 걸리는 문제가 생겼다. 사이드가진짜너무많이났다

또 복잡한 쿼리문들을 전부 JPA로 옮기려니 당연히도 JPA용 쿼리생성 라이브러리를 택할 수 밖에 없었다.

JPA에서는 복잡한 쿼리를 작성하는 방법이 대표적으로 4가지 정도로 나뉜다. Method Naming, JPQL, Criteria API, QueryDSL에 대해서 간단히 알아보고 어떤 기술을 사용하기로 했는지 왜 그 기술을 택했는지에 대해 개인적인 생각을 적어보려 한다.

Method Naming

제일 간단하다. JPA 인터페이스 내 메소드 이름만으로 복잡한 쿼리를 작성할 수 있다. 단 정해진 컨벤션이 있기 때문에 잘 살펴보고 작성해야 한다.

컨벤션을 전부 외우고 있지 않아서 작성 할 때마다 공식문서를 보고 작성을 하는데, 그래도 아무런 문제가 없을 정도로 간단하다.

단점이라고 한다면 먼저 타입 검사가 안된다. IntellJ Ultimate을 사용하면 엔티티를 읽어서 어느정도 자동완성을 잡아주는것 같지만 거기에 따르지 않더라도 코드 작성 단계에서는 오류를 잡아주지 않는다.

또 코드가 복잡해지면 메소드 이름 자체가 엄청 길어지고 가독성이 좋지 않다.

List<User> findByUserNameAndAddressAndAgeLessThanTop1OrderByAgeDesc(String userName, Address address, int age);

뭐 이런식으로 메소드 이름 자체가 쿼리가 된다. 읽으면 이해가 되지만 매번 저걸 다 읽어야 한다는 것 자체가 유지보수 경험에서 별로 좋지 않다. findByUserName 여기에 +@ 조건 하나 정도까지는 Method Naming으로 작성을 하되 조건이 3개 이상이 되면 가능하면 사용하지 않으려고 한다.

참고로 이 영상에서 JPA Method Naming 부분을 보면 Method Naming 방식으로 Join 쿼리, Projection 등 생각보다 복잡한 부분까지 지원하는것을 확인할 수 있었다.

JPQL

JPQL은 JPA 엔티티를 대상으로 하는 특수한 객체지향 쿼리이다. SQL과 굉장히 비슷하며 테이블이 아닌 JPA 엔티티로부터 쿼리한다. 에일리어싱이 필수적으로 들어가야 하며 SQL의 몇몇 문법은 지원하지 않는 특징을 가지고 있다. 하지만 SQL에 익숙한 개발자라면 금방 복잡한 쿼리를 작성할 수 있게 된다.

단점으로는 문자열 그대로 쿼리를 적기 떄문에 컴파일 단계에서 오류를 검출해낼 수 없다.

가독성면에서는 개발자에게 익숙한 SQL을 그대로 입력한다는게 장점이 될 수 있지만 자바 코드가 아닌 SQL 코드를 타이핑 해야한다는 사실 자체가 단점으로 다가올 수 있다.

Criteria API

Criteria API는 JPQL을 자바 코드로 작성할수 있도록 도와준다. 자바 코드로 작성한다는 것은 결국 문법 오류를 컴파일 단계에서 잡아준다는 뜻이 된다.

하지만 Criteria는 여기서 소개하는 방법들 중에 가장 복잡한 방식을 가지고 있다. 코드 자체도 길어지며 결국 가독성 면에서 별로 좋지 않다.

CriteriaBuilder cb = em.getCriteriaBuilder();

CriteriaQuery<Member> cq = cb.createQuery(Member.class);

Root<Member> m = cq.from(Member.class);
cq.select(m); 

TypedQuery<Member> query = em.createQuery(cq);
List<Member> resultList = query.getResultList();

위 코드는 select m from Member m 이라는 반 줄짜리 쿼리를 생성해낸다. 간단한 쿼리임에도 코드양이 많아진다.

QueryDSL

QueryDSL은 블로그에도 몇 번 글을 쓴적이 있다. Hibernate Entity Manager로 JPAQueryFactory를 생성해서 자바코드로 쿼리를 작성한다.

근데 또 자바 코드가 굉장히 SQL과 비슷하며 100% 완벽한 타입 검사를 해준다.

이는 QClass라고 하는 메타데이터 모델을 통해서 가능한 부분인데, 결국 이 QClass를 만들어주는 수고가 필요하다는 단점이 있다.

    List<Member> members = query.selectFrom(QMemeber)
    	.where(Qmember.id.eq(memberId)
        .orderBy(Qmenber.age.desc())
        .fetch();

이런식으로 작성이 되는데 가독성도 좋고 코드도 짧고 타입 세이프하다는 장점으로 가장 많이 채택하게 되는 라이브러리이다.

여기까지 각각 방식에 대한 간단한 설명이다. 위와 같은 설명은 각각 키워드로 구글링하면 더 자세한 정보를 찾을 수 있다. 자세한 설명은 여기서 필자가 말하고 싶어하는 바에 벗어나므로 이정도만 적도록 하겠다.

그래서

라인개발실록이라는 유튜브에서 코틀린 kdsl이라는 쿼리 생성 라이브러리를 만들게 된 배경을 소개하는 영상이 있는데, 여기서 처음에 라인의 서버 개발 스택을 알려준다. 영상 초반에서 확인할 수 있는데, JPA의 쿼리 생성 라이브러리로 QueryDSL이 아닌 Criteria API를 채택한 이유를 설명해준다.

영상에서 설명한 이유는 아래와 같았다.

  1. 메타데이터 클래스를 생성해야한다.
  • 메타데이터 클래스 생성 자체가 개발 경험상 좋지 않다.
  • 빌드 순서가 꼬이면 에러가 날 수 있고 원인 찾기가 힘들다.
  1. IDE도 QueryDSL용 세팅을 해줘야 한다.
  • IDE에서 어노테이션 프로세싱을 켜줘야 한다.
  • 인텔리제이 기준으로 QueryDSL이 생성해주는 폴더를 소스 폴더로 지정해주어야 IDE에 에러가 안생긴다.(이 부분은 영상에 나온 부분은 아니고 제 경험입니다.)
  1. JPA 공식 지원이 아니다.
  • 2018년 3월부터 업데이트가 없다.

결국 개발을 편하게 하려고 하는 QueryDSL이 개발 경험을 해치고 또 원인을 찾기 힘든 에러를 만들수 있다는 점과 공식 지원이 아니라는 부분에서 라인이라는 거대한 기업이 채택하기에는 조금 무리가 있다는 의견을 말했다.

영상은 이어서 QueryDSL을 사용하지 않고 Criteria를 사용하기로 결정했지만 Criteria 특성상 복잡한 코드도 역시 불편해 라인에서 직접 코틀린용으로 라이브러리를 만들었다는 내용으로 이어진다.

영상을 보고 나서

굉장히 많은 걸 느꼈다. QueryDSL을 공부하고 사용하게 된 이유는 코드 자체도 깔끔하다는 면도 있지만 배달의민족이 주스택으로 사용한다는 점이 컸다. 배민에서 선택했으면 라이브러리에 안정감이 있는거니까 잘은 모르지만 내가 이해 못하는 그만한 이유가 있겠지라는 안일한 생각이었다.

물론 QueryDSL이 당장 유지 안되고 사라지는 것을 걱정해야하는 라이브러리는 아니다. 최근 동향을 보면 4월에도 RDB가 아닌 요즘 핫한 NoSQL DB에 대해 들어오는 풀리퀘를 적용해주는 듯 계속해서 유지보수를 하려는 움직임은 보인다.

하지만 QueryDSL을 처음 부분 도입했을 때 사수는 QueryDSL을 전혀 몰랐어서 빌드할 때마다 신경써줘야 하는 부분에서 부정적으로 생각했다.

또 QueryDSL을 사용하기 위해 JPAQueryFactory를 Entity Manager 마다 생성하고 QClass 제너레이팅해주고 build.gradle 혹은 pom.xml에(이마저도 maven이냐 gradle마다 또 다르다) 세팅하는 과정이 굉장히 귀찮게 느껴지긴 했다.

하지만 한 번 다 해두면 개발 경험에서 좋았는데, 위 과정이 개발 경험을 해친다는 생각은 못했던 것 같다.

또 라이브러리를 채택하여 회사에 도입하고 그 라이브러리 공부를 위해 내 리소스를 쏟는데, 조금 더 신중한 생각을 할 필요가 있다고 생각했다. 돌이켜보면 정말 롱런하는 코드 작성을 위해서는 Criteria를 선택하는게 맞지 않을까 싶다. 지금은 QueryDSL로 작성된 한 모듈의 쿼리를 전부 Criteria로 변경을 해보았다.

복잡하다고는 하는데 코드가 길뿐 그렇게 어렵지는 않았다. 또 QueryDSL 내부가 Criteria로 작성되어 있다보니 QueryDSL의 내부를 이해하는데도 도움이 됐다.

앞으로는 복잡한 쿼리는 Criteria로 작성을 하면서 또 Criteria의 장단점을 직접 몸으로 느껴봐야겠다. 어떤 라이브러리를 선택해야하고 그 근거는 무엇이다라는 답을 내기에는 아직 너무 주니어인 나...

Criteria의 Root 객체로부터 컬럼을 가져올때 결국 스트링으로 컬럼명을 쓰게 되는데 이 부분을 일단 불편하게 느꼈다. 작성이 불편한건 아니고 그냥 마음이 불편하다.

이 외의 단점은 앞으로 몸으로 느껴봐야 한다. 사실 일 처음 시작할 때는 아직 신입이니까 괜찮다라는 마인드가 좀 있었다. 개발자로 일을 한지 1년이 조금 넘은 이 시점에서는 아직 주니어니까 괜찮겠지라는 생각을 한다. 이 생각이 언제까지 나를 지켜줄 수 있을까?

profile
기억하기 위해 혹은 잊어버리기 위해 글을 씁니다.

0개의 댓글