서평 - 이펙티브 자바 3판

출처: yes24

 

이펙티브 자바란?

자바 개발자의 필독서라고 불리는 책.

자바의 기본 문법에 대해 다루기보다는 어떤 상황에서 어떤 방법으로 문제를 해결해나가면 좋을지에 대한 가이드를 해 주는 책이다.

그렇기 때문에 언어의 문법에 대해 다루는 책보다는 난이도가 있는 편으로, 나 역시 첫 번째 시도 때에는 초반부를 읽다가 어려워서 책을 덮고 현업에서 일한지 만 1년이 다 되어가는 이 시점에서야 1회독을 한 책이다.

또한, 책 구성도 그러하고 저자 역시 권장하듯 순서대로 읽지 않고 원하는 아이템을 먼저 읽어도 되며, 다회독을 하는 것도 좋아 보인다. 나 역시 1회독에는 순서대로 읽으며 카테고리별 팁을 얻는 데에 집중했고, 2회독부터는 미리 정리해둔 요약집을 기반으로 그때그때 필요한/흥미가 가는 내용들을 골라 읽을 예정이다.

이 서평에서는 2회독에 앞서 1회독으로 이 책을 순차적으로 읽은 주니어 개발자의 시점에서 도움이 됐던 내용들을 대분류별로 정리해보고자 한다.

1장은 책의 서언(introduction)에 해당하는 부분이므로 생략한다.

 

 

2장. 객체의 생성과 파괴

2장에서는 객체를 만들고 사용하고 메모리에서 방출하는 과정에서 유의할 점을 다루고 있다.

재사용이 가능한 객체는 재사용

자바는 사용하지 않는 객체를 회수해주는 GC(Garbage Collector)가 있기 때문에, 개발자로 하여금 로우레벨 언어들보다는 객체 생명 주기 관리 책임에서 벗어날 수 있도록 한다.

하지만 그렇다고 해서 객체의 생성을 신경쓰지 않으면 아래의 문제가 발생할 수도 있다.

  1. 너무 많은 객체가 생성되어 Heap 공간이 부족해질 수 있음
  2. GC가 동작하는 동안에는 STW(Stop The World)에 의해 애플리케이션이 잠시 중지되는데, 과도한 GC는 서비스를 마비시키기도 함

이러한 이유로 객체를 재사용하여 불필요한 메모리 낭비와 GC의 동작을 막는 방법을 알고 있는 것이 좋으며, 2장에서도 정적 팩토리, 싱글턴 등의 기법을 이용해 객체를 재사용하는 방법들을 소개하고 있다.

재사용 가능성은 신중하게 검토

그러나 무턱대고 객체를 재사용하는 것은 위험하다. 재사용이 불가능한 객체를 재사용했을 때 일어나는 문제점이 더 클 수 있기 때문이다.

따라서, 비용이 작은 객체는 재사용 가능 여부를 확신할 수 없다면 재사용하지 않는 편이 좋고, 비용이 큰 객체들에 한해 재사용 가능성을 엄격하게 검토한 후 재사용하는 것이 좋다.

메모리 누수에 주의

실제로는 사용되지 않음에도 GC가 회수 대상으로 판별하지 않아 메모리에 남아있는 객체가 있으면, 이를 메모리 누수가 일어났다고 한다.

이는 쓰레기(Garbage) 객체들이 메모리를 낭비하게 만들고, 심화되면 OOM(Out Of Memory)을 발생시키는 주범이 될 수 있기 때문에 반드시 회피해야 할 이슈이다.

2장에서는 이를 회피하기 위해 명시적 파괴자를 사용하지 않고, try-catch 대신 try-with-resources 문을 사용할 것을 권장한다.

 

 

3장. 모든 객체의 공통 메서드

모든 객체는 최상위 객체인 Object를 암묵적으로 상속한다. 3장에서는 Object 클래스에 정의된 메서드들을 오버라이딩할 때 지켜야 할 규약들을 다루고 있다.

클래스를 정의할 때에는 그 목적에 따라 Object 클래스에 정의된 아래 메서드들을 적절하게 오버라이딩해야 할 수도 있음을 항상 숙지하고 있어야 한다.

  • equals, hashCode
  • toString
  • clone

 

 

4장. 클래스와 인터페이스

4장에서는 클래스와 인터페이스를 설계할 때 유의할 요소들을 다룬다.

클래스와 멤버의 접근 권한

접근 권한은 공개 여부에 따라 크게 public, protected와 package, private을 묶을 수 있다. 앞의 2가지는 외부에서 제어 또는 상속에 의한 재정의가 가능하다는 점에서 신중하게 사용해야 하는 접근 권한이며, 뒤의 2가지는 클래스 또는 모듈 내에서만 사용된다는 점에서 비교적 변경에 자유로운 접근 권한이다.

클래스가 변경에 유연하게 대응하기 위해서는 외부에는 특정 기능을 수행하는 메서드만을 공개하고 내부 구현은 private 제어자를 통해 감추는 것이 좋다. 이를 정보 은닉이라고 한다.

private 필드를 선언하고 public getter 메서드를 제공하는 것 또한 정보 은닉에 해당한다. 그냥 private 필드를 제공하면 되는 것 아닌가? 라고 생각할 수도 있지만 필드를 외부에 노출하는 순간 이 필드는 절대 수정할 수 없는 요소가 된다. 외부에서 어떻게 쓰고 있을지 모르니 Side Effect가 두려워 타입도 이름도 초기값도 그 무엇도 바꿀 수 없다. 하지만 getter를 제공하면 private 필드가 수정되어도 getter 메서드에 수정에 대응하는 코드가 들어가 기존 코드들이 문제 없이 동작하도록 할 수 있다.

상속은 신중하게

상속은 단일 상속만 가능하며, 통상적으로 is-a 관계가 아니면 상속을 사용하지 말아야 한다고 말한다.

그만큼 하위 클래스에서는 신중하게 상위 클래스를 선정해야 하며, 상위 클래스에서는 변경이 필요할 때 자신의 변경이 하위 클래스에 미칠 영향을 고려해가며 신중하게 수정해야 한다.

이러한 점 때문에 상속보다는 컴포지션(상위 클래스로 두는 대신 내부 필드에서 클래스의 객체를 참조) 방식을 이용할 것을 권장하며, 반드시 상속을 이용하려는 경우에는 재정의가 가능한 모든 메서드를 문서화해 재정의 과정에서 문제가 생기는 것을 예방해야 한다.

상속을 목적으로 만들어지지 않은 모든 클래스는 final로 선언해 상속을 막는 것이 좋다. 내가 아닌 다른 사람이 이 클래스를 상속하여 사용할 수도 있기 때문이다.

 

 

5장. 제네릭

제네릭은 런타임에 잠재적으로 발생 가능한 타입 미스매치 오류를 컴파일 타임으로 끌어와 미리 예방하기 위해 사용하는 문법이다. 그 외에도 제네릭은 클래스/메서드의 선언부에 쓰여 클래스의 생성 시점, 메서드의 호출 시점에 타입을 결정할 수 있도록 하는 도구이기도 하다.

5장에서는 이러한 제네릭을 사용할 때 주의할 점을 다룬다.

런타임에는 타입 정보가 손실

제네릭의 주요 특징 중 한 가지이자 해당 글에서 소개하는 타입 안전 이종 컨테이너의 단점이다. 자바 컴파일러는 하위 호환성을 위해(제네릭이 없던 시절의 코드와의 호환을 위해) 컴파일 타임에 타입 정보를 소거한다. 이로 인해 런타임에는 타입 정보를 획득할 수 없어 타입 안정성을 확보할 수 없다.

책에서는 이 문제점을 소개하는 데에 그치고 있지만 사실 실무에서도 이 문제 때문에 원하는 방향으로 구현하는 것이 막힌 일이 있었다. 정확히는 런타임에 타입 파라미터의 정보를 획득할 수 없는 데에서 오는 문제였다.

그 때 해결책으로 선택한 개념이자 이 문제를 우회하는 대표적인 방법으로는 슈퍼 타입 토큰(Super Type Token)이 있다. 이 글은 이펙티브 자바의 서평이기 때문에 자세한 내용은 생략하고, 런타임에 제네릭 클래스의 타입 정보를 획득할 수 있는 방법이라는 것만 소개한다.

 

 

6장. 열거 타입과 어노테이션

열거 타입(Enum)과 어노테이션은 자바가 지원하는 특수한 Reference Type으로, 코드의 가독성, 효율성 등을 높이기 위한 효과적인 도구이다. 6장에서는 이러한 특수 타입들을 잘 활용하는 방법을 소개한다.

열거 타입(Enum)

상수 묶음을 나타내기 위한 특수 클래스로, 각 필드에 단 하나의(싱글턴) 불변 인스턴스만이 할당된다.

Enum을 사용할 때 주의할 것은, 상수 간 순서가 바뀌었을 때 동작이 바뀌지 않도록 해야 한다는 것이다. 이 때문에 공식 문서에서도 열거 타입 기반 자료구조를 설계할 것이 아니고서야 ordinal 메서드 사용은 지양하라고 소개하고 있다.

어노테이션(Annotation)

특정 코드에 대해 설명하거나 동작을 지시하기 위해 사용하는 도구이다. 그 중에서도 어노테이션은 대상을 마킹하는 역할을 하고, 별도 처리기가 어노테이션이 마킹한 곳에서 특정 로직을 수행하도록 하는 것을 마커 어노테이션이라고 부르는데, 최근에는 명명 패턴과 마커 인터페이스를 마커 어노테이션으로 대체하는 추세이다. (ex: JUnit3까지 테스트 여부를 판단하기 위해 사용되던 명명 패턴을 JUnit4부터 마커 어노테이션 기반으로 대체)

 

 

7장. 람다와 스트림

7장에서는 Java 8에서 도입된 람다 표현식과 스트림 API의 특징과 이들을 적재적소에 활용하는 방법을 다룬다.

람다

Java 8에서 람다가 도입되기 전까지 자바에서는 특정 행위를 수행하는 함수를 인자로 전달하기 위해 함수형 인터페이스를 구현하는 익명 클래스를 사용해왔다. 람다가 도입된 이후로는 대부분의 상황에서 함수형 인터페이스를 구현하는 익명 클래스를 람다로 대체할 수 있다. 람다가 필요한 대부분의 경우 표준 함수형 인터페이스를 숙지하고 있는 것만으로 충분하므로, 자주 사용되는 표준 함수형 인터페이스는 숙지하고 있는 편이 좋다.

람다를 사용할 때 주의할 점은, 람다는 함수로 취급되기 때문에 람다 내에서 this를 사용할 경우 this가 람다 밖의 클래스를 가리킨다는 점이다. 또한, 람다 함수는 문서화가 불가능하기 때문에 내부 구현이 너무 길어지면 오히려 가독성을 저하시킬 수도 있음에 주의해야 한다.

스트림

Java 8에서 도입된 스트림 API는 스트림과 파이프라인 2가지 요소로 구성되는데, 스트림이 데이터의 연속적 흐름(시퀀스)을 의미한다면, 파이프라인은 스트림을 구성하는 원소들로 수행하는 단계적 연산을 표현하기 위한 개념식을 의미한다.

스트림은 스트림을 구성하는 원소들의 시퀀스에 일괄적 작업을 적용하고자 할 때 효과적이다. 특정 연산을 적용하거나, 특정 조건에 맞는 값만 필터링해 새 시퀀스를 구성하거나 하는 일이 이에 속한다. 하지만, 모든 반복문을 스트림으로 대체하겠다는 생각은 위험하다. 스트림보다 반복문을 사용하는 편이 좋다거나, 경우에 따라서는 스트림을 사용할 수 없는 상황도 존재한다. 스트림은 스트림과 반복문 둘 다 적용해봤을 때 스트림 쪽이 더 좋다고 확정지을 수 있는 경우에만 사용하는 것이 좋다. 속도 면에서, 제어 흐름의 유연성 면에서 반복문이 더 뛰어난 경우도 많기 때문이다.

 

 

8장. 메서드

8장에서는 메서드를 설계할 때 주의할 점을 다룬다. 메서드는 자바 프로그램에서 객체 간 상호작용에 해당하는 가장 중요한 요소 중 하나로 가독성, 효율성, 유연성 등 많은 성질에 영향을 미친다.

매개변수의 유효성 검사를 수행할것

메서드에 전달되는 매개변수의 유효성 검사는 메서드 몸체가 실행되기 이전에 수행되는 것이 좋다. 메서드 몸체 실행 중 매개변수에 의해 비정상 종료(예외 또는 원하지 않는 결과 반환)가 일어나게 되면 원인을 추적하기 어려워지기 때문이다.

메서드 시그니처 정의 시 유의사항

메서드의 이름은 이를 호출하는 클라이언트로 하여금 해당 메서드를 통해 어떤 동작이 수행될 것이라 예측할지를 결정하게 한다. 메서드 이름은 표준 명명 규칙을 따라 짓는 것이 좋다.

메서드의 매개변수는 4개 이하로 유지하는 것이 좋다. 이를 넘어갈 때에는 이들을 담아 전달할 별도 데이터 통신 객체를 선언하는 편이 좋다. 동일 타입의 매개변수가 여러 개 존재할 때에는 특히 조심해야 한다. 헷갈릴 여지가 조금이라도 있다면 별도 객체를 선언해 넘기는 쪽을 선택하는 편이 낫다.

또한, 코드의 확장성을 고려해 참조 타입의 객체를 받을 때에는 타입을 클래스보다 인터페이스로 지정하는 편이 좋다. 이 인터페이스를 구현하는 모든 클래스를 인자로 받을 수 있고, 정해진 동작을 호출할 수 있기 때문이다.

null 반환은 신중히 할 것

비정상 종료에 대한 처리로 null을 반환하는 것은, 클라이언트에게 별다른 힌트도 없이 null 처리를 할 것을 요구하는 것과 같다. 클라이언트가 null 처리를 빼먹게 된다면 이는 런타임 에러인 NullPointerException으로 이어진다.

비정상 종료에 의해 빈 값을 반환될 수도 있는 경우 null 대신 Optional을 쓰는 것이 좋다. 적어도 클라이언트는 빈 값이 올 수 있음을 인지할 수 있고 이에 대한 처리 코드를 구현할 수도 있다.

예외적으로 컬렉션/배열은 Optional과 함께 사용하는 편이 오히려 어색하며, 빈 컬렉션/배열을 반환하는 쪽이 좀 더 자연스럽다.

메서드에 javadoc 주석 달기

메서드에 다는 javadoc 주석에는 메서드의 행동, 매개변수, 반환 타입, 던지는 예외에 대한 정보가 들어간다. 유의할 점은 다음과 같다.

  • 행동 - 어떻게 동작하는지(내부 구현)가 아니라 무엇을 수행하는지(입력에 대한 출력) 명시해야 한다.
  • 행동 - 해당 메서드를 호출하기 위한 전제조건이나 사후조건, 메서드 호출에 의한 사이드 이펙트가 있다면 명시해야 한다. 스레드 안정성이나 직렬화 가능성 등이 이에 해당한다.
  • 던지는 예외 - 비검사 예외의 경우 javadoc에 명시하고, 검사 예외의 경우 throws 문에 명시되므로 반드시 포함하지 않아도 좋다.

 

 

9장. 일반적인 프로그래밍 원칙

9장에서는 자바 프로그램에서 가독성/유지보수성 등을 확보하기 위해 준수하면 좋은 규칙들을 소개한다. 9장은 자바 프로그래밍 전체를 아우르는 원칙들을 다루기 때문에, 이 중에서 깊이 공감하는 원칙이나 직접 실천해보고 공감하게 된 원칙을 위주로 정리해보았다.

지역변수의 범위를 최소화

절차형 언어처럼 변수의 선언부를 프로그램 앞단에 별도로 두는 것은 객체형 언어에서는 오히려 좋지 않다. 가독성을 떨어트리고 가용 scope 밖에서 접근이 가능하게 만들어 프로그래머의 실수를 유발할 수 있기 때문이다. 자바에서는 지역변수의 선언 시점을 최대한 사용 시점 직전으로 미루는 것이 좋다.

라이브러리를 적절히 활용

복잡한 부가 기능을 구현해야 할 때, 대부분의 경우에는 직접 구현하는 것보다 관련 라이브러리를 이용하는 것이 좋다. 전문가들이 이미 성능, 버그 여부 등에 대해 충분한 검토를 거친 것이기 때문이다.

한 마디로 요약하자면 바퀴를 다시 발명하지 말자.

문자열보다 다른 타입이 더 적절하지 않은지 확인

특히 숫자 등 primitive type을 대체하고 있지는 않은지 주의해야 한다. 그룹화할 수 있는 특징이 있다면 열거 타입을 사용하는 편이 좋다. 혼합 타입을 하나의 문자열로 나타내기보다 각 타입의 별도 변수로 나타내는 편이 좋다.

문자열은 실수하기 딱 좋고, 실수가 컴파일타임에 발견되기는 어려우며, 불변성 관련 이슈가 생길 수 있다. 문자열은 정말 문자열일 때에만 사용하는 것이 좋다.

문자열 연결은 느리므로 경우에 따라 StringBuilder로 대체

String은 불변이므로 연결할 때마다 새 문자열을 생성한다. 이는 두 문자열을 복사한다는 점에서 시간 효율도 낮고, 연결할 때마다 새 객체가 생성되는 것이므로 공간(메모리) 효율도 낮다.

(반복문 순회 등을 통해) 많은 문자열을 연결하는 경우 최종 문자열만을 불변 String으로 변환하는 StringBuilder를 사용하는 것이 더 좋다. 많은 문자열인 이유는, StringBuilder의 초기 생성 비용이 어쨌든 String보다는 크기 때문이다.

스레드 안정성이 필요하면 StringBuffer를 사용하는 것이 더 좋은데, StringBuffer는 스레드 안정성을 보장하기 위해 StringBuilder보다 속도가 느림에 주의해야 한다.

일반적으로 통용되는 명명 규칙을 따를 것

많은 초보 개발자들이 실수 또는 간과하는 부분이다. 명명 규칙은 같은 언어를 사용하는 개발자들 간 암묵적 약속이다.

  • 패키지/모듈명 - 소문자와 숫자로만 구성하며 통상적으로는 도메인을 역순으로 구성한다.
  • 클래스 - 파스칼 케이스와 명사(구)를 사용한다. 객체를 생성하지 않고 기능만을 제공하는 클래스의 경우 특수하게 복수형 명사 사용을 권장한다. (유틸리티 관련 클래스가 이에 해당한다. ex: StringUtils)
  • 인터페이스 - 클래스처럼 파스칼 케이스와 명사(구)로 짓거나 특정 행동의 가능성을 나타내기 위해 형용사를 사용한다.
  • 메서드 - 카멜 케이스와 동사(구)를 사용한다. 함수가 수행하는 기능에 따른 명명 규칙도 존재하는데, getter 메서드의 경우 get+타입명을(boolean의 경우 is/has+타입명), setter 메서드의 경우 set+타입명을, 타입 변환 메서드는 to+타입명, 다른 타입의 뷰를 반환하는 메서드는 as+타입명을 사용한다.
  • 필드 - 카멜 케이스와 명사(구)를 사용한다.
  • 제네릭의 타입 매개변수 - T(type), E(element), K V(key, value), X(exception), R(return)을 주로 사용한다.

일반적으로 모든 요소는 줄임 표현을 사용하지 않는 편이 좋다. 변수명을 짧게 하려다가 오히려 가독성을 저하시킬 수 있기 때문이다. 정말 널리 통용되는 줄임말(max, min 등)은 괜찮다.

또한, 예외적으로 아주 짧은 scope 내에서만 사용되는 지역 변수의 경우에도 코드를 이해하는데에 큰 악영향을 미치지 않는 선에서 줄임 표현을 사용해도 괜찮다. (for 문의 i 변수 등)

 

 

10장. 예외

10장에서는 프로그램의 가독성, 신뢰성, 유지보수성을 위해 예외를 제대로 활용하는 방법을 소개한다.

언제 어떻게 예외를 사용

우선 예외는 예외상황에만 사용해야 한다. 정상적인 흐름에서 흐름 제어를 위해 예외를 사용하면 JVM의 최적화를 방해하는 요인이 되고 다른 사람이 코드를 잘못 이해할 수 있다.

또한, 언제 검사 예외(Checked Exception)를 던지고 언제 비검사 예외(Unchecked Exception)를 던지는지는 복구 가능성에 따라 결정하는 것이 좋다. 호출자가 예외를 캐치해 적절한 처리를 통해 복구할 수 있는 경우에는 검사 예외를 던지고, 복구할 수 없을 것으로 예상된다면 비검사 예외를 던지는 것이 좋다. (이 기준이 적용된 대표적 예시로 Spring 프레임워크의 @Transactional의 경우에도 이를 고려해 기본 설정으로 검사 예외는 롤백하지 않고 비검사 예외는 롤백을 수행한다.)

복구 불가능 정도가 아니라 프로그램 자체가 실행이 불가능할 정도라면 Error를 사용한다. 프로그래머가 직접 사용할 일은 거의 없고 발생하는 에러에 대해 (프로그램을 중단하고) 적절한 조치를 취하는 경우가 대부분이다. 대표적인 예시로, 무시무시한 OutOfMemoryError가 있다.

예외적으로 프로그래머가 직접 던지는 에러도 존재하는데, AssertionError가 그러하다. 정상적인 로직 내에서는 호출이 불가능한 메서드를 호출하거나 하는 경우 던지게 되며, 이 오류를 만난 프로그래머는 디버깅을 통해 에러의 원인을 찾아 코드를 수정해야 한다.

언제 표준 예외를 사용하고 언제 커스텀 예외를 정의

표준 예외를 적절히 사용하는 것은 다른 사람이 내가 만든 API의 제어 흐름을 이해하기 쉽게 만든다. 커스텀 예외는 다른 사람으로 하여금 이해하기 위한 시간을 소요하게 만들고, 더 많은 예외 클래스를 메모리에 적재하게 된다는 점에서 남발하지 말아야 한다. 하지만 표준 예외만으로 원하는 정보를 모두 제공하기 어렵다고 판단되면 적절한 커스텀 예외를 정의해야 한다.

커스텀 예외를 정의할 때에는, 적절한 계층 구조를 이루도록 하는 것이 좋다. 추상화 수준에 맞는 예외 처리 로직을 작성하고 예외를 던지는 쪽에서는 구체적인 예외를 던져 가독성을 높이는 것이 좋다.

예외를 던지거나 처리하는 목적을 명시

javadoc을 작성할 때 @throws 태그를 통해 예외를 문서화하는 것이 좋다. 검사 예외는 메서드 선언부의 throws 절에 명시되기 때문에, 태그에는 비검사 예외만 명시하는 것이 검사/비검사 예외를 명확히 구분할 수 있도록 한다.

또한, 경우에 따라 특정 예외는 처리하지 않거나 처리할 수 없어 무시하도록 구현하고 싶을 때가 있다. 이럴 때에는 그냥 catch문을 비워둘 것이 아니라 catch의 인자(예외 변수)명을 ignore로 변경하고 빈 catch 블록 내에 해당 예외를 무시하는 이유를 명시하는 편이 좋다.

 

 

11장. 동시성

11장에서는 멀티 스레드를 이용한 동시성 프로그래밍을 할 때 유의할 점을 소개한다.

동기화를 지원하는 방법들

동기화를 할 때 보장해야 할 요소로는 배타적 실행과 안정적 통신이 있다. 배타적 실행이란, 하나의 가변 데이터를 하나의 스레드에서 수정 중일 때 다른 스레드가 이를 인지하지 못하도록 하는 것이다. 안정적 통신이란, 배타적 실행의 결과로 업데이트된 최신 데이터를 다른 스레드가 인지할 수 있도록 하는 것이다. 동시성 프로그래밍은 위의 두 가지 요소가 모두 보장되어야만 여러 스레드가 가변 데이터를 수정하고 획득하는 과정에서 문제가 발생하지 않는다.

위의 요소들을 보장하기 위해 java.util.concurrent를 잘 활용하는 것이 좋다. 동기화를 위해 만들어진 다양한 도구들을 제공한다.

하지만 결국 가장 좋은 방법은 가변 데이터에 여러 스레드가 접근하는 상황 자체를 만들지 않는 것이다. 여러 스레드가 공유하는 객체는 불변으로 만든다면 데이터가 수정될 일이 없으니 동기화를 지원할 필요가 없어진다.

 

 

12장. 직렬화

직렬화는 객체를 바이트 스트림으로 인코딩해 다른 머신이나 디스크 등에 저장할 수 있도록 하는 유용한 도구이지만, 여러 위험성을 내포한다. 12장에서는 직렬화의 위험성과 이를 예방하는 방법을 소개한다.

자바 직렬화의 위험성

직렬화를 지원하는 순간 객체 내부의 공개되지 않은 구현 내용들도 직렬화 대상이 되어 외부로 드러나게 된다. 즉, 캡슐화가 깨진다. 만약 객체의 내부 구현이 변경된 후 이전 버전 객체의 직렬화 데이터를 역직렬화하려고 시도한다면 직렬화 결과가 달라 역직렬화에 실패할 수도 있다.

또한, 역직렬화를 이용하면 생성자가 아닌 방식으로도 객체를 획득할 수 있다. 이를 통해 불변 데이터를 수정하거나 허용되지 않는 값을 획득할 수 있다. 이는 버그나 보안 취약점에 취약한 코드가 된다.

직렬화 위험성을 예방하는 법

가장 좋은 방법은 직렬화하지 않는 것이다. 따라서, 반드시 직렬화가 필요한 경우에만 Serializable을 구현하는 것이 좋다. 대부분 값을 담는 클래스는 외부에 저장이 필요할 때 직렬화를 지원하고, 기능을 수행하는 클래스는 직렬화를 지원하지 않는다. 값을 담는 클래스 내에서도 직렬화 결과에 포함되지 않아도 되는 필드들은 transient로 선언하는 것이 좋다.

직렬화를 지원해야 한다면 커스텀 직렬화/역직렬화기를 구현하고, 직렬화 프록시 패턴을 이용해 역직렬화 대상에 대한 철저한 검증 절차를 마련해두는 편이 좋다. 이는 외부로부터 악의적 바이트 스트림을 역직렬화하려는 시도가 일어났을 때 방어하기 위함이다.

 

 

총평

나는 이 책이 이제 막 개발을 배우는 중인 사람보다는 다른 개발자와 협업을 해 본 경험이 있는 사람이 읽으면 많은 도움을 얻을 수 있는 책이라고 느꼈다.

나는 다른 개발자와 여러 차례 협업을 해 보고나서야 비로소 타인은 내가 짠 코드를 다른 의도로 이해할 수 있고, 다른 방식으로 사용할 수 있다는 사실을 몸소 깨닫게 되었는데, 이 글에서는 내용 전반을 통틀어 코드를 통해 의도를 설명하거나 문서화를 통해 의도를 명시하는 방식에 대해 강조하고 있다. 이 부분이 특히 나에게 가장 많은 도움이 되었지만, 협업 경험이 없는 상태에서 읽었다면 가볍게 여기고 넘어갔을지도 모르겠다는 생각이 든다.

또한, 이 책을 읽으며 다시 한 번 은총알은 없다(No Silver Bullet)라는 말이 깊게 와닿았다.

이 책을 읽으면 같은 기능도 특정 상황에서는 최고의 돌파구가 되기도, 또 다른 상황에서는 오히려 시스템의 완성도를 저하시키는 요인이 되기도 함을 강조하고 있다. 이러한 이유로 개발자는 항상 Trade-off를 고려하는 자세를 갖춰야 하는데, 이 책은 이러한 자세를 갖추는 데에 있어 어떤 요소를 기준으로 삼을지에 대해 유용한 가이드라인을 제공한다.

이러한 이유로 자신의 코드에 적절한 의도와 근거를 담고 싶은 모든 자바 프로그래머에게 이 책을 적극 권장하고 싶다.