| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 티스토리챌린지
- 클린아키텍처
- Sort
- effectivejava
- Effective Java
- 이펙티브 자바
- ElasticSearch
- kubernetes
- k8s
- 스프링부트
- 예제로 배우는 스프링 입문
- 자바
- 알고리즘
- MongoDB
- 스프링
- 이펙티브자바
- java
- Kotlin
- 엘라스틱서치
- 이차전지관련주
- 알고리즘정렬
- Spring
- JavaScript
- 자바스크립트
- 김영한
- 카카오
- 스프링 핵심원리
- 오블완
- Effective Java 3
- 스프링핵심원리
- Today
- Total
Kim-Baek 개발자 이야기
Kotlin 클래스와 객체 완벽 가이드 | OOP의 Kotlin 스타일 본문
이 글을 읽으면: Java의 장황한 클래스 선언을 단 1줄로 줄이는 방법을 배울 수 있습니다. data class, companion object, object 키워드를 활용해 더 간결하고 안전한 객체지향 프로그래밍을 실전 예제로 마스터하세요.
📌 목차
- 들어가며 - Java 클래스의 Boilerplate
- 클래스 기본 선언 - constructor
- 프로퍼티 - getter/setter 자동 생성
- Primary Constructor - 주 생성자
- Secondary Constructor - 보조 생성자
- data class - DTO의 완벽한 해결책
- companion object - static의 대안
- object - 싱글톤 패턴
- 마무리 - 다음 편 예고
들어가며 - 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 블록 모두 추가 가능합니다.
관련 글
- Kotlin 제어 구조 완벽 가이드 (이전 편)
- Kotlin Null Safety 완벽 가이드 (다음 편)
- Kotlin 상속과 인터페이스
💬 댓글로 알려주세요!
- data class를 사용해본 경험이 있나요?
- 어떤 패턴이 가장 유용했나요?
- 이 글이 도움이 되셨나요?
'개발 > java basic' 카테고리의 다른 글
| Kotlin 변수와 타입 완벽 가이드 | val vs var, 타입 추론, Nullable 타입 마스터하기 (0) | 2025.12.22 |
|---|---|
| Kotlin 제어 구조 완벽 가이드 | if, when, for, while 마스터하기 (0) | 2025.12.21 |
| Kotlin 함수 완벽 가이드 | fun으로 시작하는 간결한 코드 (0) | 2025.12.21 |
| Kotlin 시작하기 | 왜 Kotlin인가? 개발환경 설정부터 Hello World까지 (0) | 2025.12.21 |
| Kotlin 시작하기 | Java 개발자를 위한 첫 번째 Kotlin 코드 (0) | 2025.12.10 |
