ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Repository와 Service 계층 Kotlin으로 변경하기
    프로젝트/자프링 -> 코프링 마이그레이션 2022. 9. 25. 00:01
    728x90

    기존 Java Repository

    interface BookRepository extends JpaRepository<Book, Long> {
    	Optional<Book> findByName(String bookName);
    }

     

    변경된 Kotlin Repository

    interface BookRepository : JpaRepository<Book, Long> {
    	fun findByName(bookName: String): Optional<Book>
    }

    여기서? 대신에 Optional을 사용한 이유는 Service 계층의 구현 변경 최소화를 위해 사용합니다.

    추후에 Service계층을 변환하며 다시 Optional을 제거할 예정입니다.

     

    변경 전 Java Service

    package com.group.libraryapp.service.user;
    
    import com.group.libraryapp.domain.user.User;
    import com.group.libraryapp.domain.user.UserRepository;
    import com.group.libraryapp.dto.user.request.UserCreateRequest;
    import com.group.libraryapp.dto.user.request.UserUpdateRequest;
    import com.group.libraryapp.dto.user.response.UserResponse;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.Collections;
    import java.util.List;
    import java.util.stream.Collectors;
    
    @Service
    public class UserService {
    
      private final UserRepository userRepository;
    
      public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
      }
    
      @Transactional
      public void saveUser(UserCreateRequest request) {
        User newUser = new User(request.getName(), request.getAge(), Collections.emptyList(), null);
        userRepository.save(newUser);
      }
    
      @Transactional(readOnly = true)
      public List<UserResponse> getUsers() {
        return userRepository.findAll().stream()
            .map(UserResponse::new)
            .collect(Collectors.toList());
      }
    
      @Transactional
      public void updateUserName(UserUpdateRequest request) {
        User user = userRepository.findById(request.getId()).orElseThrow(IllegalArgumentException::new);
        user.updateName(request.getName());
      }
    
      @Transactional
      public void deleteUser(String name) {
        User user = userRepository.findByName(name).orElseThrow(IllegalArgumentException::new);
        userRepository.delete(user);
      }
    
    }

     

    변경된 Kotlin Service

    package com.group.libraryapp.service.user
    
    import com.group.libraryapp.domain.user.User
    import com.group.libraryapp.domain.user.UserRepository
    import com.group.libraryapp.dto.user.request.UserCreateRequest
    import com.group.libraryapp.dto.user.request.UserUpdateRequest
    import com.group.libraryapp.dto.user.response.UserResponse
    import org.springframework.stereotype.Service
    import org.springframework.transaction.annotation.Transactional
    
    
    @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 { user -> UserResponse(user)}
        }
    
        @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)
        }
    }

    이때 @Transactional 어노테이션은 오버라이드가 필요한테 코틀린에서는 기본적으로 오버라이드를 막습니다.

    이때 모든 함수에 open을 사용하는 것은 불편하기 때문에 plugin으로 해결하고자 합니다.

     

    build.gradle plugins에 다음과 같이 추가합니다

    id 'org.jetbrains.kotlin.plugin.spring' version '1.6.21'

     

     

    Optional 제거하기

    1단계 repository의 Optional  ->?으로 변경하기

    interface BookRepository : JpaRepository<Book,Long>{
        fun findByName(bookName : String) : Book?
    }

     

    2단계 서비스레이어 Optional 구문들 변경하기

    val user =userRepository.findByName(name).orElseThrow(::IllegalArgumentException)

    repository에 Optional을 제거하면 위와 같은 구문에 컴파일 에러가 발생하게 됩니다.

     

    엘비스 연산자를 활용하여 변경합니다

    val user =userRepository.findByName(name) ?: throw IllegalArgumentException()

     

     

    리팩터링

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

    위의 코드는 throw IllegalArgumentException()이 중복되는 모습을 보입니다.

     

    util 패키지를 만들고 ExceptionUtils라는 코틀린 파일을 생성합니다.

    package com.group.libraryapp.util
    
    fun fail(): Nothing{
        throw IllegalArgumentException()
    }

    이후 fail 함수를 정의합니다.

     

    리팩터링 이후 코드

       @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)
        }

     

    또한 JpaRepository에 종속적인  findById 같은 경우에는 아직까지 Optional을 사용하고 있습니다.

    Optional<T> findById(ID id);

     

    이를 확장 함수를 통해 해결할 수 있는데 스프링에서는 CrudRepository를 확장하는 findByIdOrNull이라는 확장함수를 지원합니다.

    fun <T, ID> CrudRepository<T, ID>.findByIdOrNull(id: ID): T? = findById(id).orElse(null)

     

    따라서 다음과 같이 변환해서 사용할 수 있습니다.

    val user = userRepository.findByIdOrNull(request.id) ?: fail()

     

    확장함수를 이용해서 한번 더 감싸기

    util 패키지에 다음과 같이 정의합니다.

    fun <T, ID> CrudRepository<T, ID>.findByIdOrThrow(id : ID) : T {
        return this.findByIdOrNull(id) ?: fail()
    }

    CrudRepository를 확장하고 null이 반환되면 fail() 메서드를 실행하여 예외를 던집니다.

     

    코드는 다음과 같이 간략해집니다.

     val user = userRepository.findByIdOrThrow(request.id)

     

     

     

     

    출처

    https://www.inflearn.com/course/java-to-kotlin-2/

     

    실전! 코틀린과 스프링 부트로 도서관리 애플리케이션 개발하기 (Java 프로젝트 리팩토링) - 인프

    Java + Spring Boot로 만들어진 웹 애플리케이션을 Kotlin + Spring Boot로 리팩토링 하고 추가 요구사항을 구현합니다. 이 과정에서 Junit5, SQL, JPA, Querydsl 을 사용할 뿐 아니라, 설계 및 구현 관점에서 다양

    www.inflearn.com

     

     

    댓글

Designed by Tistory.