[이펙티브 자바] 객체의 생성과 파괴 Item3 - private 생성자나 열거 타입으로 싱글턴임을 보증하라

이성훈·2022년 3월 14일
1

이펙티브 자바

목록 보기
4/17
post-thumbnail

이펙티브 자바의 첫 시작은 객체를 생성하고 파괴하는 것에 대한 고찰이다.

"2장 - 객체의 생성과 파괴" 는 다음과 같은 기준으로 맥락을 잡고 있다.

  • 객체를 만들어야 할 때는 언제인가
  • 객체를 만들지 말아야 할 때는 언제인가
  • 올바른 객체 생성 방법은 무엇인가
  • 객체의 불필요한 생성을 피하는 방법은 무엇인가
  • 객체를 제 때에 파괴시키는 방법은 무엇인가
  • 파괴 전에 수행해야 할 정리 작업을 관리하는 요령이 있는가

위와 같은 맥락을 계속 기억하며 공부하자.


  • Item1. 생성자 대신 정적 팩터리 메서드를 고려하라.
  • Item2. 생성자에 매개변수가 많다면 빌더를 고려하라.
  • Item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.
  • Item4. 인스턴스화를 막으려거든 private 생성자를 사용하라.
  • Item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.
  • Item6. 불필요한 객체 생성을 피하라.
  • Item7. 다 쓴 객체 참조를 해체하라.
  • Item8. finalizer와 cleaner 사용을 피하라.
  • Item9. try-finally 보다는 try-with-resources를 사용하라.




<"private 생성자나 열거 타입으로 싱글턴임을 보증하라">


제목에서 등장하는 단어들을 모두 알고 있다면 다행이지만,

잘 모르는 사람들은 이번에도 배경지식을 알고 가는게 좋다.



  • 열거 타입 (Enumerate -> enum)

<열거 타입>

  • 한정된 값만을 갖는 데이터 타입.
  • 지정한 몇 개의 열거 상수 중에서 하나의 상수를 저장하는 데이터 타입.

쉽게 말해, 열거 타입이라는건 자주 쓰는 일정한 상수들을 나열해 놓고,
필요할 때 하나를 정해서 쓰는거다.



다음의 예시를 한번 봐보자.

public enum Week
{
	MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
{
	public static void main(String[] args) 
  	{
        Week today = Week.SUNDAY;
    }
}

뭐 위와 같은 방식이다.
어차피 일주일에 요일 이름은 변하지 않으므로, 미리 나열해서 지정한거다.


단순히 이정도 말하려고 한 얘기는 아니고,


여기서 알아야 할 포인트는, 열거 타입의 열거되어 있는 상수는
사실 각자가 하나의 Week 객체라는 것이다.

그래서 MONDAY라는 상수는 MONDAY라는 내용을 담고있는 하나의 Week 객체다.

이 내용을 고려하며 이번 내용을 읽으면 도움이 될 것이다.

아래에 추가로 열거 타입에 대한 내용을 적어본다.

열거 타입의 특징

  • 요일이나 계절등을 예시로 생각하면 된다.
  • 관례적으로 열거 타입 이름은 첫 문자를 대문자로, 나머지 문자를 소문자로 한다.
  • public enum에서 enum은 무조건 소문자이다.
  • 열거 타입 이름은 소스 파일명과 대소문자 모두 일치해야 한다.
  • 열거 상수는 모두 대문자로만 작성한다.
  • 열거 타입 변수는 스택에, 열거 객체는 힙에 저장된다.
  • 열거 타입 변수에 저장되는 값은 열거 객체 자체가 아닌, 객체의 번지수이다.
  • 열거 타입 안에 상수는 힙에 있는 열거 객체의 번지수로 저장되어있다.
  • 그래서 "열거 타입 변수 = 열거타입.열거상수" 라고 쓰게 되면,
    "열거 상수가 바라보고 있는 열거 객체를, 열거 타입 변수도 같이 바라보게 한다"는 의미이다.



  • 싱글턴 (Singleton)

<싱글턴>

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

정의 자체가 깔끔해서 그 의미를 이해하는데는 별로 어렵지는 않다.

다만, 싱글턴이라는게 왜 필요한지 이해하는게 관건이 된다.
다음의 조금 일상적인 예시를 들어보자.

<공유 프린터의 예시>

  • 회사 3층에는 공유 프린터가 있다.
  • A는 프린터 관리자를 만들어보려고 한다.
  • 프린터는 20명 정도의 직원이 공유해서 사용하고 있다.

위와 같은 상황을 가정해보자.
기본적으로 프린터는 한대 뿐이기 때문에, 프린터 객체에 대한 생성도 오로지 한번만 이루어져야 한다.

그래서 프린터라는 인스턴스는 한번만 만들어짐이 보장되어야 하고,
클라이어트 측에서는 그 하나의 인스턴스만을 공용으로 사용해야 한다.


어떤가? 싱글턴이 필요한 상황에 대해 감을 잡았는가?

그럼 이제부터 싱글턴을 만드는 방법에 대해서 알아보자.





#  public static final 필드 방식


첫번째 방법의 흐름은 다음과 같다.

<public static final 필드 방식>

  • 생성자를 private으로 감춰 외부에서는 생성할 수 없게한다.
  • public static final 필드를 이용해, 객체를 딱 한번만 생성한다.

생각보다 단순한 방식이다.

어차피 public이나 protected로 선언된 생성자가 없으므로,
public static final 필드로 생성되는 인스턴스 외에 더이상 생성되는 것은 없다.

전체 시스템에서 인스턴스가 하나뿐임을 보장받을 수 있는 것이다.

위에서 적었던 공유프린터의 예시를 들어보자.

public class Printer
{
	private static final Printer INSTANCE = new Printer();

  	private Printer()
  	{
  	}

  	public void print(String str)
  	{
  		System.out.println(str);
  	}
}

위 코드만 봐도 너무나 간결하다는 것을 알 수 있다.

이 방법의 특징이자 장점은,
해당 클래스가 싱글턴임이 명확하게 드러나고 코드 또한 간결하다는 것이다.


다만 이 방법에는 예외가 한가지 존재하는데 클라이언트가 권한이 충분할 경우,
리플렉션 API인 AccessibleObject.setAccessible 을 사용하면
private 생성자에 접근이 가능하다.


하지만 생성자에 두번째 객체 생성에 대한 예외처리를 하면 되기 때문에,
큰 문제는 되지 않는것 같다.


<public static final 필드 방식의 장점>

  • 코드가 간결하다.
  • 해당 클래스가 싱글턴임이 API에 명확히 드러난다.




#   정적 팩토리 메서드 방식


이번에는 바로 예시 코드를 보자.


public class Printer
{
	private static final Printer INSTANCE = new Printer();

  	private Printer()
  	{
  	}

	public static Printer getInstance()
  	{
  		return INSTANCE;
  	}

  	public void print(String str)
  	{
  		System.out.println(str);
  	}
}

또는 이렇게도 쓸 수 있을 것이다.

public class Printer {

 	private static Printer INSTANCE = null;

  	private Printer() 
  	{ 
  	}

  	public static Printer getPrinter()
  	{
    	if (INSTANCE == null) 
  		{
      		INSTANCE = new Printer();
    	}
    	return printer;
  	}

  	public void print(String str) 
  	{
    	System.out.println(str);
  	}
}

어쨋든 두 코드는 같은 형태이다.

그런데 아마도 이게 방식 1과 무슨 차이가 있냐고 생각하는 사람들이 있을수 있다.
비슷하다면 비슷하지만 논리적으로는 큰 차이가 존재한다.


방식1의 경우,
외부에서 클래스 내에 직접 접근하여 final 변수로 저장된 인스턴스를 사용한다.

방식2의 경우,
외부에서 직접 접근하지 못하고 정적 팩토리 메서드를 통해 인스턴스를 넘겨받는다.


차이점이 느껴지지 않는가?

바로 이런 차이점이 방식1과는 다른 장점을 만들어낸다.

싱글턴으로 만들었던 클래스를,
언제든지 원할때 싱글턴이 아니게 만들 수 있다는 것이다.


쉬운 말이다. 싱글턴은 하나의 인스턴스만 만들어지게 하면 된다. 정적 팩토리 메서드 방식의 경우, 그 메서드 내용에 로직이 들어간다.

바꿔 말하면, 정적 팩토리 메서드의 내용만 바꾸만 싱글턴이 아니게 만들 수 있다.


그런데 사실 그럴 경우가 없다면 (저 장점이 필요하지 않다면),
public static final 필드 방식이 더 낫다.


추가적으로, 정적 팩토리 메서드 방식 또한 public static final 방식과 마찬가지로,
리플렉션 API를 통한 예외는 존재한다.


<정적 팩토리 메서드 방식의 장점>

  • 언제든 싱글턴이 아니게 변경할 수가 있다.
  • 원한다면 정적 팩토리를 "제네릭 싱글턴 팩토리"로 만들 수 있다. (= Item 30)
  • 정적 팩토리의 메서드 참조를 공급자 (Supplier)로 사용할 수 있다. (= Item 43, 44)




#   열거 타입 방식


마지막은 열거 타입을 이용한 싱글턴 방식이다.


마찬가지로 예제를 보자.
public enum Printer
{
  	INSTANCE;

	public void print(String str) 
  	{
   		System.out.println(str);
  	}
} 	

너무나도 간결하다. 간단한데..
이게 왜 싱글턴이 되는지 이해가 안되는 사람이 많을수도 있다.
(눈치챘겠지만 내가 그랬다 ㅠ..)


이건 해당 방식을 이해하기 보다 열거 타입 자체를 이해해야 가능하다.

이 글의 시작점에 열거 타입에 대한 배경지식을 설명해놨으니
혹시 읽고 오지 않았다면 읽어보길 바란다. 정말 짧으니까..


열거 타입과 더불어 위 코드를 설명해보겠다.

위의 그림을 보자.

보통 열거 타입 클래스를 만들고 상수를 지정하지만,
그 상수의 내용을 담고 있는 객체의 실체는 저 멀리 heap 영역에 있다.

열거 상수에는 열거 객체를 가르키는 주소가 담겨져 있는 원리이고,
열거 변수에는 열거 상수에 담겨져 있는 열거 객체의 주소값이 들어간다.


public static void main(String[] args) 
{
    Printer today = Printer.INSTANCE;
}

결국 위에 코드처럼 열거 변수를 만들어 새로운 객체를 생성하는 것처럼 보여도,

그 실체는 heap 영역에 있는 열거 객체의 주소값을 바라보게 하는 원리다.


그래서 만약 싱글턴으로 만들고자 하는 클래스(Printer) 타입을 열거 타입으로 지정하고,
그 내에 열거 상수를 한개만 두게 된다면,

자연히 Printer라는 인스턴스는 시스템 전체적으로 오로지 한개만 존재하는 것이다.



대부분 싱글턴을 만드는 상황에서는,
위와 같이 원소가 하나뿐인 열거 타입으로 만드는게 제일 좋다고 한다.

하지만 만약 싱글턴으로 만들려는 클래스가 enum외에 다른것을 상속해야 한다면, 이 방법은 불가하다.

profile
IT 지식 공간

0개의 댓글