티스토리 뷰

반응형

백엔드 서버에서는 JSON을 통해 클라이언트와 통신한다. 그렇다면 자바는 어떻게 객체의 데이터를 JSON 데이터로 바꿀 수 있을까?

어늘은 그 방법인 직렬화에 대해 알아볼 것이다. 그리고 그걸 역직렬화를 통해 자바 객체로 받아올 수 도 있습니다.

직렬화 / 역직렬화

자바 직렬화란 자바 시스템 내부에서 사용되는 객체 또는 데이터를 외부의 자바 시스템에서도 사용할 수 있도록 바이트 형태로 데이터 변환하는 것을 말한다. 

시스템 적으로는 JVM의 메모리에 힙 또는 스택에 존재하는 객체 데이터를 바이트 형태로 변환하여 직렬화 한다.

자바에서는 Serializable 인터페이스를 통해 직렬화를 하고 있다.

 

반대로 직렬화는 역으로 바이트 형태의 데이터를 객체 또는 자바 시스템의 데이터로 바꿔주는 것을 말한다. 마찬가지로 JVM의 메모리에 의해 바이트 형태의 데이터를 객체 데이터로 변환할 수 있다.

public interface Serializable {
}

대표적으로 HashMap 클래스를 살펴보면 알 수 있다.

그렇다면 궁금한게 생길 것이다.

private static final long serialVersionUID = 362498820763181265L;

이 코드는 해당 객체의 버전을 명시하는데 사용한다. 값은 임의로 정해주며 정의된 값을 통해 Serial 버전을 인식한다. 이 변수가 필요한 이유는 제가 겪었던 경험을 통해 정의하면 좋을 것 같다.

최근에 serialVersionUID 값에 문제가 있어 직렬화가 정상적으로 이루어지지 않은 경험이 있었는데, 코드를 보자

@Slf4j
public class CustomUserDetails implements UserDetails {

    private final Long id;
    private final String email;
    private final String password;
    private final RoleType role;

    private CustomUserDetails(Long id, String email, String password, RoleType role) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.role = role;
    }

위 코드는 Spring Security에 UserDetails 객체를 커스텀한 것이다. 이 부분에서 직렬화 중 문제가 발생했었는데 원인은 다음과 같았다.

public interface UserDetails extends Serializable {

이미 UserDetails를 구현한 User라는 클래스가 존재했고 serialVersionUID가 충돌하였고 두 객체의 전달해야 하는 데이터가 달라 발생한 문제였습니다.

그렇기 때문에 이 UID라는 개념은 상당히 중요한데, 이 UID를 새로 정의해 줌으로 객체를 구분하여 원하는 데이터를 전송할 수 있다. 문제를 해결한 코드를 보자

@Slf4j
public class CustomUserDetails implements UserDetails {

    private final long serialVersionUID = 1L;

    private final Long id;
    private final String email;
    private final String password;
    private final RoleType role;

    private CustomUserDetails(Long id, String email, String password, RoleType role) {
        this.id = id;
        this.email = email;
        this.password = password;
        this.role = role;
    }

임의의 UID를 선언해주어 직렬화 시 구분을 해주었고 이를 통해 문제를 해결할 수 있었다. 그렇다면 이제 데이터를 전송할 때 어떠한 형태로 전달되는지 알아보자

package serialization;

import java.io.Serializable;

public class Member implements Serializable {

    private String email;
    private String name;
    private String nickname;

    public Member(String email, String name, String nickname) {
        this.email = email;
        this.name = name;
        this.nickname = nickname;
    }

    public String getEmail() {
        return email;
    }

    public String getName() {
        return name;
    }

    public String getNickname() {
        return nickname;
    }

    @Override
    public String toString() {
        return "Member{" +
                "email='" + email + '\'' +
                ", name='" + name + '\'' +
                ", nickname='" + nickname + '\'' +
                '}';
    }
}

다음은 직렬화 하여 회원 정보를 넘기는 Member 클래스다. 그렇다면 Member 객체를 byte stream으로 받아와 보자 java.io.ObjectOutputStream을 사용해서 받아왔다.

그리고 그 정보를 Base64로 인코딩 한 후 Decode하여 데이터를 다시 출력해보았다. 이 때 java.io.ObjectInputStream을 사용하였다.

package serialization;

import java.io.*;
import java.util.Base64;

public class Client {

    public static void main(String[] args) {
        Member member = new Member("ilgolc@naver.com", "김골프", "골프사랑");

        byte[] serializedMember;

        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
                oos.writeObject(member);
                // serializedMember -> 직렬화된 Member 객체
                serializedMember = baos.toByteArray();
                // 바이트 배열로 생성된 직렬화 데이터를 base64로 변환
            }
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        String encodeString = Base64.getEncoder().encodeToString(serializedMember);
        byte[] decodeMember = Base64.getDecoder().decode(encodeString);

        try (ByteArrayInputStream bais = new ByteArrayInputStream(decodeMember)) {
            try (ObjectInputStream ois = new ObjectInputStream(bais)) {
                Object objectMember = ois.readObject();
                Member newMember = (Member) objectMember;
                System.out.println(newMember);
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

다음과 같이 정상적으로 출력되는 것을 알 수 있다. 역직렬화에 대한 예제도 같이 있다는 것을 알 수 있을 것이다.

 

왜 직렬화가 필요할 까?

맨 처음에 말했다 싶이 현재 JSON데이터를 이용하여 백엔드 서버와 프론트 서버간에 통신을 하는 경우가 많다. 이 때 우리는 보통 DTO 클래스를 이용하여 외부 데이터를 받아오고 내보낸다. 이 때 사용되는 것이 직렬화다. 

Spring에 Dispatcher Servlet은 데이터를 받아와 Jackson라이브러리를 이용하여 JSON데이터로 직렬화 또는 역직렬화를 한다. 

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberUpdateDTO {

    @Email(message = "이메일 형식이 아닙니다.")
    @NotBlank(message = "필수 값입니다.")
    @Size(min = 10, max = 30)
    private String email;

    @NotBlank(message = "필수 값입니다.")
    @Size(min = 8, max = 30)
    private String password;

    @NotBlank(message = "필수 값입니다.")
    @Size(min = 4, max = 20)
    private String nickname;

    public Member toEntity() {
        return Member.createMember(email, password, nickname);
    }
}

다음은 실제 프로젝트에서 JSON 데이터를 받아와 업데이트에 대한 객체로 저장하기 위한 DTO 클래스이다. 실무에서도 이런식으로 REST API를 통해 데이터를 받아오고 내보낸다.

 

Reference.

https://techblog.woowahan.com/2550/

 

자바 직렬화, 그것이 알고싶다. 훑어보기편 | 우아한형제들 기술블로그

{{item.name}} 자바의 직렬화 기술에 대한 대한 이야기입니다. 간단한 질문과 답변 형태로 자바 직렬화에 대한 간단한 설명과 직접 프로젝트를 진행하면서 겪은 경험에 대해 이야기해보려 합니다.

techblog.woowahan.com

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함