@senspond

>

개발>프론트 엔드

타입스크립트의 클로저, 클래스, 데코레이터로 싱글톤 패턴 구현하기

등록일시 : 2024-05-22 (수) 07:59
업데이트 : 2024-05-24 (금) 09:12
오늘 조회수 : 2
총 조회수 : 150

    타입스크립트의 클로저, 클래스, 데코레이터로 싱글톤 패턴 구현하기 정리를 해봤습니다.

    정말 오랜만에 글을 씁니다. ㅠㅠ

    타입스크립트의 클로저, 클래스, 데코레이터로 싱글톤 패턴 구현하기 정리를 해봤습니다.


    서론 : Java 어노테이션

    Java에서는 커스텀 어노테이션을 정의해서 코드를 간결하게 만들 수가 있는데요.

    /**
     * 자바 클래스를 JSON 형태로 출력해주는 ToString 어노테이션
     */
    @Documented
    @Target(ElementType.TYPE) // TYPE 으로 지정하면, 인터페이스 ,클래스, enum에 지정이 가능함.
    @Retention(RetentionPolicy.SOURCE) // 소스레벨에서만 유지하고, 컴파일시 애노테이션 프로세서로 소스코드를 생성할것임
    public @interface ToJsonString {
    
        /**
         * 예쁘게 출력해주는 옵션 (기본값 false)
         */
        boolean isPretty() default false;
    }

    @interface를 통해 어노테이션을 정의하고

    @SupportedAnnotationTypes("com.rgbitcode.core.annotation.ToJsonString")
    @SupportedSourceVersion(SourceVersion.RELEASE_17)
    public class JsonStringProcessor extends AbstractProcessor {
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
          	// (...생략...)
            return true;
        }
    }

    어노테이션 프로세스로 어떻게 처리할지 정의해줄 수 있습니다.

     @ToJsonString(isPretty = true)
     public static class ATest {
         private String user;
         private String password;
    
         public ATest(String user, String password) {
             this.user = user;
             this.password = password;
         }
     }


    그리고 이렇게 간단하게 어노테이션 한줄을 추가해주는 것만으로 기능을 추가하거나 제거할 수가 있습니다.

    엄밀하게는 어노테이션은 일종의 껍데기에 불과하지만, 어노테이션 프로세서, AOP 등으로 해당 어노테이션에 기능을 만들어 주는 것입니다. Spring 프레임워크에서는 다양한 어노테이션과 기능 들이 사전에 미리 정의되어있는데요. 예를 들어

    @Service, @Controller 같은 어노테이션들이 있습니다.


    그런 일부 어노테이션은 클래스를 싱글톤으로 만들어 주게되는데요.

    클라이언트에서 요청이 올 때마다 매번 DB접근 등이 담긴 각 로직을 담당하는 객체를 새로 만들어서 사용한다면 서버에 많은 부하로 인해 최악의 경우 서버가 뻗어버리거나 하는 문제가 발생 할 수 있기에.. 스프링 프레임워크에서는 그런 비지니스 로직들은 싱글톤으로 처리하고 객체의 생명주기를 관리해주고 있습니다.


    마찬가지로 React의 상태관리 라이브러리 Redux, Mobx 들도 마찬가지로 전역으로 관리할 상태에 대해서 공유객체로 만들어 싱글톤으로 관리를 합니다. 그리고 서버와 통신하는 AJAX 같은 로직을 상태관리 라이브러리와 같이 사용하고는 합니다. 그리고 요청 상태가 변경 되었을 때만 새롭게 데이터를 fecth 해도록 해서 서버에 요청하는 부하를 줄이는 기법들이 발전했습니다.


    아무튼 그럼 이제 싱글톤 디자인패턴에 대해서 애기를 해봅니다.


    싱글톤 디자인패턴

    싱글톤 디자인패턴이란 객체가 오직 한개만 생성되도록 클래스를 디자인하는 것입니다.


    Java

    기본적인 싱글톤 패턴

    class Singleton {
        private static Singleton myInstance = null;
    
        // 외부에서 생성자 호출 금지
        private Singleton() {}
    
        public static Singleton getInstance() {
            if (myInstance == null) {
                myInstance = new Singleton();
            }
    
            return myInstance;
        }
    }

    위는 기본적인 싱글톤 디자인 패턴입니다. 하지만 위의 구현은 멀티쓰레드 환경에서는 type-safe하지 않을 수 있습니다.

    즉 멀티쓰레드 환경에서 두개이상의 객체가 만들어 질 수 있다는 말입니다. 싱글톤으로 설계하고 로직을 만들게 되면 싱크가 맞지 않는 동시성 문제가 발생할 수 있습니다.


    LazyHolder 싱글톤 패턴

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

    위는 Java 멀티쓰레드 환경에서 type-safe한 LazyHolder 싱글톤 패턴입니다.

    그외 다른 방법들도 있지만 가장 많이 사용되는 패턴입니다.


    하지만 자바스크립트의 경우 싱글 쓰레드로 동작을 하기에.. Java처럼 멀티쓰레드 환경을 고려할 필요 없습니다.


    TypeScript

    클로저(closure)를 통한 방법

    아래는 클로저(closure)를 통해 구현한 싱글톤입니다.

    const SingletonCounterStore = (function () {
        let instance:object|null = null;
    
        class CounterStore {
            number = 0;
    
            // private 로 외부에서 생성자 호출 금지
            constructor() {
                console.log('생성됨')
            }
            increase = () => {
                this.number++;
            };
        }
    
        return {
            getInstance() {
                if (!instance) {
                    instance = new CounterStore();
                }
                return instance;
            }
        };
    })();
    
    const s1 = SingletonCounterStore.getInstance();
    const s2 = SingletonCounterStore.getInstance();
    
    console.log(s1);
    console.log(s2);
    console.log(s1 === s2); // true

    자바스크립트에서 위와 같은 방식으로 사용할 수 있습니다.


    클래스를 통해 Java 스럽게 구현한 방법

    아래는 좀더 Java 스럽게 구현한 싱글톤 패턴입니다.

    위 자바 첫번째 싱글톤 패턴과 거의 똑같습니다. 자바 개발자가 좋아합니다.

    class CounterStore {
        private static instance: object | null = null;
        private number: number = 0;
    
        // 이 클래스 내부에서만 접근 가능. 외부에서 생성자 호출 금지
        private constructor() {
            console.log('생성됨')
        }
    
        static getInstance() {
            return this.instance || (this.instance = new this())
        }
        public increase = () => {
            this.number++;
        };
        public getNumber() {
            return this.number
        }
    }

    자바스크립트에서는 아래 같은 코드는 불가능하고 타입스크립트를 써야만 가능합니다.

    생성자를 private 로 만들어 내부에서만 접근 가능하도록 하고 getInstance를 통해 간접적으로 접근하도록 합니다.

    const s1 = CounterStore.getInstance() as CounterStore;
    const s2 = CounterStore.getInstance() as CounterStore;
    console.log(s1 === s2);
    s1.increase();
    s1.increase();
    console.log(s1.getNumber(), s2.getNumber())



    그런데, 자바의 어노테이션과는 많이 다르지만 생긴건 비슷하게 생긴 것이 자바스크립트에도 있습니다.

    이번에는 타입스크립트의 데코레이터를 이용해 싱글톤 패턴을 만들어 보겠습니다.


    TypeScript에서 데코레이터로 싱글톤 클래스 정의하기


    싱글톤 미적용 클래스

    아래처럼 클래스를 만들었습니다.

    class CounterStore {
    
      number = 0;
    
      constructor() {
    		console.log('생성됨')
      }
    
      increase = () => {
        this.number++;
      };
    }


    두개의 인스턴스 생성해서 비교

    const a1 = new CounterStore() 
    const a2 = new CounterStore()
    
    console.log(a1 === a2) # 같은 객체인지 비교
    a1.increase();
    a1.increase();
    console.log(a1.number, a2.number)



    데코레이터를 통한 싱글톤 구현

    타입스크립트에서 데코레이터를 사용하기 위해서는 jsconfig.json에 아래처럼 설정이 필요합니다.



    {
      "compilerOptions": {
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        }
     },


    데코레이터 정의

    function Singleton<T extends { new (...args: any[]):{} }>(
    		TargetClass: T): T {
        let instance: object| null = null;
    
        class SingletonClass extends TargetClass {
            constructor(...args: any[]) {
                if (!instance) {
                    super(...args);
                    instance = this;
                }
                return instance;
            }
        }
    
        return SingletonClass;
    }

    약간의 꼼수를 써서 구현한 방법입니다.

    TargetClass를 상속받은 SingletonClass의 생성자에서 TargetClass를 싱글톤 객체로 구성하여 돌려주도록 만들었습니다.

    기존 클래스를 수정하지 않고 장식자 클래스인 SingletonClass를 통해 싱글톤을 구현한 데코레이터라고 할 수 있겠습니다.

    데코레이터 적용

    
    @Singleton
    class CounterStore {
        number = 0;
    
        constructor() {
            console.log('생성됨')
        }
        increase = () => {
            this.number++;
        };
    }

    위 CounterStore와 차이점은 @Singleton 데코레이터가 클래스에 붙어있느냐 뿐입니다.

    @Singleton 의 역할은 장식자클래스를 통해 TargetClass의 싱글톤 객체를 생성해 돌려주는 것입니다.


    
    const a1 = new CounterStore()
    const a2 = new CounterStore()
    
    console.log(a1 === a2) # 같은 객체인지 비교
    a1.increase();
    a1.increase();
    console.log(a1.number, a2.number)



    의도대로 TargetClass의 객체를 여러번 생성해도 생성자가 한번만 호출되는 것을 확인할 수 있고

    a1 인스턴스와 a2 인스턴스가 같은 객체로 true를 반환함을 알 수 있습니다.


    위에서 작성한 데코레이터를 좀더 엄격하게 작성하면 다음과 같습니다.

    interface AbstractClass {
        new (...args: any[]): {};
    }
    
    function Singleton<T extends AbstractClass>(TargetClass: T): T {
        let instance: InstanceType<T> | null = null;
    
        class SingletonClass extends TargetClass {
            constructor(...args: any[]) {
                if (!instance) {
                    super(...args);
                    instance = this as InstanceType<T>;
                } 
    					  return instance;
            }
        }
        return SingletonClass as T;
    }


    senspond

    안녕하세요. Red, Green, Blue 가 만나 새로운 세상을 만들어 나가겠다는 이상을 가진 개발자의 개인공간입니다.

    댓글 ( 0 )

    카테고리내 관련 게시글

    현재글에서 작성자가 발행한 같은 카테고리내 이전, 다음 글들을 보여줍니다

    @senspond

    >

    개발>프론트 엔드

    • NextJS useSearchParams() should be wrapped in a suspense boundary at page 오류해결

      NextJS useSearchParams() should be wrapped in a suspense boundary at page 오류해결
        2025-02-28 (금) 01:59
      1. [현재글] 타입스크립트의 클로저, 클래스, 데코레이터로 싱글톤 패턴 구현하기

        타입스크립트의 클로저, 클래스, 데코레이터로 싱글톤 패턴 구현하기 정리를 해봤습니다.
          2024-05-22 (수) 07:59
        1. NextJS App router 방식으로 다국어 설정 간단하게 하기 next-intl

          NextJS App router 방식으로 다국어 설정 하기 next-intl 를 통해 간단하게 적용
            2024-09-18 (수) 03:38
          1. Fetch API 사용시 파일업로드 Multipart: Boundary not found 에러 해결 방법, 수동전송 방법(JAVA)

            Fetch API 사용시 파일업로드 Multipart: Boundary not found 에러 해결 방법을 정리하고 multipart/formdata 수동전송 방법을 정리한 내용입니다.
              2024-02-23 (금) 02:46
            1. NextJs 프로젝트에 Jest 테스트 프레임워크 적용하기

              Jest는 메타(구 페이스북)가 유지보수하는 재스민(jasmine) 위에서 빌드되는 테스팅 프레임워크입니다. 이 글은 NextJs 프로젝트에 Jset 프레임워크를 적용하는 방법을 정리해봅니다.
                2024-01-19 (금) 11:32