독서

[이펙티브 코틀린] 4장. 추상화 설계

오렌지색 귤 2024. 7. 15. 01:54
반응형

아이템 26. 함수 내부의 추상화 레벨을 통일하라

 

 

p. 160

 

추상화 레벨 통일 원칙 (Single Level of Abstraction, SLA)

 

SLA 원칙 : 함수나 메소드가 동일한 수준의 추상화를 유지해야 한다는 원칙

 

 

SLA 원칙 위반 코드 예시

fun processData() {
    val data = fetchDataFromApi() // 추상화된 단계
    val results = data["results"] as List<Map<String, String>> // 추상화된 단계

    val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password") // 세부 사항
    val statement = connection.prepareStatement("INSERT INTO table (column1, column2) VALUES (?, ?)")

    for (item in results) {
        statement.setString(1, item["field1"])
        statement.setString(2, item["field2"])
        statement.executeUpdate() // 세부 사항
    }

    connection.close() // 세부 사항
}

 

위 예제에서는 processData 함수가 데이터를 가져오는 추상화된 작업과 데이터베이스 연결 및 데이터 삽입과 같은 세부 사항을 모두 포함하고 있다.

 

 

올바른 예시

fun processData() {
    val data = fetchDataFromApi()
    val results = extractResults(data)
    saveDataToDatabase(results)
}

fun fetchDataFromApi(): Map<String, Any> {
    // 데이터를 API에서 가져오는 추상화된 단계
    return mapOf("results" to listOf(
        mapOf("field1" to "value1", "field2" to "value2"),
        mapOf("field1" to "value3", "field2" to "value4")
    ))
}

fun extractResults(data: Map<String, Any>): List<Map<String, String>> {
    // 데이터를 추출하는 추상화된 단계
    return data["results"] as List<Map<String, String>>
}

fun saveDataToDatabase(data: List<Map<String, String>>) {
    // 데이터를 데이터베이스에 저장하는 세부 사항
    val connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db", "user", "password")
    val statement = connection.prepareStatement("INSERT INTO table (column1, column2) VALUES (?, ?)")

    for (item in data) {
        statement.setString(1, item["field1"])
        statement.setString(2, item["field2"])
        statement.executeUpdate()
    }

    connection.close()
}

 

위 수정된 예제에서는 processData 함수가 동일한 수준의 추상화된 단계만 포함하고 있다:

 

  • 데이터를 가져오는 추상화된 단계 (fetchDataFromApi)
  • 데이터를 추출하는 추상화된 단계 (extractResults)
  • 데이터를 저장하는 추상화된 단계 (saveDataToDatabase)

 

각 함수는 자신만의 세부 사항과 추상화 수준을 가지므로, 코드가 더 명확하고 유지보수하기 쉬워진다.

 

 

 

 

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

 

생략

 

아이템 28. API 안정성을 확인하라

 

생략

 

아이템 29. 외부 API를 랩(wrap)해서 사용하라

 

생략

 

아이템 30. 요소의 가시성을 최소화하라

 

생략

 

 

 

 

아이템 31. 문서로 규약을 정의하라

 

p.198

 

리스코프 치환 원칙

 

리스코프 치환 원칙 : "S가 T의 서브타입이라면, 별도의 변경 없이 T 타입 객체를 S 타입 객체로 대체할 수 있어야 한다"

 

 

리스코프 치환 원칙 위반 코드 예시

open class Bird {
    open fun fly() {
        println("Bird is flying")
    }
}

class Ostrich : Bird() {
    override fun fly() {
        // 타조는 날 수 없기 때문에 UnsupportedOperationException을 던짐
        throw UnsupportedOperationException("Ostrich can't fly")
    }
}

fun letBirdFly(bird: Bird) {
    bird.fly()
}

fun main() {
    val bird: Bird = Bird()
    letBirdFly(bird) // 정상 작동: "Bird is flying"

    val ostrich: Bird = Ostrich()
    letBirdFly(ostrich) // 예외 발생: UnsupportedOperationException
}

 

OstrichBird의 서브타입이지만 Bird처럼 동작하지 않는다.

 

 

올바른 예시 코드

open class Bird {
    open fun move() {
        println("Bird is moving")
    }
}

class Sparrow : Bird() {
    override fun move() {
        println("Sparrow is flying")
    }
}

class Ostrich : Bird() {
    override fun move() {
        println("Ostrich is running")
    }
}

fun letBirdMove(bird: Bird) {
    bird.move()
}

fun main() {
    val sparrow: Bird = Sparrow()
    letBirdMove(sparrow) // 정상 작동: "Sparrow is flying"

    val ostrich: Bird = Ostrich()
    letBirdMove(ostrich) // 정상 작동: "Ostrich is running"
}

 

Sparrow는 날고 Ostrich는 달리지만, move 메서드를 호출하는 측에서는 이를 알 필요가 없다.

이렇게 하면 Bird 타입의 객체를 SparrowOstrich로 대체하더라도 프로그램이 정상적으로 작동한다.

 

 

 

 

아이템 32. 추상화 규약을 지켜라

 

p. 203

 

Any 클래스는 다음 세 가지 중요한 메서드를 제공한다

 

public open class Any {
    public open operator fun equals(other: Any?): Boolean
    public open fun hashCode(): Int
    public open fun toString(): String
}

 

 

4장 전체

 

토론 포인트

 

  1. 팀 내에서 적절한 클래스와 함수의 추상화 수준을 논의해본 적이 있는가?
  2. 코드 리뷰 과정에서 추상화 수준을 평가하고 개선하기 위한 체계적인 방법이 있는가?
반응형