๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๐Ÿงฑ Back-end

Json Transcoder ์„ค๊ณ„ ๊ณผ์ •

ARCUS ์บ์‹œ๋ฅผ ์œ„ํ•œ Json Transcoder ํด๋ž˜์Šค๋ฅผ ์„ค๊ณ„ํ•˜๋ฉด์„œ ํƒ€์ž… ์†Œ๊ฑฐ ๊ฐœ๋…์— ๋Œ€ํ•ด ์ถ”๊ฐ€์ ์œผ๋กœ ์•Œ๊ฒŒ ๋œ ๋‚ด์šฉ์ด ์žˆ์–ด์„œ ๋ธ”๋กœ๊ทธ์— ๋‚จ๊ฒจ๋‘๋ ค๊ณ  ํ•œ๋‹ค.

์ด ํด๋ž˜์Šค๋Š” encode/decode ๋ฉ”์„œ๋“œ๋กœ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋ฅผ ์ง„ํ–‰ํ•˜๋Š”๋ฐ, decode์˜ ๊ฒฝ์šฐ์—๋Š” ์–ด๋–ค ํด๋ž˜์Šค๋กœ ๋””์ฝ”๋”ฉ์„ ํ•˜๋А๋ƒ์— ๋Œ€ํ•œ ๊ณ ๋ฏผ ์ง€์ ์ด ์žˆ์—ˆ๋‹ค.

์ฒ˜์Œ ์„ค๊ณ„ํ•  ๋•Œ, decode ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋กœ๋”ฉํ•ด์„œ Class<T>๋‚˜ TypeReference<T>๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ๋งŒ๋“œ๋ ค๊ณ  ํ–ˆ๋Š”๋ฐ, ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒ๊ฐ์ด ๋– ์˜ฌ๋ž๋‹ค:

"์• ์ดˆ๋ถ€ํ„ฐ transcoder ์ธ์Šคํ„ด์Šค๋ฅผ ์ œ๋„ค๋ฆญ <T>๋กœ ๋งŒ๋“ค๋ฉด, ๊ทธ T๋กœ TypeReference<T> ์ „๋‹ฌ์ด ๊ฐ€๋Šฅํ•˜์ง€ ์•Š์„๊นŒ?"

 

์ฆ‰, new TypeReference<T>() {}๋ฅผ decode ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ ์ƒ์„ฑํ•˜์—ฌ ํƒ€์ž… ์ •๋ณด๋ฅผ ์ „๋‹ฌํ•˜๋ฉด ๊ตณ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์—ฌ๋Ÿฌ ๊ฐœ ๋งŒ๋“ค์ง€ ์•Š์•„๋„ ๋˜์ง€ ์•Š์„๊นŒ ํ•˜๋Š” ์ƒ๊ฐ์ด์—ˆ๋‹ค.

๊ฒฐ๋ก ๋ถ€ํ„ฐ ๋งํ•˜์ž๋ฉด, Java์˜ ํƒ€์ž… ์†Œ๊ฑฐ(Type Erasure) ํŠน์„ฑ์ƒ ๋Ÿฐํƒ€์ž„์— ์ œ๋„ค๋ฆญ ํƒ€์ž… ์ •๋ณด๊ฐ€ ์†์‹ค๋˜์–ด ์ œ๋Œ€๋กœ ๋™์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค.

 

์ด ๊ณผ์ •์—์„œ ์•Œ๊ฒŒ ๋œ ๋‚ด์šฉ์„ ์ •๋ฆฌํ•ด๋ณด์ž.

TypeReference์˜ ๋™์ž‘ ์›๋ฆฌ

Jackson์˜ TypeReference๋Š” Super Type Token ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ์ œ๋„ค๋ฆญ ํƒ€์ž… ์ •๋ณด๋ฅผ ๋Ÿฐํƒ€์ž„์— ๋ณด์กดํ•œ๋‹ค.

๋‚ด๋ถ€ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค:

package com.fasterxml.jackson.core.type;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

public abstract class TypeReference<T> implements Comparable<TypeReference<T>>
{
    protected final Type _type;

    protected TypeReference()
    {
        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof Class<?>) { // sanity check, should never happen
            throw new IllegalArgumentException("Internal error: TypeReference constructed without actual type information");
        }
        /* 22-Dec-2008, tatu: Not sure if this case is safe -- I suspect
         *   it is possible to make it fail?
         *   But let's deal with specific
         *   case when we know an actual use case, and thereby suitable
         *   workarounds for valid case(s) and/or error to throw
         *   on invalid one(s).
         */
        _type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }

    public Type getType() { return _type; }

    /**
     * The only reason we define this method (and require implementation
     * of <code>Comparable</code>) is to prevent constructing a
     * reference without type information.
     */
    @Override
    public int compareTo(TypeReference<T> o) { return 0; }
    // just need an implementation, not a good one... hence ^^^
}

์ด ํด๋ž˜์Šค์˜ ํ•ต์‹ฌ์€, ์ต๋ช… ๋‚ด๋ถ€ ํด๋ž˜์Šค๋ฅผ ์ƒ์„ฑํ•  ๋•Œ ์ œ๋„ค๋ฆญ ํƒ€์ž… ์ •๋ณด๊ฐ€ ์ปดํŒŒ์ผ ํƒ€์ž„์— ๋ณด์กด๋œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

 

Java์˜ ํƒ€์ž… ์†Œ๊ฑฐ ๋ฌธ์ œ

Java์˜ ์ œ๋„ค๋ฆญ์€ ์ปดํŒŒ์ผ ํƒ€์ž„์—๋งŒ ์กด์žฌํ•˜๊ณ  ๋Ÿฐํƒ€์ž„์—๋Š” ์†Œ๊ฑฐ๋œ๋‹ค.

๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์—, ๋ฉ”์„œ๋“œ ๋‚ด๋ถ€์—์„œ new TypeReference<T>() {}๋ฅผ ์ƒ์„ฑํ•˜๋ฉด, T๋Š” ์ด๋ฏธ Object๋กœ ํƒ€์ž… ์†Œ๊ฑฐ๋œ ์ƒํƒœ์ด๋ฏ€๋กœ ์˜๋ฏธ๊ฐ€ ์—†์–ด์ง„๋‹ค.

์ •ํ™•ํžˆ ์˜ˆ์‹œ๋ฅผ ๋“ค์–ด๋ณด์ž๋ฉด

TestTranscoder<List<Employee>> transcoder = new TestTranscoder<>();

์ด๋ ‡๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ–ˆ์„ ๋•Œ, ์ปดํŒŒ์ผ๋Ÿฌ๋Š” T๊ฐ€ List<Employee> ๋ผ๋Š” ๊ฒƒ์„ ์•Œ๊ณ  ์žˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด ํƒ€์ž… ์ •๋ณด๋Š” ๋Ÿฐํƒ€์ž„์— ๋ชจ๋‘ ์ง€์›Œ์ง€๋ฏ€๋กœ, ์ž๋ฐ”๋Š” TestTranscoder๊ฐ€ ์–ด๋–ค T๋กœ ์ธ์Šคํ„ด์Šคํ™”๋˜์—ˆ๋Š”์ง€ ๋Ÿฐํƒ€์ž„์—๋Š” ์•Œ ์ˆ˜๊ฐ€ ์—†๋‹ค.

์˜ฌ๋ฐ”๋ฅธ ์‚ฌ์šฉ๋ฒ•

TypeReference<Employee> typeRef = new TypeReference<Employee>() {}; // ์ต๋ช… ๋‚ด๋ถ€ ํด๋ž˜์Šค ์ƒ์„ฑ
Employee employee = objectMapper.readValue(json, typeRef);

์ž˜๋ชป๋œ ์‚ฌ์šฉ๋ฒ•

public <T> Object decode(byte[] data) {
    TypeReference<T> typeRef = new TypeReference<T>() {}; // T๋Š” ์ด๋ฏธ ์†Œ๊ฑฐ๋จ
    return objectMapper.readValue(data, typeRef);
}

์‹ค์ œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋กœ ๊ฒ€์ฆ

์œ„ ๋‚ด์šฉ์„ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด ๊ฒ€์ฆํ•ด๋ณด๊ณ ์ž Employee, TestTranscoder ํด๋ž˜์Šค๋ฅผ ์ž„์‹œ๋กœ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

public class TestTranscoder<T> {
    
    private final ObjectMapper objectMapper;
    
    public TestTranscoder(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }
    
    // ๋ฐฉ๋ฒ• 1: Class ๋ช…์‹œ์  ์ „๋‹ฌ (์ •์ƒ)
    public T decodeWithClass(byte[] data, Class<T> clazz) {
        try {
            return objectMapper.readValue(data, clazz);
        } catch (Exception e) {
            throw new RuntimeException("Decode with class failed", e);
        }
    }
    
    // ๋ฐฉ๋ฒ• 2: TypeReference ๋ช…์‹œ์  ์ „๋‹ฌ (์ •์ƒ)
    public T decodeWithTypeReference(byte[] data, TypeReference<T> typeRef) {
        try {
            return objectMapper.readValue(data, typeRef);
        } catch (Exception e) {
            throw new RuntimeException("Decode with TypeReference failed", e);
        }
    }
    
    // ๋ฐฉ๋ฒ• 3: ์ œ๋„ค๋ฆญ T๋กœ TypeReference ์ƒ์„ฑ (ํƒ€์ž… ์†Œ๊ฑฐ)
    public Object decodeWithGenericTypeReference(byte[] data) {
        try {
            // ์ด ๋ฐฉ์‹์€ ํƒ€์ž… ์ •๋ณด๊ฐ€ ์ด๋ฏธ ์†์‹ค๋œ ์ƒํƒœ
            TypeReference<T> typeRef = new TypeReference<T>() {};
            return objectMapper.readValue(data, typeRef);
        } catch (Exception e) {
            throw new RuntimeException("Decode with generic TypeReference failed", e);
        }
    }
    
    public byte[] encode(T object) {
        try {
            return objectMapper.writeValueAsBytes(object);
        } catch (Exception e) {
            throw new RuntimeException("Encode failed", e);
        }
    }
}

ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค

ํ…Œ์ŠคํŠธ๋Š” ์ „๋ถ€ ํ†ต๊ณผํ•œ๋‹ค.

class TestTranscoderTest {
    
    private ObjectMapper objectMapper;
    private TestTranscoder<Employee> employeeTranscoder;
    private TestTranscoder<List<Employee>> listTranscoder;
    
    @BeforeEach
    void setUp() {
        objectMapper = new ObjectMapper();
        employeeTranscoder = new TestTranscoder<>(objectMapper);
        listTranscoder = new TestTranscoder<>(objectMapper);
    }
    
    @Test
    @DisplayName("Class๋ฅผ ์‚ฌ์šฉํ•œ ์—ญ์ง๋ ฌํ™”๋Š” ์ •์ƒ ๋™์ž‘ํ•œ๋‹ค")
    void testDecodeWithClass_Success() {
        // Given
        Employee original = new Employee(1L, "๊น€์ง์›", null);
        byte[] serialized = employeeTranscoder.encode(original);
        
        // When
        Employee decoded = employeeTranscoder.decodeWithClass(serialized, Employee.class);
        
        // Then
        assertThat(decoded).isNotNull();
        assertThat(decoded).isInstanceOf(Employee.class);
        assertThat(decoded.getId()).isEqualTo(1L);
        assertThat(decoded.getName()).isEqualTo("๊น€์ง์›");
    }
    
    @Test
    @DisplayName("๋ช…์‹œ์  TypeReference๋ฅผ ์‚ฌ์šฉํ•œ ์—ญ์ง๋ ฌํ™”๋Š” ์ •์ƒ ๋™์ž‘ํ•œ๋‹ค")
    void testDecodeWithExplicitTypeReference_Success() {
        // Given
        Employee original = new Employee(1L, "๊น€์ง์›", null);
        byte[] serialized = employeeTranscoder.encode(original);
        TypeReference<Employee> typeRef = new TypeReference<Employee>() {};
        
        // When
        Employee decoded = employeeTranscoder.decodeWithTypeReference(serialized, typeRef);
        
        // Then
        assertThat(decoded).isNotNull();
        assertThat(decoded).isInstanceOf(Employee.class);
        assertThat(decoded.getId()).isEqualTo(1L);
        assertThat(decoded.getName()).isEqualTo("๊น€์ง์›");
    }
    
    @Test
    @DisplayName("์ œ๋„ค๋ฆญ T๋กœ ์ƒ์„ฑํ•œ TypeReference๋Š” ํƒ€์ž… ์ •๋ณด๊ฐ€ ์†์‹ค๋œ๋‹ค")
    void testDecodeWithGenericTypeReference_TypeErasure() {
        // Given
        Employee original = new Employee(1L, "๊น€์ง์›", null);
        byte[] serialized = employeeTranscoder.encode(original);
        
        // When
        Object decoded = employeeTranscoder.decodeWithGenericTypeReference(serialized);
        
        // Then
        assertThat(decoded).isNotNull();
        // ํƒ€์ž… ์†Œ๊ฑฐ๋กœ ์ธํ•ด Employee๊ฐ€ ์•„๋‹Œ Map์œผ๋กœ ์—ญ์ง๋ ฌํ™”๋จ
        assertThat(decoded).isInstanceOf(Map.class);
        assertThat(decoded).isNotInstanceOf(Employee.class);
        
        // Map์œผ๋กœ ์บ์ŠคํŒ…ํ•ด์„œ ๋ฐ์ดํ„ฐ ํ™•์ธ
        @SuppressWarnings("unchecked")
        Map<String, Object> map = (Map<String, Object>) decoded;
        assertThat(map.get("id")).isEqualTo(1);
        assertThat(map.get("name")).isEqualTo("๊น€์ง์›");
    }
    
    @Test
    @DisplayName("๋ฆฌ์ŠคํŠธ ํƒ€์ž…์—์„œ๋„ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค")
    void testListTypeErasure() {
        // Given
        List<Employee> originalList = List.of(
            new Employee(1L, "๊น€์ง์›", null),
            new Employee(2L, "๋ฐ•์ง์›", null)
        );
        byte[] serialized = listTranscoder.encode(originalList);
        
        // ๋ช…์‹œ์  TypeReference ์‚ฌ์šฉ (์ •์ƒ)
        TypeReference<List<Employee>> explicitTypeRef = new TypeReference<List<Employee>>() {};
        List<Employee> decodedWithExplicit = listTranscoder.decodeWithTypeReference(serialized, explicitTypeRef);
        
        // ์ œ๋„ค๋ฆญ TypeReference ์‚ฌ์šฉ (ํƒ€์ž… ์†Œ๊ฑฐ)
        Object decodedWithGeneric = listTranscoder.decodeWithGenericTypeReference(serialized);
        
        // Then
        // ๋ช…์‹œ์  TypeReference๋Š” ์ •์ƒ ๋™์ž‘
        assertThat(decodedWithExplicit).hasSize(2);
        assertThat(decodedWithExplicit.get(0)).isInstanceOf(Employee.class);
        assertThat(decodedWithExplicit.get(0).getName()).isEqualTo("๊น€์ง์›");
        
        // ์ œ๋„ค๋ฆญ TypeReference๋Š” ํƒ€์ž… ์ •๋ณด ์†์‹ค
        assertThat(decodedWithGeneric).isInstanceOf(List.class);
        @SuppressWarnings("unchecked")
        List<Object> genericList = (List<Object>) decodedWithGeneric;
        assertThat(genericList).hasSize(2);
        // ๋ฆฌ์ŠคํŠธ ์š”์†Œ๋“ค์ด Map์œผ๋กœ ์—ญ์ง๋ ฌํ™”๋จ
        assertThat(genericList.get(0)).isInstanceOf(Map.class);
        assertThat(genericList.get(0)).isNotInstanceOf(Employee.class);
    }
    
    @Test
    @DisplayName("ํƒ€์ž… ์†Œ๊ฑฐ๋œ ๊ฐ์ฒด๋ฅผ Employee๋กœ ์บ์ŠคํŒ…ํ•˜๋ฉด ClassCastException์ด ๋ฐœ์ƒํ•œ๋‹ค")
    void testClassCastException() {
        // Given
        Employee original = new Employee(1L, "๊น€์ง์›", null);
        byte[] serialized = employeeTranscoder.encode(original);
        
        // When
        Object decoded = employeeTranscoder.decodeWithGenericTypeReference(serialized);
        
        // Then
        assertThrows(ClassCastException.class, () -> {
            Employee employee = (Employee) decoded;
        });
    }
    
    @Test
    @DisplayName("์ค‘์ฒฉ๋œ ๊ฐ์ฒด์—์„œ๋„ ํƒ€์ž… ์ •๋ณด ์†์‹ค์ด ๋ฐœ์ƒํ•œ๋‹ค")
    void testNestedObjectTypeErasure() {
        // Given
        Employee manager = new Employee(1L, "๊น€๋งค๋‹ˆ์ €", null);
        Employee employee = new Employee(2L, "๋ฐ•์ง์›", manager);
        byte[] serialized = employeeTranscoder.encode(employee);
        
        // When - ์ œ๋„ค๋ฆญ TypeReference ์‚ฌ์šฉ
        Object decoded = employeeTranscoder.decodeWithGenericTypeReference(serialized);
        
        // Then
        assertThat(decoded).isInstanceOf(Map.class);
        @SuppressWarnings("unchecked")
        Map<String, Object> empMap = (Map<String, Object>) decoded;
        
        // ์ค‘์ฒฉ๋œ manager ๊ฐ์ฒด๋„ Map์œผ๋กœ ์—ญ์ง๋ ฌํ™”๋จ
        Object managerObj = empMap.get("manager");
        assertThat(managerObj).isInstanceOf(Map.class);
        assertThat(managerObj).isNotInstanceOf(Employee.class);
        
        @SuppressWarnings("unchecked")
        Map<String, Object> managerMap = (Map<String, Object>) managerObj;
        assertThat(managerMap.get("name")).isEqualTo("๊น€๋งค๋‹ˆ์ €");
    }
}

ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ

  1. ํƒ€์ž… ์†Œ๊ฑฐ ํ™•์ธ: decodeWithGenericTypeReference() ๋ฉ”์„œ๋“œ๋Š” Employee ๊ฐ์ฒด๊ฐ€ ์•„๋‹Œ LinkedHashMap์œผ๋กœ ์—ญ์ง๋ ฌํ™”๋œ๋‹ค.
  2. ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ํƒ€์ž… ์†Œ๊ฑฐ๋œ ๊ฐ์ฒด๋ฅผ Employee๋กœ ์บ์ŠคํŒ…ํ•˜๋ ค ํ•˜๋ฉด ClassCastException์ด ๋ฐœ์ƒํ•œ๋‹ค.
  3. ์ค‘์ฒฉ ํƒ€์ž… ๋ฌธ์ œ: ๋ฆฌ์ŠคํŠธ๋‚˜ ์ค‘์ฒฉ๋œ ๊ฐ์ฒด์—์„œ๋„ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์—ฌ, ๋ชจ๋“  ๊ฐ์ฒด๊ฐ€ Map์œผ๋กœ ๋ณ€ํ™˜๋œ๋‹ค.
  4. ๋ช…์‹œ์  ์ „๋‹ฌ์˜ ํ•„์š”์„ฑ: Class<T>๋‚˜ TypeReference<T>๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹๋งŒ์ด ์˜ฌ๋ฐ”๋ฅธ ํƒ€์ž… ์ •๋ณด๋ฅผ ๋ณด์กดํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ ˆํผ๋Ÿฐ์Šค

- https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

- https://stackoverflow.com/questions/6846244/jackson-and-generic-type-reference

 

 

'๐Ÿงฑ Back-end' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

[Redis] Redisson Pub/Sub ๊ธฐ๋ฐ˜ Lock  (3) 2025.03.17
[Redis] Amazon Linux 2023 EC2์— Redis ์„ธํŒ…ํ•˜๊ธฐ  (0) 2024.11.25