1. 문제 발생

- 신규 서버 프로젝트의 구조 및 개발 방향 구상 중, DB model object(이하 DTO)와 Service model object(VO)을 따로 구현 및 관리하기로 결정이 되었음

- 기존 운영 프로젝트에선 DTO의 구조가 그대로 Client까지 전달이 되었지만, 신규 구성에선 그런 동작을 방지하기로 결정 

- 하지만 클라이언트로 전달되지 말아야 하는 데이터 이외의 대부분의 값들은 Client에게 전달이 되어야 하는 상황

 

2. 방안 1

- DTO로 받은 각 값들을 일일이 소스 코드에서 변환 처리 해준다

- 개인적으로는 이게 취향

- 성능적으로도 솔직히 이것만큼 예측이 쉽고 문제 원인이 될 것도 없음

- 하지만 Human error의 위험 및 엄청난 귀차니즘, 장기간의 소스 코드 관리의 문제가 발생할 수 있음 

- 나중에 DB table 구조에 수정 및 추가가 생기면 다 수정해야 하는 시간 비용도 두려움

 

3. 방안 2 (진행)

- DTO와 VO간의 자동 변환 기능을 만들자

- 솔직히 아마 검색해보면 분명히 있을거지만, 요즘 개인 개발이 좀 느슨해진김에 해볼까 함

- 개념상으로만 알고 있던 Reflection 코딩 적용하면, 약간의 규칙만 정해주면 가능할 것 같았음

- 결과가 괜찮으면 검토 건의 해보지 뭐 ㅋ

 

4. 구성 및 기초 설계

4-1. 규칙 1

DTO와 VO간의 동일한 타입 및 이름이어야 한다.

"DB에서 int로 쓰고있지만 클라이언트는 String으로 받아서 쓸 것이다"라는 규칙으로 개발을 해봤지만, 좋은 꼴을 본 적이 없다.

4-2. 규칙 2

규칙 1을 기반으로, 각 객체의 field는 Java에서 제안하는 getter/setter 규격을 따라야 한다. 이 네이밍 규칙을 기준으로 데이터 복사 작업을 진행할 것이다. 또한 최근 자바 프로젝트는 다 이 규격을 사용하는 lombok 라이브러리를 통해 관리하기 때문에 이렇게 판단했다. 

 

5. 구현 

더보기
package reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;

public class ConverterUtil{

    public static <T_DTO, T_VO> void convertObject(T_DTO objectDTO, T_VO objectVO) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 1. 공통 method 조회(Object Class method 제외 처리에 사용)
        HashSet<String> objectMethodSet = new HashSet<>();
        {
            Object testObj = new Object();
            Method[] objectMethods = testObj.getClass().getMethods();
            for(int i = 0 ; i < objectMethods.length ; i ++){
                objectMethodSet.add(objectMethods[i].getName());
            }
        }

        // 2. DTO getter method 정리
        HashSet<String> DTO_getterSet = new HashSet<>();
        {
            Method[] methods = objectDTO.getClass().getMethods();
            for(int i = 0 ; i < methods.length ; i ++){
                if(!objectMethodSet.contains(methods[i].getName())){
                    if(methods[i].getName().substring(0,3).equals("get")){
                        DTO_getterSet.add(methods[i].getName());
                    }
                }
            }
        }


        {
            // 3. VO setter method 
            HashMap<String, String> VO_getter_Map = new HashMap<>();
            HashMap<String, String> VO_setter_Map = new HashMap<>();

            Method[] methods = objectVO.getClass().getMethods();
            for (int i = 0; i < methods.length; i++) {
                if (!objectMethodSet.contains(methods[i].getName())) {
                    if (methods[i].getName().substring(0, 3).equals("get")) {
                        VO_getter_Map.put(methods[i].getName().substring(3, methods[i].getName().length()), methods[i].getName());
                    }
                    if (methods[i].getName().substring(0, 3).equals("set")) {
                        VO_setter_Map.put(methods[i].getName().substring(3, methods[i].getName().length()), methods[i].getName());
                    }
                }
            }

            // 4. 복사 작업 진행
            Iterator<String> getterKeyItr = DTO_getterSet.iterator();
            while (getterKeyItr.hasNext()) {

                String getKey = getterKeyItr.next();
                String fieldName = getKey.substring(3, getKey.length());
                if (VO_getter_Map.containsKey(fieldName)) {

                    Method testVOGetMethod = objectVO.getClass().getMethod(getKey);

                    Class dtoClass = objectDTO.getClass().getMethod(getKey).invoke(objectDTO).getClass();
                    Class objToPriClass = null;
                    if (dtoClass == Integer.class) {
                        objToPriClass = int.class;
                    } else if (dtoClass == Double.class) {
                        objToPriClass = double.class;
                    } else if (dtoClass == Float.class) {
                        objToPriClass = float.class;
                    } else if (dtoClass == Character.class) {
                        objToPriClass = char.class;
                    } else if (dtoClass == Long.class) {
                        objToPriClass = long.class;
                    } else if (dtoClass == Byte.class) {
                        objToPriClass = byte.class;
                    } else if (dtoClass == Boolean.class) {
                        objToPriClass = boolean.class;
                    } else if (dtoClass == Short.class) {
                        objToPriClass = short.class;
                    } else {
                        objToPriClass = objectDTO.getClass().getMethod(getKey).invoke(objectDTO).getClass();
                    }

                    Method testVOSetMethod = objectVO.getClass().getMethod(VO_setter_Map.get(fieldName), objToPriClass);

                    testVOSetMethod.setAccessible(true);

                    testVOSetMethod.invoke(
                            objectVO,
                            objectDTO.getClass().getMethod(getKey).invoke(objectDTO)
                    );

                }
            }
        }
    }
}

6. 테스트 진행

test.zip
0.01MB

프로젝트의 testMain.java 실행하면 된다. 

jdk 1.8 기준으로 작성했다.

그냥 객체 두 개 두고 복사되는지만 봤다. 안보는게 낫다 

 

7. 결론 및 후기 

어차피 공부 겸 재미용으로 만든 코드이기 때문에 테스트를 여러가지 케이스에서 막 굴리지는 않았다. 

무조건 이름이 같은 경우에만 복사하고 아님 말고의 방식으로 개발되어, 규칙을 지켜주며 객체 생성을 하지 않을거라면 못쓰는 코드에 가깝다. 

무엇보다 이거 괜찮은거 맞나? 싶다. 모든 서버 API 반환부에서 저 함수가 실행된다고 생각하면, 매번 method를 순회하고 타입 비교를 해가면서 변환 시켜준다는 것이 cpu 성능을 필요 이상으로 사용하는 건 아닌가 하는 걱정이 든다. 솔직히 개발 중간에 이럴거면 뭐하러 만드나 하는 생각까지 했지만, 그래 어차피 연습용이니까 하고 진행했다. 

까놓고 옛날에 xml 설정으로 각 객체간 mapping 등록을 해서 복사해주는 방식도 본적이 있긴한데 걔나 얘나 다 별로같다. 

 

* 귀찮아서 그냥 진행하긴 했는데 여기서 말하는 DTO가 Entity 객체고, VO가 DTO다. 시작할 때 네이밍을 잘못 정했다. 

* 빡치게 진짜 있네 

https://www.baeldung.com/entity-to-and-from-dto-for-a-java-spring-application

근데 이 라이브러리도 내부를 까본건 아니지만 parameter가 객체와 class type을 받아가는 것 보면, 결국 방향성은 내 코드와 비슷해보인다. 물론 예외처리나 최적화는 더 잘했겠지만!

+ Recent posts