Loading...
Spring Framework Reference Documentation 7.0.2의 Spring Projects in Kotlin의 한국어 번역본입니다.
아래의 경우에 피드백에서 신고해주신다면 반영하겠습니다.
감사합니다 :)
이 섹션은 Kotlin으로 Spring projects를 개발할 때 참고할 만한 구체적인 힌트와 권장사항을 제공합니다.
기본적으로, Kotlin의 모든 클래스와 멤버 함수는 final입니다.
클래스에 대한 open 한정자는 Java의 final과 반대로, 다른 클래스가 이 클래스를 상속할 수 있게 합니다.
이는 멤버 함수에도 적용되며, override되기 위해서는 open으로 표시되어야 합니다.
Kotlin의 JVM 친화적인 설계는 일반적으로 Spring과 마찰이 없지만, 이 특정 Kotlin 기능은 이 사실을 고려하지 않으면 애플리케이션이 시작되는 것을 방해할 수 있습니다.
이는 Spring 빈(기술적인 이유로 기본적으로 런타임에 확장되어야 하는 @Configuration으로 어노테이션된 클래스와 같은)이 일반적으로 CGLIB에 의해 프록시되기 때문입니다.
우회 방법은 CGLIB에 의해 프록시되는 Spring 빈의 각 클래스와 멤버 함수에 open 키워드를 추가하는 것이지만, 이는 빠르게 고통스러워질 수 있고, 코드를 간결하고 예측 가능하게 유지한다는 Kotlin의 원칙에 반합니다.
@Configuration(proxyBeanMethods = false)를 사용하여 설정 클래스에 대해 CGLIB 프록시를 피하는 것도 가능합니다.<br>자세한 내용은proxyBeanMethodsJavadoc을 참고하십시오.
다행히도 Kotlin은 kotlin-spring 플러그인(kotlin-allopen 플러그인의 미리 구성된 버전)을 제공하며, 다음 어노테이션 중 하나로 어노테이션되거나 메타어노테이션된 타입에 대해 클래스와 그 멤버 함수들을 자동으로 open으로 만듭니다:
@Component@Async@Transactional@Cacheable메타어노테이션 지원은 @Configuration, @Controller, @RestController, @Service, @Repository로 어노테이션된 타입이 자동으로 open된다는 것을 의미하는데, 이는 이 어노테이션들이 @Component로 메타어노테이션되어 있기 때문입니다.
Kotlin 컴파일러가 프록시와 final 메서드의 자동 생성이 관련된 일부 사용 사례는 추가적인 주의가 필요합니다.<br>예를 들어, 프로퍼티를 가진 Kotlin 클래스는 관련된
finalgetter와 setter를 생성합니다. 관련된 메서드를 프록시할 수 있도록<br>하기 위해서는, 타입 레벨@Component어노테이션을 메서드 레벨@Bean보다 선호해야 하며,<br>그래야kotlin-spring플러그인에 의해 해당 메서드가 open될 수 있습니다. 전형적인 사용 사례는@Scope와 그 인기 있는<br>@RequestScope특수화입니다.
start.spring.io는 기본적으로 kotlin-spring 플러그인을 활성화합니다.
따라서 실제로는 Java에서처럼 추가적인 open 키워드 없이 Kotlin 빈을 작성할 수 있습니다.
Spring Framework 문서의 Kotlin 코드 샘플은 클래스와 그 멤버 함수에 대해<br>
open을 명시적으로 지정하지 않습니다. 샘플은kotlin-allopen플러그인을 사용하는 프로젝트를 위해 작성되었으며,<br>이는 가장 일반적으로 사용되는 설정이기 때문입니다.
Kotlin에서는 다음 예제와 같이 primary constructor 내에서 읽기 전용 프로퍼티를 선언하는 것이 편리하며, 모범 사례로 간주됩니다:
1class Person(val name: String, val age: Int)
선택적으로 data keyword를 추가하여 primary constructor에 선언된 모든 프로퍼티로부터 다음 멤버들을 컴파일러가 자동으로 유도하게 할 수 있습니다:
equals() 및 hashCode()"User(name=John, age=42)" 형식의 toString()componentN() 함수copy() 함수다음 예제에서 보듯이, 이는 Person 프로퍼티가 읽기 전용이더라도 개별 프로퍼티를 쉽게 변경할 수 있게 해줍니다:
1data class Person(val name: String, val age: Int) 2 3val jack = Person(name = "Jack", age = 1) 4val olderJack = jack.copy(age = 2)
일반적인 퍼시스턴스 기술(JPA 등)은 기본 생성자를 요구하며, 이러한 종류의 설계를 방해합니다.
다행히도, “default constructor hell”에 대한 우회 방법이 있으며, Kotlin은 JPA 어노테이션으로 어노테이션된 클래스에 대해 synthetic no-arg constructor를 생성하는 kotlin-jpa 플러그인을 제공합니다.
다른 퍼시스턴스 기술에 대해 이러한 종류의 메커니즘을 활용해야 한다면, kotlin-noarg 플러그인을 구성할 수 있습니다.
Kay release train 시점부터, Spring Data는 Kotlin immutable 클래스 인스턴스를 지원하며<br>모듈이 Spring Data 객체 매핑(MongoDB, Redis, Cassandra 등)을 사용하는 경우<br>
kotlin-noarg플러그인을 요구하지 않습니다.
다음 예제에서 보듯이, val 읽기 전용(가능하다면 널이 될 수 없는) 프로퍼티를 사용하여 생성자 주입을 선호하는 것이 권장됩니다:
1@Component 2class YourBean( 3 private val mongoTemplate: MongoTemplate, 4 private val solrClient: SolrClient 5)
단일 생성자를 가진 클래스는 그 매개변수가 자동으로 자동 주입됩니다.<br>그래서 위 예제에서 보듯이 명시적인
@Autowired constructor가 필요하지 않습니다.
정말로 필드 주입을 사용해야 한다면, 다음 예제에서 보듯이 lateinit var 구문을 사용할 수 있습니다:
1@Component 2class YourBean { 3 4 @Autowired 5 lateinit var mongoTemplate: MongoTemplate 6 7 @Autowired 8 lateinit var solrClient: SolrClient 9}
internal 가시성 한정자가 있는 Kotlin 함수는 JVM 바이트코드로 컴파일될 때 이름이 맹글링되며, 이는 이름으로 의존성을 주입할 때 부작용을 야기합니다.
예를 들어, 다음 Kotlin 클래스를 보겠습니다:
1@Configuration 2class SampleConfiguration { 3 4 @Bean 5 internal fun sampleBean() = SampleBean() 6}
이는 컴파일된 JVM 바이트코드의 Java 표현으로 다음과 같이 변환됩니다:
1@Configuration 2@Metadata(/* ... */) 3public class SampleConfiguration { 4 5 @Bean 6 @NotNull 7 public SampleBean sampleBean$demo_kotlin_internal_test() { 8 return new SampleBean(); 9 } 10}
그 결과, 관련 빈 이름은 Kotlin 문자열으로 "sampleBean$demo_kotlin_internal_test"로 표현되며, 일반적인 public 함수 사용 사례의 "sampleBean"이 아닙니다.
이러한 빈을 이름으로 주입할 때는 맹글링된 이름을 사용하거나, 이름 맹글링을 비활성화하기 위해 @JvmName("sampleBean")을 추가해야 합니다.
Java에서는 @Value("${property}")와 같은 어노테이션을 사용하여 설정 프로퍼티를 주입할 수 있습니다.
그러나 Kotlin에서는 $가 문자열 보간에 사용되는 예약 문자입니다.
따라서 Kotlin에서 @Value 어노테이션을 사용하려면, @Value("\${property}")와 같이 $ 문자를 이스케이프해야 합니다.
Spring Boot를 사용하는 경우,
@Value어노테이션 대신<br>@ConfigurationProperties를<br>사용하는 것이 좋습니다.
대안으로, 다음 PropertySourcesPlaceholderConfigurer 빈을 선언하여 프로퍼티 플레이스홀더 접두사를 커스터마이징할 수 있습니다:
1@Bean 2fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply { 3 setPlaceholderPrefix("%{") 4}
다음 예제에서 보듯이, 여러 PropertySourcesPlaceholderConfigurer 빈을 선언하여 표준 ${…} 구문을 사용하는 컴포넌트(Spring Boot 액추에이터나 @LocalServerPort 등)와 커스텀 %{…} 구문을 사용하는 컴포넌트를 동시에 지원할 수 있습니다:
1@Bean 2fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply { 3 setPlaceholderPrefix("%{") 4 setIgnoreUnresolvablePlaceholders(true) 5} 6 7@Bean 8fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
또한, 기본 이스케이프 문자는 JVM 시스템 프로퍼티(또는 SpringProperties 메커니즘)를 통해 spring.placeholder.escapeCharacter.default 프로퍼티를 설정하여 전역적으로 변경하거나 비활성화할 수 있습니다.
Java와 Kotlin 예외 처리는 꽤 비슷하지만, 주요 차이점은 Kotlin이 모든 예외를 언체크 예외로 취급한다는 점입니다.
그러나 프록시된 객체(예: @Transactional로 어노테이션된 클래스나 메서드)를 사용할 때, 체크 예외가 던져지면 기본적으로 UndeclaredThrowableException으로 래핑됩니다.
Java에서처럼 원래 던져진 예외를 얻으려면, 메서드는 @Throws로 어노테이션되어 던져지는 체크 예외를 명시적으로 지정해야 합니다 (예: @Throws(IOException::class)).
Kotlin 어노테이션은 대부분 Java 어노테이션과 유사하지만, (Spring에서 광범위하게 사용되는) 배열 속성은 다르게 동작합니다.
Kotlin 문서에 설명된 것처럼, value 속성은 다른 속성과 달리 이름을 생략할 수 있으며, vararg 매개변수로 지정할 수 있습니다.
그 의미를 이해하기 위해, 가장 널리 사용되는 Spring 어노테이션 중 하나인 @RequestMapping을 예로 들어 보겠습니다.
이 Java 어노테이션은 다음과 같이 선언됩니다:
1public @interface RequestMapping { 2 3 @AliasFor("path") 4 String[] value() default {}; 5 6 @AliasFor("value") 7 String[] path() default {}; 8 9 RequestMethod[] method() default {}; 10 11 // ... 12}
@RequestMapping의 전형적인 사용 사례는 핸들러 메서드를 특정 경로와 메서드에 매핑하는 것입니다.
Java에서는 어노테이션 배열 속성에 대해 단일 값을 지정할 수 있으며, 이는 자동으로 배열로 변환됩니다.
그래서 @RequestMapping(value = "/toys", method = RequestMethod.GET) 또는 @RequestMapping(path = "/toys", method = RequestMethod.GET)와 같이 작성할 수 있습니다.
그러나 Kotlin에서는 @RequestMapping("/toys", method = [RequestMethod.GET]) 또는 @RequestMapping(path = ["/toys"], method = [RequestMethod.GET])와 같이 작성해야 하며 (이름 있는 배열 속성의 경우 대괄호를 지정해야 합니다).
이 특정 method 속성(가장 일반적인 것)에 대한 대안은 @GetMapping, @PostMapping 등의 단축 어노테이션을 사용하는 것입니다.
@RequestMappingmethod속성이 지정되지 않으면,GET메서드만이 아니라<br>모든 HTTP 메서드가 매칭됩니다.
Kotlin으로 작성된 Spring 애플리케이션에서 제네릭 타입을 다루는 것은, 일부 사용 사례에서는 Kotlin의 선언 지점 변성을 이해할 필요가 있습니다. 이는 타입을 선언할 때 변성을 정의할 수 있게 해주며, 사용 지점 변성만을 지원하는 Java에서는 불가능한 기능입니다.
예를 들어, Kotlin에서 List<Foo>를 선언하는 것은 개념적으로 java.util.List<? extends Foo>와 동등한데, 이는 kotlin.collections.List가 interface List<out E> : kotlin.collections.Collection<E>로 선언되어 있기 때문입니다.
이는 Java 클래스를 사용할 때 제네릭 타입에 out Kotlin 키워드를 사용해야 한다는 것을 의미하며, 예를 들어 Kotlin 타입에서 Java 타입으로의 org.springframework.core.convert.converter.Converter를 작성할 때 그렇습니다.
1class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> { 2 // ... 3}
어떠한 종류의 객체를 변환할 때는, out Any 대신 *를 사용한 스타 프로젝션을 사용할 수 있습니다.
1class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> { 2 // ... 3}
Spring Framework는 아직 빈을 주입하기 위해 선언 지점 변성 타입 정보를 활용하지 않습니다.<br>관련 진행 상황을 추적하려면 spring-framework#22313에 구독하십시오.
이 섹션은 Kotlin과 Spring Framework 조합으로 테스트하는 방법을 다룹니다. 권장되는 테스트 프레임워크는 JUnit이며, mocking을 위해 Mockk를 함께 사용하는 것입니다.
Kotlin에서는 백틱(
) 사이에 의미 있는 테스트 함수 이름을 지정할 수 있습니다.<br>구체적인 예시는 이 섹션 뒷부분의Find all users on HTML page() 테스트 함수를 참고하십시오.
Spring Boot를 사용하는 경우,<br>관련 문서를 참고하십시오.
전용 섹션에 설명된 것처럼, JUnit Jupiter는 빈의 생성자 주입을 허용하며, 이는 Kotlin에서 lateinit var 대신 val을 사용하기 위해 매우 유용합니다.
모든 매개변수에 대해 자동 주입을 활성화하려면 @TestConstructor(autowireMode = AutowireMode.ALL)을 사용할 수 있습니다.
junit-platform.properties파일에서spring.test.constructor.autowire.mode = all프로퍼티를 사용하여<br>기본 동작을ALL로 변경할 수도 있습니다.
1@SpringJUnitConfig(TestConfig::class) 2@TestConstructor(autowireMode = AutowireMode.ALL) 3class OrderServiceIntegrationTests( 4 val orderService: OrderService, 5 val customerService: CustomerService 6) { 7 8 // tests that use the injected OrderService and CustomerService 9}
PER_CLASS LifecycleJUnit Jupiter에서 Kotlin 테스트 클래스는 @TestInstance(TestInstance.Lifecycle.PER_CLASS) 어노테이션을 사용하여 테스트 클래스의 단일 인스턴스화를 활성화할 수 있으며, 이를 통해 non-static 메서드에 @BeforeAll 및 @AfterAll 어노테이션을 사용할 수 있습니다.
이는 Kotlin에 잘 맞는 방식입니다.
junit-platform.properties파일에서junit.jupiter.testinstance.lifecycle.default = per_class프로퍼티를 사용하여<br>기본 동작을PER_CLASS로 변경할 수도 있습니다.
다음 예제는 non-static 메서드에서 @BeforeAll 및 @AfterAll 어노테이션을 사용하는 방법을 보여줍니다:
1@TestInstance(TestInstance.Lifecycle.PER_CLASS) 2class IntegrationTests { 3 4 val application = Application(8181) 5 val client = WebClient.create("http://localhost:8181") 6 7 @BeforeAll 8 fun beforeAll() { 9 application.start() 10 } 11 12 @Test 13 fun `Find all users on HTML page`() { 14 client.get().uri("/users") 15 .accept(TEXT_HTML) 16 .retrieve() 17 .bodyToMono<String>() 18 .test() 19 .expectNextMatches { it.contains("Foo") } 20 .verifyComplete() 21 } 22 23 @AfterAll 24 fun afterAll() { 25 application.stop() 26 } 27}
Kotlin과 JUnit Jupiter의 @Nested 테스트 클래스 지원을 사용하여 명세 기반 테스트를 작성할 수 있습니다.
다음 예제는 그 방법을 보여줍니다:
1class SpecificationLikeTests { 2 3 @Nested 4 @DisplayName("a calculator") 5 inner class Calculator { 6 7 val calculator = SampleCalculator() 8 9 @Test 10 fun `should return the result of adding the first number to the second number`() { 11 val sum = calculator.sum(2, 4) 12 assertEquals(6, sum) 13 } 14 15 @Test 16 fun `should return the result of subtracting the second number from the first number`() { 17 val subtract = calculator.subtract(4, 2) 18 assertEquals(2, subtract) 19 } 20 } 21}
Coroutines
Getting Started