위 책을 보면서 정리한 글입니다.
쉽게 생각하고 사용한 싱글톤은 프로젝트를 더 어렵게 만든다.
코드를 구현하면서 각종 상황에 필요한 데이터를 저장하거나 저장된 데이터를 꺼내어 사용하는 경우가 생긴다.
이 경우, 파일이나 DBMS를 사용하는데, 적지 않은 라이브러리의 도움을 받아야 하고 때로는 필요한 요소보다 더 많은 예외 사항이나 추가 설계를 수반한다.
담아야 할 데이터의 양이 많지 않을 때에는 가볍게 상숑 가능한 전역 변수에 데이터를 담아 사용하기도 하지만, 이것 역시 단점이 있다.
데이터 구조가 요구사항을 구현하기 힘들고, 예상치 못한 곳에서 데이터가 조작될 수 있기 때문에 신뢰성이 떨어진다.
그러나, 대안책으로 사용하는 싱글톤 객체 역시 멀티 스레드 환경에서는 잘못된 데이터가 저장되기도 하고, 객체의 잘못된 생명주기 관리로 애플리케이션 장애가 생기는 문제점이 있다.
객실 예약 프로그램에서, 예약 가능한 객실 목록에 대한 정보를 관리하는 싱글톤 객체가 있을 때.
객체의 생명주기는 무엇인가
- 객체에도 생명 주기가 있다.
- 보통 객체의 생성부터 GD(Garbage Collection)에 의한 메모리 해제까지를 생명주기라고 한다.
- 싱글톤 객체는 시스템에 상주해야 하는 중요한 데이터를 관리하는 객체로 생각하지만, 필요에 따라서는 객체에 데이터를 재설정하거나 메모리 해제를 해야하는 경우가 있다.
- 그렇지만 대부분 특정 시점에서 데이터를 재설정하는 코드가 없는 경우가 많고,
- 싱글톤 데이터를 함부로 재설정하거나 인스턴스를 새로 생성하면 멀티스레드 환경에서는 애플리케이션 장애를 초래할 수 있다.
- 제한적 상태에서만 객체를 초기화해야 하므로 이런 조건을 어기면 싱글톤 객체의 치명적인 단점이 된다.
Thread-safe에 의한 데이터 관리는 어떤 것인가
- 멀티스레드 환경에서 데이터에 접근할 때 동기화를 관리하는 방법을 의미한다.
- 자바에서는 synchronized라는 예약어르 동기화 관리가 필요한 객체나 메서드에 접근해서 사용 가능.
- (잘못 사용하면 Dead rock(Or Rece condition)에 빠져 객체 자체를 사용 못할 수 있다.)
- 이 장에서는 Java API를 활용한 Thread-safe로 데이터를 관리
public class HotelRooms {
// 준비된 객실
private static final Room[] READY_ROOMS = {
new Room("101", Room.ReserveState.IDEL),
new Room("102", Room.ReserveState.IDEL),
new Room("103", Room.ReserveState.IDEL),
new Room("104", Room.ReserveState.IDEL),
new Room("201", Room.ReserveState.IDEL),
new Room("202", Room.ReserveState.IDEL),
new Room("203", Room.ReserveState.IDEL),
new Room("204", Room.ReserveState.IDEL)
};
// 싱글톤 인스턴스
private static HotelRooms hotelRooms;
private Map<Date, List<Room>> rooms;
// 싱글톤 인스턴스 접근 메서드
public static HotelRooms getInstance() {
if(hotelRooms == null) {
hotelRooms = new HotelRooms();
hotelRooms.rooms = new HashMap<Date, List<Room>>();
}
return hotelRooms;
}
// 특정 객실의 정보 바꾸기
public boolean putReservedRoom(Data date, String roomNuber) {
List<Room> roomsAtDate = rooms.get(date);
if(roomsAtDate == null) {
rooms.put(date, Arrays.asList(Arrays.copyOf(READY_ROOMS, READY_ROOMS.length)));
roomsAtDate = rooms.get(date);
}
for(int i=0, size= roomsAtDate.size(); i < size; ++i) {
Room room = roomsAtDate.get(i);
if(room.getRoomNumber().equals(roomNumber)) {
if(room.getReserveState() == Room.ReserveState.IDLE) {
// 해당 날짜에 같은 객실이 비어 있으면 예약 상태로 변경
room.setReserveState(Room.ReserveState.RESERVED);
return true;
} else {
return false;
}
}
}
return false;
}
// 객실 정보 데이터를 전부 가져오기
public Map<Date, List<Room>> getAllReservedRoom() {
return rooms;
}
// 해당 날짜에 비어있는 객실이 있는지 확인
public boolean containIdleRoomAtDate(Date date, String roomNumber) {
List<Room> roomsAtDate = room.get(date);
if(roomsAtDate == null) {
rooms.put(date, Arrays.asList(Arrays.copyOf(READY_ROOMS, READY_ROOMS.length)));
roomsAtDate = rooms.get(date);
}
for(int i=0, size = roomsAtDate.size(); i < size; ++i) {
Room room = roomsAtDate.get(i);
if(room.getRoomNumber().equlas(roomNumber)) {
if(room.getReserveState() == Room.ReserveState.IDLE) {
// 해당 날짜에 같은 객실이 비어있는지 확인
return true;
} else {
return false;
}
}
}
}
}
public class Room {
public enum ReserveState {
IDLE, RESERVED;
}
// 객실 번호
private String roomNumber;
// 예약 상태
private ReserveState reserveState;
public Room(String roomNumber, ReserveState reserveState) {
this.roomNumber = roomNumber;
this.reserveState = reserveState;
}
public String getRoomNumber() {
return roomNumber;
}
public void setRoomNumber(String roomNumber) {
this.roomNumber = roomNumber;
}
public ReserveState getReserveState() {
return reserveState;
}
public void setReserveState(ReserveState reserveState) {
this.reserveState = reserveState;
}
}
public class ReserveService {
public boolean reserve(Date date, String roomNumber) {
HotelRooms roomData = HotelRooms.getInstance();
boolean isReserve = roomData.putReservedRoom(date, roomNumber);
return isReserve;
}
public List<Room> getReservedRoomAtDate(Date date) {
HotelRooms roomData = HotelRooms.getInstance();
Map<Date, List<Room>> allReservedRoom = roomData.getAllReservedRoom();
return allReservedRoom.get(date);
}
}
다음과 같은 단계로 개선
- 싱글톤 객체의 생성자에 대한 개선.
- 싱글톤 객체에서 제공하는 정보의 초기화 시점에 대한 개선.
- 동시 접근성 예외 사항 개선.
- 처리 메서드에서 인스턴스 생성을 메서드 인자로 전환하여 개선.
- 싱글톤 객체를 초기화하는 메서드 구현.
public class HotelRooms {
// 싱글톤 인스턴스
private static HotelRooms hotelRooms;
private Map<Date, List<Room>> rooms;
// 싱글톤 인스턴스 접근 메서드
synchronized public static HotelRooms getInstance() {
if(hotelRooms == null) {
hotelRooms = new HotelRooms();
}
return hotelRooms;
}
// 싱글톤 접근 제한 생성자
private HotelRooms() {
rooms = new HashMap<Date, List<Room>>();
}
// 중략
}
public class HotelRooms {
// 특정 객실의 정보 바꾸기
public boolean putReservedRoom(Data date, String roomNuber) {
List<Room> roomsAtDate = getRoomAtDate(date);
// 중략
return false;
}
// 해당 날짜에 비어있는 객실이 있는지 확인
public boolean containIdleRoomAtDate(Date date, String roomNumber) {
List<Room> roomsAtDate = getRoomAtDate(date);
// 중략
return false;
}
// 특정 날짜의 데이터 가져오기
private List<Room> getRoomAtDate(Date date) {
List<Room> roomsAtDate = rooms.get(date);
if(roomsAtDate == null) {
// 데이터가 없으면 초기화
rooms.put(date, Arrays.asList(Arrays.copyOf(READY_ROOMS, READY_ROOMS.length)));
roomsAtDate = rooms.get(date);
}
return roomsAtDate;
}
}
자바에서는 Thread-safe로 변경 가능한 다양한 API를 제공
- synchronized 예약어로 메서드나 객체에 접근하여 Block 단위로 관리
- Concurrency API를 활용(Java 1.5 이상)
- Collection 데이터 객체 자체를 Thread-safe로 선언(Java 1.2이상)
예시.List<String> synchronizedList = Collections.synchronizedMap(new ArrayList<String>());
import java.util.Collections;
public class HotelRooms {
// 중략
private Map<Date, List<Room>> rooms;
// 싱글톤 접근 제한 생성자
private HotelRooms() {
// rooms를 동기화 객체로 선언
rooms = Collections.synchronizedMap(new HashMap<Date, List<Room>>());
}
// 특정 날짜의 데이터 가져오기
// 여러 곳에서 접근하더라도 초기화는 한 번만 하게 하기 위함
private List<Room> getRoomAtDate(Date date) {
List<Room> roomsAtDate = rooms.get(date);
if(roomsAtDate == null) {
// 데이터가 없으면 초기화
roomsAtDate = Arrays.asList(Arrays.copyOf(READY_ROOMS, READY_ROOMS.length));
rooms.put(date, roomsAtDate);
}
return roomsAtDate;
}
// 특정 객실의 정보 바꾸기
public boolean putReservedRoom(Data date, String roomNuber) {
List<Room> roomsAtDate = getRoomAtDate(date);
for(int i=0, size= roomsAtDate.size(); i < size; ++i) {
Room room = roomsAtDate.get(i);
synchronized(room) {
// 객실 데이터에 접근하므로 객실에 대한 동기화 관리도 필요
if(room.getRoomNumber().equals(roomNumber)) {
if(room.getReserveState() == Room.ReserveState.IDLE) {
// 해당 날짜에 같은 객실이 비어 있으면 예약 상태로 변경
room.setReserveState(Room.ReserveState.RESERVED);
return true;
} else {
return false;
}
}
}
}
return false;
}
}
InquireRequestModel의 requset() 메서드의 오버로딩 처리를 염두에 두고 단위 테스트 작성
public class InquireRqeusetModelTest {
@Test
public void testRequest_비즈니스룸_조회() {
// Given
Parser parser = new BusinessInquireRoomParser();
String checkInDate = "2014-04-27";
String checkOutDate = "2014-04-29";
int type = 1;
// When
Object InquireData = InquireRequestModel.request(checkInDate, checkOutDate, parser);
// Then
Assert.assertEquals(BusinessRoomVO.class, inquireData.getClass());
}
}
Thread-safe가 보장되는지 테스트
import java.util.Collections;
public class HotelRoomsTest {
private final static String[] ROOMS_NUMS = {"201" "202", "203", "204", "101", "102", "103", "104"};
private final static Date CURRENT = new Date();
@Test
public void testPutReservedRoom() throws Exception {
final List<String> reservingRooms = Collections.synchronizedList(new ArrayList<String>());
for(int idx = 0; idx < 30; ++idx) {
Thread t = new Thread(new Runnable() {
@Override
public void run() {
HotelRooms instance = HotelRooms.getInstance();
for(int idx = 0; idx < ROOM_NUMS.length; ++idx) {
boolean isReserving = instance.testPutReservedRoom(CURRENT, ROOM_NUMS[idx]);
try {
Thread.sleep(10);
} catch(InterruptedException e) {
e.printStackTrace();
}
}
if(isReserving) {
if(reesrvingRooms.contains(ROOMS_NUM[idx])) {
Assert.fail(ROOM_NUMS[idx] + " is Reserved");
} else {
reservingRooms.add(ROOM_NUMS[idx]);
}
}
}
});
t.start();
}
Thread.sleep(1000);
}
}
싱글톤에 접근하는 인터페이스가 전역으로 선언되어 있으므로 사용하는 객체에서 접역 접근 함수인 getInstance() 메서드를 가지고 싱글톤 객체를 사용.
메서드에서 직접 인스턴스에 접근하던 것을 매개변수로 받아 처리하도록 수정하면 싱글톤 객체의 접근 인터페이스가 변경되어도 수정 없이 객체를 사용 가능.
public class ReserveService {
public boolean reserve(Date date, String roomNumber, HotelRooms hotelRooms) {
boolean isReserve = hotelRooms.putReservedRoom(date, roomNumber);
return isReserve;
}
public List<Room> getReservedRoomAtDate(Date date, HotelRooms roomData) {
Map<Date, List<Room>> allReservedRoom = roomData.getAllReservedRoom();
return allReservedRoom.get(date);
}
}
싱글톤 객체는 메모리 영역에 상주하는 객체이다. 따라서 필요 없는 시점이 되면 불필요한 메모리 사용을 최소화 하기 위해 객체를 초기화하거나 해제해야 한다.
public class HotelRooms {
// 싱글톤 인스턴스
private static HotelRooms hotelRooms;
private Map<Date, Lisat<Room>> rooms;
// 객체 초기화
public static boolean release() {
if(hotelRooms = null) {
return true;
}
hotelRooms.rooms.clear();
hotelRooms = null;
return true;
}
}
데이터를 동기화 객체로 선언(Thread-safe)하여 여러 객체에서 동시에 접근하더라도 데이터의 일관성이 유지되도록 수정.
싱글톤 객체는 효용성이 매우 강력하나, 그만큼 사용하기 매우 까다롭다.
싱글톤 객체를 안전하게 사용하기 위해서는 Thread-safe를 보장하는 구조가 필요하고, 생명주기도 능동적으로 관리 가능해야 한다.
싱글톤 객체에 접근하는 객체는 접근 인터페이스가 변경되었을 때 수정을 최소화하는 방법으로 사용되어야 한다.
싱글톤 객체의 사용을 쉽게 생각하면 안 된다.
- 싱글톤은 쉽게 정보를 저장하고 사용 가능하지만, 그만큼 고려 사항도 많다.
- 객체 초기화 접근자가 제한되어 있는지를 확인.
- 싱글톤 내의 데이터가 동기화 관리가 되고 있는지 확인하고, 메스더 동기화와 데이터 동기화를 생각.
- 싱글톤 객체의 생명주기 관리가 능동적인지 확인.
- 싱글톤 객체에 접근하는 객체가 접근 인터페이스로부터 영향을 받는 부분이 없는지 확인.