코틀린 설계 도구 (패키지, 추상화, 인터페이스, 제네릭)

고성욱·2023년 3월 20일
0

안드로이드

목록 보기
17/26

객체지향 프로그래밍은 구현(실제 로직을 갖는 코딩)과 설계(껍데기만 있는 코딩)으로 구분할 수 있습니다.

패키지

컴퓨터 언어에서 패키지 사용의 목적이 설계라고 볼수 있습니다. 패키지는 클래스와 소스 파일을 관리 하기 위한 디렉토리 구조의 저장 공간 입니다.

코틀린에서 패키지는 물리적인 디렉토리 구조로 표현됩니다. 패키지 이름은 소문자로 작성되며, 점(.)으로 구분됩니다. 패키지 내에는 클래스, 함수, 프로퍼티 등이 포함될 수 있습니다. 패키지를 선언할 때는 package 키워드를 사용합니다.

예를 들어, org.example.myproject 패키지 내에 MyClass라는 클래스를 선언하려면 다음과 같이 작성합니다.

package org.example.myproject

class MyClass {
  // ...
}

패키지를 지정하지 않으면 기본 패키지가 사용됩니다.

추상화

추상화는 객체 지향 프로그래밍에서 중요한 개념 중 하나입니다. 추상화는 객체의 공통적인 특징을 뽑아내어 인터페이스나 추상 클래스 등으로 정의하는 것을 말합니다. 추상화를 통해 객체들 사이의 공통점을 찾는 것이 가능하며, 이를 바탕으로 코드의 유지보수성이나 재사용성을 높일 수 있습니다.

코틀린에서 추상화는 인터페이스나 추상 클래스를 사용하여 구현됩니다. 인터페이스는 객체의 동작 방식을 정의하는 일종의 계약서 역할을 하며, 추상 클래스는 추상 메서드와 구현된 메서드를 모두 포함할 수 있는 클래스입니다. 추상 클래스를 사용하면 공통적인 로직을 구현하고, 상속받은 클래스에서 필요한 부분만 구현할 수 있습니다.

  • 쉽게 설명하면 클래스 설계 과정에서 구현해야 하는 메서드의 이름만 명시 하고 구현은 나중에 하는것을 이야기 합니다.
  • Activity클래스가 상속 받는 클래스중 Context클래스는 abstract로 설계되어있습니다.

예를 들어, 다음은 추상 클래스와 인터페이스를 사용하여 추상화를 구현한 코드입니다.

// 추상 클래스
abstract class Animal {
    abstract fun makeSound()
    fun eat() {
        println("먹는다")
    }
}

// 인터페이스
interface Flyable {
    fun fly()
}

// 구현 클래스
class Dog: Animal() {
    override fun makeSound() {
        println("멍멍")
    }
}

class Bird: Animal(), Flyable {
    override fun makeSound() {
        println("짹짹")
    }

    override fun fly() {
        println("날다")
    }
}

추상화 주의 할 점

  • 과도한 추상화는 코드의 복잡도를 증가시킬 수 있습니다. 따라서 필요 이상으로 추상화를 사용하지 않도록 주의해야 합니다.
  • 추상화된 개념을 명확히 정의하고 문서화해야 합니다. 이를 통해 추상화된 개념에 대한 이해도를 높이고, 코드의 가독성을 향상시킬 수 있습니다.
  • 추상화된 개념은 실제로 사용되는 곳에서 검증되어야 합니다. 따라서 코드를 작성하기 전에 프로토타입을 만들어 테스트하는 것이 좋습니다.
  • 추상 클래스는 독립적으로 인스턴스와 할 수 없기 때문에 구현 단계가 고려되지 않는다면 잘못된 설계가 될 수 있습니다.

인터페이스

인터페이스는 객체 지향 프로그래밍에서 중요한 개념 중 하나로, 클래스나 객체가 어떤 메서드를 구현해야 하는지를 정의하는 일종의 계약서 역할을 합니다. 인터페이스는 클래스와 마찬가지로 추상적인 개념이기 때문에 직접 인스턴스화할 수 없습니다. 하지만 인터페이스를 구현하는 클래스를 만들고 해당 클래스를 인스턴스화하여 사용할 수 있습니다.

코틀린에서 인터페이스는 interface 키워드를 사용하여 정의됩니다. 인터페이스 내에서는 메서드 또는 프로퍼티를 선언할 수 있으며, 기본적으로 추상 메서드로 선언됩니다. 인터페이스 내에서 구현된 메서드는 없지만, 디폴트 구현을 제공할 수도 있습니다.

예를 들어, 다음은 Runnable 인터페이스를 구현하는 클래스의 예시 코드입니다.

interface Runnable {
    fun run()
}

class MyRunnable : Runnable {
    override fun run() {
        println("Hello, World!")
    }
}

val thread = Thread(MyRunnable())
thread.start()

이 예시 코드에서 MyRunnable 클래스는 Runnable 인터페이스를 구현합니다. Runnable 인터페이스는 run 메서드를 선언하고 있으며, MyRunnable 클래스에서는 run 메서드를 구현합니다. thread.start() 메서드를 호출하면, MyRunnable 클래스의 run 메서드가 실행됩니다.

추상화와 인터페이스의 차이점

  • 개념 클래스 중에 실행 코드가 한 줄이라도 있으면 추상화, 코드없이 메서드 이름만 나열되어 있으면 인터페이스 입니다.

접근제한자

코틀린에서 정의되는 클래스, 인터페이스, 메서트, 프로퍼티는 모두 접근 제한자를 가질 수 있습니다.

함수형 언어라는 특성으로 기존 객체지향에서 접근 제한자의 기준으로 삼았던 패키지 대신에 모듈 개념이 도입되었습니다. internal 접근 제한자로 모듈 간에 접근을 제한할 수 있습니다.

  • 접근 제한자의 종류
접근 제한자대상설명
public클래스, 인터페이스, 메서드, 프로퍼티어디서든지 접근 가능(기본적용)
internal모듈 내부같은 모듈 내에서만 접근 가능
protected클래스 내부, 하위 클래스클래스 내부와 하위 클래스에서만 접근 가능
private클래스 내부같은 클래스 내부에서만 접근 가능
  • public: 어디서든지 접근 가능합니다. 아무런 접근 제한자가 지정되지 않으면 기본값으로 public이 지정됩니다.
class Example {
    public var name: String = "John"
}
  • internal: 같은 모듈 내에서만 접근 가능합니다.
internal class Example {
    internal fun printHello() {
        println("Hello")
    }
}
  • protected: 클래스 내부와 하위 클래스에서만 접근 가능합니다.
open class Example {
    protected var name: String = "John"
}

class SubExample : Example() {
    fun printName() {
        println(name)
    }
}
  • private: 같은 클래스 내부에서만 접근 가능합니다.
class Example {
    private var name: String = "John"
    fun printName() {
        println(name)
    }
}

제네릭

제네릭은 입력되는 값의 타입을 자유롭게 사용하기 위한 설계도구입니다.

제네릭(Generic)은 입력되는 값의 타입을 자유롭게 사용하기 위한 설계도구입니다. 제네릭을 사용하면 클래스나 함수의 타입을 일반화할 수 있습니다. 이를 통해 클래스나 함수를 만들 때 타입에 대한 제약이 없어지므로, 코드의 재사용성이 높아집니다.

제네릭을 사용하는 방법은 다음과 같습니다.

class ClassName<T> {
    var variable: T
}

위의 예시에서 T는 제네릭 타입 매개변수입니다. T는 어떤 타입이든 될 수 있습니다. ClassName 클래스를 사용할 때, T 대신 구체적인 타입을 지정해줘야 합니다.

val obj = ClassName<String>()
obj.variable = "Hello, World!"

위의 예시에서 obj 변수는 ClassName 클래스의 인스턴스입니다. ClassName 클래스는 제네릭 클래스이므로, obj 변수 선언에서 제네릭 타입 매개변수 TString으로 지정해줍니다. 따라서 obj 변수의 variable 프로퍼티는 String 타입이 됩니다.

제네릭을 사용하는 가장 일반적인 예시 중 하나는 컬렉션입니다. 다음은 List 인터페이스를 구현하는 ArrayList 클래스에서 제네릭을 사용한 예시입니다.

val list: ArrayList<String> = ArrayList()
list.add("Hello")
list.add("Kotlin")
println(list)

위의 예시에서 list 변수는 ArrayList 클래스의 인스턴스입니다. ArrayList 클래스는 List 인터페이스를 구현하고 있으며, 제네릭 타입 매개변수로 String을 사용하고 있습니다. 따라서 list 변수는 String 타입을 요소로 갖는 리스트입니다. list.add() 메서드를 사용하여 요소를 추가할 수 있으며, println(list)를 호출하여 리스트의 내용을 출력할 수 있습니다.

제네릭을 사용하면 타입에 대한 제약이 없어지므로 코드의 재사용성이 높아집니다. 또한, 컴파일 시점에 타입 검사가 이루어지므로, 런타임에 발생할 수 있는 오류를 방지할 수 있습니다.

  1. 함수에서 제네릭 사용하기

    fun <T> List<T>.slice(indices: IntRange): List<T> {
        // ...
    }
    

    이 경우, 함수 이름 앞에 <T>를 작성하면 제네릭 함수를 선언할 수 있습니다. 여기서 T는 함수가 인수로 받는 타입을 나타냅니다.

  2. 클래스에서 제네릭 사용하기

    class Box<T>(t: T) {
        var value = t
    }
    

    이 경우, 클래스 이름 뒤에 <T>를 작성하면 제네릭 클래스를 선언할 수 있습니다. 여기서 T는 클래스가 인스턴스화될 때 사용되는 타입을 나타냅니다.

  3. 인터페이스에서 제네릭 사용하기

    interface List<T> {
        // ...
    }
    

    이 경우, 인터페이스 이름 뒤에 <T>를 작성하면 제네릭 인터페이스를 선언할 수 있습니다. 여기서 T는 인터페이스가 구현될 때 사용되는 타입을 나타냅니다.

profile
안드로이드, 파이썬 개발자

0개의 댓글