[Kotlin] 코틀린 기본 문법(클래스 2)

7 minute read


클래스는 내용이 많기 때문에 포스팅 2개로 나뉘어 작성됩니다.


클래스 2

이번 포스팅에서는 클래스의 핵심이라고 할 수 있는 클래스의 상속, 코틀린에서 지원하는 클래스의 확장, 그리고 각종 설계 도구들에 대해 알아봅니다.


6. 클래스의 상속과 확장


클래스의 상속은 코드를 재사용하는 측면도 있지만 코드를 체계적으로 관리할 수 있기 때문에 규모가 큰 프로젝트도 효과적으로 설계할 수 있습니다.

예를 들어 안드로이드에는 Activity라는 클래스가 있는데, 우리는 Activity 클래스에 이미 있는 기능은 직접 구현하는 대신 상속을 받아 약간의 코드만 추가하면 앱에 필요한 기능을 추가할 수 있습니다.


클래스의 상속

상속 대상이 되는 부모 클래스open 키워드로 만들어야만 자식 클래스에서 사용할 수 있습니다.

상속을 받을 자식 클래스는 콜론을 이용하여 상속할 부모 클래스를 지정합니다.

또한 상속은 부모의 인스턴스를 자식이 갖는 과정이기 때문에 부모 클래스명 다음에 괄호를 입력해서 꼭 부모의 생성자를 호출해야 합니다.

open class 상속될 부모 클래스{
    // 코드
}
class 자식 클래스: 부모 클래스(){
    // 코드
}


생성자 파라미터가 있는 클래스의 상속

상속될 부모 클래스의 생성자에 파라미터가 있다면 자식 클래스의 생성자를 통해 값을 전달합니다.

open class 부모 클래스(value: String){
    // 코드
}
class 자식 클래스(value: String): 부모 클래스(value){
    // 코드
}

부모 클래스에 세컨더리 생성자가 있다면, 역시 자식 클래스의 세컨더리 생성자에서 super 키워드로 부모 클래스에 전달할 수 있습니다.

👍 예시: 안드로이드의 View 클래스 상속 받기

class CustomView: View{
    constructor(ctx: Context): super(ctx)
    constructor(ctx: Context, attrs: AttributeSet): super(ctx, attrs)
}

💥 주의

**부모 클래스의 세컨더리 생성자를 이용하는 경우에는 부모 클래스명 다음에 오는 괄호를 생략합니다. **


부모 클래스의 프로퍼티와 메서드 사용하기

부모 클래스에서 정의된 프로퍼티와 메서드는 자식 클래스에서 내 것처럼 바로 사용할 수 있습니다.

👍 예시

open class Parent{
  var hello: String = "안녕하세요"
  fun sayHello(){
    Log.d("inheritance", "${hello}")
  }
}

class Child: Parent(){
  fun myHello(){
    hello = "Hello!"
    sayHello()
  }
}


프로퍼티와 메서드의 재정의: 오버라이드

상속받은 부모 클래스의 프로퍼티와 메서드 중에 자식 클래스에서는 같은 이름의 다른 용도로 사용해야 하는 경우가 있습니다.

이런 경우에 override 키워드를 사용해서 재정의할 수 있습니다. 오버라이드 할 때는 프로퍼티나 메서드도 클래스처럼 앞에 open을 붙여서 상속할 준비가 되어 있어야 합니다.

👍 예시

open class BaseClass{
  open var openedVar: String = "I am"
  open fun openedFun(){
    
  }
}
class ChildClass: BaseClass(){
  override var openedVar: String = "You are"
  override fun openedFun(){
    
  }
}


익스텐션

코틀린은 클래스, 메서드, 프로퍼티에 대해 익스텐션^Extention^ 을 지원합니다. 익스텐션이란, 이미 만들어져 있는 클래스에 다음과 같은 형태로 메서드를 추가하는 것을 말합니다.

fun 클래스.확장할 메서드(){
  // 코드
}

상속이 미리 만들어져 있는 클래스를 가져다 쓰는 개념이라면 익스텐션은 미리 만들어져 있는 클래스에 메서드를 넣는 개념입니다. 자신이 만든 클래스에 사용하기보다는 누군가 작성해둔, 이미 컴파일되어 있는 클래스에 메서드를 추가하기 위한 용도로 사용합니다.

👍 예제: String 클래스에 plus 함수 확장하기

fun String.plus(word: String): String{
  return this+word
}

// String 익스텐션 테스트
fun testStringExtension(){
  var original = "Hello"
  var added = "Guys~"
  // plus 메서드를 사용해서 문자열 더하기
  Log.d("Extension", "added를 더한 값은 ${original.plus(added)}입니다. ")
}

testStringExtension()

out:
added를 더한 값은 Hello Guys~입니다. 

익스텐션을 사용한다고 해서 실제 클래스의 코드가 변경되는 것은 아니며 단지 실행 시에 도트 연산자로 호출해서 사용할 수 있도록 해줍니다.



7. 설계 도구


객체 지향 프로그래밍은 구현(실제 로직을 갖는 코딩)과 설계(껍데기만 있는 코딩)로 구분할 수 있습니다. 이번에는 프로그래밍 설계에 사용하는 설계 도구에 대해 알아봅니다.


패키지

코딩하면서 파일을 분류하고, 이름을 짓고, 특정 디렉터리에 모아 놓는 일련의 행위들을 모두 설계라고 할 수 있습니다.

패키지는 클래스와 소스 파일을 관리하기 위한 디렉터리 구조의 저장 공간입니다. 다음과 같이 현재 클래스가 어떤 패키지(디렉터리)에 있는 지 표시합니다. 디렉터리가 계층 구조로 만들어져 있으면 온점( . )으로 구분해서 각 디렉터리를 모두 나눠줍니다.

package 메인 디렉터리.서브 디렉터리
class 클래스{
  
}

이 디렉터리 구조를 윈도우의 파일 탐색기에서 보면 다음과 같습니다.

image-20210725212237981

하나의 패키지에 여러 개의 파일을 생성할 수 있기 때문에 서로 관계가 있는 파일을 동일한 패키지에 만들어두면 관리가 용이합니다.


추상화

프로그래밍을 하기 전, 개념 설계를 하는 단계에서는 클래스의 이름과 메서드 이름 등을 임의로 정하여 먼저 나열합니다.

이때 명확한 코드는 설계 단계에서 메서드 블록 안에 직접 코드를 작성하는데, 그렇지 않은 경우에는 구현 단계에서 코드를 작성하도록 메서드의 이름만 작성합니다.

이것을 추상화라고 하며, abstract 키워드를 사용해서 명시합니다. abstract 키워드를 사용하면 open 키워드를 사용하지 않아도 자식 클래스에서 상속받을 수 있습니다.

구현 단계에서는 이 추상화된 클래스를 상속받아서 아직 구현되지 않은 부분들을 마저 구현합니다.

👍 예시

// 설계 단계
abstract class Animal{
  fun walk(){
    Log.d("abstract", "걷습니다.")
  }
  abstract fun move()
}

// 구현 단계
class Bird: Animal(){
  override fun move(){
    Log.d("abstract", "날아서 이동합니다.")
  }
}

위 코드의 Animal 클래스를 보죠.

walk 메서드는 명확하게 걸어가는 행위이지만 move는 어떤 동물인지에 따라 달라질 수 있습니다. 이와 같이 상속받을 자식 클래스의 특징에 따라 코드가 결정될 가능성이 있다면 해당 기능을 abstract 키워드로 추상화하는 것입니다.

이렇게 abstract 키워드가 붙은 클래스를 추상 클래스라고 하며, 추상 클래스는 독립적으로 인스턴스화 할 수 없기 때문에 구현 단계가 고려되지 않는다면 잘못된 설계가 될 수 있습니다.


인터페이스

인터페이스는 실행 코드 없이 메서드 이름만 가진 추상 클래스로 이해할 수 있습니다. 다음과 같이 말이죠.

interface 인터페이스명{
  var 변수: 타입
  fun 메서드1()
  fun 메서드2()
}

인터페이스는 interface 키워드를 사용하여 정의하며, 상속 관계의 설계보다는 외부 모듈에서 내가 만든 모듈을 사용할 수 있도록 메서드의 이름을 나열해둔 일종의 명세서로 제공됩니다.

코틀린에서는 대부분의 객체지향 언어들과 달리 프로퍼티도 인터페이스 내부에 정의할 수 있습니다.

또한, 추상 클래스와는 다르게 class 키워드는 사용하지 않습니다. 각 변수와 함수 앞에는 abstract 키워드가 생략되어 있습니다.


👍 예시

인터페이스 만들기

interface InterfaceKotlin{
  var variable: String
  fun get()
  fun set()
}

클래스에서 구현하기

인터페이스를 클래스에서 구현할 때는 상속과는 다르게 생성자를 호출하지 않고 인터페이스 이름만 지정합니다.

class KotlinImpl: InterfaceKotlin{
  override var variable: String = "init value"
  override fun get(){
    // 코드 구현
  }
  override fun set(){
    // 코드 구현
  }
}

인터페이스를 클래스의 상속 형태가 아닌 소스 코드에서 직접 구현할 때도 있는데, object 키워드를 사용해서 구현해야 합니다. 실제로 안드로이드 프로젝트를 시작하면 자주 사용하는 형태입니다.

이렇게 하면 오버라이딩된 프로퍼티와 메서드들을 갖는 클래스의 인스턴스를 바로 저장할 수 있습니다.

var kotlinImpl = object: InterfaceKotlin{
  override var variable: String = "init"
  override fun get(){
    // 코드
  }
  override fun set(){
    // 코드
  }
}

💥 주의

인터페이스는 외부의 다른 모듈을 위한 의사소통 방식을 정의하는 것입니다. 혼자 개발하거나 소수의 인원이 하나의 모듈 단위를 개발할 때는 인터페이스를 사용하지 않는 것이 좋습니다. 인터페이스를 남용하면 코드의 가독성과 구현 효율성이 떨어지기 때문입니다.

안드로이드가 제공하는 인터페이스를 자주 사용하는 이유는 안드로이드가 보았을 때 개발자가 만드는 모듈이 외부 모듈이기 때문입니다.


접근 제한자

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

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

  • 접근 제한자의 종류

    접근 제한자 제한 범위
    private 다른 파일에서 접근할 수 없습니다.
    internal 같은 모듈에 있는 파일만 접근할 수 있습니다.
    protected private와 같으나 상속 관계에서 자식 클래스가 접근할 수 있습니다.
    public 제한 없이 모든 파일에서 접근할 수 있습니다.

    접근 제한자는 서로 다른 파일에게 자신에 대한 접근 권한을 제공하는 것이며, 접근 지정자를 사용하지 않았을 경우 기본적으로 public 접근 제한자가 작용된다.

  • 모듈이란?

    코틀린에서 모듈이란 한 번에 같이 컴파일되는 모든 파일을 말합니다. 안드로이드를 예로 든다면 하나의 앱이 하나의 모듈이 될 수 있습니다. 또한 라이브러리도 하나의 모듈입니다.

👍 예시

open class Parent{
  private val privateVal = 1
  protected open val protectedVal = 2
  internal val internalVal = 3
  val defaultVal = 4
}

동일한 모듈 내의 자식 클래스에서 호출할 경우, privateVal을 제외한 프로퍼티들에만 접근이 허용됩니다.

다른 모듈의 상속 관계가 아닌 클래스에서 호출할 경우, defaultVal만 접근할 수 있습니다.


제네릭

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

👍 예시

클래스명 옆에 < E >라고 되어있는 부분에 String과 같은 특정 타입이 지정되면 클래스 내부에 선언된 모든 E에 String이 타입으로 지정됩니다.

public interface MutableList<E>{
  var list: Array<E>
}

이렇게 설계된 클래스를 우리는 주로 구현하는 용도로 사용하며, 컬렉션이나 배열에서 입력되는 값의 타입을 특정하기 위해 다음과 같이 사용합니다.

var list: MutableList<제네릭> = mutableListOf()



정리


  • 상속이 되는 클래스를 부모 클래스, 상속받는 클래스를 자식 클래스라고 합니다.
  • 부모 클래스는 open 키워드를 사용해야 합니다.
  • 자식 클래스에서 부모 클래스를 상속받을 때는 콜론( : )을 사용하고 부모 클래스명 뒤에 ( )를 붙여야 합니다.
    • 부모 클래스가 프라이머리 생성자가 있을 경우 ( ) 안에 생성자 파라미터와 함께 작성합니다.
    • 부모 클래스가 세컨더리 생성자가 있을 경우 부모 클래스 뒤에 ( )를 붙이지 않고 클래스 스코프 내에서 constructor(): super() 와 같이 작성합니다.
    • 부모 클래스의 생성자가 default 생성자일 경우에도 부모 클래스명 뒤에 ( )는 반드시 붙여야 합니다.
  • 부모 클래스로부터 상속받은 프로퍼티와 메서드들은 내 것처럼 사용할 수 있습니다.
  • 오버라이드를 원하는 경우 오버라이드되는 부모 클래스의 프로퍼티/메서드에 open 키워드가 있어야 하며, 오버라이드하는 자식 클래스의 프로퍼티/메서드에는 override 키워드를 사용합니다.
  • 코틀린은 클래스의 확장(익스텐션)을 지원하며, fun 클래스.확장할 메서드( ){ } 와 같이 사용합니다. 익스텐션을 사용한다고 해서 실제 클래스의 코드가 변경되는 것은 아니며 단지 실행 시에 사용할 수 있도록 해줍니다.
  • 패키지란 클래스와 소스 파일들을 관리하기 위한 디렉터리 구조의 저장 공간입니다.
  • 추상화란 설계 단계에서 확실하지 않은 클래스명, 프로퍼티명, 메서드명 등을 임의로 미리 선언해 놓는 것이며, 이는 구현 단계에서 작성됩니다.
    • 추상화는 abstract 키워드를 사용합니다. abstract 키워드를 사용하면 따로 open 키워드를 사용하지 않아도 상속이 가능합니다.
  • 인터페이스란 실제 실행 코드 없이 프로퍼티와 메서드들을 선언만 해놓은 추상 클래스로 이해할 수 있습니다.
    • 추상 클래스와 다르게 class 키워드는 사용하지 않고, interface 키워드를 사용하여 정의합니다.
    • 각 변수와 함수 앞에는 abstract 키워드가 생략되어 있습니다.
    • object 키워드를 사용하여 인터페이스를 상속 형태가 아닌 소스 코드 상에서 직접 구현하여 인스턴스를 생성할 수 있습니다.
  • 코틀린의 접근 제한자에는 private, internal, protected, public이 있습니다. 접근 제한자를 지정하지 않은 경우 기본적으로 public 이 적용됩니다.
  • 제네릭이란 입력되는 값의 타입을 자유롭게 사용햐기 위한 설계 도구입니다. < > 기호 내에 symbol을 사용하며, 실제 타입 지정 시 코드 블록 내의 symbol들은 지정된 타입으로 지정됩니다.
    • 보통 컬렉션이나 배열에서 입력되는 값의 타입을 특정하기 위해 **var list: MutableList<제네릭> = mutableList()** 와 같이 사용됩니다.

Categories:

Updated:

Leave a comment