Notice
Recent Posts
Recent Comments
Link
반응형
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- 자바스크립트
- 오블완
- 티스토리챌린지
- 이차전지관련주
- Kotlin
- Effective Java 3
- ElasticSearch
- 이펙티브 자바
- 스프링
- 스프링 핵심원리
- 알고리즘
- 카카오
- 알고리즘정렬
- Sort
- 엘라스틱서치
- 이펙티브자바
- Spring
- effectivejava
- java
- 예제로 배우는 스프링 입문
- 스프링부트
- k8s
- 클린아키텍처
- 자바
- 스프링핵심원리
- kubernetes
- JavaScript
- 함수형프로그래밍
- 김영한
- Effective Java
Archives
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin 상속과 인터페이스 완벽 가이드 | open, abstract, sealed 본문
반응형
이 글을 읽으면: Java의 복잡한 상속 구조를 넘어 Kotlin의 안전하고 명확한 상속 시스템을 배울 수 있습니다. open, abstract, sealed 클래스와 인터페이스 다중 상속까지 실전 예제로 완벽하게 마스터하세요.
📌 목차
- 들어가며 - Java 상속의 문제점
- 기본적으로 final - open 키워드
- 추상 클래스 - abstract
- 인터페이스 - interface
- Sealed Class - 제한된 클래스 계층
- 위임 - by 키워드
- 실전 디자인 패턴
- 마무리 - 다음 편 예고
들어가며 - Java 상속의 문제점
Java로 개발하면서 이런 경험 있으신가요?
// Java - 모든 클래스가 기본적으로 상속 가능
public class User {
private String name;
public String getName() {
return name;
}
}
// 의도하지 않았지만 누군가 상속
public class HackedUser extends User {
@Override
public String getName() {
// 악의적인 코드
System.out.println("사용자 정보 유출!");
return super.getName();
}
}
// final로 막아야 함
public final class SecureUser {
// ...
}
실제 프로젝트에서 겪은 문제:
상황: 라이브러리 클래스를 상속받아 사용
문제: 라이브러리 업데이트 시 내부 구현 변경으로 하위 클래스 전부 깨짐
원인: Fragile Base Class Problem (깨지기 쉬운 기반 클래스 문제)
Kotlin의 해결책:
// Kotlin - 기본적으로 final (상속 불가)
class User(val name: String)
// 상속하려면 명시적으로 open
open class OpenUser(val name: String)
class CustomUser(name: String) : OpenUser(name)
// Sealed로 제한된 상속만 허용
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
}
오늘은 Kotlin의 상속 시스템을 완전히 마스터해보겠습니다!

open 키워드
기본적으로 final
Kotlin의 핵심 철학: 기본적으로 상속을 막아 안전성 확보
// 기본 클래스 - final (상속 불가)
class User(val name: String)
// 상속 시도
// class AdminUser : User("admin") // ❌ 컴파일 에러
// This type is final, so it cannot be inherited from
open으로 상속 허용
// 상속 가능하게 만들기
open class User(val name: String) {
open fun greet() {
println("안녕하세요, $name님!")
}
}
class AdminUser(name: String) : User(name) {
override fun greet() {
println("관리자 $name님 환영합니다!")
}
}
fun main() {
val user = User("규철")
user.greet() // 안녕하세요, 규철님!
val admin = AdminUser("관리자")
admin.greet() // 관리자 관리자님 환영합니다!
}
메서드 오버라이드
open class Animal(val name: String) {
open fun sound() {
println("동물 소리")
}
// final 메서드 (오버라이드 불가)
fun eat() {
println("먹는 중...")
}
}
class Dog(name: String) : Animal(name) {
override fun sound() {
println("멍멍!")
}
// override fun eat() { } // ❌ 에러 (final 메서드)
}
fun main() {
val dog = Dog("바둑이")
dog.sound() // 멍멍!
dog.eat() // 먹는 중...
}
프로퍼티 오버라이드
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
class Triangle : Shape() {
override val vertexCount = 3
}
fun main() {
val rect = Rectangle()
println("사각형 꼭지점: ${rect.vertexCount}") // 4
val triangle = Triangle()
println("삼각형 꼭지점: ${triangle.vertexCount}") // 3
}
생성자와 상속
open class Person(val name: String, val age: Int) {
init {
println("Person 생성: $name, $age세")
}
}
class Student(
name: String,
age: Int,
val studentId: String
) : Person(name, age) {
init {
println("Student 생성: 학번 $studentId")
}
}
fun main() {
val student = Student("규철", 20, "2024001")
// Person 생성: 규철, 20세
// Student 생성: 학번 2024001
}
Java와 비교
// Java - 기본적으로 상속 가능
public class User {
private String name;
public void greet() {
System.out.println("안녕하세요");
}
}
// 상속 막으려면 final 명시
public final class SecureUser {
// ...
}
// Kotlin - 기본적으로 상속 불가
class User(val name: String)
// 상속하려면 open 명시
open class OpenUser(val name: String)
추상 클래스 - abstract
기본 추상 클래스
abstract class Shape {
abstract val area: Double
abstract fun draw()
// 일반 메서드도 가능
fun describe() {
println("도형의 넓이: $area")
}
}
class Circle(val radius: Double) : Shape() {
override val area: Double
get() = Math.PI * radius * radius
override fun draw() {
println("원 그리기")
}
}
class Rectangle(val width: Double, val height: Double) : Shape() {
override val area = width * height
override fun draw() {
println("사각형 그리기")
}
}
fun main() {
// val shape = Shape() // ❌ 추상 클래스는 인스턴스화 불가
val circle = Circle(5.0)
circle.draw()
circle.describe()
// 원 그리기
// 도형의 넓이: 78.53981633974483
val rect = Rectangle(10.0, 5.0)
rect.draw()
rect.describe()
// 사각형 그리기
// 도형의 넓이: 50.0
}
실전 예제 - Repository 패턴
abstract class Repository<T> {
private val items = mutableListOf<T>()
// 추상 메서드
abstract fun validate(item: T): Boolean
// 구현된 메서드
fun save(item: T): Boolean {
return if (validate(item)) {
items.add(item)
true
} else {
false
}
}
fun findAll(): List<T> = items.toList()
}
data class User(val id: Long, val email: String)
class UserRepository : Repository<User>() {
override fun validate(item: User): Boolean {
return item.email.contains("@")
}
}
fun main() {
val repo = UserRepository()
repo.save(User(1, "user@example.com")) // true
repo.save(User(2, "invalid")) // false
println(repo.findAll()) // [User(id=1, email=user@example.com)]
}
추상 프로퍼티
abstract class Vehicle {
abstract val maxSpeed: Int
abstract val fuelType: String
fun displayInfo() {
println("최고 속도: ${maxSpeed}km/h")
println("연료: $fuelType")
}
}
class ElectricCar : Vehicle() {
override val maxSpeed = 200
override val fuelType = "전기"
}
class GasCar : Vehicle() {
override val maxSpeed = 180
override val fuelType = "휘발유"
}
fun main() {
val tesla = ElectricCar()
tesla.displayInfo()
val sedan = GasCar()
sedan.displayInfo()
}
인터페이스 - interface
기본 인터페이스
interface Clickable {
fun click()
// 기본 구현 가능
fun showClick() {
println("클릭됨!")
}
}
interface Focusable {
fun focus()
fun showFocus() {
println("포커스됨!")
}
}
// 다중 인터페이스 구현
class Button : Clickable, Focusable {
override fun click() {
println("버튼 클릭")
}
override fun focus() {
println("버튼 포커스")
}
}
fun main() {
val button = Button()
button.click() // 버튼 클릭
button.showClick() // 클릭됨!
button.focus() // 버튼 포커스
button.showFocus() // 포커스됨!
}
인터페이스 프로퍼티
interface Named {
val name: String // 추상 프로퍼티
val displayName: String // 커스텀 getter
get() = "이름: $name"
}
class Person(override val name: String) : Named
class Company(override val name: String) : Named {
override val displayName: String
get() = "회사명: $name"
}
fun main() {
val person = Person("규철")
println(person.displayName) // 이름: 규철
val company = Company("테크컴퍼니")
println(company.displayName) // 회사명: 테크컴퍼니
}
인터페이스 충돌 해결
interface A {
fun foo() {
println("A의 foo")
}
}
interface B {
fun foo() {
println("B의 foo")
}
}
class C : A, B {
override fun foo() {
super<A>.foo() // A의 구현 호출
super<B>.foo() // B의 구현 호출
println("C의 foo")
}
}
fun main() {
val c = C()
c.foo()
// A의 foo
// B의 foo
// C의 foo
}
실전 예제 - 플러그인 시스템
interface Plugin {
val name: String
val version: String
fun initialize()
fun shutdown()
fun getDescription(): String {
return "$name v$version"
}
}
class LoggingPlugin : Plugin {
override val name = "Logging"
override val version = "1.0.0"
override fun initialize() {
println("로깅 플러그인 초기화")
}
override fun shutdown() {
println("로깅 플러그인 종료")
}
}
class CachePlugin : Plugin {
override val name = "Cache"
override val version = "2.0.0"
override fun initialize() {
println("캐시 플러그인 초기화")
}
override fun shutdown() {
println("캐시 플러그인 종료")
}
}
class PluginManager {
private val plugins = mutableListOf<Plugin>()
fun register(plugin: Plugin) {
plugins.add(plugin)
plugin.initialize()
}
fun shutdownAll() {
plugins.forEach { it.shutdown() }
}
fun listPlugins() {
plugins.forEach {
println(it.getDescription())
}
}
}
fun main() {
val manager = PluginManager()
manager.register(LoggingPlugin())
manager.register(CachePlugin())
manager.listPlugins()
manager.shutdownAll()
}
Sealed Class - 제한된 클래스 계층
기본 Sealed Class
같은 파일 내에서만 상속 가능한 제한된 클래스
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
object Loading : Result()
}
fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("성공: ${result.data}")
is Result.Error -> println("실패: ${result.message}")
Result.Loading -> println("로딩 중...")
// else 불필요! (모든 경우 처리됨)
}
}
fun main() {
handleResult(Result.Success("데이터 로드 완료"))
handleResult(Result.Error("네트워크 오류"))
handleResult(Result.Loading)
}
실전 예제 - API 응답
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error(val code: Int, val message: String) : ApiResponse<Nothing>()
object Loading : ApiResponse<Nothing>()
}
data class User(val id: Long, val name: String)
fun fetchUser(userId: Long): ApiResponse<User> {
return when {
userId > 0 -> ApiResponse.Success(User(userId, "규철"))
userId == 0L -> ApiResponse.Loading
else -> ApiResponse.Error(404, "사용자를 찾을 수 없습니다")
}
}
fun main() {
val response = fetchUser(1)
when (response) {
is ApiResponse.Success -> {
println("사용자: ${response.data.name}")
}
is ApiResponse.Error -> {
println("에러 ${response.code}: ${response.message}")
}
ApiResponse.Loading -> {
println("로딩 중...")
}
}
}
실전 예제 - UI 상태 관리
sealed class UiState {
object Idle : UiState()
object Loading : UiState()
data class Success(val message: String) : UiState()
data class Error(val throwable: Throwable) : UiState()
}
class ViewModel {
private var state: UiState = UiState.Idle
fun loadData() {
state = UiState.Loading
try {
// 데이터 로드
Thread.sleep(1000)
state = UiState.Success("데이터 로드 완료")
} catch (e: Exception) {
state = UiState.Error(e)
}
}
fun render() {
when (state) {
UiState.Idle -> println("대기 중")
UiState.Loading -> println("로딩 중...")
is UiState.Success -> println((state as UiState.Success).message)
is UiState.Error -> println("오류: ${(state as UiState.Error).throwable.message}")
}
}
}
fun main() {
val viewModel = ViewModel()
viewModel.render() // 대기 중
viewModel.loadData()
viewModel.render() // 데이터 로드 완료
}
Enum vs Sealed Class
// Enum - 모든 인스턴스가 동일한 타입
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
// Sealed Class - 각 하위 클래스가 다른 데이터 가질 수 있음
sealed class NetworkResult {
data class Success(val data: String, val timestamp: Long) : NetworkResult()
data class Error(val exception: Exception, val retryCount: Int) : NetworkResult()
object Timeout : NetworkResult()
}
fun main() {
// Enum - 단순한 상수
val color = Color.RED
// Sealed - 복잡한 데이터
val result = NetworkResult.Success("데이터", System.currentTimeMillis())
}
위임 패턴 - by 키워드
클래스 위임
interface Printer {
fun print(message: String)
}
class ConsolePrinter : Printer {
override fun print(message: String) {
println("콘솔: $message")
}
}
class Logger(printer: Printer) : Printer by printer {
fun log(level: String, message: String) {
print("[$level] $message")
}
}
fun main() {
val logger = Logger(ConsolePrinter())
logger.print("일반 메시지") // 콘솔: 일반 메시지
logger.log("INFO", "정보 로그") // 콘솔: [INFO] 정보 로그
}
프로퍼티 위임
import kotlin.properties.Delegates
class User {
// lazy - 처음 사용할 때 초기화
val expensiveData: String by lazy {
println("데이터 로드 중...")
"무거운 데이터"
}
// observable - 값 변경 감지
var name: String by Delegates.observable("초기값") { prop, old, new ->
println("${prop.name}: $old -> $new")
}
// vetoable - 값 변경 검증
var age: Int by Delegates.vetoable(0) { _, old, new ->
new >= 0 // 음수면 변경 거부
}
}
fun main() {
val user = User()
// lazy 테스트
println("첫 접근")
println(user.expensiveData) // 데이터 로드 중... 무거운 데이터
println("두 번째 접근")
println(user.expensiveData) // 무거운 데이터 (로드 안 함)
// observable 테스트
user.name = "규철" // name: 초기값 -> 규철
user.name = "영희" // name: 규철 -> 영희
// vetoable 테스트
user.age = 30 // OK
user.age = -5 // 거부됨 (age는 여전히 30)
println(user.age) // 30
}
커스텀 위임
import kotlin.reflect.KProperty
class UpperCaseDelegate {
private var value: String = ""
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return value.uppercase()
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
this.value = value
}
}
class User {
var name: String by UpperCaseDelegate()
}
fun main() {
val user = User()
user.name = "kotlin"
println(user.name) // KOTLIN
}
실전 패턴 모음
패턴 1: 템플릿 메서드 패턴
abstract class DataProcessor {
// 템플릿 메서드
fun process() {
loadData()
validateData()
transformData()
saveData()
}
protected abstract fun loadData()
protected abstract fun validateData()
protected abstract fun transformData()
protected abstract fun saveData()
}
class UserDataProcessor : DataProcessor() {
override fun loadData() {
println("사용자 데이터 로드")
}
override fun validateData() {
println("사용자 데이터 검증")
}
override fun transformData() {
println("사용자 데이터 변환")
}
override fun saveData() {
println("사용자 데이터 저장")
}
}
fun main() {
val processor = UserDataProcessor()
processor.process()
}
패턴 2: 전략 패턴
interface PaymentStrategy {
fun pay(amount: Int)
}
class CreditCardStrategy : PaymentStrategy {
override fun pay(amount: Int) {
println("신용카드로 ${amount}원 결제")
}
}
class KakaoPayStrategy : PaymentStrategy {
override fun pay(amount: Int) {
println("카카오페이로 ${amount}원 결제")
}
}
class PaymentProcessor(private var strategy: PaymentStrategy) {
fun setStrategy(strategy: PaymentStrategy) {
this.strategy = strategy
}
fun processPayment(amount: Int) {
strategy.pay(amount)
}
}
fun main() {
val processor = PaymentProcessor(CreditCardStrategy())
processor.processPayment(10000) // 신용카드로 10000원 결제
processor.setStrategy(KakaoPayStrategy())
processor.processPayment(20000) // 카카오페이로 20000원 결제
}
패턴 3: 상태 패턴 with Sealed Class
sealed class OrderState {
abstract fun next(): OrderState
abstract fun cancel(): OrderState
object Created : OrderState() {
override fun next() = Paid
override fun cancel() = Cancelled
}
object Paid : OrderState() {
override fun next() = Shipped
override fun cancel() = Cancelled
}
object Shipped : OrderState() {
override fun next() = Delivered
override fun cancel() = this // 배송 중엔 취소 불가
}
object Delivered : OrderState() {
override fun next() = this
override fun cancel() = this
}
object Cancelled : OrderState() {
override fun next() = this
override fun cancel() = this
}
}
class Order(private var state: OrderState = OrderState.Created) {
fun nextState() {
state = state.next()
printState()
}
fun cancel() {
state = state.cancel()
printState()
}
private fun printState() {
val stateName = when (state) {
OrderState.Created -> "주문 생성"
OrderState.Paid -> "결제 완료"
OrderState.Shipped -> "배송 중"
OrderState.Delivered -> "배송 완료"
OrderState.Cancelled -> "주문 취소"
}
println("현재 상태: $stateName")
}
}
fun main() {
val order = Order()
order.nextState() // 결제 완료
order.nextState() // 배송 중
order.cancel() // 배송 중 (취소 불가)
order.nextState() // 배송 완료
}
마무리 - 다음 편 예고
오늘 배운 것 ✅
- open - 명시적 상속 허용
- abstract - 추상 클래스와 메서드
- interface - 다중 상속과 기본 구현
- sealed class - 제한된 클래스 계층
- by - 위임 패턴
- 실전 디자인 패턴 - 템플릿, 전략, 상태 패턴
다음 편에서 배울 것 📚
12편: Delegation 완벽 가이드 | by 키워드 심화
- 클래스 위임 심화
- 프로퍼티 위임 심화 (lazy, observable, vetoable)
- Map 위임
- 커스텀 위임 프로퍼티 만들기
- 실전 위임 패턴
실습 과제 💪
// 1. 상속 구조 설계
// - 도형 클래스 계층 (Shape, Circle, Rectangle)
// - 동물 클래스 계층 (Animal, Dog, Cat)
// 2. Sealed Class 활용
// - 네트워크 요청 상태 관리
// - 화면 상태 관리
// 3. 디자인 패턴 구현
// - 템플릿 메서드 패턴
// - 전략 패턴
// - 상태 패턴
// 4. 위임 활용
// - 로깅 기능 위임
// - 캐시 기능 위임
자주 묻는 질문 (FAQ)
Q: 왜 기본적으로 final인가요?
A: 의도하지 않은 상속으로 인한 문제를 방지합니다. Effective Java의 "상속을 위한 설계와 문서화, 그렇지 않으면 상속 금지" 원칙을 따릅니다.
Q: abstract class vs interface 언제 뭘 쓰나요?
A: 상태(필드)가 필요하면 abstract class, 다중 상속이 필요하면 interface를 쓰세요.
Q: Sealed class vs Enum의 차이는?
A: Enum은 단일 인스턴스, Sealed는 각 하위 클래스가 다른 데이터를 가질 수 있습니다.
Q: by 위임은 언제 사용하나요?
A: 보일러플레이트 코드를 줄이고, 기능을 재사용할 때 사용합니다.
관련 글
- Kotlin 제네릭스 완벽 가이드 (이전 편)
- Kotlin Delegation 완벽 가이드 (다음 편)
- Kotlin 디자인 패턴
💬 댓글로 알려주세요!
- Sealed class를 어디에 활용하고 계신가요?
- 어떤 디자인 패턴을 자주 사용하시나요?
- 이 글이 도움이 되셨나요?
반응형
'개발 > java basic' 카테고리의 다른 글
| Kotlin 제네릭스 완벽 가이드 | in, out, reified 마스터하기 (0) | 2025.12.27 |
|---|---|
| Kotlin 람다와 고차 함수 완벽 가이드 | 함수형 프로그래밍 심화 (0) | 2025.12.26 |
| Kotlin 컬렉션 완벽 가이드 | List, Set, Map 함수형 프로그래밍 (1) | 2025.12.24 |
| Kotlin Null Safety 완벽 가이드 | ?, ?., ?:, !! 마스터하기 (0) | 2025.12.23 |
| Kotlin 변수와 타입 완벽 가이드 | val vs var, 타입 추론, Nullable 타입 마스터하기 (0) | 2025.12.22 |
Comments
