0. 이 글의 목적
Kotlin으로 Entity를 만드는 것에 대해 기술한다.
1. Java에서 도메인은?
아 맞다. 우리는 상남자식 코딩답게 jpa를 사용할 수 있는 환경도 만들어주지 않았다. gradle을 추가해 주자.
https://spring.io/guides/tutorials/spring-boot-kotlin/
Java에서 추가하던 것 과는 다르게 Kotlin JPA Plugin을 추가해야 한다고 한다.
내리다 보니 jakson모듈도 추가하라고 한다. 같이 추가하자.
현재까지의 gradle은 이렇게 되어있다.
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "3.1.3"
id("io.spring.dependency-management") version "1.1.3"
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
kotlin("plugin.jpa") version "1.8.22"
}
group = "com.berno"
version = "0.0.1-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_17
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
testImplementation("org.springframework.boot:spring-boot-starter-test")
runtimeOnly("com.h2database:h2")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
이제 import가 가능해졌다.
이제 자바에서 코틀린으로 수정해 보자.
Kotlin에서는 클래스 선언부에서 소괄호()를 이용해 기본 생성자를 지정해 줄 수 있다. 이 부분에 Entity의 멤버에 대한 내용을 추가해 준다.
2. 도메인 테스트
테스트 프레임워크로 JUnit이 아닌 Kotest를 정했다. 이유는 코틀린에 친화적이라고 하기 때문이다.
import io.kotest.core.spec.style.AnnotationSpec
import io.kotest.matchers.shouldBe
class UserDomainTest : AnnotationSpec() {
@Test
fun `User 도메인 객체 생성 테스트`() {
val userEmail = "hello@test.com"
val userPassword = "1q2w3e4r!!"
val userName = "tester1"
val siteUser1 = SiteUser(userEmail, userPassword, userName)
siteUser1.userId shouldBe 0
siteUser1.email shouldBe userEmail
siteUser1.password shouldBe userPassword
}
}
Kotest에는 테스트 진행 방식에 대해 Spec으로 구분하고 있다. AnnotationSpec을 사용하면 JUnit스타일로 작성할 수 있다.
3. 레포지토리 작성
interface UserJpaRepository: JpaRepository<SiteUser, Long> {
fun findByEmail(userEmail: String): SiteUser
fun findByUsername(userName: String): SiteUser
}
interface UserRepository {
fun save(siteUser : SiteUser): Long
fun findUser(id : Long): SiteUser
fun findByUserEmail(userEmail: String): SiteUser
fun findByUserName(userName: String): SiteUser
}
@Repository
class UserRepositoryImpl @Autowired constructor(
val jpaRepository: UserJpaRepository
) : UserRepository {
override fun save(siteUser: SiteUser): Long {
return jpaRepository.save(siteUser).userId
}
override fun findUser(id: Long): SiteUser {
return jpaRepository.findById(id).orElseThrow { IllegalStateException() }
}
override fun findByUserEmail(userEmail: String): SiteUser {
return jpaRepository.findByEmail(userEmail)
}
override fun findByUserName(userName: String): SiteUser {
return jpaRepository.findByUsername(userName)
}
}
코틀린에서의 생성자 주입은 기본 생성자 부분에 @Autowired부터 작성해 주면 된다.
코드가 뭔가 불편해 보일 수 있다. expression으로 바꿔보자.
@Repository
class UserRepositoryImpl @Autowired constructor(
val jpaRepository: UserJpaRepository
) : UserRepository {
override fun save(siteUser: SiteUser): Long = jpaRepository.save(siteUser).userId
override fun findUser(id: Long): SiteUser = jpaRepository.findById(id).orElseThrow { IllegalStateException() }
override fun findByUserEmail(userEmail: String): SiteUser = jpaRepository.findByEmail(userEmail)
override fun findByUserName(userName: String): SiteUser = jpaRepository.findByUsername(userName)
}
코틀린에서는 한 문장인 경우 =을 통해 expression으로 작성할 수 있다.
4. 레포지토리 테스트 작성
@SpringBootTest
@DisplayName(name = "유저 레포지토리 테스트")
class UserRepositoryTest(@Autowired var userRepository: UserRepository) : AnnotationSpec() {
@Test
fun `유저 도메인 객체 저장 테스트`() {
val user1Email = "test1@tttt.com"
val user2Email = "test2@ttt.com"
val user1Password = "1q2w3e4r!!!!"
val user2Password = "1q2w3e4r@@@!@"
val user1Name = "tester1"
val user2Name = "tester2"
val siteUser1 = SiteUser(user1Email, user1Password, user1Name)
val siteUser2 = SiteUser(user2Email, user2Password, user2Name)
val user1 = userRepository.save(siteUser1)
val user2 = userRepository.save(siteUser2)
user1 shouldBe 4
user2 shouldBe 5
}
}
여기서 뭔가 이상함을 느낄 수 있다.
왜 갑자기 shouldBe의 예상 값으로 4와 5가 튀어나왔는가
여기 스토리에는 적지 않았지만 별도로 뭣좀 하느라 미리 데이터를 3개 입력해 두는 코드를 작성해 두었다.
@Service
class PreInit(val userRepository: UserRepository){
@PostConstruct
fun addData(){
userRepository.save(SiteUser("hello@test.com","1q2w3e4r!!", "tester1"))
userRepository.save(SiteUser("hello2@test.com","1q2w3e4r@@", "tester2"))
userRepository.save(SiteUser("hello3@test.com","1q2w3e4r##", "tester3"))
}
}
이걸 끄는 방법도 존재하는데 작성 당시 귀찮았다. 나중에 Profile을 나누면서 같이 진행해 보자.
5. 서비스 파일 작성
interface UserService {
fun save(user : UserRequest) : Long
fun find(userId : Long) : UserRequest
fun signin(userSignin: UserSigninDTO): String
}
@Service
class UserServiceImpl(
@Autowired var userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
private val authenticationManager: AuthenticationManager,
private val jwtUtils: JWTUtils
) : UserService {
override fun save(userDTO: UserRequest): Long = userRepository.save(userDTO2Entity(userDTO))
override fun find(userId: Long): UserRequest = userEntity2DTO(userRepository.findUser(userId))
@Transactional
override fun signin(userSignin: UserSigninDTO): String{
userRepository.save(SiteUser(userSignin.email, userSignin.password, userSignin.name))
return jwtUtils.issueToken(userSignin.name)
}
private fun userDTO2Entity(userDTO: UserRequest): SiteUser = SiteUser(userDTO.email, userDTO.userName, userDTO.password)
private fun userEntity2DTO(user: SiteUser): UserRequest = UserRequest(user.email, user.password, user.username)
}
뭔가 좀 이상한 게 껴있다. 사과하겠다.
https://developer-youn.tistory.com/167
이 글과 순서가 조금 꼬이다 보니 저런 현상이 일어났다.
여기서 DTO와 Entity의 변환은 더 나이스한 방법이 있는데 추후 기술하도록 하겠다.
6. 서비스 테스트 작성
@SpringBootTest
class UserServiceTest(@Autowired var userService: UserService) : BehaviorSpec() {
init {
given("유저 정보(이메일, 비밀번호)") {
val user1Email = "tester1@test.com"
val user1Password = "1q2w3e4r!!"
val user1Name = "tester1"
val user2Email = "tester2@test.com"
val user2Password = "1q2w3e4r@@"
val user2Name = "tester2"
`when`("유저를 저장하면") {
val user1Id = userService.save(UserRequest(user1Email, user1Password, user1Name))
val user2Id = userService.save(UserRequest(user2Email, user2Password, user2Name))
then("user1과 user2의 아이디가 다르다.") {
user1Id shouldNotBe user2Id
}
}
`when`("유저를 저장한 뒤 불러오면") {
val user1Id = userService.save(UserRequest(user1Email, user1Password, user1Name))
val user2Id = userService.save(UserRequest(user2Email, user2Password, user2Name))
val user1 = userService.find(user1Id)
val user2 = userService.find(user2Id)
then("user의 이메일과 저장하고 불러온 이메일이 동일하다.") {
user1.email shouldBe user1Email
user2.email shouldBe user2Email
}
}
}
}
}
이번에는 BehaviorSpec을 이용한다. TDD의 given-when-then을 시각적으로 잘 이용할 수 있는 방식이다.
테스트 결과에서도 given-when-then이 이루어지며 하나의 given 아래에서 여러 when-then을 구성할 수 있다.
7. 컨트롤러 작성
@RestController
@RequestMapping("/api/user")
class UserController(@Autowired var userService: UserService,){
@GetMapping("/search/{userId}")
fun searchUser(@PathVariable userId : Long): UserRequest = userService.find(userId)
@PostMapping("/signin")
fun siginin(@RequestBody userSignin: UserSigninDTO) = ResponseEntity.ok().body(userService.signin(userSignin))
}
여기서 코틀린의 재미있는 점을 하나 설명하기 위해 일부러(라고 했지만 실수) 기본 생성자 부분에 , 를 남겨보았다. 놀랍게도 에러가 발생하지 않는다.
8. 컨트롤러 테스트 작성
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
class UserControllerTest(val mockMvc: MockMvc, val userRepository: UserRepository) {
val user1Email = "test1@tttt.com"
val user2Email = "test2@ttt.com"
val user1Password = "1q2w3e4r!!!!"
val user2Password = "1q2w3e4r@@@!@"
val user1Name = "tester1"
val user2Name = "tester2"
@BeforeEach
fun insertData() {
userRepository.save(SiteUser(user1Email, user1Password, user1Name))
userRepository.save(SiteUser(user2Email, user2Password, user2Name))
}
@Test
fun 유저_찾기_테스트() {
mockMvc.perform(
get("/api/user/search/4")
).andExpect(
status().isOk
).andExpectAll(
jsonPath("$.email").value(user1Email),
jsonPath("$.password").value(user1Password)
).andDo(
print()
)
}
}
MockMvc를 이용해 컨트롤러 테스트를 진행했다.
위에서 이미 사과했지만 다시 언급하자면 PreInit으로 인해 데이터가 순서가 조금 꼬였다. 나중에 고치도록 하자.
ref
'JAVA > 코프링' 카테고리의 다른 글
맨땅에서 뭐라도 해보는 토이 코프링(3) - JWT 토큰 발급하기 (0) | 2023.09.20 |
---|---|
맨땅에서 뭐라도 해보는 토이 코프링(1) - 프로젝트 생성 (0) | 2023.09.17 |