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

데이터를 빠르게 검색하거나 저장하는 일은 개발에서 필수적이다. (항상 이걸 어떻게 하면 잘할까 고민한다..) 이러한 작업을 가장 효율적으로 수행하는 자료구조 중 하나가 바로 해시 테이블(Hash Table) 이다.

 

해시 테이블은 키-값(Key-Value) 쌍으로 데이터를 저장하며, 거의 상수 시간(평균 O(1))에 데이터에 접근할 수 있는 강력한 도구이다.

 

 

해시 테이블이란?

해시 테이블은 위에서 언급한것과 같이 키(key)와 값(value)의 쌍으로 데이터를 저장하는 구조이다. 여기서 중요한 개념은 해시 함수(hash function)로, 입력된 키를 고정된 크기의 인덱스로 변환한다. 이 인덱스는 데이터를 저장할 배열의 위치를 결정한다.

 

예를 들어 "apple"이라는 키를 입력하면, 해시 함수는 "apple"을 정수 42와 같이 특정 인덱스로 변환한다. 그러면 해시 테이블은 "apple"에 대응하는 값을 저장하게 된다.

 

 

해시 함수(Hash Function) 

해시 함수는 키를 받아서 배열의 인덱스로 변환하는 역할을 한다. 좋은 해시 함수는 다음 조건을 만족한다.

  • 결정론적 : 동일한 키는 항상 같은 해시 값을 반환해야 한다.
  • 균등 분포 : 가능한 모든 키에 대해 인덱스가 균등하게 분포되어 충돌을 최소화한다.
  • 빠른 계산 : 해시 함수는 빠르게 계산되어야 하며, 전체 성능에 큰 영향을 주지 않아야 한다.

 

해시 함수가 서로 다른 키에 대해 같은 인덱스를 반환하는 경우 이를 충돌(collision)이라고 한다. 충돌은 해시 테이블에서 피할 수 없는 문제이지만, 다양한 해결 기법을 통해 이를 효과적으로 관리할 수 있다.

 

충돌 해결 전략

  • 체이닝(Chaining) : 각 인덱스에 연결 리스트(또는 다른 자료구조)를 사용하여, 동일한 해시 값을 가진 여러 요소를 저장한다.
  • 오픈 어드레싱(Open Addressing) : 충돌이 발생하면, 해시 테이블 내에서 다른 빈 공간을 찾아 데이터를 저장한다. 대표적인 방법으로는 선형 탐사 (Linear Probing), 이차 탐사(Quadratic Probing) 등이 있다.

 

 

해시 테이블의 장단점 및 사용 시 고려사항

 

장점  : 

  • 빠른 검색 및 삽입 : 평균적으로 O(1)의 시간 복잡도로 데이터 접근이 가능하다.
  • 유연성 : 키와 값의 쌍으로 데이터를 관리하므로, 다양한 데이터 타입에 쉽게 적용할 수 있다.
  • 효율적인 메모리 사용 : 적절한 해시 함수와 충돌 해결 기법을 사용하면, 메모리 낭비 없이 효율적으로 데이터를 저장할 수 있다.

단점 및 고려사항 : 

  • 충돌 발생 가능성 : 해시 함수의 품질에 따라 충돌이 많이 발생하면 성능이 저하될 수 있다.
  • 동적 크기 조절 : 해시 테이블은 데이터의 양이 증가하면 크기를 동적으로 조절해야 하며, 이 과정에서 비용이 발생할 수 있다.
  • 해시 함수 선택 : 좋은 해시 함수를 선택하는 것은 해시 테이블의 성능과 직접적으로 연결되므로 매우 중요하다.

 

이런 해시테이블은 어디에 사용할 수 있을까?

  1. 데이터베이스 인덱싱 : 대규모 데이터베이스에서 해시 테이블은 빠른 검색과 데이터 접근을 위한 인덱스로 활용된다.
  2. 캐시(Cache) : 웹 애플리케이션에서 사용자 데이터를 캐싱하거나, 자주 사용하는 데이터를 임시 저장해주더 빠른 응답을 제공하는 데 해시 테이블을 사용한다.
  3. 집합 구현 : 집합은 중복 없는 데이터를 저장하는 자료구조이다. 해시 테이블을 기반으로 구현된 HashSet은 데이터의 중복 여부를 빠르게 판단할 수 있다.

 

Kotlin에서는 표준 라이브러리에서 HashMap을 제공하여, 해시 테이블을 쉽게 사용할 수 있다.

fun main() {
    // HashMap 생성: 키는 String, 값은 Int
    val hashMap = HashMap<String, Int>()
    
    // 데이터 삽입
    hashMap["apple"] = 5
    hashMap["banana"] = 3
    hashMap["cherry"] = 7
    
    // 데이터 조회
    println("사과의 수: ${hashMap["apple"]}") // 출력: 사과의 수: 5
    
    // 데이터 존재 여부 확인
    if (hashMap.containsKey("banana")) {
        println("바나나가 존재합니다.")
    }
    
    // 모든 키와 값 순회
    for ((key, value) in hashMap) {
        println("$key : $value")
    }
}

 

HashMap의 내부 동작 원리에 대해 살짝 알아보면

  • 해시 함수 적용 : 입력된 키에 대해 내부 해시 함수를 적용하여 인덱스를 계산하고
  • 충돌 처리 : 동일한 인덱스가 발생한 경우, 체이닝(LinkedList 등)을 사용하여 여러 값을 저장한다
  • 동적 크기 조절 : 데이터가 많아지면 자동으로 크기를 확장하여 성능 저하를 방지한다.

HashMap 내부적으로 처리를 해주기 때문에 그냥 가져다 쓰기만 하면 된다...!

 

 

해시 테이블 응용 문제

 

1. 중복된 문자 제거

문자열에서 중복된 문자를 제거하고, 결과를 유지하는 문제는 해시 테이블을 활용하여 해결할 수 있다.

fun removeDuplicates(s: String): String {
    val seen = HashSet<Char>()
    val sb = StringBuilder()
    
    for (ch in s) {
        if (seen.add(ch)) {  // add()는 중복된 문자가 없으면 true를 반환합니다.
            sb.append(ch)
        }
    }
    return sb.toString()
}

fun main() {
    val input = "banana"
    println("중복 제거 결과: ${removeDuplicates(input)}") // 출력 예: "ban"
}

 

 

2. 캐시 구현 예제

간단한 캐시를 해시 테이블로 구현하여, 자주 사용하는 데이터를 빠르게 검색하는 방법을 보여준다.

class SimpleCache<K, V> {
    private val cache = HashMap<K, V>()
    
    fun put(key: K, value: V) {
        cache[key] = value
    }
    
    fun get(key: K): V? {
        return cache[key]
    }
    
    fun contains(key: K): Boolean {
        return cache.containsKey(key)
    }
}

fun mainCache() {
    val cache = SimpleCache<String, Int>()
    cache.put("apple", 5)
    cache.put("banana", 3)
    println("apple: ${cache.get("apple")}")  // 출력: apple: 5
}

+ Recent posts