전체 보기
♻️

레거시 Repository 코드 리팩토링 (with. Spring Data, 변경 감지, 엔티티 조회)

작성일자
2023/12/11
태그
SPRING
프로젝트
FIS
책 종류
1 more property

1. JPA 사용하는 레거시 구조에 Spring Data 적용

(1) 코드로 살펴보기

레거시 코드에선 JPA 그 자체만 사용했다. 따라서 코드가 아래와 같았다.
@Repository @RequiredArgsConstructor public class AgentRepository { private final EntityManager em; // save public void save(Agent agent) { em.persist(agent); } // find public Agent findById(Long id) { return em.find(Agent.class, id); } public List<Agent> findByCode(String code) { return em.createQuery("select a from Agent a where a.code =:code", Agent.class) .setParameter("code", code) .getResultList(); } public List<Agent> findAll() { return em.createQuery("select a from Agent a", Agent.class).getResultList(); } public List<Agent> findAllByNameDESC() { return em.createQuery("select a from Agent a order by a.name desc ", Agent.class).getResultList(); } }
Java
복사
이 코드를 Spring Data JPA로 대체하면 아래와 같이 변한다.
public interface AgentRepository extends JpaRepository<Agent, Long> { // 나머지는 JpaRepository 인터페이스에 이미 정의되어 있어서 별도로 정의할 필요조차 없음 // 커스텀 쿼리 메서드들만 정의 (메서드 이름을 통해 쿼리를 정의함) List<Agent> findByCode(String code); List<Agent> findAllByOrderByNameDesc(); }
Java
복사
커스텀 쿼리 메서드의 올바른 메서드 명명 규칙
findBy 또는 findAllBy: 쿼리의 시작점을 나타낸다.
속성 이름: 필터링 또는 정렬에 사용할 엔티티의 속성 이름을 지정한다.
Order: 정렬을 나타낸다.
By: 정렬 기준이 되는 속성 이름과 정렬 키워드 사이에 위치한다.
Desc 또는 Asc: 내림차순 또는 오름차순 정렬을 나타낸다.
→ ex. findAllBy + Order + By + Name + Desc
더 자세히 알고 싶다면, “쿼리 메서드 기능”이란 키워드로 검색해보자
코드로 살펴보는 것만으로도 크게 차이가 보인다.
더 정확하게 동작과정까지 비교하며 JPA와 Spring Data JPA가 뭐가 다른지, 더 나아가선 Spring Data가 무엇인지 설명해보겠다.

(2) JPA

JPA가 제공하는 기능은 크게 두 가지다. 엔티티와 테이블을 매핑하는 설계 부분과 매핑한 엔티티를 실제 사용하는 부분으로 나눌 수 있는데,
지금 논할 부분은 후자이다. 매핑한 엔티티를 엔티티 매니저를 통해 어떻게 사용하는지 알아보자.
엔티티 매니저는 엔티티를 관리하는 관리자인데, 엔티티를 저장하는 가상의 데이터베이스로 생각해도 괜찮다.
더 정확하겐 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
영속성 컨텍스트란 엔티티를 영구 저장하는 환경이란 뜻으로,
위 레거시 코드에서 봤던 em.persist(agent)의 경우 엔티티 매니저를 사용해서 요원 엔티티를 영속성 컨텍스트에 저장했던 것이다.
엔티티를 영구 저장한단 건 무슨 뜻일까? 이와 관련해 엔티티의 생명주기를 엿보아 보자.
엔티티는 4가지 상태로 존재한다.
비영속(new/transient): 영속성 컨텍스트와 전혀 관계가 없는 상태
영속(managed): 영속성 컨텍스트에 저장된 상태
준영속(detached): 영속성 컨텍스트에 저장되었다가 분리된 상태
삭제(removed): 삭제된 상태
그리고 이 네 가지 상태를 왔다갔다 할 때 사용되는 명령어는 정해져있다. 우리가 아까 살펴본 persist 역시 여기에 속한다.
그렇다면 db를 바로 안쓰고 영속성 컨텍스트를 사용하는 이유는 뭘까?
(이하 생략).. 이론은 다음 글에서

(3) Spring Data JPA

spring data jpa는 일종의 라이브러리다.
정확하겐 Spring Data JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
가장 큰 장점은, 데이터 접근 계층(ex. repository)를 개발할 때
구현 클래스 없이 인터페이스만 작성해도 된단 점이다.
어떻게 이가 가능할까?
Spring Data JPA는 CRUD를 처리하기 위한 공통 인터페이스를 제공하기에,
우리가 리포지토리를 개발할 때 인터페이스만 작성하면
실행 시점에 Spring Data JPA가 알아서 구현 객체를 동적으로 생성해 주입해준다.

(4) Spring Data

Spring Data란 정확하겐 Spring Data 프로젝트를 말하는데,
JPA, 몽고DB, Redis 같은 다양한 데이터 저장소에 대한 접근을 추상화해서
데이터 접근 코드를 줄여주는 프로젝트다.
우리가 흔히 접해왔던 Spring Data JPA 역시
스프링 데이터 프로젝트의 하위 프로젝트 중 하나로 JPA에 특화된 기능을 제공한다
즉, 스프링 프레임워크와 JPA를 사용할 때 Spring Data JPA를 사용한다면,
지루하고 반복하는 데이터 접근 코드를 줄일 수 있는 것이다!

2. 변경감지 적용

기존 코드 중에 이런 코드들이 많았다.
// 리포지토리 @Override @Modifying public void updateScheduleComplete(Long scheduleId, Complete complete) { em.createQuery("update Schedule schedule set schedule.complete = : complete where schedule.id = : schedule_id") .setParameter("schedule_id", scheduleId) .setParameter("complete", complete) .executeUpdate(); }
Java
복사
위와 같은 레거시 코드로 겪은 문제점이 있었다.
바로, 테스트 시 아직 변경이 적용 안되는 문제가 있었다.
JPA가 친절히 변경감지 라는 기능을 제공하는 데 안 쓸 이유가 없다.
아래와 같이 바꿔주자.
// 서비스 @Transactional public void cancelSchedule(Long scheduleId, String comment) { Schedule findSchedule = findById(scheduleId); findSchedule.cancel(comment); } // 도메인 public void cancel(String comment) { this.cancel_comment = comment; this.complete = Complete.cancel; }
Java
복사

3. dto 직접 조회 방식을 엔티티 조회 방식으로 대체

(1) 변경 전 : dto 직접 조회 방식 사용

// 리포지토리 public List<ScheduleByDateResponse> findAllByDate(LocalDate date) { // TODO: 도메인 변경 전부터 애초에 빨간줄 떴음. 동작은 하지만 픽스 필요. return em.createQuery( "select new fis.iluvit.admin.domain.schedule.dto.ScheduleByDateResponse(" + "s.id, a.a_code, a.name, c.id, c.name, c.address, c.tel, s.estimate_num, s.new_cnt, s.visitDate, " + "s.visitTime, s.center_etc, s.agent_etc, s.modified_info, s.total_etc, s.call_check, s.call_check_info, s.accept, s.cancel_comment, s.complete, s.modifiedStaffName, " + "h.newChildCnt+h.newDementiaCnt+h.newDisabledCnt, h.changedChildCnt, h.newChildCnt+h.newDementiaCnt+h.newDisabledCnt+h.changedChildCnt)" // TODO: 레거시가 객체를 사용하지 않고 dto 직접 조회 방식이 채택되어 있어서 transient 값을 사용하지 못하고 직접 더해줬음. 개선 필요함 + " from Schedule s " + " join s.agent a" + // " join s.user u" + " join s.center c" + " left join headcount h on s.id = h.schedule.id" + " where s.visitDate = :date and s.valid = true" + " order by a.name desc, s.visitTime", ScheduleByDateResponse.class) .setParameter("date", date) .getResultList(); } // 서비스 public List<ScheduleByDateResponse> selectDate(LocalDate date) { return scheduleRepository.findAllByDate(date); }
Java
복사

(2) 변경 후 : 엔티티 조회 방식 사용

→ 재사용성 up + 객체를 사용하기에 transient 사용 가능
// 리포지토리 public List<Schedule> findAllByDate(LocalDate date) { return em.createQuery( "select s" + " from Schedule s " + " join s.agent a" + " join s.center c" + " left join headcount h on s.id = h.schedule.id" + " where s.visitDate = :date and s.valid = true" + " order by a.name desc, s.visitTime", Schedule.class) .setParameter("date", date) .getResultList(); } // 서비스 public List<ScheduleByDateResponse> selectDate(LocalDate date) { List<Schedule> scheduleList = scheduleRepository.findAllByVisitDate(date); return scheduleList.stream() .map(ScheduleByDateResponse::from) .toList(); } // dto public class ScheduleByDateResponse { private Long schedule_id; // 스케쥴 id private String a_code; // 현장요원 코드 private Integer totalNewCnt; private Integer totalChangedCnt; private Integer totalCnt; ... public static ScheduleByDateResponse from(Schedule schedule) { return ScheduleByDateResponse.builder() .schedule_id(schedule.getId()) .a_code(schedule.getAgent().getA_code()) .a_name(schedule.getAgent().getName()) .center_id(schedule.getCenter().getId()) .c_name(schedule.getCenter().getName()) .c_address(schedule.getCenter().getAddress()) .c_ph(schedule.getCenter().getTel()) .estimated_cnt(schedule.getEstimate_num()) .new_cnt(schedule.getNew_cnt()) .visit_date(schedule.getVisitDate()) .visit_time(schedule.getVisitTime()) .center_etc(schedule.getCenter_etc()) .agent_etc(schedule.getAgent_etc()) .modified_info(schedule.getModified_info()) .total_etc(schedule.getTotal_etc()) .call_check(schedule.getCall_check()) .call_check_info(schedule.getCall_check_info()) .accept(schedule.getAccept()) .cancel_comment(schedule.getCancel_comment()) .complete(schedule.getComplete()) .modifiedStaffName(schedule.getModifiedStaffName()) .totalNewCnt(schedule.getNewChild()) // transient 값 .totalChangedCnt(schedule.getModChild()) // transient 값 .totalCnt(schedule.getTotalChild()) // transient 값 .build(); }
Java
복사
(추가) entity manager도 제거 (1번 적용) (물론 변경 전 구조에서도 entity manager는 제거할 수 있다)
// 리포지토리 List<Schedule> findAllByVisitDate(LocalDate visitDate); // 서비스, dto는 그대로
Java
복사
두 방식을 비교하는 걸 더 자세하겐 이전에 아래 포스팅에 적어두었으니, 관심 있는 분들은 읽어봐도 좋을 거 같다

4. 결론

entityManager 사용하는 코드들 전부 라이브러리로 대체
cf. entityManager.executeUpdate 사용하는 부분은 JPA 변경감지로 대체
리포지토리에서 dto 사용하지 않기. dto가 아닌 entity를 찾아오게 변경
entity를 dto로 매핑하는 작업은 service 레이어 + dto 내부 메서드에서 하기
jpql을 queryDsl로 대체
querydsl 장점
jpql과 마찬가지로 특정 데이터베이스에 종속적이지 않다
jpql과 다르게 sql문 중 틀린 부분을 컴파일 타임에 잡아준다
다양한 동적 쿼리 쉽다 (라이브러리로 커버 안되는 부분에 사용하기)
formula vs transient
transient 메서드 → npe 따로 처리해줘야함
transient 필드 → 생성자에 넣어줘야 함
formula → npe 알아서 처리함, 따로 신경써줄 거 없음 굳.