[리뷰하기 좋은 코드 작성] 상태 관리와 코드 설계

핵심 원칙: 실행 상태 최소화

상태를 줄이면 얻는 이점

  1. 이해하기 쉬운 코드: 추적해야 할 변수와 상태 전이가 적음
  2. 견고한 코드: 잘못된 상태 조합이 발생할 가능성 감소
  3. 테스트 용이성: 검증해야 할 경우의 수 감소

 

상태를 줄이는 주요 방법

  • 가변 값 → 불변 값
  • 부수 효과가 있는 함수 → 순수 함수
  • 상태 전이 단순화
 
// 상태가 많은 예 (나쁨)
class OrderProcessor {
    var currentStep = 0
    var isValidated = false
    var isPaid = false
    var isShipped = false
    
    fun process() {
        // 여러 상태 조합 가능 → 복잡도 증가
    }
}

// 상태를 줄인 예 (좋음)
sealed class OrderState {
    object Created : OrderState()
    object Validated : OrderState()
    data class Paid(val paymentId: String) : OrderState()
    data class Shipped(val trackingNumber: String) : OrderState()
}

class Order(val state: OrderState) // 명확한 상태 하나만 존재

→ ⚠️ 주의: 불변성은 수단이지 목적이 아님

가변 값이 더 적합한 경우

ex) 이진 트리의 너비 우선 탐색 (BFS)

// 불변성에 집착한 예 - 오히려 복잡함
fun bfsImmutable(root: Node): List<Int> {
    fun traverse(queue: List<Node>, result: List<Int>): List<Int> {
        if (queue.isEmpty()) return result
        
        val current = queue.first()
        val newQueue = queue.drop(1) + 
                      listOfNotNull(current.left, current.right)
        val newResult = result + current.value
        
        return traverse(newQueue, newResult) // 매번 새 리스트 생성
    }
    return traverse(listOf(root), emptyList())
}

// 가변성을 적절히 활용한 예 - 단순하고 효율적
fun bfsMutable(root: Node): List<Int> {
    val result = mutableListOf<Int>()
    val queue = ArrayDeque<Node>()
    queue.add(root)
    
    while (queue.isNotEmpty()) {
        val current = queue.removeFirst()
        result.add(current.value)
        current.left?.let { queue.add(it) }
        current.right?.let { queue.add(it) }
    }
    
    return result // 불변 리스트로 반환
}

판단 기준:

  • 전체 코드의 복잡도가 증가하는가?
  • 성능에 실질적인 영향이 있는가?
  • 코드의 의도가 더 명확해지는가?

균형 잡힌 접근

// 국소적 가변성 + 전역적 불변성
fun processData(input: List<Int>): List<Int> {
    val buffer = mutableListOf<Int>() // 내부에서만 가변
    
    for (value in input) {
        if (value > 0) buffer.add(value * 2)
    }
    
    return buffer.toList() // 외부에는 불변으로 노출
}

변수 간의 직교성(Orthogonality)

직교성이란?

두 변수가 서로 독립적이어서 한 변수의 값이 다른 변수에 영향을 주지 않는 관계

// 비직교 관계 - 두 변수가 연관됨
class Rectangle {
    var width: Int = 0
    var height: Int = 0
    var area: Int = 0 // width, height에 종속
    
    fun updateSize(w: Int, h: Int) {
        width = w
        height = h
        area = w * h // 동기화 필요 → 실수 가능
    }
}

// 직교 관계 - 독립적인 변수만 유지
class Rectangle(val width: Int, val height: Int) {
    val area: Int get() = width * height // 계산 속성
}

 

비직교 관계의 문제점

  1. 동기화 오류: 관련 변수를 함께 업데이트하지 않으면 불일치 발생
  2. 잘못된 상태: 논리적으로 불가능한 상태 조합 가능
  3. 복잡도 증가: 변수 간 의존성 추적 필요
 
// 비직교 예시 - 잘못된 상태 가능
class User {
    var isLoggedIn: Boolean = false
    var sessionToken: String? = null
    // 문제: isLoggedIn=true인데 sessionToken=null 가능
}

// 직교성 확보 - 합 타입 사용
sealed class UserState {
    object LoggedOut : UserState()
    data class LoggedIn(val sessionToken: String) : UserState()
}
class User(val state: UserState)

 

비직교 관계 제거 방법

1. 함수로 대체

// Before: 비직교
class Circle {
    var radius: Double = 0.0
    var diameter: Double = 0.0
    var circumference: Double = 0.0
}

// After: 함수로 대체
class Circle(val radius: Double) {
    val diameter: Double get() = radius * 2
    val circumference: Double get() = 2 * Math.PI * radius
}

2. 합 타입(Sealed Class)으로 대체

// Before: 여러 boolean으로 상태 표현
class Payment {
    var isPending: Boolean = false
    var isSuccess: Boolean = false
    var isFailed: Boolean = false
    var errorMessage: String? = null
    // 문제: 여러 상태가 동시에 true일 수 있음
}

// After: 합 타입으로 명확한 상태
sealed class PaymentStatus {
    object Pending : PaymentStatus()
    data class Success(val transactionId: String) : PaymentStatus()
    data class Failed(val error: String) : PaymentStatus()
}
class Payment(val status: PaymentStatus)

 


상태 전이 설계

1. 불변성 (Immutability)

객체 생성 후 상태가 변하지 않음
모든 프로퍼티가 val이고 불변 값을 가짐
 
// ✅ 완전한 불변 객체
data class Point(val x: Int, val y: Int)

// ✅ 프로퍼티 없는 불변 객체
object Logger {
    fun log(message: String) = println(message)
}

 

불변 vs 읽기 전용

// 읽기 전용 (Read-only) - 재할당 불가하지만 내부는 가변
val list = mutableListOf(1, 2, 3)
list.add(4) // OK - 내부 상태 변경 가능
// list = mutableListOf(5) // Error - 재할당 불가

// 불변 (Immutable) - 내부 상태도 변경 불가
val immutableList = listOf(1, 2, 3)
// immutableList.add(4) // Error - 변경 불가

 

값과 참조의 가변성

// 참조는 불변, 값은 가변
data class User(val name: String, val tags: MutableList<String>)
val user = User("연이", mutableListOf("admin"))
user.tags.add("editor") // OK - tags는 가변 리스트

// 완전한 불변
data class ImmutableUser(val name: String, val tags: List<String>)
val user2 = ImmutableUser("배고파", listOf("admin"))
// user2.tags.add("editor") // Error

 

부분적인 불변성

class Cache<K, V> {
    private val data = mutableMapOf<K, V>() // 내부는 가변
    
    // 외부에는 불변 인터페이스만 노출
    fun get(key: K): V? = data[key]
    
    // 새 인스턴스 반환으로 불변성 유지
    fun put(key: K, value: V): Cache<K, V> {
        val newCache = Cache<K, V>()
        newCache.data.putAll(this.data)
        newCache.data[key] = value
        return newCache
    }
}

 

2. 멱등성 (Idempotence)

같은 작업을 여러 번 수행해도 결과가 동일함
// 멱등 함수
fun setUserName(user: User, name: String): User {
    return user.copy(name = name)
}
// setUserName을 여러 번 호출해도 같은 결과

// 비멱등 함수
fun incrementCounter(user: User): User {
    return user.copy(loginCount = user.loginCount + 1)
}
// 호출할 때마다 결과가 달라짐

 

잘못된 상태 전이 제거하기

// 비멱등 - 중복 호출 시 문제
class ShoppingCart {
    private val items = mutableListOf<Item>()
    
    fun addItem(item: Item) {
        items.add(item) // 같은 아이템이 중복 추가될 수 있음
    }
}

// 멱등성 적용
class ShoppingCart {
    private val items = mutableMapOf<String, Item>()
    
    fun addItem(item: Item) {
        items[item.id] = item // 같은 id면 덮어씀 → 멱등
    }
}

 

내부 상태 은닉하기

// 내부 상태 노출 - 잘못된 전이 가능
class BankAccount {
    var balance: Int = 0 // 외부에서 직접 수정 가능
}
val account = BankAccount()
account.balance = -1000 // 잘못된 상태!

// 상태 은닉 - 안전한 전이만 허용
class BankAccount(private var balance: Int) {
    fun deposit(amount: Int) {
        require(amount > 0) { "금액은 양수여야 함" }
        balance += amount
    }
    
    fun withdraw(amount: Int): Boolean {
        if (amount > 0 && balance >= amount) {
            balance -= amount
            return true
        }
        return false
    }
    
    fun getBalance(): Int = balance
}

 

3. 비순환 (Acyclic)

순환의 문제점

// 순환 의존 - 무한 루프 가능
class Node(var next: Node? = null)

val node1 = Node()
val node2 = Node()
node1.next = node2
node2.next = node1 // 순환!

fun traverse(node: Node) {
    var current: Node? = node
    while (current != null) {
        println(current)
        current = current.next // 무한 루프 위험
    }
}

 

비순환 구조

// 비순환 - 종료 보장
sealed class LinkedList<T> {
    object Empty : LinkedList<Nothing>()
    data class Node<T>(val value: T, val next: LinkedList<T>) : LinkedList<T>()
}

fun <T> traverse(list: LinkedList<T>) {
    when (list) {
        is LinkedList.Empty -> return
        is LinkedList.Node -> {
            println(list.value)
            traverse(list.next) // 반드시 종료됨
        }
    }
}

 

순환이 필요한 경우

// 순환이 필요한 경우: 그래프 구조
class GraphNode(val id: Int) {
    private val _neighbors = mutableSetOf<GraphNode>()
    val neighbors: Set<GraphNode> get() = _neighbors
    
    fun addEdge(other: GraphNode) {
        _neighbors.add(other)
        other._neighbors.add(this) // 양방향 연결
    }
}

// 순환 처리: 방문 기록으로 무한 루프 방지
fun dfs(start: GraphNode) {
    val visited = mutableSetOf<GraphNode>()
    
    fun visit(node: GraphNode) {
        if (node in visited) return // 이미 방문함
        visited.add(node)
        
        println(node.id)
        node.neighbors.forEach { visit(it) }
    }
    
    visit(start)
}

실전 적용 체크리스트

상태 최소화:

  • 파생 가능한 값은 함수로 대체했는가?
  • 변수 간 의존성이 직교하는가?
  • 불변성이 오히려 복잡도를 높이지 않는가?

상태 전이 설계:

  • 잘못된 상태 조합이 가능한가?
  • 멱등성이 필요한 작업인가?
  • 순환 참조로 인한 무한 루프 가능성은?

균형:

  • 부분적 가변성으로 단순화할 수 있는가?
  • 성능과 가독성 중 무엇이 우선인가?
 
// 종합 예시: 균형 잡힌 설계
class OrderService {
    // 불변: 외부 인터페이스
    fun createOrder(items: List<Item>): Order {
        require(items.isNotEmpty()) { "주문 항목 필요" }
        
        // 가변: 내부 계산 (효율성)
        val itemsMap = items.groupBy { it.id }
        val totalPrice = items.sumOf { it.price }
        
        // 직교성: 독립적 속성만
        return Order(
            id = generateId(),
            items = items.toList(), // 불변 복사
            totalPrice = totalPrice // 계산된 값
        )
    }
}

// 멱등성: 동일 ID로 여러 번 조회해도 같은 결과
fun getOrder(id: String): Order? = repository.findById(id)

// 비순환: 명확한 상태 전이
sealed class OrderStatus {
    object Created : OrderStatus()
    object Confirmed : OrderStatus()
    object Shipped : OrderStatus()
    // 순환 불가: Shipped에서 Created로 돌아갈 수 없음
}