티스토리 뷰
필요한 데이터를 저장하기 위해 Map<String, Object>를 사용하는 개발자들이 있습니다. 하지만 Map을 사용하면 너무 많은 단점들을 안게 되는 것 같아서, 왜 Map이 아닌 DTO 클래스를 사용해야 하는지에 대해 정리해보고자 합니다.
1. Map보다 DTO 클래스를 사용해야 하는 이유
[ Map을 사용할 때의 단점 ]
- 컴파일 에러를 유발할 수 없음
- String 텍스트를 Key로 사용함
- 가독성이 떨어짐
- 타입캐스팅 비용이 발생함
- 불변성을 확보할 수 없음
1. 컴파일 에러를 유발할 수 없음
Map의 Value는 Object 타입이다. 그리고 Object 클래스는 최상위 클래스이기 때문에 어떠한 데이터도 받아드릴 수 있다. Object를 사용할 때의 문제는 어떠한 데이터도 받아드릴 수 있기 때문에 타입 체크를 할 수 없다는 것 이다. 만약 long 타입을 넣어줘야 하는데, int 타입을 넣어줘도 해당 코드는 컴파일 에러를 유발하지 않는다.
Map<String, Object> map = new HashMap<>();
// long이 아닌 int를 넣어도 컴파일 에러가 발생하지 않음
map.put("height", 180);
만약 이러한 문제가 실제 코드에 있다면, 우리는 이러한 문제를 런타임 시점에 알게 될 것이다. 그렇기 때문에 타입에 대해 엄격하게 가져가고, 컴파일 에러를 얻기 위해서 DTO를 사용하는 것이 좋다.
2. String 텍스트를 Key로 사용함
예를 들어 실수로 Map에 데이터를 추가하는 경우에 name 대신 namez로 오타가 입력 되었다고 하자. 만약 우리가 이를 인지하지 못한다면 불필요한 문제 때문에 시간낭비가 발생할 수 있다.
Map<String, Object> map = new HashMap<>();
map.put("name", "MangKyu");
String name = (String) map.get("namez");
단순 String을 사용한다는 것은 문제가 발생할 여지가 항상 열려있다는 것이다. 굳이 문제의 가능성을 열어두고 이로 인해 불필요한 시간 낭비의 여지를 줄 필요가 없다. DTO를 이용하면 이러한 문제를 해결할 수 있다.
3. 가독성이 떨어짐
위와 같이 Map을 사용하는 구조는 가독성이 떨어진다. 어떠한 데이터를 가지고 있는지 확인할 때에는 Map보다 DTO를 확인하는 것이 직관적이고 좋다. 만약 Map<String, Object>를 본다면, 우리가 받는 Key값은 무엇이고, Value값은 무엇이며 어떠한 타입인지를 파악하기가 쉽지 않다. 만약 Map안에 또 다른 Map이 들어있다면 이러한 문제는 더욱 심각해진다. 결국 Map으로 작성된 코드를 이해하기 위해서는 불필요한 코드 리딩 시간이 필요하고 생산성이 떨어지게 된다.
Map<String, Object> result = getResult();
Map<String, Object> user = (Map<String, Object>) map.get("user");
만약 Map이 여러 메소드들을 통해 파라미터로 넘어가는 경우에 이러한 문제는 더욱 심각해진다. 만약 어떤 메소드부터 시작해서 다른 메소드를 호출할 때 Map을 파라미터로 남겨준다면 우리는 해당 모든 메소드들을 뒤적거리며 어떠한 값이 put 또는 remove 되는지 파악해야 할 것이다.
public void start() {
Map<String, Object> map = new HashMap<>();
... // map이 put/remove 될 수 있음
Map<String, Object> result = logic1(map);
}
public Map<String, Object> logic1(Map<String, Object> map) {
... // map이 put/remove 될 수 있음
logic2(map);
... // map이 put/remove 될 수 있음
return map;
}
public Map<String, Object> logic2(Map<String, Object> map) {
... // map이 put/remove 될 수 있음
logic3(map);
... // map이 put/remove 될 수 있음
return map;
}
예를 들어 위와 같은 코드가 있다고 하자. 우리는 start에서 만들어진 map이 logic1을 거쳐 다른 logicN에 도달해서 최종적으로 어떠한 값이 반환되는지를 파악하기 위해 모든 코드들을 앞뒤로 살펴 파악해야 한다.
하지만 만약 DTO를 사용한다면 가독성과 관련된 문제를 해결하고 생산성을 높일 수 있다.
public class UserResponse {
private String name;
private int age;
private int height;
private int iq;
}
public void start() {
...
UserResponse response = logic1();
}
4. 타입캐스팅 비용이 발생함
Map에 있는 데이터를 꺼내서 사용하기 위해서는 반드시 타입 캐스팅을 해야한다.
String name = (String) map.get("name");
그리고 이러한 타입 캐스팅은 당연히 컴퓨팅 비용을 필요로 한다. 만약 꺼내야 하는 데이터가 많다면 이러한 비용은 더욱 증가하게 된다.
불필요한 타입 캐스팅 비용을 줄이기 위해서도 DTO를 사용하는 것이 좋다.
5. 불변성을 확보할 수 없음
Map을 사용하면 해당 데이터의 불변성을 확보할 수 없다. 만약 누군가가 실수로 put 코드를 추가하였다면 기존의 데이터는 없어지고 잘못된 데이터로 덮어 씌워진다.
Map<String, Object> map = new HashMap<>();
map.put("name", "MangKyu");
// 불변성을 확보할 수 없고 값이 변경될 수 있음
map.put("name", 1);
불변성을 확보하는 것은 불필요한 시간을 절감하는 등을 위해 상당히 중요하다. 하지만 Map을 사용하면 불변성을 확보할 수 없으니 DTO를 사용하는 것이 좋다. (불변성을 확보해야 하는 이유에 대해 이해가 부족하다면 이 포스팅을 참고해주세요)
Map을 사용하면 위와 같은 단점들을 안게 된다. 이러한 이유로 DTO(Data Transfer Object)라는 데이터 전달 클래스를 사용하는 것이 좋다. DTO를 사용하면 추가적으로 정적 팩토리 메소드를 구현하여 많은 이점을 얻을 수도 있고, 빌더 패턴도 적용할 수 있어 상당히 유용하다. 실제로 업무를 하다보면 이러한 단점을 더욱 잘 체감할 수 있다.
물론 개인적으로 매우 제한적인 경우에 Map을 사용하기도 하는데, 현재도 그렇고 미래에도 절대적으로 단일 Key값을 갖는 케이스라면 컨트롤러에서 Collections.singletonMap로 응답을 반환하기도 한다. 그 외에도 Map을 사용해서 위에서 얘기한 단점들이 부각되지 않거나 유지보수성을 떨어뜨리지 않는다면 Map을 사용해서 오히려 좋아지는 케이스도 있다. 그렇기 때문에 상황을 고려해보았을 때, Map을 사용하여도 위에 적힌 단점들이 부각되지 않거나 오히려 이점을 얻을 수 있는 경우에는 Map을 사용해도 괜찮다. 하지만 위에서 얘기했던 단점들이 드러나는 상황이라면 Map보다는 DTO를 이용하는 것을 권장한다.
혹시 제가 놓쳤거나 추가할만한 내용이 있다면 댓글 남겨주세요! 업데이트 하도록 하겠습니다:)
'Java & Kotlin' 카테고리의 다른 글
[Java] 언제 Optional을 사용해야 하는가? 올바른 Optional 사용법 가이드 - (2/2) (34) | 2022.01.02 |
---|---|
[Java] 메소드 오버라이딩/ 메소드 오버로딩을 통한 상속 다형성에 대한 이해와 Self 참조 (0) | 2021.10.15 |
[Java] 빌더 패턴(Builder Pattern)을 사용해야 하는 이유 (18) | 2021.06.26 |
[Java] 체크 예외(Check Exception)와 언체크 예외/런타임 예외 (Uncheck Exception, Runtime Exception)의 차이와 올바른 예외 처리 방법 (11) | 2021.05.13 |
[Java] JUnit을 활용한 Java 단위 테스트 코드 작성법 (2/3) (11) | 2021.04.20 |