Java 제네릭(Generic)은 왜 사용할까?

Posted by Space_Jin
2025. 6. 30. 15:48 Programming/Java, Kotlin
728x90
반응형
🔍 Java 제네릭(Generic)은 왜 사용할까?

Java를 사용하다 보면 List<String>, Map<Integer, String> 같은 제네릭(Generic) 문법을 자주 접하게 됩니다.
그렇다면 왜 굳이 제네릭을 써야 할까요?
이번 글에서는 제네릭의 도입 이유, 내부 동작 방식, 실제 예제, 주의할 점까지 정리해보겠습니다.


✅ 제네릭이란?

제네릭(Generic) 은 클래스나 메서드에서 사용할 데이터 타입을 외부에서 지정할 수 있게 해주는 기능입니다.

List<String> list = new ArrayList<>();

위 코드에서 List는 String만 담을 수 있다는 뜻입니다.


✅ 왜 제네릭을 사용하는가?

1. 컴파일 타임 타입 체크 (타입 안정성 확보)

  • 제네릭이 없던 시절에는 모든 타입이 Object로 처리되었고,
    잘못된 형변환으로 인해 ClassCastException이 자주 발생했습니다.
 
List list = new ArrayList();
list.add("hello");
list.add(123);  // 😨 문제 발생 소지

String s = (String) list.get(1); // ❌ 런타임 오류

제네릭을 사용하면 아래처럼 컴파일 타임에 타입 체크가 되어 실수를 방지할 수 있습니다.

List<String> list = new ArrayList<>();
list.add("hello");
list.add(123); // ❌ 컴파일 오류 발생!

2. 형변환 필요 없음 (가독성, 생산성 증가)

String str = list.get(0); // ✅ 제네릭 덕분에 형변환 생략

3. 재사용성과 유지보수성 증가

public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
}
  • Box<Integer>, Box<String> 등 다양한 타입으로 재사용 가능
  • 같은 코드지만 타입만 달라지는 경우 활용도 극대화

✅ Java 제네릭은 타입 소거(Type Erasure)를 사용한다

  • 제네릭 타입은 컴파일 타임에만 존재하며,
  • JVM에서는 모든 제네릭 타입 정보가 사라지고 Object로 처리됩니다.
 
List<String> list = new ArrayList<>();

컴파일 이후에는 내부적으로 다음과 같이 바뀝니다:

List list = new ArrayList(); // 타입 정보 없음

이것을 타입 소거 (Type Erasure) 라고 부릅니다.


✅ 타입 소거로 인한 한계

제한 사항설명

 

제한 설명
instanceof List<String> 사용 불가 타입이 런타임에 존재하지 않음
T[] arr = new T[10] 불가 배열은 런타임 타입 유지가 필요함
T + T 연산 불가 연산자는 Object 타입에 정의돼 있지 않음
기본형 int, long 사용 불가 제네릭은 참조형만 지원 (→ Integer, Long 등으로 박싱 필요)
 

✅ 제네릭을 사용할 때 주의할 점

  • 기본형(int, long) 을 직접 사용할 수 없음 → Integer, Long으로 박싱해야 함
  • 런타임에는 타입 정보가 사라지므로 instanceof, 배열 생성 등의 제약 존재
  • 오버플로우 방지를 위해 비교 연산은 Integer.compare(), Long.compare() 사용 권장

✅ 요약

목적 효과
타입 안정성 확보 컴파일 타임에 오류 사전 방지
형변환 제거 코드 간결화
재사용성 증가 다양한 타입 대응 가능
유지보수 용이 타입 변경 시 영향 최소화
 

✅ 결론

Java 제네릭은 타입 안정성과 재사용성을 높이기 위해 도입된 문법입니다.
비록 타입 소거로 인해 몇 가지 제약이 있지만, 올바르게 활용하면
실수는 줄이고 생산성과 유지보수성은 크게 향상시킬 수 있습니다.

728x90
반응형

[Java] 코딩 테스트에서 잘 쓰이는 java 자료구조와 알고리즘

Posted by Space_Jin
2025. 5. 3. 13:24 Programming/Java, Kotlin
728x90
반응형
자주 쓰이는 자료구조
배열
import java.util.*;

public class ListExample {
    public static void main(String[] args) {
		// int형 배열
        int[] intArray = new int[100]; // 크기가 100인 int형 배열 생성 (초기값 0)
        int[] intArrayInit = {1, 2, 3, 4, 5};	// 생성과 함께 값을 할당한 배열

        // 요소 접근 (인덱스 사용)
        System.out.println(intArray[0)); 	// 출력: 0
        System.out.println(intArrayInit[0)); // 출력: 1

		// 문자열 배열
        String[] stringArray = new String[100];	// 크기가 100인 문자열 배열
        
        // 논리형 배열
        boolean[] boolArray = new boolean[100];	// 크기가 100인 논리형 배열(초기 값 false)
        
		// ArrayList 생성
        List<Integer> list = new ArrayList<>();

        // 요소 추가
        list.add(10);
        list.add(20);
        list.add(30);

        // 요소 접근 (인덱스 사용)
        System.out.println(list.get(0)); // 출력: 10

        // 크기 확인
        System.out.println(list.size()); // 출력: 3

        // 요소 삭제
        list.remove(1); // 인덱스 1의 요소 (20) 삭제
        System.out.println(list); // 출력: [10, 30]
    }
}

배열의 경우, 정수형(int or long 타입), 문자형(String 타입), 논리형(boolean 타입)의 배열을 주로 사용한다.

값을 계속 삽입, 제거하는 경우 ArrayList를 사용하기도 한다.

하지만 최대 크기가 정해진 배열 안에서 조작을 한다면, 성능적으로 ArrayList보다 int, String, boolean 타입의 배열을 생성해서 사용하는 것이 좋다.

배열 - ArrayList  배열
import java.util.ArrayList;
import java.util.List;

public class ArrayListArrayExample {

    public static void main(String[] args) {

        // 1. ArrayList<Integer>를 담을 수 있는 크기 5짜리 배열 선언 및 생성
        ArrayList<Integer>[] arrayOfLists = new ArrayList[5];
        // 또는 List<Integer>[] arrayOfLists = new List[5]; (List 인터페이스로 선언하는게 더 유연)

        // 2. 배열의 각 칸에 실제 ArrayList 객체를 생성해서 넣어주기
        for (int i = 0; i < arrayOfLists.length; i++) {
            arrayOfLists[i] = new ArrayList<>();
        }

        // 3. 각 ArrayList에 데이터 추가하기
        arrayOfLists[0].add(1);
        arrayOfLists[0].add(2);

        arrayOfLists[1].add(11);

        arrayOfLists[2].add(100);
        arrayOfLists[2].add(200);
        arrayOfLists[2].add(300);

        // 4. 데이터 확인
        for (int i = 0; i < arrayOfLists.length; i++) {
            System.out.println("arrayOfLists[" + i + "]의 내용: " + arrayOfLists[i]);
        }

        // 5. 특정 요소 접근
        System.out.println("\narrayOfLists[2]의 첫 번째 요소: " + arrayOfLists[2].get(0));
    }
}

리스트 배열

import java.util.ArrayList;
import java.util.List;

public class NestedArrayListExample {

    public static void main(String[] args) {

        // ArrayList<ArrayList<Integer>> 선언 및 생성
        ArrayList<ArrayList<Integer>> listOfLists = new ArrayList<>();
        // 또는 List<List<Integer>> listOfLists = new ArrayList<>();

        // 안쪽 ArrayList (행) 추가 및 데이터 삽입
        ArrayList<Integer> row1 = new ArrayList<>();
        row1.add(1);
        row1.add(2);
        listOfLists.add(row1); // 첫 번째 행 추가

        ArrayList<Integer> row2 = new ArrayList<>();
        row2.add(10);
        row2.add(20);
        row2.add(30);
        listOfLists.add(row2); // 두 번째 행 추가

        // 세 번째 행은 만들면서 바로 데이터 추가
        listOfLists.add(new ArrayList<>()); // 세 번째 행 추가
        listOfLists.get(2).add(100);
        listOfLists.get(2).add(200);


        // 내용 출력
        System.out.println("전체 리스트 내용:");
        for (int i = 0; i < listOfLists.size(); i++) {
            System.out.println("  행 " + i + ": " + listOfLists.get(i));
        }

        // 특정 요소 접근
        System.out.println("\n두 번째 행 (인덱스 1)의 세 번째 요소 (인덱스 2): " + listOfLists.get(1).get(2)); // 출력: 30

        // 새로운 행 추가
        listOfLists.add(new ArrayList<>()); // 네 번째 행 추가
        listOfLists.get(3).add(999);

        System.out.println("\n네 번째 행 추가 후 전체 리스트 내용:");
        for (int i = 0; i < listOfLists.size(); i++) {
            System.out.println("  행 " + i + ": " + listOfLists.get(i));
        }
    }
}

리스트 타입을 가지는 리스트

 

Set

Set 자료구조는 중복 값을 제거해서 값들을 저장할 때 유용

import java.util.HashSet;
import java.util.Set;

public class SetExample {
    public static void main(String[] args) {
        // HashSet 생성
        Set<String> set = new HashSet<>();

        // 요소 추가 (중복은 하나만 저장됨)
        set.add("Java");
        set.add("Python");
        set.add("Java"); // 중복
        set.add("C++");

        System.out.println(set); // 출력: [Java, C++, Python] (순서 보장 안됨)

        // 요소 존재 확인
        System.out.println(set.contains("Python")); // 출력: true

        // 요소 삭제
        set.remove("C++");
        System.out.println(set); // 출력: [Java, Python] (순서 보장 안됨)
    }
}

 

Stack
import java.util.*;

class Main {
    public static void main(String[] args) {
        Stack<Integer> s = new Stack<>();
        s.push(1);
        s.push(2);
        s.push(3);
		
        // stack이 비어있지 않다면, 계속해서 pop
        while (!s.empty()) {
            System.out.println("stack pop " + s.pop());    
        }
    }
}

Stack은 First in Last out으로 마지막에 들어간 값(push)이 처음으로 출력된다.(pop)

stack의 경우, 값이 비어있을 때, pop을 시도하면 EmptyStackException이 발생한다.

때문에 사용 시, try-cath로 묶어서 사용하거나 pop 이전에 empty() 함수로 사전 체크해서 오류를 방지해줘야 한다.

Queue
import java.util.*;

class Main {
    public static void main(String[] args) {
        Queue<Integer> q = new LinkedList<>();

        q.offer(1); // 1추가
        q.offer(2); // 2추가
        q.offer(3); // 3추가

        // 큐가 비어있지 않다면 계속 출력
        while(q.peek() != null) {
            System.out.println("다음 값은 " + q.peek());
            q.poll(); // 가장 앞의 값을 리턴 
        }

        System.out.println("마지막 " + q.poll());	// 출력: 마지막은 null
    }
}

Queue 자료구조는 First in First out으로 처음 들어온 값을 처음 출력하는 자료구조이다.

Queue 자료구조는 인터페이스로 존재하며, 인스턴스 생성은 LinkedList로 생성하면 된다.

 

offer(value): value 값을 추가한다.

peek(): 큐의 가장 앞의 값을 확인한다(출력 X). 비어 있다면 null을 출력한다. 비슷하게 isEmpty() 함수로 확인 가능하다.

poll(): 큐의 가장 앞의 값을 출력한다(가장 먼저 들어온 값). 값이 비어 있다면, null을 출력한다.

 

Deque
import java.util.ArrayDeque;
import java.util.Deque;

public class Main {
    public static void main(String[] args) {
        // Deque 생성 (Queue처럼 사용)
        Deque<Integer> queue = new ArrayDeque<>();
        queue.offerLast(1); // 뒤에 추가 (enqueue)
        queue.offerLast(2);
        queue.offerLast(3);
        System.out.println("Queue poll: " + queue.pollFirst()); // 앞에서 제거 (dequeue), 출력: 1
        System.out.println("Queue peek: " + queue.peekFirst()); // 앞에서 확인, 출력: 2

        // Deque 생성 (Stack처럼 사용)
        Deque<String> stack = new ArrayDeque<>();
        stack.offerFirst("A"); // 앞에 추가 (push)
        stack.offerFirst("B");
        stack.offerFirst("C");
        System.out.println("Stack poll: " + stack.pollFirst()); // 앞에서 제거 (pop), 출력: C
        System.out.println("Stack peek: " + stack.peekFirst()); // 앞에서 확인, 출력: B
    }
}

덱은 큐와 스택을 동시에 함께 상용할 수 있는 자료구조이다.

또한, 성능적인 측면에서도 Stack과 Queue 보다 유리한 면이 있어서 실제 테스트에서 Stack과 Queue를 사용해야할 때, 모두 Deque를 사용해도 좋다.

다만, Heap 구조를 위한 우선순위 큐(PriorityQueue)로 사용은 불가능하다.

 

Heap
import java.util.PriorityQueue;
import java.util.Collections; // 내림차순 정렬에 필요

public class Main {
    public static void main(String[] args) {
        // 기본 PriorityQueue (최소 힙 - 오름차순)
        PriorityQueue<Integer> minHeap = new PriorityQueue<>();

        minHeap.offer(30);
        minHeap.offer(10);
        minHeap.offer(20);
        System.out.println("Min Heap: " + minHeap); // 내부 저장 순서는 다를 수 있지만, poll/peek은 우선순위대로!

        System.out.println("Poll from Min Heap: " + minHeap.poll()); // 출력: 10 (가장 작은 값)
        System.out.println("Poll from Min Heap: " + minHeap.poll()); // 출력: 20
        System.out.println("Poll from Min Heap: " + minHeap.poll()); // 출력: 30

        System.out.println("--------------------");

        // PriorityQueue (최대 힙 - 내림차순)
        // Collections.reverseOrder() 사용해서 내림차순으로 정렬 기준 변경
        PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());

        maxHeap.offer(30);
        maxHeap.offer(10);
        maxHeap.offer(20);
        System.out.println("Max Heap: " + maxHeap); // 내부 저장 순서는 다를 수 있지만, poll/peek은 우선순위대로!

        System.out.println("Poll from Max Heap: " + maxHeap.poll()); // 출력: 30 (가장 큰 값)
        System.out.println("Poll from Max Heap: " + maxHeap.poll()); // 출력: 20
        System.out.println("Poll from Max Heap: " + maxHeap.poll()); // 출력: 10
    }
}

 

자주 쓰이는 알고리즘
정렬
import java.util.*;

public class SortingExample {
    public static void main(String[] args) {
        // 배열 정렬 (기본 오름차순)
        int[] arr = {5, 2, 8, 1, 4};
        Arrays.sort(arr);
        System.out.println("Sorted Array: " + Arrays.toString(arr)); // 출력: [1, 2, 4, 5, 8]

        // 배열 정렬 (람다식을 이용한 내림차순)
        Integer[] arrDesc = {5, 2, 8, 1, 4}; // 기본형 int는 람다식 쓰려면 Wrapper 클래스(Integer) 사용 필요
        Arrays.sort(arrDesc, (a, b) -> b - a);
        System.out.println("Sorted Array (Desc): " + Arrays.toString(arrDesc)); // 출력: [8, 5, 4, 2, 1]

        // List 정렬
        List<String> list = new ArrayList<>(Arrays.asList("Cherry", "Apple", "Banana"));
        Collections.sort(list); // 기본 오름차순
        System.out.println("Sorted List: " + list); // 출력: [Apple, Banana, Cherry]

        // List 정렬 (람다식을 이용한 내림차순)
        Collections.sort(list, (a, b) -> b.compareTo(a));
        System.out.println("Sorted List (Desc): " + list); // 출력: [Cherry, Banana, Apple]
    }
}

오름차순 정렬을 위해서는 Wrapper 클래스로 람다식을 사용하는 것이 편리하다.

다만 int 배열의 경우, Integer 배열을 사용해야하는데 Integer 배열을 사용하지 않으려면 약간의 기교로 이용해서 쉽게 사용 가능하다.

 

1. 오름차순으로 정렬 후 필요시 검색을 반대로 수행

2. 모든 값에 -1을 곱해서 오름차순 후 실제 필요한 값을 -1을 곱해서 사용

import java.util.*;

public class SortingExample {
    public static void main(String[] args) {
        // 배열 정렬 (기본 오름차순)
        int[] arr = {5, 2, 8, 1, 4};
        Arrays.sort(arr);
        System.out.println("Sorted Array: " + Arrays.toString(arr)); // 출력: [1, 2, 4, 5, 8]
		
        // 내림차순 기교
        int[] arr2 = {-5, -2, -8, -1, -4};	// 원래 값에 -1을 곱한 값을 사용
        Arrays.sort(arr2);	// 오름차순 정렬
        System.out.println("Sorted Array: " + Arrays.toString(arr2)); // 출력: [-8, -5, -4, -1] -> 값을 사용할 때, -1을 곱해서 사용
    }
}

 

 

이진탐색
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        // 이진 탐색은 반드시 정렬된 배열에서 사용
        int[] sortedArr = {1, 2, 4, 5, 8, 10, 12};

        // 값 5 찾기
        int index = Arrays.binarySearch(sortedArr, 5);
        System.out.println("Index of 5: " + index); // 출력: 3 (0부터 시작)

        // 값 7 찾기 (없는 값)
        int indexNotFound = Arrays.binarySearch(sortedArr, 7);
        System.out.println("Index of 7: " + indexNotFound); // 출력: 음수 값 (찾지 못했음을 의미)
    }
}

이진탐색은 정렬된 배열에서 특정 값을 찾는데 유용하다. 정렬이 안되어있는 배열이 주어진다면 sort로 직접 정렬 수행.

Java에서는 이진탐색 알고리즘을 메서드로 제공하여 쉽게 사용 가능하다.

 

BFS
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class BFSExample {
    static List<List<Integer>> graph;
    static boolean[] visited;

    public static void bfs(int startNode) {
        Queue<Integer> q = new LinkedList<>();
        q.offer(startNode);
        visited[startNode] = true;

        while (!q.isEmpty()) {
            int currentNode = q.poll();
            System.out.print(currentNode + " "); // 방문 노드 출력

            // 현재 노드와 연결된 노드들 확인
            for (int nextNode : graph.get(currentNode)) {
                if (!visited[nextNode]) {
                    visited[nextNode] = true;
                    q.offer(nextNode);
                }
            }
        }
    }

    public static void main(String[] args) {
        // 그래프 생성 (예시)
        int numNodes = 5;
        graph = new ArrayList<>();
        for (int i = 0; i <= numNodes; i++) {
            graph.add(new ArrayList<>());
        }
        graph.get(1).add(2);
        graph.get(1).add(3);
        graph.get(2).add(4);
        graph.get(3).add(4);
        graph.get(3).add(5);
        graph.get(4).add(5);

        visited = new boolean[numNodes + 1]; // 방문 기록 배열

        System.out.print("BFS 탐색 결과 (1부터): ");
        bfs(1); // 1번 노드부터 BFS 시작
        System.out.println();
    }
}

 

BFS(너비 우선탐색)은 시작 노드를 주변으로 가까운 노드들을 먼저 탐색하면서 범위를 확장하는 탐색 방법이다.

그래프나 트리구조의 탐색에 사용하며, 최단거리, 네트워크 찾기 등 이동의 가중치가 동일한 경우 사용한다.

 

DFS
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class Main {
    static int[] numbers = {1, 2, 3};
    static int n = numbers.length; // 총 숫자의 개수
    static int r = 3; // 선택할 숫자의 개수 (3자리 순열)
    static boolean[] visited = new boolean[n]; // 숫자를 사용했는지 체크
    static int[] result = new int[r]; // 현재 만들어지고 있는 순열

    // DFS (Backtracking) 함수
    // depth: 현재 몇 번째 숫자를 선택하고 있는지 (0부터 시작)
    public static void dfs(int depth) {
        // 기저 조건 (Base Case): r개의 숫자를 모두 선택했을 때
        if (depth == r) {
            // 완성된 순열 출력
            System.out.println(Arrays.toString(result));
            return; // 되돌아가기
        }

        // 재귀 호출 부분: 현재 depth 위치에 어떤 숫자를 넣을지 선택
        for (int i = 0; i < n; i++) {
            // i번째 숫자를 아직 사용하지 않았다면
            if (!visited[i]) {
                // 1. 선택 (상태 변경)
                visited[i] = true;      // i번째 숫자를 사용했다고 표시
                result[depth] = numbers[i]; // 현재 depth 위치에 i번째 숫자 저장

                // 2. 탐색 (더 깊이 들어가서 다음 숫자 선택)
                dfs(depth + 1);

                // 3. 원복 (백트래킹)
                // depth+1 에서 돌아오면 i번째 숫자를 사용했던 것을 취소
                // 그래야 다음 반복(i+1)에서 다른 숫자를 선택해서 새로운 순열
                visited[i] = false;
                // result[depth]는 다음 반복에서 덮어씌워지므로 여기서 따로 원복 안 해도 됨 (새로운 값으로 채워짐)
            }
        }
    }

    public static void main(String[] args) {
        System.out.println("순열 생성 결과:");
        dfs(0); // depth 0부터 시작 (첫 번째 숫자 선택)
        /*
        출력 결과:
        순열 생성 결과:
        [1, 2, 3]
        [1, 3, 2]
        [2, 1, 3]
        [2, 3, 1]
        [3, 1, 2]
        [3, 2, 1]
         */
    }
}

 

DFS(깊이 우선탐색)의 경우, 그래프나 트리구조의 탐색에서 사용하며 현재 노드와 연결된 노드를 모두 먼저 탐색하지 않고

다음 노드와 연결된 다음 노드가 있다면 해당 노드를 우선탐색한다.

1번 노드가 2, 3, 4 노드이고 2번과 연결된 노드가 5, 6(1제외)라면, 1->2->5 순으로 탐색한다.

 

위 코드는 1~3까지의 숫자로 만들 수 있는 배열의 조합을 출력하는 코드

DFS 탐색은 완전탐색으로 모든 경우를 탐색하다보니 시간초과의 이슈가 있을 수 있다.

 

3번의 백트래킹을 통해서 불가능한 경우, 더 이상 탐색하지 않고 종료하거나 모든 경우를 탐색하나 조건에 맞는 경우만 찾아야할 때 사용할 수 있다.

728x90
반응형

[Kotlin] 알고리즘을 위한 코틀린 자료구조

Posted by Space_Jin
2025. 3. 3. 19:03 Programming/Java, Kotlin
728x90
반응형
배열
    // 배열 초기화
    val array1 = arrayOf(1, 2, 3)
    val array2 = Array(3) {0}
    
    println(array1.joinToString())	// 1, 2, 3
    println(array2.joinToString())	// 0, 0, 0
    
    array2[0] = 1					// 0 인덱스의 값을 1로 변경
    println(array2.joinToString())	// 1, 0, 0
 
 		
    // ArrayList 생성
    val list = ArrayList<Int>() 
	
    list.add(1)
    list.add(2)
    
    println(list)	// 1, 2

 

 

스택 자료구조를 따로 가지고 있지 않으며, MutableList 인터페이스를 활용하여 스택을 구현이 가능하다

스택 - MutableList
    val stack1 = mutableListOf<Int>() 	// 빈 리스트 생성
    val stack2 = MutableList<Int>(3){0} // 0으로 초기화된 크기 3의 스택
	
    println(stack1) // []
    println(stack2) // [0, 0, 0]
    
    // 맨 끝에 1을 추가
    stack1.add(1)
    stack2.add(1)
    
    println(stack1) // [1]
    println(stack2) // [0, 0, 0, 1]
    
    println(stack1.isEmpty())		// false
    println(stack1.isNotEmpty())	// true
    
    val last = stack1.removeLast()	// 맨 끝 추출 후 리턴(pop)
    
    println(last)	// 1
    println(stack1)	// []

 

ArrayDeque를 이용하면 조금 더 빠르고 메모리 효율이 좋은 스택 자료구조를 표현할 수 있다.

스택 - ArrayDeque
    // 스택 생성
    val stack = ArrayDeque<Int>()

    // 스택에 요소 추가 (Push)
    stack.push(1)
    stack.push(2)
    stack.push(3)

    // 스택의 최상단 요소 확인
    println(stack.peek()) // 3

    // 스택에서 요소 제거 (Pop)
    println(stack.pop()) // 3
    println(stack.pop()) // 2

    // 스택의 현재 상태
    println(stack) // [1]

    // 스택이 비어 있는지 확인
    println(stack.isEmpty()) // false

 

 

큐 자료구조가 별도로 존재하지 않으며, LinkedList 인터페이스를 이용하여 구현가능

    val queue = LinkedList<Int>()
    queue.offer(1) // 1
    queue.offer(2) // 2
    queue.offer(3) // 3
    println(queue) // 1, 2, 3
    
    println(queue.peek()) // 가장 앞을 리턴
    println(queue) // 1, 2, 3

    println(queue.poll()) // 가장 앞을 추출하여 리턴
    println(queue) // 2, 3

 

 

우선순위 큐의 경우, 자료구조가 존재하며, 가장 작은 값을 우선시하는 min-heap 구조가 기본이다.

큰 값을 우선하는 max-heap 구조는 Comparator를 활용해서 구현 가능하다.

우선순위 큐
	// 우선순위 큐 생성, 기본적으로 최소 힙(min-heap) 구조
    val minHeap = PriorityQueue<Int>()

    // 요소 추가
    minHeap.add(5)
    minHeap.add(1)
    minHeap.add(3)
    minHeap.add(2)

    println("우선순위 큐 상태: $minHeap") // 1, 2, 3, 5

    // 요소 제거 (가장 낮은 값)
    while (minHeap.isNotEmpty()) {
        val removedOne = minHeap.poll() // 최상위 요소 제거 및 반환
        println("제거된 값: $removedOne") // 제거된 값 출력 (1, 2, 3,5 순)
    }

    
    // max-heap을 생성하기 위해 Comparator를 사용
    val maxHeap = PriorityQueue<Int>(compareByDescending { it })

    // 요소 추가
    maxHeap.add(5)
    maxHeap.add(1)
    maxHeap.add(3)
    maxHeap.add(2)

    println("최대 힙 상태: $maxHeap") // 5, 3, 2, 1

    // 요소 제거 (가장 큰 값)
    while (maxHeap.isNotEmpty()) {
        val removedOne = maxHeap.poll() // 최상위 요소 제거 및 반환
        println("제거된 값: $removedOne") // 제거된 값 출력 (5, 3, 2, 1 순서)
    }
728x90
반응형

[Spring] redis 분산 락 전략을 통한 동시성 처리(선착순, 더티 리드, 팬텀 리드)

Posted by Space_Jin
2025. 2. 23. 17:29 Programming/Java, Kotlin
728x90
반응형

Spring과 Redis를 활용한 동시성 처리 및 데이터 일관성

현대의 애플리케이션에서는 동시성 처리가 중요한 요소입니다. 특히 여러 사용자가 동시에 데이터를 수정하거나 조회하는 환경에서 데이터의 일관성을 유지하는 것은 큰 도전 과제가 됩니다. 

 

실제로 무작위 포인트(랜덤 포인트) 지급 이벤트를 개발했던 경험을 정리해보려고 글을 작성해 봅니다.


1. 동시성 처리의 필요성

동시성이란 여러 트랜잭션이 동시에 실행될 때 발생하는 현상으로, 데이터베이스에서의 읽기 및 쓰기 작업의 충돌을 관리하는 것이 중요합니다. 이러한 충돌이 발생하면 데이터의 일관성이 저해될 수 있으며, 이는 사용자 경험에 부정적인 영향을 미칠 수 있습니다.

 

2. Spring에서의 트랜잭션 격리 수준

Spring에서는 트랜잭션의 격리 수준을 설정하여 동시성 문제를 해결할 수 있습니다. 격리 수준에는 여러 종류가 있으며, 대표적으로 다음과 같은 것들이 있습니다:

● READ UNCOMMITTED: 다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있습니다. 가장 낮은 격리 수준으로 더티 리드가 발생할 수 있습니다.
 READ COMMITTED: 커밋된 데이터만 읽을 수 있으며, 더티 리드는 방지할 수 있습니다. 하지만 팬텀 리드가 발생할 수 있습니다.
 REPEATABLE READ: 같은 트랜잭션 내에서 같은 데이터를 여러 번 읽을 경우, 항상 같은 결과를 보장합니다. 팬텀 리드는 방지하지만 성능 저하가 있을 수 있습니다.

 SERIALIZABLE: 가장 높은 격리 수준으로, 트랜잭션을 순차적으로 처리하여 데이터의 일관성을 보장합니다. 하지만 성능이 크게 저하될 수 있습니다.


3. Redis의 분산 락

Redis는 인메모리 데이터베이스로 빠른 데이터 접근이 가능하며, 분산 락을 통해 동시성 문제를 해결하는 데 유용합니다. Redis의 분산 락을 활용하면 특정 트랜잭션이 진행 중일 때 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 할 수 있습니다. 이를 통해 다음과 같은 이점을 얻을 수 있습니다:

빠른 성능: Redis는 인메모리 데이터베이스이므로 높은 성능을 제공합니다.
동시성 관리: 분산 락을 통해 데이터의 동시 수정 문제를 방지할 수 있습니다.

 


실제 사용했던 랜덤 포인트 지급 이벤트 동시성 처리 예시 코드

Redis config
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {
    
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory());
        return template;
    }
}

 

Redis 분산 락 서비스
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class DistributedLockService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public boolean acquireLock(String lockKey, long timeout) {
        String lockValue = UUID.randomUUID().toString();
        Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, timeout, TimeUnit.MILLISECONDS);
        
        if (success != null && success) {
            return true; // 락을 성공적으로 얻음
        }
        return false; // 락을 얻지 못함
    }

    public void releaseLock(String lockKey, String lockValue) {
        // 현재 락의 값과 비교하여 동일할 경우에만 락 해제
        if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
            redisTemplate.delete(lockKey);
        }
    }
}

 

포인트 관리 서비스
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class PointConfigService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private PointConfigRepository pointConfigRepository; // 메인 DB와 연결된 리포지토리

    // 초기값 로딩
    public void loadInitialPointConfig() {
        int initialPoints = pointConfigRepository.getDailyPointLimit();
        redisTemplate.opsForValue().set("daily_point_limit", initialPoints);
    }

    // 관리자 페이지에서 포인트 조정
    public void updateDailyPointLimit(int newLimit) {
        // 메인 DB 업데이트
        pointConfigRepository.updateDailyPointLimit(newLimit);
        // Redis 업데이트
        redisTemplate.opsForValue().set("daily_point_limit", newLimit);
    }

    // 포인트 조회
    public int getDailyPointLimit() {
        Integer limit = (Integer) redisTemplate.opsForValue().get("daily_point_limit");
        if (limit == null) {
            // Redis에 값이 없으면 메인 DB에서 조회
            limit = pointConfigRepository.getDailyPointLimit();
            // Redis에 캐시
            redisTemplate.opsForValue().set("daily_point_limit", limit);
        }
        return limit;
    }
}

 

포인트 지급 서비스
@Service
public class PointService {

    @Autowired
    private DistributedLockService distributedLockService;

    @Autowired
    private PointRepository pointRepository; // 포인트 정보를 조회할 수 있는 레포지토리

    public int getCurrentPoints(String userId) {
        // Redis에서 포인트 조회
        Integer points = redisTemplate.opsForValue().get(userId);
        if (points == null) {
            // Redis에 값이 없으면 DB에서 조회
            points = pointRepository.getCurrentPoints(userId);
            // Redis에 캐시
            redisTemplate.opsForValue().set(userId, points);
        }
        return points;
    }

    public void allocatePoints(String userId, int pointsToAllocate) {
        String lockKey = "point_lock";
        long timeout = 30000; // 30초

        if (distributedLockService.acquireLock(lockKey, timeout)) {
            try {
                // 메인 DB에서 포인트 업데이트
                pointRepository.allocatePoints(userId, pointsToAllocate);
                // Redis에서도 값 업데이트
                int currentPoints = redisTemplate.opsForValue().get(userId);
                redisTemplate.opsForValue().set(userId, currentPoints + pointsToAllocate);
            } finally {
                distributedLockService.releaseLock(lockKey, UUID.randomUUID().toString());
            }
        } else {
            // 락을 얻지 못했을 경우 처리
        }
    }
}

 

관리자 페이지에서 일일 지급 가능한 포인트 조정
public void adjustPoints(int dailyPointLimit) {
    PointConfigService pointConfigService = new PointConfigService();
    
    // 초기값 로딩
    pointConfigService.loadInitialPointConfig();
    
    // 포인트 조정
    pointConfigService.updateDailyPointLimit(dailyPointLimit); // 새로운 포인트 한도 설정
}

실제 요구사항에서 관리자 페이지를 통통 일일 지급 가능한 포인트 조정이 실시간으로 필요했어서 추가되었다.

 

4. 더티 리드와 팬텀 리드

- 더티 리드(Dirty Read)
더티 리드는 트랜잭션이 커밋되지 않은 데이터를 읽는 현상입니다. 예를 들어, 한 트랜잭션이 데이터를 수정 중일 때 다른 트랜잭션이 그 데이터를 읽으면, 수정이 완료되기 전에 잘못된 정보를 기반으로 작업을 수행할 수 있습니다. 이는 데이터의 일관성을 저해합니다.

- 팬텀 리드(Phantom Read)
팬텀 리드는 한 트랜잭션에서 특정 조건을 만족하는 데이터셋을 조회할 때, 다른 트랜잭션이 데이터를 추가하거나 삭제하여 이전에 조회한 데이터셋과 다른 결과를 얻는 현상입니다. 이는 비즈니스 로직에 오류를 초래할 수 있습니다.

728x90
반응형

[Kotlin] IDE없이 온라인으로 Kotlin 연습하기

Posted by Space_Jin
2024. 10. 19. 15:22 Programming/Java, Kotlin
728x90
반응형
온라인 Kotlin 연습장

https://play.kotlinlang.org/

 

Kotlin Playground: Edit, Run, Share Kotlin Code Online

 

play.kotlinlang.org

위 사이트에 접속하면 별도의 IntelliJ와 같은 별도의 IDE 없이 Kotlin 코드를 작성하고 연습할 수 있다.

 

백준과 같은 알고리즘을 연습할 때, IDE를 통해서 프로젝트를 만들지 않아도된다는 점에서 유용하게 사용할 수 있을 것 같다.

728x90
반응형

[Java] modelmapper jdk 17 issue "modelmapper Unable to make field private final java.time.LocalDate java.time.LocalDateTime.date accessible: module java.base does not "opens java.time" to unnamed module" 해결

Posted by Space_Jin
2024. 3. 16. 17:08 Programming/Java, Kotlin
728x90
반응형

java spring 환경에서 modelmapper 를 사용하던 중 아래와 같은 이슈가 발생

"modelmapper Unable to make field private final java.time.LocalDate java.time.LocalDateTime.date accessible: module java.base does not "opens java.time" to unnamed module"

 

Java 9부터는 모듈 시스템이 도입되었으며, 모듈 간의 접근을 명시적으로 제어하게 되었습니다. 이 문제는 Java 9 이상에서 ModelMapper와 java.time 패키지의 사용 사이의 접근 제한으로 발생한 이슈

 

현재 내 환경은 jdk 17이었고 entity의 private으로 설정된 LocalDateTime 필드에 modelmapper로 접근하다가 일어난 문제이다.

 

여러 해결 방법이 있겠지만, 기존의 코드를 가장 건드리지 않고 해결하는 확실한 방법은 modelmapper의 버젼을 올리는 것

 

위 문제가 발생한 modelmapper의 버젼은 2.4.4 -> 3.1.1로 올려서 문제를 해결

 

내 빌드환겨은 gradle이었기에 build.gradle 에 dependency 를 아래와 같이 수정하여서 해결하였다.

 

implementation 'org.modelmapper:modelmapper:3.1.1'

 

728x90
반응형

[Java] 자바에서 인자의 실제 값이 변경되는 이유

Posted by Space_Jin
2022. 4. 5. 23:39 Programming/Java, Kotlin
728x90
반응형

Java는 인자로 넘겨준 객체의 값을 변경하면 해당 객체의 실제 값이 변경된다.

 

add 함수는 인자로 받는 model 객체 안의 number 필드의 값을 1 더하는 함수이다.

처음에 있는 model 객체를 add 함수의 인자로 넘겨서 실행하면 처음 model의 필드 값이 변경된다.

 

C언어나 python에서 함수에 인자를 넘기면 실제 값(혹은 객체의 주소 값)이 아닌 인자의 값을 복사 값이 넘어간다.

이렇게 원본의 복사 값을 인자에 넘겨주는 방법을 "Call By Value"라고 부른다.

실제로 원본 A를 변경하기 위해서 add함수의 return 값인 B 다시 대입해줄 수 있다.

Model model = new Model()

model = add(model)	// add 함수의 return은 Model class​

하지만 이러한 방법은 불편한 점이 많다.

 

만약, add 함수의 역할이 model의 number 필드 값에 특정 값을 더한 후 "ok"라는 문자열을 리턴해야 한다면 위 같은 방법을 사용할 수 없다.

 

하지만, java에서 코드를 짜 보면 알 수 있듯이 인자로 받은 객체의 값을 수정하면 원본의 값도 변경이 된다.

 

📌 그렇다면 java는 참조 값을 직접 호출하는 "Call By Reference" 방식을 지원하는 것인가?

그렇지 않다고 한다.

 

java는 기본적으로 call by value 방식을 지원한다.

 

그런데 왜 원본의 값이 변할까?

 

java는 인자를 넘겨줄 때, 해당 객체의 주소 값의 복사 값을 넘겨주기 때문이다.

 

위 그림을 예시로 들면 원본 A 역시 실제 model 객체의 주소 값을 가지고 있는 변수이다.

A를 add함수의 인자로 넘겨주면 B라는 변수에 A값의 복사 값을 담고 B값을 함수에 인자로 사용하게 된다.

이때, B를 통해 필드 값에 접근하면 A와 동일한 주소를 가지고 있으므로 실제 model 객체의 필드 값이 변경된다.

 

여러 언어를 섞어서 사용하다 보니 문득 헷갈리는 경우가 있는데 이번 기회에 정리하면서 기억에 더 오래 남았으면 좋겠다.

 

조금 더 자세한 내용이 필요하면 아래 글을 참고하시면 좋겠습니다.

 

아주 잘 정리해주셔서 많은 도움이 되었습니다^^.

 

[Java] 자바가 언제나 Call By Value인 이유 (Call By Reference X)

Intro 시작하기 앞서 CS이론에서는 "Call by value"와 "Call by reference"를 구분하는 것은 더 이상 쓸모없다고 한다. 왜냐하면 "Call By Reference"은 이제 트렌드에 뒤쳐진 기술로 선호도 굉장히 낮아져 최신..

loosie.tistory.com

728x90
반응형

[Java] XML파일과 자바(Java)로 파싱(parsing)하기

Posted by Space_Jin
2021. 12. 29. 20:22 Programming/Java, Kotlin
728x90
반응형

xml예시 파일

<?xml version="1.0" encoding="UTF-8"?>
<sample>
	<customer>
		<name>한국이</name>
		<age>25</age>
		<address>서울</address>
	</customer>
</sample>

java로 xml파일 parsing 하기

XML 파일에서 데이터를 가져오는 절차는 HTML 파일에서 데이터를 가져오는 절차와 유사합니다.

 

HTML에서 데이터를 가져오거나 수정하기 위해서는 필요한 태그를 인식해줄 필요가 있습니다.

 

예를 들어, document.getElementById("가져올 태그의 Id") 명령어를 이용해서 특정 아이디를 가진 태그를 인식할 수 있습니다.

 

이처럼 xml파일 역시 태그를 인식하기 위해서 필요한 절차는 문서의 자체 객체인 document를 가져오는 것입니다.

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;

// xml 파싱 빌드업
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();

// xml 파일을 document로 파싱하기
Document document = builder.parse("xml/sample.xml");

xml 파일의 위치는 원하는 상대 경로 혹은 절대 경로를 통해서 가져옵니다.

xml 파일 위치 예시

이제 자바가 xml 파일의 document를 가지고 와서 태그들을 인식할 준비가 되었습니다.

 

태그라는 것이 결국 document안에 있는 요소(element)이기 때문에 이 Element를 가져와야 합니다.

 

getDocumentElement() 메서드를 사용하면 가장 첫번째 요소(root 요소)를 가져 올 수 있습니다.

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

// root 요소 가져오기
Element root = document.getDocumentElement();
// root 요소 확인 : 첫 태그 sample
System.out.println(root.getNodeName()); 
// root 요소의 첫번째 노드는 #Text
Node firstNode = root.getFirstChild();
// 다음 노드는 customer
Node customer = firstNode.getNextSibling();
// customer 요소 안의 노드 리스트
NodeList childList = customer.getChildNodes();

root요소가 <sample> 태그이기 때문에 첫번째 Element인 root를 가져와서 getNodeName() 메소드를 이용해서 리턴된 문자열(String)을 출력하면 "sample"이라는 값을 얻을 수 있습니다.

 

getFirstChilde() 메소드를 사용하면 해당 요소의 첫 번째 있는 노드를 가져와서 리턴합니다.

 

이때, root라는 변수에 저장된 <sample> 태그의 첫 번째 node가 <customer> 태그가 아닌 #Test로 표시되는데 이는 xml파일의 빈 공백을 노드로 인식하기 때문입니다.

 

태그와 태그 사이의 공백을 하나의 노드로 인식하기 때문에 공백의 길이에 상관없이 하나의 노드가 됩니다.

 

getNextSibiling() 메서드를 이용하면 계속해서 해당 요소의 다음 노드를 리턴하게 됩니다.

 

첫 공백 노드에서 다음 노드인 <customer> 태그 노드에 오게 되면 getChildeNodes() 메서드를 이용해서 <customer> 태그 안의 노드들을 리스트로 리턴 받을 수 있습니다.

 

이제 customer 태그 안에 있는 태그들의 모든 데이터를 가져올 수 있습니다.

for(int i = 0; i < childList.getLength(); i++) {
	Node item = childList.item(i);
	if(item.getNodeType() == Node.ELEMENT_NODE) { // 노드의 타입이 Element일 경우(공백이 아닌 경우)
		System.out.println(item.getNodeName());
		System.out.println(item.getTextContent());
	} else {
		System.out.println("공백 입니다.");
	}
}

--출력 결과--

공백입니다.
name
한국이
공백 입니다.
age
25
공백 입니다.
address
서울
공백 입니다.


가져온 요소들의 집합으로 이루어진 리스트에서. item(index) 메서드를 이용해서 각각의 요소들을 가져올 수 있습니다.

 

위와 마찬가지로 첫 번째 요소는 공백을 요소로 가져오고 태그, 공백을 반복하면서 리스트에 저장되어있습니다.

 

이제 getTextContext() 메서드를 사용하면 태그 사이의 우리가 필요로 하는 데이터를 가져올 수 있습니다.

728x90
반응형