본문 바로가기
쿤즈 Dev/Design pattern

[Design Pattern] 싱글톤 패턴: 자바에서의 효율적인 인스턴스 관리

by Koonz:) 2024. 7. 5.
728x90

소프트웨어 개발에서는 종종 애플리케이션 전역에서 하나의 인스턴스만 필요하거나 허용되는 객체를 사용해야 하는 상황이 발생합니다. 이때 싱글톤 패턴을 사용하면 유용합니다.

 

이번 글에서는 싱글톤 패턴의 개념과 그 필요성, 그리고 자바와 스프링 부트에서의 구현 방법을 살펴보겠습니다. 또한, 도서 관리 프로젝트 혹은 커피 주문 프로젝트를 예제로 들어 싱글톤 패턴을 설명하겠습니다.


싱글톤 패턴이란 무엇인가

싱글톤 패턴(Singleton Pattern)은 특정 클래스의 인스턴스가 하나만 생성되도록 보장하는 디자인 패턴입니다. 이 패턴은 전역적으로 접근 가능한 하나의 인스턴스를 제공하며, 이를 통해 리소스를 절약하고 일관성을 유지할 수 있습니다.

싱글톤 클래스

 

싱글톤 패턴은 다음과 같은 상황에서 유용합니다.

  • 애플리케이션 설정이나 환경 설정을 관리하는 클래스
  • 로깅 클래스
  • 데이터베이스 연결 관리 클래스
  • 캐시 클래스

싱글톤 패턴의 구현 방법

자바에서 싱글톤 패턴을 구현하는 방법은 여러 가지가 있지만, 그중에서도 가장 일반적인 방법은 다음과 같습니다.

  1. 이른 초기화 (Eager Initialization)
  2. 지연 초기화 (Lazy Initialization)
  3. 이중 검사 락 (Double-Checked Locking)
  4. Bill Pugh Signleton Design

각 방법을 간단히 설명하고, 실제 예제를 통해서 이해해볼게요.

 

이른 초기화 (Eager Initialization)

이 방법은 클래스 로딩 시점에 인스턴스를 생성합니다. 이 방법은 구현이 간단하지만, 인스턴스가 필요하지 않을 때도 생성된다는 단점이 있습니다.

public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();
    
    private EagerSingleton() {}
    
    public static EagerSingleton getInstance() {
        return instance;
    }
}

 

지연 초기화 (Lazy Initialization)

이 방법은 인스턴스가 처음 필요할 때 생성합니다. 이는 메모리 절약에 도움이 되지만, 멀티스레드 환경에서 문제가 발생할 수 있습니다.

public class LazySingleton {
    private static LazySingleton instance;
    
    private LazySingleton() {}
    
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

 

이중 검사 락 (Double-Checked Locking)

이 방법은 Lazy Initialization의 단점을 보완하여 멀티쓰레드 환경에서도 안전하게 사용할 수 있습니다.

public class DoubleCheckedLockingSingleton {
    private static volatile DoubleCheckedLockingSingleton instance;
    
    private DoubleCheckedLockingSingleton() {}
    
    public static DoubleCheckedLockingSingleton getInstance() {
        if (instance == null) {
            synchronized (DoubleCheckedLockingSingleton.class) {
                if (instance == null) {
                    instance = new DoubleCheckedLockingSingleton();
                }
            }
        }
        return instance;
    }
}

 

Bill Pugh Singleton

이 방법은 가장 효율적인 싱글톤 패턴 구현 방법 중 하나로, 내부 정적 클래스의 인스턴스를 사용하는 방법입니다.

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

스프링 부트에서의 싱글톤 패턴

스프링 부트에서는 기본적으로 모든 빈(Bean)이 싱글톤으로 관리됩니다. 스프링 부트에서 싱글톤 빈을 설정하는 방법은 매우 간단합니다. 예를 들어, 스타벅스에서 커피를 주문하고 주문된 내역을 간단하게 알아보는 프로젝트를 만들어 보겠습니다. 우선 OrderManager 클래스를 스피링 부트에서 싱글톤 빈으로 관리하려면 다음과 같이 설정할 수 있습니다.

 

🧑🏻‍💻 Github 링크
https://github.com/koonsland/design-patterns.git

 

package com.koonsland.designpatterns.creational.singleton.bean;

import java.util.ArrayList;
import java.util.List;

public class OrderManager {
    private static OrderManager instance;
    private List<String> orders;

    private OrderManager() {
        orders = new ArrayList<>();
    }

    public static OrderManager getInstance() {
        if (instance == null) {
            synchronized (OrderManager.class) {
                if (instance == null) {
                    instance = new OrderManager();
                }
            }
        }
        return instance;
    }

    public void addOrder(String order) {
        orders.add(order);
    }

    public List<String> getOrders() {
        return orders;
    }
}

 

OrderManager 자체를 우리는 getInstance()라는 메서드를 이용해서 싱글톤으로 관리할 수 있습니다. 하지만 스프링에서는 어노테이션을 이용해서 싱글톤으로 만들어 줄 수 있습니다. AppConfig 클래스를 만들고 OrderManager를 싱글톤으로 등록해 보겠습니다.

 

package com.koonsland.designpatterns.creational.singleton.bean;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public OrderManager orderManager() {
        return OrderManager.getInstance();
    }
}

 

클래스 내에 OrderManager를 orderManager() 메서드에서 인스턴스를 반환합니다. 그리고 이 클래스를 @Bean 어노테이션을 이용해서 싱글톤으로 등록시켜 줍니다. 이 경우에 OrderManager는 프로젝트에서 싱글톤으로 관리하게 됩니다.

 

이제 OrderService 클래스를 만들고 OrderController에서 호출해 볼게요.

 

package com.koonsland.designpatterns.creational.singleton.service;

import com.koonsland.designpatterns.creational.singleton.bean.OrderManager;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class OrderService {
    private final OrderManager orderManager;

    // 생성자 주입
    public OrderService(OrderManager orderManager) {
        this.orderManager = orderManager;
    }

    public void placeOrder(String order) {
        orderManager.addOrder(order);
    }

    public List<String> getOrders() {
        return orderManager.getOrders();
    }
}

 

OrderService 클래스에서는 스프링부트 애플리케이션이 로드되고 클래스가 로드될 때 OrderManager 클래스를 생성자로 주입받습니다. 이때, 주입이 가능한 이유는 OrderManager는 AppConfig에서 인스턴스가 반환되는 @Bean으로 등록이 되어 있는 싱글톤이기 때문입니다. 만약 빈 주입을 하지 않으면 아래와 같은 오류가 발생합니다. @Bean을 제거하고 애플리케이션을 다시 실행해 볼게요.

 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in com.koonsland.designpatterns.creational.singleton.service.OrderService required a bean of type 'com.koonsland.designpatterns.creational.singleton.bean.OrderManager' that could not be found.


Action:

Consider defining a bean of type 'com.koonsland.designpatterns.creational.singleton.bean.OrderManager' in your configuration.

Disconnected from the target VM, address: '127.0.0.1:49969', transport: 'socket'

Process finished with exit code 1

 

OrderService의 생성자의 paramter는 bean이 필요하다는 내용입니다.

 

이제 컨트롤러를 작성해 볼게요.

 

package com.koonsland.designpatterns.creational.singleton;

import com.koonsland.designpatterns.creational.singleton.dto.OrderDto;
import com.koonsland.designpatterns.creational.singleton.example.BillPughSingleton;
import com.koonsland.designpatterns.creational.singleton.example.DoubleCheckedLockingSingleton;
import com.koonsland.designpatterns.creational.singleton.example.EagerSingleton;
import com.koonsland.designpatterns.creational.singleton.example.LazySingleton;
import com.koonsland.designpatterns.creational.singleton.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/creational/singleton")
public class SingletonController {
    private final OrderService orderService;

    public SingletonController(OrderService orderService) {
        this.orderService = orderService;
    }

    @GetMapping("/eager")
    public String getSingleton() {
        EagerSingleton instance = EagerSingleton.getInstance();
        return "Singleton Instance: " + instance.toString();
    }

    @GetMapping("/double-checked-locking")
    public String getDoubleCheckedLocking() {
        DoubleCheckedLockingSingleton instance = DoubleCheckedLockingSingleton.getInstance();
        return "Double Checked Locking Intance: " + instance.toString();
    }

    @GetMapping("/lazy")
    public String getLazy() {
        LazySingleton instance = LazySingleton.getInstance();
        return "Lazy Instance: " + instance.toString();
    }

    @GetMapping("/bill-pugh")
    public String getBillPugh() {
        BillPughSingleton instance = BillPughSingleton.getInstance();
        return "Bill Pugh Instance: " + instance.toString();
    }

    @GetMapping("/orders")
    public List<OrderDto> getOrders() {
        return orderService.getOrders()
                .stream()
                .map(OrderDto::new)
                .toList();
    }

    @PostMapping("/orders")
    public void placeOrder(@RequestBody OrderDto orderDto) {
        orderService.placeOrder(orderDto.getName());
    }
}

 

추가적으로 @RestController, @Service, @Configuration 모두 @Component로 관리되는 어노테이션이며 이들 모두 역시 싱글톤으로 관리되는 클래스들입니다. 여기까지 작성이 완료되었다면 프로젝트를 실행합니다.

 

테스트는 rest를 이용한 http 파일에서 실행해 보았습니다.

### 주문
POST localhost:8080/creational/singleton/orders
Content-Type: application/json

{
  "name": "아이스 아메리카노"
}


### 주문 전체 조회
GET localhost:8080/creational/singleton/orders

 

POST를 실행하고 GET을 실행하면 다음과 같이 저장된 데이터가 return 됩니다.

GET http://localhost:8080/creational/singleton/orders

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 29 Jun 2024 03:58:40 GMT
Keep-Alive: timeout=60
Connection: keep-alive

[
  {
    "name": "아이스 아메리카노"
  }
]
Response file saved.
> 2024-06-29T125840.200.json

Response code: 200; Time: 17ms (17 ms); Content length: 22 bytes (22 B)

 

이렇게 스프링 부트에서 싱글톤 패턴을 활용하면, 애플리케이션 전역에서 하나의 인스턴스를 관리할 수 있습니다. 스프링의 DI(Dependency Injenction)을 통해 더욱 간편하게 싱글톤 인스턴스를 사용할 수 있으며, 이는 코드의 일관성과 유지보수성을 크게 향상시킵니다.


싱글톤 패턴은 소프트웨어 개발에서 매우 유용한 디자인 패턴입니다. 이 패턴을 통해 리소스를 절약하고, 일관성을 유지하며, 코드의 유지보수성을 높일 수 있습니다. 자바와 스프링 부트에서 싱글톤 패턴을 구현하는 방법을 잘 이해하고 적용하면, 보다 효율적이고 안정적인 애플리케이션을 개발할 수 있을 것입니다.

 

다음 글에서는 다른 생성 패턴들에 대해서 알아보겠습니다. 싱글톤 패턴 외에도 팩토리 메서드 패턴, 추산 팩토리 패턴, 빌더 패턴 등 다양한 생성 패턴들이 있으며, 각 패턴들은 각기 다른 상황에서 유용하게 사용될 수 있습니다.

댓글