실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기

Ch02. Java 서버를 Kotlin 서버로 리팩토링하자 - 서비스 계층 Kotlin으로 변경하기

webmaster 2022. 11. 1. 22:56
728x90

UserService

@Service
class UserService(
    private val userRepository: UserRepository,
) {

    //Transactional 기능을 사용하기 위해서는 오버라이드 될 수 있어야 하는데, 코틀린은 함수를 기본적으로 상속이 불가능하기 떄문에 오류가 발생한다.
    //open 키워드를 붙여서 상속이 가능하도록 해도 되지만, 플러그인을 추가해도 된다(spring)
    @Transactional
    fun saveUser(request: UserCreateRequest) {
        val newUser = User(request.name, request.age)
        userRepository.save(newUser)
    }

    @Transactional(readOnly = true)
    fun getUsers(): List<UserResponse> {
        //return userRepository.findAll().map { user -> UserResponse(user) }
        //return userRepository.findAll().map { UserResponse(it) }
        return userRepository.findAll().map(::UserResponse)
    }

    @Transactional
    fun updateUserName(request: UserUpdateRequest){
        val user = userRepository.findById(request.id).orElseThrow(::IllegalArgumentException)
        user.updateName(request.name)
    }

    @Transactional
    fun deleteUser(name: String){
        val user = userRepository.findByName(name).orElseThrow(::IllegalArgumentException)
        userRepository.delete(user)
    }
}
  • private val 키워드로 변경이 불가능하고 내부에서만 사용 가능하도록 빈을 주입받는다.
  • @Transactional 같은 경우 기본적으로 상속이 가능해야지만 사용할 수 있다(트랜잭션은 Proxy 기술이 사용되기 때문)
    • 코틀린에서는 기본적으로 모든 클래스는 상속이 막혀 있고, 상속이 가능한 클래스/메서드 에는 open 키워드가 붙는다.
    • 매번 @Transactional 애노테이션이 있는 메서드마다, open 키워드가 붙이지 말고 Spring 플러그인을 설치하면 문제가 해결된다.(id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21')
  • 클래스(it)나 ::클래스를 통해 생성자를 호출할 수 있다.

BookService

@Service
class BookService(
    private val bookRepository: BookRepository,
    private val userRepository: UserRepository,
    private val userLoanHistoryRepository: UserLoanHistoryRepository,
) {
    @Transactional
    fun saveBook(request: BookRequest) {
        val book = Book(request.name)
        bookRepository.save(book)
    }

    @Transactional
    fun loanBook(request: BookLoanRequest) {
        val book =
            bookRepository.findByName(request.bookName).orElseThrow(::IllegalArgumentException)
        if (userLoanHistoryRepository.findByBookNameAndIsReturn(request.bookName, false) != null) {
            throw IllegalArgumentException("진작 대출되어 있는 책입니다")
        }
        val user =
            userRepository.findByName(request.userName).orElseThrow(::IllegalArgumentException)
        user.loanBook(book)
    }

    @Transactional
    fun returnBook(request: BookReturnRequest) {
        val user =
            userRepository.findByName(request.userName).orElseThrow(::IllegalArgumentException)
        user.returnBook(request.bookName)
    }
}
  • 이전 코드와 비슷하게 작성하면 된다.
  • 생성자를 통해 주입받아야 되는 빈을 private val 키워드를 사용한다.

Optional 제거하기

JDK8에서 등장한 Optional은 어떤 값이 null이 될 수 있는지를 표시했는데, 코틀린은 언어 자체에서 null여부를 판단할 수 있기 때문에 필요 없다.

1) UserRepository, BookRepository Optional 제거

interface UserRepository : JpaRepository<User, Long> {
    fun findByName(name: String): User?
}
interface BookRepository : JpaRepository<Book, Long> {
    fun findByName(bookName: String): Book? //코틀린에서는 Optional이 필요 없다
}

2) BookService, UserService에서 더 이상 Optional을 사용하지 않으니까, 엘비스 연산자 사용

- throw IllegalArgumentException이 반복되므로 util에 공통 함수로 만들어서 분리

@Service
class BookService(
    private val bookRepository: BookRepository,
    private val userRepository: UserRepository,
    private val userLoanHistoryRepository: UserLoanHistoryRepository,
) {
    @Transactional
    fun saveBook(request: BookRequest) {
        val book = Book(request.name)
        bookRepository.save(book)
    }

    @Transactional
    fun loanBook(request: BookLoanRequest) {
        val book =
            bookRepository.findByName(request.bookName) ?: fail() //항상 반복되므로 리펙토링 가능
        if (userLoanHistoryRepository.findByBookNameAndIsReturn(request.bookName, false) != null) {
            throw IllegalArgumentException("진작 대출되어 있는 책입니다")
        }
        val user =
            userRepository.findByName(request.userName) ?: fail()
        user.loanBook(book)
    }

    @Transactional
    fun returnBook(request: BookReturnRequest) {
        val user =
            userRepository.findByName(request.userName)?: fail()
        user.returnBook(request.bookName)
    }
}
@Service
class UserService(
    private val userRepository: UserRepository,
) {

    @Transactional
    fun saveUser(request: UserCreateRequest) {
        val newUser = User(request.name, request.age)
        userRepository.save(newUser)
    }

    @Transactional(readOnly = true)
    fun getUsers(): List<UserResponse> {
        return userRepository.findAll().map(::UserResponse)
    }

    @Transactional
    fun updateUserName(request: UserUpdateRequest){
        val user = userRepository.findById(request.id).orElseThrow(::IllegalArgumentException)
        user.updateName(request.name)
    }

    @Transactional
    fun deleteUser(name: String){
        //val user = userRepository.findByName(name) ?: throw IllegalArgumentException() //엘비스 연산자 사용
        val user = userRepository.findByName(name) ?: fail()
        userRepository.delete(user)
    }
}
fun fail(): Nothing {
    throw IllegalArgumentException()
}

3) findById와 같이 JPA에서 제공되는 메서드는 Optional로 감싸서 주게 되는데, JPA에서는 이를 코틀린에서 확장해서 사용할 수 있도록 findByIdOrNull() 함수를 제공한다.

- 코틀린에서는 확장 함수 기능을 사용해 기능을 간편하게 추가할 수 있다.

import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.findByIdOrNull

fun fail(): Nothing {
    throw IllegalArgumentException()
}

fun <T, ID> CrudRepository<T, ID>.findByIdOrThrow(id: ID): T { //확장함수 기능을 사용해서 더욱확장이 가능하다
    return this.findByIdOrNull(id) ?: fail()
}
  • CrudRepository에 확장 함수를 만들어서 커스텀 함수를 간편하게 만들 수 있다.
728x90