Study/object

[엘레강트 오브젝트] 4-3장 final이나 abstract이거나

에디개발자 2021. 10. 13. 06:00
반응형

나를 닮았다고 한다...

이 글은 엘레강트 오브젝트 새로운 관점에서 바라본 객체지향 도서를 보며 스터디한 글입니다.

책에서 주장하는 내용을 정리하였으며 예제들은 모두 코틀린 코드로 변환하여 작성하였습니다.

 

목차

  1. 잘못된 상속
  2. 클래스의 신분
  3. 올바른 설계
  4. 결론

1. 잘못된 상속

상속은 매우 강력한 기능입니다. 하지만 상속을 잘못 사용한다면 문제를 일으킬 수 있습니다. 문제를 일으키는 원인은 가상 메서드입니다. 

코드로 살펴보겠습니다. 

open class Document {
    fun length(): Int {
        return content().length
    }
    
    open fun content(): String {
        // read document
        // load byte array
        return ""
    }
}

위 코드와 같은 Document 클래스가 있습니다. 

  • content(): Document의 내용을 읽어와 반환합니다.
  • length(): content 메서드의 내용의 길이를 반환합니다.

 

그렇다면 Document의 내용을 인코딩하여 반환하는 클래스인 EncryptedDocument를 생성해보겠습니다. 이 클래스는 위 클래스를 상속받아 content 메서드를 Override하여 작성해보겠습니다.

class EncryptedDocument: Document() {

    override fun content(): String {
        // read document
        // load byte array
        // encrypt document
        return ""
    }
}

 

위 클래스의 length 메서드를 사용하면 어떤 값이 반환될까요??

val length = EncryptedDocument().length()

length() 메서드는 content() 메서드의 내용의 길이를 반환하는 메서드입니다. 하지만 EncryptedDocument 클래스에서 content 메서드를 Override하여 재구성했기 때문에 length() 메서드에서 반환하는 값도 바뀔 것입니다.

length() 
기대한 값: Document의 길이
실 결과값: Document를 인코딩한 길이

상속은 자식 클래스가 부모 클래스의 코드를 계승받는 하향식 프로세스입니다. 하지만 위 코드는 자식이 부모의 기능에 접근하여 영향을 미치는 구조로 작성되었습니다. 이런 문제는 유지보수할 때 큰 위험이 될 수 있습니다. 

 

이러한 잘못된 설계를 막는 방법에는 final이나 abstract를 사용하여 막을 수 있습니다.

2. 클래스의 신분

클래스는 final, abstract, 앞의 두 경우가 아닌 경우로 3가지 신분을 가지고 있습니다.

final

final 클래스는 사용자 관점에서 블랙 박스에 해당합니다. final 클래스는 상속이 불가능하고 이 클래스만으로 역할을 수행할 수 있는 능력을 가지고 있는 클래스입니다. 

 

abstract

abstract 클래스는 글래스 박스에 해당합니다. 불완전한 클래스로 스스로 행동할 수 없고 상속받는 클래스의 도움을 필요로 합니다. abstract 클래스는 특정 메서드를 abstract 처리해야하며 상속받는 클래스는 이 메서드를 반드시 구현해야만 합니다.

 

앞의 두 경우가 아닌 경우

이 클래스는 블랙 박스, 글래스 박스 두 경우 모두 해당하지 않습니다. 모든 메서드를 오버라이드할 수 있고 자신 혼자서 모든 역할을 수행할 능력도 갖추고 있습니다. 그렇기 때문에 개발자는 이 클래스를 사용하는 데에 있어 혼란스러울 수밖에 없습니다. 어떤 목적성을 가지고 구현된 클래스인지 모르기 때문입니다. 

 

3. 올바른 설계

위에서 작성한 Document에 대한 설계는 interface, abstract 를 이용한 2가지 방법으로 개선할 수 있습니다.

Interface

// Document interface
interface Document {
    fun length(): Int
    fun content(): String
}

// Document 인터페이스를 구현하는 DefaultDocument
// kotlin은 default가 final
class DefaultDocument: Document {
    override fun length(): Int {
        // code 동일
    }

    override fun content(): String {
        // code 동일
    }

}

// Document 인터페이스를 구현하는 EncryptedDocument
class EncryptedDocument(
    private val plain: Document
): Document {
    override fun length(): Int {
        return this.plain.length()
    }

    override fun content(): String {
        val content = this.plain.content()
        return encrypt(content)
    }

}

Document 인터페이스를 통해 length(), content() 메서드를 오버라이드하여 구현하는 방법입니다.

 

Document 인터페이스를 구현하는 DefaultDocument 클래스는 final 처리되어있기 때문에 상속이 불가능합니다. Document를 암호화하는 클래스를 만들려면 새로운 클래스를 통해 Document를 구현하는 방식으로 설계할 수 밖에 없습니다. 이렇게 개발자에게 문제가 발생할 가능성을 닫아두게 설계할 수 있습니다.

 

abstract

// Document 추상 클래스
abstract class Document {
    abstract fun content(): String
    fun length(): Int {
        return this.content().length
    }
}

class DefaultDocument: Document() {
    override fun content(): String {
        // read document
    }
}

class EncryptedDocument: Document() {
    override fun content(): String {
        // read document
        // encrypt document
    }

}

Document 추상클래스를 이용하여 설계한 코드입니다.

 

이 방법은 위에서 설계상 혼란을 줄 수 있는 코드와 비슷하게 느껴질 것입니다. 당연하게도 결과는 동일할 것입니다. 하지만 차이점은 Document가 추상클래스로 content() 메서드를 구현을 필수적으로 선언했다는 차이가 있습니다. 이를 통해 개발자는 length() 메서드가 어떻게 동작할지 미리 인지하고 코드를 작성하게 될 것입니다.

 

4. 결론

클래스는 명확하게 어떤 의도로 설계된 것인지 구분해야합니다. 명확한 설계가 없다면 상속을 사용하지 않는 것이 나을 수 있습니다.

반응형