0. 이 글의 목적
Kotlin으로 Entity를 만드는 것에 대해 기술한다.
1. Java에서 도메인은?
아 맞다. 우리는 상남자식 코딩답게 jpa를 사용할 수 있는 환경도 만들어주지 않았다. gradle을 추가해 주자.
https://spring.io/guides/tutorials/spring-boot-kotlin/
Getting Started | Building web applications with Spring Boot and Kotlin
Instead of using util classes with abstract methods like in Java, it is usual in Kotlin to provide such functionalities via Kotlin extensions. Here we are going to add a format() function to the existing LocalDateTime type in order to generate text with th
spring.io
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
맨땅에서 뭐라도 해보는 토이 코프링(3) - JWT 토큰 발급하기
0. 이 글의 작성 이유 최소한으로 JWT토큰을 발급하고 사용자에게 전달하는 과정을 설명하기 위함 다음에 Spring Security를 이용한 보안 처리를 진행할 예정이어서 미리 대비해 둔 코드가 존재할 수
developer-youn.tistory.com
이 글과 순서가 조금 꼬이다 보니 저런 현상이 일어났다.
여기서 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
스프링에서 코틀린 스타일 테스트 코드 작성하기 | 우아한형제들 기술블로그
{{item.name}} 안녕하세요 저는 공통시스템개발팀에서 플랫폼 개발을 담당하고 있는 김규남이라고 합니다. 이 글은 올해 사내에서 진행한 코틀린 밋업에서 스프링에서 코틀린 스타일 테스트 코드
techblog.woowahan.com
'JAVA > 코프링' 카테고리의 다른 글
맨땅에서 뭐라도 해보는 토이 코프링(3) - JWT 토큰 발급하기 (0) | 2023.09.20 |
---|---|
맨땅에서 뭐라도 해보는 토이 코프링(1) - 프로젝트 생성 (0) | 2023.09.17 |