공부/이펙티브코틀린

아이템 27 - 변화로부터 코드를 보호하려면 추상화를 사용하라

띵커베르 2024. 6. 21. 23:33
728x90
  • 소프트웨어는 시간이 지남에 따라 요구 사항, 기술 스택, 외부 라이브러리 등 다양한 이유로 변경될 수 있다. 이러한 변화는 코드의 유지보수에 큰 부담을 줄 수 있다.
  • 추상화로 실질적인 코드를 숨기면, 사용자가 세부 사항을 알지 못해도 괜찮다는 장점이 있다.
    • ex) 자동차가 작동만 한다면 내부를 변경하더라도 사용자는 무엇이 바뀐지 모를것이다.
  • 상수
    • 하드코딩된 값을 상수로 추출하여 코드의 가독성을 높이고, 변경 시 코드 전체를 수정할 필요 없이 상수만 수정하면 되게 한다.
    • 두 번 이상 사용되는 값은 이렇게 상수로 추출하는 것이 좋다.
    • 상수로 추출하면
      • 이름을 붙일 수 있고
      • 난중에 해당 값을 쉽게 변경할 수 있습니다.
// 하드코딩된 값
fun calculateDiscount(price: Double): Double {
    return price * 0.1
}

// 상수로 추출된 값
const val DISCOUNT_RATE = 0.1

fun calculateDiscount(price: Double): Double {
    return price * DISCOUNT_RATE
}

 

  • 함수
  • 동작을 함수로 래핑한다
    • 반복되는 코드나 복잡한 로직을 함수로 분리하여 재사용성을 높이고, 변경 시 함수 내부만 수정하면 되게 한다.
// 반복되는 로직
fun processOrder(order: Order) {
    if (order.isValid()) {
        order.complete()
    }
}

fun cancelOrder(order: Order) {
    if (order.isValid()) {
        order.cancel()
    }
}

// 함수로 래핑된 로직
fun executeIfValid(order: Order, action: (Order) -> Unit) {
    if (order.isValid()) {
        action(order)
    }
}

fun processOrder(order: Order) {
    executeIfValid(order) { it.complete() }
}

fun cancelOrder(order: Order) {
    executeIfValid(order) { it.cancel() }
}

 

  • 클래스
  • 함수를 클래스로 래핑한다.
    • 함수의 동작을 클래스로 감싸서 상태를 관리하거나 여러 관련된 동작을 하나의 객체로 묶어 다룰 수 있게 한다.
    • 클래스가 함수보다 강력한 이유는 상태를 가질 수 있으며, 많은 함수를 가질 수 있다는 점 때문이다.
// 함수만 존재하는 경우
fun logMessage(message: String) {
    println("Log: $message")
}

// 클래스로 래핑된 경우
class Logger(private val prefix: String) {
    fun log(message: String) {
        println("$prefix: $message")
    }
}

val logger = Logger("Log")
logger.log("This is a message")

 

  • 인터페이스
  • 인터페이스 뒤에 클래스를 숨긴다.
  • 구체적인 구현 클래스를 인터페이스 뒤에 숨겨서 의존성을 줄이고, 구현을 쉽게 교체할 수 있게 한다.
// 구체적인 구현에 의존하는 경우
class UserService {
    private val userRepository = DatabaseUserRepository()

    fun getUser(userId: String): User {
        return userRepository.getUser(userId)
    }
}

// 인터페이스 뒤에 클래스를 숨긴 경우
interface UserRepository {
    fun getUser(userId: String): User
}

class DatabaseUserRepository : UserRepository {
    override fun getUser(userId: String): User {
        // 데이터베이스 접근 로직
    }
}

class UserService(private val userRepository: UserRepository) {
    fun getUser(userId: String): User {
        return userRepository.getUser(userId)
    }
}

 

  • 보편적인 객체(universal obejct)를 특수한 객체(special object)로 래핑한다.
    • 일반적인 데이터 구조나 객체를 특정한 용도로 사용할 수 있도록 감싸는 패턴
// 보편적인 객체 (Map)
val userMap: MutableMap<String, String> = mutableMapOf()

// 특수한 객체로 래핑한 클래스
class UserRepository {
    private val users: MutableMap<String, String> = mutableMapOf()

    fun addUser(id: String, name: String) {
        users[id] = name
    }

    fun getUser(id: String): String? {
        return users[id]
    }

    fun removeUser(id: String) {
        users.remove(id)
    }

    fun getAllUsers(): Map<String, String> {
        return users.toMap() // 불변 Map을 반환
    }
}

// 사용 예제
fun main() {
    val userRepository = UserRepository()

    userRepository.addUser("1", "Alice")
    userRepository.addUser("2", "Bob")

    println(userRepository.getUser("1")) // 출력: Alice
    println(userRepository.getAllUsers()) // 출력: {1=Alice, 2=Bob}

    userRepository.removeUser("1")
    println(userRepository.getAllUsers()) // 출력: {2=Bob}
}

 


  • 추상화는 단순하게 중복성을 제거해서 코드를 구성하기 위한 것이 아닙니다.
  • 추상화는 코드를 변경해야 할 때 도움이 됩니다.,
  • 극단적인 추상화는 좋치않으니, 여러 경험을 통하여 균형을 찾아야 한다.
  • nextId
1.스레드 세이프하지 않은
// 이 코드는 ID가 무조건 0부터 시작하고, 스레드 세이프하지 않습니다. 
// 여러 스레드가 동시에 접근하면 올바른 ID를 생성하지 못할 수 있습니다.
var nextId: Int = 0
val newId = nextId++

2.함수로 추출
//이 코드는 ID 생성 방식을 함수로 추출하여 어느 정도의 보호를 제공
// 그러나 여전히 스레드 세이프하지 않으며, ID 타입 변경에 유연하지 않습니다.
private var nextId: Int = 0

fun getNextId(): Int {
    return nextId++
}

// 사용
val newId = getNextId()

3.클래스를 사용
data class Id(private var id: Int)

private var nextId: Int = 0

fun getNextId(): Id {
    return Id(nextId++)
}

4.스레드 세이프하게 만듦
data class Id(private var id: Int)

private var nextId: Int = 0

@Synchronized
fun getNextId(): Id {
    return Id(nextId++)
}

 

 


  • 균형은 어떻게 맞출까
    • 팀의 크기
    • 팀의 경험
    • 프로젝트의 크기
    • 특징 세트(feature set)
    • 도메인 지식
  • 많은 개발자가 참여하는 프로젝트는 이후에 객체 생성과 사용 방법을 변경하기 어렵습니다
    따라서 추상화 방법을 사용하는 것이 좋습니다.
    최대한 모듈과 부분(part)을 분리하는 것이 좋습니다.
  • 의존성 주입 프레임워크를 사용하면, 생성이 얼마나 복잡한지는 신경 쓰지 않아도 됩니다.
    클래스 등은 한 번만 정의하면 되기 때문입니다.
  • 테스트를 하거나, 다른 애플리케이션을 기반으로 새로운 애플리케이션을 만든다면 추상화를 사용하는 것이 좋습니다.
  • 프로젝트가 작고 실험적이라면, 추상화를 하지 않고도 직접 변경해도 괜찮습니다.
    문제가 발생했다면, 최대한 삘리 직접 변경하면 됩니다.

 

 

  • 생각
    • 적당한 추상화는 항상 좋은 것 같고, 대부분의 프로젝트가 크기에 상관없이 인터페이스 방식과 이를 구현하는 클래스로 정의를 많이하는 편이여서, 최소한의 추상화는 지켜진다고 생각한다.
    • 너무 앞서나가는 것도 좋치는 않지만, 프로젝트의 크기와 앞으로의 발전 가능성을 생각해서 어느정도 확장성을 생각하여 코드를 만드는것도 나쁘지 않은것 같다.
      음,,정확히는 확장성을 생각한다기보다는, 달리는 자동차에서 바퀴를 간다던지, 자동차에서 비행기로 변경할때 처음부터 다시 만드는일은 없었으면 한다...
      추상화의 개념을 조금 넘어선거 같긴하지만, 설계를 잘하는것도 중요하다고 생각한다.

 

728x90