자바 싱글톤 패턴 구현
싱글톤(Singleton) 패턴은 특정 클래스의 인스턴스가 애플리케이션 내에서 오직 하나만 존재하도록 보장하는 디자인 패턴입니다. 이는 전역적으로 접근 가능한 유일한 객체가 필요할 때 유용하게 사용됩니다. 예를 들어, 로거, 설정 관리자, 데이터베이스 연결 풀 등에서 활용될 수 있습니다. 자바에서 싱글톤을 구현하는 대표적인 두 가지 방식은 '이른 초기화(Eager Initialization)'와 '늦은 초기화(Lazy Initialization)'입니다.
1. 이른 초기화 (Eager Initialization)
이 방식은 클래스 로딩 시점에 인스턴스를 미리 생성합니다. 구현이 가장 간단하고 스레드 안전성을 별도로 고려할 필요가 없다는 장점이 있습니다. 하지만 인스턴스가 실제로 사용되지 않더라도 메모리에 상주한다는 특징이 있습니다.
public class ApplicationConfig {
// 클래스 로드 시점에 유일한 인스턴스를 미리 생성합니다.
private static final ApplicationConfig configuration = new ApplicationConfig();
// 외부에서 직접 인스턴스 생성을 막기 위해 private 생성자를 선언합니다.
private ApplicationConfig() {
// 애플리케이션 초기 설정 로드 또는 초기화 작업 수행
System.out.println("ApplicationConfig 인스턴스가 초기화되었습니다.");
}
// 미리 생성된 유일한 인스턴스를 반환하는 정적 메서드입니다.
public static ApplicationConfig getInstance() {
return configuration;
}
// 예시 메서드
public String getAppVersion() {
return "1.0.0";
}
public static void main(String[] args) {
ApplicationConfig config1 = ApplicationConfig.getInstance();
ApplicationConfig config2 = ApplicationConfig.getInstance();
System.out.println("두 인스턴스는 동일한가? " + (config1 == config2));
System.out.println("애플리케이션 버전: " + config1.getAppVersion());
}
}
2. 늦은 초기화 (Lazy Initialization) - 스레드 안전성 고려
늦은 초기화 방식은 인스턴스가 실제로 필요할 때 생성됩니다. 이는 자원 낭비를 줄일 수 있지만, 멀티스레드 환경에서는 여러 스레드가 동시에 인스턴스를 생성하려 할 수 있으므로 스레드 안전성을 반드시 고려해야 합니다. 일반적으로 'Double-Checked Locking' 기법을 사용하여 효율적으로 스레드 안전성을 확보합니다.
public class DatabaseConnectionPool {
// volatile 키워드를 사용하여 poolInstance 변수의 가시성과 재정렬 문제를 방지합니다.
private static volatile DatabaseConnectionPool poolInstance;
// private 생성자로 외부 인스턴스 생성을 차단합니다.
private DatabaseConnectionPool() {
// 데이터베이스 연결 풀 초기화 로직 (예: 최대 연결 수 설정, 실제 연결 생성)
System.out.println("데이터베이스 연결 풀 인스턴스가 초기화 중입니다...");
}
// 스레드 안전한 인스턴스 반환 메서드 (Double-Checked Locking)
public static DatabaseConnectionPool getPool() {
if (poolInstance == null) { // 첫 번째 체크: 인스턴스가 null이 아닌 경우 불필요한 동기화를 피합니다.
synchronized (DatabaseConnectionPool.class) { // 동기화 블록: 한 번에 하나의 스레드만 진입 가능
if (poolInstance == null) { // 두 번째 체크: 동기화 블록 안에서 인스턴스가 실제로 null일 때만 생성
poolInstance = new DatabaseConnectionPool();
}
}
}
return poolInstance;
}
// 예시 메서드
public void executeQuery(String query) {
System.out.println("쿼리 실행: " + query);
}
public static void main(String[] args) {
DatabaseConnectionPool dbPool1 = DatabaseConnectionPool.getPool();
DatabaseConnectionPool dbPool2 = DatabaseConnectionPool.getPool();
System.out.println("두 풀 인스턴스는 동일한가? " + (dbPool1 == dbPool2));
dbPool1.executeQuery("SELECT * FROM USERS");
}
}
자바 정렬 알고리즘
데이터를 특정 순서로 배열하는 것은 프로그래밍의 기본이자 다양한 응용에서 필수적인 작업입니다. 여기서는 선택 정렬과 버블 정렬의 구현을 살펴봅니다.
1. 선택 정렬 (Selection Sort)
선택 정렬은 배열에서 가장 작은(또는 가장 큰) 요소를 찾아 배열의 적절한 위치로 이동시키는 방식입니다. 각 반복(패스)마다 아직 정렬되지 않은 부분에서 최솟값(또는 최댓값)을 찾아 정렬된 부분의 끝에 배치합니다. 이는 매 패스마다 단 한 번의 교환만 발생시키는 효율적인 접근 방식입니다.
public class ArraySelectionSorter {
/**
* 선택 정렬 알고리즘을 사용하여 주어진 정수 배열을 오름차순으로 정렬합니다.
* @param elements 정렬할 정수 배열
*/
public static void sortWithSelection(int[] elements) {
int arrayLength = elements.length;
// 배열의 모든 요소를 순회하며 정렬 위치를 결정합니다.
for (int i = 0; i < arrayLength - 1; i++) {
int minElementIndex = i; // 현재 위치를 최솟값의 인덱스로 초기 설정
// 아직 정렬되지 않은 나머지 배열에서 최솟값을 탐색합니다.
for (int j = i + 1; j < arrayLength; j++) {
if (elements[j] < elements[minElementIndex]) {
minElementIndex = j; // 더 작은 값을 찾으면 최솟값 인덱스를 갱신합니다.
}
}
// 현재 위치의 값과 찾은 최솟값 위치의 값을 교환합니다.
// 만약 minElementIndex가 i와 같다면 (이미 정렬된 경우) 교환할 필요가 없습니다.
if (minElementIndex != i) {
int tempValue = elements[i];
elements[i] = elements[minElementIndex];
elements[minElementIndex] = tempValue;
}
}
}
public static void main(String[] args) {
int[] numbers = {2, 4, 6, 7, 3, 5, 1, 9, 8};
System.out.print("정렬 전 배열: ");
for (int num : numbers) {
System.out.print(num + " ");
}
System.out.println();
sortWithSelection(numbers);
System.out.print("선택 정렬 후 배열: ");
for (int num : numbers) {
System.out.print(num + " ");
}
System.out.println(); // 출력: 1 2 3 4 5 6 7 8 9
}
}
2. 버블 정렬 (Bubble Sort)
버블 정렬은 인접한 두 요소를 비교하여 정렬 순서가 맞지 않으면 교환하며 데이터를 정렬하는 방식입니다. 작은 요소들이 배열의 시작 부분으로, 큰 요소들이 배열의 끝으로 '버블'처럼 떠오르거나 가라앉는 모습을 보입니다. 구현이 간단하지만, 비효율적이라는 단점이 있어 실용적인 용도로는 잘 사용되지 않습니다.
public class SimpleBubbleSorter {
/**
* 버블 정렬 알고리즘을 사용하여 주어진 정수 배열을 오름차순으로 정렬합니다.
* @param dataArray 정렬할 정수 배열
*/
public static void performBubbleSort(int[] dataArray) {
int n = dataArray.length;
boolean swapped; // 최적화를 위해, 한 번의 패스에서 교환이 일어나지 않으면 배열이 정렬된 것으로 간주
// 배열의 각 요소를 순회 (총 n-1 패스)
for (int i = 0; i < n - 1; i++) {
swapped = false; // 각 패스 시작 시 swapped 플래그 초기화
// 각 패스에서 인접한 요소들을 비교하고 필요하면 교환합니다.
// 마지막 i개의 요소는 이미 정렬되어 있으므로 비교 대상에서 제외합니다.
for (int j = 0; j < n - 1 - i; j++) {
if (dataArray[j] > dataArray[j + 1]) {
// 인접한 두 요소가 정렬 순서에 맞지 않으면 교환합니다.
int temporary = dataArray[j];
dataArray[j] = dataArray[j + 1];
dataArray[j + 1] = temporary;
swapped = true; // 교환이 발생했음을 표시
}
}
// 한 번의 패스에서 교환이 전혀 발생하지 않았다면 배열이 이미 정렬된 것입니다.
// 이 경우 더 이상 정렬할 필요가 없으므로 루프를 종료합니다.
if (!swapped) {
break;
}
}
}
public static void main(String[] args) {
int[] valuesToSort = {2, 4, 6, 7, 3, 5, 1, 9, 8};
System.out.print("정렬 전 배열: ");
for (int val : valuesToSort) {
System.out.print(val + " ");
}
System.out.println();
performBubbleSort(valuesToSort);
System.out.print("버블 정렬 후 배열: ");
for (int val : valuesToSort) {
System.out.print(val + " ");
}
System.out.println(); // 출력: 1 2 3 4 5 6 7 8 9
}
}
자바 검색 알고리즘
데이터 집합에서 특정 값을 찾는 검색 알고리즘은 정렬과 함께 가장 많이 사용되는 기본 연산 중 하나입니다. 여기서는 정렬된 배열에서 매우 효율적인 검색을 제공하는 이진 탐색을 살펴봅니다.
1. 이진 탐색 (Binary Search)
이진 탐색은 정렬된 배열에서 특정 값을 찾는 데 사용되는 효율적인 알고리즘입니다. 배열의 중간 값을 확인하여, 찾으려는 값이 중간 값보다 크면 오른쪽 절반을, 작으면 왼쪽 절반을 탐색하는 과정을 반복하여 검색 범위를 절반씩 줄여나갑니다. 이 방법은 데이터 양이 많을수록 선형 탐색보다 훨씬 빠릅니다.
public class SortedArrayFinder {
/**
* 이진 탐색 알고리즘을 사용하여 정렬된 배열에서 특정 값의 인덱스를 찾습니다.
* @param sortedData 정렬된 정수 배열
* @param searchTarget 찾으려는 값
* @return 값이 발견되면 해당 인덱스를, 찾지 못하면 -1을 반환합니다.
*/
public static int findElementBinary(int[] sortedData, int searchTarget) {
if (sortedData == null || sortedData.length == 0) {
return -1; // 배열이 비어있거나 null인 경우 -1 반환
}
int lowIndex = 0;
int highIndex = sortedData.length - 1;
// lowIndex가 highIndex보다 작거나 같을 때까지 반복
while (lowIndex <= highIndex) {
// 중간 인덱스 계산 (오버플로우 방지를 위해 (lowIndex + highIndex) / 2 대신 사용)
int midIndex = lowIndex + (highIndex - lowIndex) / 2;
if (searchTarget == sortedData[midIndex]) {
return midIndex; // 값을 찾으면 해당 인덱스 반환
} else if (searchTarget < sortedData[midIndex]) {
highIndex = midIndex - 1; // 목표 값이 중간 값보다 작으면 왼쪽 절반 탐색
} else { // searchTarget > sortedData[midIndex]
lowIndex = midIndex + 1; // 목표 값이 중간 값보다 크면 오른쪽 절반 탐색
}
}
return -1; // 배열에서 값을 찾지 못함
}
public static void main(String[] args) {
int[] dataSorted = {1, 3, 6, 8, 9, 10, 12, 18, 20, 34};
int valueToFind1 = 12;
int valueToFind2 = 7;
int valueToFind3 = 1;
int valueToFind4 = 34;
int valueToFind5 = 50;
int foundIndex1 = findElementBinary(dataSorted, valueToFind1);
if (foundIndex1 != -1) {
System.out.println(valueToFind1 + "는(은) 인덱스 " + foundIndex1 + "에 위치합니다."); // 출력: 12는(은) 인덱스 6에 위치합니다.
} else {
System.out.println(valueToFind1 + "를(을) 배열에서 찾을 수 없습니다.");
}
int foundIndex2 = findElementBinary(dataSorted, valueToFind2);
if (foundIndex2 != -1) {
System.out.println(valueToFind2 + "는(은) 인덱스 " + foundIndex2 + "에 위치합니다.");
} else {
System.out.println(valueToFind2 + "를(을) 배열에서 찾을 수 없습니다."); // 출력: 7를(을) 배열에서 찾을 수 없습니다.
}
System.out.println(valueToFind3 + "의 인덱스: " + findElementBinary(dataSorted, valueToFind3)); // 출력: 1의 인덱스: 0
System.out.println(valueToFind4 + "의 인덱스: " + findElementBinary(dataSorted, valueToFind4)); // 출력: 34의 인덱스: 9
System.out.println(valueToFind5 + "의 인덱스: " + findElementBinary(dataSorted, valueToFind5)); // 출력: 50의 인덱스: -1
}
}