이번 글에서는 객체지향 프로그래밍의 가장 유용한 문법 중 하나인 상속 inheritance 입니다.
지난번 글에서 소개했듯이 객체란 변수와 기능을 가진 것이라고 했습니다. 그리고 그 객체가 어떤 변수와 기능을 가질 것인지를 설계하는 것을 class 라고 했습니다.
개발자가 필요한 모든 데이터와 기능들을 매번 직접 class로 설계하기에는 번거로울 겁니다. 나에게 필요한 기능과 비슷한 기능을 가진 class가 이미 다른 누군가에 의해 만들어져 있을 수 있다면 처음 부터 설계하기 보다는 기존에 존재하는 클래스 설계를 그대로 가져와서 나에게 필요한 몇가지 기능만 수정하거나 추가하면 프로그래밍 노고가 상당부분 개선되고 생산성도 높아질 겁니다. 이렇게 이미 존재하는 클래스의 변수와 함수를 그대로 물려받아 재사용할 수 있도록 하는 문법이 상속입니다.
그럼. 코틀린에서 상속을 다루는 문법에 대해 알아보도록 하겠습니다. 역시, 자바와는 약간 차이가 있으니 주의깊게 살펴보시기 바랍니다.
새로운 코틀린 파일 Test07OOP2.kt 를 만들고 문법을 알아보겠습니다.
코틀린 프로그램의 시작은 언제나 main() 함수이니 만들고 시작하겠습니다.
1. 상속 inheritance
먼저 상속을 해줄 부모 클래스를 만들어 보겠습니다. [ 클래스명 First ]
코틀린 class는 자바와 다르게 기본적으로 final 클래스처럼 상속이 불가능한 클래스로 만들어집니다. 그래서 부모클래스로서 상속을 해주는 클래스는 반드시 class 앞에 open 키워드로 열어줘야 합니다.
메소드 역시 자바와 다르게 기본적으로 final 메소드처럼 기능을 개선하는 오버라이드 Override 가 불가능한 메소드로 만들어 집니다. 그래서 부모클래스의 메소드 중 오버라이드를 허용하는 메소드는 fun 앞에 open 키워드로 오버라이드를 허용해야 합니다.
//시작함수
fun main(){
}//main 함수 종료...
//상속을 해줄 부모클래스는 반드시 open 키워드가 추가되어야 함. 없으면 Java에서의 final 클래스와 같은 것임
open class First{
var a:Int=10
//override 해줄 메소드라고 명시하기위해 open키워드 추가 [없으면 final method ]
open fun show(){
println(" a : $a ")
}
}
멤버변수(프로퍼티) a 와 멤버함수(메소드) show()를 가진 First 클래스를 상속하는 Second 클래스를 만들어 보겠습니다.
자바에서는 상속을 위해 extends 라는 키워드를 사용하였다면 코틀린에서는 C계열 언어처럼 콜론 : 으로 extends 키워드를 대체합니다.
또한 상속을 위해 부모클래스명을 작성할 때 반드시 명시적으로 생성자 호출을 위한 소괄호()가 있어야 합니다.
가끔 초보자들 중에 상속이 부모의 멤버들만 쏙 뽑아오는 것처럼 인식하는 분들이 있더군요. 하지만 실제로 상속이라는 것은 자식객체 안에 부모객체가 존재하는 겁니다. 마치 인형안에 인형이 들어 있는 러시안 인형(마트료시카)처럼 말이죠. 다만 자식객체에서 부모객체의 멤버를 마치 내것인양 사용하는 문법이 상속인 겁니다.
코틀린의 상속은 콜론 : 다음에 부모 클래스의 생성자 호출문을 직접 작성하기에 부모 객체를 명시적으로 생성하는 표기문법이서 개인적으로는 더 직관적이고 좋다고 생각합니다. 더불어 부모생성자에 파라미터를 전달하는 코드가 매우 간결해지는 장점도 있습니다. 코드를 보면서 차차 소개하도록 하겠습니다.
//시작함수
fun main(){
//1) 자식 클래스의 객체 생성하기 - 코틀린의 상속은 부모클래스에 open키워드를 추가해야만 함.
var obj= Second()
obj.a=10 //부모클래스 First의 멤버변수를 내것인양 사용
obj.b=20 //자식클래스 Second의 멤버변수
obj.show() //부모클래스 First의 멤버함수를 내것인양 호출
}//main 함수 종료...
//상속을 해줄 부모클래스는 반드시 open 키워드가 추가되어야 함. 없으면 Java에서의 final 클래스와 같은 것임
open class First{
var a:Int=10
//override 해줄 메소드라고 명시하기위해 open키워드 추가 [없으면 final method ]
open fun show(){
println(" a : $a ")
}
}
//상속을 받은 자식클래스
//상속의 문법 [ 클래스명 뒤에 : 후 부모클래스명() 작성 <- 부모클래스명 뒤에 주 생성자호출()를 주의할것!! ]
class Second : First(){
var b:Int=20
}
Second 클래스는 명시적으로는 멤버변수 b 만 직접 만들었지만 First 클래스를 상속받았기 때문에 이미 a, show() 멤버를 보유한 상태입니다. 다만 부모클래스 First의 출력 기능 show()를 사용한 것이어서 자식 클래스 Second의 멤버변수 b는 출력되지 않습니다. 이렇게 부모로 부터 받은 특정 기능의 메소드를 개선해야 할 필요가 있을 때 사용하는 것이 오버라이드 override 입니다. 이미 First 클래스의 show메소드는 open 키워드를 통해 오버라이드를 허용하였기에 Second에서 개선이 가능합니다. 단, 자바와 다르게 이 메소드가 오버라이드 메소드라는 것을 반드시 명식적으로 표기해야 합니다. 자바의 @Override 와 같이 어노테이션을 사용하는 것이 아니고 fun 앞에 override 키워드를 붙여줘야 합니다. 또한 자식클래스에서 부모클래스의 show()를 호출하기 위해 super 키워드를 사용하는 것은 자바와 동일합니다.
//시작함수
fun main(){
//1) 자식 클래스의 객체 생성하기 - 코틀린의 상속은 부모클래스에 open키워드를 추가해야만 함.
var obj= Second()
obj.a=10 //부모클래스 First의 멤버변수를 내것인양 사용
obj.b=20 //자식클래스 Second의 멤버변수
obj.show() //**오버라이드에 의해 Second의 show()메소드 호출**
}//main 함수 종료...
//상속을 해줄 부모클래스는 반드시 open 키워드가 추가되어야 함. 없으면 Java에서의 final 클래스와 같은 것임
open class First{
var a:Int=10
//override 해줄 메소드라고 명시하기위해 open키워드 추가 [없으면 final method ]
open fun show(){
println(" a : $a ")
}
}
//상속의 문법 [ 클래스명 뒤에 : 후 부모클래스명() 작성 <- 부모클래스명 뒤에 주 생성자호출()를 주의할것!! ]
class Second : First(){
var b:Int=20
//같은 이름의 메소드를 부모로부터 상속받았기에 그냥 쓰면 에러!!
//오버라이드라고 명시적으로 표기를 해야함.[ 이때, 오버라이드가 되는 메소드도 역시 open키워드가 있어야함. 없으며 final 메소드로 인식함 ]
override fun show(){
super.show() //First 부모클래스의 show() 호출 - a 변수 출력
println(" b : $b")
}
}
//**출력**
a : 10
b : 20
2. 업 캐스팅, 다운 캐스팅
· UP casting : 부모참조변수가 자식객체를 참조하는 것
· DOWN casting : 자식참조변수가 부모를 참조하는 것 - 단, 부모는 업 캐스팅 상태여야 함
다형성을 위한 업 캐스팅과 다운 캐스팅은 자바와 특징이 동일합니다. 다만, 형변환[type cast] 문법에 차이가 있습니다.
위 상속 클래스 소개에 사용했던 First - Second 클래스를 사용하겠습니다.
부모클래스 : First
자식클래스 : Second
//시작함수
fun main(){
var f:First= Second() // up casting - 부모참조변수로 자식 객체 참조
f.show() // 실제 가리키는 대상객체인 Second의 show() 메소드 호출
}//main 함수 종료...
//**출력**
a : 10
b : 20
부모참조변수인 f는 First참조변수이지만 실제로는 Second 객체를 참조하고 있기에 Second객체의 show()메소드가 실행됩니다.
즉, 부모참조변수로 자식객체의 기능 메소드를 사용할 수 있다는 점에 그 의미가 있습니다.
다만, f 참조변수는 본인의 자료형이 First 로 지정되어 있기에 실제 참조하는 있는 대상이 Second 객체인지 알지 못합니다. 그럼에도 show()메소드를 호출할 수 있었던 이유는 First클래스에도 show()메소드가 있었기 때문입니다. 즉, f 참조변수는 본인의 멤버메소드인 show()메소드를 호출한 겁니다. 다만, 자식 객체에 의해 show()메소드가 오버라이드 되어 있어서 Second의 show()를 호출된 것 입니다.
그렇기에 업 캐스팅을 통해 부모참조변수가 자식객체를 참조할 수는 있더라도 부모의 멤버나 오버라이드 된 메소드만 호출 할 수 있고 자식객체만의 고유한 멤버는 사용할 수 없습니다.
확인을 위해 Second 클래스에 aaa() 라는 이름의 메소드를 하나 추가하고 First로 호출하려 하면 에러가 발생되는 것을 확인할 수 있습니다.
//시작함수
fun main(){
var f:First= Second() // up casting
f.show() // 실제 가리키는 대상객체인 Second의 show() 메소드 호출
f.aaa() // ERRPR : 부모참조변수인 f 가 실제 Second를 참조하고 있더라도 Second클래스의 고유 멤버는 사용불가
}//main 함수 종료...
//부모 클래스
open class First{
var a:Int=10
//override 해줄 메소드
open fun show(){
println(" a : $a ")
}
}
class Second : First(){
var b:Int=20
//오버라이드 한 show() 메소드
override fun show(){
super.show()
println(" b : $b")
}
//Second 클래스만의 고유한 메소드
fun aaa(){
println("Second의 고유 메소드")
}
}
결국 업 캐스팅을 통해 부모가 자식을 참조하여 제어할 수 있더라도 자식만의 고유기능은 사용할 수 없다는 것입니다.
만약, 자식만의 고유한 기능을 사용하고 싶다면, 자식 참조변수를 새로 만들어서 부모참조변수의 참조값을 대입해 주어야 합니다.
이를 다운캐스팅 이라고 부릅니다.
//시작함수
fun main(){
var f:First= Second() // up casting
//down casting.
val s:Second = f //ERROR - 형변환 없이 대입하려하면 자식이 부모를 참조한다고 판단하여 에러문법처러됨
}//main 함수 종료...
코드에서 확인했듯이 자식은 부모를 참조할 수 없습니다. 왜 못 하게 했을 까요?
자식의 멤버는 부모의 멤버들을 상속받아 온 후 추가로 새로운 멤버들을 추가해서 만든 클래스 입니다. 그러니 부모보다 멤버의 개수가 같거나 많을 수 밖에 없습니다. 그렇기에 자식 참조변수가 사용하려는 멤버를 실제 부모객체가 보유하지 않는 문제로 실행이 불가능한 상황이 생길 수 있습니다. 하여. 이렇게 자식이 부모를 참조하는 것 자체를 문법적으로 막아 놓았습니다.
그런데. 위 코드 처럼 부모참조변수 f 가 참조하는 실제 객체는 Second 객체 입니다. 그렇기에 f 의 참조값을 Second 참조변수 s 에 대입하면 결국 s 는 Second 객체를 온전히 참조하는 상황이 되니 문제 될 것이 없습니다.
그래서 참조값을 대입할 때 명시적으로 Second 임을 알려주기 위해 형변환 하면서 대입 해주면 문제없이 대입이 됩니다. 코드적으로 보면 자식 참조변수 s 가 부모인 f 를 대입하여 참조하는 모습이어서 down casting 이라고 부릅니다. 즉, 다운 캐스팅은 형변환이 필수입니다.
참고로, 코틀린의 형변환 연산자는 as 입니다. '~로서' 라는 의미로 읽으시면 쉽게 기억될 겁니다.
//시작함수
fun main(){
var f:First= Second() // up casting
//down casting.
val s:Second = f //ERROR - 형변환 없이 대입하려하면 자식이 부모를 참조한다고 판단하여 에러문법처러됨
//val s:Second = (Second)f // ERROR - 자바에서의 형변환 연산자 (Type) 은 코틀린에 없는 문법임
val s:Second = f as Second // 형변환 연산자 as
s.show()
s.aaa()
}//main 함수 종료...
//**출력**
a : 10
b : 20
Second의 고유 메소드
♣상속 문법 마무리 예제
지금껏 학습한 상속에 대해 문법적으로 정리해 보는 마무리 예제를 만들어 보겠습니다. 자바 학습 자료들에 상속관련 예제 중 가장 유명한 예제이며 이를 통해 class, 객체, 주 생성자, 보조 생성자, 상속 들에 대해 정리 해보겠습니다. 코드의 효율성 보다는 학습내용을 정리하기 위해 일부러 주 생성자, 보조 생성자를 섞어가며 만들어 볼 겁니다.
[예제]
어느 대학의 학사정보를 제공하는 앱을 만들고자 합니다. 이 앱을 사용하는 사용자에 따라 접근할 수 있는 권한을 제한하고자 합니다.
그렇기에 이 앱의 사용자 종류는 크게 4 종류로 구분하였습니다. 일반회원, 학생회원, 교수회원, 근로장학생회원 으로 구분하고자 합니다.
일반회원은 전공이 있을 수 없으니 회원마다 저장할 정보가 다르겠지요. 그래서 회원별 저장할 대표 데이터를 정리해 보겠습니다.
* 대학교 학사정보 앱 회원 데이터*
일 반 : 이름, 나이
학 생 : 이름, 나이, 전공
교 수 : 이름, 나이, 연구과제
근로학생 : 이름, 나이, 전공, 업무
각 각의 회원들은 저장 정보가 다르기에 같은 멤버변수의 개수와 종류가 다릅니다. 하여 각 회원별로 별도의 class 를 만들어야 합니다.
다들 느끼셨겠지만, 4개의 회원에 공통적으로 저장되는 데이터가 보이네요. 상속을 활용하기에 적합해 보입니다. 코드를 보면서 확인해보겠습니다.
1) 먼저, 일반 회원 부터 class 를 만들어 보겠습니다. 별도의 Person.kt 파일을 만들어 클래스를 설계하겠습니다. [ 주 생성자 활용 ]
//일반회원 : [이름, 나이] 정보를 프로퍼티(멤버변수)로 가지는 클래스 - 주 생성자 활용 [ 클래스 명 옆에 constructor 키워드]
open class Person constructor(var name:String, var age:Int){ // 상속을 허용하기 위해 open 키워드
init {
println("create Person instance")
}
//멤버변수 값을 출력해주는 기능 메소드 - 자식클래스에서 override 하는 것을 허용하기 위해 open 키워드
open fun show(){
println("name : $name age: $age ")
}
}
주 생성자의 파라미터를 만들 때 var 키워드를 적용함으로 파라미터면서 멤버변수를 만들어서 코드가 간결해 진 것을 확인할 수 있습니다. 또한, [이름, 나이] 정보는 이후 만들 학생회원에서도 사용하는 멤버이기에 상속을 해주고자 open 키워드를 적용한 부분이 특별히 보셔야 할 부분입니다.
잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 Person 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.
//시작 함수
fun main(){
//상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
//1) 일반회원
var p= Person("sam", 20) //이름, 나이
p.show()
println()
}//main 함수 죵료..
//**출력**
create Person instance
name : sam age: 20
2) 다음으로, [이름, 나이, 전공] 정보를 가지는 학생회원 클래스로 별도의 Student.kt 파일을 만들어 보겠습니다. [이름, 나이] 정보는 이미 설계된 일반 회원 클래스를 상속하고 추가로 [전공] 정보만 직접 설계하겠습니다. 즉, Person 클래스를 상속하여 Student 클래스를 만들고자 합니다. Person 클래스를 만들 때 처럼 주 생성자를 이용하여 만들어 보겠습니다. 상속받을 Person클래스에 이미 [name, age] 프로퍼티들이 있으므로 Student에서는 주 생성자의 파라미터로 [name, age]를 받을 때는 멤버변수를 만들어주는 var키워드를 사용하면 안됩니다. 단지 매개변수로만 만들어야 하며 Student 클래스에만 존재하는 전공 정보를 저장할 프로퍼티를 만들기 위해 major만 var 키워드를 추가해 줘야 합니다. 또한, 상속하는 Person 클래스의 생성자를 호출하면서 파라미터로 받은 name, age 값을 전달 해야 합니다. 코드로 확인해 보겠습니다.
//학생회원 : [이름, 나이, 전공]을 저장하는 클래스 - 일반회원을 상속받아 제작
//상속받을 Person클래스에 이미 name, age 프로퍼티들이 있으므로 Student에서는 name, age를 받을 때 var키워드를 사용하면 안됨!!! 변수 오버라이드가 됨.
open class Student constructor(name:String, age:Int, var major:String) : Person(name, age) {
init {
println("create Student instance")
}
//override 키워드가 추가되면 open키워드가 이미 적용된 것임.
override fun show(){
//super.show()
println("name : $name age: $age major : $major ")
}
}
상속받은 Person의 show()메소드는 name, age 값만 출력해주는 기능이기에 이를 개선하기 위해 오버라이드를 통해 name, age, major 값을 출력해 주도록 설계하였습니다. 참고로 override 키워드는 open 키워드가 이미 적용된 것이기에 오버라이드를 허용합니다.
잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 Student 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.
//시작 함수
fun main(){
//상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
//1) 일반회원
var p= Person("sam", 20) //이름, 나이
p.show()
println()
//2) 학생회원
var stu= Student("robin", 25, "kotlin android")
stu.show()
println()
}//main 함수 죵료..
//**출력**
create Person instance
name : sam age: 20
create Student instance
name : robin age: 25 major : kotlin android
3) 다음으로, [이름, 나이, 전공, 업무] 정보를 가지는 근로장학생 회원 클래스로 AlbaStudent.kt 파일을 만들어 보겠습니다. [이름, 나이, 전공] 정보는 Student 학생 회원 클래스에 있는 정보이니 굳이 다시 설계하지 않고 Student 클래스를 상속하여 만들고 [업무] 프로퍼티 만 직접 추가하여 클래스를 설계하겠습니다. 주 생성자를 이용하되 constructor 키워드를 생략하여 코드를 보다 간결하게 작성하겠습니다.
//근로장학생 : [이름, 나이, 전공, 업무] property(멤버변수) 를 가지는 클래스 - Student 클래스 상속
//constructor 키워드 생략해보기
class AlbaStudent(name:String, age:Int, major:String, var task:String) : Student(name, age, major){
init {
println("create AlbaStudent instance")
}
override fun show() {
//super.show()
println("name : $name age: $age major : $major task : $task ")
}
}
constructor 키워드를 생략한 것 말고는 Student 클래스를 만들 때와 동일한 방식이어서 크게 어렵지 않을 겁니다. 더 이상 상속을 해줄 필요가 없기에 class 앞에 open 키워드를 추가하지 않았습니다. 새로 추가된 [ 업무 task ] 변수의 값을 출력해주도록 상속받은 show() 메소드를 오버라이드 하였습니다.
잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 AlbaStudent 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.
//시작 함수
fun main(){
//상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
//1) 일반회원
var p= Person("sam", 20) //이름, 나이
p.show()
println()
//2) 학생회원
var stu= Student("robin", 25, "kotlin android")
stu.show()
println()
//3) 근로장학생 회원
val alba= AlbaStudent("tom", 27, "java android", "PC Management")
alba.show()
println()
}//main 함수 죵료..
//**출력**
create Person instance
name : sam age: 20
create Student instance
name : robin age: 25 major : kotlin android
create AlbaStudent instance
name : tom age: 27 major : java android task : PC Management
4) 마지막으로, [이름, 나이, 연구과제] 정보를 가지는 교수 회원 클래스로 Professor.kt 파일을 만들어 보겠습니다. 앞서 만든 Student 처럼 일반 회원 클래스 Person을 상속하여 [이름, 나이] 정보는 파라미터로만 받고 [연구과제] 정보만 멤버변수로 직접 만들겠습니다. 다양한 실습을 위해 보조 생성자를 이용하여 설계해 보겠습니다. 보조 생성자를 사용하여 상속할 때는 부모생성자 클래스명 뒤에 ()를 쓰지 않고 자식클래스의 보조 생성자에서 super() 생성자로 호출해야 합니다.
//교수회원 : [이름, 나이, 연구과제] 프로퍼티를 가지는 클래스 - Person 클래스 상속
//보조 생성자를 사용하여 상속할 때 부모생성자 클래스명 뒤에 ()를 쓰지 않고.. 자식클래스의 보조 생성자에서 super()로 호출하도록 함.
class Professor : Person{
//Professor 클래스의 property(멤버변수) : 보조생성자는 직접 프로퍼티를 만들 수 없기에....
var subject:String?= null
//보조 생성자
constructor(name:String, age:Int, subject: String) : super(name, age){ //super()키워드로 부모생성자 호출
//멤버변수(프로퍼티)에 매개변수 전달!!
this.subject= subject
println("create Professor instance")
}
override fun show() {
//super.show()
println("name : $name age: $age subject: $subject ")
}
}
보조 생성자에서 name, age 값을 전달 받기에 클래스명 옆에 : Person()의 주 생성자로 전달 할 수 없기에 보조생성자 옆에 : super 생성자를 통해 값을 전달하는 것을 주의 깊게 보시기 바랍니다.
잘 만들어 졌는지 확인 해 볼까요? main() 함수에서 Professor 객체를 생성하고 멤버를 출력하는 기능 show()를 호출해서 멤버값이 온전히 전달 되었는지 확인해 보겠습니다.
//시작 함수
fun main(){
//상속 마무리 연습 [ Person <- Student <- AlbaStudent ]
//1) 일반회원
var p= Person("sam", 20) //이름, 나이
p.show()
println()
//2) 학생회원
var stu= Student("robin", 25, "kotlin android")
stu.show()
println()
//3) 근로장학생 회원
val alba= AlbaStudent("tom", 27, "java android", "PC Management")
alba.show()
println()
//4) 교수회원 - 보조생성자를 이용하는 Professor 클래스(Person을 상속하는) 연습
val t= Professor("son", 45, "mobile optimization")
t.show()
println()
}//main 함수 죵료..
//**출력**
create Person instance
name : sam age: 20
create Student instance
name : robin age: 25 major : kotlin android
create AlbaStudent instance
name : tom age: 27 major : java android task : PC Management
create Professor instance
name : son age: 45 subject: mobile optimization