핵심 원칙: 실행 상태 최소화
상태를 줄이면 얻는 이점
- 이해하기 쉬운 코드: 추적해야 할 변수와 상태 전이가 적음
- 견고한 코드: 잘못된 상태 조합이 발생할 가능성 감소
- 테스트 용이성: 검증해야 할 경우의 수 감소
상태를 줄이는 주요 방법
- 가변 값 → 불변 값
- 부수 효과가 있는 함수 → 순수 함수
- 상태 전이 단순화
// 상태가 많은 예 (나쁨)
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 // 계산 속성
}
비직교 관계의 문제점
- 동기화 오류: 관련 변수를 함께 업데이트하지 않으면 불일치 발생
- 잘못된 상태: 논리적으로 불가능한 상태 조합 가능
- 복잡도 증가: 변수 간 의존성 추적 필요
// 비직교 예시 - 잘못된 상태 가능
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로 돌아갈 수 없음
}
'Devlog > 공부 아카이브' 카테고리의 다른 글
| [리뷰하기 좋은 코드 작성] 주석 작성하기 (0) | 2025.12.21 |
|---|---|
| [스프링 부트 정리] 스프링 부트 자세히 살펴보기 (0) | 2025.10.06 |
| [스프링 부트 정리] Spring JDBC 자동 구성 개발 (0) | 2025.10.06 |
| [스프링 부트 정리] 외부 설정을 이용한 자동 구성 (0) | 2025.10.06 |
| [스프링 부트 정리] 조건부 자동 구성 (0) | 2025.10.06 |