잡동사니

백기선님의 이펙티브 자바 완주 후기벽 공략 1부 완주 후기 (feat. Inflearn) 본문

IT/Java

백기선님의 이펙티브 자바 완주 후기벽 공략 1부 완주 후기 (feat. Inflearn)

yeTi 2022. 8. 9. 23:30

안녕하세요. yeTi입니다.
오늘은 백기선님의 인프런 강좌 중 하나인 이펙티브 자바 완벽 공략 1부 의 완주 후기를 공유하려고 합니다.

강의 수강 계기

앞서 백기선님의 이팩티브 자바 완주 후기 를 공유 했는데요.

유튜브 버전 강의는 맛보기 버전인데 그 동안 당연하게 사용하던 코드들의 시발점부터 장단점까지 알아가는 과정이 즐거웠어서 정식으로 인프런 강의를 보게 됐습니다.

배운점

객체 생성의 시점을 관리하고 유연하게 할 수 있는 방법

생성자는 다양한 매개변수 타입을 지원하려면 많은 수의 생성자를 만들어 둬야한다는 단점본인의 객체 타입만 반환할 수 있다는 제한이 있습니다.

이를 좀더 유연하게 대응하게 하기 위해 static 메소드를 활용하여 객체 생성을 할 수 있는데요. 이를 정적 팩토리 메소드라고 합니다.

반환할 객체를 본인뿐 아니라 다른 타입의 객체를 반환할 수도 있고, 클라이언트에는 인터페이스만 제공하고 구현체를 주입받아 사용할 수도 있습니다. (서비스 제공자 프레임워크)

또한 생성자의 경우 인스턴스의 생성 시점을 관리 할 수 없는반면 정적 팩토리 메소드를 활용하면 인스턴스의 생성 시점을 관리할 수 있습니다.

반면 정적 팩토리 메소드 를 사용하면 상속이 불가하다는 특징과 클라이언트에게 사용

또한 매개변수가 많다면 객체를 생성할 때 코드를 읽기가 힘들수 있는데요.
이를 직관적으로 표현할 수 있는것이 빌더 패턴입니다.

빌더 패턴을 사용하지 않고 해결할 수 있는 방법으로는 생성자 체이닝자바빈즈 규약 이 있는데요. 생성자 체이닝은 변수가 증가함에 따라 코드가 증가하는 단점이 있고, 자바빈즈 규약은 인스턴스가 온전히 설정된 시점을 알 수 없는 단점이 있습니다.

빌더 패턴 을 적용하면 필수 필드와 옵션 필드의 특성에 맞게 구조를 가져갈 수 있고 인스턴스의 생성 시점을 명확하게 알 수 있는 장점이 있지만 구현을 위한 공수도 들기 때문에 적절하게 이득이 되는 경우에만 사용하기를 추천드립니다.

싱글톤 패턴을 적용하려면 Enum을 활용할 것

Class 타입으로 싱글톤 패턴을 적용할 수 있지만 멀티 쓰레드 환경에서 단일 인스턴스에 대한 것을 100% 보장할 수 없습니다.
이에 Enum 타입을 활용하면 단일 인스턴스 생성에 대한 보장을 받을 수 있지만, 상속을 사용할 수 없다는 단점이 있습니다.

싱글톤을 생성하는 보다 다양한 방법은 GoF 디자인 패턴 수강 후기 (feat. Youtube) 를 참고해주세요.

Util 클래스는 명시적으로 생성자를 만들지 못하도록 하자

유틸 클래스처럼 static 함수를 사용하는 클래스들은 private 생성자를 사용해서 인스턴스화를 못하도록 합니다.

사용하는 자원에 따라 동작이 달라지는 클래스는 의존 객체 주입을 사용하자

예를 들어 결제 서비스에서 결제라는 이벤트는 덩일하지만 결제 수단에 따라 연계 모듈이나 후처리가 다를 수 있습니다.

이 때, 결제 서비스를 사용하는 클라이언트가 각각의 결제 모듈을 직접 의존한다면 결제 모듈이 늘어나는 경우에 의존관계가 늘어나는 단점이 있습니다.

이를 긴편하게 풀기 위한 방법으로 클라이언트는 정의된 결제 서비스의 인터페이스만을 사용하고 결제를 수행할 때 클라이언트가 결제 모듈을 결제 서비스에 주입함으로써 확장성을 가질 수 있습니다.

또한 테스트시에도 실존 객체가 아닌 mock 객체를 주입하여 보다 가볍게 테스트를 수행할 수 있다는 장점 또한 따라오게 됩니다.

이는 객체 지향의 SOLID 에서 DIP(Dependency Inversion Principle) 과 일맥 상통하며, 이러한 장점을 극대화 한 것이 바로 스프링에서 제공하는 IoC(Inversion of Control) 컨테이너입니다.

참고적으로 DIP(Dependency Inversion Principle) 에서 inversion 의 의미는 결제 모듈에 대한 의존성을 결제 서비스가 가지는게 아니고 클라이언트가 가지기 때문의 의존성의 역전 현상이 일어 났다고 표현합니다.

불필요한 객체 생성을 피하자

String 은 문자열 리터럴(String s = "sample")을 사용하면 동일한 값에 대해서는 캐싱하여 동일한 인스턴스를 제공합니다.

하지만 new String() 으로 인스턴스를 생성하면 매번 다른 인스턴스가 만들어져 캐싱을 못하므로 주의가 필요합니다.

Pattern 클래스는 정규식을 사용할 수 있도록 제공되는 클래스인데 생성 비용이 비싸므로 캐싱하여 사용하는게 좋습니다.

오토 박싱(Auto Boxing) 과정에서 불필요한 임시 객체가 생성되어 성능이 안 좋을 수 있으니 사용되지 않도록 주의할 필요가 있습니다.

자기 메모리를 직접 관리하는 클래스는 메모리릭에 유의하자

스택, 캐시, 리스너와 같이 클래스 필드로 데이터셋을 관리하는 클래스들은 객체 사용 후 레퍼런스에 대한 해제를 명시적으로 관리를 해야합니다.

가령, 스택과 같은 경우는 pop() 호출시 반환 객체가 위치한 position에 null을 설정함으로써 명시적으로 레퍼런스를 제거하거나

캐시의 경우는 weak reference 를 사용하거나 LRU(Lateast Recently used) 캐시를 사용하거나

기타, ShceduledExcutorService 를 활용하여 직접 reference를 관리하는 방법이 있습니다.

다만, reference에 직접 null을 설정하는것보다 객체의 scope 밖으로 유도하는것을 추천합니다. (이는 명시적으로 null 설정이후에 발생할 수 있는 개발자의 실수를 방지하기 위함으로 추측됩니다)

그리고 weak reference의 경우에 entry의 value보다 key를 더 보장하고 싶을 때 사용하는데, WeakHashMap 의 경우 key를 primitive 타입으로 사용하면 jvm 자체적으로 캐싱을 하기 때문에 GC 대상이 되지 않습니다.

만일 List 자료구조에 Weak Reference 를 활용하여 객체를 GC 대상에 넣고 싶다고 하다라도, 이는 WeakHashMap 의 기능이기 때문에 직접 구현해야 합니다.

객체 소멸을 보장하자

finalizercleaner 로 객체 소멸시에는 관리하는 리소스의 반환 작업을 수행할 수 있습니다.

하지만 finalizercleaner 가 즉시 수행된다는 보장이 없으며 수행이 안 될수도 있습니다.

또한 finalizer 를 사용하는 경우에는 finalizer attack 을 당할 수도 있기 때문에 이를 보완하도록 개발이 되어야 합니다.

따라서 자바 9 이상부터는 AutoCloseable 을 사용하여 리소스를 반환하도록 권장합니다.

AutoCloseableimplements 한 구현체는 try-with-resources 구문을 사용할 수 있습니다.

try-with-resources 구믄을 사용하면 finally 를 사용하지 않더라도 객체 소멸시 close() 를 호출하여 매모리를 정리할 수 있도록 해줍니다.

또한 exception 발생시 발생한 모든 exception 을 subpress 로 담아 표시해주는 장점도 있습니다.

객체 비교를 잘 하자 (feat. Equals)

자바의 최상위 클래스인 Object 에는 객체 비교를 위한 equals() 가 존재합니다.

Object 에 구현된 equals()객체의 참조값이 동일한지(물리적 동치성)에 대해서만 확인 하고 값에 대해 비교는 하지 않기 때문에 때로는 개발자의 의도와는 다르게 비교 행위가 일어납니다.

이에 개발자가 값에 대한 비교 특성(논리적 동치성)을 주기 위해 equals()@Overide 하여 구현할 수 있는데요.

모든 클래스에 대해서 equals() 를 @Overide 하는 갓 또한 쉽지 않은 일이기 때문에 여러가지 편의 장치들이 존재합니다.

구글 라이브러리(AutoValue)를 활용하거나 IDE 에서 제공하는 자동완성 기능을 사용하거나 롬복을 사용하거나 자바 17부터 record 를 활용할 수 있습니다.

물론 모든 경우에 equals() 를 @Overide 해야하는건 아닙니다. 논리적 동치성을 검사할 필요가 없거나 인스턴스가 유일하거나 프로그램 코드내에 비교하는 구문이 없을 경우에는 굳이 equals() 를 @Overide 구현할 팔요가 없습니다.

그렇다면 어떻게 equals() 구현야 잘 구현한 것일까요?

다음 5가지 조건을 만족하면 됩니다.

  • 반사성
  • 대칭성
  • 추이성
  • 일관성
  • null 아님

반사성은 동일한 객체를 비교할 때 참으로 나오는가를 확인하면 됩니다.

대칭성은 A, B 두 객체간 상호 비교의 값이 일치하면 됩니다.

추이성은 A, B, C 세 객체간 상호 비교의 값이 일치하면 됩니다.

일관성은 동일한 비교 연산을 반복적으로 수행하더라도 동일한 결과를 반환하면 됩니다.

null 아님은 객체와 null 비교시 거짓을 반환하면 됩니다.

위의 다섯 가지 조건 중 추이성을 지키기가 가장 까다로운데요.

상속받은 클래스에서 필드를 추가하면 추이성을 절대 지킬 수 없고, 구현한 코드에 따라 무한루프리스코프 치환법칙이 위배되는 경우가 있기도 합니다.

이럴 때는 Composition pattern을 적용하여 문제를 해결할 수 있습니다.

이러한 5가지 원칙을 준수하면서 적정 수준의 편의성을 가져가기 위해서 롬복을 사용하는것을 추천합니다.

추가적으로, HashMap 과 같이 객체의 hash 를 사용하는 경우에 성능 저하를 막기 위해서 hashcode() 또한 override 해야합니다.

HashMap 의 경우 object 를 키로 활용할 때 해당 object 의 hashcode 를 가져와 키 값으로 활용하는데요.

이 때 해당 클래스의 hashcode() 가 override 되어있지 않다면 O(1) 의 성능을 낼 수 있는 HashMap.get() 함수가 중복된 키에 대해서 O(n) 으로 성능이 저하되게 됩니다.

자바 8 부터는 HashMap 클래스가 개선되어 중복 키에 대해서 List 로 관리되던 방식에서 이진 트리 로 변경되어 O(logN) 으로 성능이 향상되었지만, 중복 키가 발생되지 않을 때의 성능이 가장 좋기 때문에 hashcode() 를 override 하여 중복 키가 발생하지 않도록 해주는게 좋습니다.

그랗다면 hashcode() 를 어떻 조건을 만족시켜야 할까요?

equals() 에 사용히는 값이 변경되지 않았다면, hashcode는 매번 같은 값을 반환해야 합니다.

두 객체에 대한 equals() 가 같다면 hashcode 또한 같아야 합니다.

두 객체에 대한 equals() 가 다르더라도 hashcode 는 같을 수 있지만 HashTable 의 성능을 고려해 다름 값을 가지도록 해야합니다.

hashcode() 의 구현은 equals() 와 마찬가지로 구글 라이브러리(AutoValue)를 활용하거나 IDE 에서 제공하는 자동완성 기능을 사용하거나 롬복을 사용 수 있지만 그 중 롬복을 활용하는것을 추천합니다.

toString() 은 항상 재정의 하자

Object 에서 제공하는 toString() 은 16진수의 hashcode 만 제공하기 때문에 유용한 정보를 포함하지 않습니다.

따라서 toString() 을 재정의하여 사람이 읽기 쉽게 간결한 형태로 반환해주는게 좋습니다.

이 때 모든 정보를 포함하기 보다는 노출 가능한 정보만 표현해주는게 좋습니다.

만일 값 클래스라면 포맷을 문서에 명시하여 정보를 명확히 표현해주는게 좋고, 해당 포맷으로 객체를 생상할 수 있도록 정적 팩토리나 생성자를 제공해주는게 좋습니다.

마지막으로 포맷의 각 데이터들을 조회할 수 있도록 getter 를 제공하는것이 좋습니다.

hashcode() 의 구현은 equals() 와 마찬가지로 구글 라이브러리(AutoValue)를 활용하거나 IDE 에서 제공하는 자동완성 기능을 사용하거나 롬복을 사용 수 있지만 직접 필요한 정보를 구현하는것을 추천합니다.

객체 복사를 잘하자

자바에는 Primitive typeReference type이 있습니다.

두 타입은 객체를 복사할 때 다른 특성을 보이는데요. Primitive type 은 값을 복사하고 Reference type 은 참조수를 증가시킵니다.

이러한 특성에 따라 Primitive type 의 복사본은 원본과 물리적으로 다른 객체로 만들어 지지만 Reference type 의 복사본은 원본과 같은 객체를 참조하여 변경에 대한 결과를 공유하는 특징을 가집니다.

때로는 Reference type 의 복사본을 참조값이 아닌 물리적으로 복사를 해야하는 경우가 있는데요.

이 때 Cloneable 를 활용하거나 생성자 를 활용하여 객체 복사를 할 수 있습니다.

불변 객체의 경우에는 Cloneable 인터페이스를 구현하여 shallow copy 로도 충분하지만 (이 때, super.clone() 을 사용해야 합니다.)

가변 객체의 경우에는 deep copy 할 수 있도록 말단의 값까지 명시적으로 복사작업을 수행해야 합니다.

가변 객체를 복사하기 위해서는 많은 고려사항이 존재하기 때문에 생성자에서 복사를 하도록 구현하는것이 간단하게 문제를 해결할 수 있습니다.

객체 비교를 잘하자 (feat. Compare)

앞서 객체 비교를 잘하는 방법에 대해 equals 를 기반 설명이 있었는데요. 이번에는 compare 를 기반으로 객체를 비교하는 방법에 대해 알아보는 시간입니다.

Equals 와 비교하여 compare 의 차이점은 동등 비교뿐만아니라 부등호에 대한 비교를 해준다는 것과 컴파일타임에 오류를 찾아낼 수 있는 제네릭을 사용한다는 점입니다.

Compare를 구현하는 방법에는 comparable 을 구현하는 방법과 comparator 를 사용하는 방법이 있습니다.

무엇을 사용하든 지켜야할 규약이 있는데요. 반사성, 대칭성, 추이성, 일반성 입니다. 이는 앞서 equals 에서 다뤘던 내용과 동일합니다.

자바 8부터는 interface 에 defualt, static 함수를 정의할 수 있게 변경이 됐는데요.

이를 활용해서 comparator 인터페이스기 제공하는 static 함수를 사용하여 comparator 를 natual order 를 제공하는 객체에서 제공하고 이후 comparable 을 implements 하여 comparator 를 사용하는 방식으로 구현할 수 있습니다.

이렇게하면 comparator 가 제공하는 함수 체이닝을 활용하여 보다 가독성 높게 구현할 수 있어 추천합니다.

느낀점

출퇴톼근시간 틈틈히 보느라 오래 걸린거 같습니다. 처음에 강의가 14강까지 있는것을 보고 언제 다보나... 하는 생각을 했었는데요.

조금씩 꾸준히 보다보니 완강하는 날이 오긴하네요.ㅎㅎ

사실 강의의 내용들을 한번에 모두 이해했다고 하기에는 무리가 있는거 같습니다. 각 챕터의 내용을 내 것으로 만들어 설명한다는게 쉽지 않은 일이니까요.

반복적으로 들어 내 본연의 개발 사상으로 만들어가야할 것같습니다.

이번 이펙티브 자바 완벽 공략 1부 의 초범은 객체의 생애주기에 대한 관리입니다.

객체를 생성하고 소멸하고 비교하고 복사하는 행위가 대부분이었으니까요.

이러한 개념들이 어떻게 보면 너무나도 당연하고 쉬워 보이지만 이제라도 제가 느꼈던 부분은 객체에 요구사항을 투영하는것을 너무 소홀이 했다는 것입니다.

프로그래머의 의도에 맞게 구현하여 사용자가 의도에 맞게 사용할 수 있도록 제공해야한다는 부분이 가장 크게 와닿았습니다.

본 강의를 보고 그 동안 개발했던 코드를 보면서 명확하지 않은 의도로 짜여진 코드가 만들어낼 수 있는 혼잡함만 해결해도 굉장히 좋은 코드를 만들 수 있겠다는 생각이 듭니다.

또한

왜 hashcode 와 equals 를 동일선상에서 변경해줘야하는지,

객체의 비교나 복사시에 고려해야할 사항들은 무엇인지 등등 프로그래밍의 기본 개념을 느낄 수 있는 시간이었습니다.

본 강의는 좋은 코드를 고민하는 중급자 이상의 개발자분들에게 추천 합니다!!

Comments