코틀린(Kotlin)/TIL

[Kotlin 문법] 객체지향 프로그래밍의 기초

초보왕보초 2023. 11. 28. 19:59
728x90
  • 메서드 설계
  • 클래스 설계
  • 생성자의 활용
  • 객체의 활용
  • 상속
  • 오버라이딩
  • 오버로딩
  • 인터페이스

 

 

메서드 설계

특정한 로직을 가지는 소스코드에 이름을 붙일 수 있는데, 이름이 붙은 로직메서드라고 한다

// Kotlin의 메서드 구조
fun 메서드이름(변수명:자료형, 변수명:자료형 ...) : 반환자료형 {
	소스코드 로직
}
  • 로직을 추상화해 놓고 상황에 맞게 실행할 수 있다
  • 코드의 재사용성을 높일 수 있다
// 두 개의 숫자를 더하는 메서드를 만들고, 전달하는 숫자에 따라 덧셈결과를 받도록

fun main() {
	var num1 = readLine()!!.toInt()
    var num2 = readLine()!!.toInt()
    
    // sum 이라는 이름의 메소드를 호출
    sum(num1, mum2)
}

fun sum(num1:Int, num2:Int){
	var result = num1 + num2
    println("num1과 num2의 덧셈결과는 ${result}")
}



// 출력 (num1= 20, num2=10 입력가정)
num1과 num2의 덧셈결과는 30

 

 

 

클래스 설계

객체지향 프로그래밍(OOP)이란?

  • 코틀린은 모든 것이 클래스형태이므로 객체화할 수 있다
  • 프로그램에서 필요한 데이터를 추상화시켜 상태와 행위를 가진 객체를 만든다
  • 객체들 간의 적절한 결합을 통해 유지보수를 쉽게 한다

 

클래스란?

  • 프로그램의 각 요소별 설계도라고 해석할 수 있다
  • 클래스에는 정보(프로퍼티)행위(메서드)를 작성한다
  • 보통 하나의 파일은 한 개의 클래스를 의미하지만, 하나의 파일 안에 여러 개의 클래스가 존재할 수도 있다
// 클래스의 기본 구조

class 클래스이름 {
	정보1
    정보2
    
    행위1
    행위2
}

 

데이터 클래스(data class)

  • 정보(프로퍼티)만 가지고 있는 클래스가 필요할 때, 정보만 가지고 있으면서 설계하려면 힘들어진다고 한다. 그럴 때 data class 키워드를 사용하면 매우 간편해진다고 한다
  • 기본 생성자에 1개 이상의 매개변수나 변수나 상수로 존재해야 한다
  • 유용한 메서드( )를 자동으로 만들어준다
  • hashCode() : 객체를 구분하기 위한 고윳값을 리턴해준다
  • eauals() : 동일한 객체인지 비교해서 true 또는 false를 리턴해준다
  • copy() : 현재 객체의 모든 정보를 복사해서 새로운 객체를 리턴해준다
  • toString() : 현재 객체의 모든 정보(프로퍼티)를 출력해 준다
  • getXXX() / setXXX(매개변수) : 변수의 값을 리턴(get)하거나 설정(set)한다
// 데이터 클래스의 구조

data class 클래스 이름 {
	정보1
        정보2
}

 

 

실드 클래스(sealed class)

  • 클래스 상속과 관련된 개념
  • 상속받을 수 있는 자식클래스들을 미리 정의할 수 있다
  • 무분별한 상속을 방지할 수 있다
// 실드 클래스의 구조

sealed class 부모클래스 {
    class 자식클래스1 : 부모클래스생성자
    class 자식클래스2 : 부모클래스생성자
}

 

 

오브젝트 클래스(object class)

  • Java의 static 대신 사용하는 키워드
  • 프로그램을 실행하는 동시에 인스턴스화한다

 

 

열거 클래스(enum class)

 

  • 여러 곳에 동일한 상수를 정의하거나, 상수 외부에 관련된 변수나 함수를 정의하게 되면 코드가 증가할수록 관리가 어려워지는데 enum class를 이용해서 상수값에 대한 관리 지점을 줄일 수 있다
  • Comparable 인터페이스를 구현하는 추상 클래스이다
// 열거 클래스의 구조

enum class 클래스1 {
	C, JAVA, KOTLIN
}

enum class 클래스2(val code: Int) {
    C(10),
    JAVA(20),
    KOTLIN(30)
}

fun main() {
    println(클래스1.C.toString()) // 출력 : C
    println(클래스2.KOTLIN.code)  // 출력 : 30
    println(클래스2.KOTLIN.name)  // 출력 : KOTLIN
}

 

 

예시) 프로그램에서 사용하는 특정 캐릭터들의 설계도를 작성해 본다

fun main() {
    
}

class Character {
    var name:String = ""
    var hairColor:String = ""
    var height:Double = 0.0

    fun fireBall() {
        println("파이어볼!")
    }
    fun compositing(device1:String, device2:String): String {
        var device3 = device1 + device2
        println("새로운 무기인 ${device3}입니다")
        return device3
    }
}

 

 

 

생성자의 활용

  • 클래스(설계도)를 실체화할 때 최초로 실행할 로직을 작성한다
  • 기본 생성자와 명시적 생성자가 존재한다
  • 기본 생성자는 이전까지 클래스를 만들던 행위와 차이가 없다
  • 명시적 생성자는 주 생성자와 부 생성자로 구분할 수 있다
  • Init (주 생성자)
fun main() {

}

// 클래스 선언부에 생성자를 명시함
class Character(_name:String, _hairColor:String, _height:Double) {
    var name:String = ""
    var hairColor:String = ""
    var height:Double = 0.0

// 매개변수를 직접 넘기지않음
    init {
        println("매개변수없는 생성자 실행 완료!")
    }

    fun fireBall() {
        println("파이어볼!")
    }
    fun compositing(device1:String, device2:String): String {
        var device3 = device1 + device2
        println("새로운 무기인 ${device3}입니다")
        return device3
    }
}

 

 

  • Constructor (부 생성자)
  fun main() {

  }

  class Character {
      var name:String = ""
      var hairColor:String = ""
      var height:Double = 0.0

      // 명시적 생성자 (Constructor)
      // _name, _hairColor, _height와 같이 생성자에 변수를 넘기는 경우에 사용함
      constructor(_name:String, _hairColor:String, _height:Double) {
          println("${_name}을 생성자로 넘겼어요")
          println("${_hairColor}를 생성자로 넘겼어요")
          println("${_height}를 생성자로 넘겼어요")
      }

      fun fireBall() {
          println("파이어볼!")
      }
      fun compositing(device1:String, device2:String): String {
          var device3 = device1 + device2
          println("새로운 무기인 ${device3}입니다")
          return device3
      }
  }

 

Init(주 생성자)반드시 하나의 형태(name, hairColor, height)만 받는 생성자를 만들 수 있다

Constructor(부 생성자)는 constructor를 여러 개 만들어서

constructor1은 name만 받고, constructor2는 name, hairColor만 받고, constructor3는 name, height만 받는 등

여러 개의 생성자를 만들 수 있다

 

 

 

객체의 활용

객체란?

  • 모든 인스턴스를 포함하는 개념이다
  • 클래스 타입으로 선언된 것들을 객체(Object)라고 한다

인스턴스란?

  • 클래스형태로 설계된 객체를 실체화하면 인스턴스가 생긴다
  • 인스턴스는 메모리 공간을 차지한다

클래스를 실체화한다

  • 정보와 행위를 작성한 클래스를 실체화해서 프로그램에 로딩한다(메모리에 적재)
  • 정보나 행위가 그대로 로딩되는 것이 아니라 위치정보를 메모리에 로딩한다
  • 프로그램은 객체의 위치정보를 변수에 저장해 두고, 필요할 때 참조한다

예시) Character 클래스를 객체화해서 여러 가지 캐릭터를 만든다

fun main() {
		// 불마법사로 객체화
    var magicianOne = Character("불마법사", "red", 180.2)
    println("${magicianOne.name}의 머리색상은 ${magicianOne.hairColor}입니다")
    magicianOne.fireBall()

		// 냉마법사로 객체화
    var magicianTwo = Character("냉마법사", "blue", 162.2, 25, "여")
    println("${magicianTwo.name}의 머리색상은 ${magicianTwo.hairColor}이고 나이는 ${magicianTwo.age}입니다.")
    magicianTwo.fireBall()
}

class Character {
    var name:String = ""
    var hairColor:String = ""
    var height:Double = 0.0
    var age:Int = 0
    var gender:String = ""

    // 명시적 생성자 (Constructor)
    // _name, _hairColor, _height와 같이 생성자에 변수를 넘기는 경우에 사용함
    constructor(_name:String, _hairColor:String, _height:Double) {
        println("${_name}을 생성자로 넘겼어요")
        println("${_hairColor}를 생성자로 넘겼어요")
        println("${_height}를 생성자로 넘겼어요")
        name = _name
        hairColor = _hairColor
        height = _height
    }
    // _name, _hairColor, _height, _age, _gender와 같이 생성자에 변수를 넘기는 경우에 사용함
    constructor(_name:String, _hairColor:String, _height:Double, _age:Int, _gender:String) {
        println("${_name}을 생성자로 넘겼어요")
        println("${_hairColor}를 생성자로 넘겼어요")
        println("${_height}를 생성자로 넘겼어요")
        println("${_age}를 생성자로 넘겼어요")
        println("${_gender}를 생성자로 넘겼어요")

        name = _name
        hairColor = _hairColor
        height = _height
        age = _age
        gender = _gender
    }

    fun fireBall() {
        println("파이어볼!")
    }
    fun compositing(device1:String, device2:String): String {
        var device3 = device1 + device2
        println("새로운 무기인 ${device3}입니다")
        return device3
    }
}



// 출력
불마법사을 생성자로 넘겼어요
red을 생성자로 넘겼어요
180.2를 생성자로 넘겼어요
불마법사의 머리색상은 red입니다
파이어볼!
냉마법사을 생성자로 넘겼어요
blue를 생성자로 넘겼어요
162.2를 생성자로 넘겼어요
25를 생성자로 넘겼어요
여를 생성자로 넘겼어요
냉마법사의 머리색상은 blue이고 나이는 25입니다.
파이어볼!

 

 

 

상속

  • 공통적인 요소들이 있다면 부모/자식 클래스를 구분해서 상속관계를 만들 수 있다
  • 코틀린은 다른 언어들과 달리 생략된 final 키워드로 기본적으로 상속을 막아두었다
  • 무분별한 상속으로 예상치 못한 흐름을 방지하기 위해 막았다고 한다
  • 코틀린은 open 키워드를 활용해서 상속 관계를 만들 수 있다

상속이 필요한 이유

  • 다형성을 구현할 수 있다
  • 클래스의 내용을 변경해야 하는 경우에 부모 클래스만 변경하는 것으로 공수를 줄일 수 있다

예시) 닭, 참새, 비둘기는 새라는 공통점이 있고, 새라는 부모에서 출발했다는 관계를 만들 수 있다

fun main() {
    var bird = Bird()
    var chicken = Chicken()
    var sparrow = Sparrow()
    var pigeon = Pigeon()

    bird.fly()
    chicken.fly()
    sparrow.fly()
    pigeon.fly()
}

open class Bird {
    fun fly() {
        println("새는 날아요~")
    }
}

class Chicken : Bird() {

}

class Sparrow : Bird() {

}

class Pigeon : Bird() {

}



// 출력
새는 날아요~
새는 날아요~
새는 날아요~
새는 날아요~

 

부모클래스(Bird)에서 생성자를 활용하는 경우에 자식에서 객체 생성 시 전달해줘야 한다

fun main() {
    var bird = Bird("새")
    var chicken = Chicken("닭")
    var sparrow = Sparrow("참새")
    var pigeon = Pigeon("비둘기")

    bird.fly()
    chicken.fly()
    sparrow.fly()
    pigeon.fly()
}

open class Bird(name:String) {
    var name: String = ""

    init {
        // this는 현재 클래스의 상태변수를 의미합니다
        // var name: String = ""
        this.name = name
    }

    fun fly() {
        println("${name} 날아요~")
    }

}

class Chicken(name: String) : Bird(name) {

}

class Sparrow(name: String) : Bird(name) {

}

class Pigeon(name: String) : Bird(name) {

}



// 출력
새 날아요~
닭 날아요~
참새 날아요~
비둘기 날아요~

 

 

 

오버라이딩

부모 클래스의 정보를 재설계할 수 있다

  • 상속받은 부모 클래스의 정보(프로퍼티)나 행위(메서드)를 재설계할 수 있다
  • 주로 부모 클래스의 행위(메서드)를 재설계한다
  • 이러한 행위를 오버라이딩(overriding)이라고 한다

 

오버라이딩이 필요한 이유는?

  • 공통적인 내용을 부모 클래스에서 관리하는 건 좋지만, 자식 클래스의 개성을 살리고 싶을 때

어차피 재설계하는 거라면 상속이나 오버라이딩을 하는 이유는?

  • OOP관점에서는 클래스들 간의 관계를 만들고, 일관성을 유지하는 목표를 가진다
  • 만약 필요한 기능이 있을 때마다 별도의 이름으로 만들게 된다면 일관성을 해친다
  • 프로그램에 문제가 생기지는 않지만 재사용성이 떨어져 유지보수가 어렵다

 

예시) 새라는 부모에서 출발했지만, 각 개체마다 개성을 더해주고 싶다면

[오버라이딩 단축키]
// 상속받은 메소드를 오버라이딩하고 싶을 때 자식 클래스 내부에서 단축키 입력
// Control + O

fun main() {
    var bird = Bird("새")
    var chicken = Chicken("닭", 2)
    var sparrow = Sparrow("참새", "갈색")
    var pigeon = Pigeon("비둘기", "서울")

    bird.fly()
    chicken.fly()
    sparrow.fly()
    pigeon.fly()
}

open class Bird(name:String) {
    var name: String = ""

    init {
        // this는 현재 클래스의 상태변수를 의미합니다
        // var name: String = ""
        this.name = name
    }

    open fun fly() {
        println("${name}은 날아요~")
    }

}

class Chicken(name: String, age: Int) : Bird(name) {
    var age:Int = 0

    init {
        this.age = age
    }

    override fun fly() {
//        super객체는 부모의 객체를 의미하며 자동으로 생성됨
//        즉 부모객체의 fly메소드를 부르는 행위임
//        필요없으니 주석처리완료
//        super.fly()
        println("${age}살의 ${name}가 날아봅니다~ 꼬끼오!")
    }
}

class Sparrow(name: String, color: String) : Bird(name) {
    var color:String = ""

    init {
        this.color = color
    }

    override fun fly() {
//        super객체는 부모의 객체를 의미하며 자동으로 생성됨
//        즉 부모객체의 fly메소드를 부르는 행위임
//        필요없으니 주석처리완료
//        super.fly()
        println("${color}의 ${name}이 날아봅니다~ 짹짹!")
    }
}

class Pigeon(name: String, address: String) : Bird(name) {
    var address: String = ""

    init {
        this.address = address
    }

    override fun fly() {
//        super객체는 부모의 객체를 의미하며 자동으로 생성됨
//        즉 부모객체의 fly메소드를 부르는 행위임
//        필요없으니 주석처리완료
//        super.fly()
        println("${address} 살고있는 ${name}가 날아봅니다~ 구구!")
    }
}



// 출력
새은 날아요~
2살의 닭가 날아봅니다~ 꼬끼오!
갈색의 참새이 날아봅니다~ 짹짹!
서울 살고있는 비둘기가 날아봅니다~ 구구!

 

 

 

오버로딩

동일한 이름의 메서드를 여러 형태로 만들 수 있다

  • 매개변수의 개수를 다르게 하면 동일한 이름으로 메서드를 만들 수 있다
  • 매개변수의 자료형을 다르게 하면 동일한 이름으로 메서드를 만들 수 있다
  • 반환자료형(반환형)은 오버로딩에 영향을 주지 않는다

 

오버로딩이 필요한 이유?

  • 두 개의 정수를 매개변수로 받아 더하는 메서드를 add라는 이름으로 만들었다고 가정
  • 하지만 두 개의 실수(소수)를 매개변수로 받아 더하는 메서드도 만들어야 한다면
  • 더하는 거니까 add라는 이름이 적합한데 어떻게 해야 할까?
  • addInt, AddDouble 이렇게 메서드를 따로 만들면 나중에 관리하기 힘들어진다
  • 이 상황에서는 더해야 하는 자료형이 정수, 실수로 다르니까 오버로딩으로 해결할 수 있다

예시) 정수나 실수 값 두 개를 매개변수로 받아서 덧셈결과를 리턴해주는 add 메서드를 만든다

fun main() {
    var calc = Calculator()
    
    var intResult = calc.add(1,2)
    var doubleResult = calc.add(1.2, 2.2)
    
    println("정수 덧셈결과: ${intResult}")
    println("실수 덧셈결과: ${doubleResult}")
    
}

class Calculator {
    
    fun add(num1: Int, num2: Int): Int {
        return num1+num2
    }
    
    fun add(num1: Double, num2: Double): Double {
        return num1+num2
    }
}



// 출력
정수 덧셈결과: 3
실수 덧셈결과: 3.400000000000004

 

 

 

인터페이스

공통적으로 필요한 기능을 외부에서 추가해 줄 수 있다

  • 앞서 상속으로 닭, 참새, 비둘기와 부모 클래스인 Bird의 관계를 만들었다
  • 하지만 새에도 많은 종류가 있고, 고유한 행동도 다름
  • 코틀린은 반드시 부모 클래스는 한 개라서 모두 상속으로 처리할 수 없다
  • 따라서 근본적인 공통점을 상속받고, 추가적인 기능들은 인터페이스로 추가할 수 있다
  • 코틀린은 인터페이스를 만들기 위해 interface 키워드를 사용한다
  • 아래의 예시처럼 메서드의 로직이 존재하지 않고 이름만 존재할 때추상메서드 라고한다
  • 인터페이스는 추상메서드만 작성하는 게 원칙이지만, 최근에는 추상메서드가 아니어도 된다고 한다
  • 하지만 인터페이스는 추상메서드를 작성하는 습관을 가지는 게 좋다고 한다
interface 인터페이스이름 {
	fun 메소드이름()
}

 

인터페이스가 필요한 이유?

  • 위 예시들에서 상속으로 클래스들 간의 관계를 성공적으로 구분했다
  • 닭, 참새, 비둘기까지는 문제없지만 오리가 추가된다면 고민이 생긴다
  • 보통 새는 헤엄치다라는 행위가 없기 때문에 오리를 부모 클래스(Bird)에 추가하는 것은 올바르지 않다
  • 오리를 제외하고도 물에서 서식하는 조류는 별도의 기능이 필요하다

예시) 물에서 생활하는 오리에게 WaterBirdBehavior라는 인터페이스를 추가해 준다

[Project 뷰에서 New -> Kotlin Class/File -> Interface로 생성]

interface WaterBirdBehavior {
    fun swim()
//    fun swim() {
//        println("수영 합니다")
//    }
}
fun main() {
    var bird = Bird("새")
    var chicken = Chicken("닭")
    var sparrow = Sparrow("참새")
    var pigeon = Pigeon("비둘기")
    var duck = Duck("오리")

    bird.fly()
    chicken.fly()
    sparrow.fly()
    pigeon.fly()
    duck.swim()
}

open class Bird(name:String) {
    var name: String = ""

    init {
        // this는 현재 클래스의 상태변수를 의미합니다
        // var name: String = ""
        this.name = name
    }

    fun fly() {
        println("${name} 날아요~")
    }

}

class Chicken(name: String) : Bird(name) {

}

class Sparrow(name: String) : Bird(name) {

}

class Pigeon(name: String) : Bird(name) {

}

class Duck(name: String) : Bird(name), WaterBirdBehavior {
    override fun swim() {
        println("${name}가 수영해요~")
    }
}



// 출력
새 날아요~
닭 날아요~
참새 날아요~
비둘기 날아요~
오리가 수영해요~
728x90