super와 this의 참조
상위 클래스는 super 키워드로, 현재 클래스는 this 키워드로 참조가 가능합니다.
super | this |
super.프로퍼티 이름 // 상위 클래스의 프로퍼티 참조 super.메서드 이름() // 상위 클래스의 메서드 참조 super() // 상위 클래스의 생성자 참조 |
this.프로퍼티 이름 // 현재 클래스의 프로퍼티 참조 this.메서드 이름() // 현재 클래스의 메서드 참조 this() // 현재 클래스의 생성자 참조 |
fun main() {
// this로 현재 객체 참조
val sean = Developer("Sean")
}
/*
결과 :
[Person] firstName: Sean, 10
[Developer] firstNam: Sean, 10
[Developer] firstNam: Sean
*/
open class Person {
constructor(firstName: String) {
println("[Person] firstName : $firstName")
}
constructor(firstName: String, age: Int) { // 호출 순서 : 3, 실행 순서 : 1
println("[Person] firstName: $firstName, $age")
}
}
class Developer: Person {
constructor(firstName: String) : this(firstName, 10) { // 호출 순서 : 1, 실행 순서 : 3
println("[Developer] firstNam: $firstName")
}
constructor(firstName: String, age: Int) : super(firstName, age) { // 호출 순서 : 2, 실행 순서 : 2
println("[Developer] firstNam: $firstName, $age")
}
}
main() 함수에서 Developer("Sean")을 통해 객체를 생성하고, 인자가 1개이므로 호출 순서 1번의 생성자로 진입합니다. 그리고 2개의 인자를 가진 부 생성자 2번으로 진입합니다. 여기에 다시 super() 할 수 있는 Person 클래스의 3번 부 생성자를 호출합니다 코드를 실행하면 차례로 3, 2, 1번 코드 순으로 작업이 진행됩니다.
주 생성자와 부 생성자 함께 사용
fun main() {
// 주 생성자와 부 생성자 사용
val p1 = Person2("Kildong", 30) // 1 -> 2 호출, 3 -> 4 -> 5 실행
println()
val p2 = Person2("Dooly") // 2 호출 3 -> 4 실행
}
/*
결과 :
[Secondary Constructor] Parameter
[Primary Constructor] Parameter
[Primary] Person fName: Kildong
[init] Person init block
[Secondary Constructor] Body: Kildong, 30
[Primary Constructor] Parameter
[Primary] Person fName: Dooly
[init] Person init block
*/
class Person2(firstName: String,
out: Unit = println("[Primary Constructor] Parameter")) { // 2. 주 생성자
val fName = println("[Primary] Person fName: $firstName") // 3. 프로퍼티 할당
init {
println("[init] Person init block") // 4. 초기화 블록
}
// 1. 부 생성자
constructor(firstName: String, age: Int,
out: Unit = println("[Secondary Constructor] Parameter")): this(firstName) {
println("[Secondary Constructor] Body: $firstName, $age") // 5. 부 생성자 본문
}
}
this는 이제 주 생성자를 가리킵니다. 생성하는 객체의 인자 수에 다라 부 생성자 혹은 주 생성자를 호출합니다. 고차 함수에서 배웠듯이 인자에 함수를 지정할 수 있고, out인자에 println()을 기본값으로 할당해 인자에 접근할때 출력되도록 했습니다. fName이라는 프로퍼티에도 출력문을 할당했습니다.
초기화 되는 순서를 추적하면 main()함수의 p1객체가 생성될 떄 인자가 2개 사용되면 1번 부 생성자에서 2번 주 생성자를 호출하고 3번 프로퍼티 할당 -> 4번 초기화 블록 -> 5번 부 생성자의 본문을 차례로 실행합니다.
바깥 클래스 호출
특정 클래스 안에 선언된 클래스를 이너 클래스(Inner Class)라고 합니다. 이너 클래스에서 바깥 클래스의 상위 클래스를 호출하려면 super 키워드와 함꼐 @기호 옆에 바깥 클래스 이름을 작성합니다.
fun main() {
// Inner Class에서 외부 클래스 접근
val c1 = Child()
c1.Inside().test() // Inner 클래스 Inside의 메서드 test() 실행
}
/*
결과 :
Inside Class f()
Childe Class f()
Base Class f()
[Inside] super@Child.x: 1
*/
open class Base {
open val x: Int = 1
open fun f() = println("Base Class f()")
}
class Child: Base() {
override val x: Int = super.x + 1
override fun f() = println("Childe Class f()")
inner class Inside {
fun f() = println("Inside Class f()")
fun test() {
f() // 1. 현재 이너 클래스의 f() 접근
Child().f() // 2. 바로 바깥 클래스 f() 접근
super@Child.f() // 3. Child의 상위 클래스인 Base 클래스의 f() 접근
println("[Inside] super@Child.x: ${super@Child.x}") // 4. Base의 x접근
}
}
}
chile()클래스는 Base클래스를 상속하고 있고, Child 클래스 안에 inner키워드로 선언된 이너 클래스인 Inside 클래스가 있습니다. Inside 클래스의 객체를 생성하고 test() 메서드를 호출합니다.
c1.Inside().test()
인터페이스 참조하기
인터페이스(Interface)는 일종의 구현 약속으로 인터페이스를 참조하는 클래스는 인터페이스가 가지고 있는 내용을 구현해야 하는 가이드를 제시합니다. 인터페이스 자체로는 객체를 생성할 수 없고 항상 인터페이스를 구현하는 클래스에서 생성해야 합니다.
앵글 브래킷으로 사용한 이름 중복 해결
fun main() {
// 앵글 브래킷을 사용한 이름 중복 해결
val c = C()
c.test()
}
/*
결과 :
C Class f()
B interface b()
A Class a()
B Interface f()
*/
open class A {
open fun f() = println("A Class f()")
fun a() = println("A Class a()")
}
interface B {
fun f() = println("B Interface f()") // 인터페이스는 기본적으로 open
fun b() = println("B interface b()")
}
class C : A(), B { // 1. 쉼표(,)를 사용해 클래스와 인터페이스를 지정
// 컴파일되려면 f()가 오버라이딩 되어야함.
override fun f() = println("C Class f()")
fun test() {
f() // 2. 현재 클래스의 f()
b() // 3. 인터페이스 B의 b
super<A>.f() // 4. A 클래스의 f()
super<B>.f() // 5. B 클래스의 f()
}
}
인터페이스의 프로퍼티나 메서드를 사용할 수 있는데 이때 f() 메서드의 이름이 중복되고 있습니다. b()처럼 이름이 중복되지 않은 경우에는 그냥 사용할 수 있으나 중복된 이름은 앵글 브래킷을 사용해 super<A>.f()와 super<B>.f()로 구분할 수 있습니다. f()를 그냥 사용하면 현재 클래스의 f()를 호출합니다.
정보 은닉 캡슐화
캡슐화(Encapsulation)은 객체 지향 프로그래밍의 가장 큰 특징입니다.
가시성 지시자
각 클래스나 메서드, 프로퍼티의 접근 범위를 가시성(Visibility)이라고 합니다. 각 클래스나 메서드, 프로퍼티에 가시성 지시자(Visibility Modifier)에 의해 공개할 부분과 숨길 부분을 정해 줄 수 있습니다.
private: 이 요소는 외부에서 접근할 수 없습니다. public: 이 요소는 어디서든 접근이 가능합니다.(기본값) protected: 외부에서 접근할 수 없으나 하위 상속 요소에서는 가능합니다. internal: 같은 정의의 모듈 내부에서는 접근이 가능합니다. |
가시성 지시자 선언 위치
[가시성 지시자] <val | var> 전역 변수 이름 [가시성 지시자] fun 함수 이름() { ... } [가시성 지시자] [특정 키워드] class 클래스 이름 [가시성 지시자] constructor(매개변수) { [가시성 지시자] constructor() { ... } [가시성 지시자] 프로퍼티 [가시성 지시자] 메서드 } |
(가시성 지시자의 기본값은 public이며, 주 생성자 앞에 가시성 지시자를 사용하는 경우 constructor를 생략 할 수 없습니다.)
private 가시성 테스트
fun main() {
// private 가시성 지시자 예제(Visiblity Modifier)
val pc = PrivateClass() // 생성 가능
// pc.i // 접근 불가
// pc.privateFunc() // 접근 불가
}
fun topFunction() {
val tpc = PrivateClass() // 객체 생성 가능
}
private class PrivateClass {
private var i = 1
private fun privateFunc() {
i += 1 // 접근 허용
}
fun access() {
privateFunc() // 접근 허용
}
}
class OtherClass {
// val opc = PrivateClass() // 불가 - 프로퍼티 opc는 private가 되어야 접근 가능
fun test() {
val pc = PrivateClass()
}
}
PrivateClass 클래스가 private로 선언되어 있으므로 다른 파일에서는 접근할 수 없습니다. 같은 파일에서는 PrivateClass 객체를 생성할 수 있습니다. 만약 다른 클래스에서 프로퍼티로서 PrivateClass의 객체를 지정하려면 똑같이 private로 선언해야 합니다.
객체를 생성했다고 하더라도 PrivateClass의 멤버인 i, privateFunc()메서드가 private으로 선언되었기 떄분에 다른 클래스나 main() 같은 최상위 함수에서 접근할 수 없습니다. private멤버는 해당 클래스 내부에서만 접근 가능합니다.
protected 가시성 테스트
fun main() {
// protected 가시성 테스트
val base = BaseProtected() // 생성 가능
// base.i // 접근 불가
// base.protectedFunc() // 접근 불가
base.access()
}
open class BaseProtected { // 최상위 클래스에는 protected를 사용할 수 없음.
protected var i = 1
protected fun protectedFunc() {
i += 1 // 접근 허용
}
fun access() {
protectedFunc() // 접근 허용
}
protected class Nested // 내부 클래스에는 지시자 허용
}
class Derived : BaseProtected() {
fun test(base: BaseProtected): Int {
protectedFunc() // Base 클래스의 메서드 접근 가능
return i // Base 클래스의 프로퍼티 접근 가능
}
}
protected 멤버 프로퍼티인 i와 메서드 protectedFunc()는 하위 클래스인 Derived 클래스에서 접근할 수 있습니다. protected로 지정된 멤버는 상속된 하위 클래스에서는 자유롭게 접근 가능합니다. 다만 외부 클래스나 객체 생성 후 점(.) 표기를 통해 protected 멤버에 접근하는 것은 허용하지 않습니다.
internal 가시성 테스트
코틀린의 internal은 자바와 다르게 새롭게 정의되었습니다. internal은 프로텍트 단위의 모듈(Module)을 가리키기도 합니다. 기존 자바에서는 package라는 지시자에 의해 패키지 이름이 같은 경우에 접근을 허용했지만, 코틀린에서는 패키지에 제한하지 않고 하나의 모듈 단위를 대변하는 internal을 사용합니다.
자바의 package 지시자 자바의 가시성 지시자 기본값인 package 지시자는 코틀린에서 사용하지 않습니다. 자바에서 package로 지정된 경우 접근 요소가 패키지 내부에 있다면 접근할 수 있습니다. 하지만 프로젝트 단위 묶음의 .jar 파일이 달라져도 패키지 이름이 동일하면 다른 .jar에서도 접근할 수 있었기 떄문에 보안 문제가 발생할 수 있었습니다. 코틀린에서는 이것을 막고자 기존의 package를 버리고 internal로 프로젝트의 같은 모듈(빌드된 하나의 묶음)이 아니면 외부에서 접근할 수 없게 했습니다. 이것은 모듈이 다른 .jar 파일에서는 internal로 선언된 요소에 접근할 수 없는 뜻입니다. |
fun main() {
// Internal 가시성 테스트
val mic = InternalClass() // 생성 가능
mic.i // 접근 허용
mic.icFunc() // 접근 허용
}
internal class InternalClass {
internal var i = 1
internal fun icFunc() {
i += 1 // 접근 허용
}
fun access() {
icFunc() // 접근 허용
}
}
class Other{
internal val ic = InternalClass() // 프로퍼티를 지정할 떄 internal로 맞춤
fun test() {
ic.i // 접근 허용
ic.icFunc() // 접근 허용
}
}
가시성 지시자와 클래스의 관계
가시성 지시자에 따라 공개되는 범위가 달라지는데 가시성 지시자는 클래스 간의 관계에서도 접근 범위를 정할 수 있기 떄문에 상속된 하위 클래스에서 가시성 지시자를 사용하거나 다른 클래스와의 연관 관계를 지정하기 위해서도 사용할 수 있습니다.
UML의 가시성 표기 기호 설계를 시각화하기 위한 다이어그램 표기법인 UML에서는 다음과 같은 기호를 사용해 가시성을 표기하고 있습니다. - : private + : public # : protected ~ : package |
fun main() {
// Internal 가시성 테스트
val mic = InternalClass() // 생성 가능
mic.i // 접근 허용
mic.icFunc() // 접근 허용
}
internal class InternalClass {
internal var i = 1
internal fun icFunc() {
i += 1 // 접근 허용
}
fun access() {
icFunc() // 접근 허용
}
}
class Other{
internal val ic = InternalClass() // 프로퍼티를 지정할 떄 internal로 맞춤
fun test() {
ic.i // 접근 허용
ic.icFunc() // 접근 허용
}
}
(참고로 오버라이딩된 멤버가 있는 경우에는 상위 클래스와 동일한 가시성 지시자를 갖습니다.)
자동차와 도둑 예제
open class TestCar protected constructor(_year: Int, _model: String, _power: String, _wheel: String) {
// 1.
private val year: Int = _year
val model = _model
protected open val power: String = _power
internal var wheel: String = _wheel
protected fun start(key: Boolean) {
if (key) println("Start the Engine!")
}
class TestDriver(_name: String, _licence: String) {
// 2.
private var name: String = _name
var license: String = _licence
internal fun driving() = println("[Driver] Driving() - $name")
}
}
class Tico(_year: Int, _model: String, _power: String, _wheel: String, var name: String,
private var key: Boolean): TestCar(_year, _model, _power, _wheel) {
override var power: String = "50hp"
val driver = TestDriver(name, "first class")
constructor(_name: String, _key: Boolean ) : this(2014, "basic", "100hp",
"normal", _name, _key) {
name = _name
key = _key
}
fun access(password: String) {
if (password == "gotico") {
println("----[Tico] access( )--------")
// super.year // 3. 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}") // public
driver.driving() // internal
} else {
println("You`re a burglar")
}
}
}
class Burglar() {
fun steal(anycar: Any) {
if (anycar is Tico) { // 4. 인자가 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("dontknow")
} else {
println("Nothing to steal")
}
}
}
fun main() {
// val car = TestCar()
val tico = Tico("kildong", true)
tico.access("gotico")
val burglar = Burglar()
burglar.steal(tico)
}
/*
결과 :
----[Tico] access( )--------
super.model = basic
super.power = 100hp
super.wheel = normal
Start the Engine!
Driver().license = first class
[Driver] Driving() - kildong
----[Burglar] steal()--------
anycar.name = kildong
anycar.wheel = normal
anycar.model = basic
first class
[Driver] Driving() - kildong
You`re a burglar
*/
1번의 Car 클래스의 주 생성자에는 protected 지시자가 있기 떄문에 constructor키워드를 생략할 수 없으며 Car 클래스를 상속한 클래스만이 Car 클래스의 객체를 생성할 수 있습니다.
2번의 Driver클래스는 Car 클래스 안에 있습니다. Car 클래스를 상속받는 Tico 클래스에서는 access() 메서드에서 super를 사용해 상위 클래스에 접근을 시도합니다. 이때 상위 클래스의 private 요소인 3번의 super.year에는 접근할 수 없습니다.
model, power, wheel 같은 public, protected, internal 요소는 Tico 클래스에서 접근할 수 있습니다.
이제 4번의 Burglar 클래스를 살펴보면 steal() 메서드 하나만 정의하고 있습니다. 여기서 Any 자료형의 매개변수인 anycar를 받아서 검사하고 있습니다. 이때 자료형 검사 키워드인 is를 사용해 Tico의 객체인 경우에 이 Tico 객체인 anycar를 통해 접근을 시도합니다. 이때 name, wheel, model 같은 public, internal 요소는 접근이 가능합니다. 특히 internal의 경우는 파일이 달라져도 같은 모듈에 있으므로 접근이 가능합니다.
클래스와 클래스의 관계
클래스 혹은 객체 간의 관계
클래스나 객체들간의 관계(Relationship)는 약하게 연결된 관계부터 강하게 결합된 관계가 있습니다. 먼저 약하게 참조되고 있는 관계로 연관(Association)이나 의존(Dependency)관계가 있습니다.
다이어그램에서 연관 관계는 실선으로 표기하고 의존 관계에 있을 떄는 점선 화살표를 사용합니다.
좀더 쉽게 예를 들어보면
연못(Pond) 클래스는 다수의 오리(Duck)를 가질 수 있습니다. 연못(Pond)과 같이 무언가 여러 요소를 담을 수 있는 컨테이너 자료형에 해당하는 경우에는 포함 관계를 위한 개수를 적어줍니다. 제로(0), 하나(0..1), 다수(0..*)의 객체와 연관을 가질 수 있다는 것을 나타내기 위한 표기법을 사용하기도 합니다. 연못에 오리가 한 마리도 없을 경우도 있으니 개수 관계는 0..* 로 표기할 수 있습니다. 따라서 연못과 오리는 집합(Aggregation)관계라고 합니다. 하지만 연못과 오리는 따로 떨어져도 상관 없습니다. 별도로 서로 따로 존재하는 경우 흰색 다이아몬드 모양의 표기법으로 나타냅니다.
마지막으로 합성 혹은 구성(Composition)관계에 있는 경우 두 개체가 아주 밀접하게 관련되어 있어 독립적으로 존재하기 힘든 것을 말합니다. 앞에서 살펴본 자동차와 도둑의 예제에서 자동차인 Car클래스는 엔진 Engine 클래스를 하나 가지고 있습니다. 이것은 자동차의 구성품으로 자동차 클래스가 파괴되면 엔진도 더 이상 동작하지 않게 됩니다. 두 개체 간의 생명주기는 의존되어 있습니다. 이런 구성 관계는 검정 다이아몬드로 표기합니다.
클래스 간의 관계를 판별하는 방법
크래스 간의 관계는 두 클래스가 서로 참조하느냐 아니냐에 따라 나뉘고, 그런 다음 두 클래스가 생명주기에 영향을 주는지에 따라 나뉠 수 있습니다.
객체는 서로 독립적으로 존재할 수 있으며 서로 참조를 유지하면 연관 관계입니다. 참조를 유지하지 않는다면 연관보다 약한 의존 관계가 됩니다. 포함 관계에 있지만 객체의 생명주기가 서로 유지되고 있는 경우에는 집합 관계가 되며, 포함 관계의 객체가 사라질 떄 같이 사라져 생명주기가 유지되지 않으면 구성 관계입니다.
연관관계
fun main() {
val doc1 = Doctor("Kimsabu")
val patient1 = Patient("Kildong")
doc1.patientList(patient1)
patient1.doctorList(doc1)
}
/*
결과 :
Doctor: Kimsabu, Patient: Kildong
Patient: Kildong, Doctor: Kimsabu
*/
class Patient(val name: String) {
fun doctorList(d: Doctor) {// 인자로 참조
println("Patient: $name, Doctor: ${d.name}")
}
}
class Doctor(val name: String) {
fun patientList(p: Patient) { // 인자로 참조
println("Doctor: $name, Patient: ${p.name}")
}
}
Doctor와 Patient 클래스의 객체는 따로 생성되며 서로 독립적인 생명주기를 가지고 있습니다. 이 코드에서는 두 클래스가 서로의 객체를 참조하고 있으므로 양방향 참조를 가집니다. 단방향이든 양방향이든 각각의 객체의 생명주기에 영향을 주지 않을 때는 연관관계라고 합니다.
의존 관계
fun main() {
val patient1 = Patient2("kildong", 1234)
val doc1 = Doctor2("kimSabu", patient1)
doc1.patientList()
}
/*
결과 :
Doctor: kimSabu, Patient: kildong
Patient Id: 1234
*/
class Patient2(val name: String, var id: Int) {
fun doctorList(d: Doctor2) {
println("Patient: $name, Doctor: ${d.name}")
}
}
class Doctor2(val name: String, private val p: Patient2) {
private val customerId: Int = p.id
fun patientList() {
println("Doctor: $name, Patient: ${p.name}")
println("Patient Id: $customerId")
}
}
Doctor 클래스는 주 생성자에 Patient를 매개변수로 받아야 하므로 Patient 객체가 먼저 생성되어 있어야 합니다. 따라서 Doctor클래스는 Patient 클래스에 의존합니다.
집합관계
집합(Aggregation)관계는 연관 관계와 거의 동일하지만 특정 객체를 소유한다는 개념이 추가된 것입니다.
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)
}
}
/*
결과 :
Duck1
Duck2
*/
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) {
}
연못은 개념적으로 여럿의 오리를 소유할 수 있습니다. 오리 입장에서는 한 번에 한 연못에서만 놀 수 있습니다. 동시에 다른 연못에서 놀 수는 없습니다. 오리 여럿을 추가하기 위해서는 여러개의 데이터를 담을 수 있는 배열이나 리스트 구조가 필요합니다. 코드상에서 오리와 연못 두 개체는 따로 생성되어 서로의 생명주기에 영향을 주지 않습니다. members가 MutableList로 선언되어 있으므로 여러 개의 객체를 담을 수 있는 add() 메서드를 사용할 수 있습니다.
pond.members.add(duck1)
구성관계
구성(Composition)관계는 집합 관계와 거의 동일하지만 특정 클래스가 어느 한 클래스의 부분이 됩니다. 구성품으로 지정된 클래스는 생명주기가 소유자 클래스에 의존되어 있습니다. 만일 소유자 클래스가 삭제되면 구성하고 있던 클래스도 같이 삭제됩니다.
fun main() {
val car = Car("Tico", "100hp")
car.startEngine()
car.stopEngine()
}
class Car(val name: String, val power: String) {
private var engine = Engine(power) // Engine 클래스 객체는 Car에 의존적
fun startEngine() = engine.start()
fun stopEngine() = engine.stop()
}
class Engine(val power: String) {
fun start() = println("Engine has been started.")
fun stop() = println("Engine has been stopped.")
}
객체간의 메시지 전달하기
두 객체 간의 메시지 전달(Message Sending)은 프로그래밍에서 아주 흔하며 시간의 흐름에 따라 일어나는 경우가 대부분이기 떄문에 주로 UML 시퀀스 다이어그램(Sequence Diagram)으로 표현합니다.
이상 마치겠습니다.:)
'책 요약하기 > Do it! 코틀린 프로그래밍' 카테고리의 다른 글
#07-1. 추상 클래스와 인터페이스 2021-05-08 (0) | 2021.05.08 |
---|---|
#06. 프로퍼티와 초기화 2021-05-02 (0) | 2021.05.02 |
#05-1. 클래스와 객체, 생성자, 상속과 다형성 2021-04-25 (0) | 2021.04.25 |
#04. 함수와 함수형 프로그래밍 2021-04-25 (0) | 2021.04.25 |
#03-2. 함수와 함수형 프로그래밍 2021-04-20 (0) | 2021.04.20 |