DTO란?
DTO는 data transfer object로써, 기능은 딱히 없고 데이터를 계층별로 전송하는데 사용하는 객체를 의미한다.
주 목적은 데이터를 전송하는 것이고 가능한 종속되는 기술이 없는게 좋다.
어느 계층에 DTO를 두면 좋을까?
@Data
@Entity
public class Item {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) // PK / DB 에서 값 증가
private Long id;
@Column(name = "item_name", length = 10) // item_name 은 DB 컬럼 명
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
--------------------------------------------------------------------------------
@Repository
@Transactional
public class JpaItemRepositoryV1 implements ItemRepository {
private final EntityManager em;
public JpaItemRepositoryV1(EntityManager em) {
this.em = em;
}
@Override
public Item save(Item item) {
em.persist(item);
return item;
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
Item findItem = em.find(Item.class, itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
// 알아서 update 쿼리가 나간다 -> 트랜잭션이 커밋되는 시점
}
@Override
public Optional<Item> findById(Long id) {
Item item = em.find(Item.class, id);
return Optional.ofNullable(item);
}
@Override
public List<Item> findAll(ItemSearchCondition cond) {
String jpql = "select i from Item i";
Integer maxPrice = cond.getMaxPrice();
String itemName = cond.getItemName();
if (StringUtils.hasText(itemName) || maxPrice != null) {
jpql += " where";
}
boolean andFlag = false;
if (StringUtils.hasText(itemName)) {
jpql += " i.itemName like concat('%',:itemName,'%')";
andFlag = true;
}
if (maxPrice != null) {
if (andFlag) {
jpql += " and";
}
jpql += " i.price <= :maxPrice";
}
log.info("jpql={}", jpql);
TypedQuery<Item> query = em.createQuery(jpql, Item.class);
if (StringUtils.hasText(itemName)) {
query.setParameter("itemName", itemName);
}
if (maxPrice != null) {
query.setParameter("maxPrice", maxPrice);
}
return query.getResultList();
}
}
--------------------------------------------------------------------------------
@Service
@RequiredArgsConstructor
public class ItemServiceV1 implements ItemService {
private final ItemRepository itemRepository;
@Override
public Item save(Item item) {
return itemRepository.save(item);
}
@Override
public void update(Long itemId, ItemUpdateDto updateParam) {
itemRepository.update(itemId, updateParam);
}
@Override
public Optional<Item> findById(Long id) {
return itemRepository.findById(id);
}
@Override
public List<Item> findItems(ItemSearchCondition cond) {
return itemRepository.findAll(cond);
}
}
위 코드는 도메인 - 레포지토리 - 서비스 순이다.
만약 아래와 같은 DTO가 있다고 가정하자.
package hello.itemservice.repository;
import lombok.Data;
@Data
public class ItemUpdateDto {
private String itemName;
private Integer price;
private Integer quantity;
public ItemUpdateDto() {
}
public ItemUpdateDto(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
이제 다시 위 코드들을 보자. 혹시 어느 패키지에 DTO를 둬야할지 감이 잡히는가?
사실 그때마다 다르겠지만 적어도 현재 프로젝트에선 Service의 용도는 Repository의 기능을 호출하는게 끝이다.
이번 프로젝트를 기준으로 생각해보자.
DTO 위치는 항상 의존성을 생각해보자
먼저, 사실 DTO들만 따로 패키지에 모아 만들어도 좋다.
그게 아니라면, 항상 의존성에 근거하여 생각해보자. 현재 코드에서 Service는 대부분의 로직을 Repository에 위임(의존)한다.
즉, 현재 프로젝트에서 DTO는 Repository가 가져다 쓰는 것이라고 봐도 크게 이상하지 않다.
현재 구조에선 결국 DTO는 두가지 파라미터를 받아야하는데, 이 파라미터는 Repository로 부터 받아야하는 구조기 때문에, 최종적으로 의존 관계상 Repository에 가까이 두는것이 좋다.
쉽게 생각하면 결국 누가 이 DTO를 가져다 쓰는가? 를 생각해보고 결정하자.
예를 들어 Service에서만 사용하고 다른 계층으로 넘기지 않는 DTO 같은 경우에는 Service 단계에 둬도 괜찮다.
늘 의존성을 생각하며 결정해야하는 가장 중요한 이유는 순환 참조가 발생할 수 있는 경우이다.
순환 참조는 정말 최악의 수인데, Repository가 DTO를 사용하는 최종 목적지인데, 만약 Service 계층에 이 DTO를 두게 되면 Repository가 Service 패키지를 참조하지 않아도 되는데 DTO 클래스 하나때문에 의존성을 가져야한다.
이처럼 최종목적지인 Repository가 이전 계층인 Service를 참조해야하고, Service 계층 또한 Repository가 있어야 명령을 위임할 수 있기 때문에 서로가 서로를 참조하는 순환참조가 발생할 수도 있다.
결론
최종적으로 누가 갖다 쓰냐? 를 생각해서 알잘딱 설정하면 된다. 여기서는 Repository에 뒀다.
'Life > Insight' 카테고리의 다른 글
| [인사이트] 백엔드 개발자도 HTML이 필수 스펙인 이유 (0) | 2025.07.24 |
|---|