티스토리 뷰
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를 사용하기 위해서는 아래와 같은 절차가 필요하다.
- query를 외부 dbms가서 짠다.
- 별도의 xml 파일을 만들고 쿼리를 그대로 붙여 넣는다.
- 입출력 객체를 생성 후 xml에 명시
- mapper interface 만든다.
- xml에 명시한 id와 동일한 이름, 입출력 형태를 갖는 매서드를 mapper에 생성
- 사용
여기서 동적 쿼리가 추가되면, 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보다는 편하겠지만, 구현해야하는 부분에 여전히 손이 간다.
- 테이블과 같은 이름을 갖는 entity 생성
- 별도의 repository interface에 CRUD 코드를 작성하고, interface 구현체를 만들어 쿼리 구현.
- 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에서 디버그까지 가능하다.
하지만 모든 것에 만능인 은빛탄환은 없듯이
다만 몇 가지 단점이 있는데
- 빌드 시 Q클래스를 만들어줘야되기 때문에 조금 시간이 더 걸린다.
- 서브쿼리를 JOIN절, FROM절에 사용할 수 없다.
- 러닝 커브
2번은 인프런의 김영한 선생님의 강좌에도 짚고 넘어간 부분인데,
이 부분은 쿼리를 개선하면 서브쿼리를 사용하지 않고도 구현이 가능하기 때문에 큰 문제는 아니라고 한다.
그렇게 되면 러닝 커브가 가장 큰 걸림돌인데.. 이건 시간이 해결해주리라 믿는다.
(실제로 적용한건 2~3달 전인데 현재는 너무나 잘 사용하고 있다.)
그리고, 성능 이슈는 없는 것으로 파악된다.
마치며
회사에서 몇 달전 QueryDSL을 적용하자고 발표하기 위해 만든 글을 각색해서 작성했다.
가까운 미래에 잠재적 문제(스프링부트 버전 업)를 내포하고 있지만, 현재까지는 너무나 잘 쓰고 있다.
Mybatis랑 다르게 쓸때마다 기분이 좋다.
다음 글로는 어떻게 스프링부트에 적용할 수 있는 지 작성해보겠다.
'개발 > DB' 카테고리의 다른 글
MySQL Auto Increment 값 재설정하기 (1) | 2024.02.16 |
---|---|
스프링부트에 QueryDSL 적용기 - 2 (설치 및 사용법, Spring Data JPA와 함께 사용하기) (0) | 2023.08.28 |
The MySQL server is running with the --read-only option so it cannot execute this statement 에러와 @Transactional (1) | 2023.06.01 |
[DB] 파티셔닝(Partitioning), 샤딩(Sharding) (0) | 2023.03.12 |
SQL Mapper, MyBatis (0) | 2023.01.01 |
- Total
- Today
- Yesterday
- terraform
- GIT
- docker
- serverless
- jenkins
- 스프링부트
- AWS
- 람다
- AWS EC2
- CloudFront
- springboot
- openAI API
- Elastic cloud
- 코딩테스트
- Spring
- java
- cache
- EKS
- lambda
- awskrug
- chat GPT
- Kotlin
- AOP
- MySQL
- S3
- OpenAI
- Log
- JWT
- elasticsearch
- ChatGPT
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |