[Effective Java] 1, 2장 객체 생성과 파괴

kkatal_chae·2022년 8월 20일
0

Effective Java

목록 보기
1/11
post-thumbnail

1장. 들어가기

책을 관통하고 있는 규칙들

  • 명료성과 단순성
  • 코드는 복사되는 게 아니라 재사용되어야 한다.
  • 컴포넌트 사이의 의존성은 최소로 유지해야 한다.

자바가 지원하는 타입

  • 인터페이스
  • 클래스
  • 배열
  • 기본 타입 ( Primitive type )

API ( application programming interface )

: 프로그래머가 클래스, 인터페이스, 패키지를 통해 접근할 수 있는 모든 클래스, 인터페이스, 생성자, 멤버, 직렬화된 형태

2장. 객체 생성과 파괴

아이템 1. 생성자 대신 정적 팩토리 메서드를 고려하라

클라이언트 ( 개발자 ) 가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자다. 하지만 모든 프로그래머가 꼭 알아둬야 할 기법이 하나 더 있다.

클래스는 생성자와 별도로 정적 팩토리 메서드 static factory method 를 제공할 수 있다.

정적 팩토리 메서드는 제공함으로써 얻을 수 있는 이점

1. 이름을 가질 수 있다.

⇒ 생성자는 어떤 객체를 반환하는지 알기 위해 API 문서를 참고해야하는 반면 정적 팩토리 메서드는 메서드명만으로 반환하는 객체를 유추해 볼 수 있다.

ex) BigInteger( int, int, Random ) vs BigInteger.probablePrime

2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

⇒ 반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩토리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다.

3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

5번과 같은 유연함 → 서비스 제공자 프레임워크

ex ) JDBC ( Java Database Connectivity ) : 서비스 제공자 프레임워크

서비스 제공자 프레임워크 핵심 컴포넌트

  • 서비스 인터페이스 ( Conncection )
  • 제공자 등록 API ( DriverManager.registerDriver )
  • 서비스 접근 API ( Drivermanager.getConnection )
  • 서비스 제공자 인터페이스 ( Driver )

단점

  1. 상속을 하려면 public 이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.
  2. 정적 팩토리 메서드는 프로그래머가 찾기 어렵다

💡 정적 팩토리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩토리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.

정적 팩토리 메서드 명명 방식

from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
valueOf : from과 of의 더 자세한 버전
instance / getInstance : 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지는 않는다.
create / newInstance : instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스 생성해 반환함을 보장한다.
getType : getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다.
newType : newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용한다.
type : getType과 newType의 간결한 버전

아이템 2. 생성자에 매개변수가 많다면 빌더를 고려하라

  • 점층적 생성자 패턴

자바 공식문서에서 어떤 클래스를 보면 생성자가 적은 매개 변수를 받는 부분부터 점점 많은 매개 변수를 받는 생성자가 나열되어 있는 것을 볼 수 있다.

이러한 방식을 점층적 생성자 패턴이라고 한다.

점층적 생성자 패턴은 매개변수 개수가 많아지면 코드를 작성하거나 읽기 어렵다. 또한 개발자가 실수로 매개변수의 순서를 바꾸면 컴파일러가 알아채지 못하고 런타임에서 의도하지 않은 동작이 일어나게 된다.

  • 자바빈즈 패턴 ( JavaBeans pattern )

: 매개변수가 없는 생성자로 객체를 만든 후, 세터 ( setter ) 메서드들을 호출해 원하는 매개변수의 값을 설정하는 방식이다.

점층적 생성자 패턴보다 인스턴스를 생성하기 쉽고, 더 읽기 쉬운 코드지만 자바빈즈 패턴도 심각한 단점을 가지고 있다.

단점

객체 하나를 만들기 위해 메서드 여러개를 호출해야 하고, 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓이기 된다는 점

💡 일관성이 깨진다
- 한 번 객체를 생성할 때, 그 객체가 변할 가능성이 있다는 것
- 즉, Setter 메소드에 의해서 악의적으로 각 필드값들이 변경된 가능성이 있다

⇒ 이러한 단점으로 인해 클래스를 불변으로 만들 수 없으며 스레드 안정성을 얻으려면 추가적인 작업이 필요하다.

💡 불변 클래스
변경이 불가능한 클래스이며, 가변적이지 않는 클래스이다. 레퍼런스 타입의 객체이기 때문에 heap영역에 생성된다.
대표적으로 String, Boolean, Integer, Float, Long 등등이 있음.

자바빈즈 클래스 예시

public class NutritionFacts {
	private int servingSize = -1;
	private int serving = -1;
	private int calories = 0;
	private int fat = 0;
	private int sodium = 0;

	public void setServingSize( int val ) { servingSize = val };
	public void setServing ( int val ) { serving = val };
	...
}

빌더 패턴

: 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자 혹은 정적 팩토리 메서드를 호출해 빌더 객체를 얻는다. 이후 빌더 객체가 제공하는 build 메서드를 호출해 필요한 객체를 얻는다.

빌더의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다. 이런 방식을 메서드 호출이 흐르듯 연결된다는 뜻으로 fluent API 또는 메서드 연쇄라고 한다.

빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.

빌더 패턴은 상당히 유연하다. 빌더 하나로 여러 객체를 순회하면서 만들 수 있고, 빌더에 넘기는 매개변수에 따라 다른 객체를 만들 수도 있다. 객체마다 부여되는 일련번호와 같은 특정 필드는 빌더가 알아서 채우도록 할 수도 있다.

💡 생성자나 정적 팩토리 메서드가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

빌더 패턴 사용 예시

public NutritionFacts {

	private final int servingSize;
	private final int serving;
	private final int calories;
	private final int fat;
	private final int sodium;

	public static class Builder {
		// 필수 매개변수 
		private final int servingSize;
		private final int serving;

		// 선택 매개변수 - 기본값으로 초기화 
		private int calories = 0;
		private int fat = 0;
		private int sodium = 0;
	
		public Builder( int servingSize, int serving) {
			this.servingSize = servingSize;
			this.serving = serving;
		}
		
		public Builder calories ( int val ) { calories = val; return this; }
		...
		
		public NutritionFacts build() {
			return new NutritionsFacts ( this );
		}

	private NutritionFacts( Builder builder ) {
		servingSize = builder.servingSize;
		serving = builder.serving;
		...
	}
}

아이템 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

싱글턴 ( Singleton )

인스턴스를 오직 하나만 생성할 수 있는 클래스

[Java] Singleton 생성 방법

싱글턴을 만드는 방식

  • public static 멤버가 final 필드인 방식

private 생성자는 public static final 필드인 객체.INSTANCE 를 초기화할 때 딱 한 번만 호출된다. public 이나 protected 생성자가 없으므로 객체가 초기화될 때 만들어진 인스턴스가 시스템에서 하나뿐임이 보장된다.

장점

  • 싱글턴임이 API 에 명확하게 드러남, 간결함
public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	private Elvis() { }
	
	public void leaveTheBuilding () {   }
}
  • 정적 팩토리 메서드를 public static 멤버로 제공하는 방식

장점

  • API 를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다는 점 , 정적 팩토리를 제네릭 싱글턴 팩토리로 만들 수 있다는 점, 정적 팩토리의 메서드 참조를 공급자로 사용할 수 있다는 점
public class Elvis {

	private static final Elvis INSTANCE = new Elvis();
	private Elvis() {   }
	public static Elvis getInstance() { return INSTANCE; }

	public void leaveTheBuilding() {  }
}
  • 원소가 하나인 열거 타입을 선언하는 방식

대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다. 단, 만들려는 싱글턴이 Enum 외의 클래스를 상속해야 한다면 이 방법은 사용할 수 없다. ( 열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있다. )

public enum Elvis {
	INSTANCE;

	public void leaveTheBuilding() { }
}

아이템 4. 인스턴스화를 막으려거든 private 생성자를 사용하라

단순히 static 메서드와 필드만을 담은 클래스를 만들고 싶을 때가 있다. 이는 객체 지향적인 방법은 아니지만 분명 쓰임새가 있다.

쓰임새

  1. java.lang.Math 와 java.util.Arrays 처럼 기본 타입 값이나 배열 관련 메서드를 모아놓을 수 있다.
  2. java.util.Collections 처럼 특정 인터페이스를 구현하는 객체를 생성해주는 static 메서드를 모아놓을 수 있다.
  3. final 클래스와 관련한 메서드들을 모아놓을 때도 사용한다. final 클래스를 상속해서 하위 클래스에 메서드를 추가하는 것은 불가능하기 때문

static 멤버만 담은 유틸리티 클래스는 인스턴스로 만들어 쓰려고 설계한 것이 아니지만 생성자를 명시하지 않으면 컴파일러가 자동으로 기본 생성자를 만들기 때문에 인스턴스화 될 수 있다.

인스턴스화를 방지하기 위해 추상 클래스로 만드는 방법을 택하는 사람이 있지만 이는 인스턴스화를 막는 적절한 방법이 아니다. 하위 클래스를 만들어 인스턴스화 할 수 있기 때문이다.

인스턴스화를 방지하는 방법은 private 생성자를 추가하는 것이다. 이 방식은 상속을 불가능하게 하는 효과도 있다.

모든 생성자는 명시적이든 묵시적이든 상위 클래스의 생성자를 호출하게 되는데 이를 private 로 선언했으니 하위 클래스가 상위 클래스의 생성자에 접근할 길이 막혀 상속이 불가능하다.

아이템 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라

사용하는 자원에 따라 동작이 달라지는 클래스에는 정적 유틸리티 클래스나 싱글턴 방식은 적합하지 않다.

이러한 클래스에는 인스턴스를 생성할 때 생성자에 필요한 자원을 넘겨주는 의존 객체 주입의 형태를 사용하는 것이 적절하다.

public class SpellChecker {
	private final Lexicon dictionary;

	public SpellChecker ( Lexicon dictionary ) {
		this.dictionary = Objects.requireNonNull(dictionary);
	}
	
	public boolean isVaild ( String word ) { }
	public List < String > suggestion ( String typo ) { }
}

의존 객체 주입은 생성자, 정적 팩토리, 빌더 모두에 똑같이 응용할 수 있다.

이 패턴의 변형으로, 생성자에 자원 팩토리를 넘겨주는 방식이 있다. 팩토리란 호출할 때마다 특정 타입의 인스턴스를 반복해서 만들어주는 객체를 말한다.

의존 객체 주입이라 하는 이 기법은 클래스의 유연성, 재사용성, 테스트 용이성을 개선해준다.

아이템 6. 불필요한 객체 생성을 피하라

생성자는 호출할 때마다 새로운 객체를 만들지만, 팩토리 메소드는 전혀 그렇지 않다. 불변 객체만이 아니라 가변 객체라 해도 사용 중에 변경되지 않을 것임을 안다면 재사용할 수 있다.

static boolean isRomanNumeral ( String s ) {
	return s.matches(" ");
}

정규표현식용 Pattern 인스턴스는, 한 번 쓰고 버려져서 곧바로 가비지 컬렉션 대상이 된다. Pattern 은 입력받은 정규표현식에 해당하는 유한 상태 머신을 만들기 때문에 인스턴스 생성 비용이 높다.

public class RomanNumerals {
	private static final Pattern ROMAN = Pattern.compile();

	static boolean isRomanNumeral ( String s ) {
		return ROMAN.matcher(s).matches();
	}
}

불필요한 객체를 만들어내는 예로 오토박싱을 들 수 있다.

오토박싱은 프로그래머가 기본 타입과 박싱된 기본 타입을 섞어 쓸 때 자동으로 상호 변환해주는 기술이다. 오토박싱은 기본 타입과 그에 대응하는 박싱된 기본 타입의 구분을 흐려주지만, 완전히 없애주는 것은 아니다.

private static long sum() {
	Long sum = 0L;
	for ( long i = 0; i <= Integer.MAX_VALUE; i++)
		sum += i;
	
	return sum;
} 

아이템 7. 다 쓴 객체 참조를 해제하라

[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 Object pop() {
		if ( size == 0 ) throw new EmptyStackException();
		return elements[--size];
	}

	
}

위의 예제에서 pop 메소드를 통해 꺼낸 값은 다시는 사용하지 않는 값이다.

하지만 여전히 Stack 클래스 내부에서 관리하고 있는 elements 배열에 값이 존재한다.

실제적으로 사용하지 않지만 elements 는 해당 값을 참조하고 있다. 이런 경우 Garbage Collector 에서 처리하지 않기 때문에 메모리 누수가 발생한다.

다만 객체 참조를 null 처리하는 일은 예외적인 경우여야 한다.

다 쓴 참조를 해제하는 가장 좋은 방법은 그 참조를 담은 변수를 유효 범위 밖으로 밀어내는 것이다.

메모리 누수를 주의해야 하는 경우

  • 자기 메모리를 직접 관리하는 클래스라면 항시 메모리 누수에 주의해야 한다.

  • 캐시 역시 메모리 누수를 일으키는 주범이다.

WeakHashMap 은 이러한 상황을 타개하는 하나의 방법이다. 단 외부에서 키를 참조하는 동안만 필요하는 상황에서만 유용하다.

  • 리스너 혹은 콜백

클라이언트가 콜백을 등록만 하고 명확히 해지하지 않는다면, 조치하지 않는 한 쌓여간다. 이럴 때 콜백을 약한 참조로 저장하면 가비지 컬렉터가 즉시 수거해간다.

아이템 8. finalizer 와 cleaner 사용을 피하라

💡 cleaner ( 자바 8까지는 finalizer ) 는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.

아이템 9. try-finally 보다는 try-with-resources 를 사용하라

static String firstLineOfFile( String path ) throws IOException {
	BufferReader br = new BufferedReader ( new FileReader ( path ) );
	try {
		return br.readLine();
	} finally {
		br.close();
	}
}
static String firstLineOfFile( String path ) throws IOException {
	try ( BufferReader br = new BufferedReader ( new FileReader ( path ) ) ){
		return br.readLine();
	} 
}
💡 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources 를 사용하자. 예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다. try-finally 로 작성하면 실용적이지 못할 만큼 코드가 지저분해지는 경우라도, try-with-resources 로는 정확하고 쉽게 자원을 회수할 수 있다.

0개의 댓글