코틀린 숙련도를 높이는 가장 좋은 방법은 코틀린으로 작성한 코드가 자바로 어떻게 표현되는지 확인하기
역컴파일로 예기치 않은 코드 생성을 방지
기존 자바 라이브러리와 프레임워크를 사용하며 문제가 발생할 때 빠르게 확인 가능
Kotlin to Java
Tool > Kotlin > Show Kotlin Bytecode > Decompile
Java to Kotlin
Convert Java File to Kotlin File
Item 3. 롬복대신 데이터 클래스
롬복 대신 데이터 클래스 사용하기
데이터 클래스를 사용하면 컴파일러가 equals(), hashCode(), toString(), copy() 등을 자동 생성
주 생성자의 매개변수를 기준으로 생성하고, 주 생성자에는 하나 이상의 매개변수가 있어야 함
매개변수는 val 또는 var로 표시
데이터 클래스에 property 를 선언하는 순간 해당 property 는 field, Getter, Setter, 생성자 파라미터 역할
val person =Person(name ="Aaron", age =29)val person2 = person.copy(age =25)dataclassPerson(val name: String, val age: Int)
plugins
plugins { val kotlinVersion ="1.6.21" id("org.springframework.boot") version "2.3.3.RELEASE"// 스프링 부트를 사용하기 위한 플러그인 id("io.spring.dependency-management") version "1.0.10.RELEASE"// 스프링 부트 프로젝트에서 의존성 관리를 쉽게 할 수 있도록 지원하는 플러그인 kotlin("jvm") version kotlinVersion // Kotlin JVM을 사용하는 프로젝트를 위한 플러그인 kotlin("plugin.spring") version kotlinVersion kotlin("plugin.jpa") version kotlinVersion id("org.jlleitschuh.gradle.ktlint") version "10.3.0"// Kotlin 코드 스타일을 자동으로 검사하고 포맷팅하는 도구 id("org.asciidoctor.jvm.convert") version "3.3.2"// Asciidoctor를 사용해서 문서를 생성하고 변환할 수 있도록 지원하는 플러그인 id("org.flywaydb.flyway") version "7.12.0"// Flyway를 사용하여 데이터베이스 마이그레이션을 자동으로 관리할 수 있게 해주는 플러그인}
Spring Boot
final 클래스
@SpringBootApplication은 @Configuration을 포함하고, 스프링은 기본적으로 CGLIB을 사용해서 @Configuration 클래스에 대한 프록시를 생성
하지만, final 클래스는 상속이나 오버라이드가 불가하므로 프록시 생성이 불가
상속을 허용하고 오버라이드를 허용하려면 open 변경자 추가 필요
Spring Framework 5.2 부터는 @Configuration 의 proxyBeanMethod 옵션을 통해 프록시 생성 비활성화 가능
All-open 컴파일러 플러그인
코틀린은 다양한 컴파일러 플러그인을 제공
all-open 컴파일러 플러그인은 지정한 애너테이션이 있는 클래스와 모든 멤버에 open 변경자를 추가
스프링을 사용할 경우 all-open 컴파일러 플러그인을 래핑한 kotlin-spring 컴파일러 플러그인을 사용 가능
@Component, @Transactional, @Async 등이 기본적으로 지정
File > Project Structure > Project Settings > Modules > Kotlin > Compiler Plugins 에서 지정된 애너테이션 확인 가능
plugins { kotlin('plugins.spring') version "1.5.21"}allOpen { annotation("com.my.Annotation")}
Item 4. 지연 초기화
필드 주입이 필요하면 지연 초기화를 사용하자
코틀린에서 lateinit 변경자를 붙이면 프로퍼티를 나중에 초기화할 수 있다. 나중에 초기화하는 프로퍼티는 항상 var
no-arg 컴파일러 플러그인은 지정한 애너테이션이 있는 클래스에 매개변수가 없는 생성자를 추가
JPA, Kotlin에서 직접 호출할 수 없지만 리플랙션을 사용하여 호출 가능
kotlin-spring Compiler Plugin과 마찬가지로 JPA를 사용하는 경우 no-arg 컴파일러 플러그인을 래핑한 kotlin-jpa Compiler Plugin 사용 가능
JPA, Kotlin 사용 시 자동으로 kotlin-jpa Compiler Plugin 추가
@Entity, @Embeddable, @MappedSuperclass 가 기본적으로 지정
plugins { kotlin('plugins.spring') version "1.5.21" kotlin('plugins.jpa') version "1.5.21"}allOpen { // JPA 지연로딩 적용을 위해 프록시 생성 목적으로 설정 annotation("javax.persistence.Entity") annotation("javax.persistence.MappedSuperclass")}
Item 6. 엔티티
엔티티에 데이터 클래스 사용을 피하자.
양방향 연관 관계의 경우 toString(), hashcode() 로 무한 순환 참조가 발생
Item 7. 사용자 지정 getter
사용자 지정 getter를 사용하자.
JPA 에 의해 인스턴스화 될 때, 초기화 블록이 호출되지 않으므로, 영속화하지 않는 필드는 사용자 지정 getter를 사용하자.
/** * AS-IS: 초기화된 프로퍼티 * 영속화하지 않을 목적이었지만, JPA 에 의해 인스턴스화 될 때 * null을 허용하지 않았음에도 불구하고, 초기화 블록이 호출되지 않으므로 null이 들어가게 된다. */@Transientval fixed: Boolean= startDate.until(endDate).year <1---/** * TO-BE: 사용자 지정 getter * 따라서, 사용자 지정 getter 를 정의해서 프로퍼티에 접근할 때마다 호출되도록 하자. * 뒷받침하는 필드(backing property)가 존재하지 않으므로 AccessType.FIELD 라도 @Transient 불필요 */val fixed: Booleanget() = startDate.until(endDate).year <1// 일종의 메서드로 생각하자
Item 8. Null 타입 제거
Null이 될 수 있는 타입은 빠르게 제거하자.
Null이 될 수 있는 타입을 사용하면 Null 검사나 !! 연산자가 필요하다.
엔티티 클래스의 id를 0 또는 빈 문자열로 초기화해서 Null이 될 수 있는 타입을 제거하자.
Optional 보다 Nullable 한 타입을 사용해서 불필요한 java import 를 줄이자.
funMemberRepository.getById(id: Long): Member {if (id ==0L) {return Member.SINGLE }returnfindByIdOrNull(id) ?: throwNoSuchElementException("존재하지 않는 아이디 입니다. id: $id")}interfaceMemberRepository : JpaRepository<Member, Long>
Test
단위 테스트
Junit, Mockito 같은 Java 라이브러리로 테스트하므로 Kotlin 다운 코드 작성이 어려움
예외 테스트는 JUnit5의 assertThrows 및 assertDoesNotThrow 같은 Kotlin 함수를 사용하면 간결
// Java 스타일@DisplayName("회원의 비밀번호와 다를 경우 예외가 발생한다")@Testfunauthenticate {val user = createUserassertThatExceptionOfType(UnidentifiedUserException::class.java) .isThrownBy { user.authenticate(WRONG_PASSWORD) }}// Kotlin 스타일@Testfun`회원의 비밀번호와 다를 경우 예외가 발생한다`() {val user = createUserassertThrows<UnidentifiedUserException> { user.authenticate(WRONG_PASSWORD) }}---// Kotest"회원의 비밀번호와 다를 경우 예외가 발생한다" {val user =createUser()shouldThrow<UnidentifiedUserException> { user.authenticate(WRONG_PASSWORD) }}
테스트 팩토리
테스트 픽스처를 반환하는 팩토리 함수를 만들 수 있다.
테스트 픽스처: 테스트를 위한 전제 조건
Kotlin 기본 인자를 사용하면 빌더 패턴처럼 다양한 경우를 처리 가능
기본 인자와 이름 붙인 인자를 적절히 사용하면 테스트하려는 관심사를 드러내는 데 사용 가능
createMission(submittable =true) // 과제 제출물을 제출할 수 있는 과제createMission(submittable =false) // 과제 제출물을 제출할 수 없는 과제createMission(title ="과제1", evaluationId =1L) // 특정 평가에 대한 과제createMission(hidden =false) // 숨기지 않은 과제// 관련 없는 인자에는 합리적인 기본값을 사용funcreateMission( title: String= MISSION_TITLE, description: String= MISSION_DESCRIPTION, evaluationId: Long=1L, startDateTime: LocalDateTime= START_DATE_TIME, endDateTime: LocalDateTime= END_DATE_TIME, submittable: Boolean=true, hidden: Boolean=false, id: Long=0L): Mission {returnMission(title, description, evaluationId, startDateTime, endDateTime, submittable, hidden, id)}
privatefunApplicantAndFormResponse.hasKeywordInNameOrEmail(keyword: String): Boolean {return name.contains(keyword) || email.contains(keyword)}...When("특정 키워드로 특정 모집에 지원한 지원 정보를 조회하면") {val actual = applicantService.findAllByRecruitmentIdAndKeyword(recruitmentId, keyword)Then("지원 정보 및 부정행위 여부를 확인할 수 있다") { actual shouldHaveSize 2 actual.filter { it.hasKeywordInNameOrEmail(keyword) } shouldHaveSize 2// 확장함수 적용 actual[0].isCheater.shouldBeTrue() actual[1].isCheater.shouldBeFalse() }}
Kotest 어설션
👎🏻 테스트 어설션
일부 어설션이 실패하더라도 테스트가 즉시 중지되지 않고, 끝까지 검증
어떤 프로퍼티 때문에 실패했는지 바로 알아채기 어려움
val excepted =Speaker("Aaron", "Park", 30)val actual =Speaker("Aaron", "Kim", 28)assertAll ( { assertThal(actual.firstName).isEqualTo(expexted.firstName) }, { assertThal(actual.lastName).isEqualTo(expexted.lastName) }, { assertThal(actual.age).isEqualTo(expexted.age) },)
인스턴스를 데이터 클래스로 만들고 비교하는 것을 권장
val excepted =Speaker("Aaron", "Park", 30)val actual =Speaker("Aaron", "Kim", 28)assertThal(actual).isEqualTo(expexted)
주로 JUnit의 Assertions 대신 읽기 쉬운 AssertJ의 Assertions를 사용
👍🏼 Kotest 어설션
Kotest 어설션은 간결하고 기존 JUnit5와 혼용 가능
스마트 캐스트 같은 Kotlin 기능을 지원
한 번 스마트 캐스팅 되었다면 null을 다루지 않아도 된다.
val actual = userRepository.findByEmail("aaron@google.com")actual.sholudNotBeNull() // 스마트 캐스트actual.name shouldBe "박지훈"
Given, When, Then 을 이루는 모든 것을 beforeSpec, afterSpec으로 listen
Given, When, Then 만을 본다면 Container 라고 부르고
beforeContainer, afterContainer 메서드로 수명주기 관리 가능
Then 은 beforeEach, afterEach 메서드로 수명주기 관리 가능
Given("특정 모집에 대한 임시 지원서를 작성한 지원자가 있고 모집 항목이 있는 경우") {...When("미제출 항목이 있는 상태에서 지원서를 최종 제출하면") {..Then("예외가 발생한다") {... } }When("항목이 비어 있는 상태에서 지원서를 최종 제출하면") {...Then("예외가 발생한다") {... } }}Given("특정 회원이 특정 모집에 대해 작성한 임시 지원서가 있는 경우") {...When("해당 지원서를 조회하면") {...Then("지원서를 확인할 수 있다") {... } }}
Kotest의 격리 모드
Kotest는 SingleInstance, InstancePerLeaf, InstancePerTest 세 가지 값이 존재
기본은 SingleInstance 이며 테스트 상황에 맞게 격리 모드를 선택
ex. 테스트 클래스 전체에 모의 객체를 만드는 데 비용이 많이 든다면 SingleInstance를 선택하고, clearMocks()을 호출하는 것이 더 나을 수 있다.
통합 테스트
Spring 5.2부터 @TestConstructor 사용 시 생성자를 통한 주입이 가능
Kotest는 Spring 통합 테스트를 지원하기 위해 SpringExtenstion을 제공
별도의 애너테이션 없이 생성자 주입이 가능하며, SpringExtenstion을 통해서 트랜잭션 롤백도 가능
인수 테스트
사용자 관점에서 기능이 올바르게 작동하는지 확인하기 위한 테스트
일반적으로 사용자 스토리에 따라 Given-When-Then 스타일로 작성
연관 관계가 복잡할수록 테스트 픽스처 생성이 어려워짐
인수 테스트를 위한 DSL
연관 관계를 도메인에 특화된 언어로 표현하고, 필요한 데이터가 생성되도록 할 수 있다.
코드를 처음 접하는 사람들의 도메인 학습에 많은 도움이 된다.
부록
Kover
IntelliJ, JaCoCo 에이전트를 사용하는 Kotlin 코드 커버리지 도구
Kotlin이 생성한 바이트코드로 측정하며 인라인 함수와 같이 JaCoCo로 측정할 수 없는 영역도 측정