사실 가벼운 마음으로 강의에서 배웠던 DTO, DAO를 이용한 포스팅을 하려고 하였으나, Singleton은 단점이 명확해서 배운 것 외에도 새로 알게된 사실이 많았다. 그래서 그냥 내가 새로이 안 사실들을 포함해서 이론적으로 서술하려고 한다.
오직 하나의 객체만 생성하여 사용할 수 있는 인스턴스를 말한다. 클래스 전역에서 이 인스턴스를 공유하여 사용할 수 있기때문에 여러 곳에서 공통된 내용의 객체를 다수 생성해서 사용할 때 Singleton으로 만들어 사용한다.
내가 배운 연락처 관리 프로그램 구현을 예로 들어 설명하자면 데이터 베이스 접근을 위해서 사용해야할 공통된 요소가 존재한다.
private Connection con; // DB접속
private PreparedStatement ps; // 쿼리문 실행
private ResultSet rs; // SELECT 결과
private String sql; // 쿼리문
private int result; // INSERT, UPDATE, DELETE 결과 반환
필드들과
public Connection getConnection() throws Exception {
Class.forName("oracle.jdbc.OracleDriver");
Properties p = new Properties();
p.load(new FileReader("db.properties"));
String url = p.getProperty("url");
String user = p.getProperty("user");
String password = p.getProperty("password");
return DriverManager.getConnection(url, user, password);
}
oracle과 접속하기 위한 메소드
public void close() {
try {
if(rs != null) rs.close();
if(ps != null) ps.close();
if(con != null) con.close();
} catch(Exception e) {
e.printStackTrace();
}
}
그리고 oracle과의 연결을 닫아주기 위한 메소드가 있다.
이 많은 코드들을 매 클래스마다 새로 작성하고 또 작성한다면 시간의 문제도 문제거니와 공통된 내용의 다중 작성으로인한 메모리 낭비 문제도 클것이다. 또, 만약 메소드 내용을 수정 할 일이 생긴다면 그 수 많은 메소드들을 일일이 찾아서 고쳐줘야한다. 그러다보면 휴먼에러가 일어날 가능성이 다분한 것이다. 그런 불필요한 낭비를 방지하고자 Singleton 패턴을 사용한다.
1) 메모리 관리
2) 쉬운 접근성, 데이터 공유
3) 그로인한 속도 향상
Singleton은 이런 편리한 이점이 있는 반면에 단점 또한 두드러지게 나타난다.
바로 객체지향설계의 개방-폐쇄 원칙 위배, 멀티 스레드 환경에서 동시 호출되었을 때 유일성을 잃는 것 이다.
전자는 너무 많은 곳에서 사용될 경우인데 다른 클래스들과의 결합도가 높아져 수정이 어려워지고 유지 보수 비용이 커지는 단점을 이야기 한다.
후자는 두 가지의 경우를 이야기할 수 있는데
첫째는 Multi-thread 환경에서 instance가 아직 존재하지 않을 때 여러 곳에서 동시에 singleton을 호출할 경우이다.
이 경우 미처 생성되기 전에 호출 되어 미생성되었다고 인식하여 각자 새로운 instance로 생성될 가능성이 있다.
두 번째는 Multi-thread 환경에서 동시에 plusCount()
를 실행할 때이다.
이 또한 실행된 pluscount가 끝마치기 전에 반복 실행이되어 count가 제대로 실행되지 않을 가능성이 있다.
1) 객체지향설계의 개방-폐쇄 원칙 위배
2) 멀티 스레드 환경에서 동시 호출되었을 때
가장 좋은 예방 방법은 Singleton을 사용하지 않는 것이지만 사용되는 경우가 있기에 이를 극복하기 위한 다양한 구현 방법이 존재한다.
Singleton은 외부에서 여러 객체를 만들 수 없도록 private로 생성자를 만들고 getInstance()
라는 메소드를 이용해서 외부에서 불러온다. 기본적인 형태는 이렇다.
private static Singleton singletonA = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return singletonA;
}
Singleton.getInstance()
메서드 호출 시점에 정적 필드가 아직 초기화되지 않았으면 객체를 생성하는 방법이다.
public static Singleton getInstance() {
if (singletonA == null) {
singletonA = new Singleton();
}
return singletonA;
}
하지만 이 구현 법은 다중 스레드(Multi-thread)에서 2번째 문제를 일으킬 가능성이 있다.
다중 스레드의 동기화는 2번째 문제를 막기 위해 사용할 수 있는 방법이다. 이것은 Java에서 제공하는 키워드로 한 번에 하나의 스레드만 접근할 수 있도록 해주는 방식이다.
public static synchronized Singleton getInstance() {
if (singletonA == null) {
singletonA = new Singleton();
}
return singletonA;
}
아쉽지만 이 방법도 성능 저하를 발생시키기때문에 권장하지 않는 방법이고 남발해서는 안된다.
위에서 일어나는 성능 저하를 막기 위해서 두 번에 걸쳐 검사를 하도록 구현되는 방식이다. Synchronized와 Double-Checked Locking 둘 다 getInstance() 메소드에서 수정되는 방식이다.
public static Singleton getInstance() {
if (singletonA == null) {
synchronized (Singleton.class) {
if (singletonA == null) {
singletonA = new Singleton();
}
}
}
return singletonA;
}
동기화의 오버헤드를 막아서 성능적으로 이점이 있지만 이 또한 멀티 스레드에서 객체의 초기화가 완전히 끝나기 전에 다른 스레드가 실행되어서 null이 아님을 확인하고 초기화 중인 객체 참조를 그대로 반환하는 경우가 생길 수 있다. 동기화 블록 내부에서 컴파일러가 최적화를 위해 재배열할 수 있기때문이다.
이러한 것을 방지하기 위해서 singleton 변수를 volatile로 선언해준다. volatile에 관해서는 따로 게시글을 작성할 예정이기에 여기서는 기술하지 않고 volatile선언까지 완료된 코드만 작성하면
public class Singleton {
private static volatile Singleton singletonA;
private Singleton() { }
public static Singleton getInstance() {
if (singletonA == null) {
synchronized (Singleton.class) {
if (singletonA == null) {
singletonA = new Singleton();
}
}
}
return singletonA;
}
}
이러한 모습이 된다.
아주 슬프게도 이 방법 또한 문제점이 있다. 프로그램의 구조가 복잡해지고 비용이 커지며 코드가 부정확하게 작성될 가능성이 있다는 것이다. 그로인해 이번에는 JVM에게 Singleton의 초기화 문제를 떠넘기는 방식을 기술한다.
클래스 안에 클래스(holder)를 두는 방식으로 더블 체크 락킹보다 더 단순하며 안전하다. 그리고 실제로 가장 많이 사용되는 일반적인 singleton 방식이라고 한다.
public class Singleton {
private Singleton() { }
private static final class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
Holder
클래스에서 선언된 INSTANCE
가 getInstance()
에서 실행되기 전 클래스가 로딩되면서 초기화가 되는데 static final로 선언되어 있기 때문에 단 한번만 초기화되어 호출된다. 다시 값이 할당되지 않도록 만드는 방식인 것이다.
인강이나 학원에서 배울 때에 한 줄 요약, 혹은 이런거다 라는 느낌으로 별거 없는 내용인줄 알았는데 생각보다 무거운 이야기였다. 솔직히 배우면서 이게 어디다 쓰이는거야 싶은게 많았는데 이렇게 벨로그 작성한다고 따로 찾아보니 이렇게 활용되는구나 이런 문제가 있구나 알 수 있었다. 벨로그 쓰는게 공부가 꽤 된다는 사실에 놀랐고 Singleton을 이용해 어플 개발하신 대단한 분! 이런 능력자가 많다는 것도 놀랐다.. 세상은 넓고 나는 아직 우물 안 개구리구나 현업에 들어가서 잘 할 수 있을까 싶은데 그냥 잘 해야지 뭐.. 여튼 Singleton 공부 끝