Kim-Baek 개발자 이야기

Kotlin 클래스와 객체 완벽 가이드 | OOP의 Kotlin 스타일 본문

개발/java basic

Kotlin 클래스와 객체 완벽 가이드 | OOP의 Kotlin 스타일

김백개발자 2025. 12. 22. 08:44
반응형

이 글을 읽으면: Java의 장황한 클래스 선언을 단 1줄로 줄이는 방법을 배울 수 있습니다. data class, companion object, object 키워드를 활용해 더 간결하고 안전한 객체지향 프로그래밍을 실전 예제로 마스터하세요.


📌 목차

  1. 들어가며 - Java 클래스의 Boilerplate
  2. 클래스 기본 선언 - constructor
  3. 프로퍼티 - getter/setter 자동 생성
  4. Primary Constructor - 주 생성자
  5. Secondary Constructor - 보조 생성자
  6. data class - DTO의 완벽한 해결책
  7. companion object - static의 대안
  8. object - 싱글톤 패턴
  9. 마무리 - 다음 편 예고

들어가며 - Java 클래스의 Boilerplate

Java로 간단한 User 클래스를 만들 때 이런 경험 있으신가요?

// Java - 80줄의 Boilerplate 코드
public class User {
    private final Long id;
    private final String name;
    private final String email;
    
    // 생성자
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }
    
    // Getter
    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
    
    // equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
               Objects.equals(name, user.name) &&
               Objects.equals(email, user.email);
    }
    
    // hashCode
    @Override
    public int hashCode() {
        return Objects.hash(id, name, email);
    }
    
    // toString
    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", email='" + email + '\'' +
                '}';
    }
}

Kotlin은 단 1줄입니다:

// Kotlin - 1줄로 완벽한 클래스
data class User(val id: Long, val name: String, val email: String)

// getter, equals, hashCode, toString, copy 모두 자동 생성!

오늘은 Kotlin의 클래스 선언 방식을 완전히 마스터해보겠습니다!


 

클래스 기본 선언

가장 간단한 클래스

class Person {
    // 빈 클래스도 유효함
}

fun main() {
    val person = Person()  // new 키워드 불필요!
    println(person)
}

프로퍼티가 있는 클래스

class Person {
    var name: String = ""
    var age: Int = 0
}

fun main() {
    val person = Person()
    person.name = "규철"
    person.age = 30
    
    println("${person.name}, ${person.age}세")  // 규철, 30세
}

생성자를 사용한 초기화

class Person {
    var name: String
    var age: Int
    
    // init 블록 - 생성자 실행 시 호출됨
    init {
        name = "기본이름"
        age = 0
        println("Person 객체 생성됨")
    }
}

프로퍼티 - getter/setter 자동 생성

기본 프로퍼티

Kotlin의 프로퍼티는 필드 + getter + setter를 모두 포함합니다.

class Person {
    var name: String = ""  // 자동으로 getter/setter 생성
        get() {
            println("name getter 호출")
            return field
        }
        set(value) {
            println("name setter 호출: $value")
            field = value
        }
}

fun main() {
    val person = Person()
    person.name = "규철"  // setter 호출
    println(person.name)  // getter 호출
}

val vs var 프로퍼티

class User {
    val id: Long = 1L        // 읽기 전용 (getter만)
    var name: String = ""    // 읽기/쓰기 (getter + setter)
}

fun main() {
    val user = User()
    
    println(user.id)      // OK
    // user.id = 2        // ❌ 컴파일 에러 (val은 불변)
    
    user.name = "규철"    // OK
    println(user.name)    // OK
}

커스텀 getter/setter

class Rectangle(var width: Int, var height: Int) {
    // 계산된 프로퍼티 (backing field 없음)
    val area: Int
        get() = width * height
    
    // setter에서 검증
    var diagonal: Double = 0.0
        set(value) {
            if (value < 0) {
                println("음수는 안됩니다!")
            } else {
                field = value
            }
        }
}

fun main() {
    val rect = Rectangle(10, 20)
    println("넓이: ${rect.area}")  // 200
    
    rect.width = 15
    println("넓이: ${rect.area}")  // 300 (자동 재계산)
}

Java와 비교

// Java - getter/setter 수동 작성
public class Person {
    private String name;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

// 사용
Person person = new Person();
person.setName("규철");
String name = person.getName();
// Kotlin - 자동 생성
class Person {
    var name: String = ""
}

// 사용
val person = Person()
person.name = "규철"  // setter 자동 호출
val name = person.name  // getter 자동 호출

Primary Constructor - 주 생성자

기본 문법

클래스 헤더에 바로 선언하는 생성자입니다.

class Person(val name: String, var age: Int)

fun main() {
    val person = Person("규철", 30)
    println("${person.name}, ${person.age}세")
}

init 블록과 함께 사용

class Person(val name: String, var age: Int) {
    val isAdult: Boolean
    
    init {
        println("Person 생성: $name")
        isAdult = age >= 18
    }
}

fun main() {
    val person = Person("규철", 30)
    // Person 생성: 규철
    
    println("성인: ${person.isAdult}")  // 성인: true
}

생성자 파라미터 기본값

class User(
    val id: Long,
    val name: String,
    val email: String? = null,  // 기본값: null
    val isActive: Boolean = true  // 기본값: true
)

fun main() {
    // 필수 파라미터만
    val user1 = User(1L, "규철")
    
    // 일부만 지정
    val user2 = User(2L, "영희", email = "test@example.com")
    
    println(user1.email)    // null
    println(user2.email)    // test@example.com
}

실전 예제 - 회원 정보

class Member(
    val id: String,
    val password: String,
    val name: String,
    val email: String,
    val phoneNumber: String? = null,
    val address: String? = null,
    val createdAt: Long = System.currentTimeMillis()
) {
    init {
        require(password.length >= 8) { "비밀번호는 8자 이상이어야 합니다" }
        require(email.contains("@")) { "올바른 이메일 형식이 아닙니다" }
    }
    
    fun displayInfo() {
        println("이름: $name")
        println("이메일: $email")
        phoneNumber?.let { println("전화번호: $it") }
    }
}

fun main() {
    val member = Member(
        id = "user123",
        password = "password123",
        name = "규철",
        email = "user@example.com",
        phoneNumber = "010-1234-5678"
    )
    
    member.displayInfo()
}

Secondary Constructor - 보조 생성자

기본 사용법

class Person(val name: String, var age: Int) {
    var email: String = ""
    
    // 보조 생성자 - 주 생성자 호출 필수
    constructor(name: String, age: Int, email: String) : this(name, age) {
        this.email = email
    }
}

fun main() {
    val person1 = Person("규철", 30)
    val person2 = Person("영희", 25, "test@example.com")
    
    println(person2.email)  // test@example.com
}

여러 보조 생성자

class User {
    val id: String
    val name: String
    var email: String? = null
    
    // 주 생성자 없이 보조 생성자만 사용 가능
    constructor(id: String, name: String) {
        this.id = id
        this.name = name
    }
    
    constructor(id: String, name: String, email: String) : this(id, name) {
        this.email = email
    }
}

fun main() {
    val user1 = User("user1", "규철")
    val user2 = User("user2", "영희", "test@example.com")
}

Java와 비교

// Java - 생성자 오버로딩
public class Person {
    private String name;
    private int age;
    private String email;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public Person(String name, int age, String email) {
        this(name, age);
        this.email = email;
    }
}
// Kotlin - 기본값으로 더 간단하게
class Person(
    val name: String,
    var age: Int,
    var email: String? = null
)

// 또는 보조 생성자
class Person(val name: String, var age: Int) {
    var email: String? = null
    
    constructor(name: String, age: Int, email: String) : this(name, age) {
        this.email = email
    }
}

data class - DTO의 완벽한 해결책

기본 선언

data class는 데이터를 담는 클래스에 필요한 모든 메서드를 자동 생성합니다.

data class User(
    val id: Long,
    val name: String,
    val email: String
)

fun main() {
    val user = User(1L, "규철", "user@example.com")
    
    // toString 자동 생성
    println(user)  // User(id=1, name=규철, email=user@example.com)
}

자동 생성되는 메서드

data class Point(val x: Int, val y: Int)

fun main() {
    val p1 = Point(10, 20)
    val p2 = Point(10, 20)
    val p3 = Point(30, 40)
    
    // equals 자동 생성
    println(p1 == p2)  // true (값 비교)
    println(p1 == p3)  // false
    
    // hashCode 자동 생성
    println(p1.hashCode())  // 동일한 값
    println(p2.hashCode())  // 동일한 값
    
    // toString 자동 생성
    println(p1)  // Point(x=10, y=20)
}

copy - 불변 객체 복사

가장 강력한 기능! 일부 프로퍼티만 변경한 복사본을 만듭니다.

data class User(
    val id: Long,
    val name: String,
    val email: String,
    val isActive: Boolean = true
)

fun main() {
    val user = User(1L, "규철", "user@example.com")
    
    // 이름만 변경한 복사본
    val updatedUser = user.copy(name = "김규철")
    
    println(user)         // User(id=1, name=규철, ...)
    println(updatedUser)  // User(id=1, name=김규철, ...)
    
    // 여러 필드 변경
    val deactivatedUser = user.copy(
        name = "탈퇴회원",
        isActive = false
    )
}

componentN - 구조 분해 선언

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("규철", 30)
    
    // 구조 분해 선언
    val (name, age) = person
    println("이름: $name, 나이: $age")  // 이름: 규철, 나이: 30
    
    // 일부만 사용
    val (personName, _) = person  // age는 무시
    println(personName)
}

실전 예제 - API Response

data class ApiResponse<T>(
    val success: Boolean,
    val data: T?,
    val errorMessage: String? = null,
    val timestamp: Long = System.currentTimeMillis()
)

data class UserDto(
    val id: Long,
    val username: String,
    val email: String
)

fun main() {
    // 성공 응답
    val successResponse = ApiResponse(
        success = true,
        data = UserDto(1L, "규철", "user@example.com")
    )
    
    // 실패 응답
    val errorResponse = ApiResponse<UserDto>(
        success = false,
        data = null,
        errorMessage = "사용자를 찾을 수 없습니다"
    )
    
    println(successResponse)
    println(errorResponse)
}

Java와 비교

// Java - 모든 걸 수동으로 작성
public class User {
    private final Long id;
    private final String name;
    private final String email;
    
    // 생성자, getter, equals, hashCode, toString 모두 수동 작성...
    // 약 80줄
}
// Kotlin - 1줄로 끝
data class User(val id: Long, val name: String, val email: String)

companion object - static의 대안

기본 사용법

Java의 static과 유사하지만, 실제로는 싱글톤 객체입니다.

class MathUtils {
    companion object {
        const val PI = 3.14159
        
        fun add(a: Int, b: Int) = a + b
        fun multiply(a: Int, b: Int) = a * b
    }
}

fun main() {
    println(MathUtils.PI)           // 3.14159
    println(MathUtils.add(10, 20))  // 30
}

팩토리 메서드 패턴

class User private constructor(
    val id: Long,
    val name: String,
    val email: String
) {
    companion object {
        private var nextId = 1L
        
        fun create(name: String, email: String): User {
            return User(nextId++, name, email)
        }
        
        fun createGuest(): User {
            return User(0L, "손님", "guest@example.com")
        }
    }
}

fun main() {
    val user1 = User.create("규철", "user1@example.com")
    val user2 = User.create("영희", "user2@example.com")
    val guest = User.createGuest()
    
    println(user1)  // User(id=1, ...)
    println(user2)  // User(id=2, ...)
    println(guest)  // User(id=0, name=손님, ...)
}

상수와 함께 사용

class Config {
    companion object {
        const val MAX_RETRY = 3
        const val TIMEOUT_MS = 5000
        const val API_VERSION = "v1"
        
        fun getBaseUrl(env: String): String {
            return when (env) {
                "dev" -> "https://dev.api.example.com"
                "prod" -> "https://api.example.com"
                else -> "http://localhost:8080"
            }
        }
    }
}

fun main() {
    println(Config.MAX_RETRY)
    println(Config.getBaseUrl("dev"))
}

인터페이스 구현 가능

interface JsonConverter {
    fun toJson(obj: Any): String
}

class User(val name: String, val age: Int) {
    companion object : JsonConverter {
        override fun toJson(obj: Any): String {
            val user = obj as User
            return """{"name": "${user.name}", "age": ${user.age}}"""
        }
    }
}

fun main() {
    val user = User("규철", 30)
    println(User.toJson(user))  // {"name": "규철", "age": 30}
}

Java와 비교

// Java - static
public class MathUtils {
    public static final double PI = 3.14159;
    
    public static int add(int a, int b) {
        return a + b;
    }
}

// 사용
MathUtils.add(10, 20);
// Kotlin - companion object
class MathUtils {
    companion object {
        const val PI = 3.14159
        fun add(a: Int, b: Int) = a + b
    }
}

// 사용
MathUtils.add(10, 20)

object 키워드 - 싱글톤 패턴

싱글톤 객체

object 키워드로 싱글톤을 쉽게 만듭니다.

object DatabaseConnection {
    private var connectionCount = 0
    
    fun connect() {
        connectionCount++
        println("데이터베이스 연결됨 (총 ${connectionCount}회)")
    }
    
    fun getConnectionCount() = connectionCount
}

fun main() {
    DatabaseConnection.connect()  // 데이터베이스 연결됨 (총 1회)
    DatabaseConnection.connect()  // 데이터베이스 연결됨 (총 2회)
    
    println(DatabaseConnection.getConnectionCount())  // 2
}

실전 예제 - 설정 관리

object AppConfig {
    private val config = mutableMapOf<String, String>()
    
    init {
        // 초기 설정 로드
        config["app.name"] = "MyApp"
        config["app.version"] = "1.0.0"
        config["db.host"] = "localhost"
    }
    
    fun get(key: String): String? = config[key]
    
    fun set(key: String, value: String) {
        config[key] = value
    }
    
    fun getAll(): Map<String, String> = config.toMap()
}

fun main() {
    println(AppConfig.get("app.name"))  // MyApp
    
    AppConfig.set("db.host", "192.168.1.100")
    println(AppConfig.get("db.host"))   // 192.168.1.100
    
    println(AppConfig.getAll())
}

익명 객체 (Anonymous Object)

fun main() {
    val clickListener = object {
        fun onClick() {
            println("클릭됨!")
        }
        
        fun onDoubleClick() {
            println("더블클릭됨!")
        }
    }
    
    clickListener.onClick()
    clickListener.onDoubleClick()
}

Java와 비교

// Java - 싱글톤 구현 (복잡함)
public class DatabaseConnection {
    private static DatabaseConnection instance;
    private int connectionCount = 0;
    
    private DatabaseConnection() {}
    
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
    
    public void connect() {
        connectionCount++;
    }
}

// 사용
DatabaseConnection.getInstance().connect();
// Kotlin - object 키워드 하나로 끝
object DatabaseConnection {
    private var connectionCount = 0
    
    fun connect() {
        connectionCount++
    }
}

// 사용
DatabaseConnection.connect()

마무리 - 다음 편 예고

오늘 배운 것 ✅

  • 클래스 기본 선언과 프로퍼티
  • Primary Constructor와 init 블록
  • data class - DTO 작성의 완벽한 해결책
  • companion object - static 대체
  • object 키워드로 싱글톤 패턴

다음 편에서 배울 것 📚

6편: Null Safety 완벽 가이드 | ?, ?., ?:, !! 마스터하기

  • Nullable Types (?) - null 가능 타입
  • Safe Call (?.) - 안전한 호출
  • Elvis Operator (?:) - 기본값 제공
  • Not-null Assertion (!!) - 주의사항
  • let, run, apply와 함께 사용하기

실습 과제 💪

// 1. data class로 상품 정보 만들기
// - 상품명, 가격, 재고, 카테고리
// - copy로 할인가 계산 메서드

// 2. companion object로 팩토리 패턴
// - User 클래스 생성
// - createAdmin, createGuest 메서드

// 3. object로 로거 만들기
// - log, error, debug 메서드
// - 로그 레벨 설정 기능

자주 묻는 질문 (FAQ)

Q: class와 data class의 차이는?
A: data class는 equals, hashCode, toString, copy를 자동 생성합니다. 데이터 저장용이면 data class를 쓰세요.

Q: companion object와 object의 차이는?
A: companion object는 클래스 내부의 싱글톤이고, object는 독립적인 싱글톤입니다.

Q: val 프로퍼티도 setter가 있나요?
A: 아니요! val은 getter만 있습니다. 불변이므로 setter는 없습니다.

Q: data class에 메서드를 추가할 수 있나요?
A: 네! 일반 클래스처럼 메서드, init 블록 모두 추가 가능합니다.


관련 글


💬 댓글로 알려주세요!

  • data class를 사용해본 경험이 있나요?
  • 어떤 패턴이 가장 유용했나요?
  • 이 글이 도움이 되셨나요?

 

반응형
Comments