[Android/Kotlin] 12 클래스와 설계

[Android/Kotlin] 12 클래스와 설계

프로그래밍에서 필수적으로 사용되는 클래스에 대해서 알아보자!


1) 클래스의 기본 구조

  • Kotlin에서 사용되는 클래스의 기본 구조는 다음과 같다.
class 클래스명 {
    var 변수
    fun 함수() {
        // 코드
    }
}
  • 다음은 문자열을 저장할 수 있는 String 클래스의 코드를 함축해서 보여주는 것이다.
class String{
    var length: Int
    fun plus(other: Any){
        // code
    }
    fun compareTo(other: String){
        // code
    }
}
  • 위 예제에서 length 변수로 문자열이 길이를 알 수 있고, plus 함수는 문자열을 이어붙일 수 있다.

  • 또한 compareTo는 문자열을 비교하는 기능을 제공한다.

2) 클래스 코드 작성하기

  • 클래스를 만들기 위해서는 먼저 클래스의 이름을 정하고 이름 앞에 class 키워드를 붙여서 만들 수 있다.

  • 클래스 이름 다음에는 클래스의 범위를 지정하는 중괄호 { }가 있어야 한다.

  • 중괄호 { }를 스코프(Scope)라고 하는데, 클래스에서 사용하면 클래스 스코프라고 한다.

class 클래스이름 {
    // 클래스 스코프 (class scope)
}
  • 몇몇 예외는 존재하지만 대부분의 코드는 클래스 스코프 안에 작성된다.

  • 작성된 클래스를 사용하기 위해서는 생성자라고 불리는 함수가 호출되어야 하는데, Kotlin은 프라이머리(Primary)와 세컨더리(Secondary) 2개의 생성자를 제공한다.

프라이머리 생성자

class Person 프라이머리 생성자() {
}
  • 프라미어리 생성자(Primary Constructor)는 마치 클래스의 헤더처럼 사용할 수 있으며 constructor 키워드를 사용해서 정의하는데 조건에 따라 생략할 수 있다.

  • 프라이머리 생성자도 결국은 함수이기 때문에 파라미터를 사용할 수 있다.

class Person constructor(value: String){
    // code
}
  • 생성자에 접근 제한자나 다른 옵션이 없다면 constructor 키워드를 생략할 수 있다.
class Person(value: String){
    // code
}
  • 프라이머리 생성자는 마치 헤더처럼 class 키워드와 같은 위치에 작성된다.

  • 클래스의 생성자가 호출되면 init 블록의 코드가 실행되고, init 블록에서는 생성자를 통해 넘어온 파라미터에 접근할 수 있다.

class Person(value: String){
    init{
        Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
    }
}
  • init 초기화 작업이 필요하지 않다면 init 블록을 작성하지 않아도 된다.

  • 대신 파라미터로 전달된 값을 사용하기 위해서는 파라미터 앞에 변수 키워드인 val을 붙여주면 클래스 스코프 전체에서 해당 파라미터를 사용할 수 있다.

class Person(val value: String) {
    fun process() {
        print(value)
    }
}
  • 생성자 파라미터 앞에 var도 사용할 수 있지만, 읽기 전용인 val을 사용하는 것을 권장한다.

세컨더리 생성자

  • 세컨더리 생성자(Secondary Constructor)는 constructor 키워드를 마치 함수처럼 클래스 스코프 안에 직접 작성할 수 있다.

  • 다음과 같이 init 블록을 작성하지 않고 constructor 다음에 괄호를 붙여서 코드를 작성하면 된다.

class Person {
    constructor (value: string) {
        Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
    }
}
  • 세컨더리 생성자는 파라미터의 개수, 또는 파라미터의 타입이 다르다면 여러 개를 중복해서 만들 수 있다.
class Kotlin {
    constructor (value: String){
        Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")        
    }
    constructor (value: Int){
        Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")        
    }
    constructor (value1: Int, value2: String){
        Log.d("class", "생성자로부터 전달받은 값은 ${value1}, ${value2}입니다.")        
    }
}

Default 생성자

  • 생성자는 작성하지 않을 경우 파라미터가 없는 프라이머리 생성자가 하나 있는 것과 동일하다.
class Student { // 생성자를 작성하지 않아도 기본 생성자가 동작합니다.
    init {
        // 기본 생성자가 없더라도 초기화가 필요하면 여기에 코드를 작성합니다.
    }
}

3) 클래스의 사용

  • 클래스의 이름에 괄호를 붙여서 클래스의 생성자를 호출한다.
  • constructor 키워드를 호출하지는 않는다.
클래스명()
  • 아무런 파라미터 없이 클래스명에 괄호를 붙여주면 생성자가 호출되면서 init 블록 안의 코드가 자동으로 실행된다.

  • 세컨더리 생성자의 경우 init 블록이 먼저 실행되고, constructor 블록 안의 코드가 그다음 실행된다.

  • 다음과 같이 Kotlin 클래스의 생성자를 호출한 후 생성되는 것을 인스턴스(Instance)라고 하는데, 생성된 인스턴스는 변수에 담아둘 수 있다.

var kotlin = Kotlin()
  • 클래스와 인스턴스의 관계를 비교할 때 가장 많이 사용되는 예가 붕어빵 틀과 붕어빵이다.

img

  • 여기서 붕어빵들이 클래스에 해당되고 계속 만들 수 있는 붕어빵이 인스턴스와 같다.
  • 생성자에 파라미터가 있으면 값을 입력해서 호출해야 한다.
var one = Person("value")
// 또는
var two = Person(1004)

여기서 잠깐!

  • 프로퍼티와 메서드

    • 클래스 내부에 정의되는 변수와 함수를 멤버 변수, 멤버 함수라고 부른다.

    • 또 다른 용어로 프로퍼티, 메서드라고도 부른다.

      • 클래스의 변수 > 멤버 함수 > —– 프로퍼티(Property)

      • 클래스의 함수 > 멤버 함수 > —– 메서드(Method)

    • 클래스 안에 정의된 변수는 프로퍼티라고 하지만 함수 안에 정의된 변수는 프로퍼티가 아닌 변수(지역 변수)라고 한다.

class 클래스명 {
    var 변수A    // 프로퍼티: 함수 밖에 있어야 합니다.
    fun 함수(){
        var 변수B    // 변수(또는 지역변수): 함수 안에 있어야 합니다.
    }
}
  • 프로퍼티와 메서드를 사용하기 위해서 다음과 같이 프로퍼티 1개와 메서드 1개를 갖는 클래스를 만든다.
class Pig {
    var name: String = "Pinky"
    fun printName(){
        Log.d("class", "Pig의 이름은 ${name}입니다.")
    }
}
  • 위에서 정의한 클래스를 생성자로 다음과 같이 인스턴스화해서 변수에 담는다.
var pig = Pig()
  • 인스턴스가 담긴 변수명 다음에 도트 연산자(.)를 붙여서 프로퍼티와 메서드를 사용한다.
pig.name = "Pooh"
pig.printName()

/** 실행결과
Pig의 이름은 Pooh입니다.
*/

클래스 안에 정의된 함수와 변수 사용하기

  • 클래스를 사용한다는 것은 사실상 클래스 내부에 정의된 변수와 함수를 사용한다는 것 이다.

  • 생성자를 통해 변수에 저장된 클래스의 인스턴스는 내부에 정의된 변수와 함수를 도트 연산자(.)로 접근할 수 있다.

4) 오브젝트

  • 오브젝트(Object)를 사용하면 클래스 생성자로 인스턴스화 하지 않아도 블록 안의 프로퍼티와 메서드를 호출해서 사용할 수 있다. (Java의 static과 같은 역할)
object Pig{
    var name: String = "Pinky"
    fun printName() {
        Log.d("class", "Pig의 이름은 ${name}입니다.")
    }
}
  • object 코드 블록 안의 프로퍼티와 메서드는 클래스명에 도트 연산자를 붙여서 생성자 없이 직접 호출할 수 있다.
Pig.name = "Mikey"
Pig.printName()
  • 주의할 점은 클래스명을 그대로 사용하기 때문에 호출하는 클래스명의 첫 글자가 대문자이다.

  • object는 클래스와 다르게 앱 전체에 1개만 생성된다.

컴패니언 오브젝트 (companion object)

  • 일반 클래스에 object 기능을 추가하기 위해서 사용한다.

  • 위에서 작성한 Pig 코드를 다음과 같이 companion object 블록으로 감싸주면 생성 과정 없이 오브젝트처럼 사용할 수 있다.

class Pig {
    companion object {
        var name: String = "None"
        fun printName(){
            Log.d("class", "Pig의 이름은 ${name}입니다.")
        }
    }
    fun walk() {
        Log.d("class", "Pig가 걸어갑니다.")
    }
}
  • 위 Pig는 클래스로 선언했기 때문에 일반 함수인 walk()는 생성자인 Pig()를 호출한 다음 변수에 저장한 후에 사용할 수 있다.
// companion object 안의 코드 사용하기
Pig.name = "Linda"
Pig.printName()    // Pig의 이름은 Linda입니다.
// companion object 밖의 코드 사용하기
val cutePig = Pig()
cutePig.walk()    // Pig가 걸어갑니다.

Log 클래스의 메서드 d(), e()가 모두 object 코드 블록 안에 만들어져 있기 때문에 생성자 없이 바로 호출해서 사용할 수 있다.

5) 데이터 클래스

  • Kotlin은 간단한 값의 저장 용도로 데이터 클래스(data class)를 제공한다.

  • 기본 형식은 다음과 같다.

data class 클래스명(val 파라미터1: 타입, var 파라미터2: 타입)
  • 데이터 클래스를 정의할 때 class 앞에 data 키워드를 사용해야 하고, 생성자 파라미터 앞에 입력하는 var(또는 val) 키워드는 생략할 수 없다.

  • 생성하는 코드는 일반 클래스와 동일하게 작성한다.

// 정의 - 주로 코드 블록(클래스 스코프)을 사용하지 않고 간단하게 작성합니다.
data class UserData(val name: String, var age: Int)
// 생성 - 일반 class의 생성자 함수를 호출하는 것과 동일합니다.
var userData = UserData("Michael", 21)

// name은 val로 선언되었기 때문에 변경 불가능합니다.
userData.name = "Sindy" // (X)
// age는 var로 선언되었기 때문에 변경 가능합니다.
userData.age = 18 // (O)
  • 일반 변수 선언처럼 데이터 클래스의 파라미터를 val로 정의하면 읽기 전용이 된다.

toString() 메서드와 copy() 메서드

  • 일반 클래스에서 toString() 메서드를 호출하면 인스턴스의 주소 값을 반환하지만, 데이터 클래스는 값을 반환하기 때문에 실제 값을 모니터링할 때 좋다.
Log.d("DataClass", "DataUser는 ${dataUser.toString()}")
// DataUser는 DataUser(name=Michael, age=21)
  • 또 copy() 메서드로 간단하게 값을 복사할 수 있다.
var newData = dataUser.copy()

일반 클래스처럼 사용하기

  • 일반 클래스와 동일하게 생성자를 호출하면 init 블록이 동작하고 메서드도 사용할 수 있다.
data class UserData(var name: String, var age: Int){
    init{
        Log.d("UserData", "initialized")
    }
    fun process(){
        // 클래스와 동일하게 메서드 사용이 가능합니다.
    }
} // 클래스가 생성되면 "initialized"가 출력됩니다.
  • 이처럼 클래스와 사용법이 동일하지만 주로 네트워크를 통해 데이터를 주고받거나, 혹은 로컬 앱의 데이터베이스에서 데이터를 다루기 위한 용도로 사용하는 것이 데이터 클래스이다.

6) 클래스의 상속과 확장

  • Kotlin은 클래스의 재사용을 위해 상속을 지원한다.

  • 상속은 클래스를 생성한 후 도트 연산자(.)를 통해 메서드와 프로퍼티를 사용하는 것처럼 클래스의 자원을 사용하는 또 다른 방법이다.

  • 상속을 사용하면 부모 클래스의 메서드와 프로퍼티를 마치 내 클래스의 일부처럼 사용할 수 있다.

  • 그러면 상속은 왜 사용할까요?

    • 안드로이드에는 Activity라는 클래스가 미리 만들어져 있으며, 이 Activity 클래스 내부에는 글자를 쓰는 기능, 그림을 그리는 기능, 화면에 새로운 창을 보여주는 기능이 미리 정의되어 있다.

    • 상속이 있기에 이러한 기능을 직접 구현하지 않고 Activity 클래스를 상속받아 약간의 코드만 추가하면 앱에 필요한 기능을 추가할 수 있다.

class Activity {
    fun drawText()
    fun draw()
    fun showWindow()
    // ...
}

class MainACtivity: Activity() {
    fun onCreate(){
        draw("새 그림")    // 미리 만들어진 기능(draw)을 호출만으로 사용할 수 있습니다.
    }
}
  • 상속은 코드를 재사용하는 측면도 있지만 코드를 체계적으로 관리할 수 있기 때문에 규모가 큰 프로텍트도 효과적으로 설계할 수 있다.

클래스의 상속

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

  • 만약 open 키워드로 열려 있지 않으면 상속할 수 없다.

  • 상속을 받을 자식 클래스에서는 콜론(:)을 이용해서 상속할 부모 클래스를 지정한다.

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

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

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

open class 상속될 부모 클래스 {
    // 코드
}

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

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

  • 상속될 부모 클래스의 생성자에 파라미터가 있다면 자식 클래스의 생성자를 통해 값을 전달할 수 있다.
open class 부모 클래스(value: String) {
    // 코드
}

class 자식 클래스(value: String): 부모 클래스(value) {
    // 코드
}
  • 부모 클래스에 세컨더리 생성자가 있다면, 역시 자식 클래스의 세컨더리 생성자에서 super 키워드로 부모 클래스에 전달할 수 있다.

  • 다음은 안드로이드의 View 클래스를 상속받는 예제이다. 자식 클래스에 세컨더리 생성자만 있을 경우 상속되는 클래스 이름 다음에 괄호가 생략된다.

class CustomView: View {    // 부모 클래스명 다음 괄호를 생략했습니다.
    constuctor(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()
    }
}
  • 위 코드에서 Child에는 hello라는 프로퍼티와 sayHello라는 메서드가 없지만 myHello() 메서드를 실행하면 로그에 “Hello!”가 출력된다.

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

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

  • 앞의 예제에서 Parent 클래스의 메서드를 sayHello로, Child 클래스의 메서드를 myHello라 했는데 이런 경우가 오버라이드(Override)가 필요한 대표적인 경우입니다.

  • 오버라이드로 Child 클래스의 메서드도 sayHello라고 하는 것이 의미상 더 적합합니다.

  • 이처럼 동일한 이름의 메서드나 프로퍼티를 사용할 필요가 있을 경우에 override 키워드를 사용해서 재정의할 수 있습니다.

  • 오버라이드할 때는 프로퍼티나 메서드도 클래스처럼 앞에 open을 붙여서 상속할 준비가 되어 있어야 합니다.

메서드 오버라이드

  • 상속할 메서드 앞에 open 키워드를 붙이면 오버라이드할 수 있지만, open 키워드가 없는 메서드는 오버라이드할 수 없다.
open class BaseClass {
    open fun opened() {
    }
    fun notOpened(){
    }
}

class ChildClass: BaseClass(){
    override fun opened(){
    }
    override fun notOpened(){ // notOpened 메서드는 open 키워드가 없으므로 잘못된 사용입니다.
    }
}
  • 클래스의 세컨더리 생성자를 여러 개 중복해서 사용할 수 있는 것도 오버라이딩이 가능하기 때문이다.

프로퍼티 오버라이드

  • 메서드 오버라이드처럼 프로퍼티 역시 open으로 열려 있어야만 오버라이드할 수 있다.
open class BaseClass2 {
    open var opened: String = "I am"
}

class ChildClass2: BaseClass2(){
    override var opened: String = "You are"
}

익스텐션

  • Kotlin은 클래스, 메서드, 프로퍼티에 대해 익스텐션(Extension)을 지원한다.

  • 이미 만들어져 있는 클래스에 다음과 같은 형태로 메서드를 추가할 수 있다.

fun 클래스.확장할 메서드(){
    // 코드
}
  • 상속이 미리 만들어져 있는 클래스를 가져다 쓰는 개념이라면 익스텐션은 미리 만들어져 있는 클래스에 메서드를 넣는 개념이다.

  • 자신이 만든 클래스에 사용하기보다는 누군가 작성해둔, 이미 컴파일되어 있는 클래스에 메서드를 추가하기 위한 용도로 사용하는 것이 좋다.

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

  • 특별한 경우를 제외하고는 거의 메서드 확장 용도로 사용된다.

  • 다음 예제는 기본 클래스인 String에 plus 메서드를 확장하는 전체 코드이다. test 메서드 안에 선언한 original에 문자열을 입력했기 때문에 original은 String의 익스텐션 메서드인 plus를 호출해서 사용할 수 있다.

package net.flow9.thisiskotlin.basicsyntax

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        testStringExtension()
    }

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

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

/** [로그캣 출력 내용]
added를 더한 값은 Hello Guys~입니다.
*/
  • 이어서 클래스의 상속과 확장을 코드 하나로 살펴보겠다. 다음 코드를 실행해보자!
package net.flow9.thisiskotlin.basicsyntax

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 1. 부모 클래스 직접 호출하기
        var parent = Parent()
        parent.sayHello()
        // 1. 자식 클래스 호출해서 사용하기
        var child = Child()
        child.myHello()

        testStringExtension()
    }

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

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

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

// 메서드 오버라이드 연습
open class BaseClass {
    open fun opened() {

    }

    fun notOpend() {

    }
}

class ChildClass : BaseClass() {
    override fun opened() {

    }
//    override fun notOpend(){ // 오버라이드 되지 않고 에러가 발생한다.
//
//    }
}

// 프로퍼티 오버라이드 연습
open class BaseClass2 {
    open var opened: String = "I am"
}

class ChildClass2 : BaseClass2() {
    override var opened: String = "You are"
}

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

/** [로그캣 출력 내용]
안녕하세요.
Hello
added를 더한 값은 Hello Guys~입니다.
*/

7) 설계 도구

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

  • 지금까지는 모두 구현에 중점을 둔 기법을 살펴봤다.

  • 이번에는 프로그래밍 설계에 사용하는 설계 도구에 대해 알아보겠다.

  • 설계 기법은 굉장히 방대한데 그 중 꼭 포함되는 기본 내용 중에 필요한 몇 가지만 알아보겠다.

패키지

  • 패키지(Package)를 구현과 설계 중 어디에 포함할지 고민했는데 컴퓨터 언어에서의 패키지 사용 목적이 설계라고 볼 수 있다고 생각해서 설계 부분에서 다룬다.

  • 코딩하면서 파일을 분류하고, 이름을 짓고, 특정 디렉터리에 모아 놓는 것이 모두 설계이다.

  • 패키지는 클래스와 소스 파일을 관리하기 위한 디렉터리 구조의 저장 공간이다.

  • 다음과 같이 현재 클래스가 어떤 패키지(디렉터리)에 있는지 표시한다.

  • 디렉터리가 계층 구조로 만들어져 있으면 온점(.)으로 구분해서 각 디렉터리를 모두 나열해준다.

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

}

추상화

  • 프로그래밍을 하기 전 개념 설계를 하는 단계에서는 클래스의 이름과 클래스 안에 있음 직한 기능을 유추해서 메서드 이름으로 먼저 나열한다.

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

  • 이것을 추상화(Abstract)라고 하며 abstract 키워드를 사용해서 명시한다.

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

abstract class Design {
    abstract fun drawText()
    abstract fun draw()
    fun showWindow() {
        // code
    }
}

class Implements: Design(){
    fun drawText() {
        // 구현 코드
    }
    fun draw() {
        // 구현 코드
    }
}
  • 다음과 같이 추상화된 Animal 클래스를 만들고 동물이 사용할 것 같은 기능 중에 walk와 move를 설계한다고 가정해보자.
abstract class Animal {
    fun walk(){
        Log.d("abstract", "걷습니다.")
    }
    abstract fun move()
}
  • walk는 명확하게 걸어가는 행위이지만 move는 어떤 동물이냐에 따라서 달라질 수 있다.

  • 예를 들어 새는 날아가겠지만 고래는 수영을 하지않는가?

  • 이렇게 앞으로 상속받을 자식 클래스의 특징에 따라 코드가 결정될 가능성이 있다면 해당 기능도 모두 abstract 키워드로 추상화한다.

  • 그리고 실제 구현 클래스는 이 추상 클래스를 상속받아서 아직 구현되지 않은 추상화되어 있는 기능을 모두 구현해준다.

  • 추상 클래스는 독립적으로 인스턴스화 할 수 없기 때문에 구현 단계가 고려되지 않는다면 잘못된 설계가 될 수 있다.

class Bird: Animal(){
    override fun move(){
        Log.d("abstract", "날아서 이동합니다.")
    }
}
  • 위에서 잠간 안드로이드의 Activity 클래스를 언급했는데, Activity도 수많은 클래스를 상속받아 만들어진다.

  • 이 Activity가 상솓받는 클래스 중에 최상위에 Context라는 클래스가 있는데, 최상위 클래스인 Context가 바로 abstract로 설계되어 있다.

인터페이스

  • 인터페이스(Interface)는 추상화와 비교하면 가장 명확하게 이해할 수 있는데 실행 코드 없이 메서드 이름만 가진 추상 클래스라고 생각해도 된다.

  • 즉, 누군가 설계해 놓은 개념 클래스 중에 실행 코드가 한 줄이라도 있으면 추상화, 코드 없이 메서드 이름만 나열되어 있으면 인터페이스이다.

  • 인터페이스는 상속 관계의 설계보다는 외부 모듈에서 내가 만든 모듈을 사용할 수 있도록 메서드의 이름을 나열해둔 일종의 명세서로 제공된다.

  • 인터페이스는 interface 예약어를 사용해서 정의할 수 있고 인터페이스에 정의된 메서드를 오버라이드해서 구현할 수 있다.

  • Kotlin은 프로퍼티도 인터페이스 내부에 정의할 수 있는데, 대부분의 객체지향 언어에서는 지원하지 않는다.

  • 추상 클래스와 다르게 class 키워드는 사용되지 않는다.

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

인터페이스 만들기

  • interface 예약어로 인터페이스를 정의한다.

  • Kotlin은 인터페이스 내부에 프로퍼티도 정의할 수 있다.

  • 메서드는 코드 블록 없이 이름만 작성해 놓는다.

  • 인터페이스의 프로퍼티와 메서드 앞에는 abstract 키워드가 생략된 형태이다.

interface InterfaceKotlin{
    var variable: String // var 앞에 abstract 키워드가 생략되어 있습니다.
    fun get()
    fun set()
}

클래스에서 구현하기

  • 인터페이스를 클래스에서 구현할 때는 상속돠는 다르게 생성자를 호출하지 않고 인터페이스 이름만 지정해주면 된다.
class KotlinImpl: InterfaceKotlin {
    override var variable: String = "init value"
    override fun get() {
        // code
    }
    override fun set() {
        // code
    }
}
  • 인터페이스를 클래스의 상속 형태가 아닌 소스 코드에서 직접 구현할 때도 있는데, object 키워드를 사용해서 구현해야 한다.

  • 실제로 안드로이드 프로젝트를 시작하면 자주 사용하는 형태이다.

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

여기서 잠깐!

  • 인터페이스는 외부의 다른 모듈을 위한 의사소통 방식을 정의하는 것이다.

  • 혼자 개발하거나 소수의 인원이 하나의 모듈 단위를 개발할 때는 인터페이스를 사용하지 않는 것이 좋다.

  • 인터페이스를 남용하면 코드의 가독성과 구현 효율성이 떨어지기 때문이다.

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

접근 제한자

  • Kotlin에서 정의되는 클래스, 인터페이스, 메서드, 프로퍼티는 모두 접근 제한자(Visibility Modifiers)를 가질 수 있다.

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

  • internal 접근 제한자로 모듈 간에 접근을 제한할 수 있다.

접근 제한자의 종류

  • 접근 제한자는 서로 다른 파일에게 자신에 대한 접근 권한을 제공하는 것인데 각 변수나 클래스 이름 앞에 아무런 예약어를 붙이지 않았을 때는 기본적으로 public 접근 제한자가 적용된다.
접근 제한자제한 범위
private다른 파일에서 접근할 수 없다.
internal같은 모듈에 있는 파일만 접근할 수 있다.
protectedprivate와 같으나 상속 관계에서 자식 클래스가 접근할 수 있다.
public제한 없이 모든 파일에서 접근할 수 있다.

여기서 잠깐!

  • Kotlin에서 모듈이란 한 번에 같이 컴파일되는 모든 파일을 말한다.

  • 안드로이드를 예로 든다면 하나의 앱이 하나의 모듈이 될 수 있다.

  • 또한 라이브러리도 하나의 모듈이다.

접근 제한자의 적용

  • 접근 제한자를 붙이면 해당 클래스, 멤버 프로퍼티 또는 메서드에 대한 사용이 제한된다.

  • 다음 코드를 통해서 접근 제한자가 어떻게 작용하는지 알아보겠다. (다양한 접근 제한자를 갖는 부모 클래스를 하나 생성하자.)

open class Parent {
    private val privateVal = 1
    protected open val protectedVal = 2
    internal val internalVal = 3
    val defaultVal = 4
}
  • 자식 클래스에서 부모 클래스를 상속받고 테스트한다.
class Child: Parent() {
    fun callVariables() {
        // privateVal은 호출이 안 됩니다.   (1)
        Log.d("Modifier", "protected 변수의 값은 ${protectedVal}"). // (2)
        Log.d("Modifier", "internal 변수의 값은 ${internalVal}"). // (3)
        Log.d("Modifier", "기본 제한자 변수 defaultVal의 값은 ${defaultVal}"). // (4)
    }
}

[위 코드 설명]

(1) privateVal은 private 멤버이기 때문에 접근할 수 없다.

(2) protected 멤버 protectedVal은 상속 관계이므로 접근할 수 있다.

(3) internal 멤버 internalVal은 동일한 모듈이므로 접근할 수 있다.

(4) 접근 제한자가 없는 멤버 defaultVal에는 public이 적용되어 접근할 수 있다.

  • 상속 관계가 아닌 외부 클래스에서 Parent 클래스를 생성하고 사용해보자. 상속 관계가 아니기 때문에 public과 internal에만 접근할 수 있다.
class Stranger {
    fun callVariables() {
        val parent = Parent()
        Log.d("Modifier", "internal 변수의 값은 ${parent.internalVal}입니다.")
        Log.d("Modifier", "public 변수의 값은 ${parent.defaultVal}입니다.")
    }
}

제네릭

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

  • 다음은 자주 사용되는 MutableList 클래스의 원본 코드를 이해하기 쉽게 변형한 코드이다.

public interface MutableList<E> {
    var list: Array<E>
    // ...
}
  • 클래스명 옆에 라고 되어 있는 부분에 String과 같은 특정 타입이 지정되면 클래스 내부에 선언된 모든 E에 String이 타입으로 지정된다.

  • 결과적으로 var list: Array가 var list: ARray으로 변경되는 것이다.

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

var list: MutableList<Generic> = mutableListOf("Mon", "Tue", "Wed")
fun testGenerics() {
    // String을 제네릭으로 사용했기 때문에 list 변수에는 문자열만 담을 수 있습니다.
    var list: MutableList<String> = mutableListOf()
    list.add("Mon")
    list.add("Tue")
    list.add("Wed")
    // list.add(35) <- 입력 오류 발생합니다.
    // String 타입의 item 변수로 꺼내서 사용할 수 있습니다.
    for (item in list){
        Log.d("Generic", "list에 입력된 값은 ${item}입니다.")
    }
}
  • 지금까지 배운 내용을 코드로 살펴보겠다!
package net.flow9.thisiskotlin.basicsyntax

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 접근제한자 테스트
        var child = Child()
        child.callVariables()
        // 부모클래스 직접 호출해보기
        var parent = Parent()
        Log.d("Visibility", "Parent : protected 변수의 값은 ${parent.defaultVal}")
        Log.d("Visibility", "Parent : protected 변수의 값은 ${parent.internalVal}")
    }
}

// 추상 클래스 설계
abstract class Animal {
    fun walk() {
        Log.d("abstract", "걷습니다")
    }

    abstract fun move()
}

// 구현
class Bird : Animal() {
    override fun move() {
        Log.d("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() {
        // 코드 구현
    }
}

// 접근제한자 테스트를 위한 부모 클래스
open class Parent {
    private val privateVal = 1
    protected open val protectedVal = 2
    internal val internalVal = 3
    val defaultVal = 4
}

// 자식 클래스
class Child : Parent() {
    fun callVariables() {
        // privateVal은 호출이 안된다
        Log.d("Visibility", "Child : protected 변수의 값은 ${protectedVal}")
        Log.d("Visibility", "Child : internal 변수의 값은 ${internalVal}")
        Log.d("Visibility", "Child : 기본제한자 변수 defaultVal의 값은 ${defaultVal}")
    }
}

/** [로그캣 출력 내용]
Child : protected 변수의 값은 2
Child : internal 변수의 값은 3
Child : 기본제한자 변수 defaultVal의 값은 4
Parent : protected 변수의 값은 4
Parent : protected 변수의 값은 3
*/

[다음 요약 내용을 한 번 이상 읽어본 후에 다음 내용을 학습하자!]

  • 클래스(class): 변수와 함수의 모음으로, 연관성 있는 코드를 그룹화하고 이름을 매긴 것이다.

  • constructor: 클래스를 사용하기 위해서 호출하는 일종의 함수이다.

  • init: 기본 생성자를 호출하면 실행되는 코드 블록이다.

  • 프로퍼티(property): 클래스에 정의된 변수를 프로퍼티 또는 멤버 변수라고 한다.

  • 메서드(method): 클래스에 정의된 함수를 메서드 또는 멤버 함수라고 한다.

  • 컴패니언 오브젝트(comapnion object): 컴패니언 오브젝트의 블록 안에서 변수와 함수를 정의하면 생성자를 통하지 않고 클래스의 멤버들을 사용할 수 있다.

  • 상속: 코드를 재사용하기 위한 설계 도구이다. 상속 관계에서 자식 클래스는 부모 클래스의 멤버들을 자신의 것처럼 사용할 수 있다.

  • 추상화(abstract): 클래스를 개념 설계하기 위한 도구이다.

  • 인터페이스(interface): 외부 모듈에 제공하기 위해 메서드 이름을 나열한 명세서이다.

  • 패키지(package): 연관성 있는 클래스들을 분류하기 위한 디렉터리 구조이다.

  • 접근 제한자: 클래스의 멤버에 지정된 접근 제한자에 따라 외부에서 사용 여부가 결정된다.

  • 제네릭(generic): 타입을 특정해서 안정성을 유지하기 위한 설계 도구이다.


© 2023. All rights reserved.

by SoftyChoo