객체지향 프로그래밍 기초
📌 책임, 역할, 상호작용
- 객체지향 프로그래밍(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. 상속의 장점
- 코드 재사용성: 공통된 로직을 부모 클래스에 정의하여 중복 코드를 줄임.
- 유지보수 용이: 부모 클래스 수정만으로 자식 클래스의 동작도 일관성 유지.
- 다형성 구현: 동일한 부모 클래스를 공유하는 객체를 일관된 방식으로 처리 가능.
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
}
장점
- 중복 제거: 중복 로직을 공통 객체로 추출하여 재사용성 증가.
- 유지보수성 향상: 공통 로직 수정 시, 한 곳에서 수정하면 모든 클래스에 반영.
- 의존성 관리 용이: 의존성 주입을 통해 클래스 간 결합도를 낮춤.
- 테스트 용이성: 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. 오버로딩이란?
- **오버로딩(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. 데이터 클래스 (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 표현식과 함께 모든 경우 처리 보장. |
사용 사례
- 데이터 클래스:
- 사용자 정보, 설정값 저장 등 데이터 중심의 로직에서 사용.
- 열거 클래스:
- HTTP 상태 코드, 프로그래밍 언어 목록 등 고정된 상수값 관리.
- 실드 클래스:
- API 응답 모델, 상태(State) 관리, 예외 처리 등 구조적인 상속 설계.
장점
- 데이터 클래스: 코드 간결화, 기본 메서드 자동 생성으로 생산성 증가.
- 열거 클래스: 상수값 관리 일원화, 가독성과 유지보수성 향상.
- 실드 클래스: 무분별한 상속 방지, 안정성 있는 설계 가능.
'Back-End (Web) > Kotlin' 카테고리의 다른 글
[Kotlin] Kotlin의 테스트코드 (0) | 2025.02.16 |
---|---|
[Kotlin] Kotlin의 사용 (0) | 2025.02.11 |
[Kotlin] Kotlin이란? (0) | 2025.02.10 |