[TIL]Kotlin

기 원·2025년 5월 9일

[Project] Spring-plus

목록 보기
7/8

Kotlin이란?

1. Kotlin vs Java 기본 비교

항목JavaKotlin
클래스class User {}class User
메서드void run()fun run(): Unit
게터/세터getX(), setX()val x / var x (자동 생성)
null 처리일반 타입에 null 할당 가능String? → nullable 명시 필요
데이터 클래스POJO + Lombokdata class 한 줄

2. Nullable (?)과 Non-nullable

val name: String = "hello"       // null 불가
val nickname: String? = null     // null 가능
  • Kotlin은 null safety 기본 제공
  • ?를 붙여야 null 할당 가능

3. !! (Non-null 단언 연산자)

val length = name!!.length
  • !!는 null이 절대 아님을 강제
  • null이면 런타임에서 NPE 발생 → 사용 지양

4. ?. (safe call), ?: (엘비스)

val length = name?.length               // null이면 null 반환
val finalName = name ?: "기본값"        // null이면 우측 값 반환

5. 자바 → 코틀린 자동 변환 후 필수 점검 항목

  1. !! 남용
  2. DTO → data class 변환
  3. @NotBlank@field:NotBlank
  4. 클래스 상속 필요 시 open 키워드 사용
  5. 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!")  // Hello Kotlin!

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 {

//   annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' 주석처리

    //Kotlin
    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에서 @PrePersistlateinit var 안전하게 구성
  • DTO는 모두 data class로 통일

정리

  • User, Todo 도메인의 CRUD Kotlin 전환 완료
  • QueryDSL, validation, JPA 호환 확인
profile
노력하고 있다니까요?

0개의 댓글