[GDSC Ewha 5th] Android Session

[GDSC Android] Chapter 3. 클래스와 객체 (Classes and Objects)

wlalsu_u 2023. 11. 4. 14:48

3.0 클래스와 객체 학습 목표

 

 

 

 

Kotlin은 하이브리드 언어로

 

함수와 객체지향을 모두 지원한다.

 

 

 

챕터 3 에서는 클래스와 객체를 중심으로

 

객체 지향에 대해 알아보자.

 

 

 

 

 

1) 클래스 (Classes)
2) 상속 (Inheritance)
3) 확장함수 (Extension functions)
4) 특별한 클래스 (Special Classes)
5) 코드 조직화 (Organizing your code)

 

 

 

 

 

 


 

 

 

 

 

3.1 클래스 (Classes)

 

 

 

 

 

먼저 객체지향의 클래스와 객체의 개념부터 알아보자.

 

 

 

 

 

 

클래스 (Class)

 

 

 

- 클래스는 객체(Object)의 설계도(blueprints)

 

- 클래스는 프로퍼티(properties)와 함수(function)로 구성

 

- 사용자 정의 타입(user-defined type) (<-> 미리 정의된 타입 ex. String, IntRange)

 

 

 

 

 

 

 

예를 들어 우리는 집을 짓기 위해 설계도를 작성할 수 있다.

 

 

이 설계도는 House Class 가 될 것이고,

 

아래와 같은 property와 function을 갖을 수 있다.

 

 

 

 

 

[ 설계도 : House Class ]

1) 데이터 값 ( = 프로퍼티 )

- House color (String 타입)

- Number of windows (Int 타입)

- Is for sale (Boolean 타입)

2) 행동 ( = 함수 )

- updateColor()

- putOnSale()

 

 

 

 

 

 

 

앞서 작성한 설계도를 바탕으로

 

다음과 같은 여러 객체들을 구현할 수 있다. 

 

 

 

 

 

 

[ 객체 : Object Instances ]

1) 객체 1

- House color : "blue"

- Number of windows : 3

- Is for sale : false

2) 객체 2

- House color : "red"

- Number of windows : 3

- Is for sale : true

 

 

 

 

 

 

 

즉 앞서 작성한 설계도와 객체를 코드로 작성해보자.

 

 

 

 

 

 

 

// class의 정의 및 설계

class House {
	// 프로퍼티
    val color: String = "white"
    val numberOfWindows: Int = 2
    val isForSale: Boolean = false
    
    // 함수
    fun updateColor(newColor: String){...}
    fun numberOfWindow(winNum: Int){...}
    fun isForSale(forSale: Boolean){...}
}

 

// 새로운 객체 구현
val myHouse = House()
prtinln(myHouse)

 

 

 

 

 

 

 

위의 객체 구현 함수를 확인해보면

 

House() 라는 생성자를 통해 새로운 객체를 초기화 하는 것을 확인할 수 있다.

 

 

 

 

이때 Kotlin에서는 Java와 달리 'new 키워드'를 사용하지 않음을 주의하자!

 

(Kotlin이 Java보다 간결성을 추구하는 것을 알 수 있음!)

 

 

 

 

 

 

[ 참고 ]

멤버 프로퍼티 : 클래스 안에 있는 프로퍼티

최상위 프로퍼티 : 클래스 밖에 있는 프로퍼티

 

 

 

 

 

 

 

 


 

 

 

 

 

3.2 생성자 (Constructors)

 

 

 

 

 

앞서 클래스의 객체를 구현하기 위해 생성자를 사용하였다.

 

생성자는 어떤 함수인지 자세하게 알아보자.

 

 

 

 

 

 

 

 

생성자 (Constructors)

 

 

 

- 생성자의 역할은 객체를 생성하는 것 

 

- 객체의 초기상태와 관련된 것을 초기화하는 특별한 함수

 

- 여기서 초기상태란 함수가 아닌 프로퍼티만을 의미

 

- 클래스의 헤더안에서 정의 (헤더란 클랫 본문{} 을 제외한 앞 부분)

 

- 특수한 경우가 아니라면 생략 가능

 

 

 

 

 

 

 

 

클래스에서 생성자를 작성하는 예시를 알아보자.

 

 

 

 

 

 

1) 파라미터가 없는 생성자

 

 

 

 

- class 이름 옆의 constructor 키워드 생략 가능 

 

- 단순히 class + (클래스 이름)으로 표현 가능

 

 

 

class A
val aa = A()

 

 

 

 

 

 

 

2) 파라미터가 있는 생성자

 

 

 

 

1) var / val 키워드를 사용하지 않는 경우

 

 

 

- 파라미터 인자는 초기화시에만 '정보'로 사용되고 이후에 접근 불가

 

 

 

 

val bb = B(!2) 로 작성한 12는 초기화 시에 사용되고 없어지므로,

 

아래에서 x 값을 다시 부르는 경우 컴파일 오류가 발생하는 것을 확인할 수 있다.

 

 

 

 

class B(x: Int)

 

 

 

 

 

 

 

2) var / val 키워드를 사용하는 경우

 

 

 

- 프로퍼티를 초기화 한 후 이후에 클래스의 모든 instance에 존재

 

- 생성자의 매개변수가 자동으로 클래스 멤버변수로 추가

 

- 인자로서 프로퍼티 값은 동적으로 받을 수 있음

 

 

 

 

 

val y: Int로 설정하였으므로, 생성시 y를 무조건 초기화하고 시작해야 한다.

 

또한 아래에서 y 값을 다시 불러도 컴파일 오류가 발생하지 않음을 확인할 수 있다.

 

 

 

 

 

class C(val y: Int)
val cc = C(42)
println(cc.y)

 

 

 

 

 

 

 

[ 참고 ] 생성자의 Default parameter


- 클래스 객체가 default 값을 가질 수 있음

- default 파라미터와 required 파라미터 같이 사용 가능

- 오버로딩을 줄여 간결하게 사용할 수 있음

class Box(val length: Int, val width:Int = 20, val height:Int = 40)

val box1 = Box(100, 50, 40)
// width와 height는 default값 사용
val box2 = Box(length = 100)
// named argument 사용하여 가독성 높임
val box3 = Box(lenth = 100, width = 20, height = 40)​

 

 

 

 

 

 

 

 

 

Kotlin의 생성자는 주 생성자와 부 생성자로 구분할 수 있다.

 

이에 대해 하나씩 자세하게 알아보자

 

 

 

 

 

 

 

주생성자 (Primary constructor)

 

 

 

- 클래스 헤더에 주 생성자를 선언

 

- initializer block에 주 생성자 본문을 작성

 

 

 

 

 

 

 

일반적으로 다른 언어에서

 

주 생성자 코드는 아래의 코드와 같은 형식일 것이다.

 

(코틀린에서도 아래와 같이 선언해도 된다)

 

 

 

 

 

class Circle {
	constructor(i: Int){
    }
}

 

 

 

 

 

 

하지만 코틀린에서는 아래의 예시처럼

 

클래스 헤더에 주 생성자를 (i: Int) 를 선언하여 더욱 간편하게 표기할 수 있다.

 

 

 

 

 

class Circle(i: Int){
	init {
    }
}

 

 

 

 

 

 

 

이때 클래스 헤더에는 실행 코드를 작성할 수 없으므로,

 

init 블록 안에 주 생성자의 본문을 작성하게 된다.

 

 

 

 

 

 

[ 추가 ] init 블록

- 주 생성자의 본문을 나타내는 코드

- 여러개의 init 블록을 작성할 수 있음

 

 

 

 

 

 

 

주 생성자를 선언하고, init 블록을 작성한 예시를 살펴보자.

 

 

 

 

 

 

package ClassNObject

class Square(val side: Int) {
    init {
        println(side * 2)
    }
}

fun main() {
    val s = Square(10)
}

 

 

 

 

 

 

 

 

 

 

 

 

부생성자 ( Secondary constructor )

 

 

 

 

- multiple constructors

 

- 같은 클래스에 속한 객체를 만드는 여러가지 방법을 원하는 경우 (생성자 오버로드)

 

- 부생성자를 정의하기 위해 constructor 키워드 사용

 

- 부생성자는 this 키워드를 사용하여 주생성자를 무조건 호출

 

- 코드 경로가 많아지면 테스팅 이슈가 늘어나므로 무작위하게 사용하지 말 것

 

- 생성자 본문이 없어도 됨

 

 

 

 

 

 

 

 

아래의 코드를 통해 부 생성자를 자세하게 이해해보자.

 

 

 

 

 

 

package ClassNObject

fun main() {
    class Circle(val radius: Double) {
        constructor(name: String) : this(1.0)
        constructor(diameter: Int) : this(diameter/2.0){
            println("in diameter constructor")
        }
        init {
            println("Radius: $radius")
            println("Area: ${Math.PI * radius * radius}")
        }
    }

    val c = Circle(3)
}

 

 

 

 

 

 

 

 

먼저 val c = Circle(3) 을 통해 Circle 클래스가 호출되고 있다.

 

이때 3은 Int 타입이므로 두번째 부 생성자가 호출된다.

 

 

 

 

부 생성자에서 this 키워드로 주 생성자를 호출하므로,

 

주 생성자의 본문인 init 코드가 실행되는데,

 

이때 두번째 부생성자는 diameter/2 연산을 수행하게 된다.

 

 

 

 

마지막으로 부 생성자 본문을 모두 수행하고 나면

 

부생성자의 "in diameter constructor" 코드가 출력된다.

 

 

 

 

 

실제 아래 실행 결과를 확인해보면

 

주생성자 본문이 실행된 후, 두번째 부생성자 코드가 출력된 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


 

 

 

 

 

3.3 프로퍼티와 멤버함수 (Properties and Member functions)

 

 

 

 

 

클래스는 프로퍼티와 멤버 함수로 구성되어 있다.

 

각각에 대해 하나씩 자세하게 알아보자.

 

 

 

 

 

 

프로퍼티 ( Properties )

 

 

 

 

- 프로퍼티는 객체 상태 유지를 위해 정의됨

 

- val 또는 var 키워드를 사용하여 프로퍼티 정의 (선언시 get, set 자동 생성)

 

- get() / set() : dot ( . ) 과 프로퍼티 이름을 사용하여 프로퍼티 사용

 

- set() 의 경우 var(가변) 인 경우만 사용 가능 (val로 선언시 컴파일 에러)

 

 

 

 

 

 

 

 

Person 클래스에서 name 이라는 프로퍼티를 정의하고 사용하는 예시를 살펴보자.

 

 

 

 

 

 

 

 

package ClassNObject

class Person(var name: String)

fun main() {
    val person = Person("Alex")
    println(person.name)
    person.name = "Joey"
    println(person.name)
}

 

 

 

 

 

 

 

 

String 타입의 name 이라는 프로퍼티가 선언된 것을 확인할 수 있는데,

 

이때 dot(.)<property name> 인 person.name으로 프로퍼티에 접근하고 있다.

 

 

 

또한 프로퍼티를 var (가변)으로 선언하여

 

person.name 을 Joey로 set 하는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

[ 참고 ] 프로퍼티 vs 파라미터

- 프로퍼티 : val / var 키워드로 정의하며 계속해서 사용 가능

- 파라미터 : val / var 키워드를 사용할 수 없으며, 한 번 쓰고 사라짐

 

 

 

 

 

 

 

 

 

앞서 val / var을 선언하면

 

get() / set() 이 자동으로 생성된다고 하였는데,

 

기본 동작을 하는 경우가 아닌 custom getter setter 는 어떻게 만들 수 있을까?

 

 

 

 

 

 

 

[ Custom getters and setters ]


- 기본 get / set 동작을 사용하고 싶지 않을 경우 사용

- get() / set() 을 오버라이딩하여 명시적으로 적음

- set() 의 경우 프로퍼티가 var로 선언된 경우만 가능

- 오버라이드 키워드는 사용하지 않음


var propertyName: DataType = initialValue
	get() = ...
    ser(value) {
    	...
    }

 

 

 

 

 

 

 

 

 

커스텀 getter 를 사용하는 방법을 아래의 예시를 통해 알아보자

 

 

 

 

 

 

 

 

package ClassNObject

fun main() {
    class Person(val firstName: String, val lastName: String) {
        val fullName: String
            get() {
                return "$firstName $lastName"
            }
    }

    val person = Person("John", "Doe")
    println(person.fullName)
}

 

 

 

 

 

 

 

 

person.fullName 으로 get() 함수를 호출할 때

 

앞서 작성한 커스텀 get 의 리턴값에 해당하는 값을 출력하는 것을 확인할 수 있다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

다음으로 커스텀 setter 를 사용하는 방법을 아래의 예시를 통해 알아보자

 

 

 

 

 

 

 

 

package ClassNObject

fun main() {
    class Person(var firstName: String, var lastName: String) {
        var fullName: String = ""
            get() = "$firstName $lastName"
            set(value) {
                val components = value.split(" ")
                firstName = components[0]
                lastName = components[1]
                field = value
            }
    }
    var person = Person("John", "Doe")
    person.fullName = "Jane Smith"
    print(person.fullName)
}

 

 

 

 

 

 

 

 

person.fullName 을 Jane Smith 로 set 할때

 

커스텀 set 함수에서 작성한 대로 띄어쓰기를 기준으로 compoennt에 나누어 담은 후

 

이를 출력하는 것을 확인할 수 있다.

 

(이때 가변일때만 set 함수 사용이 가능하므로 앞선 예제에서 var로 고치는 것을 잊지 말자!)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

멤버 함수 ( Member Functions )

 

 

 

 

- fun 키워드를 사용

 

- default / required 파라미터를 가질 수 있음

 

- Unit 이 아닌 경우 특정 리턴타입을 무조건 반환

 

 

 

 

 

 

https://wlalsu.tistory.com/115

 

[GDSC Android] Chapter 2. Functions 함수

2.0 Kotlin Functions 학습 목표 Kotlin은 하이브리드 언어로 함수형과 객체지향을 모두 지원한다. 이번 챕터에서는 함수에 대해 먼저 알아보자. 1) Kotlin 프로그래밍의 기본 2) (Almost) Everything has a value 3)

wlalsu.tistory.com

 

 

 

 

 

 

 

 

 


 

 

 

 

 

 

3.4 상속 (Inheritance)

 

 

 

 

 

상속은 일반적으로 기존 크래스를 확장한 새로운 클래스를 말한다.

 

즉 처음부터 클래스를 작성하는 것이 아닌 일종의 재사용 매커니즘이라고 할 수 있다.

 

 

 

오버라이드와 멤버/프로퍼티 함수를 추가하여 확장할 수 있는데,

 

상속의 개념과 확장 방법에 대해 자세하게 알아보자.

 

 

 

 

 

 

 

상속 ( Inheritance )

 

 

 

 

- 코틀린은 단일 부모 클래스 상속을 가짐

 

- 각 클래스는 하나의 superclass 부모만 가짐

 

- 각 subclass는 superclass의 모든 멤버를 상속받음

 

 

 

 

 

 

 

[ 추가 ] 단일 상속이 좋은 이유는 무엇일까?

- 코틀린에서는 단일 상속만 가능하지만, C++과 같은 다른 객체 지향 언어에서는 다중 상속이 가능

- 다중상속은 다이아몬드 상속 문제를 발생




예를 들어 Son 이라는 클래스가 FatherA와 FatherB를 상속받는다고 가정하자.

이때 FatherA와 FatherB 모두 GrandFather 이라는 클래스를 상속받으므로, 같은 메서드가 존재할 수 있다.

이때 Son은 FatherA와 FatherB중 어디에서 해당 메소드를 가져와야 할지 정할 수 없게 된다.

따라서 다중상속은 이러한 논리적 모호함이나 충돌을 야기할 수 있다.

 

 

[ 참고 ] 좁은 의미의 상속과 넓은 의미의 상속

1) 확장 (extends 키워드)

- 단일 확장 : (일반/추상) 클래스를 클래스가 확장
- 다중 확장 : 인터페이스를 인터페이스가 확장

2) 구현 (implements 키워드)

- 다중구현 : 클래스가 인터페이스를 구현

3) 좁은 의미의 상속

- 클래스를 클래스가 확장하는 단일 상속 확장만 의미

4) 넓은 의미의 상속

- 확장과 구현 개념을 모두 포함

 

 

 

[ 추가 ] 상속의 대상

아래의 3가지 상속의 대상은 가장 추상적인 것 부터 구체적인 것으로 나열하였다.

1) 인터페이스 : 완전 추상이므로 모든 멤버가 abstract

2) 추상클래스 : 부분 추상, 부분 구체이므로 멤버 중 하나 이상이 abstract

3) (일반) 클래스 : 완전 구체이며, 객체를 생성할 수 있음

 

 

 

 

 

 

 

 

 

 

위에서 언급한 것처럼

 

상속의 대상이 되는 인터페이스 / 추상클래스 / 클래스 확장에 대해 하나씩 자세하게 알아보자.

 

 

 

 

 

 

 

 

 

인터페이스 ( Interfaces )

 

 

 

 

- 완전 추상

 

- 프로퍼티와 메소드에 대해 구현하지 않고 선언만 한 것

 

- 인터페이스는 클래스가 모두 오버라이드해서 명시적으로 구현해야 함

 

- 인터페이스는 다른 인터페이스가 확장할 수 있음

 

 

 

 

 

interface NameOfInterface { interfaceBody }

 

 

 

 

 

 

 

 

아래의 예시를 통해 인터페이스에 대해 자세하게 알아보자.

 

 

 

 

 

 

package ClassNObject

interface Shape {
    fun computeArea() : Double
}

class Circle(val radius: Double) : Shape {
    override fun computeArea() = Math.PI * radius * radius
}

fun main() {
    val c = Circle(3.0)
    println(c.computeArea())
}

 

 

 

 

 

 

 

Shape 이라는 인터페이스를 선언한 후,

 

Circle 이라는 클래스에서 이를 명시적으로 구현한 것을 확인할 수 있다.

 

 

 

Circle 클래스에서는 computeArea() 메소드를 오버라이딩하였으므로,

 

실제 결과값을 확인해보면 아래와 같이 원의 넓이가 출력된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

클래스 확장 ( Extending classes )

 

 

 

 

 

기존 클래스를 확장하기 위해서는 아래의 2가지 방법을 사용할 수 있다.

 

 

 

 

1) subclass : 기존 class를 사용하여 새로운 클래스를 생성 

 

 

- 코틀린 클래스들은 기본적으로 open 되어 있지 않음

 

- 따라서 상속이 불가능

 

(interface / abstract class는 기본적으로 열려 있음)

 

- subclass를 만들기 위해서는 open 키워드 사용

 

- 프로퍼티나 함수를 재정의 하고 싶은 경우 override 키워드 사용

 

 

 

 

class A

 

 

 

 

다음과 같은 클래스가 있다고 가정해보자.

 

 

앞서 언급했듯이 코틀린은 기본적으로 모든 클래스가 닫혀 있으므로,

 

해당 클래스는 open 되지 않은 상태일 것이다.

 

 

 

 

class B : A

 

 

 

 

따라서 위의 코드와 같이 상속을 받으려고 하면 오류가 발생한다.

 

 

 

 

open class C
class D : C()

 

 

 

 

subclass를 생성하기 위해서는 위의 코드와 같이

 

open 키워드를 사용하여 클래스를 열어준 후 상속한다.

 

 

 

 

 

 

2) 확장함수 : 기존에 정의된 클래스에 함수를 추가

 

 

 

 

 

 

[ 추가 ] 오버라이딩 (Overriding)

- 오버라이딩 하기 위해서는 프로퍼티와 함수 앞에 open 키워드를 붙여야 함

- 프로퍼티나 함수를 오버라이딩 할 때는 override 키워드를 붙여야 함

- 이미 override 된 것도 다른 subclass 에서 다시 override 될 수 있음 (final 키워드가 붙었다면 불가능)

 

 

 

 

 

 

 

 

 

 

 

추상 클래스 ( Abstract classes )

 

 

 

 

- abstract 키워드를 사용한 클래스

 

- 부분 추상(= 부분 구체) 이므로 객체를 생성할 수 없음

 

- abstract 키워드를 갖고 있는 프로퍼티나 함수는 오버라이딩 되어야 함

 

 

 

 

 

package ClassNObject

abstract class Food {
    abstract val kcal: Int
    abstract val name: String
    fun consume() = println("I'm eating ${name}")
}

class Pizza(): Food() {
    override val kcal = 600
    override val name = "Pizza"
}

fun main() {
    Pizza().consume()
}

 

 

 

 

 

 

 

Food 라는 추상 클래스는 kcal / name 이라는 추상 프로퍼티와

 

consume 이라는 구현 함수를 갖고 있다.

 

 

 

Pizza 클래스에서 kcal / name 프로퍼티를 오버라이딩 하여 명시적으로 구현하고 있고,

 

consume 함수의 경우 추상함수가 아니므로, 오버라이딩하지 않았다.

 

(선택적이므로 오버라이딩 할 수도 있음)

 

 

 

 

 

 

실행 결과를 확인해보면 아래와 같다.

 

 

 

 

 

 

 

 

 

 

 

[ 참고 ] 인터페이스 / 추상클래스 / (일반)클래스는 각각 언제 사용할까?

1) 인터페이스 : 함수나 프로퍼티를 보다 넓은 스펙트럼으로 정의하고 싶을 때

2) 추상클래스 : 일부 프로퍼티와 클래스만 추상적으로 정의하고 싶을 때

3) (일반) 클래스 : 함수나 프로퍼티를 특정한 타입으로 구현하고 싶을 때

* 오직 하나의 클래스만 extends 할 수 있지만, 여러 개의 인터페이스를 implemtents 할 수 있음

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'GDSC Android 교육 세션 자료 및 강의 내용' 과 'Codelab 3강 : Classes and objects' 내용을 기반으로 작성하였습니다.