
싱글턴(Singleton)은 인스턴스를 단 하나만 생성할 수 있는 클래스를 의미한다.
시스템 전체에서 해당 클래스의 객체가 하나만 존재하도록 보장하는 패턴이다.
Math 클래스의 Math.abs(), Math.max() 등의 메서드가 있다.public class AppConfig {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() {
return INSTANCE;
}
}기존에 프로젝트를 진행하면서 이메일 인증 관련 작업을 진행하기위해서 MailConfig라는 클래스를 만들어서 사용했다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import java.util.Properties;
// 메일 전송을 위한 JavaMailSender 설정
@Configuration
public class MailConfig {
@Value("${mail.host}")
private String host;
@Value("${mail.port}")
private int port;
@Value("${mail.username}")
private String username;
@Value("${mail.password}")
private String password;
@Value("${mail.properties.mail.smtp.auth}")
private boolean smtpAuth;
@Value("${mail.properties.mail.smtp.starttls.enable}")
private boolean startTls;
@Value("${mail.properties.mail.debug}")
private boolean debug;
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", smtpAuth);
props.put("mail.smtp.starttls.enable", startTls);
props.put("mail.debug", debug);
return mailSender;
}
}
스프링에서는 @Configuration으로 등록된 클래스에 대해 싱글턴 범위(bean scope) 를 적용하므로, 실제로는 애플리케이션 내에서 하나의 인스턴스만 생성된다.
순수 자바 코드 관점에서 "private 생성자 + getInstance()"로 만든 것은 아니지만, 스프링 프레임워크가 대신 싱글턴처럼 관리하기 위해서 @Configuration을 통해서 싱글턴 범위를 적용하고 @Bean을 선언해서 하나의 빈만 생성하도록 관리 한다.
싱글턴을 구현하는 대표적인 방법은 3가지가 존재한다.
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {}
}
장점
단점
public class Singleton {
public static final Singleton INSTANCE = new Singleton();
private Singleton() {
// 리플렉션 공격 방어
if (INSTANCE != null) {
throw new IllegalStateException("Cannot create another instance");
}
}
}
리플렉션은 클래스 정보를 실행 중(Runtime)에 동적으로 분석하고 조작할 수 있는 기능이다.
프로그램 실행 중에 클래스 정보를 동적으로 획득하고, 멤버(필드, 메서드) 접근 제어를 우회할 수 있는 기능이라고 한다.
구체적으로 찾아보니 아래와 같은 원리 때문에 접근이 가능하다고 한다.
JVM(자바 가상 머신)은 클래스 로딩 시점에 클래스 정보를 메타데이터 형태로 저장해둔다.
Reflection API(java.lang.reflect 패키지)는 이 “메타데이터”에 접근할 수 있는 표준 인터페이스를 제공한다.
Class.forName("item3.Singleton")으로 Class<?> 객체를 얻어서 그 안에서 생성자, 메서드, 필드를 반영할 수 있다.setAccessible(true)
setAccessible(true)를 호출하면 JVM의 보호 체계를 일시적으로 해제할 수 있다.private 키워드를 사용했더라도 마음대로 접근이 가능해지는 것이다.

public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
장점
단점
getInstance() 메서드를 수정해서 싱글턴이 아닌 객체를 반환하도록 변경하는 것이다.public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
return new Singleton()으로 코드를 작성하면 인스턴스를 호출할 때마다 다른 객체를 생성해서 반환할 수 있게 된다는 말이다.public class GenericSingletonFactory<T> {
private static final GenericSingletonFactory INSTANCE = new GenericSingletonFactory();
private GenericSingletonFactory() {}
// 어떤 타입 파라미터가 오든 같은 INSTANCE만 반환한다.
public static <T> GenericSingletonFactory<T> getInstance() {
return INSTANCE;
}
}
Supplier<Singleton> supplier = Singleton::getInstance;
Singleton instance = supplier.get();
getInstance() 호출을 Supplier 형태로 사용할 수 있다는 말이다.supplier.get()을 호출하면 getInstance() 메서드가 실행된다.방법 1과 방법 2로 만든 싱글턴 클래스를 직렬화하기 위해서는 단순히 Serializable을 구현한다고 선언하는 것만으로는 부족하다고 하다.
모든 인스턴스 필드를 일시적(transient)이라고 선언하고 readResolve 메서드를 제공해야 한다고한다.
이렇게 하지않으면 직렬화된 인스턴스를 역직렬화 할 때마다 새로운 인스턴스가 만들어진다고 한다.
직렬화와 역직렬화를 생각했을 때 대표적인 예가 일단 자바 객체를 Json으로 데이터를 주고받는것이 제일 먼저 떠올랐다. 개념적으로 비슷하긴 하지만 여기서 말하는 직렬화/역직렬화와는 조금다른 것 같다.
여기서 말하는 직렬화/역직렬화는 자바가 내부적으로 정의한 바이너리 포맷을 사용해서 객체를 바이트로 변환하고 다른 서버나 클라이언트로 전송할 때 주로 사용되는 방식이다.
그렇다면 왜 역직렬화를 하면 새로운 인스턴스가 생기는 건지도 궁금했다.
이를 해소하기 위해서 readResolve() 메서드를 추가해줘야 하는 것이다.
private Object readResolve() {
return INSTANCE; // 새로운 인스턴스 대신 진짜 싱글턴 반환
}
readResolve() 메서드를 이용해서 "가짜" 인스턴스를 가비지 컬렉터에게 맡기고 "싱글턴 인스턴스"를 반환해서 싱글턴을 지킨다.
JVM이 내부적으로 readResolve() 메서드가 있는지 확인하고 자동으로 호출해주기 때문에 싱글턴임이 보장 가능해진다.
public enum Singleton {
INSTANCE;
public void doSomething() {
System.out.println("싱글턴 메서드 호출!");
}
}
장점
java.lang.Enum 클래스를 상속받기 때문에 다른 클래스를 상속하는 것이 불가능하다.enum을 사용해서 싱글턴을 만드는 것이 가장 좋은 방법이다.readResolve()를 구현해야 한다.