[Spring] 계층간 데이터 교환, Map vs DTO

얼레벌레 프로젝트를 진행할 적에는, 레이어 간 데이터를 교환할 때 Map을 사용했다.

이유는 VO, DTO, Entity 등 데이터 객체에 대한 이해가 부족했기 때문에.. 그냥 손에 잡히는 걸 사용했던 것이다.

Map을 사용하면서 여러모로 불편함을 느껴 DTO 클래스로 리팩토링했는데, 실제로 구글링해봤을 때에도 많은 개발자들이 Map보다는 DTO를 선호하는 것으로 보였다.

🤔 왜 DTO인가?

Map 대신 DTO를 사용하는 이유는 뭘까?

이는 Map의 단점으로 대신할 수 있다.

타입체크 불가

Map은 일반적으로 다음과 같이 사용한다.

HashMap<String, Object> map = new HashMap();

다양한 타입의 데이터를 받기 위해서는 value의 제네릭 타입을 Object로 설정하는 것이 불가피하다.

하지만 모든 클래스의 조상인 Object 클래스의 특성상 타입체크를 해주지 못해 잘못된 형식의 데이터가 입력되기도 한다.

 

가령 user 테이블에 새로운 행을 추가하기 위한 데이터 전달체로 Map을 사용한다고 하자.

나이를 나타내는 age 컬럼에는 숫자가 들어오는 것이 적합해 보인다.

 

하지만 이렇게 문자열이 들어와도 딱히 막을 방법이 없다.

 

HashMap<String, Object> map = new HashMap();
map.put("age", "seventeen");

 

이 코드 한줄이 불러올 대참사는 런타임에 SQL 에러가 발생하고 나서야 발견될 수 있을 것이다.

 

하지만 Map 대신 DTO를 사용하면 문제를 컴파일타임에 잡아낼 수 있다.

클래스에서 지정해준 타입에 부합하지 않는 데이터가 들어오면 바로 빨간줄이 뜨기 때문이다.

// UserDto
public class UserDto {
	/* ... */
    private int age;
}

// UserService
UserDto userDto = new UserDto();
userDto.setAge("seventeen"); // 에러

타입 캐스팅 비용

Object를 제네릭 타입으로 지정했을 때의 문제는 단순히 타입체크를 해주지 않는 불편함에 그치지 않는다.

map에서 age를 찾아 int 타입 변수로 참조한다고 하자.

 

다음과 같이 작성하면 컴파일 에러가 발생하다.

컴파일타임에는 객체의 실제 타입을 알 수 없기 때문에, Object 객체를 int 변수에 담으려는 시도는 허용되지 않는 것이다.

int age = map.get("age");

 

때문에 매번 형변환을 해줘야 하는데, 이는 상당히 번거롭다.

또한 정해진 타입이 없으니 누군가는 int로 받고, 누구는 long로 받게 되는데, 협업에서 장애로 작용할 우려가 있다.

int age = (int)map.get("age");

낮은 직관성

한 계층에서 다른 계층으로, 혹은 특정 메서드에서 다른 메서드로 데이터를 전달하는 상황을 생각해보자.

Map에 데이터를 담아 보내면, 받은 쪽에서는 콘솔에 찍어보지 않는 이상 Map에 어떤 key가 있는지 직관적으로 알기 힘들다.

나이 데이터를 보낼 때 age라는 키로 넣을 수도 있고 user_age라는 키로 넣을 수도 있지만, 받는 입장에서는 둘 중 무슨 키를 사용했는지 알길이 없다.

map.put("age", 11);
map.put("user_age", 11);

 

또한 get(K key) 메서드를 통해 요소를 찾은 결과값이 null인 경우, 그것이 정확히 무엇을 의미하는지 알 수 없다.

맵에 해당 key가 없는 것일수도 있고, key는 있지만 연결된 값이 null일 수도 있지만 이 두가지는 정확하게 구분되지 않는다.

map.put("age", null);

Integer age = map.get("age");	// null
Integer age = map.get("user_age");	// null

 

하지만 DTO를 사용할 경우, 해당 객체에 어떤 속성이 어떤 속성명으로 정의되어 있는지 직관적으로 파악할 수 있다.

속성에 접근하려면 클래스에 정의된 getter를 사용해야 하는데, 부적절한 메서드명으로 접근할 경우 컴파일 에러를 유발해 전술한 문제의 발발 가능성을 차단한다.

// UserDto
public class UserDto {
	private String id;
    /* ... */
    private int age;
}

// UserService
UserDto userDto = new UserDto();
int age = userDto.getAge();
int age = userDto.getUserAge();	// 에러