- 지금까지 공부한 내용 안에서 클래스를 정리하면, 클래스는 단지 변수와 함수의 모음이다!
- 그룹화 할 수 있는 함수와 변수를 한군데에 모아놓고, 사용하기 쉽게 이름을 붙여놓은 것을 "클래스" 라고 이해하면 쉬울것이다!
# 클래스의 기본 구조
- Kotlin에서 사용되는 클래스의 기본 구조는 다음과 같다
class 클래스명 {
var 변수
fun 함수() {
// 코드
}
}
다음은 문자열을 저장할 수 있는 String 클래스의 코드를 함축해서 보여주는 코드다
class String{
var length: Int
fun plus(other: Any){
// code
}
fun compareTo(other: String){
// code
}
}
-> length로 문자열의 길이를 알 수 있고, plus로는 문자열을 이어붙일 수 있다. 또한 compareTo는 문자열을 비교하는 기능을 제공한다
# 클래스 코드 작성하기
- 클래스를 만들기 위해서는 먼저 클래스의 이름을 정하고, 이름 앞에 class 키워드를 붙여서 만들 수 있다
- 클래스 이름 다음에는 클래스의 범위를 지정하는 중괄호 { } 가 있어야 한다
- 중괄호 { } 를 스코프(Scope)라고 하는데, 클래스에서 사용하면 "클래스 스코프" 라고 한다
class 클래스이름 {
// 클래스 스코프 (class scope)
}
-> 몇몇 예외는 존재하지만, 대부분의 코드는 클래스 스코프 안에 작성된다
-> 작성된 클래스를 사용하기 위해서는 생성자라고 불리는 함수가 호출되어야하는데, kotlin은 프라이머리(Primary)와 세컨더리(Secondary) 2개의 생성자를 제공한다
# 프라이머리 생성자
class Person 프라이머리 생성자() {
}
class Person constructor(value: String){
// code
}
-> 프라이머리 생성자(Primary Constructor)는 마치 클래스의 헤더처럼 사용할 수 있으며,
Constructor 키워드를 사용해서 정의하는데 조건에 따라 생략할 수 있다
-> 프라이머리 생성자도 결국은 함수이기 때문에 파라미터를 사용할 수 있다
- 파라미터(Parameter) : 입력될 값을 기술한 것
- 예제에서는 value부분이 파라미터에 해당
class Person(value: String){
// code
}
-> 생성자에 접근 제한자나 다른 옵션이 없다면, Constructor 키워드를 생략할 수 있다
class Person(value: String){
init{
Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
}
}
-> 프라이머리 생성자는 마치 헤더처럼 class 키워드와 같은 위치에 작성된다
-> 클래스의 생성자가 호출되면 init블록의 코드가 실행되고, init블록에서는 생성자를 통해 넘어온 파라미터에 접근할 수 있다
class Person(val value: String) {
fun process() {
print(value)
}
}
-> init 초기화 작업이 필요하지 않다면, init 블록을 작성하지 않아도 된다
-> 대신 파라미터로 전달된 값을 사용하기 위해서는, 파라미터 앞에 변수키워드인 val을 붙여주면, 클래스 스코프 전체에서 해당 파라미터를 사용할 수 있다
-> 생성자 파라미터 앞에 var도 사용할 수 있지만, 이땐 읽기 전용인 val을 사용하는 것을 권장한다
# 세컨더리 생성자
- 세컨더리 생성자(Secondary Constructor)는 constructor 키워드를 마치 함수처럼 클래스 스코프 안에 직접 작성할 수 있다
class Person {
constructor (value: string) {
Log.d("class", "생성자로부터 전달받은 값은 ${value}입니다.")
}
}
-> init 블록을 작성하지 않고, constructor 다음에 괄호를 붙여서 코드를 작성할 수 있다
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 {
// 기본 생성자가 없더라도 초기화가 필요하면 여기에 코드를 작성합니다.
}
}
# 클래스의 사용 (호출)
- 클래스의 이름에 괄호를 붙여서 클래스의 생성자를 호출한다
- constructor 키워드를 호출하지는 않는다
클래스명()
-> 아무런 파라미터 없이 클래스명에 괄호를 붙여주면, 생성자가 호출되면서 init 블록 안의 코드가 자동으로 실행된다
-> 세컨더리 생성자의 경우 init블록이 먼저 실행되고, constructor블록 안의 코드가 그 다음에 실행된다
var kotlin = Kotlin()
-> 위와 같이 kotlin 클래스의 생성자를 호출한후 생성되는것을 인스턴스(Instance)라고 하는데, 생성된 인스턴스는 변수에 담아둘 수 있다
- 클래스와 인스턴스의 관계를 비교할 때 붕어빵틀과 붕어빵으로 예를 들어서 설명할 수 있다
- 여기서 붕어빵틀이 클래스에 해당되고, 계속 만들 수 있는 붕어빵이 인스턴스와 같다
var one = Person("value")
// 또는
var two = Person(1004)
-> 생성자에 파라미터가 있으면, 값을 입력해서 호출해야한다
# 여기서 잠깐!!
- 프로퍼티와 메서드란??
- 클래스 내부에 정의되는 변수와 함수를 멤버변수, 멤버함수 라고 부른다
- 또 다른 용어로는 프로퍼티, 메서드라고 부르기도 한다
- 클래스 내부에 있는 변수 -> 멤버 변수 -> 프로퍼티(Property)
- 클래스 내부에 있는 함수 -> 멤버 함수 -> 메서드(Method)
- 클래스 안에 정의된 변수는 프로퍼티라고 하지만, 함수 안에 정의된 변수는 프로퍼티가 아닌 지역변수 라고 한다
class 클래스명 {
var 변수A // 프로퍼티: 함수 밖에 있어야 합니다.
fun 함수(){
var 변수B // 변수(또는 지역변수): 함수 안에 있어야 합니다.
}
}
class Pig {
var name: String = "Pinky"
fun printName(){
Log.d("class", "Pig의 이름은 ${name}입니다.")
}
}
-> 프로퍼티와 메서드를 사용하기 위해서 위와같이 프로퍼티 1개와 메서드 1개를 갖는 클래스를 만든다
var pig = Pig()
-> 위에서 정의한 클래스를 생성자로 다음과 같이 인스턴스화해서 변수에 담는다
pig.name = "Pooh"
pig.printName()
/** 실행결과
Pig의 이름은 Pooh입니다.
*/
-> 인스턴스가 담긴 변수명 다음에 도트 연산자(.)를 붙여서 프로퍼티와 메서드를 사용한다
# 클래스 안에 정의된 함수와 변수 사용하기
- 클래스를 사용한다는것은 사실상 클래스 내부에 정의된 변수와 함수를 사용한다는 것이다
- 생성자를 통해 변수에 저장된 클래스의 인스턴스는 내부에 정의된 변수와 함수를 도트연산자(.)로 접근할 수 있다
# 오브젝트
- 오브젝트(Object)를 사용하면, 클래스 생성자로 인스턴스화 하지 않아도, 블록 안의 프로퍼티와 메서드를 호출해서 사용할 수 있다
object Pig{
var name: String = "Pinky"
fun printName() {
Log.d("class", "Pig의 이름은 ${name}입니다.")
}
}
Pig.name = "Mikey"
Pig.printName()
-> object 코드블록 안의 프로퍼티와 메서드는 클래스명에 도트연산자를 붙여서 생성자 없이 직접 호출할 수 있다
-> 주의할 점은 클래스명을 그대로 사용하기 때문에, 호출하는 클래스의 첫글자가 대문자이다
-> 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가 걸어갑니다.
# 데이터 클래스
- Kotlin은 간단한 값의 저장용도로 데이터클래스(data class)를 제공한다
- 기본형식은 아래와 같다
data class 클래스명(val 파라미터1: 타입, var 파라미터2: 타입)
// 정의 - 주로 코드 블록(클래스 스코프)을 사용하지 않고 간단하게 작성합니다.
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)
-> 데이터 클래스를 정의할 때 class앞에 data키워드를 사용해야하고, 생성자 파라미터 앞에 입력하는 var(또는 val) 키워드는 생략할 수 없다
-> 생성하는 코드는 일반 클래스와 동일하게 작성한다
-> 일반 변수 선언처럼 데이터 클래스의 파라미터를 val로 정의하면 읽기전용이 된다
# toString()메서드와 copy()메서드
Log.d("DataClass", "DataUser는 ${dataUser.toString()}")
// DataUser는 DataUser(name=Michael, age=21)
-> 일반 클래스에서 toString() 메서드를 호출하면 인스턴스의 주소값을 반환하지만, 데이터 클래스는 값을 반환하기 때문에 실제값을 모니터링할때 좋다
var newData = dataUser.copy()
-> copy() 메서드로는 간단하게 값을 복사할 수 있다
# 일반 클래스처럼 사용하기
data class UserData(var name: String, var age: Int){
init{
Log.d("UserData", "initialized")
}
fun process(){
// 클래스와 동일하게 메서드 사용이 가능합니다.
}
} // 클래스가 생성되면 "initialized"가 출력됩니다.
-> 일반 클래스와 동일하게 생성자를 호출하면, init 블록이 동작하고 메서드도 사용할 수 있다
- 이처럼 클래스와 사용법이 동일하지만, 주로 네트워크를 통해 데이터를 주고받거나, 혹은 로컬 앱의 데이터베이스에서 데이터를 다루기 위한 용도로 사용하는 것이 "데이터 클래스" 다
# 클래스의 상속과 확장
- Kotlin은 클래스의 재사용을 위해 상속을 지원한다
- 상속은 클래스를 생성한 후, 도트 연산자(.) 를 통해 메서드와 프로퍼티를 사용하는 것처럼, 클래스의 자원을 사용하는 또다른 방법이다
- 상속을 사용하면 부모클래스의 메서드와 프로퍼티를, 마치 내 클래스의 일부처럼 사용할 수 있다
class Activity {
fun drawText()
fun draw()
fun showWindow()
// ...
}
class MainACtivity: Activity() {
fun onCreate(){
draw("새 그림") // 미리 만들어진 기능(draw)을 호출만으로 사용할 수 있습니다.
}
}
- 그러면 상속은 왜 사용할까??
- 안드로이드에는 Activity라는 클래스가 미리 만들어져있으며, 이 Activity클래스 내부에는 글자를 쓰는 기능, 그림 그리는 기능, 화면에 새로운 창을 보여주는 기능이 미리 정의되어있다
- 상속이 있으면, 이러한 기능을 직접 구현하지 않고, Activity클래스를 상속받아 약간의 코드만 추가하면 앱에 필요한 기능을 추가할 수 있기 때문이다
- 상속은 코드를 재사용하는 측면도 있지만, 코드를 체계적으로 관리할 수 있기 때문에 규모가 큰 프로젝트도 효과적으로 설계할 수 있다!!
# 클래스의 상속
open class 상속될 부모 클래스 {
// 코드
}
class 자식 클래스: 부모 클래스() {
// 코드
}
-> 상속 대상이되는 부모 클래스는, open키워드로 만들어야만 자식 클래스에서 사용할 수 있다
-> 만약 open 키워드로 열려있지 않으면 상속할 수 없다
-> 상속을 받을 자식 클래스에서는 콜론(:)을 이용해서 상속할 부모 클래스를 지정한다
-> 상속은 부모의 인스턴스를 자식이 갖는 과정이기 때문에, 꼭 부모 클래스명 다음에 괄호를 입력해서 부모의 생성자를 호출해야한다
# 생성자 파라미터가 있는 클래스의 상속
open class 부모 클래스(value: String) {
// 코드
}
class 자식 클래스(value: String): 부모 클래스(value) {
// 코드
}
-> 상속될 부모 클래스의 생성자에 따라, 파라미터가 있다면 자식클래스에 생성자를 통해 값을 전달할 수 있다
class CustomView: View { // 부모 클래스명 다음 괄호를 생략했습니다.
constuctor(ctx: Context): super(ctx)
constructor(ctx: Context, attrs: AttributeSet): super(ctx, attrs)
}
-> 부모 클래스에 세컨더리 생성자가 있다면, 역시 자식 클래스의 세컨더리 생성자에서 super키워드로 부모 클래스에 전달할 수 있다
-> 자식클래스에 세컨더리 생성자만 있을경우, 상속되는 클래스 이름 다음에 괄호가 생략된다
# 부모 클래스의 프로퍼티와 메서드 사용하기
- 부모 클래스에서 정의된 프로퍼티와 메서드를 마치 내것처럼 사용할 수 있다
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 class BaseClass {
open fun opened() {
}
fun notOpened(){
}
}
class ChildClass: BaseClass(){
override fun opened(){
}
override fun notOpened(){ // notOpened 메서드는 open 키워드가 없으므로 잘못된 사용입니다.
}
}
-> 상속할 메서드 앞에 open 키워드를 붙이면 오버라이드 할 수 있지만, open 키워드가 없는 메서드는 오버라이드 할 수 없다
-> 클래스의 세컨더리 생성자를 여러개 중복해서 사용할 수 있는것도 오버라이딩이 가능하기 때문이다
# 프로퍼티 오버라이드
open class BaseClass2 {
open var opened: String = "I am"
}
class ChildClass2: BaseClass2(){
override var opened: String = "You are"
}
-> 메서드 오버라이드처럼 프로퍼티 역시 open으로 열려있어야만 오버라이드가 가능하다
# 익스텐션
- 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~입니다.
*/
# 설계도
- 객체지향 프로그래밍은 구현(실제 로직을 갖는 코딩)과 설계(껍데기만 있는 코딩)으로 구분할 수 있다
- 지금까지는 모두 구현에 중점을 둔 기법을 살펴봤는데, 이번에는 프로그래밍 설계에 사용하는 설계도구에 대해 알아볼것이다
- 설계 기법은 굉장히 방대한데 그 중 꼭 포함되는 기본 내용 중에 필요한 몇 가지만 알아보겠다
# 패키지
- 코딩하면서 파일을 분류하고, 이름을 짓고, 특정 디렉터리에 모아 놓는 것이 모두 설계다
- 패키지는 클래스와 소스 파일을 관리하기 위한 디렉터리 구조의 저장 공간이다
- 다음과 같이 현재 클래스가 어떤 패키지(디렉터리)에 있는지 표시한다
- 디렉터리가 계층 구조로 만들어져 있으면 온점(.)으로 구분해서 각 디렉터리를 모두 나열해준다
package 메인 디렉터리.서브 디렉터리
class 클래스 {
}
# 추상화
- 프로그래밍을 하기 전 개념 설계를 하는 단계에서는, 클래스의 이름과 클래스 안에 있음 직한 기능을 유추해서 메서드 이름으로 먼저 나열한다
- 이 때 명확한 코드는 설계 단계에서 메서드 블록 안에 직접 코드를 작성하는데, 그렇지 않은 경우에는 구현 단계에서 코드를 작성하도록 메서드의 이름만 작성한다
- 이것을 추상화(Abstract)라고 하며 abstract 키워드를 사용해서 명시한다
- 구현 단계에서는 추상화된 클래스를 상속받아서, 아직 구현되지 않은 부분을 마저 구현한다
abstract class Design {
abstract fun drawText()
abstract fun draw()
fun showWindow() {
// code
}
}
class Implements: Design(){
fun drawText() {
// 구현 코드
}
fun draw() {
// 구현 코드
}
}
abstract class Animal {
fun walk(){
Log.d("abstract", "걷습니다.")
}
abstract fun move()
}
class Bird: Animal(){
override fun move(){
Log.d("abstract", "날아서 이동합니다.")
}
}
-> 다음과 같이 추상화된 Animal 클래스를 만들고 동물이 사용할 것 같은 기능 중에 walk와 move를 설계한다고 가정해보자
-> walk는 명확하게 걸어가는 행위이지만 move는 어떤 동물이냐에 따라서 달라질 수 있다
-> 이렇게 앞으로 상속받을 자식 클래스의 특징에 따라 코드가 결정될 가능성이 있다면, 해당 기능도 모두 abstract 키워드로 추상화할 수 있다
-> 그리고 실제 구현 클래스는 이 추상 클래스를 상속받아서, 아직 구현되지 않은 추상화되어 있는 기능을 모두 구현해준다
-> 그치만 추상 클래스는 독립적으로 인스턴스화 할 수 없기 때문에, 구현 단계가 고려되지 않는다면 잘못된 설계가 될 수 있다
- 위에서 잠깐 안드로이드의 Activity 클래스를 언급했는데, Activity도 수많은 클래스를 상속받아 만들어진다
- 이 Activity가 상솓받는 클래스 중에 최상위에 Context라는 클래스가 있는데, 최상위 클래스인 Context가 바로 abstract로 설계되어 있다
# 인터페이스
- 인터페이스(Interface)는 추상화와 비교하면 가장 명확하게 이해할 수 있는데, 실행 코드 없이 메서드 이름만 가진 추상 클래스라고 생각해볼 수 있다
- 즉, 누군가 설계해 놓은 개념 클래스 중에 실행 코드가 한 줄이라도 있으면 "추상화" , 코드 없이 메서드 이름만 나열되어 있으면 "인터페이스" 이다
- 인터페이스는 상속 관계의 설계보다는 외부 모듈에서 내가 만든 모듈을 사용할 수 있도록 메서드의 이름을 나열해둔 일종의 명세서로 제공된다
- 인터페이스는 interface 예약어를 사용해서 정의할 수 있고 인터페이스에 정의된 메서드를 오버라이드해서 구현할 수 있다
- Kotlin은 프로퍼티도 인터페이스 내부에 정의할 수 있는데, 대부분의 객체지향 언어에서는 지원하지 않는다
- 추상 클래스와 다르게 class 키워드는 사용되지 않는다
interface 인터페이스명 {
var 변수: String
fun 메서드1()
fun 메서드2()
}
# 클래스에서 구현하기
class KotlinImpl: InterfaceKotlin {
override var variable: String = "init value"
override fun get() {
// code
}
override fun set() {
// code
}
}
-> 인터페이스를 클래스에서 구현할 때는 상속돠는 다르게 생성자를 호출하지 않고 인터페이스 이름만 지정해주면 된다
var kotlinImpl = object: InterfaceKotlin {
override var variable: String = "init value"
override fun get() {
// code
}
override fun set() {
// code
}
}
-> 인터페이스를 클래스의 상속 형태가 아닌 소스 코드에서 직접 구현할 때도 있는데, object 키워드를 사용해서 구현해야 한다
-> 실제로 안드로이드 프로젝트를 시작하면 자주 사용하는 형태다
# 여기서 잠깐!!
- 인터페이스는 외부의 다른 모듈을 위한 의사소통 방식을 정의하는 것이다
- 혼자 개발하거나 소수의 인원이 하나의 모듈 단위를 개발할 때는 인터페이스를 사용하지 않는 것이 좋다
- 인터페이스를 남용하면 코드의 가독성과 구현 효율성이 떨어지기 때문이다
- 안드로이드가 제공하는 인터페이스를 자주 사용하는 이유는 안드로이드가 보았을 때 개발자가 만드는 모듈이 외부 모듈이기 때문이다
# 접근 제한자
- 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이 적용되어 접근할 수 있다
class Stranger {
fun callVariables() {
val parent = Parent()
Log.d("Modifier", "internal 변수의 값은 ${parent.internalVal}입니다.")
Log.d("Modifier", "public 변수의 값은 ${parent.defaultVal}입니다.")
}
}
-> 상속관계가 아닌 외부 클래스에서 Parent 클래스를 생성하고 사용해보자
-> 상속관계가 아니기 때문에, public과 internal에만 접근할 수 있다
# 제네릭(Generic)
- 제네릭(Generic)은 입력되는 값의 타입을 자유롭게 사용하기 위한 설계 도구이다
- 다음은 자주 사용되는 MutableList 클래스의 원본 코드를 이해하기 쉽게 변형한 코드이다
public interface MutableList<E> {
var list: Array<E>
// ...
}
-> 클래스명 옆에 <E>라고 되어 있는 부분에 String과 같은 특정 타입이 지정되면 클래스 내부에 선언된 모든 E에 String이 타입으로 지정된다
-> 결과적으로 var list: Array<E>가 var list: Array<String>으로 변경되는 것이다
- 이렇게 설계된 클래스를 우리는 주로 구현하는 용도로 사용하며, 컬렉션이나 배열에서 입력되는 값의 타입을 특정하기 위해 다음과 같이 사용한다
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): 타입을 특정해서 안정성을 유지하기 위한 설계 도구이다
'개발 노트 > Kotlin' 카테고리의 다른 글
[kotlin]지연 초기화 (0) | 2024.01.16 |
---|---|
[kotlin]null 값에 대한 안정적인 처리 - Null Safety (0) | 2024.01.16 |
[kotlin]함수 (0) | 2024.01.16 |
[kotlin]반복문 (0) | 2024.01.16 |
[kotlin]배열과 컬렉션 (0) | 2024.01.16 |