분할 정복 알고리즘의 기본 개념

분할 정복의 핵심 아이디어는 분할 정복 이름 그대로 '분할(Divide)', '정복(Conquer)', '결합(Combine)' 의 세 단계로 이루어져 있다.

  • 분할 : 큰 문제를 여러 개의 작은 문제로 나눈다.
  • 정복 : 나눈 작은 문제들을 각각 해결한다.
  • 결합 : 해결한 결과를 합쳐서 전체 문제의 답을 만든다.

쉽게 설명하면 큰 케이크를 한 번에 먹으려 하면 힘들지만, 조각조각 나눠서 먹으면 쉬운것과 비슷하다.

 

 

분할 정복의 대표 알고리즘으로는 병합 정렬과 퀵 정렬이 있다.

 

 

1. 병합 정렬(Merge Sort)

병합 정렬은 배열을 반으로 나누고, 각각을 정렬한 뒤, 두 정렬된 배열을 합쳐서 전체 배열을 정렬하는 방법이다.

 

멀쩡히 있는 배열을 왜 쪼갤까?

 

배열을 작게 나누면, 작은 문제들은 쉽게 해결되고, 마지막에 이들을 합치면 큰 문제도 쉽게 풀린다는 개념이다.

fun mergeSort(arr: IntArray): IntArray {
    if (arr.size <= 1) return arr  // 배열 크기가 1 이하이면 이미 정렬된 상태

    val mid = arr.size / 2
    val left = mergeSort(arr.copyOfRange(0, mid))
    val right = mergeSort(arr.copyOfRange(mid, arr.size))

    return merge(left, right)
}

fun merge(left: IntArray, right: IntArray): IntArray {
    var i = 0
    var j = 0
    val merged = mutableListOf<Int>()

    while (i < left.size && j < right.size) {
        if (left[i] <= right[j]) {
            merged.add(left[i])
            i++
        } else {
            merged.add(right[j])
            j++
        }
    }
    while (i < left.size) {
        merged.add(left[i])
        i++
    }
    while (j < right.size) {
        merged.add(right[j])
        j++
    }
    return merged.toIntArray()
}

fun mainMergeSort() {
    val arr = intArrayOf(38, 27, 43, 3, 9, 82, 10)
    println("원본 배열: ${arr.joinToString(", ")}")
    val sortedArr = mergeSort(arr)
    println("정렬된 배열: ${sortedArr.joinToString(", ")}")
}

 

위 코드의 실행 결과를 살펴보자

원본 배열: 38, 27, 43, 3, 9, 82, 10
정렬된 배열: 3, 9, 10, 27, 38, 43, 82

 

원본 배열이 정렬되는 것을 볼 수 있다.

 

 

 

2. 퀵 정렬(Quick Sort)

퀵 정렬은 배열에서 하나의 값을 피벗(pivot)으로 선택한 후, 피벗보다 작은 값과 큰 값으로 배열을 분할하고, 이를 재귀적으로 정렬하는 방법이다.

 

코드로 알아보는게 이해가 더 쉬울것 같다.

fun quickSort(arr: IntArray, low: Int = 0, high: Int = arr.size - 1) {
    if (low < high) {
        val pi = partition(arr, low, high)
        quickSort(arr, low, pi - 1)
        quickSort(arr, pi + 1, high)
    }
}

fun partition(arr: IntArray, low: Int, high: Int): Int {
    val pivot = arr[high]  // 1. 피벗으로 배열의 마지막 원소를 선택
    var i = low - 1        // 2. i는 피벗보다 작거나 같은 요소들이 위치할 인덱스의 마지막 위치를 추적

    // 3. low부터 high - 1까지 반복하여 피벗과 비교
    for (j in low until high) {
        if (arr[j] <= pivot) {  // 만약 현재 값이 피벗보다 작거나 같으면,
            i++                // i를 한 칸 증가시키고,
            // 4. arr[i]와 arr[j]를 교환하여, 피벗보다 작은 값들을 배열 앞쪽으로 모읍니다.
            val temp = arr[i]
            arr[i] = arr[j]
            arr[j] = temp
        }
    }
    // 5. i + 1 위치와 피벗(arr[high])을 교환하여, 피벗이 정렬된 위치에 오도록 합니다.
    val temp = arr[i + 1]
    arr[i + 1] = arr[high]
    arr[high] = temp

    return i + 1  // 피벗의 최종 위치를 반환합니다.
}

fun mainQuickSort() {
    val arr = intArrayOf(10, 7, 8, 9, 1, 5)
    println("원본 배열: ${arr.joinToString(", ")}")
    quickSort(arr)
    println("정렬된 배열: ${arr.joinToString(", ")}")
}

 

코드를 살펴보면 분할, 정복, 결합을 모두 볼 수 있다.

Kotlin을 공부중인데 val에 대해서 글을 읽던 중 엥? 이게 무슨 말이지 하는 부분이 있었다.

 

분명 "val 로 선언하면 값을 바꿀 수 없다" 라고 봤는데, val로 선언했는데 객체 내부 값이 변하는걸 목격했다.

 

분명히 고정된 값(val)인데 왜 내부 데이터는 바뀌는건지 혼란스러웠다.

 

이건 Kotlin이 값을 불변하게 만든다고 오해한 데서 시작된 착각이라는 것을 알았다.

 

진짜로 불변(Immutable)한 것은 "참조(reference)" 이지, "객체의 상태(state)" 가 아니다.

 

 

1. Kotlin의 val 과 var 기본 개념

먼저, Kotlin의 val 과 var 는 변수 선언 방식이다.

키워드 의미
val 읽기 전용(Read-only) 참조
var 읽기/쓰기(Read-Write) 참조

 

  • val 은 참조를 변경할 수 없다.
  • var 은 참조를 변경할 수 있다.

즉, val 은 "이 참조가 다른 객체를 가리키지 않도록 고정"하는 것이다. "참조하는 객체 재부"는 건들 수 있다.

 

val list = mutableListOf(1, 2, 3)
list.add(4)      //  가능
list.remove(2)   //  가능
// list = mutableListOf(5, 6, 7)  // ❌ 에러! 참조 변경 불가

 

위 코드에서 알아볼 수 있다.

  • list 가 가리키는 리스트 객체는 수정 가능하다.
  • 그러나 list 자체를 다른 리스트로 바꿀 수 없다.

 

var 은 참조 변경이 가능하다

var list = mutableListOf(1, 2, 3)
list = mutableListOf(10, 20, 30)  // 가능

 

 

오호..이제 알겠다. 근데 왜 이런 설계를 했을까???

 

2. val 설계 이유

Kotlin이 val 을 참조 고정만 보장하고, 객체 불변성을 강제하지 않는 이유는 다음과 같다.

 

이유 1 : 유연성과 기능

  • 객체를 통째로 새로 생성하는 것보다, 기존 객체를 수정하는 것이 빠를 때가 많다.
  • 특히 컬렉션 같은 경우 "수정 가능한 컬렉션 (mutableListOf)" 은 효율적이다.

이유 2: Kotlin의 철학

  • Kotlin은 "개발자에게 선택권을 준다" 는 철학을 지향한다.
  • 불변 객체를 원하면, 개발자가 immutable한 자료구조를 선택해야 한다.

Kotlin은 "불면(immutable)"을 강제하지 않는다. 읽기 전용 참조(read-only reference)만 보장한다.

 

 

3. Mutable 과 Immutable 객체

mutable = 내부 상태를 바꿀 수 있다.

immutable = 내부 상태를 바꿀 수 없다.

구분 설명 예시
Mutable 객체 객체 내부 데이터 변경 가능 mutableListOf, HashMap 등
Immutable 객체 객체 내부 데이터 변경 불가 listOf, Map (읽기 전용 View)

 

Kotlin의 listOf() 로 만든 리스트도 사실은 완전한 immutable은 아니다. 완전히 불변한 컬렉션을 원하면 별도로 관리해야 한다.

예: Collecitons.unmodifiableList(), 또는 직접 만드는 데이터 클래스

 

 

즉 비유를 하자면

 

val 은 집주소를 고정하는 것이다. 집 주소를 못 바꾸지만, 집안 가구 배치나 이런건 마음대로 바꿀 수 있지 않은가.

val 집 = MyHouse()
집.소파 = "새 소파로 교체" // OK
// 집 = 다른집() // ❌ 참조 변경은 불가

 

 

 

4. 데이터 클래스와 불변성

Kotlin에서는 데이터 클래스를 많이 사용한다.

data class Person(var name: String, var age: Int)

 

val 로 선언해도 name, age는 변경 가능하다.

 val person = Person("Alice", 25)
person.age = 26  // 내부 필드 변경 가능

 

  • person 이라는 참조는 고정된다
  • 하지만 person 객체 내부 필드(age)는 변경 가능하다.

 

정말 진짜 불변성을 원한다면 다음과 같이 만들 수 있다.

 

1. data class를 val 프로퍼티로만 만든다.

data class ImmutablePerson(val name: String, val age: Int)

 

이제 name, age를 바꿀 수 없다.

 

2. 불변 컬렉션을 사용한다.

val list = listOf(1, 2, 3)
// list.add(4)  // ❌ 컴파일 에러

 

 

 

주의할 점

상황 주의해야 할 점
API 리턴 타입을 val로 선언했지만 내부 객체가 Mutable인 경우 외부에서 상태가 변조될 수 있다.
글로벌 상태를 val로만 선언하고 안심하는 경우 Thread-safety는 별개 문제이다.

 

  • val 은 객체 상태를 보호해주지 않는다.
  • 불변성을 원하면 객체 설계 자체를 immutable하게 헤야 한다.

Q1: JWT 기반 인증 방식의 장점과 구현 방법에 대해 설명해 주세요.

A1:
"JWT(JSON Web Token)는 상태 정보를 서버에 저장하지 않는 stateless 인증 방식으로, 확장성과 성능 면에서 유리합니다. 토큰은 헤더, 페이로드, 서명으로 구성되며, 클라이언트에게 발급된 토큰을 요청 시 함께 전송하여 인증을 수행합니다. Spring Security와 함께 JWT 필터를 구현해 토큰의 유효성을 검사하고, 인증 정보를 설정하는 방식으로 구현할 수 있습니다."

 

 

Q2: Kotlin의 주요 특징과 Spring Boot와의 통합에서의 이점에 대해 설명해 주세요.

A2:
"Kotlin은 간결한 문법, Null 안전성, 데이터 클래스, 확장 함수, 람다 및 고차 함수 등 다양한 기능을 제공하여 코드의 가독성과 생산성을 크게 향상시킵니다. Spring Boot와 통합 시, 코루틴을 활용한 비동기 처리와 DSL을 통한 설정 간소화 등으로 개발 효율성을 높일 수 있습니다."

 

 

Q3: Java와 Kotlin 간의 상호 운용성에서 발생할 수 있는 문제점과 해결 방법은 무엇인가요?

A3:
"Java와 Kotlin은 JVM 기반 언어이지만, Null 처리, 제네릭, 애노테이션 등에서 차이가 발생할 수 있습니다. Kotlin에서는 Java 라이브러리를 사용할 때 Non-null과 Nullable을 명확히 구분해야 하며, 필요한 경우 @NotNull, @Nullable 애노테이션을 활용하여 상호 운용성을 보완합니다. 또한, 자동 변환 도구를 활용해 Java 코드를 Kotlin으로 마이그레이션할 때 안전하게 변환할 수 있습니다."

 

 

 

Q4: 분산 트랜잭션 문제를 해결하기 위한 SAGA 패턴에 대해 설명해 주세요.

A4:
"SAGA 패턴은 분산 환경에서 하나의 트랜잭션을 여러 개의 로컬 트랜잭션으로 나누어 실행하고, 각 단계에서 문제가 발생할 경우 보상 트랜잭션을 실행해 데이터 일관성을 유지하는 방법입니다. 이를 통해 전통적인 분산 트랜잭션의 단점을 보완할 수 있습니다."

 

 

 

Q5: CI/CD 파이프라인 구축 시 고려해야 할 사항과 이를 Spring Boot 프로젝트에 적용하는 방법은 무엇인가요?

A5:
"CI/CD 파이프라인은 소스 코드 변경 시 자동으로 빌드, 테스트, 배포가 진행되도록 하는 프로세스입니다. Jenkins, GitLab CI, GitHub Actions 등 도구를 활용하여, 코드 커밋마다 자동화된 테스트와 빌드, 그리고 스테이징/프로덕션 환경에 배포하는 과정을 구성합니다. 이를 통해 빠른 피드백과 안정적인 배포가 가능해집니다."

 

 

각각에 대해서 자세하게 알아야 할 것 같다. 프로젝트에 하나씩 적용해 나가며 학습해보자!

'기술면접준비' 카테고리의 다른 글

Spring Boot 관련 5가지 예상질문 및 답변  (0) 2025.04.16
WAS와 WS  (1) 2023.12.20
기술면접준비(1)  (0) 2023.12.19

+ Recent posts