diff --git a/chapter2/tmddus2/README.md b/chapter2/tmddus2/README.md deleted file mode 100644 index 3670527..0000000 --- a/chapter2/tmddus2/README.md +++ /dev/null @@ -1,434 +0,0 @@ -# 2. 객체 생성과 파괴 - - -## 생성자 대신 정적 팩터리 메서드를 고려하라 -#### 정적 팩터리 - -클라이언트가 클래스의 인스턴스를 얻는 전통적인 방법은 `public 생성자`이다. -생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다. - -- 정적 팩터리 메서드 - - 그 클래스의 인스턴스를 반환하는 단순한 정적 메서드 - -``` java -public static Boolean valueOf(boolean b) { - return b ? Boolean.TRUE : Boolean.FALSE; // 기본 타입인 boolean을 받아서 Boolean 객체 참조로 변환 -} -``` - -이 방식의 장점 5가지를 알아보자. - - -- 이름을 가질 수 있다. - - 생성자로 인스턴스를 생성할 때, 이 객체의 특성을 표현할 방법은 매개변수와 생성자 자체뿐 - - 정적 팩터리는 이름을 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다. - - `BigInteger(int, int, Random)` VS `BigInteger.probablePrime` - - 생성자 오버로딩을 통해서 여러 메서드를 만들 수 있지만, 하나의 시그니처로는 하나의 생성자만 만들 수 있고 매개변수 순서를 다르게 해서 새로운 생성자를 추가하는 건 좋지 않은 방식 - - 정적 팩터리 메서드로 그 특성이 잘 드러나는 이름을 붙여주자 - -- 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다. - - 인스턴스를 미리 만들어 놓거나 인스턴스를 캐싱해서 재사용 할 수 있도록 한다. - - `Boolean.valueOf(boolean)` 메서드는 객체를 아예 생성하지 않는다. - - 반복되는 요청에 같은 객체 반환 -> 언제 어떤 인스턴스를 반환하게 할 것인지 컨트롤 할 수 있다. 이런 클래스를 인스턴스 통제(instance-controlled) 클래스라 한다. - - 싱글턴으로 클래스를 만들 수도 있다. - - 인스턴스화(noninstantiable) 불가로 만들 수도 있다. - - 불변 값 클래스에서 동치인 인스턴스가 단 하나뿐임을 보장한다. - - **a == b 일 때만 a.equals(b)가 성립하도록 보장할 수 있다.** - -- 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다. - - 엄청난 유연성!! 반환할 객체 클래스를 자유롭게 선택 가능하다. - - **자바 8부터는 인터페이스가 정적 메서드를 가질 수 있다.** - -- 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다. - - 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환해도 상관 없다. - - 예를 들어 `EnumSet` 클래스는 정적 팩터리 메서드로 객체 생성을 지원하는데, 하위 클래스인 `RegularEnumSet`과 `JumboEnumSet` 만 객체를 생성해 리턴해주고 있다. - - 클라이언트는 둘의 차이점 몰라도 된다. EnumSet 하위 클래스면 어차피 쓰는데 똑같다. - -- 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다. - - **서비스 제공자 프레임워크를 만드는 근간이 된다.** - - 클라이언트는 API를 통해서 원하는 구현체의 조건을 명시할 수 있고, 서비스 제공자 프레임워크는 이에 맞춰서 구현체를 반환해준다. - - **JDBC에서는 Connection이 서비스 인터페이스 역할을, DriverManager.registerDriver가 제공자 등록 API 역할을, DriverManager.getConnection이 서비스 접근 API 역할을, Driver가 서비스 제공자 인터페이스 역할을 수행한다.** - - -
- -단점도 있다. - -- **상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.** -- 정적 팩터리 메서드를 프로그래머가 찾기 어렵다 - - API 문서를 잘 써야 하고, 메서드 이름도 알려진 규약을 따라서....
- `from`, `of`, `valueOf`, `instance`, `getInstance` ... - - -
- -> 정적 팩터리 메서드와 생성자는 각각의 장단점이 있다. 그래도 굳이 따지자면 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무조건 생성자만 썼던 습관이 있으면 고쳐라. - -
-
-
-
- -## 생성자에 매개변수가 많다면 빌더를 고려하라 -#### 빌더 - -선택적 매개변수가 많아질 때, 정적 팩터리와 생성자 모두 유연한 대응이 어렵다. -기존에는 점층적 생성자 패턴(telescoping constructor pattern)을 즐겨 사용했다. - -- 점층적 생성자 패턴 - - 선택 매개변수를 하나씩 늘려가며 선언한다. - - 매개변수가 많아질수록 코드를 작성하거나 읽기 어렵다. - -이런 문제 때문에 선택 매개변수가 많을 때 두 번째 대안인 자바빈즈 패턴(JavaBeans pattern)이 있다. - -- 자바빈즈 패턴 - - 매개변수 없는 생성자로 객체 만든 뒤에, setter로 값 넣어준다. - - 점층적 생성자 패턴의 단점들이 사라졌다. - - 그러나 객체 하나를 만들기 위해서 여러 메서드를 호출해야 하고, 객체가 완전히 완성되기 전까지는 일관성이 무너진다는 단점이 있다. - - **일관성 문제 때문에 자바빈즈에서는 클래스를 불변으로 만들 수 없다.** - - 이런 단점을 보완하고자 객체를 `freezing` 하기도 하나, 이 객체가 freezing 된 건지 아닌지 확인할 방법이 없기에 거의 쓰지 않는다. - -점층적 생성자 패턴의 `안전성`과 자바빈즈 패턴의 `가독성`을 겸비한 빌더 패턴(Builder pattern)이 있다. - -- 빌더 패턴 - - 필요한 객체를 직접 만드는 게 아니라, 생성자를 호출하되 필수 매개변수만으로 호출한다. - - 그 다음 빌더 객체가 제공하는 setter 메서드들로 선택 매개변수를 설정한다. - - 그 다음 `build` 메서드를 호출해 우리에게 필요한 객체를 얻는다. - - 보통 `Builder`는 생성할 클래스 안에 정적 멤버 클래스로 만들어둔다 - - Spring에서는 Lombok이 `@Builder` 어노테이션 제공해준다. - - **계층적으로 설계된 클래스와 함께 쓰기 좋다** - - -빌더 패턴에 장점도 있지만 단점도 있다. -- 빌더부터 만들어야 한다. 비용이 크지 않아도 상황에 따라 예민할 수 있다. -- 매개변수가 많아져야 유리하다. 그러나 API는 시간이 지날수록 매개변수가 늘어나는 경향이 있다. 애초에 빌더로 시작하는 편이 나을 때가 많다. - - -
- -> 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 더더욱 그렇다. 빌더는 점층적 생성자보다 코드를 읽고 쓰기 쉽고, 자바빈즈보다 훨씬 안전하다. - -
-
-
-
- -## private 생성자나 열거 타입으로 싱글턴임을 보증하라 - -싱글턴(singleton)이란 `인스턴스를 오직 하나만 생성할 수 있는 클래스`를 말한다.
-**예시:함수와 같은 무상태(stateless) 객체, 설계상 유일해야 하는 시스템 컴포넌트** - - -싱글턴 클래스는 클라이언트가 테스트하기 어려울 수 있다. -- 인터페이스로 정의하고 인터페이스를 구현해서 만든 싱글턴이 아니라면 구현체를 바꿀 수 없기 때문에 mock으로 대체해야 한다. - - -싱글턴 만드는 방식은 둘 중 하나다. -두 방식 모두 생성자는 private으로 감춰두고, 유일하게 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 마련해둔다. - -- public static 멤버가 final 필드인 방식 - ```java - public class Elvis { - public static final Elvis INSTANCE = new Elvis(); // private 생성자는 이때 딱 한 번 호출된다. - - private Elvis() { } // 생성자는 private으로 막아두기 - } - ``` - - 간결하다. - - `public static final` 만 봐도 싱글턴임이 명백히 드러난다. - - -- 정적 팩터리 메서드를 public static 멤버로 제공 - ```java - public class Elvis { - private static final Elvis INSTANCE = new Elvis(); - private Elvis() {} - public static Elvis getInstance() { return INSTANCE; } // 항상 같은 객체의 참조를 반환 - } - ``` - - API 바꾸지 않고도 싱글턴 아니게 변경할 수 있다. 싱글턴에서 변형을 주어, 호출하는 스레드별로 다른 인스턴스를 넘겨주게 할 수도 있다. - - **원한다면 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다.** - - **정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다.** - -**둘 중 하나의 방식으로 만든 싱글턴 클래스를 직렬화하려면 단순히 Serializable을 구현한다고 선언하는 것만으로는 부족하다. 모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 한다.** - -싱글턴을 만드는 세 번째 방법은 원소가 하나인 열거 타입을 선언하는 것이다. - -- 원소가 하나인 열거 타입을 선언 - ```java - public enum Elvis { - INSTANCE; - - public void leaveTheBuilding() {} - } - ``` - - 제 2의 인스턴스가 생기는 일을 완벽하게 막아준다. - - **조금 부자연스러워 보일 수는 있으나 대부분 상황에서 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.** - -
-
-
-
- -## 인스턴스화를 막으려거든 private 생성자를 사용하라 -#### 정적 유틸리티 클래스 - -클래스 중에서 정적 메서드와 정적 필드만을 담은 클래스가 있다. 즉, 인스턴스로 만들어 쓰려고 설계한 게 아닌 클래스다. - -- `java.lang.Math`, `java.util.Arrays`, `java.util.Collections` - - 특정 값이나 메서드들 모아두기 -- final 클래스와 관련 메서드들 모아 둔다. - -이때 문제가 인스턴스화를 의도하지 않았기 때문에 인스턴스화가 되면 안된다. -그러나 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들어주어, 매개변수가 없는 public 생성자가 생기게 된다. - -이럴 때는 private 생성자를 추가해서 클래스의 인스턴스화를 막을 수 있다. -명시적 생성자가 private이라 밖에서 접근할 수 없고, 명시적 생성자가 있어서 기본 생성자가 생기지 않는다. - -> 이렇게 인스턴스화 막아버리면 상속이 불가능하다. 상속할 때 부모 클래스의 생성자를 호출해야 하는데 private으로 막혀있기 때문 - -
-
-
-
- -## 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 - -사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식이 적합하지 않다. -클래스가 여러 자원 인스턴스를 지원해야 하며, 클라이언트가 원하는 자원을 사용해야 한다. -이를 만족할 수 있는 패턴이 바로, 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 방식! -의존 객체 주입의 한 형태다. - -```java -public class SpellChecker { - private final Lexicon dictionary; - - public SpellChecker(Lexicon dictionary) { - this.dictionary = Objects.requireNonNull(dictionary); - } - - public boolean isValid(String word) {} - public List suggestions(String type) {} -} -``` - -이처럼 `dictionary` 를 사용하는 클라이언트 안에서 인스턴스를 가져오는 것이 아니라, 인스턴스가 생성될 때 생성자에 필요한 자원을 넘겨준다. - -**이 패턴이 변형된 게, 생성자에 `자원 팩터리`를 넘겨주는 방식이다.** - -- 팩터리 - - 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체 - - **팩터리 메서드 패턴** 을 구현한 것 - -의존 객체 주입이 유연성과 테스트 용이성을 개선해주지만, 큰 프로젝트에서는 의존성이 너무 많아 관리가 어렵다. -이는 스프링 같은 `의존 객체 주입 프레임워크`를 사용하면 해소할 수 있다. - -
- -> **클래스가 내부적으로 하나 이상의 자원에 의존하고, 그 자원이 클래스 동작에 영향을 준다면 싱글턴과 정적 유틸리티 클래스는 사용하지 않는 것이 좋다.** -> **이 자원들을 클래스가 만들게 해서도 안 된다.** -> 대신 필요한 자원을 생성자에 넘겨주자. 의존 객체 주입이라는 기법이 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다. - -
-
-
-
- -## 불필요한 객체 생성을 피하라 - -똑같은 기능의 객체를 매번 생성하지 말고 재사용 하자. 특히, 불변 객체라면 언제든 재사용할 수 있다. - -``` java -String s = new String("devocean young"); // ❌ 절대 금물해야 할 예시!! ❌ - -String s = "devocean young"; // 개선된 버전 -``` - -`new String`은 실행될 때마다 매번 새로운 인스턴스를 만든다. 반복문 안에서 실행된다면, 쓸데없는 String 인스턴스가 수백만개 만들어질 수도 있다. - -개선된 버전의 경우, 매번 새로운 인스턴스를 만드는 대신 하나의 String 인스턴스를 사용한다. **이 방식을 쓰면, 같은 가상머신 안에서 이와 똑같은 문자열 리터럴을 사용하는 모든 코드가 같은 객체를 재사용함이 보장된다.** - - -불변 클래스에서 생성자 대신에 정적 팩터리 메서드를 제공한다면, 정적 팩터리 메서드를 사용해 불필요한 객체 생성을 피할 수 있다. - -- `Boolean(String)` 생성자 - - 호출할 때마다 새로운 객체를 만든다. -- `Boolean.valueOf(String)` 팩터리 메서드 - - 불변 객체와 가변 객체 상관 없이 사용 중에 변경되지 않을 것이라면 재사용 가능 - -
- -값비싼 객체를 재사용해 성능을 개선할 수도 있다. -매번 필요할 때마다 인스턴스를 생성하면 생성 비용이 높다. -이런 인스턴스는 클래스 초기화(정적 초기화) 과정에서 직접 생성해 캐싱해두고, 나중에 호출될 때마다 재사용 하도록 한다. - -```java -public class RomanNumerals { - private static final Pattern ROMAN = Pattern.compile("^(?=.)M*(C[MD]|D?C{0,3})"); - static boolean isRomanNumeral(String s) { - return ROMAN.matcher(s).matches(); - } -} -``` - -`isRomanNumeral`이 빈번히 호출되는 상황에서 성능을 상당히 끌어올릴 수 있다. - -
- -**재사용 관련 예시들** -- Map 인터페이스의 keySet 메서드 -- 오토박싱 - ``` java - private static long sum() { - Long sum = 0L; - for (long i=0; i<= Integer.MAX_VALUE; i++) - sum += i; - return sum; - } - ``` - long을 쓰지 않고 Long을 사용하는 바람에 불필요한 Long 인스턴스가 2의 31승 개나 만들어졌다. - - > 박싱된 기본 타입보다는 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하자! - - -



- -## 다 쓴 객체 참조를 해제하라 - -자바처럼 가비지 컬렉터가 있는 언어는 메모리 관리에 신경쓰지 않아도 된다는 게 아니다. 절대절대. - -```java -public class Stack { - private Object[] elements; - private int size = 0; - private static final int DEFAULT_INITIAL_CAPACITY = 16; - - public Stack() { - elements = new Object[DEFAULT_INITIAL_CAPACITY]; - } - - public void push(Object e) { - ensureCapacity(); - elements[size++] = e; - } - - public Object pop() { - if (size == 0) { - throw new EmptyStackException(); - } - return elements[--size]; - } - - private void ensureCapacity() { - if (elements.length == size) { - elements = Arrays.copyOf(elements, 2*size+1); - } - } -} -``` - -위 코드는 메모리 누수가 일어나는 코든데 어디서 일어날까? -문제는 스택에서 꺼내진 객체들을 가비지 컬렉터가 회수하지 않는다. 스택이 그 객체들의 다 쓴 참조를 여전히 가지고 있는 게 문제! - -가비지 컬렉션 언어에서는 객체 참조 하나를 살려두면, 가비지 컬렉터는 그 객체뿐 아니라 그 객체가 참조하는 모든 객체를 회수해가지 못한다. - -해결하는 방법은 참조를 다 썼을 때 null 처리(참조 해제)를 하면 된다. - - -``` java -public Object pop() { - if (size == 0) - throw new EmptyStackException(); - Object result = elements[--size]; - elements[size] = null; // 다 쓴 참조 해제 - return result; -} -``` - -null로 참조 해제를 처리해두면, 나중에 해당 값으로 잘못된 접근을 하려고 해도 NullPointerException이 발생한다. -다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효범위 밖으로 밀어내는 것이다. - -메모리 누수의 주범들은 다음과 같다. - -- 참조 해제 하지 않은 경우 -- 캐시도 메모리 누수를 일으키는 주범이다. 객체 참조를 캐시에 넣고 나서, 잊어버리는 경우가 많다. -- **리스너(listener) 혹은 콜백(callback)** - - **클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면 콜백이 계속 쌓인다.** - - **이때 콜백을 약한 참조(weak reference)로 저장하면 가비지 컬렉터가 즉시 수거해간다. 예를 들어서 WeakHashMap에 키로 저장하면 된다.** - -
- -> 메모리 누수는 빨리 드러나지 않을 수도 있기에 사전에 예방하는 게 중요하다. 예방법을 익혀두자. - -
-
-
-
- -## finalizer와 cleaner 사용을 피하라 - -자바가 제공하는 객체 소멸자는 두 가지가 있다. - -- finalizer - - 예측할 수 없고, 상황에 따라 위험할 수 있다. - - 일반적으로 불필요하다. -- cleaner - - finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리다. - - 일반적으로 불필요하다. - - - -finalizer와 cleaner의 단점들은 다음과 같다. - -- finalizer와 cleaner는 즉시 수행된다는 보장이 없다. 즉, 제때 실행되어야 하는 작업은 할 수 없다. - - 파일 닫기를 finalizer나 cleaner에게 맡긴다면, 시스템이 동시에 열 수 있는 파일 개수에 한계가 있어서 얼른 닫아줘야 하는데 안 닫고 있다가 중대한 오류가 날 수도 있다. - - 이를 수행하는 순서도 `가비지 컬렉터`의 알고리즘마다 다르다. JVM마다 성능이 천차만별일 수 있다. - -- finalizer와 cleaner는 수행 시점뿐 아니라 수행 여부도 보장해주지 않는다. - - 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다. -- 심각한 성능 문제 동반 -- finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있다. - - **final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.** - - -**finalizer와 cleaner 대신, `AutoCloseable`을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다.** - - -
- -finalizer와 cleaner는 그럼 어디에 쓸까? - -- 자원의 소유가자 close 메서드를 호출하지 않았을 때에 대비한 안전망 - - 즉시 호출된다느 보장은 없어도, 클라이언트가 안 하는 것보다 늦게라도 하는 게 낫다. - - 그만한 값어치가 있는지 고려해봐야 함 -- 네이티브 피어(native peer)와 연결된 객체에서 - - 네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다. 네이티브 피어는 자바 객체가 아니라 가비지 컬렉터가 존재를 모른다. - - 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못한다. - - 성능 저하를 감당할 수 있으면서, 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당. 아니라면 close 메서드를 사용해야 한다. - -
- -> cleaner는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 이런 경우에도 불확실성과 성능 저하에 주의해야 한다. - -
-
-
-
- -## try-finally 보다는 try-with-resources 를 사용하라 - -자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다. -`InputStream`, `OutputStream`, `java.sql.Connection` 등,, - -전통적으로는 자원이 제대로 닫히는 것을 보장하기 위해서 `try-finally`가 쓰였다. -자원이 닫히는 것뿐 아니라 예외 발생, 메서드 반환 같은 경우도 포함된다. - -`try-finally`는 try 안에서도 에러가 발생할 수 있고, finally 안에서도 에러가 발생할 수 있는데 둘 다 발생하게 되면 두 번째 예외가 첫 번째 예외를 덮게 된다. -디버깅이 굉장히 까다로워진다. - -
-이런 문제를 해결하기 위해 `try-with-resources` 를 사용해보면 좋다. catch 절도 같이 쓸 수 있다. - -
- -> 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자. 예외가 없다! \ No newline at end of file