BackEnd/Java

DTO 그렇게 쓰는 거 아닌데 ㅋㅋ

Potwings 2024. 3. 17. 20:35

미안하다 이거 보여주려고 어그로 끌었다.

 

 

최근 DTO 관련된 글을 하나 읽었다.

 

원글 : https://medium.com/@bubu.tripathy/dto-free-java-ee70c43b5ad5

 

DTO-Free Java

Moving Beyond DTOs to Enhance Application Design

medium.com

 

해당 글에서 필자는 기존 DTO의 문제점을 제기하고 그에 대한 해결 방안을 제안하였다.

 

필자가 말하는 기존 DTO의 문제점

1. DTO는 비지니스 로직 없이 단순 데이터만 가지고 있어 해당 DTO를 다루는 곳에서 중복 코드가 발생할 수 있다.
2. 도메인 객체나 비즈니스 로직이 변경될 경우 DTO에도 동일한 수정 작업이 필요하다.
3. 대규모 어플리케이션에서 DTO의 증가함에 따라 클래스가 복잡해져 유지보수가 어려워진다.
4. DTO와 도메인 객체가 별개의 데이터를 관리함에 따라 잠재적으로 데이터 중복과 불일치가 발생할 수 있다.

 

 

이를 해결하기위한 필자가 제안한 방식은 아래 3가지였다.

1. DTO 대신 도메인이나 Entity 객체에 데이터와 로직 모두 담기(DDD 방식)
2. Map이나 배열을 활용하여 데이터를 주고 받기
3. DTO를 데이터에 따라 여러개로 나누어 생성하기

 

1번과 2번은 많이 접해봤으나 3번은 처음들어보는 내용이었고,

한번 사용해볼 만한 방식이라 생각이 들어 정리해보고자한다.

 

우선 간단하게 말하자면 해당 방식은 DTO 인터페이스를 생성하고 상황에 따른 DTO를 구현하여 사용하는 방식이다.

 

예시를 통하여 확인해보자.

 

우선 DTO에 들어가는 값들을 불러오는 메소드를 가지고 있는 인터페이스를 생성한다.

public interface Pizza {
    String getTopping();
    String getSize();
    String getEdge();
    // 이외 부가적인 필드를 불러오는 다른 메소드...
}

 

 

해당 인터페이스를 모든 값을 가지고 있는 객체를 하나 구현해준다.

이 객체는 DTO가 모든 값을 가지고 있는 경우 사용하게 된다.

public class FullPizza implements Pizza {

  private String topping; // 필수
  private String size; // 필수
  private String edge; // 부가

  public FullPizza(String topping, String size, String edge) {
    this.topping = topping;
    this.size = size;
    this.edge = edge;
  }

  @Override
  public String getTopping() {
    return topping;
  }

  @Override
  public String getSize() {
    return size;
  }

  @Override
  public String getEdge() {
    return edge;
  }
}

 

 

 

DTO를 사용하다보면 일부값만 가지고 있는 객체들도 있다.

이 때 사용하기 위한 일부 값들만 가지고 있는 객체를 구현해준다.

public class NoEdgePizza implements Pizza {

  private String topping; //필수
  private String size; //필수
  private String edge; //부가

  public NoEdgePizza(String topping, String size) {
    this.topping = topping;
    this.size = size;
    // 부가적인 필드는 초기화하지 않을 수 있음
  }

  @Override
  public String getTopping() {
    return topping;
  }

  @Override
  public String getSize() {
    return size;
  }

  @Override
  public String getEdge() {
    return "No Edge Pizza!"; // 부가적인 필드는 기본값 or null값 반환
  }
}

 

이를 사용할 경우 필요한 값만 사용하여 객체를 생성할 수 있고, 사용하지 않는 부가적인 값에 대해서는 객체에서 우선적으로 처리하여 DTO를 사용하는 비즈니스 로직을 좀 더 단순화할 수 있다.

 

 

아직 값을 생성하기 전 초기화에 사용하기 위한 null 객체도 존재한다.

public class NullPizza implements Pizza {

  @Override
  public String getTopping() {
    return "Select Topping Plz";
  }

  @Override
  public String getSize() {
    return "Select Size Plz";
  }

  @Override
  public String getEdge() {
    return "Select Edge Plz";
  }
}

 

기존 DTO 사용시 Pizza pizza = null 과 같이 초기화하여 개발자가 놓치는 경우 NPE가 발생할 수 있었다.

허나 위의 객체를 사용하여 초기화할 경우 NPE가 발생하지 않도록 사전에 예방할 수 있다.

 

 

마지막으로 위의 DTO들을 활용하는 예시를 확인해보자

public class Test {

  public static void main(String[] args) {
    Pizza pizza = new NullPizza(); // 기존의 Pizza pizza = null와 같은 초기화
    System.out.println("Topping: " + pizza.getTopping());
    System.out.println("Size: " + pizza.getSize());
    System.out.println("Edge: " + pizza.getEdge());

    System.out.println("\nMake Pizza....\n");

    pizza = new FullPizza("potato", "large", "cheese crust"); // 값을 가지고 있는 DTO 생성
    System.out.println("Topping: " + pizza.getTopping());
    System.out.println("Size: " + pizza.getSize());
    System.out.println("Edge: " + pizza.getEdge() + "\n");

  }
}

 

아래와 같이 결과가 출력된다.

 

 

새로운 시스템을 구성할 때 위의 방식을 한번 사용해보는 것도 좋을듯하다.