본문 바로가기
자바

간단한 ObjectMapper 만들어보기 (Java Reflection API)

by 나무후추통 2022. 9. 15.

 자바는 Serializable을 통해서 JVM끼리 객체를 주고받을 수가 있다. 하지만 모든 시스템이 JVM 기반으로 작동한다고 할 수 없다. 그렇다면 서로 다른 시스템들끼리 통신을 어떻게 할까? 간단한 방법은 약속한 형식으로 메시지를 전달해서 수신자가 메시지 정보를 기반으로 자기 시스템에 맞는 데이터를 생성하는 것이다.

 

 대부분 웹에서는 JSON 형식으로 전달 받아서 객체를 생성한다. 프레임 워크, 라이브러리가 보이지 않는 작업을 전부 처리해 주기 때문에 개발자가 직접 이것을 해석할 필요는 없지만, 곰곰이 생각해보면 상당히 복잡한 과정이다.

 

1. 런타임에 객체를 생성해야 한다.

2. 원시 타입, 레퍼런스 타입, List-Set-Map 등 컬렉션은 어떻게 변환할 것인가?

3. 객체 안에 객체가 있다면 어떻게 변환할 것인가?

4. 자바 지네릭은 런타임까지 타입 정보를 가져오지 않는다. 즉, 런타임에는 타입이 Object 또는 extends or super (type)으로 치환된다.

5. 그 외...

 

 그 외 고려할 것들도 있을 것이다. 이 글에서는 (1번)과 (2번 - 원시 타입)까지만 고민하고 간단한 ObjectMapper를 만들어 보았다.

 

1. 클래스 다이어그램

 

그림 1. 클래스 다이어그램

 객체를 전달하는 포맷은 다양한 방법들이 있다. 다양한 포맷을 지원하기 위해서 전략 패턴을 사용해서 구현하였다.

 

2. 코드 설명

2.1 클라이언트 코드

public class PrimitivesAndString {

    private boolean aBoolean;
    private byte aByte;
    private char aChar;
    private short aShort;
    private int anInt;
    private long aLong;
    private float aFloat;
    private double aDouble;
    private String aString;
		
		// getter, setter, equals, hashCode ...
}
  • 테스트를 위한 원시타입, String만 저장한 객체이다.
public class Client {

    public static void main(String[] args) throws NoSuchMethodException {
		// Generator 객체에 사용할 전략 지정.
        Generator generator = new Generator();
        generator.setStrategy(new JsonStrategy());

		// 원시타입과 String으로 이루어진 객체 초기화
        PrimitivesAndString testObject = new PrimitivesAndString();
        testObject.setaBoolean(true);
        testObject.setaByte((byte) 1);
        testObject.setaChar('a');
        testObject.setaString("hello");

		// JSON으로 직렬화 진행
        generator.writeValue(new File("testObject.json"), testObject);

		// JSON 역직렬화 진행
        PrimitivesAndString jsonObject = null;
        jsonObject = (PrimitivesAndString) generator.readValue(new File("testObject.json"), PrimitivesAndString.class);

		// true 반환
        System.out.println(testObject.equals(jsonObject));
        
    }
}
  • 객체를 직렬화한 뒤 역직렬화 하여 원본 객체와 같은 지를 비교하였다.
{
	"aBoolean":true,
	"aByte":1,
	"aChar":"a",
	"aShort":0,
	"anInt":0,
	"aLong":0,
	"aFloat":0.0,
	"aDouble":0.0,
	"aString":"hello"
}
  • 직렬화된 객체는 위와 같다.

2.2 Generator 코드

public class Generator {

    private FormatStrategy strategy;

    public void setStrategy(FormatStrategy strategy) {
        this.strategy = strategy;
    }

    public void writeValue(File file, Object obj) {
        strategy.writeValue(file, obj);
    }

    public Object readValue(File file, Class<?> cls) throws NoSuchMethodException {
        return strategy.readValue(file, cls);
    }
}
  • 사용할 전략(포맷)을 할당하고 직렬화와 역직렬화를 실행한다.

2.3 전략 구현 코드 (FormatStrategy와 JsonStrategy 코드)

public interface FormatStrategy {

	// 지정된 파일에 객체를 씀.
    void writeValue(File file, Object obj);

	// 지정된 파일을 읽어 객체를 생성함.
    Object readValue(File file, Class<?> cls) throws NoSuchMethodException;

	// 파일 형식을 fieldName-fieldValue 형식으로 매핑.
    Map<String, String> parse(String format);
}

public class JsonStrategy implements FormatStrategy {

    @Override
    public void writeValue(File file, Object obj) {
    	// 객체에서 필드 정보를 가져옴
        Field[] fields = obj.getClass().getDeclaredFields();

        StringBuilder jsonBuilder = new StringBuilder();
        jsonBuilder.append("{");

        for (Field field : fields) {
        	// 모든 필드에 접근이 가능하도록 설정
            field.setAccessible(true);
            Object fieldValue = null;
            // 필드 타입과 이름 가져오기
            Class<?> fieldType = field.getType();
            String fieldName = field.getName();

            try {
                fieldValue = field.get(obj);
				// 필드 타입에 따라 이름과 값을 문자열로 변환
                jsonBuilder.append(addFieldAndValue(fieldType, fieldName, fieldValue));

            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            jsonBuilder.append(",");
        }
        jsonBuilder.deleteCharAt(jsonBuilder.length()-1);
        jsonBuilder.append("}");

		// 만들어진 문자열을 파일에 씀.
        writeToFile(file, jsonBuilder.toString());

    }

    @Override
    public Object readValue(File file, Class<?> cls) {
        String json = readFromFile(file);

		// JSON 파일을 파싱해 fieldName-fieldValue로 매핑.
        Map<String, String> map = parse(json);

        Object obj = null;

        try {
        	// JSON 파일에서 필드 이름과 타입에 따라 객체에 값 지정.
            obj = setFields(cls, map);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return obj;
    }

    @Override
    public Map<String, String> parse(String json) {
        Map<String, String> map = new LinkedHashMap<>();

        StringTokenizer tokenizer = new StringTokenizer(json, "{}:,\"");
        while(tokenizer.hasMoreTokens()) {
            String fieldName = tokenizer.nextToken();
            String fieldValue = tokenizer.nextToken();
            map.put(fieldName, fieldValue);
        }

        return map;
    }
}
  • 필드 값과 타입, 이름 정보를 가져와 파일에 쓴다.

상세 코드

더보기
public class JsonStrategy implements FormatStrategy {

    @Override
    public void writeValue(File file, Object obj) {
        Field[] fields = obj.getClass().getDeclaredFields();

        StringBuilder jsonBuilder = new StringBuilder();
        jsonBuilder.append("{");

        for (Field field : fields) {
            field.setAccessible(true);
            Object fieldValue = null;
            Class<?> fieldType = field.getType();
            String fieldName = field.getName();

            try {
                fieldValue = field.get(obj);
                jsonBuilder.append(addFieldAndValue(fieldType, fieldName, fieldValue));
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }

            jsonBuilder.append(",");
        }
        jsonBuilder.deleteCharAt(jsonBuilder.length()-1);
        jsonBuilder.append("}");

        writeToFile(file, jsonBuilder.toString());

    }

    @Override
    public Object readValue(File file, Class<?> cls) {
        String json = readFromFile(file);
        Map<String, String> map = parse(json);
        Object obj = null;

        try {
            obj = setFields(cls, map);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return obj;
    }


    @Override
    public Map<String, String> parse(String json) {
        Map<String, String> map = new LinkedHashMap<>();

        StringTokenizer tokenizer = new StringTokenizer(json, "{}:,\"");
        while(tokenizer.hasMoreTokens()) {
            String fieldName = tokenizer.nextToken();
            String fieldValue = tokenizer.nextToken();
            map.put(fieldName, fieldValue);
        }

        return map;
    }

    private Object setFields(Class<?> cls, Map<String, String> map) throws NoSuchMethodException {
        Field[] fields = cls.getDeclaredFields();
        Constructor<?> constructor = cls.getConstructor();
        Object o= null;

        try {
            o = constructor.newInstance();
        } catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }

        for (Field field : fields) {
            field.setAccessible(true);
            String fieldName = field.getName();
            Class<?> fieldType = field.getType();

            try {
                if (fieldType.equals(Boolean.TYPE)) {
                    field.setBoolean(o, Boolean.valueOf(map.get(fieldName)));
                } else if (fieldType.equals(Byte.TYPE)) {
                    field.setByte(o, Byte.valueOf(map.get(fieldName)));
                } else if (fieldType.equals(Character.TYPE)) {
                    char c = map.get(fieldName).equals("null") ? '\u0000' : map.get(fieldName).charAt(0);
                    field.setChar(o, c);
                } else if (fieldType.equals(Short.TYPE)) {
                    field.setShort(o, Short.valueOf(map.get(fieldName)));
                } else if (fieldType.equals(Integer.TYPE)) {
                    field.setInt(o, Integer.valueOf(map.get(fieldName)));
                } else if (fieldType.equals(Long.TYPE)) {
                    field.setLong(o, Long.valueOf(map.get(fieldName)));
                } else if (fieldType.equals(Float.TYPE)) {
                    field.setFloat(o, Float.valueOf(map.get(fieldName)));
                } else if (fieldType.equals(Double.TYPE)) {
                    field.setDouble(o, Double.valueOf(map.get(fieldName)));
                } else {
                    String str = map.get(fieldName).equals("null") ? null : map.get(fieldName);
                    field.set(o, str);
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

        return o;
    }

    private String addFieldAndValue(Class<?> fieldType, String fieldName, Object fieldValue) {
        StringBuilder pair = new StringBuilder();
        pair.append("\"")
            .append(fieldName)
            .append("\"")
            .append(":");

        if (Character.TYPE.equals(fieldType)) {
            fieldValue = fieldValue.equals('\u0000') ? "null" : fieldValue;
        } else {
            fieldValue = fieldValue == null ? "null" : fieldValue;
        }

        if (!fieldValue.equals("null") && fieldType.equals(String.class)){
            pair.append("\"")
                .append(fieldValue)
                .append("\"");
        } else if (!fieldValue.equals("null") && Character.TYPE.equals(fieldType)) {
            pair.append("\"")
                .append(fieldValue)
                .append("\"");
        } else {
            pair.append(fieldValue);
        }

        return pair.toString();
    }

    private void writeToFile(File file, String json) {
        try(BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(file))){
            bufferedWriter.write(json);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private String readFromFile(File file) {
        String json = null;
        try (BufferedReader bufferedReader = new BufferedReader(new FileReader(file))) {
            json = bufferedReader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return json;
    }
}

'자바' 카테고리의 다른 글

JVM과 Call Stack (Stack, Frame)  (0) 2023.01.29