ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [1단계] Spring MVC 웹 앱 6일차. JPA 코틀린 적용하기
    레거시/레거시-마이 셀파 리부트 2018. 6. 24. 00:30
    반응형

    *본 글은 필자가 진행한 프로젝트 '마이 셀파'의 작업 일기입니다. 재밌게 봐주세요. 코드와 작업 진행 사항은 https://github.com/gurumee92/MySelpaReboot 에서 확인해볼 수 있습니다.


    마이 셀파 리부트 1단계 Spring MVC를 활용한 웹 앱

    6일차 JPA 코틀린 적용하기


    오늘은 5일차에서 진행했던 JPA 작업들을 실제 앱에 적용하여서 자바 코드를 코틀린 코드로 바꾸고 조금 더 코틀린답게 코드를 다듬는 작업을 진행하였습니다. 오늘은 주말이기도 하고 개인적인 사정이 생겨서 계획한 작업량보다 덜 작업하게 되었습니다. 그래도 이번 프로젝트를 주력 언어로 코틀린을 쓰는 것인 만큼 꽤 의미 있는 작업이었습니다. 바로 시작해보죠.


    build.gradle 변경


    우선 build.gradle에 JPA와 MySQL의 의존성을 추가 시켜주어야 합니다. 


    buildscript {
        //동일

        dependencies {
            classpath "org.jetbrains.kotlin:kotlin-noarg:${kotlinVersion}"
            //동일
        }
    }

    apply plugin: "kotlin-jpa"
    //동일

    dependencies {
    compile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.0'
        compile('org.springframework.boot:spring-boot-starter-data-jpa')
        runtime('mysql:mysql-connector-java')
        //동일
    }


    이번에 새롭게 추가된 부분만 언급하겠습니다. 밑의 dependecies의 밑에 2줄은 jpa와 mysql 의존성 추가를 위해 필요한 부분입니다. 그 외에는 코틀린 혹은 자바9 이상에서 MySql 접속과 JPA 관련해서 추가적으로 설정해주는 부분입니다.


    참고1. jaxb-api

     Spring Boot 2 이상 JDK 9 이상이신 분들은 jaxb-api를 무조건적으로 의존성 관계를 추가시켜주어야 합니다. jdk9를 할 때 내부적으로 뭔가 바뀌어서 그런것 같은데 정확한 이유는 잘 모르겠습니다. 만약 추가하지 않으면 컴파일 시 ClassNotFoundException 에러가 뜰 겁니다.


    참고2. koltin-jpa

    원래 코틀린에서 JPA를 쓰려면 data class에서 의무적으로 생성자를 만들어주어야 했습니다. 왜냐하면 저번 JPA 작업 시 언급했던 몇가지 조건 때문인데요, 그러한 불편함을 없애주기 위한 젯브레인즈 라이브러리입니다. 이따 코드 작업을 할 때 다시 언급하겠습니다.


    의존성 관계를 모두 추가한 뒤 다시 build.gradle과 프로젝트를 동기화시켜주었습니다.



    1. build.gradle 우클릭
    2. synchronize 'project명' 클릭


    JPA 작업한 코드들 긁어오기


    5일차에 작업할 코드들을 인텔리제이에 복붙하였습니다. 지난 테스트 프로젝트에서 복사한 파일은 다음과 같습니다.


    • model/User.java
    • model/ParkingLot.java
    • model/Reservation.java
    • repository/user/UserRepository.java
    • repository/lot/ParkingLotRepository.java
    • repository/reservation/ReservationRepository.java
    • service/user/UserService.java
    • service/lot/ParkingLotService.java
    • service/reservation/ReservationService.java


    이제 이 파일들을 코틀린 파일로 바꾸겠습니다. 인텔리제이는 기본적으로 자바를 코틀린으로 변경해줄 수 있는 기능을 자체적으로 가지고 있습니다.




    • code -> Convert Java File to Kotlin File


    또는 자바 파일에서 Ctrl + Alt + Shift + k를 눌러주면 자바 파일을 바꿔줄 수 있습니다. 본 포스팅에서는 유저 관련만 작업을 진행하겠습니다. 그 외의 클래스들은 깃헙을 참고해주세요!


    코틀린 코드 다듬기


    이제부터 변환한 코틀린 파일을 좀 더 코틀린스러운 코드로 다듬는 작업을 진행하겠습니다.


    src/main/kotlin/[프로젝트]/domain/model/User.kt


    package com.gurumee.myselpa.domain.model

    import java.io.Serializable

    import javax.persistence.CascadeType
    import javax.persistence.Column
    import javax.persistence.Entity
    import javax.persistence.FetchType
    import javax.persistence.Id
    import javax.persistence.OneToOne
    import javax.persistence.Table

    @Entity
    @Table(name = "user")
    class User : Serializable {

    @Id
    @Column(name = "id")
    var id: String? = null

    @Column(name = "pw")
    var pw: String? = null

    @Column(name = "phone")
    var phone: String? = null

    @Column(name = "carno")
    var carNo: String? = null

    @Column(name = "is_master")
    var isMaster: Boolean? = null

    @OneToOne(fetch = FetchType.LAZY, mappedBy = "user", cascade = arrayOf(CascadeType.ALL))
    var reservation: Reservation? = null

    override fun toString(): String {
    return ("User [id=" + id + ", pw=" + pw + ", phone=" + phone + ", carNo=" + carNo + ", isMaster=" + isMaster
    + "]")
    }
    }

    기존에 코틀린을 써보지 않으신 분들은 약간 생소할 수도 있는데요, 코틀린은 모던 언어답게 기본적으로 변수들을 val/var 키워드로 지정합니다. 그리고 : Type 을 지정하는데 뒤에 ?표가 붙은 타입들은 널이 될 수 있는 타입들을 말합니다. 기본적으로 ? 가 붙지 않은 타입들은 생성시에 널이 될 수 없습니다. 뭔가 이 코드는 전혀 코틀린 스럽지 못합니다. 코틀린이 궁극적으로 지향하는 바는 다음과 같습니다.


    • 코드 레벨에서 Null Safety한 프로그램
    • 보다 더 생산성 높은 코드

    이제 차근 차근 변경하면 됩니다. 먼저 코틀린에는 data class 라는 키워드가 존재합니다. 컴파일러가 알아서 get/set, toString, hashCode등의 메소드를 만들어주는 클래스를 지칭하는데요 유저를 다음처럼 바꿔주었습니다.


    package com.gurumee.myselpa.domain.model

    import javax.persistence.*
    import java.io.Serializable

    @Entity
    @Table(name = "user")
    data class User(
    @Id
    @Column(name = "id")
    var id: String,

    @Column(name = "pw")
    var pw: String,

    @Column(name = "phone")
    var phone: String,

    @Column(name = "carno")
    var carNo: String,

    @Column(name = "is_master")
    var isMaster: Boolean,

    @OneToOne(fetch = FetchType.LAZY,
    mappedBy = "user",
    cascade = arrayOf(CascadeType.ALL))
    var reservation: Reservation
    ) : Serializable

    data class 키워드를 통해서 훨씬 더 간결하게 코드를 작성하였습니다. 그러나 아직 작업이 더 남아 있습니다. 왜냐하면 이 코드는 컴파일이 되지 않기 때문입니다. 에러가 나는 이유는 OneToOne에 정의된 무언가가 public 지정이 되있기 때문인데 정상적으로 컴파일 시키게 위해서 data class 앞에 internal 키워드를 붙여주었습니다. 또한 일대일 매핑되는 Reservation클래스는 나중에 JPA가 채울 필드이므로 lateinit 키워드를 붙이고 따로 빼 주었습니다. 코드는 다음과 같습니다.


    참고1. internal 키워드

    internal은 기존 자바의 private, package, protected, public 외에 모듈 안에서만 클래스를 접근을 허용하는 코틀린만의 새로운 접근자입니다.


    참고2. lateinit 키워드

    lateinit은 var키워드를 사용하는 필드에 대해서 DI 패턴을 이용하는 프레임워크에서 어떤 필드를 자동적으로 제공해줄 때 쓰면 좋은 키워드입니다. val은 lazy라는 키워드를 이용해서 이런 기능들을 제공합니다. 만약, 이런 키워드를 붙이지 않는다면 클래스 뒤에 ?를 붙여서 널이 될 수 있는 타입으로 만들어야 합니다. 이렇게 되면 코드에서 !!, ?!, ? 등을 계속 쓰게 되서 굉장히 코드가 더러워지니 어지간하면 붙여주세요.


    참고3. kotlin-jpa

    이 플러그인이 없다면 코틀린에서 기본 생성자를 만들어주어야 합니다. data class는 기본적으로 클래스 뒤에 ()로 묶여 있는 필드들에 대해 생성자를 모두 제공해줍니다.  그러나 이럴 경우 주 생성자를 지정하는 작업이 필수적인데 이 플러그인을 적용시켜주면 컴파일러가 JPA가 data class를 쓸 수 있게 알아서 작업해줍니다. 따라서 자바에서 JPA를 썼던 것처럼 주 생성자를 설정해주는 작업을 뺼 수 있습니다.

    package com.gurumee.myselpa.domain.model

    import javax.persistence.*
    import java.io.Serializable

    @Entity
    @Table(name = "user")
    internal data class User(
    @Id
    @Column(name = "id")
    var id: String,

    @Column(name = "pw")
    var pw: String,

    @Column(name = "phone")
    var phone: String,

    @Column(name = "carno")
    var carNo: String,

    @Column(name = "is_master")
    var isMaster: Boolean
    ) : Serializable {
    @OneToOne(fetch = FetchType.LAZY,
    mappedBy = "user",
    cascade = arrayOf(CascadeType.ALL))
    lateinit var reservation: Reservation
    }

    이제 UserRepostiory를 조금 더 코틀린 스럽게 다듬어 보겠습니다. 말은 거창하지만 별로 할 건 없습니다.


    src/main/kotlin/[프로젝트]/domain/repository/user/UserRepository.kt


    package com.gurumee.myselpa.domain.repository.user

    import com.gurumee.myselpa.domain.model.User
    import org.springframework.data.jpa.repository.JpaRepository
    import org.springframework.data.jpa.repository.Query
    import org.springframework.data.repository.query.Param
    import org.springframework.stereotype.Repository


    @Repository
    interface UserRepository : JpaRepository<User, String> {
    @Query("SELECT u FROM User u WHERE u.id = :id AND u.pw = :pw")
    fun findUserByIdAndPw(@Param("id") id: String,
    @Param("pw") pw: String): User
    }

    UserRepository는 컴파일 에러가 뜬 상태로 파일이 변경되어 있습니다. 왜냐하면 코틀린에서는 JpaRepository가 internal 제한자로 정의되어 있기 때문입니다. 따라서 interface 앞에 internal 키워드를 붙여 주었습니다


    참고!

    코틀린에서는 상속과 구현 모두 : 로 표현합니다. 


    package com.gurumee.myselpa.domain.repository.user

    import com.gurumee.myselpa.domain.model.User
    import org.springframework.data.jpa.repository.JpaRepository
    import org.springframework.data.jpa.repository.Query
    import org.springframework.data.repository.query.Param
    import org.springframework.stereotype.Repository


    @Repository
    internal interface User2Repository : JpaRepository<User, String> {
    @Query("SELECT u FROM User u WHERE u.id = :id AND u.pw = :pw")
    fun findUserByIdAndPw(@Param("id") id: String,
    @Param("pw") pw: String): User
    }

    이제 UserService를 변경해보겠습니다.


    src/main/kotlin/[프로젝트]/domain/service/user/UserService.kt


    package com.gurumee.myselpa.domain.service.user

    import com.gurumee.myselpa.domain.model.User
    import com.gurumee.myselpa.domain.repository.user.UserRepository
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.stereotype.Service
    import org.springframework.transaction.annotation.Transactional


    @Service
    @Transactional
    class UserService2 {
    @Autowired
    internal var userRepository: UserRepository? = null

    fun findUser(id: String): User {
    return userRepository!!.findById(id).get()
    }

    fun findUser(id: String, pw: String): User {
    return userRepository!!.findUserByIdAndPw(id, pw)
    } }

    이 클래스 역시 키워드가 잘못 지정되어서 컴파일 오류가 납니다. 왜냐하면 정의된 함수들의 리턴 값 User가 접근 제한자가 internal이기 때문에 함수들도 internal를 붙여주어야 합니다. 함수가 많다면 상당히 귀찮겠죠? 그래서 그 대신에 클래스를 internal 키워드를 붙여주었습니다. 그리고 @Autowired 가 붙은 필드는 lateinit 키워드로 변경해주고 Nullable 타입이 아닌 널이 될 수 없는 타입으로 변경해주었습니다. 그 다음 함수 안에 !!을 지워주었습니다.


    참고! 

    !!는 코틀린이 Nullable 타입의 프로퍼티를 호출할 때 그 변수가 절대 널이 될 수 없음을 확신할 때 쓰는 코드입니다. 코틀린의 주요 철학에 따라 널의 위험성이 되는 코드를 최대한 줄여주기 위하여 Nullable 타입의 프로퍼티들을 호출할 때 ?:, ?, !!등의 키워드를 지원합니다.


    자 이제 앱에서 한 번 모델을 불러봅시다.


    src/main/kotlin/[프로젝트]/app/login/LoginController.kt

    package com.gurumee.myselpa.app.login

    import com.gurumee.myselpa.domain.service.user.UserService
    import org.springframework.beans.factory.annotation.Autowired
    import org.springframework.stereotype.Controller
    import org.springframework.ui.Model
    import org.springframework.web.bind.annotation.RequestMapping

    @Controller
    internal class LoginController{
    @Autowired
    lateinit var userService: UserService

    @RequestMapping("/")
    fun getToLogin(model: Model): String {

    val user = userService.findUser("gurumee2")
    println("LOG: ${user.pw}")

    return "login/login.html"
    }
    }

    먼저 유저 서비스를 자동 주입하기 위하여 클래스를 internal로 바꾸고 서비스 클래스는 lateinit을 걸어 주었습니다. 이제 코드를 실행해보면 정상적으로 콘솔에서 LOG: 2라는 문자열이 뜨게 됩니다.


    참고!

    코틀린은 문자열 속에서 변수를 직접 대입할 수 있는 쿼리 스트링을 제공합니다. 변수를 문자열 속에서 ${} 로 묶어주면 됩니다.


    이렇게 해서 오늘 작업도 무사히 마쳤습니다. 약간 JDK 버전이 맞지 않아서 생기는 문제랑 코틀린 코드로 다듬는데 조금 버거웠지만 나름 재밌는 작업이었습니다. 7일차에는 이전에 예고했던 데로 Spring-MVC와 Spring-Thymeleaf를 이용하여 화면 별 기능을 만드는 작업을 진행하겠습니다.

Designed by Tistory.