Effective 자바에서 해당 아이템은 싱글턴 디자인 패턴을 구현하는 방법에 관한 것 입니다. 아래에서 싱글턴 패턴을 구현하는 방법을 알아보겠습니다.
public static final 필드 방식의 싱글턴
이 방법은 싱글턴 인스턴스를 public static final 필드로 직접 제공합니다. 하지만 이 방법은 대체로 사용하지 않는게 좋습니다.
장점
- 클래스가 싱글턴임이 API에 명백히 드러남
- public static final 필드이니 절대로 다른 객체를 참조할 수 없음
- 구현이 간함(간결함)
단점
- 유연성이 떨어짐
- 싱글턴 인스턴스가 인터페이스를 구현해야 하거나, 인스턴스를 런타임으로 바꿔야 하는 경우에 적합하지 않음
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 생성자는 private으로 선언하여 외부에서의 인스턴스 생성을 방지
}
public void doSomething() {
System.out.println("test");
}
}
위 코드에서 INSTANCE는 클래스가 로드될 때 정적 필드에 싱글턴 인스턴스가 생성되고 할당됩니다. ( 정확히 인스턴스가 힙 영역에 생성되고 생성된 참조 값이 정정 필드 영역에 할당된다는 의미) 이 방식의 핵샘은 싱글턴 인스턴스가 프로그램의 생명주기 동안 변경되지 않는다는 것입니다. 즉, Singleton.INSTANCE 는 항상 동일한 Singleton 객체를 참조 하게 됩니다.
그러므로 제한사항이 존재하게 됩니다.
- 테스트 중에 싱글턴 인스턴스를 다른 구현으로 대체하기 어렵습니다. 예를 들어 단위 테스트를 작성할 때 Singleton 클래스에 의존하는 다른 컴포넌트를 테스트 하려면, 실제 Singleton 인스턴스 대신 모의 객체 (mock object)나 다른 구현을 사용하고 싶을 수 있습니다. public static final 방식에서는 이를 위해 추가적인 설계 변경이 발생합니다.
- oracle db, mysql db, memory db 등을 사용한다고 가정을 했을 경우 처음 생성된 인스턴스가 고정되어 있기 때문에 변경할 수 없습니다. 이는 테스트에 어려움이 있습니다.
- 만약 여러 테스트가 databasemanager에 의존 한다면, 한 테스트의 결과가 다른 테스트에 영향을 줄 수 있습니다. 예를 들어 한 테스트에서 연결이 실패하면 다른 테스트도 실패 합니다.
public class DatabaseManager {
public static final DatabaseManager INSTANCE = new DatabaseManager();
private DatabaseManager() {
// 리소스 초기화 및 설정
}
public void connect() {
// 데이터베이스 연결 로직
System.out.println("Database connected");
}
public void disconnect() {
// 데이터베이스 연결 해제 로직
System.out.println("Database disconnected");
}
}
public class ClientService {
public void performDataOperation() {
// 다른 데이터베이스로 변경이 불가능 함
DatabaseManager.INSTANCE.connect();
// 데이터 작업 수행
DatabaseManager.INSTANCE.disconnect();
}
}
public class ClientServiceTest {
@Test
public void testPerformDataOperation() {
ClientService service = new ClientService();
service.performDataOperation();
}
}
이러한 문제들 때문에 실제 환경에서는 의존성 주입을 통해 모의 객체(Mock) 이나 스텁(Stub)을 사용하여 이러한 의존성을 분리하고, 테스트의 격리와 속도를 개선합니다. public static final 싱글턴 방식에서는 이러한 방법을 적용할 수 없으므로, 정적 팩터리 방식을 고려하거나 다른 디자인 패턴을 사용하는 것이 좋습니다.
정적 팩터리 방식의 싱글턴
정적팩토리 방식의 싱글턴의 경우 getInstance 호출시 다른 Mock로 대체하여 사용할 수 있습니다.
public class DatabaseManager {
public static DatabaseManager INSTANCE = new DatabaseManager();
public static DatabaseManager getInstance() {
return INSTANCE;
}
public void connect() {
// 데이터베이스 연결 로직
System.out.println("connect");
}
}
public class MockDatabaseManager extends DatabaseManager {
@Override
public void connect() {
// 테스트 용도로 사용
System.out.println("Mock database connected");
}
}
class DatabaseManagerTest {
@BeforeAll
static void setup() throws NoSuchFieldException, IllegalAccessException {
Field instanceField = DatabaseManager.class.getDeclaredField("INSTANCE");
instanceField.setAccessible(true);
instanceField.set(null, new MockDatabaseManager());
}
@Test
void testConnect() {
assertDoesNotThrow(() -> DatabaseManager.getInstance().connect());
}
}
위 방식에서 @BeforeAll 어노테이션으로 모든 테스트가 실행되기 전에 한번만 setup 메서드를 실행합니다. 이 메서드에서는DatabaseManager의 INSTANCE 필드를 MockDatabaseManager 객체로 교환합니다. 그런 다음 testConnect() 테스트 메서드에서 검증을 진행합니다. ( junit mock로 대체 가능 )
위와 같은 방식으로 테스트는 실제 데이터베이스에 의존하지 않고도 DatabaseManager 클래스의 메서드들이 예상대로 동작하는지 검증할 수 있습니다. 리플렉션 사용은 테스트 환경에서만 사용해야 합니다.
열거 타입 방식의 싱글턴 - 바람직한 방법
열거(Enum) 방식의 싱글턴 패턴은 자바에서 싱글턴을 구현하는 가장 간단하고 효과적인 방법중 하나입니다. 이 방법은 자바의 열거형(Enum)을 사용하여 싱글턴을 구현하며, 인스턴스 생성 제어, 직렬화와 스레드 안정성을 자동으로 관리 합니다.
장점
- 간결하고 명확한 구현 : 추가적인 코드 없이 싱글턴을 구성할 수 있음
- 직렬화 지원 : 열거 방식은 자동으로 직렬화를 지원하며, 직렬화와 관련된 일반적인 문제들을 방지 함
- 스레드 안정성 : Enum 인스턴스는 기본적으로 스레드 세이프 함
- 리플렉션에 의한 공격 방지 : enum을 사용하면 리플렉션을 통한 싱글턴 패턴 파괴를 방지할 수 있음
public enum DatabaseConnection {
INSTANCE;
private String connectionString;
DatabaseConnection() {
// 기본 연결 문자열
this.connectionString = "jdbc:mysql://localhost:3306/mydatabase"; // 기본값
}
public void setConnectionString(String newConnectionString) {
this.connectionString = newConnectionString;
}
public void connect() {
// 데이터베이스 연결
System.out.println("Connecting to database: " + connectionString);
// 실제 연결 로직...
}
public void disconnect() {
// 데이터베이스 연결 해제
System.out.println("Disconnecting from database.");
// 실제 연결 해제 로직...
}
// 기타 메소드들...
}
public class Main {
public static void main(String[] args) {
// 싱글턴 인스턴스에 접근
DatabaseConnection connection = DatabaseConnection.INSTANCE;
// Oracle 데이터베이스 연결 문자열로 설정
// String oracleConnectionString = "jdbc:oracle:thin:@localhost:1521:orcl";
// DatabaseConnection.INSTANCE.setConnectionString(oracleConnectionString);
// 데이터베이스 연결
DatabaseConnection.INSTANCE.connect();
// 필요한 작업 수행...
// 데이터베이스 연결 해제
DatabaseConnection.INSTANCE.disconnect();
}
}
대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만든느 가장 좋은 방법입니다. Enum 싱글턴은 간단하고 효과적이지만, 상속을 해야 한다면 이 방법은 사용할 수 없습니다.(인터페이스 구현 가능)
'이펙티브 자바' 카테고리의 다른 글
불필요한 객체 생성을 피하라 (0) | 2024.01.11 |
---|---|
자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2024.01.10 |
인스턴스화를 막으려거든 private 생성자를 사용하라 (0) | 2024.01.03 |
생성자에 매개변수가 많다면 빌더를 고려하라 (1) | 2023.12.21 |
정적 팩터리 메서드 (1) | 2023.12.20 |