ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 2. Java 서버를 Kotlin 서버로 리팩토링
    Kotlin/실전! 코틀린과 스프링 부트로 ... 개발 2023. 8. 2. 14:47

    11. Kotlin 리팩토링 계획 세우기

    - 1. Domain

        ㆍ특징 : POJO, JPA Entity 객체

    - 2. Repository

        ㆍ특징 : Spring Bean, 의존성 X

    - 3. Service

        ㆍ특징 : Spring Bean, 의존성 O, 비즈니스 로직

    - 4. Controller / DTO

        ㆍ특징 : Spring Bean, 의존성 O / DTO의 경우 그 숫자가 많다. 

     

    12. 도메인 계층을 Kotlin으로 변경하기 - Book.java

    Caused by: java.lang.NoClassDefFoundError: kotlin/reflect/full/KClasses

    - kotlin class에 대해서 리플렉션을 못한다.

    - 해결방안 : gradle에 해당 dependencies 추가

    implementation 'org.jetbrain.kotlin:kotlin-reflect:1.6.21'

     

    13. 도메인 계층을 Kotlin으로 변경하기 - UserLoanHistory.java, User.java

    - !! : null 아님 단언

     

    14. Kotlon과 JPA를 함께 사용할 때 이야기거리 3가지

    1. setter에 관한 이야기 

    @Entity
    class User(
        var name: String,
        ...

    - 생성자 안의 var 프로퍼티

    fun updateName(name: String) {
        this.name = name
    }

    - setter 대신 추가적인 함수

     

    - setter 대신 좋은 이름의 함수를 사용하는 것이 훨씬 clean 하다!

    - 하지만 name에 대한 setter는 public 이기 때문에 유저 이름 업데이트 기능에서 setter를 사용할 '수도' 있다.

        ㆍ코드 상 setter를 사용할 '수도' 있다는 것이 불편하다!

    - public getter는 필요하기 떄문에 setter만 private 하게 만드는 것이 최선이다!

     

    - 방법 1.  backing property 사용하기

    class User(
        private var _name: String
    ) {
        val name: String
          get() = this._name
    }

     

    - 방법 2. custom setter 이용하기

    class User(
        name: String
    ) {
        var name = name
          private set
    }

     

    - 두 방법 모두 프로퍼티가 많아지면 번거롭다!

    - 때문에 개인적으로 setter를 열어는 두지만 사용하지 않는 방법을 선호!

    - Trade-Off의 영역, 팀 컨벤션을 잘 맞추면 되지 않을까!

     

    2. 생성자 안의 프로퍼티. 클래스 body 안의 프로퍼티

    @Entity
    class User(
        var name: String,
    
        val age: Int?,
    
        @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true)
        val userLoanHistories: MutableList<UserLoanHistory> = mutableListOf(),
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val id: Long? = null,
    ) {

    - 꼭 primary constructor 안에 모든 프로퍼티를 넣어야 할까?!

     

    class User(
        var name: String,
    
        val age: Int?,
        
    ) {
    
        @OneToMany(mappedBy = "user", cascade = [CascadeType.ALL], orphanRemoval = true)
        val userLoanHistories: MutableList<UserLoanHistory> = mutableListOf()
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val id: Long? = null

    - 실제로 잘 동작한다.

    - User를 만드는 과정에서 userLoanHistories를 바로 넣어줄 수 없을 뿐이다.

     

    1) 모든 프로퍼티를 생성자에 넣거나

    2) 프로퍼티를 생성자 혹은 클래스 body 안에 구분해서 넣을 때 명확한 기준이 있거나

     

    3. JPA와 data class

    - Entity 는 data class를 피하는 것이 좋다.

    - equals, hashCode, toString 모두 JPA Entity와는 100% 어울리지 않는 메소드!

        ㆍ일대다 일 경우 일의 equals를 부른다면 무한루프에 빠질 수 있다.

     

    * 작은 TIP

    - Entity가 생성되는 로직을 찾고 싶다면 construtor 지시어를 명시적으로 작성하고 추적하자!

        ㆍconstructor 키워드를 생략 가능하지만 생성되는 부분을 추적하기 편하다!

     

    15. 리포지토리를 Kotlin으로 변경하기

    16. 서비스 계층을 Kotlin으로 변경하기 - UserService.java

    - 코틀린에서 @Transactional 를 쓸려면 함수가 아래로 오버라이드 될 수 있어야 한다.

        ㆍ코틀린에서는 클래스 상속이 막혀있다. - open을 붙여준다.

        ㆍ함수 역시 기본적으로 오버라이드가 불가능하고 open을 붙여야 상속이 가능하다.

    - open을 계속 붙이는 것은 번거롭기 때문에 plugins 추가

    plugins {
        ...
        id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21'
    }

        ㆍ스프링 빈 클래스들을 자동으로 열어주고, 그 안에 public 메서드들도 자동으로 열어준다.

        ㆍ일일이 open을 붙이지 않아도 된다.

     

    17. BookService.java를 Kotlin으로 변경하고 Optional 제거하기

    - CrudRepositoryExtension.kt : 기존 자바 코드를 코틀린으로 확장

    - 확장함수를 이용한 리펙토링

    fun fail(): Nothing {
        throw IllegalArgumentException()
    }
    
    fun <T, ID> CrudRepository<T, ID>.findByIdOrThrow(id: ID): T {
        return this.findByIdOrNull(id) ?: fail()
    }
    //        val user: User = userRepository.findById(request.id).orElseThrow(::IllegalArgumentException)
    //        val user: User = userRepository.findByIdOrNull(request.id) ?: fail()
    val user: User = userRepository.findByIdOrThrow(request.id)

     

    18. DTO를 Kotlin으로 변경하기

    class UserResponse(
        user: User
    ) {
        val id: Long
        val name: String
        val age: Int?
        
        init {
            id = user.id!!
            name = user.name
            age = user.age
        }
    }

    - 부생성자 사용

    class UserResponse(
        val id: Long,
        val name: String,
        val age: Int?,
    ) {
        constructor(user: User): this(
            id = user.id!!,
            name = user.name,
            age = user.age
        )
    }

    - 정적 팩토리 사용

    data class UserResponse(
        val id: Long,
        val name: String,
        val age: Int?,
    ) {
        companion object {
            fun of(user: User): UserReponse {
                return UserResponse(
                    id = user.id!!
                    name = user.name
                    age = user.age
                )
            }
        }
    }

     

    19. Controller 계층을 Kotlin으로 변경하기

    @DeleteMapping("/user")
    fun deleteUser(@RequestParam name: String) {
    
    }

    - @RequestParam은 값이 있어야 동작한다.

    - name: String? 시 null이 들어갈 수 있으므로 스프링 내부적으로 @RequestParam의 require 값을 false로 변경한다.

                  (default는 true)

     

     

    Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot construct instance of `com.group.libraryapp.dto.book.request.BookRequest` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.group.libraryapp.dto.book.request.BookRequest` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)<EOL> at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]]

    - JSON parser error - 의존성 추가

    implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.13.3'

     

    20. 리펙토링 끝! 다음으로!

    댓글

Designed by Tistory.