[면접을 위한 cs지식 ]디자인 패턴 싱글톤 패턴

GoldenDusk·2023년 8월 1일
0

CS지식

목록 보기
4/26

디자인 패턴과 프로그래밍 패러다임

라이브러리 vs 프레임워크

라이브러리

  • 공통으로 사용될 수 있는 특정한 기능을 모듈화 한 것을 의미
  • 폴더명, 파일명 등에 대한 규칙이 없고 프레임워크에 비해 자유롭다.
  • 무언가를 자를 때 도구인 가위를 사용해서 내가 직접 컨트롤하는 것

프레임워크

  • 공통으로 사용될 수 있는 특정한 기능들을 모듈화한 것을 의미
  • 폴더명, 파일명 등에 대한 규칙이 있으며 라이브러리에 비해 좀 더 엄격하다.
  • 다른 곳으로 이동할 때 도구인 비행기를 타고 이동하지만 비행기가 컨트롤 하는 것

싱글톤 패턴(singleton pattern)

  1. 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴. 보통 데이터베이스 연결 모듈에 많이 사용된다.
  2. 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴
  3. 하나의 인스턴스만을 생성하며 getInstance메서드모든 클라이언트에게 동일한 인스턴스를 반환
  4. private 생성자를 가지는 특징을 가지며, 생성된 싱글톤 오브젝트는 저장할 수 있는 자신과 같은 타입의 스태틱 필드를 정의

1. 자바스크립트의 싱글톤 패턴

  1. 자바스크립트에서는 리터럴{} 또는 new Object로 객체 생성하면 다른 어떤 객체와도 같지 않기에 이 자체로 싱글톤 구현 가능
const obj ={
a : 27
}

const obj2 = {
a : 27
}

console.log(obj == obj2)
//false
  • 원칙은 하나의 클래스에 하나의 인스턴스
  • Singleton.instance라는 하나의 인스턴스를 가지는 클래스
class Singleton{
     constructor(){
          if(!Singleton.instance){
               Singleton.instance = this
          }
          return Singleton.instance
     }

     getInstance(){
          return this.instance
     }
}

const a = new Singleton()
const b = new Singleton()

2. 데이터 베이스 연결 모듈

const URL = 'mongodb://localhost:27017/kundolapp' 
const createConnection = url => ({"url" : url})    
class DB {
    constructor(url) {
        if (!DB.instance) { 
            DB.instance = createConnection(url)
        }
        return DB.instance
    }
    connect() {
        return this.instance
    }
}
const a = new DB(URL)
const b = new DB(URL) 
console.log(a === b) // true

3. 그렇다면… 싱글톤 패턴을 데이터 베이스 연결 모듈에 많이 쓰는 이유는 뭘까?

  1. 리소스 절약
  • 데이터베이스 연결은 비용이 많이 드는 작업 중 하나
  • 매번 데이터베이스 연결을 열고 닫는 대신에 싱글톤 패턴을 사용하면 하나의 연결을 재사용하므로 리소스를 절약
  1. 성능 개선
  • 매번 데이터베이스 연결을 새로 만드는 것보다 기존 연결을 재사용하는 것이 빠름
  • 싱글톤 패턴을 사용하면 객체를 필요할 때마다 생성하는 비용을 줄여서 성능을 향상
  1. 데이터 일관성 유지
  • 싱글톤 패턴을 사용하면 항상 하나의 연결만을 사용하므로 데이터 일관성을 보다 쉽게 유지할 수 있다.
  1. 중복 연결 방지
  • 싱글톤 패턴을 사용하면 여러 곳에서 동시에 데이터베이스 연결을 생성하는 것을 방지할 수 있다.
  • 애플리케이션 전체에서 하나의 연결을 공유할 수 있다.
  1. 스레드 안전성
  • 멀티스레드 환경에서 데이터베이스 연결을 처리해야 할 때, 싱글톤 패턴을 사용하면 스레드 안정성을 보장
  • 하나의 인스턴스만을 사용하므로 동시에 여러 스레드가 데이터베이스 연결에 접근하는 것을 방지

싱글톤 패턴은 데이터베이스 연결을 관리하는 데에 많은 장점을 제공하지만, 남용할 경우 애플리케이션의 유지보수와 테스트가 어려워질 수 있다.

4. 자바에서 싱글톤 패턴

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

public class HelloWorld{ 
     public static void main(String []args){ 
        Singleton a = Singleton.getInstance(); 
        Singleton b = Singleton.getInstance(); 
        System.out.println(a.hashCode());
        System.out.println(b.hashCode());  
        if (a == b){
         System.out.println(true); 
        } 
     }
}
/*
705927765
705927765
true

1. 클래스안에 클래스(Holder), static이며 중첩된 클래스인 singleInstanceHolder를 
기반으로 객체를 선언했기 때문에 한 번만 로드되므로 싱글톤 클래스의 인스턴스는 애플리케이션 당 하나만 존재하며 
클래스가 두 번 로드되지 않기 때문에 두 스레드가 동일한 JVM에서 2개의 인스턴스를 생성할 수 없습니다. 
그렇기 때문에 동기화, 즉 synchronized를 신경쓰지 않아도 됩니다. 
2. final 키워드를 통해서 read only 즉, 다시 값이 할당되지 않도록 했습니다.
3. 중첩클래스 Holder로 만들었기 때문에 싱글톤 클래스가 로드될 때 클래스가 메모리에 로드되지 않고 
어떠한 모듈에서 getInstance()메서드가 호출할 때 싱글톤 객체를 최초로 생성 및 리턴하게 됩니다. 
*/

5. 자바에서는 왜 싱글톤 패턴을 쓸까?

  1. 리소스 절약
  • 자원이 많이 소모되는 객체를 여러 개 생성하는 것보다 하나의 인스턴스를 재사용하는 것이 리소스를 절약
  • 이는 메모리, CPU, 디스크 I/O 등 다양한 자원을 아낄 수 있다.
  1. 공유 데이터와 상태 유지
  • 싱글톤 패턴을 사용하면 여러 객체 간에 동일한 상태를 공유할 수 있다.
  • 따라서 여러 곳에서 같은 데이터를 사용해야 하는 경우 유용하게 사용될 수 있다.
  1. 설정 정보 관리
  • 애플리케이션의 설정 정보를 싱글톤으로 유지하면, 여러 컴포넌트에서 해당 설정 정보에 접근할 수 있고, 변경이 필요한 경우 일관된 방식으로 관리할 수 있다.
  1. 데이터 일관성 유지
  2. 전역적인 접근 가능성
  3. 스레드 안정성
  • 멀티스레드 환경에서 싱글톤 패턴을 구현하면 스레드 안정성을 보장
  • 여러 스레드에서 동시에 객체를 생성하는 것을 방지하여 충돌을 예방

하지만 싱글톤 패턴은 남용될 경우도 있으며, 객체간의 결합도가 높아질 수 있으므로 적절히 사용해야 한다. 또한, 멀티스레드 환경에서 동기화 문제에 주의하여 구현해야 한다. 최근에는 Spring 프레임워크와 같은 의존성 주입(Dependency Injection) 기술을 통해 객체 관리를 수행하는 것이 더 권장되는 경향이 있다.

6. 그렇다면 단점은..?

TDD(Test Driven Development)에 걸림돌이 된다.

  • 주로 TDD시 단위 테스트를 주로 하는데 단위 테스트는 서로 독립적이어야 하며, 테스트를 어떤순서로든 실행할 수 있어야 하는데 독립적인 인스턴스 만들기가 어렵다.
  • 의존 관계상 클라이언트가 구체 클래스에 의존
  • private 생성자 때문에 테스트가 어렵다.
  • 객체 인스턴스를 하나만 생성해서 공유하는 방식 때문에 싱글톤 객체를 stateful하게 설계 했을 경우 큰 장애 발생요인이 된다.

7. 이를 해결하기 위해서는 무상태(stateless)로 설계

  • 특정 클라이언트에 의존적인 필드가 있으면 안됨
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안됨
  • 가급적 읽기 전용으로 만들고, 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용

✋여기서 잠깐? TDD(Test Driven Development)란?

  1. 소프트웨어 개발 방법론 중의 하나로, 소프트웨어를 개발할 때 테스트 코드를 먼저 작성하고, 그 테스트 코드를 통과하는 코드를 구현하는 접근 방식
  2. 테스트 코드의 단계
    1. 테스트 작성 (Test): 먼저, 새로운 기능 또는 기능의 변경을 위한 테스트 코드를 작성 이때 작성한 테스트는 아직 구현되지 않은 기능이므로 실패할 것
    2. 테스트 실행 (Red): 작성한 테스트 코드를 실행 이 단계에서는 해당 테스트가 실패. 왜냐하면 아직 해당 기능이 구현되지 않았기 때문
    3. 코드 구현 (Green) : 실패한 테스트를 통과하기 위한 최소한의 코드를 구현한다. 이 단계에서는 테스트가 성공
    4. 코드 리팩토링 (Refactor): 작성한 코드를 리팩토링하여 코드의 구조와 가독성을 개선이 단계에서도 테스트를 계속 통과하는지 확인
  3. TDD의 핵심 아이디어
  • 테스트를 먼저 작성함으로써 요구사항을 명확하게 이해하고, 정확한 코드를 작성하도록 유도하는 것
  • 또한, 테스트를 작성함으로써 변경 사항이 코드에 미치는 영향을 빠르게 확인하고, 버그를 조기에 발견하여 개선할 수 있다.
  1. TDD의 장점
  • 안정성 향상
  • 코드 품질 개선
  • 유지보수 용이성:
  • 개발과 테스트의 지속적인 통합
  • 문서화
  1. 단점
  • 초기 투자

8. 의존성 주입(DI, Dependency Injection)

  • 싱글톤 패턴은 모듈 간의 결합을 강하게 만들 수 있다는 단점
  • 이때 의존성 주입(DI, Dependency Injection)을 통해 모듈 간의 결합을 좀 더 느슨하게 만들어 해결 가능
  • 의존성 = 종속성 ⇒ A가 B에 의존성이 있다는 것은 B의 변경 사항에 대해 A 또한 변해야 한다.
  1. 개념
    • 객체 간의 의존 관계를 코드 내에서 명시적으로 설정하는 것이 아니라, 외부에서 의존하는 객체를 주입하는 방식
    • 메인 모듈이 직접 다른 하위 모듈에 대한 의존성을 주기보다는 중간에 의존성 주입자가 이 부분을 가로채 메인모듈이 간접적으로 의존성 주입하는 방법
    • 객체 간의 결합도를 낮추고 유연하고 테스트하기 쉬운 코드를 작성

2. 의존성 주입의 단점

  • 모듈들이 더욱 더 분리되므로 클래스 수가 늘어나 복잡성 증가, 약간의 런타임 패널티
    3. 의존성 주입 원칙
  • 상위 모듈은 하위 모듈에서 어떤 것도 가져오면 않아야 한다.
  • 둘 다 추상화에 의존해야 하며, 이 때 추상화는 세부 사항에 의존하지 말아야 한다.

9. 의존성 주입의 예제

  • UserService 클래스에서 UserRepository 인터페이스에 의존하는 경우
  • UserService 클래스의 생성자를 통해 UserRepository 구현 클래스(UserRepositoryImpl)를 주입받아 의존성을 해결
  • 이렇게 하면 UserService 클래스는 UserRepository 인터페이스의 구현 내용에 대해 알 필요가 없으며, 다른 구현체를 주입하여 쉽게 교체할 수 있다.
  • 이로 인해 코드 유지보수와 테스트가 용이해지는 장점
  1. UserRepository 인터페이스
// UserRepository.java
public interface UserRepository {
    User findById(String userId);
    void save(User user);
}
  1. UserService 클래스
// UserService.java
public class UserService {
    private UserRepository userRepository;

    // 생성자를 통한 의존성 주입
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(String userId) {
        return userRepository.findById(userId);
    }

    public void saveUser(User user) {
        userRepository.save(user);
    }
}
  1. UserRepository 구현 클래스
// UserRepositoryImpl.java
public class UserRepositoryImpl implements UserRepository {
    // 간단한 구현으로 예시를 들기 위해 생략
    // 실제로는 데이터베이스 등에 접근하여 사용자 정보를 저장하고 조회하는 로직이 들어갈 수 있습니다.
}
  1. Main 메서드 ⇒ 메인 모듈
// Main.java
public class Main {
    public static void main(String[] args) {
        // 의존성 주입을 통해 UserRepositoryImpl 인스턴스를 UserService에 주입
        UserRepository userRepository = new UserRepositoryImpl();
        UserService userService = new UserService(userRepository);

        String userId = "exampleUserId";
        User user = userService.getUserById(userId);
        System.out.println("User ID: " + user.getUserId());
    }
}
profile
내 지식을 기록하여, 다른 사람들과 공유하여 함께 발전하는 사람이 되고 싶다. gitbook에도 정리중 ~

0개의 댓글