JSON

직렬화는 자바 객체를 JSON 형식으로 변환하는 과정이다. 반대로 역직렬화는 JSON 데이터를 자바 객체로 변환하는 과정이다. 이 두 과정은 데이터 교환 시 매우 중요하다.

간단히 말해, JSON.stringify()가 직렬화이고 JSON.parse()가 역직렬화입니다.

더 쉽게 이해할 수 있도록 비유를 들어보겠습니다.

[비유] 레고 조립품을 다른 도시로 보내기

원본 데이터: 멋지게 조립된 레고 성(城)* (메모리 위의 자바스크립트 객체) 직렬화 (Serialization): 이 레고 성을 통째로 보낼 수는 없습니다. 그래서 부품별로 분해해서, 상자에 차곡차곡 담고, 조립 설명서를 넣는 과정입니다. 이제 상자는 운반하기 좋은 상태가 되었습니다. (객체를 전송하기 좋은 JSON 문자열로 바꾸는 과정) 역직렬화 (Deserialization): 다른 도시에 도착한 상자를 열고, 조립 설명서를 보면서 다시 원래의 레고 성으로 조립하는 과정입니다. (JSON 문자열을 받아서 다시 자바스크립트 객체로 만드는 과정)


  1. 직렬화 (Serialization)

  • 개념: 메모리 위에서 사용되던 객체(Object)를 디스크에 저장하거나 네트워크를 통해 다른 곳으로 전송할 수 있는 연속적인 데이터 흐름(Stream)이나 문자열 형태로 바꾸는 과정입니다.

  • 목적:

    • 데이터 전송: 다른 시스템(예: 서버-클라이언트)으로 객체를 보내기 위해

    • 데이터 저장: 객체를 파일이나 데이터베이스에 영구적으로 저장하기 위해

  • 자바스크립트에서의 예시: JSON.stringify()

    1 // 메모리 위에 존재하는 자바스크립트 객체
    2 const userObject = {
    3   id: 1,
    4   name: "Gildong Hong",
    5   email: "hong@example.com",
    6   isActive: true
    7 };
    8
    9 // [직렬화] 객체를 -> JSON 문자열로 변환
   10 const serializedData = JSON.stringify(userObject);
   11
   12 console.log(serializedData);
   13 // 출력 결과 (전송/저장하기 좋은 문자열 형태):
   14 // "{"id":1,"name":"Gildong Hong","email":"hong@example.com","isActive":true}"
  1. 역직렬화 (Deserialization)

  • 개념: 직렬화된 데이터(문자열이나 스트림)를 읽어서, 원래의 객체(Object) 구조로 메모리 위에 다시 복원하는 과정입니다.

  • 목적:

    • 전송받은 데이터를 실제 프로그램에서 사용할 수 있는 객체로 만들기 위해

    • 파일이나 데이터베이스에서 읽은 데이터를 객체로 변환하여 사용하기 위해

  • 자바스크립트에서의 예시: JSON.parse()

   1 // 서버로부터 전송받은 JSON 문자열 데이터
   2 const serializedData = '{"id":1,"name":"Gildong Hong","email":"hong@example.com","isActive":true}';
   3
   4 // [역직렬화] JSON 문자열을 -> 자바스크립트 객체로 복원
   5 const userObject = JSON.parse(serializedData);
   6
   7 // 이제 자바스크립트에서 사용할 수 있는 객체가 되었다.
   8 console.log(userObject.name); // "Gildong Hong"
   9 console.log(userObject.email); // "hong@example.com"

@Getter
@NoArgsConstructor
public class OrderCreateRequest {

    private List<String> productNumbers;

    @Builder
    private OrderCreateRequest(List<String> productNumbers) {
        this.productNumbers = productNumbers;
    }
}


원래 기본 생성자 없으면 JSON 매핑에서 오류 나야 하는데 왜 @NoArgsConstructor 제거해도
동작하지? 스프링 버전 차이? 

OjbectMapper는 Jackson 라이브러리에서 제공하는 클래스

주요 기능 :

  • Java 객체 <-> JSON 형식으로 직렬화/역직렬화

기능
설명
예시

직렬화

Java 객체 → JSON 문자열

writeValueAsString(obj)

역직렬화

JSON 문자열 → Java 객체

readValue(json, Myclass.class)

ObjectMapper objectMapper = new ObjectMapper();

// Java 객체 -> JSON 문자열
User user = new User("Alice", 20);
String json = objectMapper.writeValueAsString(user);

// JSON 문자열 -> Java 객체
String inputJson = "{\"name\":\"Bob\",\"age\":30}";
User user = objectMapper.readValue(inputJson, User.class);

역직렬화 할때 ObjectMapper가 기본 생성자 사용 request 클래스에 NoArgsConstructor 필요

->

User user = objectMapper.readValue(inputJson, User.class);

이 코드는 Jackson의 ObjectMapper가 JSON 문자열을 User 클래스 객체로 역직렬화(deserialization) 하는 대표적인 예입니다. 내부 구현 및 동작 원리를 아래와 같이 설명할게요:


✅ 1. 내부 동작 요약 (readValue)

// 내부적으로는 이런 구조를 따릅니다:
T readValue(String content, Class<T> valueType) {
    JsonParser parser = _jsonFactory.createParser(content);
    ObjectReader reader = _config.constructReader(valueType);
    return reader.readValue(parser);
}

Jackson의 처리 흐름:

  1. JSON 파싱: JsonParser가 문자열 inputJson을 JSON 토큰으로 파싱

  2. 바인딩: ObjectReader가 토큰들을 보고 User 클래스의 필드에 값을 채움

  3. 생성자 호출 또는 필드 직접 접근:

    • 기본 생성자가 있으면 → 생성자 호출 후 setter 또는 리플렉션으로 필드 세팅

    • @JsonCreator, @JsonProperty가 붙은 생성자 있으면 → 해당 생성자 사용


✅ 2. 필수 조건: 역직렬화가 잘 되려면

Jackson이 User.class를 인식하기 위해 필요한 조건:

조건
설명

기본 생성자

있어야 Jackson이 인스턴스를 만들 수 있음 (없으면 @JsonCreator 필요)

필드 or setter

Jackson은 setter 또는 필드에 직접 접근해서 값 설정

@JsonProperty (optional)

생성자 또는 필드명이 JSON 키와 다르면 명시 필요

예시:

public class User {
    private String name;

    public User() {} // 기본 생성자

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

✅ 3. @RequestBody 와 관계

@PostMapping("/api/user")
public ResponseEntity<Void> createUser(@RequestBody User user)

여기서 @RequestBodySpring MVC가 HTTP 요청 본문을 ObjectMapper로 자동 역직렬화 하게 합니다.

즉, 위의 objectMapper.readValue(...)는 내부적으로 Spring도 HttpMessageConverter를 통해 동일하게 동작합니다.


✅ 요약 정리

항목
내용

ObjectMapper.readValue(...)

JSON → Java 객체로 역직렬화

구현 방식

JsonParser로 파싱 → ObjectReader가 필드 매핑

필요 조건

기본 생성자, 필드/setter, 필요시 @JsonProperty

Spring MVC 연동

@RequestBody가 내부적으로 Jackson 사용함 (readValue와 동일한 흐름)

@RequestMapping 관계

경로 매핑용이며 역직렬화와 직접적 관계는 없음

실제 테스트

정확히 이해하셨습니다. 아래처럼 정리해드릴게요:


@RequestBody의 역할

@PostMapping("/api/v1/products/new")
public ApiResponse<ProductResponse> createProduct(@Valid @RequestBody ProductCreateRequest request)

@RequestBody가 하는 일:

  • HTTP 요청의 JSON bodyProductCreateRequest 객체로 역직렬화(deserialize) 합니다.

  • 이걸 생략하면 Spring MVC는 request 파라미터를 폼 필드나 쿼리스트링 등에서 찾으려 함 → JSON body는 무시됨 → 결국 request 객체는 null.

👉 따라서 JSON 요청을 처리하려면 @RequestBody가 반드시 필요합니다.


✅ 응답에서 getter가 없으면 직렬화 실패

return ApiResponse.of(HttpStatus.OK, productService.createProduct(request));

여기서 반환된 ProductResponse 객체를 Spring이 JSON으로 직렬화해야 하는데, 만약 ProductResponsegetter가 없으면:

  • Jackson이 필드 값을 꺼내지 못함

  • 그래서 JSON 응답 생성에 실패

  • HttpMediaTypeNotAcceptableException (406) 발생

-> ApiResponse , ProductResponse 둘다 Getter 필요!


🔄 결론적으로:

항목
생략 시 결과
원인

@RequestBody

request = null

요청 body에서 값을 못 읽음

getter

406 Not Acceptable

응답 직렬화 실패 (JSON으로 못 만듦)


💡 요약

  • @RequestBody는 JSON → Java로 역직렬화할 때 필요

  • getter는 Java → JSON 직렬화할 때 필요

  • 테스트에서 @RequestBody를 안 달면 request 자체가 null이라, 응답 객체까지 안 가므로 직렬화 에러는 안 터짐

완벽한 흐름 이해입니다.

JSON 라이브 코딩 해봐

먼저 직렬화할 대상 Person 클래스가 있다고 가정

class Person {
    private String name;
    private int age;
    private String city;
    private boolean isStudent;
    
    public Person(String name, int age, String city, boolean isStudent) {
        this.name = name;
        this.age = age;
        this.city = city;
        this.isStudent = isStudent;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    public String getCity() { return city; }
    public boolean isStudent() { return isStudent; }
}

가장 간단한 방법은 수동으로 JSON 형식에 맞게 문자열을 조립하는 것입니다.

public String serializePersonToJson(Person person) {
    if (person == null) {
        return "null";
    }
    
    StringBuilder jsonBuilder = new StringBuilder();
    
    jsonBuilder.append("{");
    
    // 1. name 필드 (String)
    jsonBuilder.append("\"name\":\"").append(person.getName()).append("\",");
    
    // 2. age 필드 (int)
    jsonBuilder.append("\"age\":").append(person.getAge()).append(",");
    
    // 3. city 필드 (String)
    jsonBuilder.append("\"city\":\"").append(person.getCity()).append("\",");
   
    // 4. isStudent 필드 (boolean)
    jsonBuilder.append("\"isStudent\":").append(person.isStudent());
   
    jsonBuilder.append("}");
   
    return jsonBuilder.toString();
}

"면접관님, 방금 작성한 코드는 Person 객체에 대해서는 잘 동작하지만, 몇 가지 개선할 점이 보입니다."

"첫째, 현재 코드는 name이나 city 필드가 null일 경우, JSON 문자열에 "null"이 아닌 null이 그대로 들어가서 유효하지 않은 JSON이 만들어질 수 있습니다. 이 부분을 보완해야 합니다."

"둘째, Person 클래스에 필드가 추가되거나 순서가 바뀌면 serializePersonToJson 메서드의 코드도 일일이 수정해야 하는 경직된 구조입니다."

"셋째, 이 메서드는 오직 Person 타입만 처리할 수 있어 재사용성이 떨어집니다."

"이러한 문제들을 해결하기 위해, Java의 Reflection API를 사용하여 어떤 객체든 동적으로 필드를 분석하고 JSON으로 변환하는 범용 직렬화 메서드를 구현해 볼 수 있습니다. 괜찮으시다면 그 방향으로 코드를 개선해 보겠습니다."

(4. (면접관이 긍정적일 경우) Reflection을 이용한 개선된 코드 작성)

(속마음: 이제 진짜 실력을 보여줄 시간이다. Reflection을 사용해서 필드를 순회하고, 각 필드의 타입에 따라 다르게 처리하는 로직을 보여주자. String은 따옴표로 감싸고, 숫자나 boolean은 그대로 출력하는 로직이 핵심이다.)

"네, 그럼 Reflection을 사용해서 좀 더 유연하고 재사용 가능한 코드를 작성해 보겠습니다."

"이렇게 작성하면, 이제 Person 객체뿐만 아니라 다른 어떤 객체가 들어와도 그 객체의 필드를 동적으로 분석하여 JSON으로 변환할 수 있습니다. 필드가 추가되거나 삭제되어도 코드를 수정할 필요가 없습니다."

"한번 동일한 Person 객체로 테스트해 보겠습니다."

1 // --- 테스트 --- 2 Person person = new Person("홍길동", 30, "서울", true); 3 String jsonString = serializeObjectToJson(person); 4 System.out.println(jsonString); 5 // 예상 출력: {"name":"홍길동","age":30,"city":"서울","isStudent":true}

"물론 이 코드도 완벽하지는 않습니다. 중첩 객체(객체 안에 또 다른 객체가 있는 경우)나 배열/리스트 타입의 필드를 처리하려면 재귀적인 호출 로직이 추가되어야 하고, 성능 면에서는 Reflection이 직접 호출보다 느리다는 단점이 있습니다. 하지만 객체의 구조를 동적으로 처리할 수 있다는 점에서 매우 유연한 방법이라고 생각합니다."

Last updated