[이펙티브 자바] 3. private 생성자나 열거 타입으로 싱글턴임을 보증하라

노을·2023년 1월 11일
1

이펙티브 자바

목록 보기
3/14
post-thumbnail

⭐ (참고) 싱글톤의 단점

1. 테스트가 어려움

23p. 클래스를 싱글턴으로 만들면 이를 사용하는 클라이언트를 테스트하기가 어려워질 수 있다. 타입을 인터페이스로 정의한 다음 그 인터페이스를 구현해서 싱글턴이 아니라면 싱글턴 인스턴스를 가짜(mock) 구현으로 대체할 수 없기 때문이다.


단위 테스트는 서로 독립적이어야 하며, 테스트를 어떤 순서로든 실행할 수 있어야 한다. 하지만 싱글톤이면 테스트할 때 마다 객체 상태를 초기화해야 하는 번거로운 작업이 필요하다.

그런데 인터페이스를 만들면 이러한 불편함을 해소할 수 있다 !
예를 들면 하나의 차(싱글톤)를 여러 사람이 운전 할 수 있다고 가정하자.
그럼 Car라는 인터페이스를 만들고, 그걸 구현한 MyCar 클래스를 만들고, Car 타입의 객체를 사용하는 Person 클래스를 만들면 될 것이다.


그럼 Person이라는 클래스를 테스트 해보자.


@ExtendWith(MockitoExtension.class)
class PersonTest {
    @Mock
    Car mockCar;

    @Test
    void 운전() {
        // given
        Person person = new Person(mockCar);
        // when
        person.drive();

        // then
        boolean driveStatue = person.isDriveStatue();
        Assertions.assertThat(driveStatue).isTrue();
    }

}

인터페이스가 있다면, 그 인터페이스 타입의 Mock(가짜 객체)를 사용해서 테스트할 수 있다.
Mock 객체는 실제 객체를 다양한 조건으로 인해 제대로 구현하기 어려울 경우 만들어 사용한다.



2. 리플랙션, 역직렬화의 공격으로 싱글톤이 깨질 수 있다.

  • 리플랙션
    Class<?> aClass = Class.forName("패키지명+클래스명");
    아이템1에서 리플랙션 내용이 있는데, 리플랙션으로 class path 만 알고도 객체를 만들어내 낼 수 있다.
    (-> 객체가 여러 개 생길 수 있다는 의미)

    <해결 방법>
    생성자가 호출될 때 현재 객체가 만들어졌는지 확인해서 두 개 이상 생기는 것을 방지한다.
    private Elvis() {
        if (created) {
            throw new UnsupportedOperationException("can't be created by constructor.");
        }
        created = true;
    }



  • 역직렬화
    객체를 직렬화하고, 다시 역직렬화 하면 새로운 객체를 반환한다. (-> 객체가 여러 개 생길 수 있다는 의미)

    <해결 방법>
    readResolve() 메서드를 선언하고, 만들어져 있는 싱글톤 객체를 반환하게 한다.
    private Object readResolve() {
        return INSTANCE;
    }






아이템3에서는 객체를 싱글톤으로 만드는 방법에 대해 알아본다.


⭐ 방법1. public static final 필드

public class Elvis {
	public static final Elvis INSTANCE = new Elvis();
	private Elvis() { ... }

	public void leaveTheBuilding()
}

간결하다는 장점이 있다.



⭐ 방법2. 정적 팩토리 방식


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

	public void leaveTheBuilding()
}

장점1
싱글톤 말고 새로운 객체를 반환하게 만들고 싶을 때 생성자 구현부를 수정하면 된다. 그러면 객체를 사용하는 입장에서의 코드 (Elvis.getInstance()) 수정 할 필요 없게 된다.


장점2
정적 팩터리를 제네릭 싱글턴 팩토리로 만들 수 있다.

제네릭 싱글턴 팩토리 : 제네릭으로 타입설정 가능한 인스턴스를 만들어두고, 반환 시에 제네릭으로 받은 타입을 이용해 타입을 결정하는 것

public class Elvis<T> {
    private static final Elvis INSTANCE = new Elvis();
    
    private Elvis(){
        
    }

    public static <T> Elvis<T> getInstance() {
        return (Elvis<T>) INSTANCE;
    }
}

객체를 원하는 타입으로 제공할 수 있다는 장점이 있다.



장점3
정적 팩터리의 메서드 참조를 공급자(supplier)로 사용할 수 있다.
(이 부분은 내가 java8을 잘 모르기도 하고,,, 지금 당장 찾아보는 게 의미 없을 것 같아서 나중에 작성하겠다...^^)



⭐ 방법3. Enum 사용 (가장 좋은 방법!)

public enum Elvis {
	INSTANCE; 
	
	public String getName() {
		return "Elvis";
	}

	public void leaveTheBuilding() { ... }
}

대부분의 상항에서는 원소가 하나뿐인 열거타입을 만드는 게 가장 좋은 방법이다.
열거타입은 무조건 싱글톤을 보장하기 때문이다.
위 두 가지 방식에서는 리플랙션, 역직렬화 상황에서 새로운 인스턴스가 생길 가능성이 있지만
열거 타입은 애초에 하나의 객체만 생성되고, new로 객체를 새로 생성할 수도 없다.
리플랙션이나 직렬화에서도 Enum에 대한 특징이 반영되어 있기 때문에 제 2의 객체가 생성될 걱정을 안해도 된다.

0개의 댓글