Search

[독서 후기] 이펙티브 자바 (~ing)

Created
2023/08/12
Tags
Java
Code
Book
개요
객체 생성과 파괴
Static Factory Method vs Factory 패턴
불필요한 객체 생성
FlyWeight 패턴
Bridge 패턴
Adapter 패턴
Object의 Freeze 및 Seal
Immutable 과 Invariant
Bounded Wild Card Type
모든 객체의 공통 메서드

1. 개요

2. 객체 생성과 파괴

1) Static Factory Method vs Factory 패턴

Static Factory Method와 Factory 패턴의 차이를 이해할 수 있었다. 내가 알고 있는 Factory라는 용어는 주로 인터페이스와 구현체와의 접점을 제공하는 Factory 패턴으로 인지되는 경우가 많았기 때문이다. 이미 자바를 이용할 때 from, of, valueOf, getInstance, newInstance 등의 Static Factory Method를 이용하고는 있었지만, 이와 같은 메서드가 Static Factory Method라는 이름으로 불리는지는 처음 알게 되었다.

2) 불필요한 객체 생성

정말이지 책 읽고 뜨악 싶었던 부분이 있었다. String을 생성할 때 new String을 통해 리터럴을 인자로 넣는 것이 불필요하다는 것을 알았을 땐, 정말 내가 자바를 많이 모르는구나 싶었다. 심지어 어떤 부분은 리터럴로 쓰고, 어떤 부분은 생성자를 썼던 것이 마구 떠올랐다. 지금 생각해보면 당시에는 왜 그런 구분 없이 맘대로 썼는지 싶다.

3) FlyWeight 패턴

대체로 이 장에서는 객체를 생성할 수 있는 효율적인 방법을 소개했기에, 정적 팩토리 메서드를 주로 이용했다. static 메서드 특성 덕분에 이미 생성된 객체를 이용한다거나, 객체를 반환하기 이전에 라이프 사이클 관련 로직을 넣을 수 있다는 점이 특징이라는 것을 알게 되었다. 대부분 이러한 작업들은 객체를 생성하는데 들어가는 리소스를 줄이는 패턴으로써 이용된다는 것을 알게 되었다.

4) Bridge 패턴

조금 깊이가 있는 객체를 설계하는 경우엔 Factory 패턴을 구현하면서, 추상 객체와 인터페이스를 통해 객체의 세세한 동작을 결정 짓는 구현을 해왔었다. 객체 지향을 하면서 SOLID에 충실하려고 했었고, 반복되는 변동에 대처가 어려워서 나름의 답을 찾아갔던 방법이 곧 Bridge 패턴이라는 용어라는 것을 알 수 있었다.

5) Adapter 패턴

프로젝트를 수행할 때 실제로 어떤 라이브러리를 이용하고 있었는데, 이에 대한 인터페이스 운용 없이 구현체를 그대로 운용하고 있었던 적이 있다. 근데 해당 라이브러리가 Deprecated 되면서 객체를 변경해야할 상황에 놓이게 되었다. 당시에는 코드를 지금보다도 못 짜는 상황이었기 때문에 소스 코드의 많은 곳을 드러내야할 상황이었다. 이 때 내가 했던 선택인 동일한 이름의 인터페이스를 구성하고 기존 객체와 새로운 라이브러리의 객체를 연동시켜 변경을 최대한 줄이는 것이었다. 이 역시도 Adapter라는 패턴이었다는 것을 기억해낼 수 있었다.

6) Object의 Freeze 및 Seal

어렸을 적 개발에 입문하는 시기에 Flutter를 사용하면서 Freeze라는 표현을 처음 보았었다. 당시엔 이걸 왜 하는지, 무슨 의미인지 하나도 와닿지 않았었던 것으로 기억한다. 비교적 최근까지도 그랬던 것 같다. Freeze를 통해 객체를 불변으로 만드는 것이 문제 방지를 위해 실수를 줄일 수 있다는 수단이라는 걸 프로젝트를 해보면서 알게 되었다. 거기에 덧붙여 Freeze를 공부하면서 Seal라는 비슷하지만 확실히 다른 개념을 또 알게 되었다. Freeze가 필드와 값의 고정이라면, Seal은 필드의 고정이고 값의 변화는 허용하는 개념이었다.

7) Immutable 과 Invariant

두 용어 모두 불변이라는 뜻을 지니지만, 막연히 혼용해서 써왔던 것 같다. 하지만 특정 챕터를 읽을 때 두 개의 용어가 모두 등장하면서 정말이지 머리가 띵했던 것 같다. 차이를 명확히 알 수 있었다. 어떠한 일이 있어도 변하지 않는 것이 Immutable, 특정 조건식이 만족되는 동안 불변하는 것이 Invariant라는 것을 말이다.

8) Bounded Wild Card Type

프로그래밍하면서 와일드 카드는 들어봤지만, 이 용어는 처음 들었다. 그리고 이내 이 용어가 무엇을 지칭하는지 알 수 있었다. 제네릭 프로그래밍을 안 해본 것은 아니지만 정말 단순히 T에 대한 개념만 있었다. C++을 사용했을 때는 Reference의 개념까지 더해서 알고 있었지만, 자바에서는 아무런 소용이 없었다. <? extends E>, <? super E>와 같은 구문을 Java, Flutter 등을 사용할 때는 전혀 알지 못했지만, 지금은 책을 뚫어져라 몇 번을 보니 얼추 이해가 되었다. 이런 구문을 사용하는 이유에 대해서 공감을 할 수 있었던 것이 가장 의의 있었던 것 같다. 내가 코드를 짜더라도 API 의 유연성을 높여서 자원을 제공하고 싶을 것 같기 때문이다. PECS (Producer Extends Consuer Super)라는 개념을 꼭 숙지해야겠다.

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

1) Reference

자바에서는 객체의 참조 종류로 4가지가 있다는 것을 알게 되었다. 일반적으로 객체의 생성과 할당으로 이뤄지는 Strong Reference, 그리고 객체가 Collecting 대상이면서 JVM의 메모리가 부족해졌을 때 회수를 시도하는 Soft Reference, Collecting 대상이 되었을 때 즉시 회수를 시도하는 Weak Reference, Collecting 되었을 때 Reference를 별도의 Queue로 관리하는 Phantom Reference가 있단 것을 알게 되었다.

2) AutoValue

구글에서 지원하는 어노테이션 라이브러리인데, 추상 클래스와 추상 메서드를 작성했을 때 이를 기반으로 구현체 클래스를 자동으로 생성해주는 라이브러리가 있다. 늘 추상 클래스에서 파생되는 새로운 구현체를 만들 때 반복 작업이 많다고 생각했는데, 이런 것이 있다는 것도 새롭게 알게된 점이다.

3) Native Peer 객체

책에 기재된 이 용어가 무엇인지 어렵게 느껴지기도 했는데, 일반적으로 내가 이해하고 있는 Native는 플랫폼 자체에서 동작하는 무언가이다. 자바는 JVM을 이용하기 떄문에 플랫폼 종속적이지 않고, Native 특성을 타는 것들은 대체로 C와 C++ 같은 언어라고 이해하고 있다. Native는 플랫폼의 특성을 고려해야하는 대신 뛰어난 성능을 얻을 수 있다. 자바의 경우에도 성능이 필요한 경우 C, C++로 작성된 코드를 이용한다는 것을 GC 내용을 찾다가 JVM을 설명하는 글을 통해 알게 되었다. 즉, Native Peer 객체란 자바가 이런 Native 기능들을 이용하기 위한 객체이다.

4) Comparable

Comparable의 compareTo, Comparator의 comparing, 그리고 Boxed 타입의 각종 비교 메서드가 그렇게 헷갈렸는데 명확하게 이해할 수 있었다. 객체의 비교 가능성을 위해 Comparable을 인터페이스로 지원하고, 이를 구현하기 위해 Comparator의 메서드 혹은 Boxed 타입의 비교 메서드를 이용하는 것으로 이해했다. 특히, 비교 연산자 대신 비교 메서드를 이용하는 것이 추이성을 위반하지 않으면서 오버플로우로부터 안전하다는 것을 통해 잘못된 습관을 개선할 수 있었다.

5) Checked Exception

Checked Exception을 throw만 하는 것, catch까지 하는 것, StackTrace까지 얻어내는 것들의 시간을 측정했을 때, 언급한 순서대로 시간이 오래걸렸다. 그만큼 Checked Exception은 비싼 것임을 알 수 있었으며, 이러한 이유 때문에 Optional 이용하는 것을 권장하는구나 느낄 수 있었다.

6) 공유 자원

synchronized 및 lock 등을 이용하여 공유 자원을 접근할 시엔, 컴파일러 최적화의 도움을 받지 않아야하므로 공유 자원에 대해선 해당 자원에 접근할 때 명령어 순서를 바꾸지 않겠다는 volatile을 함께 명시하여 이용하는 것이 맞다는 것을 알 수 있었다.

7) HashSet, HashMap

놀랍게도 HashSet과 HashMap의 경우 충돌 임계치를 넘게 되어 버킷 내에 노드가 많아지게 되면, 자체적으로 RB Tree를 구성하여 이용하는 것을 확인할 수 있었다.

4. 클래스와 인터페이스

1) final

웃기게도 자바에서 const를 이용해본적이 없는데, const가 있다고 어렴풋이 생각했던 것 같다. 그리고 final 키워드를 그렇게 많이 보았는데도 의심 한 번 하지 않다가, final을 이용한 불변 객체의 운용이 함수형 프로그래밍의 길을 열게 되었다는 부분을 보고선 const가 떠오르게 되었다. Flutter에서는 const와 함께 사용하는 것이 가능해서 그런가 Java에서도 있을 것이라고 헷갈렸다. 그러다가 문득 왜 Java엔 final만 있고 const는 없을까라는 궁금증이 생겼고, 일반적인 const 데이터가 위치하는 것과 달리 JVM 상에서는 const를 어디에 위치시켜야 하는지 프로세스에 따라서 애매하지 않을까라는 잠정적인 결론에 다다랐다. 그 외에도 record에서의 인자가 암식적으로 final로 선언된다든가, final을 선언한 클래스는 상속이 안 된다든가, final을 선언한 메서드는 재정의가 안 된다든가 여러 용법에 대해서 이해할 수 있었다.
불변 객체를 만드는데 있어서 final은 필수불가결하다보니, 불변 객체에 대해서 많이 찾아보게 되었다. 그리고 자연스럽게 프로그래밍 패러다임까지 접하게 되었는데, 이는 책에서 풀이한 함수형 프로그래밍과 절차적 프로그래밍의 기저를 파악하는데 도움이 되었다. 함수형 프로그래밍의 기본은 불변에 있어서 메서드를 호출 했을 때 자신 객체의 상태를 바꾸지 않음으로써 새로운 객체를 반환해야하고, 이와 달리 절차적 혹은 명령형 프로그래밍에서는 메서드를 호출 했을 때 자신 객체의 상태를 바꾸는 식의 동작이 주를 이룬다는 것을 알 수 있었다. 이 때 두 방식의 명명 규칙도 알 수 있었는데, 불변 객체 운용의 명명 규칙은 자신의 상태를 바꾸지 않으므로 전치사를 이용하지만, 상태성을 갖는 객체를 대상으론 동사를 주로 이용한다는 것을 알게 되었다.
불변 객체는 기본적으로 상태를 못 바꾸므로 객체 변화가 필요한 경우 메모리에서 손해를 보는 경우가 많지만, 이러한 단점을 수용할 수 있는 경우엔 얻을 수 있는 장점이 많다는 것을 알 수 있었다. 예를 들어 상태 전이 문서화를 하지 않아도 된다는 점, 쓰기 작업이 없으므로 별도의 동기화가 불필요하다는 점, 객체 생성을 줄일 수 있어서 가비지 컬렉션 비용을 아낄 수 있다는 점, clone과 복사 생성자 그리고 방어적 복사의 개념이 불필요하다는 점, 단순하고 공유 가능하다는 점 등이 있다는 것을 알 수 있었다.

2) 상속

상속은 내가 go를 먼저 접하기도 했었고, 부작용 사례를 많이 듣거나, 코드 재활용에선 득을 얻었지만 설계 미스로 유연성에 많은 제약을 받았던 경험을 생각해보면 그렇게 좋아하지 않으며, 까다로운 기법이라고 인식하고 있었다. 책에서도 이와 비슷한 서술이 많아서 공감이 되었었다. 기본적으로 equals에서 언급되었던 대칭성과 추이성 만족이 어렵다는 것을 필두로, 상태성을 가지게 만드는 것이 가능하여 가변성을 가질 확률이 높고, 이를 통해 캡슐화를 깨면서 불필요한 데이터가 드러나기도 하고, 재정의 가능한 메서드를 생성자에 호출하면서 버그가 발생한다든가 하는 단