본문 바로가기

개발/JAVA

[Java] 싱글톤 패턴(Singleton Pattern) - 인터페이스 타입 vs 구현체 타입

싱글톤(Singleton) 패턴은 객체의 인스턴스를 하나만 생성하고, 해당 인스턴스에 전역적으로 접근할 수 있게 하는 디자인 패턴입니다. 즉, 시스템 내에서 해당 클래스의 인스턴스가 오직 하나만 존재하도록 보장하는 구조를 제공합니다.

1. 싱글톤 패턴의 필요성

  • 자원의 낭비를 방지: 여러 개의 인스턴스가 필요 없는 상황에서 불필요한 메모리 낭비를 줄입니다. 예를 들어, 데이터베이스 연결을 관리하는 클래스나 로깅 시스템은 여러 개의 인스턴스가 필요하지 않습니다. 이러한 클래스는 단 하나의 인스턴스만 있어도 충분합니다.
  • 전역적 접근: 싱글톤 인스턴스는 애플리케이션 어디에서든지 접근할 수 있습니다. 이는 클래스 내에 객체를 전역적으로 관리하고자 할 때 유용합니다.

2. 싱글톤 패턴의 구현

자바에서 싱글톤 패턴은 보통 정적 메서드와 정적 필드를 사용해 구현됩니다. 이를 통해 클래스의 인스턴스가 처음 요청될 때 생성되고, 이후에는 동일한 인스턴스를 반환합니다.

 

싱글톤 패턴 구현 예제

public class UserServiceImpl {
    // 클래스의 유일한 인스턴스를 저장할 정적 필드
    private static UserServiceImpl instance = new UserServiceImpl();

    // 외부에서 생성자를 호출하지 못하도록 private 생성자를 정의
    private UserServiceImpl() {}

    // 정적 메서드로 유일한 인스턴스를 반환
    public static UserServiceImpl getInstance() {
        return instance;
    }
}

 

구현의 주요 특징:

  • 정적 필드: 클래스 내에 static 키워드를 사용해 유일한 인스턴스를 생성 및 보관.
  • 생성자(private): 외부에서 new 키워드를 통해 인스턴스를 추가로 만들지 못하게 생성자를 private으로 설정.
  • 정적 메서드: getInstance()라는 정적 메서드를 통해 어디서든지 인스턴스에 접근 가능.

이 구현에서 중요한 점은 하나의 객체만 생성되며 그 객체는 프로그램 전체에서 공유된다는 것입니다.

 

3. 싱글톤 패턴에서 인터페이스 사용

이제 싱글톤 패턴의 기본 개념을 이해했으니, 다음으로는 인터페이스와 구현체를 어떻게 사용할 것인지에 대해 설명하겠습니다. 아래 두 가지 코드를 비교해보겠습니다.

(1) 인터페이스 타입으로 싱글톤을 반환하는 방식

public class UserServiceImpl implements UserService {
    private static UserService instance = new UserServiceImpl();

    private UserServiceImpl() {}

    public static UserService getInstance() {
        return instance;
    }
}

 

  • **인터페이스 UserService**를 반환합니다.
  • UserService는 인터페이스이므로, 그 하위의 다른 구현체로도 확장 가능.
  • 이 방식은 클라이언트 코드가 구체적인 구현체(UserServiceImpl)에 의존하지 않고, 인터페이스를 통해 객체를 다룹니다.
  • 유연한 구조를 제공하므로, 유지보수가 쉽고 나중에 다른 구현체로 바꾸는 것도 편리합니다.

(2) 구현체 타입으로 싱글톤을 반환하는 방식

public class UserServiceImpl {
    private static UserServiceImpl instance = new UserServiceImpl();

    private UserServiceImpl() {}

    public static UserServiceImpl getInstance() {
        return instance;
    }
}
  • 이 코드는 인터페이스가 아닌 **구체적인 클래스(UserServiceImpl)**를 반환합니다.
  • 클라이언트 코드가 UserServiceImpl 클래스에 종속됩니다.
  • 즉, 나중에 UserServiceImpl이 아닌 다른 구현체를 사용하고 싶다면, 클라이언트 코드 전체에서 해당 클래스를 수정해야 합니다.

4. 인터페이스 반환과 구현체 반환의 차이

인터페이스 타입으로 반환할 때의 장점:

  1. 유연성: 인터페이스를 반환하면 언제든지 다른 구현체로 변경할 수 있습니다. 예를 들어, UserServiceImpl 대신에 MockUserService를 구현하여 교체 가능.
  2. 의존성 역전 원칙(DIP) 준수: 객체가 구체적인 구현체에 의존하지 않고 인터페이스에 의존함으로써 확장 가능성이 높아집니다.
  3. 테스트 용이성: 유닛 테스트에서 모의 객체(mock)를 쉽게 주입할 수 있습니다.

구현체 타입으로 반환할 때의 단점:

  1. 구체적인 구현에 종속: 클라이언트 코드가 특정 구현체에 종속됩니다. 만약 다른 구현체로 변경해야 할 경우, 코드를 크게 수정해야 합니다.
  2. 유연성 부족: 인터페이스를 사용하지 않기 때문에 다른 구현체로 확장하기 어렵습니다.

5. 결론: 언제 인터페이스를 쓰고, 언제 구현체를 쓰나?

  • 인터페이스 반환 방식을 사용하면 유지보수성과 유연성이 높습니다. 특히 프로젝트가 커지고, 여러 개발자가 작업하거나, 테스트 주도 개발(TDD)을 할 때 유리합니다.
  • 구현체 반환 방식은 코드가 단순할 때 적합하지만, 변경 가능성이 적거나 구체적인 구현체에 강하게 의존할 필요가 있을 때 사용됩니다.

따라서 싱글톤 패턴을 구현할 때는 인터페이스를 사용한 유연성 있는 코드 구조를 권장합니다.