-
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. 리펙토링 끝! 다음으로!
'Kotlin > 실전! 코틀린과 스프링 부트로 ... 개발' 카테고리의 다른 글
6. 네 번째 요구사항 추가하기 - Querydsl (0) 2023.08.08 5. 세 번째 요구사항 추가하기 - 책 통계 (0) 2023.08.08 4. 두 번째 요구사항 추가하기 - 도서 대출 현황 (0) 2023.08.07 3. 첫 번째 요구사항 추가하기 - 책의 분야 (0) 2023.08.05 1. 리펙토링 준비 (0) 2023.07.31