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

    들어가기 전

    이 글은 인프런 백기선님의 이펙티브 자바 강의를 복습하며 작성한 글입니다.   

     

     

     

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

    어플리케이션에서 여러 인스턴스가 아닌 하나의 인스턴스만 필요하거나 유지해야하는 경우가 있다. 예를 들면 설정 관련 인스턴스가 있을 수 있다. 어떤 게임을 할 때, 색상 값은 무엇이고 해상도는 무엇인지를 설정할 수 있다. 그런데 설정을 가지고 있는 인스턴스가 여러 개가 있다면 헷갈리거나 오동작할 수 있다. 이런 경우가 있을 수 있기 때문에 어플리케이션에서 특정 인스턴스는 하나만 유지해야하는 경우도 있다. 이런 것을 싱글턴이라고 한다. 

     

    싱글턴의 장/단점은 다음과 같다.

    장점 : 간결하고 싱글턴임을 API에 드러낼 수 있다. 

    단점 

    1. 싱글톤을 사용하는 클라이언트를 테스트하기 어려워진다.

    2. 리플렉션으로 private 생성자를 호출할 수 있다. 

    3. 역직렬화 할 때 새로운 인스턴스가 생길 수 있다. 

     

     

    첫번째 방법 : private 생성자 + public static final 필드

    • private 생성자만 만들면 클래스 내에서만 생성자를 호출할 수 있다.
    • 본인의 생성자를 호출해서 내부 필드로 인스턴스를 하나만 가지고 있는다. 

    큰 틀은 위와 같고, 코드는 아래에서 볼 수 있다.

    public class Elvis {
        /**
         * 싱글톤 오브젝트
         */
        public static final Elvis INSTANCE = new Elvis();
        private Elvis() {}
    }

    이렇게 만들었을 때의 장점은 무엇이 있을까?

    • 간편하게 코드를 작성할 수 있다.  간편한 코드이기 때문에 읽기도 편리하다. 
    • 문서화 주석 또한 잘 지원된다. 따라서 문서만 읽어도 싱글턴 클래스인지 알 수 있다. 

     

    단점1. 싱글톤을 사용하는 테스트 코드에서 테스트 하기 어려워진다. 

    특히 싱글톤 객체의 클라이언트에 대한 테스트 코드를 작성할 때 매우 어려워지는 경우가 있다. 예를 들어 아래 코드를 살펴보자. 

    Concert는 Elvis를 사용하는 클래스다. 즉, Elvis 클래스의 클라이언트 클래스는 Concert가 된다. 엘비스가 노래를 부를 때 마다 돈을 줘야한다고 해보자. 그런데 콘서트 장에서 엘비스가 노래 부르기 전에 무대가 정상적으로 동작하는지를 보고 싶다. 그런데 그런 테스트를 할 때 마다 엘비스가 와서 노래를 부를 수는 없다. 왜냐하면 엘비스를 부르는 비용은 비싸기 때문이다.

    그래서 Concert 클래스를 테스트 할 때는 일반적으로는 엘비스 대신에 대역을 쓰는 것이 당연하다. 그렇지만 Elvis 클래스의 인터페이스는 존재하지 않기 때문에 Elvis 대역으로 사용할 Mock 객체의 생성이 어렵다. 따라서 테스트 코드를 작성하기 어려워진다. 

    그래서 싱글톤을 사용한다면 클라이언트 코드의 테스트를 위해서 인터페이스를 사용하도록 코드를 개선해보는 것이 도움이 된다. 클라이언트 코드(Concert)가 잘 동작하는 것을 보고 싶은 것이지 Elvis가 잘 동작하는 것을 보고 싶은 것이 아니기 때문이다. 

     

    Elvis 클래스에 인터페이스가 없는 경우

    public class Concert {
    
        private boolean lightsOn;
        private boolean mainStageOpen;
        private Elvis elvis;
    
        public Concert(Elvis elvis) {
            this.elvis = elvis;
        }
    
        public void perform() {
            mainStageOpen = true;
            lightsOn = true;
            elvis.sing();
        }
    
        public boolean isLightsOn() {
            return lightsOn;
        }
    
        public boolean isMainStageOpen() {
            return mainStageOpen;
        }
    }
    
    class ConcertTest {
        @Test
        void perform() {
    
            Concert concert = new Concert(Elvis.INSTANCE);
            concert.perform();
    
            assertTrue(concert.isLightsOn());
            assertTrue(concert.isMainStageOpen());
        }
    }
    • Elvis (싱글톤 클래스)에 인터페이스가 존재하지 않는 경우다. 이 경우에는 Elvis 클래스의 Mock 객체를 생성하기 어려워진다. 
    • 따라서 Concert 클래스트를 테스트 할 때 항상 Elvis 객체를 사용해야한다. Elvis 객체가 만약 네트워크라도 탄다고 한다면, 테스트는 너무 커지게 된다. 

     

    Elvis 클래스에 인터페이스가 있는 경우 

    public class Concert {
    
        private boolean lightsOn;
        private boolean mainStageOpen;
        private IElvis elvis;
        public Concert(IElvis elvis) {
            this.elvis = elvis;
        }
    
        public void perform() {
            mainStageOpen = true;
            lightsOn = true;
            elvis.sing();
        }
    
        public boolean isLightsOn() {
            return lightsOn;
        }
    
        public boolean isMainStageOpen() {
            return mainStageOpen;
        }
    }
    
    
    
    class ConcertTest {
    
        @Test
        void perform() {
    
            Concert concert = new Concert(new MockElvis());
            concert.perform();
    
            assertTrue(concert.isLightsOn());
            assertTrue(concert.isMainStageOpen());
        }
    }
    • Elvis 클래스에 대한 인터페이스 IElvis를 만들엇다.
    • IElvis를 구현한 MockElvis 클래스를 생성해서, 테스트 용으로 사용할 수 있게 되었다. 

     

    단점2. 리플렉션으로 private 생성자를 호출할 수 있다. 

    리플렉션을 이용해서 private 생성자에 접근해서 여러 인스턴스를 생성할 수 있다. 예를 들어서 아래 코드에서처럼 리플렉션을 이용하면 private 생성자라도 접근해서 새로운 인스턴스를 생성할 수 있게 된다. 이렇게 되면 더 이상 싱글톤이 아니게 된다. 

    @Test
    void reflectionTest() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor<Elvis> declaredConstructor = Elvis.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        Elvis elvis1 = declaredConstructor.newInstance();
        Elvis elvis2 = declaredConstructor.newInstance();
        
        assertFalse(elvis1 == elvis2);
    }

    몇몇 코드를 추가한다면 위의 문제를 방지할 수 있게 된다. 

    • static Flag를 하나 추가하고, 생성자가 처음 호출되었을 때 Flag가 True가 되도록 한다.
    • 이후 생성자가 호출될 때 마다 Flag가 True인 경우 에러가 발생하도록 작성한다. 
    public class Elvis implements IElvis {
        public static final Elvis INSTANCE = new Elvis();
        // 반드시 static으로 추가
        public static boolean created;
        private Elvis() {
            if (created) {
                throw new UnsupportedOperationException("can't be created by constructor.");
            }
            created = true;
        }
    
    	...
    }

    이렇게 작성하게 되면 가장 처음에 클래스가 생성될 때 자동으로 Elvis()가 호출되기 때문에 created 플래그는 항상 True가 되게 된다. 따라서 리플렉션을 이용해서 생성자를 호출할 수 없는 상태로 바뀌게 된다. 

    따라서 기존의 테스트 코드는 아래와 같이 바뀌게 된다. 

    • 리플렉션을 하다가 에러가 발생하고, 리플렉션에서 발생한 에러는 InvocationTargetException으로 변경되게 된다. 
    @Test
    void reflectionTest1() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Constructor<Elvis> declaredConstructor = Elvis.class.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        assertThrows(InvocationTargetException.class, declaredConstructor::newInstance);
    }

    방어 코드를 하나 추가하면서 리플렉션을 이용한 싱글톤 상태 파괴를 막을 수 있게 되었다. 하지만 방어 코드를 추가하게 되면 기존의 장점이었던 간결한 코드라는 것에서 멀어지게 된다. 

     

    단점3. 역직렬화 할 때 새로운 인스턴스가 생긴다.

    직렬화를 이용해서 객체 정보를 어딘가에 저장할 수 있고, 역직렬화를 이용해서 어딘가에 저장된 객체 정보를 읽어 인스턴스를 생성할 수도 있다.

    public static void main(String[] args) {
    
        // 직렬화를 통해 객체 정보를 파일에 저장.
        try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("elvis.obj"))) {
            out.writeObject(Elvis.INSTANCE);
        } catch (IOException e) {
            e.printStackTrace();
        }
    
        // 역직렬화를 통해 파일로부터 객체 정보를 읽어옴.
        try (ObjectInput in = new ObjectInputStream(new FileInputStream("elvis.obj"))) {
            // readObject()를 이용해서 객체를 읽어오면, 클래스 내부에 있는 readResolve()를 바탕으로 인스턴스를 읽어옴. 
            Elvis elvis3 = (Elvis) in.readObject();
            System.out.println(elvis3 == Elvis.INSTANCE);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    예를 들어 위 코드를 살펴볼 수 있다. 

    • 윗 부분은 직렬화를 이용해 객체 정보를 디스크에 저장하는 코드다. 
    • 아래 부분은 역직렬화를 이용해 객체 정보를 디스크에서 읽어와 인스턴스를 생성하는 코드다. 

    이 코드의 실행 결과 아래가 출력된다.

    false

    이 결과는 파일에서 읽어온 elvis3이라는 인스턴스와 Elvis 클래스가 가지고 있는 싱글톤 인스턴스가 서로 다른 것을 의미한다. 즉, 싱글톤이 깨졌음을 의미한다. 어떻게 동작해서 싱글톤이 깨지게 된 것일까?

    역직렬화를 할 때는 readResolve()라는 코드를 사용한다. 그런데 이 코드는 우리가 구현해두지 않으면 기본값을 사용한다. 우리가 구현해두면 이것을 사용해서 역직렬화를 한다. 오버라이딩 개념이지만, @Override 메서드 같은 것들을 사용하지 않는다는 점이 특이하다. 
    public class Elvis implements IElvis, Serializable {
        ...
        private Object readResolve() {
            return INSTANCE;
        }
    
    }

    이 단점을 해결하기 위해서 역직렬화 할 때 사용하는 readResolve()를 싱글톤 클래스 내부에 구현해서 기존에 있던 인스턴스를 반환하도록 해준다. 이렇게 코드를 수정한 다음에 테스트 코드를 실행하면 True가 나오는 것을 확인할 수 있다. 

     

    private 생성자 + public static final 필드 정리

    • 테스트 코드가 어려운 부분은 인터페이스를 제공하도록 리팩토링 해서 문제를 해결할 수 있다. 
    • 리플렉션을 통한 싱글톤 깨짐 방지를 위해서는 방어 코드가 들어가야하고, 이 때문에 이 방법의 장점 중 하나인 간결한 코드는 없어지게 된다. 
    • 역직렬화 부분을 해결하기 위해서 readResolve()를 구현해야하는데, 이 부분에 대한 배경지식도 알아야 하기 때문에 사용하기 어렵다.

    즉, 이 방법으로 싱글톤을 구현하려면 여러 배경지식이 필요하고 일부 장점도 퇴색된다는 것으로 이해할 수 있다. 

     

    두번째 방법 : private 생성자 + 정적 팩터리 메서드 

    이 방법은 private 생성자를 이용하기 때문에 private 생성자에 따른 단점을 그대로 가지고 있다. 즉, 다음 단점을 가진다.

    • 싱글톤을 사용하는 클라이언트를 테스트하기 어려워진다.
    • 리플렉션으로 private 생성자를 호출할 수 있다. 
    • 역직렬화 할 때 새로운 인스턴스가 생길 수 있다. 

    하지만 정적 팩터리 메서드를 이용해서 인스턴스에 대한 행위를 하기 때문에 이런 장점을 더 가지게 된다. 

    •  API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다.
    • 정적 팩터리를 제네릭 싱글턴 팩토리로 만들 수 있다.
    • 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다. 

    먼저 전체적인 구조는 다음과 같이 바뀐다. 

    public class Elvis implements IElvis, Serializable {
    
        private static final Elvis INSTANCE = new Elvis();
        public static boolean created;
        // 정적 메서드로 싱글턴 인스턴스를 가져온다. 
        public static Elvis getInstance() {
            return INSTANCE;
        }
    
        private Elvis() {}
        ...
    }
    • 이전에는 필드에 싱글턴 인스턴스를 저장했고, 필드로 직접 접근하는 방식이다.
    • 이 방법은 싱글턴 인스턴스를 정적 메서드 (static getInstance())를 통해서 가져오는 방법으로 바뀐다. 

    싱글턴 인스턴스를 필드로 직접 접근하던 것이 메서드를 한번 거쳐서 접근하도록 변경되었다.

     

    장점 1. API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다.

    클라이언트가 getInstance()를 이용해 인스턴스를 가져와서 사용한다고 해보자. getInstance()는 정적 메서드 (Static Method)로 구현되어 있는데, 이 경우 getInstance() 내부 구현만 바꾸면 getInstance()를 그대로 사용하면서 동작을 바꿀 수 있게 된다. 즉, API를 바꾸지 않고도 싱글톤이 아니게 변경할 수 있다. 

    public static void main(String[] args) {
        Elvis elvis = Elvis.getInstance();
        elvis.leaveTheBuilding();
    
        // getInstance()를 살짝 수정하면, 싱글턴 -> 매번 새로운 객체로 바꾼다.
        // 클라이언트 코드에 영향을 주지 않고 동작을 바꿀 수 있게 됨.
        System.out.println(Elvis.getInstance());
        System.out.println(Elvis.getInstance());
    }

    예를 들어 위 코드에서는 getInstance()를 할 때 마다 똑같은 인스턴스가 호출될 것이다. 그런데 어느 순간 싱글턴 인스턴스가 아닌 매번 새로운 인스턴스를 사용하도록 변경해야한다고 가정해보자. 이 때, 정적 메서드 getInstance()만 살짝 수정해주면 된다. 

    public static Elvis getInstance() {
            return new Elvis(); // 매번 새로운 인스턴스 반환
            // return INSTANCE; // 싱글턴 반환
        }

    위와 같이 수정해주면 된다. 이렇게 수정해주면 getInstance() 메서드를 호출할 때 마다 새로운 인스턴스가 생성되어서 반환된다. 따라서 실행 결과는 다음과 같음을 알 수 있다. 

    Whoa baby, I'm outta here!
    item03.number3.Elvis@7c30a502
    item03.number3.Elvis@49e4cb85

    만약 기존처럼 필드에 직접 접근하는 방법이었다면 싱글톤 → 매번 새로운 인스턴스로 접근하도록 수정한다면, 클라이언트 코드의 수정이 필요해진다. 그렇지만 메서드를 한번 거쳐서 접근한다면, 메서드의 동작만 수정하면 되기 때문에 클라이언트 코드의 수정 없이 원하는 방식으로 동작할 수 있게 만든다. 

     

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

    private 생성자를 이용해서 싱글턴 인스턴스를 생성하고, 싱글턴 인스턴스에는 정적 메서드로 접근하는 방법의 경우 정적 메서드를 제네릭 싱글턴 팩토리로 만들 수 있다. 제네릭 싱글턴 팩토리를 이용하면, 싱글턴 인스턴스를 제네릭한 타입으로 사용할 수 있게 된다. 이 때, 정적 메서드를 제네릭 싱글턴 팩터리로 만들어 사용해 볼 수 있다. 

    public class MetaElvis<T> {
    
        private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();
        private MetaElvis() {}
    
        @SuppressWarnings("unchecked")
        public static <E> MetaElvis<E> getInstance() {
            return (MetaElvis<E>) INSTANCE;
        }
    
        public void say(T t) {
            System.out.println(t);
        }
    
        public void leaveTheBuilding() {
            System.out.println("Whoa baby, I'm outta here!");
        }
    
        public static void main(String[] args) {
            MetaElvis<String> elvis1 = MetaElvis.getInstance();
            MetaElvis<Integer> elvis2 = MetaElvis.getInstance();
    
            System.out.println(elvis1.equals(elvis2));
            elvis1.say("hello");
            elvis2.say(1);
        }
    }

    이 방법의 핵심은 다음과 같다. 

    • 동일한 인스턴스지만, 원하는 타입으로 바꿔서 사용할 수 있다. (원하는 타입으로 형변환이 가능하다) 
    • getInstance()를 이용해 서로 다른 타입으로 인스턴스를 가져오더라도 동일한 인스턴스가 된다. (참조 주소가 동일함) 
      • 이 경우 equals 비교는 가능하지만, ==비교는 안된다. 이것은 타입이 다르기 때문이다. 

    위와 같이 싱글턴 인스턴스에 접근할 때 좀 더 유연성을 줄 수 있게 사용된다. 

    참고할만한 것 

    위 코드에서 public class <T>로 선언이 되어있는데, 왜 public static <E>로 다시 한번 선언이 되어야 하는걸까? 이것은 public class와 public static의 제네릭과 관련된 Scope이 다르기 때문이다. 

    • public static <E>에 선언된 E는 static scope이기 때문에 static 관련 인자에만 적용된다. 
    • 메서드, public class <T>에 선언된 제네릭은 인스턴스 scope이기 때문에 인스턴스 관련 인자에만 적용된다. 

     

    장점 3. 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다. 

    싱글턴 인스턴스를 가져오는 정적 메서드를 Supplier로 사용할 수 있게 된다. 

    Supplier는 Java8 부터 들어온 함수형 인터페이스를 의미한다. 아래에서 코드를 확인할 수 있다. Supplier 함수형 인터페이스는 특정 클래스에서 아래 get()과 매칭될 수 있는 함수가 구현되면, Supplier 인터페이스를 구현하지 않아도 Supplier처럼 사용할 수 있게 된다. 

    @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }

    Tget()과 매칭된다는 것은 다음을 의미한다.

    • 메서드의 이름은 동일하지 않아도 상관없다. getInstance()여도 상관없다.
    • Argument를 받지 않는 메서드여야 한다. 
    • 특정 인스턴스 T를 반환하는 메서드여야 한다. 

    특정 클래스에서 위 세가지 조건을 만족하는, 정확하게는 두 가지 조건을 만족하는 메서드가 있다고 가정해보자. 그 경우 해당 메서드를 함수형 인터페이스 Supplier처럼 사용할 수 있게 된다. 즉, Supplier 인터페이스를 구현하지 않더라도 (클래스에 implement 키워드가 없더라도) supplier처럼 사용할 수 있다. 아래 코드에서 예시를 살펴볼 수 있다. 

    public class Concert {
        public void start(Supplier<Singer> singerSupplier) {
            Singer singer = singerSupplier.get();
            singer.sing();
        }
    
        public static void main(String[] args) {
            Concert concert = new Concert();
            concert.start(Elvis::getInstance);
        }
    }
    
    public class Elvis implements IElvis, Serializable, Singer {
    
        private static final Elvis INSTANCE = new Elvis();
        public static Elvis getInstance() {
            return INSTANCE;
        }
    
    	...
    }
    • 위 코드에서 Singer를 제공해주는 Supplier 클래스가 필요하다. 
    • getInstance()는 Argument가 없는 메서드이며, Singer 타입의 인스턴스를 반환하다. 두 가지 조건을 만족하기 때문에 이 정적 메서드는 함수형 인터페이스 Supplier처럼 사용할 수 있게 된다. 

    이처럼 정적 메서드로 싱글턴에 접근할 수 있도록 하면, Supplier 인터페이스로도 해당 메서드를 사용할 수 있다는 장점이 존재한다. 


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

    앞에서 두 가지 아이템을 살펴보았다. 이 아이템의 공통점은 모두 private 생성자를 이용해서 싱글턴 인스턴스를 생성한다는 점이다. private 생성자를 통해 싱글턴 인스턴스를 생성할 경우 공통적인 단점 세 가지가 존재한다. 이 부분이 신경 쓰인다면 ENUM 타입을 이용해서 싱글턴임을 보장할 수 있다. 

    ENUM Type으로 싱글턴을 생성하는 것은 가장 간결한 방법이며 역직렬화 / 리플렉션에 의한 싱글턴 깨짐에도 안전하다. 대부분의 상황에서는 원소가 하나뿐인 ENUM Type이 싱글턴을 만드는 가장 좋은 방법이 된다. 이 방법을 권장하는 이유는  아무것도 하지 않아도 싱글턴으로 잘 동작할 수 있기 때문이다. 

    public enum Elvis {
        INSTANCE, HELLO;
    
        public void leaveTheBuilding() {
            System.out.println("Whoa baby, I'm outta here!");
        }
    
        public void sing() {
            System.out.println("I'll have a blue~ Christmas without you~");
        }
    
    }

    ENUM 클래스는 위와 같이 선언해 볼 수 있다. 

    리플렉션 방지

    public class EnumElvisReflection {
        public static void main(String[] args) {
            try {
                // 생성자가 존재는 하지만 못 쓰게 되어있다. ENUM은 그렇게 구현되어있음.
                Constructor<item03.number4.Elvis> declaredConstructor = item03.number4.Elvis.class.getDeclaredConstructor();
                System.out.println(declaredConstructor);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
        }
    }

    ENUM은 리플렉션이 생성자를 불러오는 것을 막아준다. 애초에 ENUM은 생성자를 가져올 수가 없도록 구현 되어있다. 컴파일 된 코드를 보면 ENUM 클래스의 생성자는 존재하지만 불러올 수 있다. ENUM 자체는 new 키워드를 이용해서 만들 수 없게끔 만들어져 있게 되어있고, 리플렉션에도 이런 것들이 반영된다. 

    ENUM은 인스턴스를 불러올 때 열거형으로 선언한 것들만 쓸 수 있게 된다. 이것은 자바 전체에 작용되기 때문에 ENUM은 안전하게 싱글턴으로 사용할 수 있게 된다. 

    public class EnumElvisSerialization {
    
        public static void main(String[] args) {
    
            // 직렬화를 통해 객체 정보를 파일에 저장.
            try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("elvis.obj"))) {
                out.writeObject(item03.number4.Elvis.INSTANCE);
            } catch (IOException e) {
                e.printStackTrace();
            }
    
            // 역직렬화를 통해 파일로부터 객체 정보를 읽어옴.
            try (ObjectInput in = new ObjectInputStream(new FileInputStream("elvis.obj"))) {
                // readObject()를 이용해서 객체를 읽어오면, 클래스 내부에 있는 readResolve()를 바탕으로 인스턴스를 읽어옴.
                item03.number4.Elvis elvis3 = (item03.number4.Elvis) in.readObject();
                System.out.println(elvis3 == item03.number4.Elvis.INSTANCE);
            } catch (IOException | ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
    }

     

    테스트 코드 보완

    인터페이스가 없다면 테스트 코드에서 Mock 객체를 생성하기 어렵다. 이 어려움은 ENUM 인스턴스에서도 마찬가지로 동작한다. 이 부분을 해결하기 위해서 ENUM이 특정 인스턴스를 구현하도록 하면 손쉽게 Mock 객체를 생성하고 테스트 할 수 있게 된다. 정확하게는 ENUM 클래스의 Mock 객체를 생성하는 것이 아니라 ENUM 클래스의 인터페이스를 구현하는 것이다. 

    그렇지만 크게 문제는 없는 것이 Mock 객체를 생성하는 것은 ENUM 클래스를 테스트 하는 것이 아니라 ENUM 클래스를 사용하는 다른 클래스를 테스트 할 때 사용하는 것이기 때문이다. 

    // ENUM도 인터페이스 구현이 가능함. 
    public enum Elvis implements IElvis{
        INSTANCE, HELLO;
    
        ...
    
    }

     

     


    아이템3 관련 완벽 공략

    https://ojt90902.tistory.com/1341

    댓글

    Designed by JB FACTORY