[Android/Kotlin] 12 클래스와 설계
프로그래밍에서 필수적으로 사용되는 클래스에 대해서 알아보자!
1) 클래스의 기본 구조
- Kotlin에서 사용되는 클래스의 기본 구조는 다음과 같다.
class 클래스명 {
var 변수
fun 함수() {
// 코드
}
}
- 다음은 문자열을 저장할 수 있는 String 클래스의 코드를 함축해서 보여주는 것이다.
class String{
var length: Int
fun plus(other: Any){
// code
}
fun compareTo(other: String){
// code
}
}
위 예제에서 length 변수로 문자열이 길이를 알 수 있고, plus 함수는 문자열을 이어붙일 수 있다.
또한 compareTo는 문자열을 비교하는 기능을 제공한다.
2) 클래스 코드 작성하기
클래스를 만들기 위해서는 먼저 클래스의 이름을 정하고 이름 앞에 class 키워드를 붙여서 만들 수 있다.
클래스 이름 다음에는 클래스의 범위를 지정하는 중괄호 { }가 있어야 한다.
중괄호 { }를 스코프(Scope)라고 하는데, 클래스에서 사용하면 클래스 스코프라고 한다.
class 클래스이름 {
// 클래스 스코프 (class scope)
}
몇몇 예외는 존재하지만 대부분의 코드는 클래스 스코프 안에 작성된다.
작성된 클래스를 사용하기 위해서는 생성자라고 불리는 함수가 호출되어야 하는데, Kotlin은 프라이머리(Primary)와 세컨더리(Secondary) 2개의 생성자를 제공한다.
프라이머리 생성자
class Person 프라이머리 생성자() {
}
프라미어리 생성자(Primary Constructor)는 마치 클래스의 헤더처럼 사용할 수 있으며 constructor 키워드를 사용해서 정의하는데 조건에 따라 생략할 수 있다.
프라이머리 생성자도 결국은 함수이기 때문에 파라미터를 사용할 수 있다.
class Person constructor(value: String){
// code
}
- 생성자에 접근 제한자나 다른 옵션이 없다면 constructor 키워드를 생략할 수 있다.
class Person(value: String){
// code
}
프라이머리 생성자는 마치 헤더처럼 class 키워드와 같은 위치에 작성된다.
클래스의 생성자가 호출되면 init 블록의 코드가 실행되고, init 블록에서는 생성자를 통해 넘어온 파라미터에 접근할 수 있다.
class Person(value: String){
init{
Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
}
}
init 초기화 작업이 필요하지 않다면 init 블록을 작성하지 않아도 된다.
대신 파라미터로 전달된 값을 사용하기 위해서는 파라미터 앞에 변수 키워드인 val을 붙여주면 클래스 스코프 전체에서 해당 파라미터를 사용할 수 있다.
class Person(val value: String) {
fun process() {
print(value)
}
}
- 생성자 파라미터 앞에 var도 사용할 수 있지만, 읽기 전용인 val을 사용하는 것을 권장한다.
세컨더리 생성자
세컨더리 생성자(Secondary Constructor)는 constructor 키워드를 마치 함수처럼 클래스 스코프 안에 직접 작성할 수 있다.
다음과 같이 init 블록을 작성하지 않고 constructor 다음에 괄호를 붙여서 코드를 작성하면 된다.
class Person {
constructor (value: string) {
Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
}
}
- 세컨더리 생성자는 파라미터의 개수, 또는 파라미터의 타입이 다르다면 여러 개를 중복해서 만들 수 있다.
class Kotlin {
constructor (value: String){
Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
}
constructor (value: Int){
Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
}
constructor (value1: Int, value2: String){
Log.d("class", "생성자로부터 전달받은 값은 ${value1}, ${value2}입니다.")
}
}
Default 생성자
- 생성자는 작성하지 않을 경우 파라미터가 없는 프라이머리 생성자가 하나 있는 것과 동일하다.
class Student { // 생성자를 작성하지 않아도 기본 생성자가 동작합니다.
init {
// 기본 생성자가 없더라도 초기화가 필요하면 여기에 코드를 작성합니다.
}
}
3) 클래스의 사용
- 클래스의 이름에 괄호를 붙여서 클래스의 생성자를 호출한다.
- constructor 키워드를 호출하지는 않는다.
클래스명()
아무런 파라미터 없이 클래스명에 괄호를 붙여주면 생성자가 호출되면서 init 블록 안의 코드가 자동으로 실행된다.
세컨더리 생성자의 경우 init 블록이 먼저 실행되고, constructor 블록 안의 코드가 그다음 실행된다.
다음과 같이 Kotlin 클래스의 생성자를 호출한 후 생성되는 것을 인스턴스(Instance)라고 하는데, 생성된 인스턴스는 변수에 담아둘 수 있다.
var kotlin = Kotlin()
- 클래스와 인스턴스의 관계를 비교할 때 가장 많이 사용되는 예가 붕어빵 틀과 붕어빵이다.
- 여기서 붕어빵들이 클래스에 해당되고 계속 만들 수 있는 붕어빵이 인스턴스와 같다.
- 생성자에 파라미터가 있으면 값을 입력해서 호출해야 한다.
var one = Person("value")
// 또는
var two = Person(1004)
여기서 잠깐!
프로퍼티와 메서드
클래스 내부에 정의되는 변수와 함수를 멤버 변수, 멤버 함수라고 부른다.
또 다른 용어로 프로퍼티, 메서드라고도 부른다.
클래스의 변수 > 멤버 함수 > —– 프로퍼티(Property)
클래스의 함수 > 멤버 함수 > —– 메서드(Method)
클래스 안에 정의된 변수는 프로퍼티라고 하지만 함수 안에 정의된 변수는 프로퍼티가 아닌 변수(지역 변수)라고 한다.
class 클래스명 {
var 변수A // 프로퍼티: 함수 밖에 있어야 합니다.
fun 함수(){
var 변수B // 변수(또는 지역변수): 함수 안에 있어야 합니다.
}
}
- 프로퍼티와 메서드를 사용하기 위해서 다음과 같이 프로퍼티 1개와 메서드 1개를 갖는 클래스를 만든다.
class Pig {
var name: String = "Pinky"
fun printName(){
Log.d("class", "Pig의 이름은 ${name}입니다.")
}
}
- 위에서 정의한 클래스를 생성자로 다음과 같이 인스턴스화해서 변수에 담는다.
var pig = Pig()
- 인스턴스가 담긴 변수명 다음에 도트 연산자(.)를 붙여서 프로퍼티와 메서드를 사용한다.
pig.name = "Pooh"
pig.printName()
/** 실행결과
Pig의 이름은 Pooh입니다.
*/
클래스 안에 정의된 함수와 변수 사용하기
클래스를 사용한다는 것은 사실상 클래스 내부에 정의된 변수와 함수를 사용한다는 것 이다.
생성자를 통해 변수에 저장된 클래스의 인스턴스는 내부에 정의된 변수와 함수를 도트 연산자(.)로 접근할 수 있다.
4) 오브젝트
- 오브젝트(Object)를 사용하면 클래스 생성자로 인스턴스화 하지 않아도 블록 안의 프로퍼티와 메서드를 호출해서 사용할 수 있다. (Java의 static과 같은 역할)
object Pig{
var name: String = "Pinky"
fun printName() {
Log.d("class", "Pig의 이름은 ${name}입니다.")
}
}
- object 코드 블록 안의 프로퍼티와 메서드는 클래스명에 도트 연산자를 붙여서 생성자 없이 직접 호출할 수 있다.
Pig.name = "Mikey"
Pig.printName()
주의할 점은 클래스명을 그대로 사용하기 때문에 호출하는 클래스명의 첫 글자가 대문자이다.
object는 클래스와 다르게 앱 전체에 1개만 생성된다.
컴패니언 오브젝트 (companion object)
일반 클래스에 object 기능을 추가하기 위해서 사용한다.
위에서 작성한 Pig 코드를 다음과 같이 companion object 블록으로 감싸주면 생성 과정 없이 오브젝트처럼 사용할 수 있다.
class Pig {
companion object {
var name: String = "None"
fun printName(){
Log.d("class", "Pig의 이름은 ${name}입니다.")
}
}
fun walk() {
Log.d("class", "Pig가 걸어갑니다.")
}
}
- 위 Pig는 클래스로 선언했기 때문에 일반 함수인 walk()는 생성자인 Pig()를 호출한 다음 변수에 저장한 후에 사용할 수 있다.
// companion object 안의 코드 사용하기
Pig.name = "Linda"
Pig.printName() // Pig의 이름은 Linda입니다.
// companion object 밖의 코드 사용하기
val cutePig = Pig()
cutePig.walk() // Pig가 걸어갑니다.
Log 클래스의 메서드 d(), e()가 모두 object 코드 블록 안에 만들어져 있기 때문에 생성자 없이 바로 호출해서 사용할 수 있다.
5) 데이터 클래스
Kotlin은 간단한 값의 저장 용도로 데이터 클래스(data class)를 제공한다.
기본 형식은 다음과 같다.
data class 클래스명(val 파라미터1: 타입, var 파라미터2: 타입)
데이터 클래스를 정의할 때 class 앞에 data 키워드를 사용해야 하고, 생성자 파라미터 앞에 입력하는 var(또는 val) 키워드는 생략할 수 없다.
생성하는 코드는 일반 클래스와 동일하게 작성한다.
// 정의 - 주로 코드 블록(클래스 스코프)을 사용하지 않고 간단하게 작성합니다.
data class UserData(val name: String, var age: Int)
// 생성 - 일반 class의 생성자 함수를 호출하는 것과 동일합니다.
var userData = UserData("Michael", 21)
// name은 val로 선언되었기 때문에 변경 불가능합니다.
userData.name = "Sindy" // (X)
// age는 var로 선언되었기 때문에 변경 가능합니다.
userData.age = 18 // (O)
- 일반 변수 선언처럼 데이터 클래스의 파라미터를 val로 정의하면 읽기 전용이 된다.
toString() 메서드와 copy() 메서드
- 일반 클래스에서 toString() 메서드를 호출하면 인스턴스의 주소 값을 반환하지만, 데이터 클래스는 값을 반환하기 때문에 실제 값을 모니터링할 때 좋다.
Log.d("DataClass", "DataUser는 ${dataUser.toString()}")
// DataUser는 DataUser(name=Michael, age=21)
- 또 copy() 메서드로 간단하게 값을 복사할 수 있다.
var newData = dataUser.copy()
일반 클래스처럼 사용하기
- 일반 클래스와 동일하게 생성자를 호출하면 init 블록이 동작하고 메서드도 사용할 수 있다.
data class UserData(var name: String, var age: Int){
init{
Log.d("UserData", "initialized")
}
fun process(){
// 클래스와 동일하게 메서드 사용이 가능합니다.
}
} // 클래스가 생성되면 "initialized"가 출력됩니다.
- 이처럼 클래스와 사용법이 동일하지만 주로 네트워크를 통해 데이터를 주고받거나, 혹은 로컬 앱의 데이터베이스에서 데이터를 다루기 위한 용도로 사용하는 것이 데이터 클래스이다.
6) 클래스의 상속과 확장
Kotlin은 클래스의 재사용을 위해 상속을 지원한다.
상속은 클래스를 생성한 후 도트 연산자(.)를 통해 메서드와 프로퍼티를 사용하는 것처럼 클래스의 자원을 사용하는 또 다른 방법이다.
상속을 사용하면 부모 클래스의 메서드와 프로퍼티를 마치 내 클래스의 일부처럼 사용할 수 있다.
그러면 상속은 왜 사용할까요?
안드로이드에는 Activity라는 클래스가 미리 만들어져 있으며, 이 Activity 클래스 내부에는 글자를 쓰는 기능, 그림을 그리는 기능, 화면에 새로운 창을 보여주는 기능이 미리 정의되어 있다.
상속이 있기에 이러한 기능을 직접 구현하지 않고 Activity 클래스를 상속받아 약간의 코드만 추가하면 앱에 필요한 기능을 추가할 수 있다.
class Activity {
fun drawText()
fun draw()
fun showWindow()
// ...
}
class MainACtivity: Activity() {
fun onCreate(){
draw("새 그림") // 미리 만들어진 기능(draw)을 호출만으로 사용할 수 있습니다.
}
}
- 상속은 코드를 재사용하는 측면도 있지만 코드를 체계적으로 관리할 수 있기 때문에 규모가 큰 프로텍트도 효과적으로 설계할 수 있다.
클래스의 상속
상속 대상이 되는 부모 클래스는 open 키워드로 만들어야만 자식 클래스에서 사용할 수 있다.
만약 open 키워드로 열려 있지 않으면 상속할 수 없다.
상속을 받을 자식 클래스에서는 콜론(:)을 이용해서 상속할 부모 클래스를 지정한다.
open class 부모 클래스(value: String) { // 코드 }
class 자식 클래스(value: String): 부모 클래스(value) { // 코드 }
상속은 부모의 인스턴스를 자식이 갖는 과정이기 때문에 부모 클래스명 다음에 괄호를 입력해서 꼭 부모의 생성자를 호출해야 한다.
open class 상속될 부모 클래스 {
// 코드
}
class 자식 클래스: 부모 클래스() {
// 코드
}
생성자 파라미터가 있는 클래스의 상속
- 상속될 부모 클래스의 생성자에 파라미터가 있다면 자식 클래스의 생성자를 통해 값을 전달할 수 있다.
open class 부모 클래스(value: String) {
// 코드
}
class 자식 클래스(value: String): 부모 클래스(value) {
// 코드
}
부모 클래스에 세컨더리 생성자가 있다면, 역시 자식 클래스의 세컨더리 생성자에서 super 키워드로 부모 클래스에 전달할 수 있다.
다음은 안드로이드의 View 클래스를 상속받는 예제이다. 자식 클래스에 세컨더리 생성자만 있을 경우 상속되는 클래스 이름 다음에 괄호가 생략된다.
class CustomView: View { // 부모 클래스명 다음 괄호를 생략했습니다.
constuctor(ctx: Context): super(ctx)
constructor(ctx: Context, attrs: AttributeSet): super(ctx, attrs)
}
부모 클래스의 프로퍼티와 메서드 사용하기
- 부모 클래스에서 정의된 프로퍼티와 메서드를 내 것처럼 사용할 수 있다.
open class Parent {
var hello: String = "안녕하세요"
fun sayHello(){
Log.d("inheritance", "${hello}")
}
}
class Child: Parent(){
fun myHello() {
hello = "Hello!"
sayHello()
}
}
- 위 코드에서 Child에는 hello라는 프로퍼티와 sayHello라는 메서드가 없지만 myHello() 메서드를 실행하면 로그에 “Hello!”가 출력된다.
프로퍼티와 메서드이 재정의: 오버라이드
상속받은 부모 클래스의 프로퍼티와 메서드 중에 자식 클래스에서는 다른 용도로 사용해야 하는 경우가 있습니다.
앞의 예제에서 Parent 클래스의 메서드를 sayHello로, Child 클래스의 메서드를 myHello라 했는데 이런 경우가 오버라이드(Override)가 필요한 대표적인 경우입니다.
오버라이드로 Child 클래스의 메서드도 sayHello라고 하는 것이 의미상 더 적합합니다.
이처럼 동일한 이름의 메서드나 프로퍼티를 사용할 필요가 있을 경우에 override 키워드를 사용해서 재정의할 수 있습니다.
오버라이드할 때는 프로퍼티나 메서드도 클래스처럼 앞에 open을 붙여서 상속할 준비가 되어 있어야 합니다.
메서드 오버라이드
- 상속할 메서드 앞에 open 키워드를 붙이면 오버라이드할 수 있지만, open 키워드가 없는 메서드는 오버라이드할 수 없다.
open class BaseClass {
open fun opened() {
}
fun notOpened(){
}
}
class ChildClass: BaseClass(){
override fun opened(){
}
override fun notOpened(){ // notOpened 메서드는 open 키워드가 없으므로 잘못된 사용입니다.
}
}
- 클래스의 세컨더리 생성자를 여러 개 중복해서 사용할 수 있는 것도 오버라이딩이 가능하기 때문이다.
프로퍼티 오버라이드
- 메서드 오버라이드처럼 프로퍼티 역시 open으로 열려 있어야만 오버라이드할 수 있다.
open class BaseClass2 {
open var opened: String = "I am"
}
class ChildClass2: BaseClass2(){
override var opened: String = "You are"
}
익스텐션
Kotlin은 클래스, 메서드, 프로퍼티에 대해 익스텐션(Extension)을 지원한다.
이미 만들어져 있는 클래스에 다음과 같은 형태로 메서드를 추가할 수 있다.
fun 클래스.확장할 메서드(){
// 코드
}
상속이 미리 만들어져 있는 클래스를 가져다 쓰는 개념이라면 익스텐션은 미리 만들어져 있는 클래스에 메서드를 넣는 개념이다.
자신이 만든 클래스에 사용하기보다는 누군가 작성해둔, 이미 컴파일되어 있는 클래스에 메서드를 추가하기 위한 용도로 사용하는 것이 좋다.
익스텐션을 사용한다고 해서 실제 클래스의 코드가 변경되는 것은 아니며 단지 실행 시에 도트 연산자로 호출해서 사용할 수 있도록 해준다.
특별한 경우를 제외하고는 거의 메서드 확장 용도로 사용된다.
다음 예제는 기본 클래스인 String에 plus 메서드를 확장하는 전체 코드이다. test 메서드 안에 선언한 original에 문자열을 입력했기 때문에 original은 String의 익스텐션 메서드인 plus를 호출해서 사용할 수 있다.
package net.flow9.thisiskotlin.basicsyntax
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testStringExtension()
}
// String 익스텐션 테스트 하기
fun testStringExtension() {
var original = "Hello"
var added = " Guys~"
// plus 함수를 사용해서 문자열을 더할 수 있다.
Log.d("Extension", " added를 더한값은 ${original.plus(added)}입니다")
}
}
fun String.plus(word: String): String {
return this + word
}
/** [로그캣 출력 내용]
added를 더한 값은 Hello Guys~입니다.
*/
- 이어서 클래스의 상속과 확장을 코드 하나로 살펴보겠다. 다음 코드를 실행해보자!
package net.flow9.thisiskotlin.basicsyntax
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 1. 부모 클래스 직접 호출하기
var parent = Parent()
parent.sayHello()
// 1. 자식 클래스 호출해서 사용하기
var child = Child()
child.myHello()
testStringExtension()
}
// String 익스텐션 테스트 하기
fun testStringExtension() {
var original = "Hello"
var added = " Guys~"
// plus 함수를 사용해서 문자열을 더할 수 있다.
Log.d("Extension", " added를 더한값은 ${original.plus(added)}입니다")
}
}
// 상속 연습
open class Parent {
var hello: String = "안녕하세요"
fun sayHello() {
Log.d("inheritance", "${hello}")
}
}
class Child : Parent() {
fun myHello() {
hello = "Hello"
sayHello()
}
}
// 메서드 오버라이드 연습
open class BaseClass {
open fun opened() {
}
fun notOpend() {
}
}
class ChildClass : BaseClass() {
override fun opened() {
}
// override fun notOpend(){ // 오버라이드 되지 않고 에러가 발생한다.
//
// }
}
// 프로퍼티 오버라이드 연습
open class BaseClass2 {
open var opened: String = "I am"
}
class ChildClass2 : BaseClass2() {
override var opened: String = "You are"
}
fun String.plus(word: String): String {
return this + word
}
/** [로그캣 출력 내용]
안녕하세요.
Hello
added를 더한 값은 Hello Guys~입니다.
*/
7) 설계 도구
객체지향 프로그래밍은 구현(실제 로직을 갖는 코딩)과 설계(껍데기만 있는 코딩)로 구분할 수 있다.
지금까지는 모두 구현에 중점을 둔 기법을 살펴봤다.
이번에는 프로그래밍 설계에 사용하는 설계 도구에 대해 알아보겠다.
설계 기법은 굉장히 방대한데 그 중 꼭 포함되는 기본 내용 중에 필요한 몇 가지만 알아보겠다.
패키지
패키지(Package)를 구현과 설계 중 어디에 포함할지 고민했는데 컴퓨터 언어에서의 패키지 사용 목적이 설계라고 볼 수 있다고 생각해서 설계 부분에서 다룬다.
코딩하면서 파일을 분류하고, 이름을 짓고, 특정 디렉터리에 모아 놓는 것이 모두 설계이다.
패키지는 클래스와 소스 파일을 관리하기 위한 디렉터리 구조의 저장 공간이다.
다음과 같이 현재 클래스가 어떤 패키지(디렉터리)에 있는지 표시한다.
디렉터리가 계층 구조로 만들어져 있으면 온점(.)으로 구분해서 각 디렉터리를 모두 나열해준다.
package 메인 디렉터리.서브 디렉터리
class 클래스 {
}
추상화
프로그래밍을 하기 전 개념 설계를 하는 단계에서는 클래스의 이름과 클래스 안에 있음 직한 기능을 유추해서 메서드 이름으로 먼저 나열한다.
이 때 명확한 코드는 설계 단계에서 메서드 블록 안에 직접 코드를 작성하는데, 그렇지 않은 경우에는 구현 단계에서 코드를 작성하도록 메서드의 이름만 작성한다.
이것을 추상화(Abstract)라고 하며 abstract 키워드를 사용해서 명시한다.
구현 단계에서는 추상화된 클래스를 상속받아서 아직 구현되지 않은 부분을 마저 구현한다.
abstract class Design {
abstract fun drawText()
abstract fun draw()
fun showWindow() {
// code
}
}
class Implements: Design(){
fun drawText() {
// 구현 코드
}
fun draw() {
// 구현 코드
}
}
- 다음과 같이 추상화된 Animal 클래스를 만들고 동물이 사용할 것 같은 기능 중에 walk와 move를 설계한다고 가정해보자.
abstract class Animal {
fun walk(){
Log.d("abstract", "걷습니다.")
}
abstract fun move()
}
walk는 명확하게 걸어가는 행위이지만 move는 어떤 동물이냐에 따라서 달라질 수 있다.
예를 들어 새는 날아가겠지만 고래는 수영을 하지않는가?
이렇게 앞으로 상속받을 자식 클래스의 특징에 따라 코드가 결정될 가능성이 있다면 해당 기능도 모두 abstract 키워드로 추상화한다.
그리고 실제 구현 클래스는 이 추상 클래스를 상속받아서 아직 구현되지 않은 추상화되어 있는 기능을 모두 구현해준다.
추상 클래스는 독립적으로 인스턴스화 할 수 없기 때문에 구현 단계가 고려되지 않는다면 잘못된 설계가 될 수 있다.
class Bird: Animal(){
override fun move(){
Log.d("abstract", "날아서 이동합니다.")
}
}
위에서 잠간 안드로이드의 Activity 클래스를 언급했는데, Activity도 수많은 클래스를 상속받아 만들어진다.
이 Activity가 상솓받는 클래스 중에 최상위에 Context라는 클래스가 있는데, 최상위 클래스인 Context가 바로 abstract로 설계되어 있다.
인터페이스
인터페이스(Interface)는 추상화와 비교하면 가장 명확하게 이해할 수 있는데 실행 코드 없이 메서드 이름만 가진 추상 클래스라고 생각해도 된다.
즉, 누군가 설계해 놓은 개념 클래스 중에 실행 코드가 한 줄이라도 있으면 추상화, 코드 없이 메서드 이름만 나열되어 있으면 인터페이스이다.
인터페이스는 상속 관계의 설계보다는 외부 모듈에서 내가 만든 모듈을 사용할 수 있도록 메서드의 이름을 나열해둔 일종의 명세서로 제공된다.
인터페이스는 interface 예약어를 사용해서 정의할 수 있고 인터페이스에 정의된 메서드를 오버라이드해서 구현할 수 있다.
Kotlin은 프로퍼티도 인터페이스 내부에 정의할 수 있는데, 대부분의 객체지향 언어에서는 지원하지 않는다.
추상 클래스와 다르게 class 키워드는 사용되지 않는다.
interface 인터페이스명 {
var 변수: String
fun 메서드1()
fun 메서드2()
}
인터페이스 만들기
interface 예약어로 인터페이스를 정의한다.
Kotlin은 인터페이스 내부에 프로퍼티도 정의할 수 있다.
메서드는 코드 블록 없이 이름만 작성해 놓는다.
인터페이스의 프로퍼티와 메서드 앞에는 abstract 키워드가 생략된 형태이다.
interface InterfaceKotlin{
var variable: String // var 앞에 abstract 키워드가 생략되어 있습니다.
fun get()
fun set()
}
클래스에서 구현하기
- 인터페이스를 클래스에서 구현할 때는 상속돠는 다르게 생성자를 호출하지 않고 인터페이스 이름만 지정해주면 된다.
class KotlinImpl: InterfaceKotlin {
override var variable: String = "init value"
override fun get() {
// code
}
override fun set() {
// code
}
}
인터페이스를 클래스의 상속 형태가 아닌 소스 코드에서 직접 구현할 때도 있는데, object 키워드를 사용해서 구현해야 한다.
실제로 안드로이드 프로젝트를 시작하면 자주 사용하는 형태이다.
var kotlinImpl = object: InterfaceKotlin {
override var variable: String = "init value"
override fun get() {
// code
}
override fun set() {
// code
}
}
여기서 잠깐!
인터페이스는 외부의 다른 모듈을 위한 의사소통 방식을 정의하는 것이다.
혼자 개발하거나 소수의 인원이 하나의 모듈 단위를 개발할 때는 인터페이스를 사용하지 않는 것이 좋다.
인터페이스를 남용하면 코드의 가독성과 구현 효율성이 떨어지기 때문이다.
안드로이드가 제공하는 인터페이스를 자주 사용하는 이유는 안드로이드가 보았을 때 개발자가 만드는 모듈이 외부 모듈이기 때문이다.
접근 제한자
Kotlin에서 정의되는 클래스, 인터페이스, 메서드, 프로퍼티는 모두 접근 제한자(Visibility Modifiers)를 가질 수 있다.
함수형 언어라는 특성 때문에 코틀린은 기존 객체지향에서 접근 제한자의 기준으로 삼았던 패키지 대신에 모듈 개념이 도입되었다.
internal 접근 제한자로 모듈 간에 접근을 제한할 수 있다.
접근 제한자의 종류
- 접근 제한자는 서로 다른 파일에게 자신에 대한 접근 권한을 제공하는 것인데 각 변수나 클래스 이름 앞에 아무런 예약어를 붙이지 않았을 때는 기본적으로 public 접근 제한자가 적용된다.
접근 제한자 | 제한 범위 |
---|---|
private | 다른 파일에서 접근할 수 없다. |
internal | 같은 모듈에 있는 파일만 접근할 수 있다. |
protected | private와 같으나 상속 관계에서 자식 클래스가 접근할 수 있다. |
public | 제한 없이 모든 파일에서 접근할 수 있다. |
여기서 잠깐!
Kotlin에서 모듈이란 한 번에 같이 컴파일되는 모든 파일을 말한다.
안드로이드를 예로 든다면 하나의 앱이 하나의 모듈이 될 수 있다.
또한 라이브러리도 하나의 모듈이다.
접근 제한자의 적용
접근 제한자를 붙이면 해당 클래스, 멤버 프로퍼티 또는 메서드에 대한 사용이 제한된다.
다음 코드를 통해서 접근 제한자가 어떻게 작용하는지 알아보겠다. (다양한 접근 제한자를 갖는 부모 클래스를 하나 생성하자.)
open class Parent {
private val privateVal = 1
protected open val protectedVal = 2
internal val internalVal = 3
val defaultVal = 4
}
- 자식 클래스에서 부모 클래스를 상속받고 테스트한다.
class Child: Parent() {
fun callVariables() {
// privateVal은 호출이 안 됩니다. (1)
Log.d("Modifier", "protected 변수의 값은 ${protectedVal}"). // (2)
Log.d("Modifier", "internal 변수의 값은 ${internalVal}"). // (3)
Log.d("Modifier", "기본 제한자 변수 defaultVal의 값은 ${defaultVal}"). // (4)
}
}
[위 코드 설명]
(1) privateVal은 private 멤버이기 때문에 접근할 수 없다.
(2) protected 멤버 protectedVal은 상속 관계이므로 접근할 수 있다.
(3) internal 멤버 internalVal은 동일한 모듈이므로 접근할 수 있다.
(4) 접근 제한자가 없는 멤버 defaultVal에는 public이 적용되어 접근할 수 있다.
- 상속 관계가 아닌 외부 클래스에서 Parent 클래스를 생성하고 사용해보자. 상속 관계가 아니기 때문에 public과 internal에만 접근할 수 있다.
class Stranger {
fun callVariables() {
val parent = Parent()
Log.d("Modifier", "internal 변수의 값은 ${parent.internalVal}입니다.")
Log.d("Modifier", "public 변수의 값은 ${parent.defaultVal}입니다.")
}
}
제네릭
제네릭(Generic)은 입력되는 값의 타입을 자유롭게 사용하기 위한 설계 도구이다.
다음은 자주 사용되는 MutableList 클래스의 원본 코드를 이해하기 쉽게 변형한 코드이다.
public interface MutableList<E> {
var list: Array<E>
// ...
}
클래스명 옆에
라고 되어 있는 부분에 String과 같은 특정 타입이 지정되면 클래스 내부에 선언된 모든 E에 String이 타입으로 지정된다. 결과적으로 var list: Array
가 var list: ARray 으로 변경되는 것이다. 이렇게 설계된 클래스를 우리는 주로 구현하는 용도로 사용하며 컬렉션이나 배열에서 입력되는 값의 타입을 특정하기 위해 다음과 같이 사용된다.
var list: MutableList<Generic> = mutableListOf("Mon", "Tue", "Wed")
fun testGenerics() {
// String을 제네릭으로 사용했기 때문에 list 변수에는 문자열만 담을 수 있습니다.
var list: MutableList<String> = mutableListOf()
list.add("Mon")
list.add("Tue")
list.add("Wed")
// list.add(35) <- 입력 오류 발생합니다.
// String 타입의 item 변수로 꺼내서 사용할 수 있습니다.
for (item in list){
Log.d("Generic", "list에 입력된 값은 ${item}입니다.")
}
}
- 지금까지 배운 내용을 코드로 살펴보겠다!
package net.flow9.thisiskotlin.basicsyntax
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 접근제한자 테스트
var child = Child()
child.callVariables()
// 부모클래스 직접 호출해보기
var parent = Parent()
Log.d("Visibility", "Parent : protected 변수의 값은 ${parent.defaultVal}")
Log.d("Visibility", "Parent : protected 변수의 값은 ${parent.internalVal}")
}
}
// 추상 클래스 설계
abstract class Animal {
fun walk() {
Log.d("abstract", "걷습니다")
}
abstract fun move()
}
// 구현
class Bird : Animal() {
override fun move() {
Log.d("abstract", "날아서 이동합니다")
}
}
// 인터페이스 설계
interface InterfaceKotlin {
var variable: String
fun get()
fun set()
}
// 구현
class KotlinImpl : InterfaceKotlin {
override var variable: String = "init value"
override fun get() {
// 코드 구현
}
override fun set() {
// 코드 구현
}
}
// 접근제한자 테스트를 위한 부모 클래스
open class Parent {
private val privateVal = 1
protected open val protectedVal = 2
internal val internalVal = 3
val defaultVal = 4
}
// 자식 클래스
class Child : Parent() {
fun callVariables() {
// privateVal은 호출이 안된다
Log.d("Visibility", "Child : protected 변수의 값은 ${protectedVal}")
Log.d("Visibility", "Child : internal 변수의 값은 ${internalVal}")
Log.d("Visibility", "Child : 기본제한자 변수 defaultVal의 값은 ${defaultVal}")
}
}
/** [로그캣 출력 내용]
Child : protected 변수의 값은 2
Child : internal 변수의 값은 3
Child : 기본제한자 변수 defaultVal의 값은 4
Parent : protected 변수의 값은 4
Parent : protected 변수의 값은 3
*/
[다음 요약 내용을 한 번 이상 읽어본 후에 다음 내용을 학습하자!]
클래스(class): 변수와 함수의 모음으로, 연관성 있는 코드를 그룹화하고 이름을 매긴 것이다.
constructor: 클래스를 사용하기 위해서 호출하는 일종의 함수이다.
init: 기본 생성자를 호출하면 실행되는 코드 블록이다.
프로퍼티(property): 클래스에 정의된 변수를 프로퍼티 또는 멤버 변수라고 한다.
메서드(method): 클래스에 정의된 함수를 메서드 또는 멤버 함수라고 한다.
컴패니언 오브젝트(comapnion object): 컴패니언 오브젝트의 블록 안에서 변수와 함수를 정의하면 생성자를 통하지 않고 클래스의 멤버들을 사용할 수 있다.
상속: 코드를 재사용하기 위한 설계 도구이다. 상속 관계에서 자식 클래스는 부모 클래스의 멤버들을 자신의 것처럼 사용할 수 있다.
추상화(abstract): 클래스를 개념 설계하기 위한 도구이다.
인터페이스(interface): 외부 모듈에 제공하기 위해 메서드 이름을 나열한 명세서이다.
패키지(package): 연관성 있는 클래스들을 분류하기 위한 디렉터리 구조이다.
접근 제한자: 클래스의 멤버에 지정된 접근 제한자에 따라 외부에서 사용 여부가 결정된다.
제네릭(generic): 타입을 특정해서 안정성을 유지하기 위한 설계 도구이다.