본문 바로가기
컴퓨터/소프트웨어 공학

[OOP] 올바른 DTO 객체를 만들어 보자/+github

by 도도새 도 2023. 11. 24.

DTO

 

Data Transfer Objects는 소프트웨어 개발에서 흔한 디자인 패턴이다. dto는 레이어 사이, 혹은 시스템 사이 데어터를 전달할 때 사용된다. 이를테면 Spring의 경우 컨트롤러에서 서비스 레이어로 값을 전달할 때 dto를 사용할 수 있다.

 

스프링의 레이어

스프링의 레이어

  • 프레젠테이션 계층
    • 클라이언트의 요청 및 응답을 처리한다
    • @Controller 어노테이션이 사용된 클래스는 스프링 MVC에서 웹 요청을 처리하는 역할을 하게 된다.
  • 비즈니스 계층
    • 어플리케이션의 핵심 비즈니스가 위치하는 곳이다.
    • 트랜잭션, 보안, 데이터 처리 등 주요 비즈니스 로직이 포함된다.
  • 데이터 액세스 계층
    • 데이터베이스와의 상호작용을 담당한다.
    • JDBC나 ORM 기술을 사용하여 DB와 통신한다.

 

DTO

 

DTO를 사용하는 이유?

dto는 레이어 간 데이터를 안전하고 효율적으로 전송하는 방법을 제공해준다. DTO객체를 캡슐화함으로서 해당 데이터들이 허가된 특정 레이어나 시스템에서만 액세스되거나 수정되는 것을 보장받을 수 있다. 게다가 전송 되어야 할 데이터의 양을 DTO를 사용해 제한함으로서 시스템의 성능을 향상시킬 수 있다.

 

좋은 DTO를 만드는 방법

dto는 간단해야한다. 즉, 로직 없이 데이터만을 담고 있어야한다.

(로직 없이 - 이 부분에 대해서 고민을 해봤다. 예를 들어 productDto에 stock이라는 프로퍼티가 있다. 객체 지향적 관점으로 봤을 때, stock이 0일 경우 에러를 던지는 코드는 productDto가 책임져야 하지 않을까 하는 생각을 했다. 그러나 조금 더 조사한 바로 이러한 로직은 해당 dto를 사용하는 서비스나 도메인 객체에서 책임지거나, 팩토리를 사용하여 dto를 생성하는 단계에서 처리하여야한다.)

 

단순하게 작성

DTO는 데이터만 담고 있을 뿐, 로직이 담겨서는 안된다. 그러므로 어떠한 메소드도 없도록 구성한다.

 

DTO factory 사용

팩토리 패턴을 이용하여 DTO 객체를 생성한다. 팩토리는 DTO를 생성하는 일관되고 효율적인 방법이다.

 

빌더 패턴 사용

빌더 패턴을 사용하면 유연한 DTO 객체를 생성할 수 있다. 빌더 패턴을 통해서 DTO객체의 프로퍼티를 하나씩 초기화 할 수 있게 된다.

 

불변성 지니기

DTO 객체는 불변성을 지녀야한다. 즉 DTO 객체가 한번 세팅되고 나면 프로퍼티에 변화가 있어서는 안된다. 이는 값을 실수로 변경하는 에러를 방지해준다.

 

Value Object 사용

Value Object는 값을 나타내는 객체이다. 주소를 나타내는 Address를 값 객체로 생성할 수 있다. DTO 객체는 특정 의미있는 데이터를 나타내기 위해 이러한 value object를 사용해야한다. 즉, 프로퍼티(링크)로 객체를 가져야 한다는 것이다.

// 값 객체: Address
public class Address {
    private String street;
    private String city;
    private String zipCode;

    public Address(String street, String city, String zipCode) {
        this.street = street;
        this.city = city;
        this.zipCode = zipCode;
    }

    // 게터 메서드는 여기에 추가될 수 있음
}

// DTO: UserDTO
public class UserDTO {
    private String username;
    private String email;
    private Address address; // 값 객체를 포함

    public UserDTO(String username, String email, Address address) {
        this.username = username;
        this.email = email;
        this.address = address;
    }
}

 

일관적인 네이밍 컨벤션 사용

일관된 네이밍 컨벤션을 사용함으로서 이해하기 좋고 협업에 유용한 dto를 만들 수 있게 된다.

 

null 사용 피하기

DTO의 값에 null을 넣는 것을 피해야 한다. null사용은 예상치 못한 동작이나 에러를 발생시키기 때문이다. 대신 -1 같은 기본 값(default value)를 데이터에 삽입하며 사용하자.

 

DTO 생성 전에 유효성 체크하기

DTO를 사용하기 전에 DTO에 삽입될 값들이 유효한지 검증하여야한다. 이로서 DTO내의 데이터가 일관되게 유효성을 가질 수 있다.

 

DTO 생성 실습

 

학생의 정보를 담고 있는 StudentDTO를 생성한다.

우선 StudentDTO를 생성한다.

package practice;

public class StudentDTO {
	public static final String NO_DATA = (String)DtoDefaultEnum.NO_DATA.getValue();
	public static final int NO_DATA_NUMBER = (int)DtoDefaultEnum.NO_DATA_NUMBER.getValue();

	private int id = NO_DATA_NUMBER;
	private String name = NO_DATA;
	private int age = NO_DATA_NUMBER;
	private Address address = new Address();
	
	public StudentDTO(int id, String name, int age, Address address) {
		super();
		this.id = id;
		this.name = name;
		this.age = age;
		this.address = address;
	}

	public int getId() {
		return id;
	}

	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}

	public Address getAddress() {
		return address;
	}
}
  • 불변성을 지키기 위해 setter는 구현하지 않는다. 만약 새로운 값이 들어간 student가 필요하다면? 새로운 dto객체를 생성하여야 한다. 이런 식으로 불변성을 지킬 수 있다.
  • default값을 삽입하여 프로퍼티에 null이 들어가는 것을 방지하였다. default값은 DTO에 공통적으로 사용될 것이므로 enum클래스로 관리한다.

StudentDTOBuilder

package practice;

public class StudentDTOBuilder {
	public static final String NO_DATA = (String)DtoDefaultEnum.NO_DATA.getValue();
	public static final int NO_DATA_NUMBER = (int)DtoDefaultEnum.NO_DATA_NUMBER.getValue();
	
	private int id = NO_DATA_NUMBER;
	private String name = NO_DATA;
	private int age = NO_DATA_NUMBER;
	private Address address = new Address(NO_DATA, NO_DATA, NO_DATA);
	
	public StudentDTOBuilder() {}

	public StudentDTOBuilder id(int id) {
		this.id = id;
		return this;
	}
	
	public StudentDTOBuilder name(String name) {
		this.name = name;
		return this;
	}
	
	public StudentDTOBuilder age(int age) {
		this.age = age;
		return this;
	}
	
	public StudentDTOBuilder address(Address address) {
		this.address = address;
		return this;
	}
	
	public StudentDTO build() {
		return new StudentDTO(id, name, age, address);
	}
}
  • StudentDTO 인스턴스 생성을 위한 빌더 패턴을 구현한다. 빌더 패턴을 이용하면 더욱 쉽게 값을 세팅할 수 있게 된다.
  • 마찬가지로 default값을 넣었다.

StudentDTOFactory

package practice;

public class StudentDTOFactory {
	public static StudentDTO createStudentDto(StudentDTO student) {
		checkError(student);
		
		return student;
	}
	
	public static StudentDTO createStudentDto(int id, String name, int age, Address address) {
		StudentDTO student = new StudentDTOBuilder()
				.id(id)
				.age(age)
				.address(address)
				.build();
		checkError(student);

		return student;
	}
	
	private static void checkError(StudentDTO student) {
		boolean errorCondition = (student.getId() <= 0)||
				(student.getName().equals("noData") )||
				(student.getAge() <= -1)||
				(student.getAddress().hasError());
		//유효성 검증
		if(errorCondition) {
            throw new IllegalArgumentException("Invalid StudentDTO properties");
		}
	}
}
  • 빌더 패턴과 마찬가지로 생성 패턴에 해당하는 심플 팩토리 패턴을 구현하였다.
  • creatStudentDto는 오버로딩 되어있다. 이 중 하나는 빌더 패턴으로 생성한 객체를 삽입하는 형태, 나머지는 값을 직접 넘겨주는 형태이다. 두 형태 모두 에러 체킹을 한다.
  • 사실상 에러 체킹을 하기 위한 목적으로 도입된 팩토리이다. 여기서는 값이 세팅이 되지 않았을 경우 에러를 뱉도록 처리하였다.

DtoDefaultEnum

package practice;

public enum DtoDefaultEnum {
    NO_DATA("noData"),
    NO_DATA_NUMBER(-1);

    private final Object value;

    DtoDefaultEnum(Object value) {
        this.value = value;
    }

    public Object getValue() {
        return value;
    }
}
  • 디폴트 데이터를 나타내는 이넘클래스이다.

Main(사용하기)

package practice;
import java.util.*;
import java.io.*;

public class Main {
	static Scanner sc = new Scanner(System.in);
	public static void main(String[] args) {
		try {
			StudentDTO student1 = StudentDTOFactory.createStudentDto(new StudentDTOBuilder()
					.id(1)
					.name("홍길동")
					.age(23)
					.address(new Address("here", "Pusan", "123"))
					.build()); 
			
			System.out.println(student1.getAge());
		} catch (Exception e) {
			System.out.println("exception : " + e);
		}
		
	}

}
  • 이렇게 구현된 코드는 위의 형태처럼 사용된다. 저 간단한 코드 내에 에러 체킹까지 포함되는 것이다. 이렇게 함으로서 DTO는 DTO의 역할(데이터를 저장하고 전달한다)에 집중 할 수 있게 된다. 즉, 에러 체킹 등 기타 로직을 신경 쓸 필요 없다는 것이다!

댓글