Kotlin이란?
1. Kotlin vs Java 기본 비교
| 항목 | Java | Kotlin |
|---|
| 클래스 | class User {} | class User |
| 메서드 | void run() | fun run(): Unit |
| 게터/세터 | getX(), setX() | val x / var x (자동 생성) |
| null 처리 | 일반 타입에 null 할당 가능 | String? → nullable 명시 필요 |
| 데이터 클래스 | POJO + Lombok | data class 한 줄 |
2. Nullable (?)과 Non-nullable
val name: String = "hello"
val nickname: String? = null
- Kotlin은 null safety 기본 제공
?를 붙여야 null 할당 가능
3. !! (Non-null 단언 연산자)
val length = name!!.length
!!는 null이 절대 아님을 강제
- null이면 런타임에서 NPE 발생 → 사용 지양
4. ?. (safe call), ?: (엘비스)
val length = name?.length
val finalName = name ?: "기본값"
5. 자바 → 코틀린 자동 변환 후 필수 점검 항목
!! 남용
- DTO →
data class 변환
@NotBlank → @field:NotBlank
- 클래스 상속 필요 시
open 키워드 사용
companion object에 @JvmStatic 필요한 경우
6. Kotlin의 대표 문법들
data class
data class User(val id: Long, val name: String)
equals, hashCode, toString, copy() 자동 생성
when
when (role) {
"ADMIN" -> ...
"USER" -> ...
else -> ...
}
문자열 템플릿
val name = "Kotlin"
println("Hello $name!")
7. 코틀린 스타일
val → 불변 (기본적으로 사용)
var → 변경 가능한 경우만 사용
- 명확한 null 구분 (
?, !!, ?:)
- 한 줄 함수는
= 로 작성 가능
fun square(x: Int) = x * x
8. 팁
- 자동 변환 →
!! 제거 → 의존성 주입 수정 → DTO 정리
- QueryDSL은
@QueryProjection 사용 + data class 유지
- Entity는 가급적
var, DTO는 val
lateinit은 @PrePersist, Bean 주입 외에는 지양
2. Kotlin 리팩토링
1. Build.gradle 설정
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.0.0'
id 'org.jetbrains.kotlin.plugin.lombok' version '2.0.0'
id 'org.jetbrains.kotlin.plugin.spring' version '2.0.0'
id 'org.jetbrains.kotlin.plugin.jpa' version '2.0.0'
id 'org.jetbrains.kotlin.kapt' version '2.0.0'
}
sourceSets {
main {
java {
srcDirs += "src/main/kotlin"
}
}
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation "org.jetbrains.kotlin:kotlin-reflect"
kapt "com.querydsl:querydsl-apt:5.0.0:jakarta"
kapt "jakarta.annotation:jakarta.annotation-api"
kapt "jakarta.persistence:jakarta.persistence-api"
}
kapt {
keepJavacAnnotationProcessors = true
}
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.Embeddable")
annotation("jakarta.persistence.MappedSuperclass")
}
2. 주요 작업
- 기존 Java 코드를 Kotlin으로 자동 변환(
ctrl + shift + a -> Convert Java File to Kotlin File ) 후 수작업 리팩토링
!! 제거, 명확한 null-safe 설계
- DTO는
data class, Entity는 class
@field:NotBlank 등 validation 어노테이션 위치 수정
- Service/Controller의 의존성 주입은 생성자 기반으로 재작성
3. User / Todo 도메인 리팩토링
User 엔티티
@Entity
@Table(name = "users")
class User(
@Column(unique = true)
var email: String,
var password: String? = null,
@Enumerated(EnumType.STRING)
var userRole: UserRole,
var nickname: String,
var profileImage: String? = null
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
protected set
companion object {
@JvmStatic
fun fromAuthUser(authUser: AuthUser): User {
return User(authUser.email, null, authUser.userRole, authUser.nickname).apply {
this.id = authUser.id
}
}
}
}
Todo 엔티티
@Entity
@Table(name = "todos")
class Todo(
var title: String,
var contents: String,
var weather: String,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
var user: User
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
@OneToMany(mappedBy = "todo", cascade = [CascadeType.REMOVE])
val comments: MutableList<Comment> = mutableListOf()
@OneToMany(mappedBy = "todo", cascade = [CascadeType.ALL], orphanRemoval = true)
val managers: MutableList<Manager> = mutableListOf()
init {
managers.add(Manager(user, this))
}
}
TodoService 일부
@Transactional
fun saveTodo(authUser: AuthUser, request: TodoSaveRequest): TodoSaveResponse {
val user = User.fromAuthUser(authUser)
val weather = weatherClient.todayWeather
val saved = todoRepository.save(Todo(request.title, request.contents, weather, user))
return TodoSaveResponse(saved.id!!, saved.title, saved.contents, weather, UserResponse(user.id!!, user.email))
}
4. 이슈 및 해결
| 문제 | 해결 방법 |
|---|
!! 사용 | 생성자 주입 및 null-safe 구조로 교체 |
| DTO validation 안됨 | @field:NotBlank 명시 |
| Kotlin Entity + private setter 사용 시 오류 | setter 열어두거나 val createdAt = now() 로 변경 |
JPAQueryFactory 주입 실패 | @Configuration에서 직접 Bean 등록 |
| Java → Kotlin companion object 접근 불가 | @JvmStatic 사용 |
@JvmRecord 사용 시 QueryDSL 오류 | 제거하고 data class 사용 유지 |
5. 리팩토링 항목
TodoRepository, TodoQueryRepository Kotlin 스타일로 정리
TodoController에서 !! 제거, request/response 명확화
Log Entity에서 @PrePersist와 lateinit var 안전하게 구성
- DTO는 모두
data class로 통일
정리
- User, Todo 도메인의 CRUD Kotlin 전환 완료
- QueryDSL, validation, JPA 호환 확인