전전 공댕이의 공부 기록
[코틀린 쿡북 12장] 스프링 프레임워크 본문
스프링 프레임워크란?
오픈소스 프레임워크 중 하나
개발자 -> 목표 달성에 필요한 비즈니스 로직 담는 빈 작성
스프링 -> 개발자가 작성한 메타데이터를 바탕으로 보안, 트랜잭션, 리소스 풀링 등 제공
12장에서 다룰 것들
- 코틀린으로 스프링 어플 작성 시 사용 가능한 몇 가지 기술
- 스프링 생태계에 코틀린을 적용하는 방법에 대한 영감 제공
[레시피 12.1] 확장을 위해 스프링 관리 빈 클래스 오픈하기
문제
스프링은 개발자가 작성한 클래스를 확장하는 프록시를 생성해야한다. 하지만 코틀린 클래스는 기본으로 닫혀있다.
해법
확장을 위해 자동으로 필요한 스프링 관리 클래스를 열어주는 코틀린 스프링 플러그인을 빌드 파일에 추가
프록시와 실체
프록시 & 실체 -> 둘 다 같은 인터페이스 구현 / 같은 클래스 확장
1. 들어오는 요청을 프록시가 가로챔
2. 서비스가 요구하는 모든 것을 프록시가 적용
3. 요청을 실체로 전달
+참고 사이트: www.cloudflare.com/ko-kr/learning/cdn/glossary/reverse-proxy/
+ 도움이 될만한 영상: www.youtube.com/watch?v=jGQTS1CxZTE
스프링 트랜젝션 프록시:
1. 어떤 메소드 호출을 가로챈 다음 트랙잭션 시작
2. 해당 메소드를 호출
3. 실체 메소드 안에서 일어난 상황에 맞춰 트랜잭션 커밋/롤백
스프링의 경우, 시동 과정에서 프록시를 생성하는데 실체가 클래스라면 해당 클래스를 확장해야합니다!
그런데 코틀린은 기본으로 정적으로 결합해서 문제가 된다고 합니다.
즉, 클래스가 open 키워드를 사용해 확장을 위한 열림으로 표시되지 않으면 메소드 재정의 / 클래스 확장이 불가능하다고 합니다. 그래서 코틀린은 이런 문제를 all-open이라는 플러그인으로 해결합니다.
all-open 플러그인
클래스&클래스에 포함된 함수에 명시적으로 open 키워드를 추가하지 않고 명시적인 open 애노테이션으로 클래스를 설정합니다.
이것도 유용하지만, kotlin-spring 플러그인이 더 뛰어나다고 합니다.
kotlin-spring 플러그인 추가하기
사용하기 위해 그레이들이나 메이븐 빌드 파일에 플러그인을 추가해야합니다.
start.spring.io/에서 새로운 kotlin-spring 플러그인이 포함된 그레이들 빌드 파일을 만들 수 있습니다.
참고 사이트:
spring.io/guides/tutorials/spring-boot-kotlin/
infoscis.github.io/2018/08/30/spring-boot-kotlin/
//build.gradle.kts 파일 내용
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
id("org.springframework.boot") version "2.4.5"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.4.32"
kotlin("plugin.spring") version "1.4.32"
kotlin("plugin.jpa") version "1.4.32"
}
group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-mustache")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
[레시피 12.2] 코틀린 data 클래스로 퍼시스턴스 구현하기
문제
코틀린 data 클래스로 자바 퍼시스턴스 API(=JPA)를 사용하고 싶다
해법
kotlin-jpa 플러그인을 빌드 파일에 추가한다.
일반적으로 코틀린 data 클래스를 정의할 때 필요한 속성을 아래처럼 주 생성자에 추가합니다.
data class Person(val name:Stirng, val dob: LocalDate)
JPA 관점에서 data 클래스는 두 가지 문제가 있습니다.
1. JPA는 모든 속성에 기본 값을 제공하지 않는 이상 기본 생성자가 필수지만 data 클래스에는 기본 생성자가 없다.
2. val 속성과 함께 data 클래스를 생성하면 불변 객체가 생성되는데, JPA는 불변 객체와 더불어 잘 동작하도록 설계되지 않았다.
각각의 문제들을 살펴봅시다.
문제 1. JPA는 모든 속성에 기본 값을 제공하지 않는 이상 기본 생성자가 필수지만 data 클래스에는 기본 생성자가 없다.
=> 코틀린에서 2가지 플러그인 제공
1) no-arg 플러그인
2) kotlin-jpa 플러그인
1) no-arg 플러그인
- 인자가 없는 생성자를 추가할 클래스 선택 가능
- 기본 생성자 추가를 호출하는 애노테이션 정의 가능
- 코틀린 엔티티에 기본 생성자를 자동으로 구성
추가 방법
12.1에서 설명한 kotlin-sprin 플러그인처럼 빌드 파일에 필요한 문법을 추가해 no-arg 플러그인을 사용할 수 있다.
앞 파일에서 살펴본 후 내용을 추가하자.
plugins {
//...
kotlin("plugin.jpa") version "1.4.32"
}
//...
dependencies {
//...
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
}
근데 저는 이미 다 추가되어있더라구요. 미리 dependencies 부분에 이것저것 추가해놔서일까요?
no-arg 컴파일러 플러그인은 합성한 기본 연산자를 코틀린 클래스에 추가합니다.
즉, 자바나 코틀린에서는 합성 기본 연산자를 호출할 수 없지만, 스프링에서는 리플렉션을 사용해 합성 기본 연산자를 호출할 수 있습니다.
원한다면 no-arg 플러그인을 사용할 수 있지만, 인자가 없는 생성자가 필요한 클래스를 표시하는 데 사용할 애노테이션을 정의해야합니다.
2) kotlin-jpa 플러그인
사용이 더 쉬운 플러그인
no-arg 플러그인을 기반으로 만들어졌다.
@Entity
@Embeddable
@MappedSuperclass 와 같은 애노테이션으로 자동 표시된 클래스에 기본 생성자를 추가한다.
문제 2. val 속성과 함께 data 클래스를 생성하면 불변 객체가 생성되는데, JPA는 불변 객체와 더불어 잘 동작하도록 설계되지 않았다.
JPA가 엔티티에 불변 클래스를 사용하고 싶어하지 않는다.
따라서 스프링 개발팀은 엔티티로 사용하고 싶은 코틀린 클래스에 필드 값을 변경할 수 있게 속성에 var 타입을 사용하는 단순 클래스의 사용을 추천한다.
//예제 12-4
@Entity
class Article(
var title: String,
var headline: String,
var content: String,
@ManyToOne var authorL User,
var slug: String = title.toSlug(),
var addedAt: LocalDateTime = LocalDateTime.now(),
@Id @GeneratedValue var id: Long? = null)
@Entity
class User(
var login: String,
var firstname: String,
var lastname: String,
var description: String? = null,
@Id @GeneratedValue var id: Long? = null)
두 클래스 Article, User 은 속성에 var을 사용하고, 생성된 기본 키 필드는 널을 허용합니다.
하이버네이트 문법에서는 널 기본 키는 임시 상태에 있는 인스턴스임을 나타냅니다.
즉, 연결된 데이터베이스 테이블에 해당 인스턴스와 연관된 행이 없다는 것입니다.
기본 키가 없는 상황은 클래스를 처음 인스턴스화하고 아직 데이터베이스에 저장하지 않았을 때 혹은 데이터베이스에서 행을 삭제했지만 해당 인스턴스가 아직 메모리에 있을 때에만 발생합니다.
이렇게 var 사용을 광범위하게 하고, 자동으로 생성되는 toString, equals, hashCode 함수의 부재는 불편함을 느끼게 할 수 있으나 JPA가 요구하는 방식에 더 적합합니다.
[레시피 12.3] 의존성 주입하기
문제
오토와이어링이 필요한 빈과 필요하지 않는 빈을 선언하고 의존성 주입을 사용해서 빈을 서로 오토와이어링하고 싶다.
해법
코틀린 스프링은 생성자 주입을 제공하지만 필드 주입에는 lateinit var 구조를 사용해야한다. 선택적인 빈은 널 허용 타입으로 선언한다.
의존성 주입
빈을 서로 연결
한 타입의 레퍼런스를 다른 타입의 클래스에 추가하면 스프링은 개발자를 대신해서 레퍼런스 타입의 인스턴스를 제공하는 방법을 찾아줍니다.
스프링은 의존성을 생성자로 주입하는 것을 선호하고,
@Autowired 애노테이션을 생성자 인자에 직접 사용할 수 있습니다.
클래스에서 생성자가 하나 뿐이면 스프링이 자동으로 클래스의 유일한 생성자에 모든 인자를 자동으로 오토와이어링해 이 애노테이션을 쓸 필요가 없습니다!
즉, 스프링이 관리하는 빈에 생성자가 하나만 있다면 스프링은 모든 인자를 자동으로 주입합니다.
서비스를 주입하는 REST 컨트롤러 예제를 봅시다.
이 예제에서는 의존성을 주입하는 4가지 방법을 보여줍니다.
//01. 단일 생성자를 갖는 클래스
@RestController
class GreetingController(val service: GreetingService) {//...}
//02. 명시적으로 오토와이어링
@RestController
class GreetingController(@Autowired val service: GreetingService) {//...}
//03. 오토와이어링 생성자 호출, 주로 다수의 의존성을 갖는 클래스
@RestController
class GreetingController @Autowired constructor(val service: GreetingService) {//...}
//04. 필드 주입 (비추천)
@RestController
class GreetingController{
@Autowired
lateinit var service: GreetingService
//...
}
01. 단일 생성자를 갖는 클래스
생성자가 하나뿐인 클래스는 의존성을 속성으로 선언하면 해당 속성을 자동으로 오토와이어링한다.
02. 명시적으로 오토와이어링
같은 방식으로 동작하는 @Autowired 애노테이션을 생성자 인자에 명시적으로 사용한다.
하지만 @Autowired의 명시적인 선언은 두 번째 생성자를 추가해도 계속해서 잘 동작한다.
03. 오토와이어링 생성자 호출, 주로 다수의 의존성을 갖는 클래스
대체로 다수의 의존성을 주입할 때는 @Autowired를 생성자 함수 앞에 위치시켜 간소화한다.
04. 필드 주입 (비추천)
반드시 필드 주입을 사용해야하는 경우 lateinit var 변경자를 사용해야한다.
lateinit이 var과 함께 사용된 이유:
val 속성 -> 선언 시 값이 반드시 있어야 함 => 값을 나중에 제공해 초기화할 수 없음
var 속성 -> 단점: 원치 않더라도 정의대로 차후에 언제든지 값이 변경될 수 있음
생성자 주입을 선호하는 이유: 값을 언제든지 변경할 수 있음
클래스의 속성이 필수가 아니라면 해당 속성을 널 허용 타입으로 선언할 수 있습니다.
코틀린은 널 허용 타입 속성을 선택 가능한 속성이라는 의미로 받아들입니다.
@DataJpaTest
class RepositoriesTests @Autowired constructor(
val entityManager: TestEntityManager,
val userRepository: UserRepository,
val articleRepository: ArticleRepository) {
//...
}
//RestTemplate 클래스를 사용하는 테스트
@SpringBootTest(webEnviroment = SpringBootTest.WebEnviroment.RANDOM_PORT)
class IntegrationTests(@Autowired val restTemplate: TestRestTemplate) {
//...
}
-> autowired 생성자를 사용하면 lateinit var 접근 방식을 사용할 필요가 없음
->
첫 번째 RepositoriesTests: 개발자가 생성한 클래스를 오토와이어링함
두 번째 IntegrationTests: 임의의 포트 번호로 테스트 서버를 시작, 웹 애플리케이션을 테스트 서버에 배포한 다음 getForObject 또는 getForEntity 함수를 사용해 REST 요청을 만들기 위해 TestRestTemplate 클래스를 사용
'Kotlin > 기본 개념' 카테고리의 다른 글
[코틀린 쿡북 13장.1] 코루틴과 구조적 동시성 (0) | 2021.04.29 |
---|