Singleton Pattern

Kevin·2024년 9월 28일
4

Design Pattern

목록 보기
2/2
post-thumbnail

서론

대학교 2학년 때 수강 신청 할 과목들을 찾던 중 “디자인 패턴”이라는 수업을 찾았던 적이 있다.

당시 디자인 패턴이 어떠한 개념인지도 모르고 단순히 멋있어 보이기에 수강 신청을 했다.

수업을 신청한 사람의 수가 부족해 폐강 했지만..

그 때 간단하게 디자인 패턴에 대해서 공부를 해보자! 라고 결심해 디자인 패턴을 공부했던 적이 있다.

그러나 와닿지 않았다.

무엇에 쓰는 것이고, 왜 쓰는지에 대해서 문자로써만 이해를 했지, 내 스스로 왜 사용해야 하는지 납득이 되지 않았다.

일종의 똥 고집이라 할까?

그러다 개발에 대한 경험들이 조금씩 생겨나가면서 더 개발을 잘 할 수 있는, 그리고 더 효율적으로 코드를 작성할 수 있는 방법들이 필요해졌다.

나보다 더 뛰어나고, 능력있는 선배들은 과연 이 문제를 어떻게 해결했을까?

위 생각을 따라가다보면 늘 마지막은 “디자인 패턴”이었다.

그렇다 “디자인 패턴”은 단순한 이론이 아니라 문제라는 큰 협곡을 만났을 때 친절한 천재들이 만들어 놓은 “다리”이다.


Singleton Pattern

개발을 하면서 멤버 변수가 없고, 특정 행위만을 하는 객체(상태가 없는 객체)를 만드는 일들이 잦았다.

이 때 같은 행위를 하기 위해 매번 반복적으로 객체를 생성하는 것이 불필요하게 메모리를 사용하는 일이라고 생각 되었다.

그래서 어떻게 하면 매번 반복적으로 객체를 생성하지 않을 수 있을까라는 생각을 하게 되었고, 여러 방법들을 찾아보니 Singleton Pattern이라는 디자인 패턴을 찾게 되었다.

그리하여 이 글에서는 Singleton Pattern에 대해서 다루고자 한다.

Singleton의 사전적 의미는 ‘단독 개체’ 라는 뜻이다.

이를 프로그래밍적으로 풀어보면 프로그램의 시작부터 종료까지 클래스의 객체를 단 한번만 생성하여, 사용할 수 있게 하는 디자인 패턴을 의미한다.

클래스의 객체를 단 한번만 생성하여, 사용할 수 있게 하는 것이 그렇다면 어떠한 장점이 있을까?

바로 객체를 하나만 생성 함으로써, 고정된 메모리를 사용할 수 있고 이를 통해 필요 없는 메모리 낭비를 줄일 수 있다는 장점이 있다.

이러한 Singleton Pattern을 내가 작성했던 코드를 통해 더 자세한 설명을 해보겠다.

아래는 내가 사용한 Singleton Pattern 코드이다.

public class HeaderCreate {

    private static HeaderCreate headerCreate = null;

    private HeaderCreate() {}

    public synchronized static HeaderCreate newInstance() {

        if (headerCreate == null) {
            headerCreate = new HeaderCreate();
        }

        return headerCreate;
    }

    public void headerCreate(String strXmlPath, HeaderInfVO headerInfVO) {
                
            // 내부 로직...
            
    }
}

Singleton의 핵심은 설명 했던 것처럼 클래스의 객체를 단 한번만 생성하여 사용하는 것이다.

그러기 위해서 생성자를 private으로 선언하여 외부에서 객체를 생성할 수 없게 강제한다.

이를 통해 클라이언트는 HeaderCreate 클래스의 객체를 생성하기 위해서 newInstance() 메서드를 통해 객체를 생성할 것이다.

newInstance() 메서드는 맨 처음 접근시에만 HeaderCreate 클래스의 객체를 생성하여 반환 및 외부에서 접근이 불가한 private 멤버 변수에 저장한다.

그리고 그 다음 접근시에는 객체를 새롭게 생성하지 않고 private 멤버 변수를 반환하게 된다.

위 과정을 통해 Singleton Pattern을 따를 수 있다.


Singleton Pattern에서의 synchronized 키워드

여기서 newInstance() 메서드에 synchronized 키워드를 왜 넣었을까?

synchronized는 멀티 쓰레드 환경에서 현재 해당 데이터 및 메서드를 사용하고 있는 쓰레드를 제외하고 나머지 쓰레드의 접근을 막는 키워드이다.

바로 여러 쓰레드가 동시에 newInstance() 메서드를 처음 호출 했을 때 여러 객체가 생기는 문제를 방지하기 위해서이다.


Singleton Pattern 단점

이러한 Singleton Pattern은 장점만 있을까?

단점도 있다.

바로 Singleton 객체의 역할이 많아지거나, 많은 데이터를 공유하게 된다면 SRP(Single Responsibility Principle), DIP(Dependency Inversion Principle), OCP(Open-Closed Principle) 를 어기게 될 수 있다.

먼저 SRP 원칙의 경우 아래의 예시를 통해 어길 수 있다.

public void headerCreate(String strXmlPath, HeaderInfVO headerInfVO) {
    // 헤더 생성 로직

    // 헤더 검증 로직 추가 (SRP 위반)
    if (!validateHeader(headerInfVO)) {
        throw new IllegalArgumentException("Invalid Header");
    }

    // 헤더 저장 로직 추가 (SRP 위반)
    saveHeaderToDatabase(headerInfVO);
}

private boolean validateHeader(HeaderInfVO headerInfVO) {
    // 헤더 검증 로직
    return true;
}

private void saveHeaderToDatabase(HeaderInfVO headerInfVO) {
    // 데이터베이스 저장 로직
}

// 더... 더... 많이...

HeaderCreate 클래스의 역할은 Header 파일을 만드는 것이다.

그러나 Singleton 유틸성으로 인해 더 많은 역할을 부여해 사용하고자 하는 욕심이 들게 된다.

그래서 헤더를 검증하는 로직도 넣고.. 데이터베이스에 이러한 헤더 파일을 저장하는 로직도 추가하게 된다.

결국 이는 SRP 원칙을 어기게 되는 이유가 된다.

그 다음 DIP 원칙의 경우 아래의 예시를 통해 어길 수 있다.

public class OrderService {

    // OrderService가 HeaderCreate의 구체 구현에 의존하고 있음
    private HeaderCreate headerCreate = HeaderCreate.newInstance();

    public void processOrder(Order order) {
        HeaderInfVO headerInfVO = new HeaderInfVO();
        String xmlPath = "path/to/order.xml";

        // 구체적인 클래스의 메서드를 직접 호출
        headerCreate.headerCreate(xmlPath, headerInfVO);
        
        System.out.println("Order processed.");
    }
}

DIP의 핵심은 상위 모듈이 하위 모듈에 직접적으로 의존하는 것이 아니라, 추상화에 의존 해야 하는데, OrderService의 의존성이 구체적인 클래스인 HeaderCreate를 사용하기에 DIP를 어기게 된다.

위 DIP 원칙을 어기는 부분을 생각하면서 아래와 같은 질문이 생겼다.

그런데 이러면 사실상 Spring에서 싱글톤인 Bean 객체들 또한 클래스를 그대로 사용하기에 Spring도 DIP를 어기고 있는거 아니야?

위 질문에 대한 결론부터 말하자면 Spring은 기가 막히게 DIP를 준수하고 있다.


Spring은 Singleton으로 객체를 관리하면서 DIP를 어기지 않을 수 있는 이유

Spring이 DIP 원칙을 위반하지 않는 이유는 클래스 설계에서의 추상화 의존과 외부에서의 의존성 주입에 있다.

public class OrderService {

    // OrderService가 HeaderCreate의 구체 구현에 의존하고 있음
    private HeaderCreate headerCreate = HeaderCreate.newInstance();

    public void processOrder(Order order) {
        HeaderInfVO headerInfVO = new HeaderInfVO();
        String xmlPath = "path/to/order.xml";

        // 구체적인 클래스의 메서드를 직접 호출
        headerCreate.headerCreate(xmlPath, headerInfVO);
        
        System.out.println("Order processed.");
    }
}

위 예시를 다시 보자.

HeaderCreate를 객체를 생성하는 책임을 현재 OrderService가 가지고 있다.

이는 객체에서 어떤 Class를 참조해서 사용해야하는 상황이 생긴다면, 그 Class를 직접 참조하는 것이 아니라 그대상의 상위 요소(추상 클래스 or 인터페이스)로 참조하라는 원칙인 DIP를 어기게 된다.

@Service
@Requiredargsconstructor
public class OrderService {

    private final HeaderCreate headerCreate;

    public void processOrder(Order order) {
        HeaderInfVO headerInfVO = new HeaderInfVO();
        String xmlPath = "path/to/order.xml";

        // DI를 통해 주입된 의존성을 확인
        headerCreate.headerCreate(xmlPath, headerInfVO);
        
        System.out.println("Order processed.");
    }
}

위 Spring 코드를 살펴보자.

외부(Spring Container)에서 객체 Bean(HeaderCreate)을 먼저 생성 해두고, 이를 지정한 객체(OrderService)에 의존성을 주입 해준다.

즉, 외부에서 의존성을 주입 해주기 때문에 DIP를 어기지 않게 되는 것이다.

다음 글에서 Spring에서 객체를 싱글톤으로 관리하는 방법을 작성 해보고자 한다.

중요한 것은 객체 생성의 책임이 의존 객체를 사용하는 클래스에 없다는 점이다.

profile
Hello, World! \n

0개의 댓글