티스토리 뷰

SI 회사에서 일할 무렵엔 항상 MyBatis만 사용했었다.

(생각해보니 현재 기준으로 반년도 안지났다.)

 

JPA를 사용하자고, 사용해보자고 자주 이야기했지만 결국 도입에 실패했던 기억이 있다. ㅠ

 

그런데 이직한 곳도 막 서비스가 런칭한지 얼마 안됐음에도 MyBatis를 쓰고 있었다.

 

하지만 여긴 JPA 도입을 권장해줘서 JPA를 사용해볼 수 있었는데,

 

사용하면서 마음에 들지 않는 부분들이 보이기 시작했다.

 

가장 큰 문제점이라 생각하는건 쿼리가 복잡해지면 JPQL이란걸 사용해야 했다.

 

아래는 JPQL의 예시다.

public interface HistoryRepository extends JpaRepository<UserActivity, Long> {

    @Query("SELECT h FROM UserActivity h WHERE h.timestamp BETWEEN :startDate AND :endDate AND h.userId = :userId")
    List<UserActivity> findUserActivitiesBetweenDates(
        @Param("startDate") LocalDateTime startDate,
        @Param("endDate") LocalDateTime endDate,
        @Param("userId") String userId
    );
}

이걸 보니 내가 하고자했던 걸 알게 됐다.

 

내가 하고자했던건 단순히 JPA를 사용해보자가 아니라,

 

코드 단에서 네이티브 쿼리를 지워버리고 싶었던 것이다.

 

모두가 나와 같은 생각을 할지는 모르겠지만, 이리저리 찾아본 결과 몇 가지 대안을 찾을 수 있었고

 

이 중 선택된게 QueryDSL이다.

 

우선 다른 선택지들을 선택하지 않은 이유를 짚고 넘어가자.

 

Mybatis

Mybatis가 뭔지에 대한 설명은 생략한다.

 

MyBatis를 사용하기 위해서는 아래와 같은 절차가 필요하다.

 

  1. query를 외부 dbms가서 짠다.
  2. 별도의 xml 파일을 만들고 쿼리를 그대로 붙여 넣는다.
  3. 입출력 객체를 생성 후 xml에 명시
  4. mapper interface 만든다.
  5. xml에 명시한 id와 동일한 이름, 입출력 형태를 갖는 매서드를 mapper에 생성
  6. 사용

 

여기서 동적 쿼리가 추가되면, xml 파일 내부의 복잡도가 더 상승한다.

<select id="findActiveBlogLike" resultType="Blog">
  SELECT * FROM BLOG
  <where>
    <if test="state != null">
         state = #{state}
    </if>
    <if test="title != null">
        AND title like #{title}
    </if>
    <if test="author != null and author.name != null">
        AND author_name like #{author.name}
    </if>
  </where>
</select>

위와 같이 동적 쿼리가 들어가면, DBMS로 쿼리를 그대로 가져다 쓸 수 없기 때문에 테스트가 어렵고 유지보수가 까다로워진다.

그리고 쿼리 외의 다른 문구가 들어가기 때문에 가독성이 떨어진다. (코드 품질 저하) 

가장 큰 문제는 xml에서 쿼리를 작성 하기 때문에 IDE의 도움을 받을 수 없다. 

컴파일단에서 에러를 찾을 수 없기 때문에, 빌드를 해야만 에러를 발견할 수 있다. (생산력 저하) 

 

특이사항으로는 xml 파일 내부에서 <= 사용이 불가능하다. (<![CDATA[<=]]> 이런 식으로 써야됨) 

 

몇 년 사용해봤지만 성능을 떠나서 사용성은 진짜 별로였다.

 

그래서 ORM인 JPA를 많이 사용한다.

 

JPA(Java Persistence API)

그렇다고 해서 단일 JPA만 사용하면 Mybatis보다는 편하겠지만, 구현해야하는 부분에 여전히 손이 간다.

 

  1. 테이블과 같은 이름을 갖는 entity 생성
  2. 별도의 repository interface에 CRUD 코드를 작성하고, interface 구현체를 만들어 쿼리 구현.
  3. entityManager라고 하는 엔티티들을 관리하는 manager를 설정하고 트랜잭션을 관리하도록 해야한다.

 

생각보다 2번 과정에서 많은 공수가 필요하고, 중복 코드가 많아진다.

 

그래서 Spring Data JPA 를 사용한다.

 

매서드의 네이밍을 통해 쿼리를 자동 생성해주며, 아래와 같은 기본적인 쿼리들을 제공한다.

 

 

where 절도 다양한 기능을 제공하니 이 문서를 확인해보면 좋다.

 

Spring data JPA를 사용하게되면 interface만 선언해도 별도의 구현체 없이 JPA의 쿼리 기능을 바로 사용 가능하다.

 

아래와 같이 네이밍 규칙을 반드시 지켜줘야 한다.

@Repository
public interface UserActivityRepository extends JpaRepository<MissionReward, Integer>, UserActivityQuerydslRepo {
    List<MissionReward> findActivitiesByUserAndDateRange(LocalDateTime start, LocalDateTime end, String userId, String activityType);
}

pk를 사용한 findBy(PK명)과 같은 경우는 위와 같이 인터페이스 내부에 선언할 필요 없이 사용할 수 있다.

 

하지만, JPA도 만능이 아니라 복잡한 다중 Join, 서브쿼리 등을 사용하게 되면 JPQL을 사용하게 된다.

 

결국 네이티브 쿼리가 코드 안으로 들어올 수 밖에 없다는 것이다.

 

코드 안으로 네이티브 쿼리가 들어오는 경우를 피하고 싶었다.

 

JOOQ(Java Object-Oriented Querying)

처음 듣는 사람도 있을 것 같다. 생소하지만 강력한 기능을 제공한다.

 

문제는 설정법부터가 좀 어지러워서 가장 먼저 후보군에서 제외됐다.

 

설정법은 링크로 대체한다.

 

아래는 사용 예시이다.

public List<UserActivity> findActivitiesByUserAndDateRange(LocalDateTime start, LocalDateTime end, String userId, String activityType) {
    return dslContext.selectFrom(QUserActivity.userActivity)
        .where(QUserActivity.userActivity.timestamp.between(start, end)
            .and(QUserActivity.userActivity.userId.eq(userId))
            .and(QUserActivity.userActivity.activityType.eq(activityType)))
        .fetchInto(UserActivity.class);
}

내가 찾아다녔던 "코드 내에서 쿼리를 처리하자"라는 취지에는 맞는 기능이다.

 

다만 우리 백엔드 개발팀에서는 NoSQL DB(Dynamo DB)를 적극적으로 쓰고 있는데,

 

JOOQ는 NoSQL DB(ex. MongoDB, DynamoDB)와 호환이 안된다고 한다.

 

그래서 최종적으로 선택하게된 게 QueryDSL이었다.

 

QueryDSL 

QueryDSL을 간단히 살펴보고 들어가자.

@Override
public List<MissionReward> findActivitiesByUserAndDateRange(LocalDateTime start, LocalDateTime end, String userId, String activityType) {
    JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);

    return queryFactory.selectFrom(userActivity)
        .where(userActivity.timestamp.between(start, end)
                .and(userActivity.userId.eq(userId))
                .and(userActivity.activityType.eq(activityType)))
        .fetch();
}

매서드를 하나 가져온건데, 쿼리를 코드처럼 사용이 가능해서 컴파일 단에서 오류를 확인할 수 있다는 장점이 있다.

 

또한 쿼리를 작성하는데 IDE의 자동 완성 기능의 도움을 받을 수 있고, IDE에서 디버그까지 가능하다.

 

하지만 모든 것에 만능인 은빛탄환은 없듯이

 

다만 몇 가지 단점이 있는데

 

  1. 빌드 시 Q클래스를 만들어줘야되기 때문에 조금 시간이 더 걸린다.
  2. 서브쿼리를 JOIN절, FROM절에 사용할 수 없다.
  3. 러닝 커브

2번은 인프런의 김영한 선생님의 강좌에도 짚고 넘어간 부분인데,

 

이 부분은 쿼리를 개선하면 서브쿼리를 사용하지 않고도 구현이 가능하기 때문에 큰 문제는 아니라고 한다.

 

그렇게 되면 러닝 커브가 가장 큰 걸림돌인데.. 이건 시간이 해결해주리라 믿는다.

(실제로 적용한건 2~3달 전인데 현재는 너무나 잘 사용하고 있다.)

 

그리고, 성능 이슈는 없는 것으로 파악된다.

마치며

회사에서 몇 달전 QueryDSL을 적용하자고 발표하기 위해 만든 글을 각색해서 작성했다.

 

가까운 미래에 잠재적 문제(스프링부트 버전 업)를 내포하고 있지만, 현재까지는 너무나 잘 쓰고 있다.

 

Mybatis랑 다르게 쓸때마다 기분이 좋다.

 

다음 글로는 어떻게 스프링부트에 적용할 수 있는 지 작성해보겠다.

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함