[Kotlin 기초 문법] 객체 지향 프로그래밍(객체활용, 상속, 오버라이딩, 오버로딩, 인터페이스)

이도연·2023년 8월 1일
0

기초 문법

목록 보기
4/10

1. 객체의 활용

1-1. 정의

  • 모든 인스턴스를 포함하는 개념
  • 클래스 타입으로 선언된 것들을 객체(object)라고 정의
  • 클래스 형태의 객체를 '실체화'하면 인스턴스가 생김.(메모리 공간 차지)
  • 클래스 객체를 몇 번 생성하더라도 항상 동일한 인스턴스를 얻음.

1-2. 클래스를 실체화

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

.
.
.

1-3. 예제 1

아래는 Person 클래스를 객체화해서 자기소개하는 코드이다.

// 클래스 정의
class Person(val name: String, var age: Int, var mbti: String) {
    fun sayHello() {
        println("안녕하세요, 저는 ${name} 입니다.")
    }

    fun increaseAge() {
        age++
    }
}

fun main() {
    // 클래스를 객체화하여 객체 생성
    val person1 = Person("Alice", 25, "ESTP")
    val person2 = Person("Bob", 30, "ISTJ")

    // 객체의 속성과 메서드 사용
    println("${person1.name}의 나이는 ${person1.age}세, MBTI는 ${person1.mbti}입니다.")
    person1.sayHello()

    println("${person2.name}의 나이는 ${person2.age}세, MBTI는 ${person2.mbti}입니다.")
    person2.sayHello()

    // 객체의 속성 변경
    person1.mbti = "ENTP"
    println("${person1.name}의 나이는 ${person1.age}세, MBTI는 ${person1.mbti}입니다.")

    // 객체의 메서드 호출
    person2.increaseAge()
    println("${person2.name}의 나이는 ${person2.age}세, MBTI는 ${person2.mbti}입니다.")
}

Person이라는 클래스를 정의했다.
이 클래스는 세 가지 속성 (이름(name), 나이(age), mbti),
두 가지 메서드 (sayHello(), increaseAge()를 가진다.

main() 함수에서 Person 클래스를 기반으로 두 개의 객체를 생성했다.
person1과 person2는 Person 클래스의 인스턴스, 각자 고유한 이름과 나이, mbti를 가진다.

객체를 생성하면 해당 객체의 속성과 메서드를 사용할 수 있다.
person1과 person2의 이름, 나이, mbti를 출력하고,
sayHello() 메서드를 호출하여 각 객체가 자기 소개를 할 수 있다.
객체의 속성은 .(도트 연산자)를 사용하여 접근하고, 메서드도 .를 사용하여 호출한다.

val 를 이용했기때문에 객체의 속성은 변경 가능하다.
person1.mbti = ENTP 처럼 mbti를 변경할 수 있고,
person2.increaseAge()와 같이 메서드를 호출하여 나이를 증가시킬 수도 있다.

.
.
.

1-4. 예제 2

다음은 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 gameStart() {
        println("게임 시작")
    }
}

캐릭터 속성을 저장하는 변수는 총 5개이다. (이름, 머리색상, 키, 나이, 성별)
Character 클래스에는 두 개의 생성자가 있다. 이 생성자는 객체가 생성될 때 호출된다.

첫 번째 생성자는 name, hairColor, height를 매개변수로 받는다.
이를 통해 캐릭터의 이름, 머리 색상, 키를 초기화하고
두 번째 생성자는 이름, 머리 색상, 키, 나이, 성별을 초기화한다.

main 함수에서 프로그램을 시작하며, 두 캐릭터 객체가 생성되고 사용된다.

.
.

  • 불마법사 객체 생성 (magicianOne)

var magicianOne = Character("불마법사", "red", 180.2)

를 통해 불마법사라는 이름의 Character 객체를 생성했고,
생성자의 매개변수를 통해 이름은 "불마법사", 머리 색상은 "red", 키는 180.2로 초기화했다.

println("${magicianOne.name}의 머리색상은 ${magicianOne.hairColor}입니다")

를 통해 magicianOne 객체의 이름, 머리 색상 출력

마지막으로, magicianOne.gameStart()를 호출하여
gameStart() 메서드를 실행하고 "게임시작!"이 출력된다.

.
.

  • 냉마법사 객체 생성 (magicianTwo)

var magicianTwo = Character("냉마법사", "blue", 162.2, 25, "여")

를 통해 냉마법사라는 이름의 Character 객체를 생성했고,
생성자의 매개변수를 통해 이름은 "냉마법사", 머리 색상은 "blue", 키는 162.2, 나이는 25,
성별은 "여"로 초기화했다.

println("${magicianTwo.name}의 머리색상은 ${magicianTwo.hairColor}이고 나이는 ${magicianTwo.age}입니다.")

를 통해 magicianTwo 객체의 이름, 머리 색상, 나이를 출력

마지막으로, magicianOne.gameStart()를 호출하여
gameStart() 메서드를 실행하고 "게임시작!"이 출력된다.

이처럼 객체 생성, 사용은 객체지향 프로그래밍의 핵심 개념 중 하나이다.
객체는 프로그램에서 데이터와 기능을 캡슐화하고 모듈화하는데 유용하며,
가독성과 유지보수 향상에 기여한다.

.
.
.
.
.

2. 상속

2-1. 정의

  • open 키워드로 상속 관계를 만들 수 있다.
  • 클래스 내용을 변경해야 할 경우 상위 클래스만 변경하면 된다.

2-2. 예제 1

아래 코드는 디저트 주문 코드이며, 상속 관계를 가지는 클래스들로 구성되어있다.

fun main() {
    var dessert = Dessert("디저트")
    var cake = Cake("케이크")
    var cookie = Cookie("쿠키")
    var icecream = Icecream("아이스크림")

    dessert.order()
    cake.order()
    cookie.order()
    icecream.order()
}

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

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

    fun order() {
        println("${name}이 주문이 접수되었습니다.")
    }

}

class Cake(name: String) : Dessert(name) {

}

class Cookie(name: String) : Dessert(name) {

}

class Icecream(name: String) : Dessert(name) {

}
  • main 함수
    main 함수에서 Dessert, Cake, Cookie, Icecream 클래스의 객체들을 생성하고,
    order() 메서드를 호출하여 디저트 주문 동작을 출력한다.

  • Dessert 클래스
    Dessert 클래스는 다른 디저트들이 상속을 받을 기반이 되며,
    멤버변수 name, order() 메서드를 가진다.

멤버 변수 : name(디저트 이름을 저장하는 문자열 변수)
생성자: Dessert 클래스의 생성자는 name을 매개변수로 받아서 name 멤버 변수를 초기화
메서드: order() 메서드는 Dessert 클래스에 정의된 함수이다. 디저트를 주문하는 동작을 나타내며, 드저트 이름을 포함한 메시지를 출력한다.

  • Cake, Cookie, Icecream 클래스
    이 세 클래스들은 Dessert 클래스를 상속받은 서브 클래스다. 상속을 받음으로써 Dessert 클래스의 모든 속성과 메서드를 갖게 된다. 각 클래스는 디저트의 종류를 나타내는 이름을 받아서 Dessert 클래스의 생성자를 호출하여 초기화합니다.

생성자: 이 클래스들은 각각의 이름을 매개변수로 받아서, 상위 클래스인 Dessert의 생성자를 호출하여 name 멤버 변수를 초기화한다.

.
.
.

이 예제에서는 상속의 개념을 활용하여 공통적인 특성과 동작을 가지는 Dessert 클래스를 정의하고, Cake, Cookie, Icecream 클래스가 이를 상속받는다. 후에 자신만의 특성을 추가할 수도 있다.

상속 개념을 활용하면, 코드의 재사용성과 확장성이 높아지며,
상위 클래스 메서드인 Order()를 서브 클래스에서 그대로 사용하거나,
재정의(오버라이딩)하여 동작을 바꿀 수 있다.

.
.
.
.
.

3. 오버라이딩

3-1. 정의

  • 상속받은 상위 클래스의 정보(프로퍼티)나 행위(메소드)를 재설계할 수 있다.
  • 공통적인 내용을 상위 클래스에서 관리하면서, 서브 클래스의 특성을 살릴 수 있다.

3-2. 상속, 오버라이딩이 필요한 이유

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

3-3. 예제 1

아래 코드는 디저트 주문 코드이며, 상속과 오버라이딩 개념을 가지는 클래스들로 구성되어있다.

fun main() {
    var dessert = Dessert("디저트")
    var cake = Cake("케이크", 2)
    var cookie = Cookie("쿠키", "초코칩")
    var icecream = Icecream("아이스크림", "바닐라")

    dessert.order()
    cake.order()
    cookie.order()
    icecream.order()
}

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

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

    open fun order() {
        println("${name}를 선택했습니다.")
    }

}

class Cake(name: String, count: Int) : Dessert(name) {
    var count:Int = 0

    init {
        this.count = count
    }

    override fun order() {
//        super객체는 부모의 객체를 의미하며 자동으로 생성됨
//        즉 부모객체의 fly메소드를 부르는 행위임
//        필요없으니 주석처리완료
//        super.order()
        println("${count}개의 ${name}를 주문했습니다.")
    }
}

class Cookie(name: String, cookieFlavor: String) : Dessert(name) {
    var cookieFlavor:String = ""

    init {
        this.cookieFlavor = cookieFlavor
    }

    override fun order() {
//        super객체는 부모의 객체를 의미하며 자동으로 생성됨
//        즉 부모객체의 fly메소드를 부르는 행위임
//        필요없으니 주석처리완료
//        super.order()
        println("${cookieFlavor} ${name}를 주문했습니다.")
    }
}

class Icecream(name: String, icecreamFlavor: String) : Dessert(name) {
    var icecreamFlavor: String = ""

    init {
        this.icecreamFlavor = icecreamFlavor
    }

    override fun order() {
//        super객체는 부모의 객체를 의미하며 자동으로 생성됨
//        즉 부모객체의 fly메소드를 부르는 행위임
//        필요없으니 주석처리완료
//        super.order()
        println("${icecreamFlavor} ${name}를 주문했습니다.")
    }
}
  • Cake, Cookie, Icecream 클래스

Chicken, Sparrow, Pigeon 클래스에서는 drder() 메서드를 오버라이딩한다. 각각의 클래스에 적합한 동작을 추가하여 디저트 이름과 추가 특성을 출력한다. 오버라이딩된 order() 메서드는 super.order()를 호출하지 않고 직접 새의 이름과 추가적인 특성을 출력한다.

.
.
.
.
.

4. 오버로딩

4-1. 정의

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

4-2. 예제 1

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
    }
}

main 함수에서는, Calculator 클래스의 객체 calc를 생성한 후, add 메서드를 호출하여
두 개의 정수 1과 2를 더한 결과를 intResult 변수에 저장하고,
두 개의 실수 1.2와 2.2를 더한 결과를 doubleResult 변수에 저장한다.
println 함수로 정수 덧셈 결과와, 실수 덧셈 결과를 출력합니다.

Calculator 클래스에는 두 가지 add 메서드가 오버로딩되어 있다. 매개변수의 타입이나 개수가 다르기 때문에, 같은 이름의 메서드를 사용할 수 있다.

add(num1: Int, num2: Int): Int

정수형 매개변수 두 개를 받아서 덧셈 연산을 수행하고, 정수형 결과를 반환한다.

add(num1: Double, num2: Double): Double

실수형 매개변수 두 개를 받아서 덧셈 연산을 수행하고, 실수형 결과를 반환한다.
add 메서드는 매개변수의 타입에 따라 오버로딩되어, 정수형 매개변수를 받는 경우와 실수형 매개변수를 받는 경우에 각각 다른 덧셈 연산을 수행한다.

.
.
.
.
.

5. 인터페이스

5-1. 정의

  • 상속을 통해 케이크, 쿠키, 아이스크림과 상위 클래스인 Dessert 의 관계를 만들었다.
  • 하지만 디저트에는 많은 종류가 있고, 고유한 특성도 다르다.
  • 상위 클래스는 한 개라서 모두 상속으로 처리할 수 없다. (부모가 여러명은 아니니까)
  • interface 키워드를 사용하여 근본 공통점은 상속 받고, 추가적인 기능들은 인터페이스로 추가한다.

5-2. 구조

interface 인터페이스이름 {
	fun 메소드이름()
}
  • 매소드의 로직이 존재하지 않고 이름만 존재하는 것을 추상 메소드라고 한다.
  • 원래 인터페이스는 추상메소드만 허용했으나, 최근에는 추상메소드가 아니여도 상관없다.
  • 하지만 추상메소드를 작성하는 습관을 가지는게 좋다.

5-3. 예제 1

interface HandmadeDessert {
    fun cook()
//    fun cook() {
//        println("조리를 시작합니다.")
//    }
}
  • 상속으로 클래스들간 관계를 구분했다.
  • 신메뉴 떡볶이가 출시되었다.
  • 케이크, 쿠키, 아이스크림과 달리 조리가 필요하다.
interface HandmadeDessert{
	fun cook()
}

fun main() {
    var dessert = Dessert("디저트")
    var cake = Cake("케이크", 2)
    var cookie = Cookie("쿠키", 초코칩")
    var icecream = Icecream("아이스크림", "바닐라")
    var tteokbokki = Tteokbokki("떡볶이")

    dessert.order()
    cake.order()
    cookie.order()
    icecream.order()
    tteokbokki.cook()
}

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

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

    open fun order() {
        println("${name}를 선택했습니다.")
    }

}

class Cake(name: String, count: Int) : Dessert(name) {
	var count:Int = 0
    
    init {
    	this.count = count
    }
    
    override fun order() {
		println("${count}개의 ${name}를 주문했습니다.")
    }

}

class Cookie(name: String, cookieFlavor: String) : Dessert(name) {
	var cookieFlavor:String = ""
    
    init {
    	this.cookieFlavor = cookieFlavor
    }
    
    override fun order() {
		println("${cookieFlavor} ${name}를 주문했습니다.")
    }

}

class Icecream(name: String, icecreamFlavor: String) : Dessert(name) {
	var icecreamFlavor:String = ""
    
    init {
    	this.icecreamFlavor = icecreamFlavor
    }
    
    override fun order() {
		println("${icecreamFlavor} ${name}를 주문했습니다.")
    }

}

class Tteokbokki(name: String) : Dessert(name), WaterBirdBehavior {
    override fun cook() {
        println("${name}를 주문했습니다. 조리를 시작합니다.)
    }
}

.
.
.

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

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

    open fun order() {
        println("${name}를 선택했습니다.")
    }

}

Dessert 클래스의 name 프로퍼티는 디저트의 이름을 나타낸다.
생성자에서 name을 받아 해당 프로퍼티를 초기화하고
order() 함수는 디저트를 주문을 수행한다.

class Cake(name: String, count: Int) : Dessert(name) {
    var count: Int = 0
    
    init {
        this.count = count
    }
    
    override fun order() {
        println("${count}개의 ${name}를 주문했습니다.")
    }
}

Cake 클래스는 Dessert 클래스를 상속받아서 만들어진 클래스로, count 프로퍼티를 이용하여 개수를 나타낸다.
생성자에서 name과 count를 받아 각각의 프로퍼티를 초기화하고, order() 함수를 오버라이드하여 케이크 주문 동작을 출력한다. Cookie 클래스, Icecream 클래스 모두 Cake 와 구조가 동일하다.

class Tteokbokki(name: String) : Dessert(name), HandmadeDessert {
    override fun cook() {
        println("${name}를 주문했습니다. 조리를 시작합니다.")
    }
}

Tteokbokki 클래스는 Dessert 클래스를 상속받아 만들어진 클래스이며, HandmadeDessert 인터페이스를 구현하고 있다. cook() 함수를 구현하여 떡볶이 조리 동작을 출력한다.

interface HandmadeDessert {
    fun cook()
}

cook() 함수를 선언하는 HandmadeDessert 인터페이스이다. 이 인터페이스를 구현하는 클래스들은 cook() 함수를 반드시 실행한다.

fun main() {
    var dessert = Dessert("디저트")
    var cake = Cake("케이크", 2)
    var cookie = Cookie("쿠키", "초코칩")
    var icecream = Icecream("아이스크림", "바닐라")
    var tteokbokki = Tteokbokki("떡볶이")

    dessert.order()
    cake.order()
    cookie.order()
    icecream.order()
    tteokbokki.cook()
}

main 함수에서는 Dessert, Cake, Cookie, Icecream, Tteokbokki 클래스의 객체들을 생성하고 각각의 order() 또는 cook() 함수를 호출한다. 출력은 아래와 같다.

.
.
.

0개의 댓글