Search

JdbcTemplate 를 JPA로 손쉽게 갈아껴보자

태그
우테코
Java
Spring
작성 상태
작성 완료
작성일
2024/05/15
참고 링크 2

아니 내 코드 다 지우라고?

우테코 레벨 2 3번째 미션을 수행하면서 기존의 JdbcTemplate을 사용한 코드를 Spring Data JPA를 이용하도록 수정하라는 요구사항을 만났다. 그리고 미션을 소개하는 시간에 코치 브라운이 이렇게 말했다.
아쉽지만 기존에 있던 JdbcTemplate 코드는 지우세요!
꼭 그렇게 다 가져가야만 속이 후련하십니까? ㅠㅠㅠㅠ

어떻게 하면 내 코드를 최대한 덜 바꿀까?

레벨 1때도 비슷한 고민을 했다. 그때는 지금의 코드에 어떤 문제가 있었지만, 코드를 최대한 조금 수정하고 문제를 해결하고 싶어했다. 당시에 코치 구구와 이야기 했었는데 구구는 이런식으로 말했다.
지금 엎는게 제일 쉬운 방법이에요.
나중에 지금 덮어둔 문제가 큰 문제가 될 수 있으니 미리 싹을 뽑아놓는 것이 좋다는 취지로 이해했다. 그리고 결국 구구의 말을 듣지 않고 고집부리다가 하루를 날리고 결국 엎었다. 시간도 날리고 목표도 이루지 못한 것이었다. 시간은 흘러 레벨 2의 절반이 지나간 지금, 다시 선택의 갈림길 앞에 섰다.
인간은 같은 실수를 반복한다지만, 시도를 하지 않으면 발전도 없을 것이다. 이번에는 반드시 적은 수정으로 큰 이득을 얻어보고 싶었다.

그냥 extends JpaRepository<Domain, Long> 붙이면 안되나?

처음에는 단순히 extends JpaRepository<Reservation, Long>만 붙이면 되는줄 알았다.
package roomescape.repository; // import 생략 public interface ReservationRepository { Reservation save(Reservation reservation); List<Reservation> findAll(); List<Reservation> findByMemberAndThemeBetweenDates(long memberId, long themeId, LocalDate start, LocalDate end); List<Reservation> findByMemberId(long memberId); boolean existsByThemeAndDateAndTime(Theme theme, LocalDate date, ReservationTime reservationTime); boolean existsByTime(ReservationTime reservationTime); boolean existsByTheme(Theme theme); void delete(long id); }
Java
복사
원래 ReservationRepository 아주 교묘하게 JPA QueryMethod 규칙에 어긋난다.
만약 단순히 인터페이스 뒤에 extends JpaRepository<Reservation, Long> 를 붙여준다면,
package roomescape.repository; // import 생략, 문제 없는 메서드 생략 public interface ReservationRepository extends JpaRepository<Reservation, Long>{ List<Reservation> findByMemberAndThemeBetweenDates(long memberId, long themeId, LocalDate start, LocalDate end); void delete(long id); }
Java
복사
원래 ReservationRepository 아주 교묘하게 JPA QueryMethod 규칙에 어긋난다.
이 두 메서드가 QueryMethod 규칙을 모호하게 위반한다. 하지만, 이는 인텔리제이의 리팩토링 기능을 이용하면 간단히 해결할 수 있는 수준이다. 하지만, 진짜 문제는 따로 있다.

내 Fake가 이상해요!

기존 Repository의 Collection 기반 구현체에 수많은 메서드를 추가해야만 한다!!!
이전 미션에서 Servic 레이어가 Repository의 SQL에 상관 없이 잘 동작하는지 확인하기 위해 Collection 기반 Fake 객체를 만들었다. 그런데, 기존 인터페이스가 JpaRepository를 확장한다면 수많은 메서드를 구현해야 하는 상황이 생긴다.
이전 미션에서 열심히 신경쓴 테스트가, 모두 박살나게 된다. 혹자는 테스트가 깨지는 것은 당연하다고 말하지만, 기능이 변경된게 아니고, 내부 구현 방식이 변경되는 것 뿐인데 테스트가 깨지는 것이 당연한가?

결국 인터페이스의 호환성이 문제다!

이런 문제가 생기는 원인은 내가 정의한 인터페이스와 Spring Data JPA 가 정의한 인터페이스가 다르기 때문이다.
그렇다면, 인터페이스를 변환하는 무언가가 있으면 된다!
레벨 1에서 상속과 조합에 대해 공부할 때, 상속 관계를 조합으로 변경하면 상위 클래스(였던) 것의 메서드를 감싸 메서드의 시그니처를 변경할 수 있음을 알았다. 이를 잘 사용한다면, 인터페이스의 불일치를 해결할 수 있을 것이라 생각했다.

대충 이런 식으로 하면 되지 않을까?

네이밍이 이상한 건 넘어가자. 실제로 페어랑 이렇게 설계했다.. ㅋㅋㅋ
왼쪽의 두 가지는 Spring Data JPA를 적용하면서 새로 생긴 클래스다. 오른쪽은 기존의 인터페이스와, 이를 구현하는 구현체다. 기존 인터페이스의 구현체를 Repository로 등록하고, 새로 만든 레포지토리를 생성자를 통해 주입받도록 하기로 했다.

결과물

기존의 인터페이스를 그대로 유지하면서 JPA 를 사용할 수 있는 설계
이를 실제로 코드로 작성하면서 기존 Repository와 이름의 중복을 막기 위해 JpaRepository를 확장한 인터페이스는 Dao 라는 이름을 붙여주었다. 실제로도 Dao와 동일한 역할을 수행하니까, 별 문제는 없다고 생각한다.
JPA 도입이 끝난 시점의 변경 항목을 보면 오직 도메인에 JPA 의 어노테이션이 추가된 것과 Jdbc 구현체가 삭제된 것 뿐 다른 프로덕션 코드의 변경은 없다.
테스트 코드 역시 기존 Jdbc 구현체 테스트의 이름과 내부 필드만 살짝 바꿨을 뿐이다.
테스트 역시 그대로 돌아간다
리팩토링 과정에서도 예상했던 것 처럼 차근차근 변경되는 과정에서 전혀 문제가 없었다.

루퍼트 왕자의 눈물에도 꼬리가 있는데 내 코드에는 없겠니? : 단점

이런 구조를 가져가면서 당연히 잃은 점도 있다.
@Repository public class JpaReservationRepository implements ReservationRepository { private final JpaReservationDao jpaReservationDao; public JpaReservationRepository(JpaReservationDao jpaReservationDao) { this.jpaReservationDao = jpaReservationDao; } @Override public Reservation save(Reservation reservation) { return jpaReservationDao.save(reservation); } @Override public List<Reservation> findAll() { return jpaReservationDao.findAll(); } @Override public List<Reservation> findByMemberAndThemeBetweenDates(long memberId, long themeId, LocalDate start, LocalDate end) { return jpaReservationDao.findByReservationMember_IdAndTheme_IdAndDateBetween(memberId, themeId, start, end); } @Override public boolean existsByThemeAndDateAndTime(Theme theme, LocalDate date, ReservationTime reservationTime) { return jpaReservationDao.existsByThemeAndDateAndTime(theme, date, reservationTime); } @Override public boolean existsByTime(ReservationTime reservationTime) { return jpaReservationDao.existsByTime(reservationTime); } @Override public boolean existsByTheme(Theme theme) { return jpaReservationDao.existsByTheme(theme); } @Override public void delete(long id) { jpaReservationDao.deleteById(id); } }
Java
복사
이런식으로 단순 반복적인 코드가 추가되었다. 그럼에도 이런 구조를 유지한 이유는 아주 간단하다.
적어도 JpaRepository 의 모든 코드를 구현하는 Fake를 만드는 것보다는 훨씬 간단하고 유지보수하기 쉬운 작업이다.