DTO의 중요성을 알아버렸다...

2024. 11. 10. 23:07프로젝트/KEEV

최근 프로젝트를 하면서 협업을 하다보니, DTO명이나 기능이 서로 잘 안맞는 경우가 많았어서 회의를 하면서 일단은 DTO를 사용하지 말고 리팩토링 할 때 Entity를 DTO로 바꾸자고 먼저 제안하였다. 이 결정은 얼마 지나지 않아 당연히 번복되었는데 DTO를 사용하지 않기로 하면서 여러 불편한 점이 많았기 때문이다. 일단 ChatGPT가 알려준 DTO를 사용해야 하는 이유는 아래와 같다.

 

1. 불필요한 정보 노출 방지

  • 엔티티에는 비즈니스 로직과 관련된 다양한 필드가 포함되어 있습니다. 예를 들어, 민감한 사용자 정보나 내부적으로만 사용되는 필드를 외부에 그대로 노출하면 보안 및 정보 관리에 문제가 생길 수 있습니다.
  • DTO를 사용하여 필요한 정보만 클라이언트에 전달하면, 데이터 노출을 최소화하고 보안을 강화할 수 있습니다.

2. 순환 참조 문제 해결

  • JPA에서 엔티티 간 양방향 관계가 설정되어 있을 경우, 순환 참조가 발생할 수 있습니다. 엔티티를 그대로 전송하면 직렬화 과정에서 순환 구조가 무한 루프에 빠져 에러가 발생합니다.
  • DTO로 변환하여 필요한 데이터만 담아 전송하면 순환 참조 문제를 방지할 수 있습니다.

3. 명확한 역할 분리와 코드 가독성 향상

  • 엔티티는 데이터베이스와의 매핑을 위해 설계된 반면, DTO는 오직 데이터 전송을 목적으로 설계됩니다. 역할이 명확히 분리되면 코드의 의도가 뚜렷해져 가독성이 높아집니다.
  • 엔티티를 직접 변경할 경우 비즈니스 로직에 의도치 않은 영향이 갈 수 있지만, DTO는 데이터 전송만 담당하므로 코드의 안정성이 올라갑니다.

4. 성능 최적화

  • 엔티티를 그대로 전달하면 연관된 모든 엔티티가 함께 로드되어 불필요한 쿼리가 발생할 수 있습니다. 특히 @OneToMany나 @ManyToMany 같은 연관 관계가 있을 때, 데이터 양이 많아지면 성능 저하가 심각해질 수 있습니다.
  • DTO를 사용하여 필요한 필드만 선택적으로 가져오면, 성능을 크게 개선할 수 있습니다.

5. 유지보수성과 확장성

  • 서비스 계층에서 클라이언트의 요구사항이 바뀔 때, DTO를 통해 유연하게 대처할 수 있습니다. 클라이언트가 특정 필드 추가를 요청할 경우, DTO에 해당 필드를 추가하고 서비스 계층에서 처리하면 엔티티에 영향을 주지 않고도 요구사항을 충족할 수 있습니다.
  • 엔티티와 서비스 로직의 결합도를 낮추고, DTO를 통해 여러 사용 사례에 맞춰 확장할 수 있는 구조를 설계할 수 있습니다.

 

1. 불필요한 정보 노출 방지

처음에 내가 생각했던 DTO의 대표적인 기능은 필요한 최소한의 정보만을 담아 데이터를 전송하는 것이었다. 그래서 처음에는 굳이 DTO를 사용하지 않고, 엔티티(Entity)를 그대로 넘겨주자는 생각을 하게 되었다. 엔티티를 직접 넘겨주면 엔티티에서 DTO로, 혹은 DTO에서 엔티티로 변환하는 과정을 생략할 수 있을 거라고 생각했기 때문이다.

 

2. 순환 참조 문제 해결

그러자 바로 이 문제가 튀어 나왔다. 일단 우리 프로젝트 구조를 보여주면

@Entity
@Getter
@Setter
@Builder
@Table(name = "event")
@NoArgsConstructor
@AllArgsConstructor
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Event extends BaseEntity {

    @Id
    @GeneratedValue(strategy =  GenerationType.UUID)
    private UUID id;

    @Column(nullable = false)
    private String name;

    @ManyToOne
    @JoinColumn(name="event_type_id", nullable = false)
    private EventType eventType;

    @OneToMany(mappedBy = "event")
    private List<EventArtist> eventArtists = new ArrayList<>();

    // 그 외 entity 및 함수들
}

 

이런 식으로 OneToMany와 ManyToOne이 혼재되어 있는 엔티티가 여럿인 상태였다. 여기서 문제가 된 부분은 ManyToMany 부분이었는데, 우리는 ManyToMany를 OneToMany 두 개로 선언하고 그 사이에 중간 객체를 두어 코드를 작성하였다.

 

이 과정에서 Event 발동 -> Event 안의 EventArtist 발동 -> EventArtist 안의 Artist 발동 -> Artist 안의 EventArtist 발동 -> ... 이런 식으로 러시아 인형처럼 끊임없이 객체가 생성되며 순환 참조 문제가 발생하였다.

 

객체 안의 객체 안의 객체 안의... (출처 : https://www.sungyujin.co.kr/essay/1811)

이 문제는

@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")

 

를 사용하여 해결함으로써 일단은 일단락되는 듯했으나...

 

3. 명확한 역할 분리와 코드 가독성 향상

결국 이 문제가 결정타를 날렸다.

 

{
  "id": "event-uuid",
  "name": "Event Name",
  "eventArtists": [
    {
      "id": "event-artist-uuid-1",
      "name": "EventArtist 1",
      "event": {
        "id": "event-uuid",
        "name": "Event Name"
      },
      "artist": {
        "id": "artist-uuid-1",
        "name": "Artist 1"
      }
    },
    {
      "id": "event-artist-uuid-2",
      "name": "EventArtist 2",
      "event": {
        "id": "event-uuid",
        "name": "Event Name"
      },
      "artist": {
        "id": "artist-uuid-2",
        "name": "Artist 2"
      }
    }
  ]
}

 

필드명이나 구조가 정확히 일치하지 않지만, 위와 같은 형식으로 프론트 입장에서 봤을 때 매우 불친절한 데이터가 출력되었고, 그 결과 우리는 필요한 정보만을 제공하기 위해 DTO를 사용하기로 방향을 전환했다.

 

 

4, 5. 성능 최적화, 유지보수성과 확장성

이 부분은 아직까지 크게 체감하지 못했다. 아무래도 대용량 트래픽을 처리하는 것도 아니고, 신규 개발 단계에서 느끼기에는 힘든 부분이였던 것 같다.

 

'프로젝트 > KEEV' 카테고리의 다른 글

Lombok을 이용한 생성자 주입  (0) 2024.11.10
새로운 프로젝트를 시작했다  (0) 2024.11.03