클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요? #42

Merge SimpsonMerge Simpson
5 min read

이 아티클은 깃허브 nettee-space 조직의 디스커션 #42 항목을 옮겨 온 것입니다.
관련 논의: nettee-space/backend-sample-hexagonal-simple-crud/discussions/42


Question: 클래스에 Serializable 인터페이스를 구현 받는 이유가 무엇인가요?

여러 소스들을 접하면서 VO 객체 등에 Serializable를 구현받는 것을 많이 접했습니다.

저희 헥사고날(스터디 팀내 2단계 프로젝트)에서도 하기와 같이 적용을 하였는데, 어떤 의도로 사용하게 되는 걸까요?

@Getter
@MappedSuperclass
public abstract class LongBaseEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}

리서치를 했을 때는 객체를 자바 환경에서 직렬화, 역직렬화하여 활용한다는데 와닿지가 않네요.


The Answer Starts Here.

안녕하세요, silberbullet 님! 언제나 좋은 질문 감사드립니다! 👍
많은 분들이 의례적인 작업으로 생각하시고 넘어가는 파트인 것 같습니다.
말씀해 주신 내용과 관련해서 다형성마커 인터페이스를 짧게(?) 다루어 보겠습니다.
각 섹션은 짧습니다.


⚡️ 바쁜 직장인을 위한 요약

  • Serializable은 그 자체로 특별한 기능을 하지 않습니다. 마치 애노테이션처럼 "이 클래스는 직렬화해도 됨."을 표시하는 역할입니다. (마커 인터페이스)

  • 현대적으로는 다형성의 특징을 잘 살릴 수 있습니다.

  • 고전적으로는 다형성의 특징을 파라미터 수준에서 완성하지 않았습니다.

    • 일부러 느슨한 제약을 가진 공통 상위타입을 설계하여, 설계의 원형을 유지합니다.
      (공통 상위 타입으로 사용되는 ObjectOutput 인터페이스의 writeObject(Object object) 메서드는, 파라미터로 Serializable을 받지 않고 Object를 받는 느슨한 제약을 가진 추상메서드를 제공하죠.)

    • 대신 런타임에 Serializable 인스턴스인지 체크합니다. (instanceof)

❓ 그렇다면 Serializable이 왜 필요할까요?

  • 작업자의 의도에 맞는 객체만 취사선택하여 직렬화 대상으로 체크할 수 있습니다. 중요한 정보가 무방비하게 직렬화되지 않도록 보호됩니다.

  • 내부적으로 직렬화가 불가능하거나 부적절한 케이스를 미리 체크해 둘 수 있습니다. (빠른 예외 반환)


✔️ 마커 인터페이스: 함수를 제공하지 않음

  • Serializable 인터페이스는 아무런 함수도 제공하지 않습니다.

  • 따라서 현대적인 개발 문화로는 다형성을 제공하는 정도 역할로 생각하고 있습니다.

  • 또한 일종의 ***'마커 인터페이스'***이므로, instanceof 연산자로 체크하여 Serializable 인스턴스인지 확인할 수 있습니다. (애노테이션과 유사한 역할)

🟢🔺🟪 다형성으로서

자주 이야기되는 다형성은 런타임 다형성으로, 상위 타입 객체가 하위 타입 인스턴스를 포함할 수 있는 개념입니다. 흔히 '부모는 자식을 품을 수 있다' 등으로 연상시켜 기억을 돕습니다.

다형성 예시

만약 직렬화 함수들이 파라미터로 Serializable을 받기로 한다면, 이곳에 전달하려는 객체는 Serializable을 구현해야 합니다.

다음과 같은 함수가 있다고 하겠습니다.

public void serialize(Serializable source) {/* ... */}

위 함수를 사용하는 조건은, source로 제공된 파라미터가 Serializable의 자손(다소 틀린 표현이지만 편의상)이어야 한다는 것입니다.

// JDK 16+ (14 Preview)
if (item instanceof Serializable s) {
    serialize(s); // ✅ Ensurance
} else {
    serialize(item); // ❌ Error !!
}

이곳에 넣을 객체들은 Serializable 인터페이스를 구현한 클래스의 인스턴스여야 합니다.

또한 이는 컴파일타임에 체크할 수 있는 예시입니다.

✅ instanceof 🟩: instanceof 연산자

instanceof 연산자로 런타임에 체크하는 예시

파라미터 타입에서 Serializable로 제한하지 않고 설계하는 예시로, 이번 예시에서는 Object로 모든 객체를 받는다고 하겠습니다.

이 함수는 구현하는 사람의 의도에 따라, 런타임에 instanceofSerializable로 마크된 객체인지 확인할 수 있습니다.

public void serialize(Object source) {
    if (!(source instanceof Serializable)) {
        throw ...;
    }

    // do serialization here
    //  ...
}

이 함수를 호출할 때는 반드시 Serializable 인스턴스가 아니어도 호출 자체는 됩니다. 하지만 Serializable 인스턴스가 아니라면 런타임에 오류를 띄웁니다.

// JDK 16+ (14 Preview)
if (item instanceof Serializable s) {
    serialize(s); // ✅ Ensurance
} else {
    serialize(item); // ❎ 호출 가능 -> 실행 시 Runtime Error !!
}

⚠️ 미리 진단되지 않은 코드

이 방식은 컴파일타임에 체크할 수 있었던 문제를 런타임으로 유보시켜
프로세스 운영에 잠재적 문제를 야기하는 코드로서 현대적으로 선호되는 방식은 아닙니다.

❓ 그럼에도 자바를 설계한 사람들이 이러한 방식을 초기에 선택했던 이유가 무엇일까요?

💡 느슨한 제약의 공통 설계부

---
config:
  look: handDrawn
  theme: forest
---
classDiagram
%% 마커 인터페이스
class Serializable {
  <<marker>>
}

%% 직렬화를 위한 인터페이스
class ObjectOutput {
  <<interface>>
  +writeObject(obj: Object)
}

%% 역직렬화를 위한 인터페이스
class ObjectInput {
  <<interface>>
  +readObject() Object
}

%% 직렬화 기능 구현 클래스
class ObjectOutputStream {
  +writeObject(obj: Object)
  +[기타 메서드...]
}

%% 역직렬화 기능 구현 클래스
class ObjectInputStream {
  +readObject() Object
  +[기타 메서드...]
}

%% 관계 표현
ObjectOutputStream ..|> ObjectOutput
ObjectInputStream ..|> ObjectInput
%% ObjectOutputStream --> Serializable : <<<span>uses</span>>>
%% ObjectInputStream --> Serializable : <<<span>uses</span>>>

'추상적인' 설계 파트에서는 느슨한 제약을 선호할 수 있습니다. 위 방식은 다음과 같은 항목을 포함합니다.

  • 메서드는 Object 타입 파라미터를 받습니다.

  • 함수 내부에서 런타임에 instanceofSerializable 인스턴스인지 확인합니다.

  • (아마도) 오버라이딩이 가능한 상태거나, 수퍼 메서드가 있을 것입니다. 즉, 가상메서드(≠ 추상메서드)가 존재할 것입니다.

    • 위 다이어그램에서 ObjectOutput 인터페이스의 writeObject(Object object) 메서드가 해당합니다.

이 방식은 직렬화 함수나 그 원형이 오버라이딩이 가능한 함수라는 전제에서 다음과 같은 특징을 갖습니다.

  • 오버라이딩한 함수에서는 Serializable이 아닌 Object 인스턴스까지 실제 직렬화 대상으로 통과시키는 의도를 추가할 수 있습니다. (instanceof 제거)

  • 그 선택을 공통 상위타입을 설계하는 사람이 Serializable로 제한하지 않고, 그 설계를 이어 받은(오버라이딩하는) 사람이 선택할 수 있도록 합니다.

  • 공통 상위타입은 느슨한 제약으로 설계의 원형을 오랫동안 유지할 수 있게 됩니다. (문제가 발생할 때 상위 타입 대신 구현부를 수정할 수 있습니다.)

자바를 만든 사람들은 본인들이 자바에서 처음 설계한 것들이 나중에 과도하게 바뀌는 것을 최대한 피하려고 한 것으로 유명합니다. (1.8까지 하위호환 대체로 보장)

따라서, 처음부터 '느슨한 제약'으로 설계하여 설계의 원형을 오랫동안 유지하려고 한 것 같습니다.

만약 처음부터 Serializable 객체만 입력받도록 만들었다면, 추후 Serializable이 아닌 객체를 직렬화하는 직렬화 함수로 확장할 수 없습니다. 또한 Serializable로 마크되지 않은 객체는 직렬화할 수 없으므로, 다른 라이브러리에서 가져 온 어느 클래스의 객체를 직접 직렬화할 수 없을 수 있습니다. (그 클래스가 Serializable을 누락했다면.) 이러한 이유로 상위에서 느슨한 제약을 선택한 것으로 추측합니다.

❓ 그렇다면 Serializable이 왜 필요할까요?

앞서 설명한 대로라면, Object 타입을 받아서 모두 직렬화에 통과시키면 되는 문제로 생각이 듭니다. 그런데 왜 굳이 Serializable을 추가하고, 이 타입을 체크해 두지 않으면 필터하는 걸까요?

이는 일종의 '안전 장치'로 마크하는 것과 같은 의도입니다.
Serializable을 붙이지 않은 클래스의 객체는 직렬화 대상에서 제외하는 거죠.
직렬화할 객체의 클래스에는 Serializable을 붙여서 직렬화 대상이 된다는 걸 표시하는 거구요.

이로써 다음과 같은 효과를 얻을 수 있습니다.

🧑🏻‍💻 작업자의 의도를 담을 수 있습니다. 작업자는 이 클래스의 인스턴스가 직렬화 대상인지 아닌지 명시할 수 있습니다. 이로써 보안상 함부로 직렬화해선 안 되는 정보를 무방비한 직렬화로부터 보호할 수 있고, 내부적으로 직렬화가 부적절한 유동 환경 정보 등을 실수로 직렬화하는 상황을 피할 수 있습니다.

  • 🎁 민감한 정보 보호: 의도하지 않은 데이터가 무방비하게 직렬화되는 것을 방지할 수 있습니다.

  • 👷 안전한 직렬화 명시: 내부적으로 직렬화가 불가능하거나 부적절한 유동적 환경 정보가 포함되는 객체가 있을 수 있습니다. 이를 무작정 직렬화하면, 추후 역직렬화 시 적절한 재구조화가 안 될 수 있습니다. 예를 들어 운영체제에 종속되는 정보를 직렬화 대상으로 포함하면 안 되겠죠!


silberbullet 님께서 모두에게,
세세한 부분에서 좋은 호기심으로 접근할 수 있도록
유익한 질문을 제시해 주셨습니다! 🚀

항상 좋은 대화의 장을 만들어 주셔서 감사합니다! 👍


의견 보충 및 추가적인 질문은 언제든 멘션(Merge Simpson) 부탁드리곘습니다!

10
Subscribe to my newsletter

Read articles from Merge Simpson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Merge Simpson
Merge Simpson

Hello, I am Korean. Welcome, visitor. You are very cool. 안녕하세요, 저는 한국어입니다. 방문자여 환영한다. 당신은 매우 시원해.