Mybatis에서 JPA로
Mybatis 로 시작하다
입사를 해보니 모든 시스템의 로직이 DB의 프로시저로 개발되어있는 조직이였다.
처음에는 시스템 사용률이 높지않아서 DB 1대로 커버가 가능하였지만
점점 서비스가 확장되면서 유지보수가 어려워지고 DB의 부하가 커져만 갔다.
그러던 어느날 새로운 서비스를 오픈하면서 문제는 발생하였다.
여태 서비스하였던 것과는 비교적 트래픽이 높은 서비스였고 이정도의 트래픽을 감당할 수 있는 노하우가 없던 조직이라 오픈하자 말자 서버가 터졌다…
특히, 데이터가 중요한 조직이라 서버가 터진것은 매우 큰 이슈였고 복구를 하기위해 여러 시도를 하였지만… 일주일간 서버가 정상작동이 어려웠다.
어떻게 해서 간신히 응급조치는 하였고
원인은 DB에 모든 로직들로 인해 부하가 매우 높다는것으로 진단하고
우리는 작은 서비스 부터 spring/java를 사용하여 was로 로직들을 모두 빼는 작업을 하였다.
이 때 Mybatis를 사용할지 JPA를 사용할지에대한 고민이 있었지만
JPA의 경우 러닝커브가 높고 우리 조직의 쿼리 복잡성을 커버 할 수 없다고 판단하여 Mybatis로 시작하게되었다.
Mybatis의 문제점
대분의 서비스를 spring/java로 로직을 옮기게 되었고 몇 개월 간은 무리 없이 서버를 운영 할 수 있었다.
그러나, 요구사항들이 늘어나게되고 각각의 서비스들이 성장하면서 문제는 발생하게 되었다.
일단, 급하게 로직들 전환하던 터라 객체지향적으로 개발을 고려하지 못하여 동일한 쿼리들이 반복적으로 작성되었고
하나가 변경되면 매우 많은 쿼리들을 변경 해야했다.
변경점이 잘 정리되어 있으면 문제가 없지만,
대부분 개발하다 보면 정리가 잘되지 않아서 A만 변경되고 B는 변경되지 않는 이슈로 인해
처리하는데만 시간을 많이 소모하여 생산성이 매우 저하되었다.
Mybatis를 우아하게 써보자
위 문제를 보완하기 위해서 Mybatis의 resultMap, collection, association, extends, include를 활용하여 쿼리를 객체지향적으로 관계를 맺고 공통화 하여 사용하기로 하였다.
그러면, 쿼리들을 변경점을 최소화 시켜 유지보성이 높혀 생산성을 높여줄것을 기대하였다.
효과는 매우 좋았다. 컬럼 추가/수정/삭제 시 여러 부분들을 변경하지 않아도 되었고
전체 관계 구조만 알고 있으면 어떤 부분에 이슈가 발생하는지 한눈에 들어오기 때문에 이슈 대응 시간도 줄이게 되었다.
그러나, 변경 후에 성능이슈가 계속 발생하게 되었다..
N + 1 문제...
매우 느리게 조회되는 현상이 있어서 Mapper를 확인 중에 이런 생각을 하게 되었다.
"collection과 association은 mapper에서 관계를 지정하는데 어떻게 DB를 호출하는 것일까?"
라는 의문이 들었고 직접 쿼리를 테스트 해본 결과….
아니나 다를까 1:N 관계의 객체들에서는 부모 데이터의 1건당 자식 데이터 여러건을 DB에 호출하여 N + 1 문제를 내고있어 성능저하를 불러오고 있던것이였다.
(N + 1 문제는 https://datajoy.tistory.com/m/170?category=836315 참고)
그래서, 급하게 해결할수있는 방법을 찾게되었고
Mybatis에서는 다행히도 이를 해결해 줄 수 있는 방법을 제시하고 있었다.
쿼리를 JOIN하여 한방으로 가져와서 resultMap에 매핑하는 Nested Results 방식이였다.
기존 방식은 Nested select 방식이 였고
Nested select 방식의 경우 유지보수 성이 높으나 N + 1의 문제를 갖고 있었다.
그래서 Nested select 방식으로 작성된 쿼리들을 모두 Nested Results 방식으로 변경하게 되었고 N + 1 문제도 해결하였다.
다만, 또 다른 문제가 발생하게 되었다.
Nested Results 방식의 문제점
Nested Results 방식으로 개발하다 보니 성능이슈는 없었으나 Mapper에는 Join이 엄청걸린 통자 쿼리들이 늘어나기 시작했다.
Join 조건이 바뀌게되면 매우 당황스럽다.
어마어마한 통자쿼리를 수정을 하는것은 매우 쉽지않는 일이다.
특히, 새로운 개발자 분들이 영입되면서 문제는 더욱 심각해지기 시작했는데
어떤 한분은 이전 조직에서 DB중심으로 개발을 해왔던 개발자라 모든 로직들을 쿼리에 넣기 시작하는 것이 였다.
우리가 DB중심으로 개발하였다가 부하 이슈로 인해 was로 로직을 옮겼는데
이분은 다시 거꾸로 DB 중심으로 개발하니… 환장할 노릇이다.
여러번의 설득과 지향점을 이야기 해도 소용없다. 연차도 높은분이라 본인이 해왔던 습관들을 고치기는 어려웠다.
물론 영입을 잘하면 된다고는 하지만 어디 그게 쉬운일인가
나는 이 문제를 Mybatis가 쿼리에 대한 너무 많은 자유성을 주기 때문이라고 판단하였고
그러면서 이 구조에 대해서 다시 고민에 빠지기 시작했다.
"이게 정말 객체지향스러운것이 맞는가?”
“결국 쿼리에서 JOIN하고 resultMap에서 객체지향을 표현한들 쿼리는 객체지향이 아니지 않는가..?”
“Join되면서 반복되는 쿼리가 생산되어 정말 이게 유지보수성이 높은가?”
“그리고 xml로 작성하니 휴먼 에러 찾기 힘들고 디버깅도 힘들어서 생산성이 떨어지고 가독성도 떨어져서 눈에 잘들어오지 않고.."
“어떻게 하면 우리 조직의 개발자들이 객체지향적으로 유지보수성 높게 개발을 할 수 있게 할까?”
한 달동안 여러 고민에 빠져서 살기 시작했다.
JPA를 도입하자
고민에 빠져있다가 갑자기 이전에 검토해본 JPA가 떠올랐다.
“JPA는 이런 문제를 어떻게 해결하였을까”라는 의문이 들어 JPA를 다시 공부하기 시작했다.
JPA를 공부하면서 ORM 기술을 알게되었고
"내가 원하던 객체지향스러운 개발을 할 수 있게 해주겠구나"라는 느낌이 왔었다.
공부하면 할 수록 정말 편한 기술임을 알게되었다.
러닝커브가 존재하긴하지만 잘 알고 쓴다면 매우 유용한 기술임을 깨달았다.
특히, JPA에서는 “어떻게 N + 1 문제를 해결했을까?” 의문이 있었는데 해결방법이 놀라웠다.
JPA에서는 여러방법으로 해결방안을 제시하는데
Mybatis Nestied Results 방식처럼 통자쿼리를 Join하는 방식이 있었고, IN 구문을 통해서 매핑되는 대상의 데이터를 모두가져와서 자바단에서 매핑을 해주는 방식이 있었다.
(JPA에서는 batch 기능입니다.)
DB와의 네트워크 비용을 줄여 성능은 높이고, 객체지향성은 유지 할 수 있는 두 마리 토끼를 잡는 방법이였다.
또한, JPA는 인터페이스라서 쿼리 작성에 대한 자유도는 Mybatis 보다는 낮기 때문에
이전 쿼리에 모든것을 때려 박는 개발자라도 좀더 객체지향적으로 생각 할 수 있을것이라고 기대가 되었다.
다만, “복잡한 쿼리 작성을 하다보면 Native 쿼리를 많이 쓰게 되어서 Mybatis에서의 동일한 문제는 존재한다” 라는 이야기가 주변에서 많았는데
이런 부분들은 QueryDSL이나 여러 JPA 관련 라이브러리들로 대응이 가능하기 때문에 문제가 되지 않는다고 본다.
물론, 무조건 JPA를 쓰자는 것은 아니다.
Mybatis도 잘쓰면 좋지만 쿼리 중심으로 개발하는 개발자와 같이 잘못쓰는 경우가 존재하기 때문에
좀 더 객체지향적으로 개발하기 위해서는 JPA를 사용하는것이 많은 도움이 될것이다.