📌 1장 : 들어가기
컴포넌트 : 개별 메서드부터 여러 패키지로 이뤄진 복잡한 프레임워크까지 재사용 가능한 모든 소프트웨어 요소
자바 자료형(type)
참조 타입(reference type) : 인터페이스, 클래스, 배열
기본 타입(primitive type)
annotation : 인터페이스의 일종
enum : 클래스의 일종
📌 2장 : 객체 생성과 파괴
아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라
생성자 : 클라이언트가 클래스의 인스턴스를 얻는 수단
클래스는 생성자 대신에 정적 팩토리 메소드(static factory method)를 제공할 수 있다.
정적 팩토리 메소드의 장점
이름을 가질 수 있다.
생성자보다 객체의 특성을 이름으로 묘사할 수 있다.
호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.
인스턴스 통제 클래스 : 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지 통제할 수 있다.
인스턴스를 통제하면, 클래스를 싱글톤 패턴 or 인스턴스화 불가로 만들 수 있다.
또한, 불변 값 클래스에서 동치 인스턴스가 단 하나임을 보장할 수 있다.
ex. a == b 일 때, a.equals(b)가 성립
반환 타입의 하위 타입 객체를 반화할 수 있는 능력이 있다.
구현 클래스를 공개하지 않아도, 그 객체를 반환할 수 있어 API 생성을 할 때 작게 유지 가능하다.
입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
정적 팩토리 메소드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
서비스 제공자 프레임워크 3개의 핵심 컴포넌트
서비스 인터페이스(service interface) : 구현체의 동작을 정의
제공자 등록 API(provider registration API) : 제공자가 구현체를 등록할 때 사용
서비스 접근 API(service access API) : 클라이언트가 서비스의 인스턴스를 얻을 때 사용
종종 서비스 제공자 인터페이스(service provider interface)라는 네 번째 컴포넌트도 쓰인다.
서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명해준다.
ex. JDBC
서비스 인터페이스 : Connection
제공자 등록 API : DriverManager.registerDriver
서비스 접근 API : DriverManager.getConnection
서비스 제공자 인터페이스 : Driver
정적 팩토리 메소드 패턴의 단점
상속을 하려면 public, protected 생성자가 필요하다. 정적 팩토리 메소드만 제공하면 하위 클래스를 만들 수 없다.
정적 팩토리 메소드는 프로그래머가 찾기 어렵다.
알려진 규약을 따라 이름을 짓는 것으로 문제점을 완화한다.
from : 매개 변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드 Date d = Date.from(instant);
of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드 Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
valueOf : from과 of의 자세한 버전 BigInteger prime = BingInteger.valueOf(Integer.MAX_VALUE);
instance 혹은 getInstance : (매개 변수를 받으면) 매개 변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는다. StackWalker luke = StackWalker.getInstance(options);
create 혹은 newInstance : instance, getInstance 와 같지만 매번 새로운 인스턴스를 생성해 반환한다. Object newArray = Array.newInstance(classObject, arrayLen);
getType : getInstance 와 같지만, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. FileStore fs = Files.getFilesStore(path);
newType : newInstance 와 같지만, 생성할 클래스가 아닌 다른 클래스에 팩토리 메소드를 정의할 때 쓴다. BufferReader br = Files.newBufferReader(path);
type : getType 과 newType 의 간결한 버전 List<Complaint> litany = Collections.list(legacyLitany);
아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라
정적 팩토리, 생성자는 둘 다 선택적 매개변수가 많으면 적절히 대응하기 힘들다.
점층적 생성자 패턴(telescoping constructor pattern)
필수 매개변수만 받는 생성자, 필수 매개변수와 선택 매개변수 1개만 받는 생성자, ..., 형태들로 선택 매개변수를 전부 다 받는 생성자 까지 늘려가는 방식
점층적 패턴을 사용할 수 있지만, 매개변수의 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어려워진다.
선택 매개 변수가 많은 경우, 두 번째 대안 -> 자바빈즈 패턴(JavaBeans pattern)
매개변수가 없는 생성자로 객체를 만들고, setter 으로 원하는 매개변수 값을 설정하는 방식
하지만, 객체 하나를 만들기 위한 여러 메소드 호출의 필요성과 객체 생성 전까지 일관성이 무너진다.
세번째 대안, 빌더 패턴(Builder pattern)
점층적 생성자 패턴의 안전성 + 자바빈즈 패턴의 가독성
클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수 만으로 생성자(혹은 정적 팩토리)를 호출해 빌더 객체를 얻는다.
그 후에 빌더 객체가 제공하는 setter 으로 원하는 선택 매개변수를 설정
마지막으로, 매개 변수가 없는 build 를 호출해서 필요한 객체를 얻는다.
플루언트(fluent) API 혹은 메소드 연쇄(method chaining)
빌더의 setter 들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.
메소드 호출이 흐르듯 연결된다는 의미
public class NutritionFacts {
private final int servingSize;
private final int servings;
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
public static class Builder {
private final int servingSize;
private final int servings;
private int calories = 0 ;
private int fat = 0 ;
private int sodium = 0 ;
private int carbohydrate = 0 ;
public Builder (int servingSize, int servings) {
this .servingSize = servingSize;
this .servings = servings;
}
public Builder calories (int val)
{ calories = val; return this ; }
public Builder fatdnt val)
{ fat = val; return this ; }
public Builder sodium (int val)
{ sodium = val; return this ; }
public Builder carbohydrate (int val)
{ carbohydrate = val; return this ; }
public NutritionFacts build () {
return new NutritionFacts(this );
}
}
private NutritionFacts (Builder builder) {
servingSize = builder.servingSize;
servings = builder.servings;
calories = builder.calories;
fat = builder.fat;
sodium = builder.sodium;
carbohydrate = builder.carbohydrate;
}
}
빌더 패턴 사용한 클래스를 사용하는 클라이언트 코드 NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8) .calories(100).sodium(35).carbohydrate(27).build ();
잘못된 매개변수는 빌더의 생성자, 메소드에서 검사하는 것이 좋다.
불변성을 위해서는 빌더로부터 매개변수를 복사하고 해당 객체 필드들을 검사해야 한다.
어떤 매개 변수가 어떻게 잘못되었는지는 IllegalArgumentException를 사용한다.
빌더 패턴은 계층적으로 설계된 클래스와 함께 사용하기 좋다.
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
final Set<Topping> toppings;
abstract static class Builder <T extends Builder <T » {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public T addTopping (Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
abstract Pizza build () ;
protected abstract T self () ;
}
Pizza(Builder<?> builder) {
toppings = builder.toppings.clone();
}
}
아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라
싱글톤(singleton) : 인스턴스를 오직 하나만 생성할 수 있는 클래스
전형적인 예시 : 무상태(stateless) 객체, 설계상 유일해야 하는 시스템 컴포넌트
클래스를 싱글톤으로 만들면 클라이언트가 테스트하기 어려울 수 있다.
싱글톤을 만드는 방식 : 생성자는 private 으로 감추고, 유일하게 인스턴스에 접근할 수 있는 수단으로 public static 멤버를 하나 만든다.
방식1 : public static 멤버가 final 필드
방식2 : 정적 팩토리 메소드를 public static 멤버로 제공
장점 : API를 바꾸지 않아도 싱글톤이 아니게 변경 가능하다. 정적 팩토리를 제너릭 싱글톤 팩토리로 만들 수 있다. 정적 팩토리 메소드 참조를 공급자(supplier)로 사용할 수 있다.
장점이 필요하지 않으면 방법1이 더 좋다.
싱글톤 클래스 직렬화
Serializable 을 구현하는 것 만으로는 부족하다.
모든 인스턴스 필드를 일시적(transient)으로 선언하고, readResolve 메소드를 제공해야 한다.
이렇게 하지 않으면, 직렬화된 인스턴스를 역직렬화할 때마다 새로운 인스턴스가 만들어진다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis () { ... }
public void leaveTheBuilding () { ... }
}
public class Elvis {
private static final Elvis INSTANCE = new Elvis(); private Elvis ( ) { ... }
public static Elvis getlnstance ( ) { return INSTANCE; }
public void leaveTheBuilding ( ) { ... }
}
아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라
생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만든다.
추상 클래스로 만드는 것으로는 인스턴스화를 막을 수 없다.
컴파일러가 기본 생성자를 만드는 경우는 명시된 생성자가 없을 때 뿐이기 때문에
private 생성자를 추가하면 클래스의 인스턴스화를 막을 수 있다.
아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라
public class Spellchecker {
private static final Lexicon dictionary = ...;
private SpellChecker ( ) {}
public static boolean isValid (String word ) { ... }
public static List<String > suggestions (String typo ) { ... }
}
public class Spellchecker {
private final Lexicon dictionary = ...;
private Spellchecker (... ) {}
public static Spellchecker INSTANCE = new SpellChecker(••.);
public boolean isValid (String word ) { ... }
public List<String > suggestions (String typo ) { ... }
}
사용하는 자원에 따라 동작이 달라지는 클래스에는 유틸리티 클래스나 싱글톤 방식이 적합하지 않다.
인스턴스를 생성할 때 생성자가 필요한 자원 넘겨주는 방식
의존 객체 주입의 한 형태 : 유연성과 테스트 용이성을 높여준다.
public class Spellchecker {
private final Lexicon dictionary;
public Spellchecker (Lexicon dictionary ) {
this .dictionary = Objects.requireNonNull(dictionary);
}
public boolean isValid (String word ) { ... }
public List<String > suggestions (String typo ) { ... }
}
아이템 6. 불필요한 객체 생성을 피하라
생성자 대신 정적 팩토리 메소드를 제공하는 불변 클래스는 정적 패곹리 메소드를 사용해 불필요한 객체 생성을 피할 수 있다.
ex. Boolean(String) 생성자 대신에 Boolean.valueOf(String) 팩토리 메소드를 사용하는 것이 좋다.
String s = new StringC
String s = "bikini" ;
생성 비용이 아주 비싼 객체도 있다. 자신이 만드는 객체가 비싼 객체인지 알 방법이 명확하게 없기 때문에
정규 표현식을 사용해 성능을 끌어올릴 수 있다.
하지만, String.matches 메소드를 사용하기 때문에 성능이 중요한 상황에서 반복해 사용하기 적합하지 않다.
Pattern 인스턴스는 한 번 쓰고 버려지기 때문에 가비지 컬렉션 대상이 된다.
Pattern 은 입력받은 정규표현식에 해당하는 유한 상태 머신(finite state machine)을 만들기 때문에 인스턴스 생성 비용이 높다.
static boolean isRomanNumeraI(String s) {
return s.matchesC'^?#. ()*CM[MD] |D?C{0,3})"
+ "(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$");
}
성능을 개선하기 위해서, 필요한 정규 표현식을 표현하는 Pattern 인스턴스를 클래스 초기화 과정에서 직접 생성해 캐싱하고
나중에 메소드가 호출될 때마다 이 인스턴스를 재사용한다.
public class RomanNumerals {
private static final Pattern ROMAN = Pattern.compile(
u("*C(?[=M.)DM]|D?C{0,3})"
+ "(X[C니 |L?X{0,3})(I[XV]|V?I{0,3})$" );
static boolean isRomanNumeral (String s) {
return ROMAN.matcher(s).matches();
}
}
private static long s니m () {
Long sum = 0L ;
for (long i = 0 ; i <= Integer.MAX_VALUE; i++)
sum += i;
return sum;
}
불필요한 객체를 만드는 또 다른 예시 : 오토박싱(auto boxing)
박싱된 기본 타입보다 기본 타입을 사용하고, 의도치 않은 오토박싱이 숨어들지 않도록 주의하기
아이템 7. 다 쓴 객체 참조를 해제하라
메모리 누수가 일어나는 위치
stack은 element 배열로 저장소 풀을 만들고 원소들을 관리하기 때문에
가비지 컬렉션은 비활성 영역에 대해 알 수 없다.
비활성 영역이 되는 순간 null 처리 해준다.
public Object pop ( ) {
if (size = 0 )
throw new EmptyStackException();
return elements[—size];
}
public Object pop ( ) {
if (size = 0 )
throw new EmptyStackException();
Object result = elements[—size];
elements[size] = null ;
return result;
}
객체 참조를 null 처리하는 일은 예외적이어야 한다.
다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위(scope) 밖으로 밀어내는 것이다.
자기 메모리를 직접 관리하는 클래스는 항시 메모리 누수에 주의한다.
캐시도 메모리 누수를 일으키기 쉽다. 객체 참조를 캐시에 넣고 놔두는 경우가 있다.
weakHashMap 사용 : 다 쓴 entry는 즉시 자동 제거
Scheduled ThreadPoolExecutor 같은 백그라운드 스레드를 사용하거나 LinkedHashMap은 removeEldestEnry 메소드를 사용
아이템 8. finalizer 와 cleaner 사용을 피하라
자바의 두 가지 객체 소멸자
finalizer : 예측 불가능, 상황에 따라 위험할 수 있어 일반적으로 불필요
cleaner : finalizer보다 덜 위험하지만, 예측 불가능, 느리고 일반적으로 불필요하다.
finalizer, cleaner 로는 제때 실행되어야 하는 작업은 절대 할 수
상태를 영구적으로 수정하는 작업에서는 절대 finalizer, cleaner 에 의존해서는 안된다.
ex. 데이터베이스 같은 공유 자원의 lock 해제를 맡기면 분산 시스템 전체가 서서히 멈추게 된다.
finalizer 는 가비지 컬렉터의 효율을 약 50배 떨어뜨리기 때문에 성능 저하 문제가 있다.
finalizer, cleaner 대신에 사용할 방법 : AutoCloseable
AutoCloseable
클라이언트에서 인스턴스를 다 쓰고, close 메소드를 호출하면 된다.
아이템 9. try-finally 보다는 try-with-resources 를 사용하라
자바에서는 close 메소드를 호출해서 직접 닫아줘야하는 경우가 있다.
try-finally 를 사용해서 자원을 안전하게 닫을 수 있지만, 자원이 둘 이상이면 지저분해진다.
자바 7에서 try-with-resources 덕에 문제 해결
해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
static void copy (String src, String dst) throws lOException {
try (Inputstream in = new FilelnputStream(src);
Outputstream out = new FileOutputStream(dst)) {
byte [] buf = new byte [BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0 )
out.write(buf, 0 , n);
}
}
공유하기
URL 복사 카카오톡 공유 페이스북 공유 엑스 공유