객체지향 프로그래밍 기초

📌 책임, 역할, 상호작용

    • 객체지향 프로그래밍(OOP)은 프로그램을 여러 개의 독립된 객체로 나누고, 각 객체가 데이터와 기능을 가지며 상호작용하는 방식
    • 객체(Object): 상태와 그 상태를 처리하는 행동의 묶음
    • 책임(Responsibility): 객체가 수행해야 하는 역할 또는 기능을 말합니다. 예를 들어, 자동차 객체의 주행이나 정지는 그 객체의 책임입니다.
    • 역할(Role): 객체가 수행하는 책임의 집합을 의미합니다. 여러 객체가 동일한 역할을 수행할 수 있습니다.
    • 상호작용(Interaction): 객체 간에 서로 메시지를 주고받아 책임을 수행하고 상호작용하는 것을 의미합니다.
    • 객체지향 프로그래밍을 잘 한다는건 책임과 역할을 잘 나누어 코드를 작성한다는 것!

 

 

전략 패턴

/ 디자인 패턴들 / 행동 패턴 전략 패턴 다음 이름으로도 불립니다: Strategy 의도 전략 패턴은 알고리즘들의 패밀리를 정의하고, 각 패밀리를 별도의 클래스에 넣은 후 그들의 객체들을 상호교환

refactoring.guru

 

 

 

클래스와 인스턴스

1. 클래스와 인스턴스

더보기

클래스

  • 프로그램의 각 요소별 설계도라고 해석할 수 있어요
  • 코틀린에서는 class 키워드를 활용해서 클래스를 만들어요
  • 클래스에는 정보(프로퍼티)와 행위(메소드)를 작성해요
  • 보통 하나의 파일은 한개의 클래스를 의미하지만, 하나의 파일안에 여러개의 클래스가 존재할 수도 있어요

 

인스턴스

  • 클래스형태로 설계된 객체를 실체화하면 인스턴스가 생겨요
  • 인스턴스는 메모리 공간을 차지해요
  • 정보와 행위를 작성한 클래스를 실체화해서 프로그램에 로딩해요 (메모리에 적재)
  • 정보가 행위가 그대로 로딩되는것이 아니라 위치정보를 메모리에 로딩해요
  • 프로그램은 객체의 위치정보를 변수에 저장해두고, 필요할 때 참조해요

2. 클래스 선언과 인스턴스 생성

// **클래스 선언**
class Person {
    var name: String = "" // 속성 (멤버 변수)
    var age: Int = 0

    fun introduce() { // 메서드
        println("Hi, my name is $name and I am $age years old.")
    }
}

// **인스턴스 생성**
val person = Person() // Person 클래스의 인스턴스를 생성
person.name = "Alice" // 속성 값 설정
person.age = 25
person.introduce() // 메서드 호출

// **결과**
// 출력: Hi, my name is Alice and I am 25 years old.

3. 생성자(Constructor)

1) 기본 생성자

// **기본 생성자 선언**
class Person(val name: String, var age: Int) { // 주 생성자
    fun introduce() {
        println("Hi, my name is $name and I am $age years old.")
    }
}

val person = Person("Bob", 30) // 생성자 호출
person.introduce() // 출력: Hi, my name is Bob and I am 30 years old.

2) 부 생성자

// **부 생성자 사용**
class Person {
    var name: String
    var age: Int

    constructor(name: String, age: Int) { // 부 생성자
        this.name = name
        this.age = age
    }

    fun introduce() {
        println("Hi, my name is $name and I am $age years old.")
    }
}

val person = Person("Charlie", 35)
person.introduce() // 출력: Hi, my name is Charlie and I am 35 years old.

4. 프로퍼티 초기화

class Person(name: String, age: Int) {
    var name = name
    var age = age
}

val person = Person("David", 40)
println("Name: ${person.name}, Age: ${person.age}") // 출력: Name: David, Age: 40

5. 초기화 블록

// **초기화 블록 (init) 사용**
class Person(val name: String, var age: Int) {
    init {
        println("Person initialized with name = $name and age = $age")
    }
}

val person = Person("Eve", 28) // 출력: Person initialized with name = Eve and age = 28

6. 기본값을 가진 클래스

class Person(val name: String = "Unknown", var age: Int = 0) {
    fun introduce() {
        println("Hi, my name is $name and I am $age years old.")
    }
}

val defaultPerson = Person() // 기본값 사용
defaultPerson.introduce() // 출력: Hi, my name is Unknown and I am 0 years old.

val customPerson = Person("Frank", 45) // 사용자 지정 값
customPerson.introduce() // 출력: Hi, my name is Frank and I am 45 years old.

7. 클래스의 주요 특징

1) 데이터 클래스

  • 데이터 저장용 클래스를 간결하게 선언.
data class User(val name: String, val age: Int)

val user = User("Alice", 25)
println(user) // 출력: User(name=Alice, age=25)

2) 상속

  • 클래스 상속은 open 키워드를 사용해야 가능.
open class Animal {
    open fun sound() {
        println("Some generic animal sound")
    }
}

class Dog : Animal() {
    override fun sound() {
        println("Bark")
    }
}

val dog = Dog()
dog.sound() // 출력: Bark

3) 인터페이스

  • Kotlin은 다중 상속 대신 인터페이스를 지원.
interface Flyable {
    fun fly()
}

class Bird : Flyable {
    override fun fly() {
        println("Bird is flying")
    }
}

val bird = Bird()
bird.fly() // 출력: Bird is flying

8. 객체 선언 (싱글톤)

object Singleton {
    var count = 0
    fun increment() {
        count++
    }
}

Singleton.increment()
println("Count: ${Singleton.count}") // 출력: Count: 1

요약

항목  설명 예제
클래스 정의 객체를 생성하기 위한 설계도. class Person { var name: String = "" }
인스턴스 생성 클래스에서 생성된 개별 객체. val person = Person()
생성자 객체 초기화를 위한 특별한 메서드. class Person(val name: String)
초기화 블록 초기화 로직을 정의하는 블록. init { ... }
데이터 클래스 데이터 저장 및 처리에 최적화된 클래스. data class User(val name: String)
상속 부모 클래스의 기능을 자식 클래스가 확장. class Dog : Animal()
인터페이스 다중 상속 대신 사용하는 구조. interface Flyable { fun fly() }
싱글톤 객체 하나의 인스턴스만 존재하는 객체. object Singleton { ... }

 

 

인터페이스

 

1. 인터페이스란?

  • 인터페이스는 클래스가 구현해야 할 메서드와 프로퍼티를 정의하는 틀입니다.
  • 인터페이스는 다중 상속을 지원하며, 구현체는 여러 인터페이스를 동시에 상속받을 수 있습니다.
  • 인터페이스는 구현부가 없는 추상 메서드와, 기본 구현을 가진 구체 메서드를 모두 포함할 수 있습니다.
  •  

2. 인터페이스 선언과 사용

// **인터페이스 선언**
interface Animal {
    fun sound() // 추상 메서드
    fun eat() { // 구체 메서드 (기본 구현 제공)
        println("This animal is eating.")
    }
}

// **클래스가 인터페이스 구현**
class Dog : Animal {
    override fun sound() {
        println("Bark")
    }
}

// **인스턴스 생성 및 사용**
val dog = Dog()
dog.sound() // 출력: Bark
dog.eat()   // 출력: This animal is eating.

3. 인터페이스와 다중 상속

// **두 개 이상의 인터페이스 정의**
interface Flyable {
    fun fly()
}

interface Swimable {
    fun swim()
}

// **클래스가 다중 인터페이스 구현**
class Duck : Flyable, Swimable {
    override fun fly() {
        println("Duck is flying.")
    }

    override fun swim() {
        println("Duck is swimming.")
    }
}

// **인스턴스 생성 및 사용**
val duck = Duck()
duck.fly()  // 출력: Duck is flying.
duck.swim() // 출력: Duck is swimming.

4. 인터페이스의 프로퍼티

// **인터페이스에서 프로퍼티 정의**
interface Shape {
    val name: String // 추상 프로퍼티
    fun area(): Double
}

// **클래스가 인터페이스 구현**
class Circle(val radius: Double) : Shape {
    override val name: String = "Circle" // 추상 프로퍼티 구현
    override fun area(): Double = Math.PI * radius * radius
}

val circle = Circle(5.0)
println("${circle.name} Area: ${circle.area()}") // 출력: Circle Area: 78.53981633974483

5. 인터페이스와 기본 구현 충돌

// **두 인터페이스의 기본 구현 충돌 처리**
interface A {
    fun show() {
        println("Interface A")
    }
}

interface B {
    fun show() {
        println("Interface B")
    }
}

// **클래스가 두 인터페이스를 구현**
class C : A, B {
    override fun show() {
        super<A>.show() // A의 show 호출
        super<B>.show() // B의 show 호출
    }
}

val c = C()
c.show()
// 출력:
// Interface A
// Interface B

6. 인터페이스 확장

// **인터페이스 확장**
interface Vehicle {
    fun drive()
}

interface ElectricVehicle : Vehicle {
    fun chargeBattery()
}

// **구현 클래스**
class Tesla : ElectricVehicle {
    override fun drive() {
        println("Tesla is driving.")
    }

    override fun chargeBattery() {
        println("Tesla is charging.")
    }
}

val tesla = Tesla()
tesla.drive()         // 출력: Tesla is driving.
tesla.chargeBattery() // 출력: Tesla is charging.

7. 인터페이스를 활용한 다형성

// **다형성 예제**
interface Animal {
    fun sound()
}

class Cat : Animal {
    override fun sound() {
        println("Meow")
    }
}

class Dog : Animal {
    override fun sound() {
        println("Bark")
    }
}

fun makeSound(animal: Animal) {
    animal.sound() // Animal 타입으로 다양한 구현 호출 가능
}

makeSound(Cat()) // 출력: Meow
makeSound(Dog()) // 출력: Bark

8. 요약

특징 설명 예제
인터페이스 정의 interface 키워드로 정의, 메서드와 프로퍼티 선언 가능. interface Flyable { fun fly() }
추상 메서드 구현체에서 반드시 오버라이드해야 하는 메서드. fun sound()
구체 메서드 기본 구현을 제공하며, 필요 시 오버라이드 가능. fun eat() { println("Eating") }
프로퍼티 추상 프로퍼티 또는 기본값을 가진 프로퍼티 선언 가능. val name: String
다중 상속 여러 인터페이스를 동시에 구현 가능. class Duck : Flyable, Swimable
다형성 지원 공통된 인터페이스를 통해 다양한 구현체를 동일한 방식으로 처리 가능. fun makeSound(animal: Animal)

 

상속

 

1. 상속이란?

  • 상속은 부모 클래스(상위 클래스)의 속성과 메서드를 자식 클래스(하위 클래스)가 물려받는 개념입니다.
  • Kotlin에서는 클래스가 기본적으로 final(상속 불가)로 설정되어 있으므로, 상속하려면 부모 클래스에 open 키워드를 붙여야 합니다.

2. 기본 상속 구조

// **부모 클래스**
open class Animal { // open 키워드를 붙여야 상속 가능
    open fun sound() { // open 키워드를 붙여야 오버라이드 가능
        println("Some generic animal sound")
    }
}

// **자식 클래스**
class Dog : Animal() { // Animal 클래스를 상속
    override fun sound() { // 부모 메서드를 오버라이드
        println("Bark")
    }
}

// **인스턴스 생성 및 사용**
val dog = Dog()
dog.sound() // 출력: Bark

3. 부모 클래스의 속성과 메서드 상속

// **부모 클래스**
open class Person(val name: String, var age: Int) {
    open fun introduce() {
        println("Hi, I'm $name and I'm $age years old.")
    }
}

// **자식 클래스**
class Student(name: String, age: Int, val grade: Int) : Person(name, age) { // 부모 생성자 호출
    override fun introduce() {
        super.introduce() // 부모 메서드 호출
        println("I'm in grade $grade.")
    }
}

// **사용 예제**
val student = Student("Alice", 20, 3)
student.introduce()
// 출력:
// Hi, I'm Alice and I'm 20 years old.
// I'm in grade 3.

4. 생성자와 상속

// **부모 클래스**
open class Vehicle(val brand: String) {
    init {
        println("Vehicle brand: $brand")
    }
}

// **자식 클래스**
class Car(brand: String, val model: String) : Vehicle(brand) {
    init {
        println("Car model: $model")
    }
}

// **사용 예제**
val car = Car("Toyota", "Corolla")
// 출력:
// Vehicle brand: Toyota
// Car model: Corolla

5. 상속에서의 메서드 호출 순서

open class Parent {
    init {
        println("Parent class initialized")
    }

    open fun greet() {
        println("Hello from Parent")
    }
}

class Child : Parent() {
    init {
        println("Child class initialized")
    }

    override fun greet() {
        println("Hello from Child")
    }
}

val child = Child()
child.greet()
// 출력:
// Parent class initialized
// Child class initialized
// Hello from Child

6. 상속에서의 final, open, override 키워드

  • final: 더 이상 상속이나 오버라이드가 불가능하게 설정.
  • open: 상속이나 오버라이드를 허용.
  • override: 부모 클래스의 메서드나 프로퍼티를 재정의.
open class Parent {
    open fun greet() {
        println("Hello from Parent")
    }

    final fun sayGoodbye() {
        println("Goodbye from Parent")
    }
}

class Child : Parent() {
    override fun greet() { // 부모 메서드를 오버라이드
        println("Hello from Child")
    }

    // fun sayGoodbye() {} // 컴파일 에러: final 메서드는 오버라이드 불가능
}

7. 추상 클래스

  • 추상 클래스는 인스턴스를 생성할 수 없으며, 일부 메서드나 프로퍼티를 구현하지 않고 남겨둠.
  • abstract 키워드를 사용하며, 반드시 하위 클래스에서 구현해야 함.
abstract class Shape {
    abstract fun area(): Double // 추상 메서드 (구현 없음)
    open fun display() { // 구체 메서드
        println("This is a shape.")
    }
}

class Circle(val radius: Double) : Shape() {
    override fun area(): Double = Math.PI * radius * radius // 추상 메서드 구현
}

val circle = Circle(5.0)
circle.display() // 출력: This is a shape.
println("Area: ${circle.area()}") // 출력: Area: 78.53981633974483

8. 인터페이스와 상속

  • Kotlin은 다중 상속을 지원하지 않지만, 다중 인터페이스 구현이 가능합니다.
interface Flyable {
    fun fly() {
        println("Flying...")
    }
}

open class Bird {
    open fun sound() {
        println("Tweet")
    }
}

class Eagle : Bird(), Flyable { // Bird를 상속하고 Flyable 인터페이스 구현
    override fun sound() {
        println("Screech")
    }
}

val eagle = Eagle()
eagle.sound() // 출력: Screech
eagle.fly()   // 출력: Flying...

9. 요약

키워드 설명 예제
open 상속 및 오버라이드 가능. open class Parent { open fun greet() }
final 상속 및 오버라이드 불가능. final fun sayGoodbye()
override 부모의 메서드나 프로퍼티를 재정의. override fun greet()
abstract 구현되지 않은 메서드나 프로퍼티를 정의. abstract fun area(): Double

10. 상속의 장점

  1. 코드 재사용성: 공통된 로직을 부모 클래스에 정의하여 중복 코드를 줄임.
  2. 유지보수 용이: 부모 클래스 수정만으로 자식 클래스의 동작도 일관성 유지.
  3. 다형성 구현: 동일한 부모 클래스를 공유하는 객체를 일관된 방식으로 처리 가능.

 

11. 상속의 단점

  • 유연성, 확장성이 떨어져요
  • 다중 상속에 의해 문제가 발생할 수 있어요. (그래서 kotlin에서는 다중상속을 막아놓았어요)
  • 자식 클래스에서 부모 클래스의 내부 구조를 잘 알아야만 해요

 

 

의존성 주입 (DI)

이것은 "의존성 주입"(Dependency Injection)과 관련된 개념으로, 중복되는 로직을 별도의 공통 객체로 추출하여, 이를 다른 클래스에서 재사용하는 방식입니다. 이는 **SOLID 원칙 중 DIP(의존성 역전 원칙)**에 해당하며, 코드의 재사용성과 유지보수성을 크게 향상시킵니다.


구현 방법

1. 중복되는 로직을 갖는 공통 객체 정의

  • 공통 로직을 별도의 인터페이스클래스로 정의합니다.

2. 공통 객체를 호출하는 클래스에 주입

  • 의존성 주입을 사용하여 공통 객체를 호출하는 클래스에 전달합니다.

구현 예제

1. 중복 로직이 들어갈 객체 정의

// **1. 인터페이스 정의**
interface CommonLogic {
    fun executeLogic(data: String): String
}

// **2. 구현체 정의**
class CommonLogicImpl : CommonLogic {
    override fun executeLogic(data: String): String {
        return "Processed: $data" // 중복 로직 구현
    }
}

2. 공통 객체를 주입받아 사용하는 클래스

// **1. 주입받아 사용하는 클래스**
class Service(private val commonLogic: CommonLogic) {
    fun performTask(input: String): String {
        return commonLogic.executeLogic(input) // 주입받은 객체의 로직 호출
    }
}

3. 의존성 주입 및 사용

fun main() {
    // **공통 로직 구현체 생성**
    val commonLogic = CommonLogicImpl()

    // **Service 클래스에 주입**
    val service = Service(commonLogic)

    // **Service 메서드 호출**
    val result = service.performTask("Hello Kotlin")
    println(result) // 출력: Processed: Hello Kotlin
}

의존성 주입(DI) 활용

의존성 주입을 직접 구현하는 방식

fun main() {
    val commonLogic = CommonLogicImpl() // 의존성 객체 생성
    val service = Service(commonLogic) // 생성자 주입

    println(service.performTask("DI Example")) // 출력: Processed: DI Example
}

의존성 주입 프레임워크 활용 (예: Spring, Koin)

// Koin DI 예제
import org.koin.core.context.startKoin
import org.koin.dsl.module

// **1. 모듈 정의**
val appModule = module {
    single<CommonLogic> { CommonLogicImpl() } // CommonLogic에 CommonLogicImpl 주입
    factory { Service(get()) } // Service에 CommonLogic 주입
}

// **2. Koin 시작**
fun main() {
    startKoin {
        modules(appModule)
    }

    // **3. Koin으로 객체 가져오기**
    val service: Service = getKoin().get()
    println(service.performTask("Koin DI")) // 출력: Processed: Koin DI
}

장점

  1. 중복 제거: 중복 로직을 공통 객체로 추출하여 재사용성 증가.
  2. 유지보수성 향상: 공통 로직 수정 시, 한 곳에서 수정하면 모든 클래스에 반영.
  3. 의존성 관리 용이: 의존성 주입을 통해 클래스 간 결합도를 낮춤.
  4. 테스트 용이성: Mock 객체를 주입하여 테스트 가능.

요약

구성 요소  설명
공통 객체 정의 인터페이스나 클래스에 중복 로직 구현.
주입받는 클래스 공통 객체를 의존성으로 받아 로직 호출.
의존성 주입 방법 직접 주입(생성자, 메서드) 또는 DI 프레임워크(Koin, Dagger 등) 활용.

 

 

생성자

 

1. 생성자란?

  • **생성자(Constructor)**는 객체 생성 시 호출되는 함수.
  • Kotlin은 두 가지 생성자를 제공:
    • 주 생성자(Primary Constructor): 클래스 헤더에서 선언.
    • 부 생성자(Secondary Constructor): 클래스 본문에서 선언.


2. 주 생성자 (Primary Constructor)

1) 기본 주 생성자

// **주 생성자 정의**
class Person(val name: String, val age: Int)

// **객체 생성**
val person = Person("Alice", 25)
println("Name: ${person.name}, Age: ${person.age}")
// 출력: Name: Alice, Age: 25

2) 주 생성자와 초기화 블록

// **init 블록을 사용한 초기화**
class Person(val name: String, val age: Int) {
    init {
        println("Person initialized with name = $name and age = $age")
    }
}

// **객체 생성**
val person = Person("Bob", 30)
// 출력: Person initialized with name = Bob and age = 30

3) 디폴트 값을 가진 주 생성자

// **디폴트 값 설정**
class Person(val name: String = "Unknown", val age: Int = 0)

// **객체 생성**
val person1 = Person() // 기본값 사용
val person2 = Person("Charlie", 35) // 커스텀 값 사용

println("Person1: ${person1.name}, ${person1.age}") // 출력: Person1: Unknown, 0
println("Person2: ${person2.name}, ${person2.age}") // 출력: Person2: Charlie, 35

3. 부 생성자 (Secondary Constructor)

1) 기본 부 생성자

// **부 생성자 정의**
class Person {
    var name: String
    var age: Int

    constructor(name: String, age: Int) { // 부 생성자
        this.name = name
        this.age = age
    }
}

// **객체 생성**
val person = Person("Dave", 40)
println("Name: ${person.name}, Age: ${person.age}")
// 출력: Name: Dave, Age: 40

2) 주 생성자와 부 생성자 동시 사용

// **주 생성자와 부 생성자**
class Person(val name: String) {
    var age: Int = 0

    constructor(name: String, age: Int) : this(name) { // 주 생성자 호출
        this.age = age
    }
}

// **객체 생성**
val person = Person("Eve", 28)
println("Name: ${person.name}, Age: ${person.age}")
// 출력: Name: Eve, Age: 28

4. 데이터 클래스와 생성자

  • 데이터 클래스는 주 생성자에서 속성을 정의.
data class User(val id: Int, val name: String)

// **객체 생성**
val user = User(1, "Alice")
println(user)
// 출력: User(id=1, name=Alice)

5. 생성자와 가시성 (Visibility)

  • 생성자에 가시성 변경자를 추가해 접근 제어 가능.
class Person private constructor(val name: String) { // private 생성자
    companion object {
        fun create(name: String): Person {
            return Person(name)
        }
    }
}

// **객체 생성**
val person = Person.create("Alice")
println("Name: ${person.name}")
// 출력: Name: Alice

6. 추상 클래스와 생성자

// **추상 클래스**
abstract class Animal(val name: String) {
    abstract fun sound()
}

class Dog(name: String) : Animal(name) {
    override fun sound() {
        println("$name says: Woof!")
    }
}

// **객체 생성**
val dog = Dog("Buddy")
dog.sound()
// 출력: Buddy says: Woof!

요약

유형  설명 예제
주 생성자 클래스 헤더에서 선언되는 기본 생성자. class Person(val name: String)
부 생성자 클래스 본문에서 선언되며 주 생성자를 호출하거나 별도의 로직 추가 가능. constructor(name: String)
init 블록 주 생성자와 함께 사용되며, 초기화 로직을 정의. init { ... }
디폴트 값 주 생성자에 기본값을 제공하여 선택적 매개변수 구현 가능. val name: String = "Unknown"
데이터 클래스 데이터를 저장하기 위한 클래스로, 주 생성자에서 속성을 정의. data class User(val id: Int, name: String)
가시성 제어 생성자의 접근 범위를 제한. class Person private constructor(...)

7. 생성자 사용 예제

fun main() {
    // 주 생성자를 사용한 객체 생성
    val person = Person("Alice", 25)
    println("Name: ${person.name}, Age: ${person.age}")

    // 데이터 클래스를 사용한 객체 생성
    val user = User(1, "Bob")
    println(user)

    // 부 생성자를 사용한 객체 생성
    val person2 = Person("Charlie", 30)
    println("Name: ${person2.name}, Age: ${person2.age}")
}

 

 

접근제한자

 

1. 접근 제한자란?

  • 접근 제한자는 클래스, 함수, 프로퍼티, 생성자 등에 대한 접근 범위를 제어합니다.
  • Kotlin에서 지원하는 접근 제한자:
    • public: 어디서나 접근 가능 (기본값).
    • private: 해당 선언이 속한 클래스 또는 파일 내부에서만 접근 가능.
    • protected: 클래스와 그 하위 클래스에서만 접근 가능.
    • internal: 같은 모듈(컴파일 단위) 내에서만 접근 가능.

2. 접근 제한자 종류 및 사용 예제


1) public (기본값)

  • 모든 클래스, 파일, 모듈에서 접근 가능.
  • 선언하지 않으면 기본적으로 public으로 설정됨.
class PublicExample { // public이 기본값
    public val name = "Public Property" // 명시적 public
    fun show() {
        println("This is a public function.")
    }
}

val obj = PublicExample()
println(obj.name) // 접근 가능
obj.show() // 접근 가능

2) private

  • 선언된 클래스 또는 파일 내부에서만 접근 가능.
  • 클래스 외부다른 파일에서는 접근 불가.
class PrivateExample {
    private val secret = "Private Property" // private 프로퍼티
    fun accessSecret() {
        println(secret) // 클래스 내부에서 접근 가능
    }
}

val obj = PrivateExample()
// println(obj.secret) // 오류: secret은 private
obj.accessSecret() // 정상: 클래스 내부에서 secret 접근
  • 파일 스코프에서의 private
private fun fileScopedFunction() {
    println("This function is private to this file.")
}

// 다른 파일에서는 fileScopedFunction() 호출 불가
fileScopedFunction() // 동일 파일에서만 호출 가능

3) protected

  • 선언된 클래스 및 그 하위 클래스에서만 접근 가능.
  • 다른 클래스에서는 접근 불가.
open class Parent {
    protected val protectedProperty = "Protected Property"
}

class Child : Parent() {
    fun showProtected() {
        println(protectedProperty) // 하위 클래스에서 접근 가능
    }
}

val child = Child()
// println(child.protectedProperty) // 오류: protected는 외부에서 접근 불가
child.showProtected() // 정상 실행

4) internal

  • 같은 모듈 내에서만 접근 가능.
  • 다른 모듈(예: 다른 라이브러리)에서는 접근 불가.
internal class InternalExample {
    internal val internalProperty = "Internal Property"
    internal fun internalFunction() {
        println("This is an internal function.")
    }
}

// 같은 모듈에서는 접근 가능
val internalObj = InternalExample()
println(internalObj.internalProperty) // 정상
internalObj.internalFunction() // 정상
  • 다른 모듈에서의 접근
// 다른 모듈에서는 접근 불가
val internalObj = InternalExample()
// println(internalObj.internalProperty) // 오류

5) 생성자와 접근 제한자

  • 생성자에도 접근 제한자를 사용할 수 있음.
class Restricted private constructor() { // private 생성자
    companion object {
        fun create(): Restricted {
            return Restricted() // 클래스 내부에서는 호출 가능
        }
    }
}

val obj = Restricted.create() // 생성자는 외부에서 직접 호출 불가
// val obj2 = Restricted() // 오류: private 생성자

3. 접근 제한자 비교

제한자 클래스 내부 하위 클래스 같은 파일  같은 모듈 다른 모듈
public
private
protected
internal

4. 주요 사용 사례

제한자 사용 사례
public 대부분의 일반적인 코드, API, 또는 외부 모듈에 제공되는 클래스/메서드.
private 클래스 내부에서만 사용하는 데이터나 메서드를 숨기고 싶을 때.
protected 상속을 사용하는 경우 부모 클래스에서 하위 클래스에 로직을 공유할 때.
internal 같은 모듈 내에서만 사용되며 외부에 노출되지 않아야 하는 코드.

 

 

오버라이딩

 

1. 오버라이딩이란?

  • 오버라이딩은 부모 클래스에서 정의한 메서드나 프로퍼티를 자식 클래스에서 재정의하는 것.
  • Kotlin에서는 오버라이딩하려면 부모 메서드나 프로퍼티에 open 키워드를 붙이고, 자식 클래스에서는 override 키워드를 사용해야 함.

2. 메서드 오버라이딩

// **부모 클래스**
open class Animal {
    open fun sound() { // open 키워드를 사용해야 오버라이드 가능
        println("Some generic animal sound")
    }
}

// **자식 클래스**
class Dog : Animal() {
    override fun sound() { // 부모의 sound() 메서드 재정의
        println("Bark")
    }
}

// **사용 예제**
val animal: Animal = Dog() // 다형성
animal.sound() // 출력: Bark

3. 프로퍼티 오버라이딩

  • 프로퍼티도 open 키워드가 있어야 오버라이딩 가능.
  • val로 선언된 프로퍼티는 val이나 var로 오버라이딩 가능.
  • var로 선언된 프로퍼티는 반드시 var로만 오버라이딩 가능.
// **부모 클래스**
open class Animal {
    open val type: String = "Unknown Animal"
}

// **자식 클래스**
class Dog : Animal() {
    override val type: String = "Dog" // 부모의 type 프로퍼티 재정의
}

// **사용 예제**
val dog = Dog()
println(dog.type) // 출력: Dog

4. 메서드와 프로퍼티 오버라이딩

// **부모 클래스**
open class Shape {
    open val name: String = "Shape"
    open fun area(): Double {
        return 0.0
    }
}

// **자식 클래스**
class Circle(val radius: Double) : Shape() {
    override val name: String = "Circle" // 프로퍼티 재정의
    override fun area(): Double { // 메서드 재정의
        return Math.PI * radius * radius
    }
}

// **사용 예제**
val circle = Circle(5.0)
println("Shape: ${circle.name}, Area: ${circle.area()}") 
// 출력: Shape: Circle, Area: 78.53981633974483

5. 부모 메서드 호출 (super)

  • 오버라이드된 메서드에서 부모 클래스의 메서드를 호출할 때 super 사용.
open class Animal {
    open fun sound() {
        println("Some generic animal sound")
    }
}

class Dog : Animal() {
    override fun sound() {
        super.sound() // 부모 클래스의 메서드 호출
        println("Bark") // 추가 로직
    }
}

// **사용 예제**
val dog = Dog()
dog.sound()
// 출력:
// Some generic animal sound
// Bark

6. 오버라이딩 제한 (final)

  • final 키워드를 사용하면 해당 메서드나 프로퍼티는 오버라이딩 불가.
open class Animal {
    open fun eat() {
        println("Animal is eating")
    }

    final fun sleep() { // 오버라이딩 불가
        println("Animal is sleeping")
    }
}

class Dog : Animal() {
    override fun eat() {
        println("Dog is eating")
    }

    // override fun sleep() { ... } // 오류: final로 선언된 메서드는 오버라이딩 불가
}

7. 추상 클래스와 오버라이딩

  • 추상 클래스에서는 추상 메서드를 선언 가능.
  • 추상 메서드는 반드시 하위 클래스에서 오버라이딩해야 함.
// **추상 클래스**
abstract class Animal {
    abstract fun sound() // 구현 없음 (추상 메서드)
    open fun eat() { // 기본 구현 제공
        println("Animal is eating")
    }
}

// **구현 클래스**
class Dog : Animal() {
    override fun sound() {
        println("Bark") // 추상 메서드 구현 필수
    }
}

// **사용 예제**
val dog = Dog()
dog.sound() // 출력: Bark
dog.eat()   // 출력: Animal is eating

8. 요약

키워드 설명 예제
open 부모 클래스에서 메서드나 프로퍼티를 오버라이드 가능하게 설정. open fun sound()
override 자식 클래스에서 부모의 메서드나 프로퍼티를 재정의. override fun sound()
super 부모 클래스의 메서드나 프로퍼티에 접근. super.sound()
final 오버라이드를 제한. final fun sleep()
추상 메서드 반드시 하위 클래스에서 구현해야 하는 메서드. abstract fun sound()

9. 오버라이딩의 장점

  1. 코드 재사용성: 부모 클래스의 로직을 활용하여 중복 코드 제거.
  2. 다형성 지원: 상위 타입의 참조로 다양한 하위 클래스의 동작을 처리 가능.
  3. 유연성: 기본 동작을 수정하거나 확장 가능.

 

 

오버로딩

 

1. 오버로딩이란?

  • **오버로딩(Overloading)**은 같은 이름의 메서드나 생성자를 매개변수의 개수, 타입, 순서가 다르게 정의하는 것.
  • 컴파일러는 메서드 호출 시 매개변수의 타입과 개수를 기준으로 적절한 메서드를 선택.

2. 메서드 오버로딩

class Calculator {
    // **정수 두 개의 합**
    fun add(a: Int, b: Int): Int {
        return a + b
    }

    // **실수 두 개의 합**
    fun add(a: Double, b: Double): Double {
        return a + b
    }

    // **세 개의 정수의 합**
    fun add(a: Int, b: Int, c: Int): Int {
        return a + b + c
    }
}

// **사용 예제**
val calculator = Calculator()
println(calculator.add(2, 3))          // 출력: 5
println(calculator.add(2.5, 3.5))      // 출력: 6.0
println(calculator.add(1, 2, 3))       // 출력: 6

3. 생성자 오버로딩

  • 클래스의 생성자도 매개변수의 종류와 개수를 다르게 설정하여 오버로딩 가능.
class Person {
    var name: String
    var age: Int

    // **기본 생성자**
    constructor() {
        this.name = "Unknown"
        this.age = 0
    }

    // **이름만 받는 생성자**
    constructor(name: String) {
        this.name = name
        this.age = 0
    }

    // **이름과 나이를 받는 생성자**
    constructor(name: String, age: Int) {
        this.name = name
        this.age = age
    }
}

// **사용 예제**
val person1 = Person()
val person2 = Person("Alice")
val person3 = Person("Bob", 25)

println("${person1.name}, ${person1.age}") // 출력: Unknown, 0
println("${person2.name}, ${person2.age}") // 출력: Alice, 0
println("${person3.name}, ${person3.age}") // 출력: Bob, 25

4. 확장 함수 오버로딩

// **Int 확장 함수**
fun Int.times(value: Int): Int = this * value

// **Double 확장 함수**
fun Double.times(value: Int): Double = this * value

// **사용 예제**
val intResult = 5.times(2) // Int 타입 확장 함수 호출
val doubleResult = 5.5.times(2) // Double 타입 확장 함수 호출

println(intResult) // 출력: 10
println(doubleResult) // 출력: 11.0

5. 매개변수 기본값과 오버로딩

  • 기본값을 사용하여 오버로딩 대체 가능.
class Greeter {
    fun greet(message: String = "Hello", name: String = "Guest") {
        println("$message, $name!")
    }
}

// **사용 예제**
val greeter = Greeter()
greeter.greet()                    // 기본값 사용: 출력: Hello, Guest!
greeter.greet("Hi")                // 이름 기본값: 출력: Hi, Guest!
greeter.greet("Hi", "Alice")       // 출력: Hi, Alice!

6. 오버로딩의 주의점

  • 매개변수의 타입이나 개수가 같고 반환 타입만 다르면 컴파일러가 구분하지 못해 오류 발생.
class Example {
    // 오류: 반환 타입만 다른 경우 컴파일 불가
    // fun process(): Int = 0
    // fun process(): String = ""
}

7. 요약

항목 설명 예제
메서드 오버로딩 매개변수의 타입, 개수, 순서가 다르게 동일 이름의 메서드를 여러 개 정의. fun add(a: Int, b: Int)
생성자 오버로딩 클래스의 생성자를 매개변수의 개수와 타입에 따라 여러 개 정의. constructor(name: String, age: Int)
확장 함수 오버로딩 동일한 이름의 확장 함수를 타입에 따라 정의. fun Int.times(value: Int)
기본값 활용 매개변수 기본값으로 오버로딩을 대체하여 코드 간결화. fun greet(message: String = "Hello")

8. 오버로딩의 장점

  1. 코드 가독성 향상: 같은 이름으로 다양한 동작을 처리할 수 있어 가독성이 높아짐.
  2. 유연성 증가: 사용자가 다양한 방식으로 메서드나 생성자를 호출 가능.
  3. 코드 재사용성 증가: 같은 이름으로 다양한 동작을 구현하여 중복 코드 제거.

 

 

다양한 클래스들

 

1. 데이터 클래스 (data class)

  • 데이터를 저장하기 위해 설계된 클래스.
  • data 키워드를 사용하면 기본적인 메서드(예: toString, equals, hashCode, copy)가 자동으로 생성.
  • 특징:
    • 주 생성자에 최소 하나 이상의 프로퍼티를 선언해야 함.
    • 데이터만 관리하는 클래스에 적합.
// **데이터 클래스 선언**
data class User(val id: Int, val name: String)

fun main() {
    val user1 = User(1, "Alice")
    val user2 = User(1, "Alice")

    // **기본 메서드 사용**
    println(user1) // 출력: User(id=1, name=Alice) -> toString()
    println(user1 == user2) // 출력: true -> equals()
    println(user1.copy(name = "Bob")) // 출력: User(id=1, name=Bob) -> copy()
}

2. 열거 클래스 (enum class)

  • 상수값을 관리하기 위한 클래스.
  • 상수와 관련된 속성이나 메서드를 정의할 수 있음.
  • 특징:
    • 관리할 상수값을 명확히 정의.
    • 각 상수는 고유한 값이나 동작을 가질 수 있음.
// **기본 열거 클래스**
enum class ProgrammingLanguage {
    C, JAVA, KOTLIN
}

// **속성과 메서드를 가진 열거 클래스**
enum class ProgrammingLanguageWithInt(val code: Int) {
    C(10),
    JAVA(20),
    KOTLIN(30);

    fun description() = "Code: $code, Language: $name"
}

fun main() {
    println(ProgrammingLanguage.C) // 출력: C
    println(ProgrammingLanguageWithInt.KOTLIN.code) // 출력: 30
    println(ProgrammingLanguageWithInt.KOTLIN.name) // 출력: KOTLIN
    println(ProgrammingLanguageWithInt.KOTLIN.description()) // 출력: Code: 30, Language: KOTLIN
}

3. 실드 클래스 (sealed class)

  • 상속을 제한하고, 미리 정의된 자식 클래스만 허용.
  • 특징:
    • 새로운 자식 클래스를 추가하려면 같은 파일에서만 정의 가능.
    • when 표현식과 함께 사용하면, 모든 경우를 처리했는지 컴파일러가 확인 가능.
    • 불필요한 상속을 방지하고, 구조적 안정성을 제공.
// **실드 클래스 선언**
sealed class Shape {
    data class Circle(val radius: Double) : Shape()
    data class Rectangle(val width: Double, val height: Double) : Shape()
    object Unknown : Shape()
}

fun describeShape(shape: Shape): String {
    return when (shape) {
        is Shape.Circle -> "Circle with radius ${shape.radius}"
        is Shape.Rectangle -> "Rectangle with width ${shape.width} and height ${shape.height}"
        Shape.Unknown -> "Unknown shape"
    }
}

fun main() {
    val circle = Shape.Circle(5.0)
    val rectangle = Shape.Rectangle(4.0, 6.0)
    val unknown = Shape.Unknown

    println(describeShape(circle)) // 출력: Circle with radius 5.0
    println(describeShape(rectangle)) // 출력: Rectangle with width 4.0 and height 6.0
    println(describeShape(unknown)) // 출력: Unknown shape
}

요약

클래스  설명  주요 기능
데이터 클래스 데이터 저장 및 처리용 클래스. toString, equals, hashCode, copy 자동 생성.
열거 클래스 상수를 그룹화하여 관리. 상수별 속성 및 메서드 정의 가능.
실드 클래스 상속 가능한 클래스와 자식 클래스를 제한하여 안전성을 높임. when 표현식과 함께 모든 경우 처리 보장.

사용 사례

  1. 데이터 클래스:
    • 사용자 정보, 설정값 저장 등 데이터 중심의 로직에서 사용.
  2. 열거 클래스:
    • HTTP 상태 코드, 프로그래밍 언어 목록 등 고정된 상수값 관리.
  3. 실드 클래스:
    • API 응답 모델, 상태(State) 관리, 예외 처리 등 구조적인 상속 설계.

장점

  1. 데이터 클래스: 코드 간결화, 기본 메서드 자동 생성으로 생산성 증가.
  2. 열거 클래스: 상수값 관리 일원화, 가독성과 유지보수성 향상.
  3. 실드 클래스: 무분별한 상속 방지, 안정성 있는 설계 가능.

 

'Back-End (Web) > Kotlin' 카테고리의 다른 글

[Kotlin] Kotlin의 테스트코드  (0) 2025.02.16
[Kotlin] Kotlin의 사용  (0) 2025.02.11
[Kotlin] Kotlin이란?  (0) 2025.02.10

+ Recent posts