Search

[독서 후기] 이펙티브 자바

Tags
독서 후기
Java
Created
2024/05/05 10:00
Created time
2023/08/13 12:50
category
diary

1. 개요 (5번으로 건너뛰세요^^)

Effective Java를 공부하면서 작성한 글을 통해 독서 후기를 작성했다.

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에서 언급되었던 대칭성과 추이성 만족이 어렵다는 것을 필두로, 상태성을 가지게 만드는 것이 가능하여 가변성을 가질 확률이 높고, 이를 통해 캡슐화를 깨면서 불필요한 데이터가 드러나기도 하고, 재정의 가능한 메서드를 생성자에 호출하면서 버그가 발생한다든가 하는 단점이 있었다.

5. 진짜 독서 후기

2~4번 항목은 2023년 스터디를 하면서 중간 중간 작성했던 글들이다. 독서와 스터디가 생각보다 길게 이어져서 2024년이 되어서야 마치게 되었고, Java는 내 주력 스킬 중 하나지만 더 이상 주력이 아니게 된 상황이다. 이러한 이유로 더 자세하게 항목 별로 작성하는 것은 수지타산이 맞지 않아서 항목 별 작성을 멈추게 되었다. 그럼에도 독서 후기를 적지 않는건 너무 아쉽지 않은가라는 생각이 들었다. 그래서 2024년 5월 5일 스터디를 마침과 동시에 진짜 독서 후기를 작성한다.
일단 이전에 작성한 항목들을 시간이 지나서 다시 읽어봤을 때 느낀 점이다.
객체 지향 언어에서 집중해야할 요소들을 정말 많지 않은가 싶고, 그런 의미에서 위에 나열한 것들은 정말 순수한 그 의미 자체로 “나열”에 불과한 것 같다. 2023년에 내가 알고 있던 자바 지식과 사용하던 자바 스킬에 비해, 드라마틱하게 2024년에 달라졌다고는 말 못하겠지만 많은 이해도가 내재되었다고 생각한다. Java를 이용해서 일도 해보고, Spring 프레임워크를 분석해보기도 하고, Spring에 걸쳐 있는 다른 프로젝트들을 이용하여 어플케이션도 만들어보고 했으니, 피부로도 느끼는 만큼 많이 늘었다고 본다. 이런 경험을 바탕으로 책에서 소개한 각 아이템 별로 2~4번 항목과 같이 자잘하게 느낀 점들이 많으나, 2~4번 항목처럼 정리하려니 대체로 스킬적인 부분과 맞닿아서 이전처럼 작성하기엔 애매하다고 생각했다. 그래서 지식의 나열보다는 느낀 점 몇가지로 독서 후기를 구성해볼까 한다.

자바는 오래된 언어이다

책을 읽으면서 느낀 첫 번째였다. 현재 이용하고 있는 11, 17, 21 버전의 자바는 정말 멋지다. 하지만 Effective Java를 공부하면서 Thread Pool, Serializable, Stream, Collection 들을 분석했을 때, “어떻게 옛날에도 이런 생각을 했지” 싶은 코드도 있었고, “현재 자주 사용하는 메서드랑 역할이 비슷하고 애매한데” 싶은 코드도 있었던 것 같다. 대체로 자바라는 언어가 어떻게 발전해왔고, 어떤 문제점들을 극복하려고 집중해왔는지 간접적으로 느낄 수 있었다.
지극히 주관적인 생각이지만, Go, Python, C, C++, Java, Rust 등 여러 언어를 사용해보면서 느낀 부분인데, Java는 생각보다 프로그래밍 세계에서도 깊은 곳에 그 개념들이 산재되어, 여기저기 영향을 끼치고 있다는 생각을 많이 했다. 누군가는 이걸 근본이라고 부를 거고 나도 크게 부정은 하고 싶지 않은데, 그만큼 다른 언어, 도구, 개념, 철학을 접할 때 많은 도움을 받고 있어서 그런게 아닐까 라는 생각을 한다.

자바는 제품을 위해 생산성을 높이기 좋은 언어이다

여러 상황 별로 적합한 언어가 있다는 말을 정말 많이 들었고, 이는 부정하기 힘들다. 이 얘기가 머리 속에 있음에도 불구하고, 나는 스크립트 언어를 정말 많이 부정해왔다. 그래서 어떠한 경우에도 주력 언어로 사용하고 있는 도구들을 이용해서 목표를 달성하곤 했다. 아마 많은 개발자들이 그러하지 않을까라는 생각을 했다.
모순적이게도 이러한 생각을 (자바 후기인데) 다른 언어를 사용하면서 느끼게 되었다. 독서, 스터디를 병행하면서 카카오에서 인턴십을 수행했고, 주로 실험을 위한 프로그래밍 (환경 구축, 벤치마킹 등)을 많이 했다. 처음엔 자바를 많이 이용했는데, 다른 사람들은 내가 작성한 것의 1/4 정도의 시간 만으로 원하는 바를 달성하고 쭉쭉 치고 나가는 것을 보고, 다시금 언어의 목적에 대해서 고민을 많이 했다. 그 때 처음으로 스크립트 언어에 대한 인식이 좋아졌다.
스크립트 언어의 실행 속도가 빠르다는 말에도 깊이 공감했고, 자바가 동작하는 원리와 별개로 컴파일이라는 과정 하에 이뤄지니 엔지니어링에서 선호되는 언어가 아니라는 부분에도 공감했고, 파이썬이 왜 엔지니어들에게 사랑받는 언어 1위인지도 공감할 수 있었다. 고찰이 극한에 다다른 어느 날 잠들기 직전에, 자바는 어느 상황에 좋을까라고 고민했다. 언제나 머리 속에서 인지하고 있던, “제품”, “생산성”이라는 용어가 먼저 떠올랐고, 확실히 자바는 (물론 이 마저도 상황마다 달라질 수 있지만) 제품을 개발하는 과정에서 여러 개발자들 사이에 생산성을 추구하기 좋은 언어라고 생각했다.

객체 지향의 본질을 놓치지 말자

현재는 개발을 하더라도 제품을 위한 개발은 하지 않고 있고, 이러한 형태의 개발은 안할 확률이 더 높을 것이다. 하지만 업무상 그런 것이니, 언제 어떤 상황에 다시 제품 개발을 할지 모르지 않은가. 객체 지향은 복잡한 상황 (비즈니스 로직)을 역할 별로 나누고, 이 역할을 객체가 담당하도록 책임을 얹어서 간단하게 해결할 수 있는 패러다임이라고 생각한다. 언제든 복잡한 상황을 풀어내야하는 상황이라면 객체 지향을 잊지 말고 하나의 선택지로 열어둘 수 있어야겠다고 생각한다.
객체 지향을 하게 되었을 때 단순히 클래스만 만들고, 디자인 패턴만 적용하면 끝일까? 이 책을 비롯한 많은 책들이 시사하는 바로, 클래스는 객체가 아니라는 점이다. 객체가 갖고 있는 속성은 객체만 알도록 캡슐화하고, 실제로 객체간 소통으로 복잡한 로직을 풀어갈 때 최대한 메서드 기반으로 객체가 객체에게 말을 전한다는 느낌으로 작성할 수 있도록 해야된다는 것도 몸에 많이 새길 수 있었다. 특히 자바라는 언어를 사용할 땐 이런 소통 과정을 개발자가 명확히 알아챌 수 있도록 길더라도 정확한 메서드 이름을 명시하는 것이 중요하다는 것도 느낄 수 있었다.
이 책에서 주는 가치는 겉보기엔 스킬적인 부분이 많지만, 그 의미는 객체 지향을 위해 선배 개발자들이 수없이 부딪히고 얻은 노하우이다. 겉모습과 화려함에 속지 않고, Effective Java를 모르더라도 자신이 작성한 코드가 어떤 의미를 전달하고자 작성한 것인지 어필할 수 있어야한다고 생각했다.

자바 API와 객체 내부 구조는 장식이 아니다

개발하다보면 내가 사용하고 있는 의존성들이 정말 Low하지 않다는 것을 알 수 있다. 여기서 Low/High의 구분은 내가 작성한 구문이 실제로는 여러 메서드와 객체를 기반으로 두고 있냐 아니냐를 말하고 싶다. 만일 의존성이 존재하고, 그 의존성이 꽤나 Java 기본 라이브러리와 밀접하다면 언젠가는 가장 우선적으로 딥 다이브할 필요가 있지 않을까라는 생각을 한다.
“그런거 몰라도 구현 잘 되고, 잘 동작하고, 문제 없는데요?”라고 말한다면, 자신이 무엇을 목표로 하는지 돌이켜봐야하지 않을까라고 느꼈다. 자신이 그저 그런 개발자가 아니라 조금이라도 훌륭한 동료가 되려고 하고 좋은 코드 작성하려고 한다면, 그저 그런 이해도론 곤란하지 않을까라는 생각이 먼저 들었다. 자신이 작성한 코드가 어떤 상황에서만 유효한 코드인지 감을 잡을 때까진 자바가 제공하는 API와 자신이 사용하는 객체의 내부 구조를 소홀히 해선 안 된다고 생각했다.
특히 생각보다 기술이라는 건 예리해야하고, 깐깐하고, 명확해야한다는 것을 많이 느꼈다. 실제로 카카오에서도 많은 개발자분들께서는 이러한 기반 지식을 바탕으로 많은 것들을 판단하는 것을 보고 느껴서 그런지도 모르겠다. 비록 지금은 주력 언어가 자바가 아니기 때문에 주력으로 다뤄야하는 기술에 대해서 여기서 얻은 인사이트를 적용해서 Low하게 학습해야겠다고 느끼고 있다.

기타

어려웠던 순간들이 정말 많았다. 특히 책의 마지막 부분 Concurrency와 Serializable은 직접적으로 이용할 상황이 크게 없거나, 있더라도 사용자에게 맞닿아 있는 아주 일부만 사용하는 경향이 있다보니, 책에서 전달하고자 하는 바를 캐치하기 정말 어려웠다. 이를 이해할 수 있도록 도와준 Effective Java 스터디 구성원들에게 정말 감사하다고 느꼈다. 좋은 책을 두고 9개월 간 고민하면서 읽고, 스터디원들과 논의할 수 있어서 진심으로 행복했다.