상세 컨텐츠

본문 제목

[CS] 소프트웨어 설계를 위한 두 기초: DTO와 의존성 주입의 장단점

CS 과제 정리

by thisnorm 2025. 1. 20. 17:55

본문

DTO 정의

DTO (Data Transfer Object) 는 데이터 전송 객체로, 데이터베이스와 애플리케이션 간에 데이터를 전송하기 위해 사용되는 객체이다. DTO는 주로 네트워크를 통해 데이터를 전송할 때 사용되며, 데티어의 구조를 정의한다.

 

 

 

 

주요 목적

  • 데이터 전송 최적화 : DTO는 필요한 데이터만 포함하여 전송함으로써 네트워크 대역폭을 절약하고 성능을 향상시킨다.
  • 계층 간 데이터 전송 : DTO는 애플리케이션의 여러 계층(예: 프레젠테이션 계층과 비즈니스 계층) 간에 데이터를 전송하는데 사용된다.

 

 

 

특징

  • 불변성 : DTO는 일반적으로 불변 객체로 설계되어, 생성 후에는 상태가 변경되지 않는다. 이는 데이터의 일관성을 유지하는 데 도움이 된다.
  • 단순한 구조 : DTO는 일반적으로 단순한 속성 집합으로 구성되며, 비즈니스 로직을 포함하지 않는다. 이는 DTO가 데이터 전송에만 집중하도록 한다.

 

 

 

사용 사례

  • API 응답 : 웹 API에서 클라이언트에 데이터를 전송할 때 DTO를 사용하여 필요한 데이터만 포함된 응답을 생성한다.
  • 데이터베이스와의 상호작용 : 데이터베이스에서 데이터를 가져오거나 저장할 때 DTO를 사용하여 데이터의구조를 정의한다.

DTO 사용 코드 예시

1. DTO 클래스 정의

먼저, BookDtoBookDetailDto 라는 두 개의 DTO 클래스를 정의한다. 이 클래스들은 데이터베이스의 Book 엔터티에서 필요한 속성만 포함한다.

namespace BookService.Models  
{  
    public class BookDto  
    {  
        public int Id { get; set; }  
        public string Title { get; set; }  
        public string AuthorName { get; set; }  
    }  
}  

namespace BookService.Models  
{  
    public class BookDetailDto  
    {  
        public int Id { get; set; }  
        public string Title { get; set; }  
        public int Year { get; set; }  
        public decimal Price { get; set; }  
        public string AuthorName { get; set; }  
        public string Genre { get; set; }  
    }  
}

2. GET 메서드에서 DTO 반환

BooksController 클래스에서 DTO를 반환하는 GET 메서드를 정의한다. LINQ의 Select 문을 사용하여 Book 엔터티를 DTO로 변환한다.

// GET api/Books  
public IQueryable<BookDto> GetBooks()  
{  
    var books = from b in db.Books  
                select new BookDto()  
                {  
                    Id = b.Id,  
                    Title = b.Title,  
                    AuthorName = b.Author.Name  
                };  

    return books;  
}  

// GET api/Books/5  
[ResponseType(typeof(BookDetailDto))]  
public async Task<IHttpActionResult> GetBook(int id)  
{  
    var book = await db.Books.Include(b => b.Author).Select(b =>  
        new BookDetailDto()  
        {  
            Id = b.Id,  
            Title = b.Title,  
            Year = b.Year,  
            Price = b.Price,  
            AuthorName = b.Author.Name,  
            Genre = b.Genre  
        }).SingleOrDefaultAsync(b => b.Id == id);  

    if (book == null)  
    {  
        return NotFound();  
    }  

    return Ok(book);  
}

3. POST 메서드에서 DTO 변환

책을 추가하는 POST 메서드에서도 DTO를 반환하도록 수정한다.

[ResponseType(typeof(BookDto))]  
public async Task<IHttpActionResult> PostBook(Book book)  
{  
    if (!ModelState.IsValid)  
    {  
        return BadRequest(ModelState);  
    }  

    db.Books.Add(book);  
    await db.SaveChangesAsync();  

    // Load author name  
    db.Entry(book).Reference(x => x.Author).Load();  

    var dto = new BookDto()  
    {  
        Id = book.Id,  
        Title = book.Title,  
        AuthorName = book.Author.Name  
    };  

    return CreatedAtRoute("DefaultApi", new { id = book.Id }, dto);  
}

위의 예시는 DTO를 사용하여 데이터베이스 엔터티에서 클라이언트로 전송할 데이터를 정의하고, 이를 통해 클라이언트가 필요한 정보만을 받을 수 있도록 하는 방법을 보여준다.


장점

  • 유지보수성 향상 : DTO를 사용하면 데이터 구조가 명확해져 코드의 유지보수성이 향상된다.
  • 보안 : DTO를 통해 클라이언트에 노출되는 데이터의 양을 제한할 수 있어 보안성을 높일 수 있다.

 

 

 

 

단점

  • 추가적인 코드 : DTO를 사용하면 추가적인 클래스를 생성해야 하므로 코드가 복잡해질 수 있다.
  • 성능 오버헤드 : DTO를 매핑하는 과정에서 성능 오버헤드가 발생할 수 있다.

https://learn.microsoft.com/en-us/aspnet/web-api/overview/data/using-web-api-with-entity-framework/part-5

 

Create Data Transfer Objects (DTOs)

Describes how to create data transfer objects (DTOs) manually using code to change the shape of the data sent to the client.

learn.microsoft.com


의존성 주입의 방법

의존성 주입 정의

  • 의존성 주입은 객체지향 프로그래밍에서 객체의 의존성을 외부에서 주입하여 클래스 간의 결합도를 낮추고, 교체 가능성을 높이는 디자인 패턴이다.
  • NestJS와 같은 프레임워크에서는 이 패턴을 활용하여 loC(Inversion of Control) 컨테이너를 통해 관리한다.

의존성 주입 사용 코드 예시

 

예를 들어, NestJS에서 간단한 서비스와 컨트롤러를 작성하여 의존성 주입을 구현하는 방법은 다음과 같다.

1. 서비스 클래스 정의

import { Injectable } from '@nestjs/common';  

@Injectable()  
export class CatsService {  
    private readonly cats: string[] = [];  

    findAll(): string[] {  
        return this.cats;  
    }  
}

2. 컨트롤러 클래스 정의

import { Controller, Get } from '@nestjs/common';  
import { CatsService } from './cats.service';  

@Controller('cats')  
export class CatsController {  
    constructor(private catsService: CatsService) {}  

    @Get()  
    async findAll(): Promise<string[]> {  
        return this.catsService.findAll();  
    }  
}

이 예시에서 CatsService@Injectable() 데코레이터를 통해 NestJS의 의존성 주입 컨테이너에 등록되고, CatsController 에서 생성자 주입을 통해 CatsService 에 접근할 수 있다.

https://docs.nestjs.com/fundamentals/custom-providers

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

 

 

 

장점

  1. 결합도 감소 : 객체가 자신의 의존성을 직접 생성하지 않기 때문에 클래스 간의 결합도가 낮아지며, 코드가 더 깨끗하고 유지보수가 쉬워진다.
  2. 테스트 용이성 : 의존성이 외부에서 주입되므로, 목(mock) 객체를 사용하여 단위 테스트를 쉽게 수행할 수 있다.
  3. 유연성 : 환경이나 상황에 따라 다양한 구현체를 주입할 수 있어 코드의 재사용성과 유연성이 증가한다.
  4. 유지보수성 향상 : 코드를 변경할 필요 없이 새로운 구현체를 만들어 쉽게 대체할 수 있다.

 

 

 

 

단점

  1. 복잡성 증가 : 의존성 주입을 구현하는 코드가 복잡해질 수 있으며, 특히 애플리케이션 규모가 커질 경우 흐름을 이해하기 어려울 수 있다.
  2. 애플리케이션 시작 시 오버헤드 : DI 컨테이너가 애플리케이션 시작 시 모든 의존성을 처리해야 하므로 초기화 과정에서 성능 저하가 있을 수 있다.
  3. 디버깅 어려움 : 의존 관계가 복잡하게 얽히게 되어 디버깅 시 문제가 발생하는 위치를 파악하기 어려울 수 있다.
  4. 기능이 고도하게 분리될 수 있음 : 너무 많은 작은 클래스와 인터페이스로 나누게 되어, 전체적인 로직 흐름이 산만해질 수 있다.

관련글 더보기