꾸준한 스터디
article thumbnail

싱글턴은 인스턴스를 오직 하나만 생성하여 재사용하는 클래스이며 무상태, 유일 객체이다.

인터페이스 정의 없이 클래스를 싱글턴으로 만들면 유일 객체이므로 비용 등이 고려 될 경우 테스트가 불가피해진다.

따라서, 인터페이스 정의 및 구현을 통해 Mock 객체 구현을 통해 테스트를 진행해야한다.

 

mock 테스트 방식이 궁금하다면 하단 github 링크의 mock_test 예제 코드를 참고 바란다.

 

싱글턴 방식에는 필드, 정적 팩터리, 열거 타입 3가지로 볼 수 있다.

 

필드 방식

private 기본 생성자를 만들어 놓았기에 필드를 초기화 할 때 한번 호출되어 하나뿐임이 보장된다.

또한 API를 통해 싱글턴 클래스 임을 명확하게 드러낼 수 있고 방식이 간결하다.

public class Person {
    public static final Person INSTANCE = new Person();

    private Person() {}

    public void move() {
        System.out.println("Person is moving.");
    }

    public void run() {
        System.out.println("Person is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = Person.INSTANCE;
        person.move();
        person.run();
    }
}

 

그러나 리플렉션을 통한 private 생성자 호출이 가능하여

이를 방지하기 위해 두번째 객체가 생성될때 예외처리를 해야한다.

 

public class Person {
    public static final Person INSTANCE = new Person();
    private static boolean isCreated;

    private Person(){
        if(isCreated) {
            try {
                throw new InstanceAlreadyExistsException("싱글톤으로 생성 불가능합니다.");
            } catch (InstanceAlreadyExistsException e) {
                System.out.println(e.getMessage());
            }
        }
        isCreated = true;
    }

    public void move() {
        System.out.println("Person is moving.");
    }

    public void run() {
        System.out.println("Person is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        try {
        
            Constructor<Person> constructor = Person.class.getDeclaredConstructor();
            constructor.setAccessible(true);

            Person person = Person.INSTANCE;
            Person person1 = constructor.newInstance();
            Person person2 = constructor.newInstance();
            
            System.out.println(person == person1);
            System.out.println(person1 == person2);

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

 

정적 팩터리 방식

항삭 같은 객체를 참조하고 하고 있어 필드 방식과 동일하게 유일성을 보장할 수 있지만 

예외 또한 동일하게 리플랙션을 통한 접근이 가능하다.

 

필드 방식과 다른 장점 크게 3가지가 있는데 아래의 장점을 고려하지 않는다면 필드 방식 혹은 열거 타입 방식을 이용하는 것을 추천한다.

 

1. API 변경없이 싱글턴 여부를 결정할 수 있다.

새로운 인스턴스를 반환하도록 하여 싱글턴이 아니도록 한다.

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

    private Person() {}

    public static Person getInstance() {
        return INSTANCE;
        /*
        * return new Person();
        */
    }

    public void move() {
        System.out.println("Person is moving.");
    }

    public void run() {
        System.out.println("Person is running.");
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = Person.getInstance();
        person.move();
        person.run();
    }
}

 

2. 제네릭 싱글턴 팩터리로 만들 수 있다.

제네릭을 통해 런타임 시점에  인자의 타입으로 유동적으로 객체의 타입을 지정할 수 있다.

 

public class GenericSingletonFactory<T> {

    private static final GenericSingletonFactory<Object> INSTANCE = new GenericSingletonFactory<>();

    private GenericSingletonFactory() {}

    public static <E> GenericSingletonFactory<E> getInstance() {
        return (GenericSingletonFactory<E>) INSTANCE;
    }

    public void typing(T t) {
        System.out.println("Typing "+t.getClass()+" "+t);
    }
    
}

public class GSFMain {
    public static void main(String[] args) {
        GenericSingletonFactory<String> stringSingletonFactory = GenericSingletonFactory.getInstance();
        GenericSingletonFactory<Boolean> booleanSingletonFactory = GenericSingletonFactory.getInstance();

        System.out.println(stringSingletonFactory.equals(booleanSingletonFactory));
        stringSingletonFactory.typing("false");
        booleanSingletonFactory.typing(false);
    }
}

 

3. 메서드 참조를 Supplier로 사용할 수 있다.

Supplier는 함수형 인터페이스의 한 종류로 인자에 맞는 타입을 반환하는 get 메서드를 지원한다.

 

@FunctionalInterface
public interface Supplier<T> {
    /**
     * Gets a result.
     * @return a result
     */
    T get();
}


public class Supplier {
    public void StartAllBehavior(java.util.function.Supplier<Person> personSupplier) {
        Person person = personSupplier.get();
        person.move();
        person.run();
    }
}

public class SupplierMain {
    public static void main(String[] args) {
        Supplier supplier = new Supplier();

        // Method Reference로 사용할 수 있다.
        supplier.StartAllBehavior(Person::getInstance);
    }
}

 

직렬화 시 고려사항 (필드 방식, 정적팩터리 방식)

위에서 언급한 필드, 정적팩터리 방식은 직렬화, 역직렬화시 3가지 사항을 고려해야 한다.

- Serializable 인터페이스를 구현해야 한다.

- transient를 통해 직렬화 과정에서 제외하고 싶을 경우 선언한다.

- readResolve 메서드를 추가해 주어야 역직렬화 후 동치를 보장한다.

 

public class NoneReadResolvePerson implements Serializable {
    private transient String temp;
    public static final NoneReadResolvePerson INSTANCE = new NoneReadResolvePerson();

    private NoneReadResolvePerson(){
    }

    public void move() {
        System.out.println("Person is moving.");
    }

    public void run() {
        System.out.println("Person is running.");
    }
}

public class ReadResolvePerson implements Serializable {

    private transient String temp;

    public static final ReadResolvePerson INSTANCE = new ReadResolvePerson();

    private ReadResolvePerson(){
    }

    public void move() {
        System.out.println("Person is moving.");
    }

    public void run() {
        System.out.println("Person is running.");
    }

    public Object readResolve() {
        return INSTANCE;
    }
}


public class Main {
    public static void main(String[] args) {
        try {
            //None ReadResolve
            ObjectOutput objectOutput =  new ObjectOutputStream(new FileOutputStream("NoneReadResolvePerson"));
            objectOutput.writeObject(NoneReadResolvePerson.INSTANCE);

            ObjectInput objectInput = new ObjectInputStream(new FileInputStream("NoneReadResolvePerson"));
            NoneReadResolvePerson noneReadResolvePerson = (NoneReadResolvePerson) objectInput.readObject();
            System.out.println("NoneReadResolvePerson >> "+ (noneReadResolvePerson == NoneReadResolvePerson.INSTANCE));

            //ReadResolve
            objectOutput =  new ObjectOutputStream(new FileOutputStream("ReadResolvePerson"));
            objectOutput.writeObject(ReadResolvePerson.INSTANCE);

            objectInput = new ObjectInputStream(new FileInputStream("ReadResolvePerson"));
            ReadResolvePerson readResolvePerson = (ReadResolvePerson) objectInput.readObject();
            System.out.println("ReadResolvePerson >> "+ (readResolvePerson == ReadResolvePerson.INSTANCE));

        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

열거 타입 방식

필드 방식과 생성자 방식의 고려 대상인 직렬화와 리플렉션에 대한 예외를 방지할 수 있다.

완벽해 보이는 열거 타입 방식에도 단점이 존재하는데 Enum 외의 클래스 상속이 불가능 하다는 점이다.

public enum Person {
    INSTANCE;
    
    public void move() {
        System.out.println("Person is moving");
    }
    
    public void run() {
        System.out.println("Person is running");
    }
}

public class Main {
    public static void main(String[] args) {
        Person person = Person.INSTANCE;
        person.move();
        person.run();

        // 리플렉션 테스트
        try {
            Constructor<Person> constructor = Person.class.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            System.out.println(e.getCause() + " " + e.getMessage());
        }
    }
}

 

예제 코드

 

GitHub - VenusIM/Study_Record

Contribute to VenusIM/Study_Record development by creating an account on GitHub.

github.com

profile

꾸준한 스터디

@StudyRecord

포스팅이 유익하셨다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!