본문 바로가기
Dev/Effective Java

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

by VIPeveloper 2021. 1. 11.
반응형

1. 서론

저번 포스팅에서는 매개변수가 많다면, 빌더타입을 고려하라는 지침에 대해 배웠습니다. 점층적 생성자 타입, 자바빈즈를 거쳐 빌더타입의 탄생?을 보는 역사를 공부해보았습니다. 이번에는 private 한 생성자나 열거 타입을 지정해서 싱글톤임을 보증하는 법에 대해 포스팅해보려합니다.

2. 본론

Q. 싱글톤은 뭘까? 들어는 봤는데,, 자세하겐 몰라!

A. 싱글톤은 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말합니다. 생존범위는 App 안 입니다.

Q. 해당 클래스는 하나의 인스턴스를 만들기 위해 존재한다니.. 먼가 대단히 중요한 일을 하는 것 같아!

A. 한번 객체가 생성되면 그 하나의 인스턴스로 앱 내에서 살아가야 합니다. 다른 메모리를 사용하지 않기 때문에 메모리 낭비가 없죠. 또한, 전역적인 성격을 띄므로 다른 객체와의 공유가 가능합니다.

하지만 멀티 쓰레드 환경에서 동기화 처리 문제가 있습니다.

Q. 멀티 쓰레드 환경.. 이거뭐지..? 구글링 ㄱㄱ


더보기

쉽게 말하면, 음악들으면서 코딩하는 행위를 멀티 쓰레드 환경이라고 합니다. 

장점

1. 응답성 

 : 이 때문에 프로그램의 일부분이 중단되거나, 긴 작업을 수행하더라도 프로그램의 수행이 계속되어 사용자에 대한 응답성이 증대됩니다.

걍 쉽게 말해서 음악을 끄더라도 코딩은 계속 할 수 있다고 보면 될 것 같네요.

2. 경제성

 : 프로세스 내부 자원들과 메모리를 공유하는 방식(메인 스레드가 있고, 이걸 멀티 스레드로 나누는 것이므로)이므로 메모리 공간과 시스템 자원소모가 줄어들게 됩니다. 메모리를 공유하기 때문에 자연스럽게 스레드간 통신이 용이하고, 쉽게 데이터를 주고받을 수 있습니다. 

 

단점

1. 임계 영역의 문제

 : 둘 이상의 스레드가 동시에 실행되면 문제를 일으키는 코드 블록. 공유 자원에 동시에 딱 접근한 경우, 프로세스와는 달리 스레드는 데이터 및 동적 메모리를 담당하는 힙 영역을 공유하기 때문에, 어떤 스레드가 다른 스레드에서 사용중인 변수나 자료구조에 접근해서 엉뚱한 값을 읽어오거나 수정할 수 있다. 따라서 동기화가 필수적이다.

 : 동기화를 통해 스레드의 작업 처리 순서 및 공유 자원에 대한 접근을 통제할 수 있다. (Java Synchronized). 하지만 불필요한 부분까지 동기화를 하는 경우, 과도한 lock으로 인해 병목 현상이 발생. 성능이 저하될 수 있습니다. 동기화 방법으로는 뮤텍스와 세마포어가 있습니다.

 : context switching, 동기화 등의 이유로 싱글 코어 멀티 스레딩은 스레드 생성 시간이 오히려 오버헤드로 작용해 단일 스레드보다 느리게 동작할 수 있습니다.

[그림01] 프로세스와 스레드

삼천포로 빠진 김에 멀티 스레드 환경을 구성해보겠습니다. 먼저, 자원을 공유하지 않는 프로세스상태라면 코드는 어떻게 동작할 지 알아보겠습니다.

class Count {
    private int count;
    public int view() {return count++;}
    public int getCount() {return count;}
}

단순히 카운트를 하나씩 더해주는 객체입니다. 

    @Test
    @DisplayName("자원을 공유하지 않는 프로세스")
    public void CountTest1(){
        Count count = new Count();
        for(int i = 0; i < 100; i++) {
            for (int j = 0; j < 100; j++) {
//                System.out.println(count.view());
                count.view();
            }
        }
        assertEquals(10000,count.getCount());
    }

[그림02] 프로세스 결과

이제 이것을 스레드 환경에서 돌리면 어떻게 될까요?

    @Test
    @DisplayName("자원을 공유하는 스레드")
    public void CountTest2(){
        Count count = new Count();
        for(int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 100; j++) {
//                    System.out.println(count.view());
                    count.view();
                }
            }).start();
        }
        assertNotEquals(10000,count.getCount());
    }

자원을 공유하기 때문에 전혀 다른 결과를 발생시키는 것을 확인할 수 있었습니다. 직접 코드로 보니까 너무 신기하네요!

[그림03] 스레드 결과

스레드 환경에서는 자원이 공유되기 때문에 뒤죽박죽 순서가 결정되고 있는 것을 확인할 수 있었습니다. 마지막으로 이를 해결하기 위한 방법까지만 코드작성을 해보겠습니다. 

class Count {
    private int count;
    public synchronized int view() {return count++;}	// 리팩토링
}

바로 이렇게 synchronized를 붙이면 됩니다. 테스트 코드는 같으니 테스트를 해보겠습니다.

[그림04] 어느정도의 sync를 가지는 스레드

완벽하게 프로세스처럼 동작하는 것은 아니지만, 어느정도 순서를 가지게 됨을 볼 수 있습니다. 어쩌다보니 OS 포스팅이 되어버렸는데, 다시 돌아와서 싱글톤 만드는 방법에 대해 알아보겠습니다.

 

싱글톤 만드는 방법

1. 필드 방식의 싱글톤 만들기

 : 싱글턴 인스턴스를 public static final 필드로 만들고, 생성자를 private하게 지정합니다. 이렇게 하면 외부에서 기본생성자를 생성할 수 없습니다.

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

또한, 같은 인스턴스를 참조하므로 두 객체는 같습니다. 테스트 코드를 보겠습니다.

    @Test
    @DisplayName("두 객체가 같은 인스턴스를 참조함")
    public void PersonTest1(){
//        Person item1 = new Person();
        Person item2 = Person.INSTANCE;
        Person item3 = Person.INSTANCE;
        assertEquals(item2,item3);
    }

주의) 예외가 있습니다. Java Reflection API를 이용하면 싱글톤이라 할 지라도 다른 객체를 생성해낼 수 있습니다.

    @Test
    public void PersonTest2() throws Exception {
        Person item = Person.INSTANCE;
        Constructor<Person> constructor = (Constructor<Person>) item.getClass().getDeclaredConstructor();
        constructor.setAccessible(true);
        Person item2 = constructor.newInstance();
        assertNotEquals(item2,item);
    }

Q. 싱글톤으로 분명히 만들었는데.. 이놈 도대체 어떤놈이야!

A. 주로 프레임워크를 구성하는 프로젝트에서 쓰이고 있고, 구체적이지 않은 객체를 받아 동적으로 해결해주는 장치라고 생각하면 됩니다.(참고4)

이를 방어하기 위해서는 private한 생성자에서 방어코드를 작성해야합니다.

class Person {
    public static final Person INSTANCE = new Person();
    private Person() {
        if(INSTANCE != null){
            throw new RuntimeException("이미 생성된 싱글톤 객체가 존재합니다.");	// 리팩토링
        }
    }
}

[그림05] test2를 다시 실행해보았을 때 발생하는 에러.

그래서 테스트 코드도 다시 리팩토링해줍니다.

    @Test(expected = InvocationTargetException.class)   // 리팩토링
    @DisplayName("예외가 발생함")
    public void PersonTest2() throws Exception {
        Person item = Person.INSTANCE;
        Constructor<Person> constructor = (Constructor<Person>) item.getClass().getDeclaredConstructor();
        constructor.setAccessible(true);
        Person item2 = constructor.newInstance();
        assertNotEquals(item2,item);
    }

expected 문법을 이용하면, 이렇게 실패할 것이다! 를 알 수 있도록 도와줍니다.

 

2. 정팩매를 public method로 제공하기

아이템1에서 배웠던 정적 팩토리 메서드를 public 하게 만들어 싱글톤을 반환해줍니다.

class Person {
    public static final Person INSTANCE = new Person();
    private Person() {
        if(INSTANCE != null){
            throw new RuntimeException("이미 생성된 싱글톤 객체가 존재합니다.");
        }
    }
    // 리팩토링
    public static Person getInstance(){
        return INSTANCE;
    }
}

이런 구성으로 설정하면 정팩매 수정만으로 싱글톤 유무를 결정할 수 있고, 제네릭 싱글턴 팩토리로 만들어 타입에 유연하게 대처할 수 있습니다. 또 공급자로 만들 수 있습니다.

    @Test
    @DisplayName("정팩매 사용하기")
    public void PersonTest3(){
        Supplier<Person> barSupplier = Person::getInstance;
        Person person1 = barSupplier.get();
        Person person2 = barSupplier.get();
        assertEquals(person1,person2);
    }

 

3. Enum 방식의 싱글턴

enum Phone_item3{
    INSTANCE("Galaxy");

    private String name;

    private Phone_item3(String name){
        this.name = name;
    }
}

이 방식은 약간 억지인 것 같긴 한데, 이런 방식으로도 싱글톤을 만들 수 있구나! 하는 정도만 알면 될 것 같다.

 

 

3. 결론

. ENUM, 정팩매, 필드 방식을 이용한 싱글톤을 만들어 보았다.

 

. 오늘 정말 놀라운 경험을 했다. 정말 놀랐다. 난 그저 웹 개발자일 뿐인데,, CS 지식을 왜 배워야하지? 라고 평소 생각했었는데, JAVA를 이해하기 위해서 프로세스와 스레드의 개념, 멀티 스레딩의 개념을 다시 한번 짚고 넘어가보니 이해가 확 와닿았다. 옛말 틀린거 하나 없다. CS 지식에 꾸준하자.

 

. 테스트 코드를 작성하고 > 성공 > 코드를 바꾸고 > 실패 > 리팩토링 > 성공 > 테스트 코드를 수정의 프로세스를 경험해보았다. 코드를 작성할 때 의미 있는 작업이 될 것이라고 생각한다.

 

 

참고1 : [운영체제] 멀티스레드 : Multi-thread (장단점, 멀티프로세스와 차이)

 

참고2 : [운영체제] 프로세스와 스레드 : Process vs. Thread

 

참고3 : [Java] Multi Thread환경에서 동시성 제어를 하는 방법

 

참고4 : Reflection API 간단히 알아보자.

 

 

 

 

반응형