본문 바로가기
JPA

[JPA] Repository에서 DTO로 데이터 셋팅 @Query()

by 개발LOG 2024. 2. 22.

jpa를 사용할 때 Repository에서 데이터를 바로 DTO로 셋팅하는 방법을 사용하려면 2가지 방법이 있다.

1. 첫번째 방법: @Query()를 사용

쿼리어노테이션을 사용해 바로 DTO에 매핑하는 방법

  • 이 방법은 쿼리의 결과를 바로 DTO에 매핑하므로, 불필요한 객체 변환 단계를 거치지 않아서 성능상 이점이 있을 수 있습니다.
  • 코드가 간결해지고 읽기 쉬워집니다. 쿼리의 내용이 명확히 드러나므로 이해하기 쉽습니다.
  • 그러나 DTO의 생성자에 맞게 쿼리 결과를 매핑해야 하므로 DTO의 생성자와 쿼리의 필드 리스트가 일치해야 합니다.

별다른 DTO 가공이 필요 없다면 바로 DTO로  @Query을 사용해 DTO로 넣으면 된다.

예시:

DistrictRepository.java

package com.pnow.repository;

import com.pnow.domain.District;
import com.pnow.dto.DistrictDTO;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface DistrictRepository extends JpaRepository<District, Long> {
    // cityId에 해당하는 district들을 DistrictDTO로 바로 넣는 방법
    @Query("SELECT new com.pnow.dto.DistrictDTO(d.id, d.districtName) FROM District d WHERE d.city.id = :cityId") //추가로직 필요없으므로 DTO에 바로 받아 넣기
    List<DistrictDTO> findDistrictDTOsByCityId(@Param("cityId") Long cityId);
}

 

District.java 엔티티

package com.pnow.domain;

import lombok.Getter;
import javax.persistence.*;
import java.util.List;

@Getter
@Entity
public class District {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "district_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "city_id")
    private City city;

    @Column(nullable = false)
    private String districtName; //시군구 이름

    @OneToMany(mappedBy = "district", cascade = CascadeType.REMOVE)
    private List<Store> storeList; //district_id 해당하는 storeList
}

 

DistrictDTO.java 

package com.pnow.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor  //기본생성자
@AllArgsConstructor //모든생성자
public class DistrictDTO {
    private Long id;             //지역ID
    private String districtName; //지역이름
}

 

2. 두번째 방법: 엔티티로 받은 후 Service에서 DTO로 직접 set 하기

service에서 DTO에 값을 설정하는 방법

  • 이 방법은 먼저 엔티티 객체를 받은 다음, 서비스 레이어에서 DTO로 변환하는 과정이 필요합니다. 이로 인해 성능이 약간 저하될 수 있습니다.
  • 그러나 변환 과정에서 추가적인 로직이 필요할 경우에 유용합니다. 예를 들어, 엔티티의 특정 필드를 기반으로 DTO의 특정 값을 계산하거나, 다른 로직을 적용할 수 있습니다.
  • 또한, DTO와 엔티티의 구조가 다를 경우에는 DTO에 데이터를 수동으로 매핑할 수 있습니다.

DTO 가공이 필요하다면, 두번째 방법인 엔티티로 객체를 받아 service에서 DTO로 변환해주면 된다.

예시:

StoreRepository.java

package com.pnow.repository;

import com.pnow.domain.Store;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface StoreRepository extends JpaRepository<Store, Long> {
    List<Store> findByCategoryIdAndDistrictIdOrderByStoreNameAsc(Long categoryId, Long districtId); //추가로직 필요하므로 일단 엔티티에 받기(추가로직은 서비스에서 작업)
}

 

StoreService.java

 

package com.pnow.service;

import com.pnow.domain.City;
import com.pnow.domain.District;
import com.pnow.domain.Store;
import com.pnow.dto.StoreDTO;
import com.pnow.repository.CityRepository;
import com.pnow.repository.DistrictRepository;
import com.pnow.repository.StoreRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

@RequiredArgsConstructor
@Service
@Slf4j
public class StoreService {

    private final StoreRepository storeRepository;
    private final CityRepository cityRepository;
    private final DistrictRepository districtRepository;

    public List<StoreDTO> findStoreDTOList(Long categoryId, Long cityId, Long districtId) {
        // 1. categoryId, districtId에 해당하는 Store 엔티티 가져오기
        List<Store> stores = storeRepository.findByCategoryIdAndDistrictIdOrderByStoreNameAsc(categoryId, districtId);

        // 2. 전달받은 cityId에 해당하는 cityName 가져오기
        Optional<City> cityOptional = cityRepository.findById(cityId);
        String cityName = cityOptional.map(City::getCityName).orElse(null);


        // 3. 전달받은 districtId에 해당하는 districtName 가져오기
        Optional<District> districtOptional = districtRepository.findById(districtId);
        String districtName = districtOptional.map(District::getDistrictName).orElse(null);


        // 4. List<Store>를 List<StoreDTO>로 변환
        return stores.stream()
                .map(store -> {
                    StoreDTO storeDTO = new StoreDTO();
                    storeDTO.setId(store.getId());
                    storeDTO.setStoreName(store.getStoreName());
                    storeDTO.setOpeningTime(store.getOpeningTime());
                    storeDTO.setClosingTime(store.getClosingTime());
                    storeDTO.setCityName(cityName);
                    storeDTO.setDistrictName(districtName);
                    storeDTO.setDetailAddress(store.getDetailAddress());
                    log.info("storeDTO.getStoreName() = {}", storeDTO.getStoreName());
                    return storeDTO;
                })
                .collect(Collectors.toList());

    }
}

 

Store.java 엔티티

package com.pnow.domain;

import lombok.Getter;
import javax.persistence.*;
import java.time.LocalTime;
import java.util.List;

@Getter
@Entity
public class Store {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "store_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "category_id")
    private Category category;


    @Column(nullable = false)
    private String storeName; //음식점명

    @ManyToOne
    @JoinColumn(name = "district_id")
    private District district; //시군구 district_id (도 관련 city_id도 내포되어 있음)

    @Column(nullable = false)
    private String detailAddress; //나머지 상세주소

    @Column
    private String phoneNumber; //음식점 전화번호

    @Column(nullable = false)
    private LocalTime openingTime; // 오픈시간

    @Column(nullable = false)
    private LocalTime closingTime; // 오프시간

    @OneToMany(mappedBy = "store", cascade = CascadeType.REMOVE)
    private List<Bookmark> bookmarkList;

    @OneToMany(mappedBy = "store", cascade = CascadeType.REMOVE)
    private List<Reservation> reservationList;

//    @OneToMany(mappedBy = "restaurant", cascade = CascadeType.REMOVE)
//    private List<Review> reviewList;

    @OneToMany(mappedBy = "store", cascade = CascadeType.REMOVE)
    private List<Menu> menuList;

}

StoreDTO.java 

package com.pnow.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalTime;

@Getter
@Setter
@NoArgsConstructor  //기본생성자
@AllArgsConstructor //모든생성자
public class StoreDTO {
    private Long id;                    //가게 id
    private String storeName;           //가게 이름
    private LocalTime openingTime;      //오픈 시간
    private LocalTime closingTime;      //오프 시간
    private String cityName;            //도시 이름
    private String districtName;        //지역 이름
    private String detailAddress;       //상세 주소
}

 


정리하자면,

  • 받아오는 엔티티 객체와 DTO가 거의 일치하고 DTO에 별다른 가공 작업이 필요 없다면 첫번째 @Query 어노테이션사용해서 DTO에 바로 셋팅하기
  • 반대로 DTO 가공 작업이 필요하다면, 일단 엔티티 객체로 받은 후 service 부분에서 DTO로 가공작업 하며 set하기