코틀린의 기본 문법까지 알아보았으니 이제 코틀린에서의 객체지향프로그래밍 문법을 알아보도록 하겠습니다.
자바와 문법적 차이가 많이 발생하기에 다소 어색할 수 있지만 하면 할수록 보다 간결하고 좋다고 느낄겁니다. 이해가 다소 힘든 부분이 있더라도 끝까지 잘 따라와 주시기 바랍니다.
♣ OOP : Object Oriented Programming 객체 지향 프로그래밍
서로 연관있는 데이터와 이를 제어하는 기능을 묶어서 만든 것을 객체라고 부릅니다.
잠시 복습하는 의미에서 객체지향 프로그래밍을 간단하게 이해해 보도록 하겠습니다.
아래와 같이 학생들의 기본정보를 보여주는 데이터가 있습니다.
이름 | 나이 | 주소 |
sam | 20 | seoul |
robin | 25 | busan |
hong | 30 | newyork |
이 데이터들을 앱에서 사용하기 위해 변수를 만들어 저장하도록 하겠습니다.
첫번째 학생의 이름, 나이, 주소 정보를 저장하기 위해 name, age, address 변수 3개를 만들어 각각의 값을 대입해주면 될겁니다.
근데 문제는 학생이 한명이 아니라는 거죠. 두번째 학생의 정보를 저장하려면 또 다시 변수 3개가 필요합니다. 기존에 만든 name, age, address 변수를 사용하면 첫번째 학생의 데이터가 없어지니 어쩔 수 없이 name2, age2, address2 라는 식으로 변수명을 조금 다르게 하여 3개를 또 만들어야합니다. 세번째 학생의 데이터도 마찬가지로 변수 3개를 만들어야 겠죠. 그럼 3명의 학생 데이터를 저장하기 위해 변수를 9개나 만들어서 관리해야 합니다. 9개 정도면 할만합니다. 근데 만약 학생이 10명, 20명, 100명이 되면 만들어야 하는 변수가 너무 많습니다. 관리가 너무 어렵습니다. 그래서 이 3개의 변수를 묶어서 새로운 큰 박스로 묶어서 하나의 변수로 만들면 관리가 더 용이하겠죠. 이렇게 한 학생의 정보인 이름, 나이, 주소 정보를 하나의 그룹으로 묶어주는 문법이 class 라는 문법입니다.
또한 이 변수들의 값을 출력하거나 제어하는 기능(함수, 메소드)을 변수들이 있는 class에 같이 묶어서 설계하면 관리가 훨씬 용이합니다.
이렇게 설계된 class 를 실제 프로그램에서 사용하기 위해서 객체 라는 것으로 만들어 사용하게 됩니다.
정리하면. class 는 어떤 변수들과 기능(함수)을 묶을 것인가를 설계해놓은 코드이고 이 코드를 사용하려면 객체 라는 것으로 실체화 하여 안에 있는 변수와 기능(함수)를 사용하는 겁니다. 그래서 class를 설계도, 객체를 설계도로 만든 제품으로 비유하기도 합니다.
대략적인 개념적 복습을 해봤으니 실습을 해보면서 객체지향프로그래밍에 대해 학습하면서 코틀린에서 OOP를 다루는 문법을 알아보도록 하겠습니다.
새로운 코틀린 파일 Test06OOP.kt 를 만들고 문법을 알아보겠습니다.
코틀린 프로그램의 시작은 언제나 main() 함수이니 만들고 시작하겠습니다.
1. 클래스와 객체 생성
먼저 새로운 class 를 만들어 보겠습니다. 특별한 의미는 없이 그냥 문법적 표기법을 알아보기 위한 class입니다.
class 를 작성하는 위치는 어디든 상관없는 데 우선은 main()함수 밖에 정의해 보겠습니다.
//시작함수
fun main(){
}//main 함수 종료...
//클래스 선언 - 자바나 C++과 기본 모습은 비슷함. 단, 멤버변수(Field)를 Property(프로퍼티) 라고 부름
class MyClass{
//멤버변수[ Property:프로퍼티] -반드시 초기화 해야함
var a:Int= 10
//메소드 : Method
fun show(){
println(" show : $a ")
println()
}
}
주석으로 설명했듯이. class 정의 문법은 자바와 동일합니다.
멤버변수를 만드는 문법은 코틀린의 변수 선언 문법인 var, val 키워드를 이용하여 만듭니다.
다만, 멤버변수를 Field(필드)라고 부르지 않고 Property(프로퍼티) 라고 부릅니다. 또한 반드시 초기화를 해야 합니다. 초기화를 하지 않으면 문법적 에러입니다.
또한, 자바와 마찬가지로 멤버함수를 Method(메소드)라고 부르면 작성 방식은 함수를 만드는 문법과 동일하게 fun 키워드로 만들어야 합니다.
앞에서 설명했듯이 클래스는 어떤 변수와 기능메소드를 묶을것인가를 설계한 설계도를 만든 것이기에 이 상태에서 실행을 해도 main()함수 안에 아무것도 작성한 것이 없어서 아무 동작을 하지 않습니다.
설계한 class를 사용하려면 객체로 만들어서 사용해야 합니다. main()함수 안에 위에서 설계한 MyClass 클래스를 객체로 생성해 보겠습니다. 자바에서는 객체를 생성하기 위해 new 라는 키워드를 사용했습니다. 하지만 코틀린은 new 키워드 없이 클래스명과 생성자 호출로 객체를 생성합니다. 파이썬 언어의 객체 생성과 같은 모습입니다.
//시작함수
fun main(){
//1. 클래스 정의 및 생성 - main()함수 밑에 클래스 선언
//객체 생성 [매우 특이함!!! new키워드가 존재하지 않음 ]
var obj= MyClass()
obj.show()
}//main 함수 종료...
//클래스 선언 - 자바나 C++과 기본 모습은 비슷함. 단, 멤버변수(Field)를 Property(프로퍼티) 라고 부름
class MyClass{
//멤버변수[ Property:프로퍼티] -반드시 초기화 해야함
var a:Int= 10
//메소드 : Method
fun show(){
println(" show : $a ")
println()
}
}
//**출력**
show : 10
클래스를 작성하는 위치는 어디서든 가능합니다.
이번에 별도의 파일로 class를 만들어 보겠습니다. 별도의 코틀린 파일을 만들 듯이 만들면 되지만 파일의 종류를 class로 선택하면 기본 class 구조까지 작성된 상태로 파일이 만들어 집니다.
파일명 : MyKotlinClass.kt
만들어진 KotlinClass 파일을 볼 수 있습니다. 그동안 만들었던 일반 코틀린 파일과 다르게 class 구조까지는 미리 작성된 상태로 만들어 줍니다.
이전에 소개했듯이 코틀린은 자바와 다르게 class 없이 변수와 함수를 만들고 사용할 수 있습니다. 그러다 보니 만들어진 코틀린 파일명 만으로는 클래스인지 일반 파일인지 구별되지 않습니다.
그래서 Android Studio IDE 는 개발자들의 혼동을 방지하기 위해 class 파일과 일반 파일을 목록에서 다르게 보이도록 했습니다. 이 파일이 Class라는 것을 인식하도록 C 모양의 아이콘이 있으면 클래스 파일이고 코틀린의 K 모양 아이콘이면 일반 파일입니다. 또 한가지 파일명 뒤에 .kt 확장자를 클래스는 보이지 않도록 하였습니다. 오해하지 마세요. 실제 클래스파일도 .kt 확장자 인것은 똑같습니다. 단지, 개발자다 편집기에서 파일을 쉽게 구분하여 사용하도록 해주는 편의 기능입니다.
자, 이제 새로 만든 MyKotlinClass 라는 클래스를 완성해 보겠습니다. 이름을 보시면 알겠지만 아무 의미없는 클래스 입니다. 단지 문법적인 소개를 위한 클래스이니 변수나 메소드에 특별한 의미를 두실 필요없습니다.
참고로 java언어와는 다르게 클래스 이름이 파일명과 반드시 같을 필요는 없습니다.
MyKotlinClass.kt |
//java언어와는 다르게 클래스 이름이 파일명과 반드시 같을 필요는 없음.
class MyKotlinClass {
//property -- 멤버변수
var a=10
val b=20
//method -- 멤버함수
fun show(){
println(a)
println(b)
println()
}
}
이제 main()함수가 있는 Test06OOP.kt 파일로 와서 MyKotlinClass 클래스를 객체로 생성하여 사용해 보겠습니다.
//시작함수
fun main(){
//1-1. 별도의 파일로 만든 MyKotlinClass 클래스를 객체로 만들기 [당연히 클래스파일의 확장자는 .kt]
var obj2= MyKotlinClass() //객체생성
obj2.show() //메소드 호출
}//main 함수 종료...
//**출력**
10
20
2. 생성자 Constructor
생성자가 무엇인지는 잘 알고 있으시겠죠? 아주 간단히 정리해 보겠습니다.
생성자(Constructor)란, 객체를 생성할 때 무조건 자동으로 실행되는 아주 특별한 메소드(함수)를 말합니다. 보통 객체의 멤버변수들에 대한 초기값을 설정하는 등의 초기화 코드를 작성하는데 많이 사용됩니다.
코틀린의 생성자 문법은 자바와 완전히 다릅니다. 너무 달라서 살짝 당혹스러울 정도 입니다. 하지만, 역시 익숙해지면 자바보다 훨씬 간결하고 효과적인 코딩이 가능합니다. 잘 익혀보시기 바랍니다.
코틀린의 생성자는 2가지 종류가 있습니다. 주 생성자 와 보조 생성자 입니다.
2.1) 주 생성자 ( Primary Constructor )
주 생성자는 클래스명 뒤에 생성자를 의미하는 constructor()라는 키워드로 만들 수 있습니다.
//주 생성자 [클래스명 옆에 constructor()키워드로 정의
class Simple constructor(){
}
자바처럼 클래스의 영역 중괄호{} 안에 클래스명과 같은 이름의 함수를 만드는 자바 생성자 문법과는 차이가 아주 큽니다. 생성자라는 의미의 영어표현으로 constructor 라는 키워드를 사용함으로서 개인적으로 가독성도 좋다고 생각합니다.
다만, 자바와 다르게 별도의 생성자함수 영역 중괄호{} 가 없기에 초기화 코드를 작성할 수 있는 영역이 필요하다면 초기화 블럭을 만들어주는 init{ ..} 영역을 통해 처리합니다. 이 init 초기화 블럭은 사실 주 생성자가 있을 때만 사용되는 것은 아니고 객체가 생성되면 자동으로 실행되는 자바에도 존재했던 초기화블럭입니다. 다만, 주 생성자의 파라미터(매개변수)가 있다면 이 초기화 블럭안에서 인식이 된다는 특징이 있어 보통 주 생성자의 코드영역으로 활용되기도 합니다.
잘 이해가 되지 않을 수 있으니 코드를 보며 확인해 보겠습니다.
별도의 파일을 만들어 새로운 클래스를 만들기는 다소 번거로우니 main()함수 아래 Simple 이라는 이름의 새로운 클래스를 설계해 보면서 주 생성자를 추가해 보겠습니다.
//시작함수
fun main(){
//2.1 주 생성자 [Primary Constructor]
var s= Simple()
}//main 함수 종료...
//2.1 주 생성자 [클래스명 옆에 constructor()키워드로 정의]
class Simple constructor(){
//근데 주 생성자가 별도의 메소드가 아니어서.. 코드를 작성할 수 없음.
//그래서 존재하는 초기화 블럭 키워드
init {
//주 생성자가 호출될 때 실행되는 영역
println("Simple primary constructor!!")
println()
}
}
//**출력**
Simple primary constructor!!
· 주 생성자에 값 전달 - 파라미터가 있는 주 생성자
일반적으로 생성자의 존재 목적은 객체를 생성할때 객체안에 있는 멤버변수(프로퍼티)의 초기화를 위한 경우가 많습니다. 즉, 객체를 생성하면서 멤버변수에 대입할 값을 생성자 메소드의 파라미터(매개변수)를 통해 주입해 줍니다.
이번에는 주 생성자에 파라미터를 추가하고 값을 전달해 보도록 하겠습니다.
여기서, 파라미터를 만들때 아주 중요한 부분이 있습니다.
코틀린에서는 함수 파라미터를 만들 때 변수를 만드는 키워드인 var, val 키워드를 사용하면 안됩니다. 자동으로 무조건 val 변수로 만들어 집니다. 근데. 이 주 생성자의 파라미터를 만들때는 var, val 키워드의 사용이 가능합니다. 이렇게 var, val 키워드를 추가하면 매개변수이면서 클래스의 멤버변수가 됩니다.
즉, 코틀린에서 클래스를 설계할 때는 멤버변수를 중괄호{} 안에 만드는 것이 아니라 주 생성자의 파라미터안에 var, val 키워드를 명시하면서 만듭니다. 그렇기에 자바에서 처럼 생성자안에서 매개변수의 값을 멤버변수에 대입해주는 코드가 필요없습니다. 이거 생각보다 코딩의 간결성이 엄청 좋아집니다. 참고로. 주 생성자는 클래스명 옆에 추가하는 문법이어서 오버로딩은 불가능 합니다.
//시작함수
fun main(){
//주 생성자에 파라미터 전달 [ 기본적으로 주 생성자는 오버로딩이 없음 ]
var s2= Simple2(1000)
println(s2.num) //멤버변수 확인
s2.show() //멤버함수 호출
}//main 함수 종료...
//주 생성자에 값 전달
//아주 특이하게 주 생성자의 파라미터를 만들 때 var, val을 사용하면 곧바로 프로퍼티, 즉, 멤버변수가 됨
class Simple2 constructor(var num:Int){
init {
println("Simple2 primary constructor!! : $num ") //초기화블럭에서는 당연히 주 생성자의 파라미터인 num 변수 사용가능
}
fun show(){
println("프로퍼티 num : $num ") //주 생성자의 파라미터면서 멤버변수인 num 변수 사용가능
println()
}
}
//**출력**
Simple2 primary constructor!! : 1000
1000
프로퍼티 num : 1000
주 생성자의 파라미터를 만들 때 var, val 키워드를 사용하지 않으면 초기화에만 사용가능한 일반 매개변수가 됩니다. 즉, 클래스 영역 전체에서 인식가능한 멤버변수가 아닙니다.
//시작함수
fun main(){
//주 생성자에 파라미터 전달
var s2= Simple2(1000, 20) //2개의 파라미터에 값 모두 전달
println(s2.num) //멤버변수 확인
//println(s2.num2) //ERROR- 멤버변수가 아니어서 인식 불가
s2.show()
}//main 함수 종료...
// var을 안써도 에러는 아니지만 그때의 파리미터는 단순 매개변수임. 즉, 멤버변수(property)가 아님
class Simple2 constructor(var num:Int, num2:Int){ //num2는 var 키워드가 없기에 일반 매개변수
init {
println("Simple2 primary constructor!! : $num ")
println("Simple2 primary constructor!! : $num2 ") //초기화 블럭에서는 num2매개변수 인식가능
}
fun show(){
println("프로퍼티 num : $num ")
//println("프로퍼티 num : $num2 ") //ERROR - 일반 메소드에서는 num2인식 불가
println()
}
}
//**출력**
Simple2 primary constructor!! : 1000
Simple2 primary constructor!! : 20
1000
프로퍼티 num : 1000
참고로, 주 생성자의 파라미터를 이용하지 않고 자바에서 처럼 클래스 영역안에 멤버변수를 만들수 있으며 일반 매개변수를 통해 초기화를 할 수도 있습니다.
//시작함수
fun main(){
//주 생성자에 파라미터 전달
var s2= Simple2(30)
println(s2.n) //멤버변수 확인
println(s2.n2) //멤버변수 확인
}//main 함수 종료...
class Simple2 constructor(num:Int){ //num : var 키워드가 없기에 일반 매개변수
//자바에서 처럼 멤버변수 만들기
var n:Int= 10
//프로퍼티에 주 생성자의 매개변수를 대입하여 사용하는 것은 가능함
var n2:Int= num;
}
//**출력**
10
30
2.2) 보조 생성자 ( Secondary Constructor )
보조 생성자는 자바처럼 class 영역 안에 메소드처럼 존재하며 constructor 라는 이름으로 만든 생성자 입니다.
//시작함수
fun main(){
//보조 생성자 [Secondary Constructor]
var s3= Simple3()
}//main 함수 종료...
//보조 생성자 - 자바처럼 class안에 메소드처럼 존재하는 생성자
class Simple3{
//보조 생성자
constructor(){
println("Simple3 Secondary 생성자")
println()
}
}//Simple3 class....
//**출력**
Simple3 Secondary 생성자
주 생성자가 있음에도 보조 생성자가 존재하는 이유가 무엇일까요? 바로 주 생성자로는 처리할 수 없는 오버로딩을 하기 위해서 입니다.
오버로딩 Overloading : 메소드의 이름은 같고 파라미터의 개수나 자료형이 다른 메소드
호출하려는 메소드의 이름이 같더라도 전달하는 값의 개수나 자료형이 다르면 해당 메소드를 식별할 수 있어서 자바에서도 많이 사용하는 문법입니다. 생성자도 메소드처럼 파라미터를 가질 수 있기에 당연히 오버로딩이 가능합니다. 즉, 객체를 생성할 때 전달하는 값을 여러형태로 만들어 사용할 수 있다는 것입니다. 이 글은 정식 문법수업이 아니기에 오버로딩의 사용사례 같은 내용은 생략하도록 하겠습니다.
위 Simple3 클래스에 정수형 숫자 1개를 파라미터로 전달 받는 생성자 오버로딩을 만들어 사용해 보겠습니다.
보조 생성자의 파라미터는 주 생성자와 다르게 var, val 키워드를 사용할 수 없습니다. 즉, 멤버변수(프로퍼티)면서 매개변수로의 사용이 불가능 합니다. 이것이 주 생성자와의 결정적 차이 입니다.
//시작함수
fun main(){
//보조 생성자 [Secondary Constructor]
var s3= Simple3()
//생성자 오버로딩
var s4= Simple3(100)
}//main 함수 종료...
//보조 생성자 - 자바처럼 class안에 메소드처럼 존재하는 생성자
class Simple3{
//보조 생성자
constructor(){
println("Simple3 Secondary 생성자")
println()
}
//보조 생성자는 Overloading 이 됨 [보조생성자의 파라미터에는 var로 곧바로 property를 만들 수 없음]
constructor(num: Int){
println("Simple3 Overloading Secondary 생성자 : $num ")
println()
}
}
//**출력**
Simple3 Secondary 생성자
Simple3 Overloading Secondary 생성자 : 100
참고로. 주 생성자가 없더라도 초기화 영역 init 은 사용할 수 있습니다.
//시작함수
fun main(){
//보조 생성자 [Secondary Constructor]
var s3= Simple3()
//생성자 오버로딩
var s4= Simple3(100)
}//main 함수 종료...
class Simple3{
//초기화 블럭 [물론 없어도 됨] -- 생성자보다 먼저 실행되는 영역
init {
println("이 영역은 항상 객체생성시에 초기화를 위해 처음으로 실행됨")
}
//보조 생성자
constructor(){
println("Simple3 Secondary 생성자")
println()
}
//보조 생성자는 Overloading 이 됨 [보조생성자의 파라미터에는 var로 곧바로 property를 만들 수 없음
constructor(num: Int){
println("Simple3 Overloading Secondary 생성자 : $num ")
println()
}
}
//**출력**
이 영역은 항상 객체생성시에 초기화를 위해 처음으로 실행됨
Simple3 Secondary 생성자
이 영역은 항상 객체생성시에 초기화를 위해 처음으로 실행됨
Simple3 Overloading Secondary 생성자 : 100
· 주 생성자 + 보조생성자
주 생성자를 사용한 상태에서 오버로딩을 위해 보조 생성자를 함께 사용하는 경우도 필요한 경우가 있습니다.
위 보조 생성자만을 사용한 오버로딩처럼 그냥 여러개의 constructor() 를 사용하면 되지만 한가지 아주 중요한 강제 사항이 있습니다.
오버로딩을 위해 추가된 보조 생성자는 반드시 명시적으로 주 생성자를 호출해 줘야 한다는 것이고 이때 사용하는 것이 자바에서도 많이 사용해 보셨을 this() 생성자 호출문법입니다. this는 클래스안에서 본인을 지칭하는 특별한 키워드(정확히는 참조변수) 입니다. 본인 생성자를 다시 호출한다고 하여 this() 생성자라고 부릅니다.
자바에도 존재하지만 다른점은 생성자의 중괄호 {..} 안에 작성하는 것이 아니고 constructor() 소괄호 다음에 콜론 : 후 위치해야 합니다. 일반 메소드의 리턴타입이 작성되는 위치입니다. 생성자에서 이 this()키워드를 중괄호 {..}보다 먼저 작성한 이유는 중괄호 안의 실행문보다 주 생성자의 호출이 더 먼저 된다는 것을 명시적으로 강제하게 함으로서 개발자의 실행 순서 이해를 실수하지 않게 함이라고 보여집니다.
//시작함수
fun main(){
//주 생성자와 보조 생성자를 동시에 사용할 때.. [즉, 주 생성자를 오버로딩하고 싶다면...]
var s5= Simple4() //주 생성자 호출
var s6= Simple4(1000) //보조 생성자 호출
}//main 함수 종료...
//주 생성자와 보조 생성자를 동시에 사용할 때..[즉, 주 생성자를 오버로딩하고 싶다면...]
class Simple4 constructor(){ //1. 주 생성자
init {
println("Simple4 init")
}
//2. 보조 생성자 - 주 생성자가 명시적으로 표기되어 있다면 반드시 주 생성자를 보조생성자에서 호출해야만 함. (Overloading)
// [보조생성자 뒤에 : this()]
constructor(num:Int) : this(){
println("Simple4 secondary constructor!!!!!!!")
println()
}
}
//**출력**
Simple4 init
Simple4 secondary constructor!!!!!!!
♣ 주 생성자의 construcor 키워드 생략
참고로 주 생성자의 constructor키워드는 접근제한자나 어노테이션이 없다면 생략이 가능합니다.
//시작함수
fun main(){
//참고. constructor키워드 생략가능
var s7= Simple5()
}//main 함수 종료...
class Simple5 (){ // 클래스명 Simple5 옆에 constructor 키워드 없이 소괄호 () 만 작성함
init {
//주 생성자가 호출될 때 실행되는 영역
println("Simple5 primary constructor!!")
println()
}
}
//**출력**
Simple5 primary constructor!!
다음으로 객체지향의 주요 기능 중 하나인 상속에 대해 알아보도록 하겠습니다.