전체 보기
🟢

API 조회 성능 최적화(with DTO 변환 방식)

작성일자
2023/02/28
태그
SPRING
프로젝트
BeachCombine
책 종류
1 more property

DTO 변환 방식 VS DTO 직접 조회 방식

아띠즈 프로젝트에서 로딩 시간이 오래 걸려 쿼리 최적화에 대한 이야기가 나왔다.
나는 DTO 변환 방식을 사용한 게 문제라고 생각했다.
결론부터 말하자면 이는 큰 영향이 없다 ㅎㅎ
우선, 팀원분께서 아마 그것보단 이미지 로딩이 오래 걸려서 일 거라 의견 주셨는데, 그때까지도 DTO 변환 방식도 문제일 것이라 생각했다.
그래서 더 찾아봤다.
찾아보니 DTO 변환 방식과 DTO 직접 조회 방식의 장단점을 비교하며 API 조회 성능 최적화에 대해 다루는 포스팅들이 꽤 있었다!
나만 궁금했던 부분이 아니라 기뻤다.
더 자세히 말하자면, (1) JPA에서 DTO를 직접 조회해오는 것과, (2) JPA에서 Entity를 조회해오고서 이를 컨트롤러나 서비스단으로 전달해 DTO로 변환하는 방식의 장단점에 대해서였다.
사실, 나는 DTO로 직접 조회해 오는 것이 무조건 좋을 줄 알았다.
그 이유는 DTO 직접 조회 방식은 모든 필드를 조회해 오지 않고, 필요한 필드만 조회해 올 수 있기 때문이다.
다만, JPA에서 얻어온 값만으로 response DTO를 그대로 담아 보낼 수 없을 때 어쩔 수 없이 DTO 변환방식을 써야 하는 거라 생각했다.
그런데, 이상하게도 다른 사람들의 프로젝트는 전부 DTO 변환 방식을 쓰는 걸 확인했다.
그리고 그 이유를 아래 블로그에서 알 수 있었다.
정리하자면, 아래와 같다.
DTO 직접 조회 방식
장점
DB 네트워크 비용이 저렴함
단점
조회된 데이터의 재사용성이 좋지 않음
결합도가 높음
DTO 변환 방식
장점
조회된 데이터의 재사용성이 좋음
레이어 간 의존성 낮아짐 (dto 직접 조회 방식은 요청 사항 바뀌었을 때 repository단까지 바뀌어야 함)
단점
DB 네트워크 비용이 비쌈
이때, 중요한 사실은 “조회 방식에 따른 DB 네트워크 비용을 비교하는 것은 요즘 서버 스펙상 무의미하다" 라는 점! 이었다.
요 부분을 난 몰랐다,,, 어디서 얼핏 듣기로 DTO 변환 방식은 모든 필드를 다 조회해오다 보니 성능이 안좋단 말만 들었어서,, 그 성능 차이가 유의미한 정도가 아니란 걸 몰랐었다!
따라서 유지보수성과 재사용성을 고려했을 때 DTO 변환 방식이 낫다고 판단했고, 많은 사람들이 쓰는 데엔 이유가 있음을 한 번 더 깨달을 수 있었다,,,
또, 대부분의 쿼리 성능 이슈는 DTO 직접 조회 방식을 사용해서 해결하는 게 아니고, Fetch Join을 사용해 해결하면 된다고 한다.
따라서 아띠즈 프로젝트의 쿼리 최적화는 Fetch Join 적용을 통해 해야겠단 생각까지 얻어갈 수 있었다!
이 둘을 내가 프로젝트에서 썼던 코드로 보여주자면 아래와 같다.
DTO 직접 조회 방식
// response DTO public class TrashcanMarkerResponse { private String lat; private String lng; private Long id; public TrashcanMarkerResponse (Trashcan trashcan) { this.lat = String.valueOf(trashcan.getLat()); this.lng = String.valueOf(trashcan.getLng()); this.id = trashcan.getId(); } } // repository public interface TrashcanRepository extends JpaRepository<Trashcan, Long> { List<TrashcanResponse> findByIsCertified(Boolean isCertified); } // service @Transactional(readOnly = true) public List<TrashcanResponse> findCertifiedTrashcanMarkers() { List<TrashcanResponse> trashcanResponseList = trashcanRepository.findByIsCertified(true); // 인증된(isCertified=true) 쓰레기통들의 좌표 반환 return trashcanResponseList; } // controller @GetMapping("map") public ResponseEntity<List<TrashcanResponse>> findCertifiedTrashcanMarkers(){ List<TrashcanResponse> trashcanResponse = trashcanService.findCertifiedTrashcanMarkers(); return ResponseEntity.status(HttpStatus.OK).body(trashcanResponse); }
Java
복사
DTO 변환 방식 (위 코드를 변경한 것입니다.)
// response DTO public class TrashcanMarkerResponse { private String lat; private String lng; private Long id; } // repository public interface TrashcanRepository extends JpaRepository<Trashcan, Long> { List<Trashcan> findByIsCertified(Boolean isCertified); } // service @Transactional(readOnly = true) public List<TrashcanMarkerResponse> findCertifiedTrashcanMarkers() { List<Trashcan> findTrashcanList = trashcanRepository.findByIsCertified(true); // 인증된(isCertified=true) 쓰레기통들의 좌표 반환 List<TrashcanMarkerResponse> responseList = findTrashcanList.stream() .map(m -> TrashcanMarkerResponse.builder() .id(m.getId()) .lat(String.valueOf(m.getLat())) .lng(String.valueOf(m.getLng())) .build()) .collect(Collectors.toList()); return responseList; } // controller @GetMapping("map") public ResponseEntity<List<TrashcanMarkerResponse>> findCertifiedTrashcanMarkers(){ List<TrashcanMarkerResponse> response = trashcanService.findCertifiedTrashcanMarkers(); return ResponseEntity.status(HttpStatus.OK).body(response); }
Java
복사