5. 클래스와 객체
1. 클래스와 객체의 정의
객체지향 프로그래밍(OOP: Object-Oriented Programming) 은 프로그램의 구조를 객체 간 상호작용으로서 표현하는 프로그래밍 방식이다. 객체 지향 기법으로 프로그램을 설계할 때 알아 두어야할 개념은 다음과 같다.
- 추상화(Abstraction): 특정 클래스를 만들 때 기본 형식을 규정하는 방법
- 인스턴스(Instance): 클래스로부터 생성한 객체
- 상속(Inheritance): 부모 클래스의 내용을 자식 클래스가 그대로 물려받음
- 다형성(Polymorphism): 하나의 이름으로 다양한 처리를 제공
- 캡슐화(Encapsulation): 내용을 숨기고 필요한 부분만 사용
- 메시지 전송(Message Sending): 객체 간에 주고받는 메시지
- 연관(Association): 클래스 간의 관계
객체 지향 프로그래밍의 용어
코틀린에서 사용하는 용어 | 다른 언어에서 사용하는 용어 |
---|---|
클래스(Class) | 분류, 범주 |
프로퍼티(Property) | 속성(Attriibute), 변수(Variable), 필드(Field), 데이터(Data) |
메서드(Method) | 함수(Function), 동작(Operation), 행동(Behavior) |
객체(Object) | 인스턴스(Instance) |
클래스 다이어그램(Class Diagram): 클래스의 정의와 관계를 나타내는 다이어그램
/* 구조 */
class 클래스 이름 {
// 프로퍼티
// 매서드
}
/* Bird 클래스 정의 */
class Bird {
// 프로퍼티(속성)는 반드시 초기화되어야 한다.
var name: String = "mybird"
var wing: Int = 2
var beak: String = "short"
var color: String = "blue"
// 메서드(함수)는 함수를 선언하는 방법과 동일하다.
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
fun main() {
val coco = Bird() // 객체 생성
coco.color = "red"
println("coco.color: ${coco.color}")
coco.fly()
coco.sing(3)
}
NOTE✏️
- 통합 모델링 언어(UML: Unified Modeling Language): 객체 지향 프로그래밍 소프트웨어 집약 시스템을 개발할 때 산출물을 명세화, 시각화, 문서화할 때 사용한다. Link
2. 생성자
생성자(Constructor) 란 클래스를 통해 객체가 만들어질 때 기본적으로 호출되는 함수를 말한다.
- 부 생성자(Secondary Constructor): 클래스의 본문에 함수처럼 선언한다.
class Bird {
// Property
var name: String
var wing: Int
var beak: String
var color: String
// Secondary Constructor
constructor(name: String, wing: Int, _beak: String, _color: String) {
this.name = name // this 키워드는 객체 자신에 대한 참조
this.wing = wing
beak = _beak // this 키워드 대신 언더스코어(_) 이용 가능
color = _color
}
// 두 번째 부 생성자
constructor(_name: String, _beak: String) {
name = _name
wing = 2
beak = _beak
color = "grey"
}
// Method
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
fun main() {
val coco = Bird("coco", 2, "short", "red") // 첫 번째 부 생성자 호출
val dove = Bird("dove", "short") // 두 번째 부 생성자 호출
println("coco.color: ${coco.color}")
coco.fly()
coco.sing(3)
}
- 주 생성자(Primary Constructor): 클래스 이름과 함께 생성자를 정의한다. 초기화에 꼭 사용할 코드가 있다면 초기화 블록을 선언해야 한다.
// Primary Constructor: constructor 키워드 생략 가능, 프로퍼티 포함 가능, 기본값 지정 가능
class Bird constructor(var name: String = "NONAME", var wing: Int = 2, var beak: String, var color: String) {
// Property 주 생성자에 포함시킴
// Initialization Block
init {
println("---------- 초기화 블록 ----------")
println("이름: $name, 부리: $beak")
this.sing(3)
println("----------------------------------")
}
// Method
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
fun main() {
val coco = Bird(beak = "long", color = "red")
println("coco.color: ${coco.color}")
coco.fly()
coco.sing(3)
}
3. 상속과 다형성
- 상속(Inheritance): 상위 클래스(부모 클래스)의 속성과 기능을 물려받아 계승하는 것이다. 코틀린에서 open 키워드 없이 선언된 클래스는 최종 클래스로 상속이 불가능하다. Link
/* 구조 */
open class 기반 클래스 이름 { // 묵시적으로 Any 클래스로부터 상속됨
...
}
class 파생 클래스 이름 : 기반 클래스 이름() { // 기반 클래스로부터 상속됨
...
}
/* Bird 클래스 상속 */
open class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
// Method
fun fly() = println("Fly wing: $wing")
fun sing(vol: Int) = println("Sing vol: $vol")
}
// 주 생성자를 사용한 상속
class Lark(name: String, wing: Int, beak: String, color: String) : Bird(name, wing, beak, color) {
// Method
fun singHitone() = println("Happy Song!")
}
// 부 생성자를 사용한 상속
class Parrot : Bird {
// Property
val language: String
// Secondary Constructor
constructor(name: String,
wing: Int,
beak: String,
color: String,
language: String) : super(name, wing, beak, color) {
this.language = language
}
// Method
fun speak() = println("Speak! $language")
}
fun main() {
val lark = Lark("lark", 2, "long", "brown")
val parrot = Parrot("parrot", 2, "short", "multiple", "korean")
lark.singHitone()
lark.fly()
parrot.speak()
parrot.sing(4)
}
다형성(Polymorphism): 메서드가 같은 이름을 사용하지만 구현 내용이 다르거나 매개변수가 달라서 하나의 이름으로 다양한 기능을 수행할 수 있는 개념이다. Link
- 오버로딩(Overloading): 동일한 클래스 안에서 같은 이름의 메서드가 매개변수만 달리하여 여러 번 정의 될 수 있는 개념
- 오버라이딩(Overriding): 상위 클래스의 메서드의 이름, 매개변수, 반환값은 동일하나 내용을 새롭게 재정의한다.
/* add 메서드 오버로딩 */
class Calculator {
fun add(x: Int, y: Int): Int = x + y
fun add(x: Double, y: Double): Double = x + y
fun add(x: String, y: String): String = x + y
}
fun main() {
val calculator = Calculator()
println(calculator.add(3, 2)) // 5
println(calculator.add(3.14, 0.4)) // 3.54
println(calculator.add("Hello ", "Kotlin!")) // Hello Kotlin!
}
/* 메서드 오버라이딩 */
open class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
// Method
fun fly() = println("Fly wing: $wing")
open fun sing(vol: Int) = println("Sing vol: $vol") // open 키워드를 사용하여 오버라이딩 허용
}
class Parrot(name: String,
wing: Int = 2,
beak: String,
color: String,
var language: String = "natural") : Bird(name, wing, beak, color) {
// Method
fun speak() = println("Speak! $language")
override fun sing(vol: Int) { // override 키워드를 사용하여 메서드 오버라이딩
println("I'm a parrot! The volume level is $vol")
speak()
}
}
open class Lark(name: String, wing: Int, beak: String, color: String) : Bird(name, wing, beak, color) {
// Method
fun singHitone() = println("Happy Song!")
final override fun sing(vol: Int) { // final override 키워드로 하위 클래스에서 재정의를 막음
println("Sing vol: $vol")
singHitone()
}
}
4. super와 this의 참조
클래스의 설계에서 상위와 현재 클래스의 특정 메서드나 프로퍼티, 생성자를 참조해야 하는 경우가 생긴다. 상위 클래스는 super 키워드로, 현재 클래스는 this 키워드로 참조가 가능하다.
참조 대상 | 상위 클래스 | 현재 클래스 |
---|---|---|
프로퍼티 | super.프로퍼티 이름 | this.프로퍼티 이름 |
메서드 | super.메서드 이름() | this.메서드 이름() |
생성자 | super() | this() |
- 상위 클래스의 메서드에 필요한 내용만 추가하기
open class Bird(var name: String, var wing: Int, var beak: String, var color: String) {
// Method
fun fly() = println("Fly wing: $wing")
open fun sing(vol: Int) = println("Sing vol: $vol")
}
class Parrot(name: String, wing: Int = 2, beak: String, color: String,
var language: String = "natural") : Bird(name, wing, beak, color) {
// Method
fun speak() = println("Speak! $language")
override fun sing(vol: Int) {
super.sing(vol) // 상위 클래스의 sing()을 먼저 수행
println("I'm a parrot! The volume level is $vol")
speak()
}
}
- this와 super를 사용하는 부 생성자
open class Person {
constructor(firstName: String) {
println("[Person] firstName: $firstName")
}
constructor(firstName: String, age: Int) { // ③
println("[Person] firstName: $firstName, age: $age") // ④
}
}
class Developer: Person {
constructor(firstName: String): this(firstName, 10) { // ①
println("[Developer] firstName: $firstName") // ⑥
}
constructor(firstName: String, age: Int): super(firstName, age) { // ②
println("[Developer] firstName: $firstName, age: $age") // ⑤
}
}
fun main() {
val sean = Developer("Sean")
}
- 주 생성자와 부 생성자 함께 사용하기
// ② 주 생성자
class Person(firstName: String, out: Unit = println("[Primary Constructor] Parameter")) {
val fName = println("[Property] Person fName: $firstName") // ③ 프로퍼티 할당
init { // ④ 초기화 블록
println("[init] Person init block")
}
// ① 부 생성자
constructor(firstName: String, age: Int,
out: Unit = println("[Secondary Constructor] Parameter")): this(firstName) {
println("[Secondary Constructor] Body: $firstName, $age") // ⑤ 부 생성자 본문
}
}
fun main() {
val person1 = Person("Kim", 30) // 실행 순서: ① -> ② -> ③ -> ④ -> ⑤
println()
val person2 = Person("Lee") // 실행 순서: ② -> ③ -> ④
}
- 인터페이스에서 참조하기: 중복된 이름의 프로퍼티나 메서드가 존재할 때, 앵글 브래킷(< >)을 사용하여을 사용하여 충돌을 방지한다.
open class A {
open fun f() = println("A Class f()")
fun a() = println("A Class a()")
}
interface B { // 인터페이스는 기본적으로 오버라이딩을 허용
fun f() = println("B Interface f()")
fun b() = println("B Interface b()")
}
class C : A(), B { // 콤마(,)를 사용해 클래스와 인터페이스를 상속
override fun f() = println("C Class f()")
override fun b() = println("C Class b()")
fun test() {
b() // 현재 클래스의 b()
super<B>.b() // 인터페이스 B의 b()
f() // 현재 클래스의 f()
super<A>.f() // 클래스 A의 f()
super<B>.f() // 인터페이스 B의 f()
}
}
fun main() {
val c = C()
c.test()
}
NOTE✏️
- 인터페이스(Interface): 클래스들이 구현해야 하는 동작을 지정하는데 사용되는 추상 자료형이다. Link
5. 정보 은닉 캡슐화
캡슐화(Encapsulation) 란 객체의 속성(Property)과 행위(Method)를 하나로 묶고, 실제 구현 내용 일부를 외부에 감추어 은닉하는 것이다. Link
가시성 지시자(Visibility Modifier)
- private: 외부에서 접근할 수 없다.
- protected: 외부에서 접근할 수 없으나 하위 상속 요소에서는 가능하다.
- internal: 같은 정의의 모듈 내부에서는 접근이 가능하다.
- public: 어디서든 접근이 가능하다.(기본값)
open class Base {
// 이 클래스에서는 a, b, c, d, e 접근 가능
private val a = 1
protected open val b = 2
internal val c = 3
val d = 4 // 가시성 지시자의 기본값은 public
protected class Nested {
// 이 클래스에서는 a, b, c, d, e, f 접근 가능
public val e: Int = 5 // public 생략 가능
private val f: Int = 6
}
}
class Derived : Base() {
// 이 클래스에서는 b, c, d, e 접근 가능
// a는 접근 불가
override val b = 0 // Base의 b는 오버라이딩 됨 - 상위와 같은 protected 지시자
}
class Other(base: Base) {
// base.a, base.b는 접근 불가
// base.c, base.d는 접근 가능(같은 모듈안에 있으므로)
// Base.Nested는 접근 불가, Nested::e 역시 접근 불가
}
/* 자동차와 도둑 예제 */
open class Car protected constructor(_year: Int, _model: String, _power: String, _wheel: String) {
// Property
private var year: Int = _year
public var model: String = _model
protected open var power: String = _power
internal var wheel: String = _wheel
// Method
protected fun start(key: Boolean) {
if (key) println("Start the Engine!")
}
// Nested Class
class Driver(_name: String, _license: String) {
private var name: String = _name
var license: String = _license // public
internal fun driving() = println("[Driver] Driving - $name")
}
}
class Tico(_year: Int, _model: String, _power: String, _wheel: String,
var name: String, private var key: Boolean) : Car(_year, _model, _power, _wheel) {
// Property
override var power: String = "50hp"
val driver = Driver(name, "first class")
// Secondary Constructor
constructor(_name: String, _key: Boolean) : this(2014, "basic", "100hp", "normal", _name, _key) {
name = _name
key = _key
}
// Method
fun access(password: String) {
if (password == "gotico") {
println("---- [Tico] access() ----")
// super.year // private 접근 불가
println("super.model = ${super.model}") // public 접근
println("super.power = ${super.power}") // protected 접근
println("super.wheel = ${super.wheel}") // internal 접근
super.start(key) // protected
// driver.name // private 접근 불가
println("Driver().license = ${driver.license}")
driver.driving() // internal
} else {
println("You're a burglar")
}
}
}
class Burglar() {
// Method
fun steal(anycar: Any) {
if (anycar is Tico) {
println("---- [Burglar] steal() ----")
// println(anycar.power) // protected 접근 불가
// println(anycar.year) // private 접근 불가
println("anycar.name = ${anycar.name}") // public 접근
println("anycar.wheel = ${anycar.wheel}") // internal 접근(같은 모듈 안에 있으므로)
println("anycar.model = ${anycar.model}") // public 접근
println(anycar.driver.license) // public 접근
anycar.driver.driving() // internal 접근(같은 모듈 안에 있으므로)
// println(Car.start()) // protected 접근 불가
anycar.access("dontknowo")
} else {
println("Nothing to steal")
}
}
}
fun main() {
// val car = Car() // protected 생성 불가
val tico = Tico("Sungje", true)
tico.access("gotico")
val burglar = Burglar()
burglar.steal(tico)
}
6. 클래스와 클래스의 관계
- 연관(Association): 2개의 서로 분리된 클래스가 연결을 가지는 관계다. 단방향 혹은 양방향으로 연결되며 두 요소가 서로 다른 생명주기를 가지고 있다.
/* 연관 관계 나타내기 */
class Patient(val name: String) {
fun doctorList(doctor: Doctor) { // 인자로 참조
println("Patient: $name, Doctor: ${doctor.name}")
}
}
class Doctor(val name: String) {
fun patienList(patient: Patient) { // 인자로 참조
println("Doctor: $name, Patient: ${patient.name}")
}
}
fun main() {
val doctor1 = Doctor("KimDoctor") // 객체는 서로 독립적으로 생성됨
val patient1 = Patient("LeePatient")
doctor1.patienList(patient1)
patient1.doctorList(doctor1)
}
- 의존(Dependency): 한 클래스가 다른 클래스에 의존되어 있어 영향을 주는 관계다.
/* 의존 관계 나타내기 */
class Patient(val name: String, var id: Int) {
fun doctorList(doctor: Doctor) { // 인자로 참조
println("Patient: $name, Doctor: ${doctor.name}")
}
}
class Doctor(val name: String, val patient: Patient) {
val customerId: Int = patient.id
fun patienList() {
println("Doctor: $name, Patient: ${patient.name}")
println("Patient Id: $customerId")
}
}
fun main() {
val patient1 = Patient("LeePatient", 1)
val doctor1 = Doctor("KimDoctor", patient1) // Patient 객체가 필요함
doctor1.patienList()
}
- 집합(Aggregation): 연관 관계와 거의 동일하나 특정 객체를 소유한다는 개념이 추가된 관계다.
/* 집합 관계 나타내기 */
class Pond(_name: String, _members: MutableList<Duck>) {
val name: String = _name
val members: MutableList<Duck> = _members
constructor(_name: String): this(_name, mutableListOf<Duck>())
}
class Duck(val name: String)
fun main() {
// 두 객체는 서로 생명주기에 영향을 주지 않음
val pond = Pond("myFavorite")
val duck1 = Duck("Duck1")
val duck2 = Duck("Duck2")
// 연못에 오리를 추가 - 연못에 오리가 집합
pond.members.add(duck1)
pond.members.add(duck2)
// 연못에 있는 오리들
for (duck in pond.members) {
println(duck.name)
}
}
- 구성(Composition): 집합관계와 거의 동일하지만 특정 클래스가 어느 한 클래스의 부분이 되어 생명주기가 소유자 클래스에 의존되는 관계다.
/* 구성 관계 나타내기 */
class Car(val name: String, val power: String) {
private var engin = Engine(power) // Engine 클래스 객체는 Car에 의존적
fun startEngine() = engin.start()
fun stopEngine() = engin.stop()
}
class Engine(power: String) {
fun start() = println("Engin has been started.")
fun stop() = println("Engin has been stoped.")
}
fun main() {
val car = Car("tico", "100hp")
car.startEngine()
car.stopEngine()
}
'Program Language > Kotlin' 카테고리의 다른 글
[Do it! 코틀린 프로그래밍] 7. 다양한 클래스와 인터페이스 (0) | 2021.04.19 |
---|---|
[Do it! 코틀린 프로그래밍] 6. 프로퍼티와 초기화 (0) | 2021.04.14 |
[Do it! 코틀린 프로그래밍] 4. 프로그램의 흐름 제어 (0) | 2021.04.05 |
[Do it! 코틀린 프로그래밍] 3. 함수와 함수형 프로그래밍 (0) | 2021.04.03 |
[Do it! 코틀린 프로그래밍] 2. 변수와 자료형, 연산자 (0) | 2021.03.30 |