# Kotlin
#### Kotlin 프로젝트를 시작하며 생긴 질문
- Gradle vs Maven: https://pearlluck.tistory.com/704
- Lombok을 사용할까 말까?: https://d2.naver.com/helloworld/6685007
#### Run Configuration
- JRE: azul-17
Active profiles: local

#### JPA
- build.gradle.kts
jpa관련 plugin과 dependency를 추가하고 allOpen, noArg 설정을 추가함
- allOpen: 자동으로 모든 클래스(프로퍼티, 함수까지)를 open 시킴 open되지 않은 class는 Java에서 final과 동일 (final: class 상속 불가, method override 불가)
- noArg: 설정한 어노테이션이 붙은 클래스에 자동으로 기본 생성자를 생성
```kotlin
plugins {
...
kotlin("plugin.jpa") version "1.7.20"
id("org.jetbrains.kotlin.plugin.allopen") version "1.7.20"
id("org.jetbrains.kotlin.plugin.noarg") version "1.7.20"
}
dependencies {
...
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
}
noArg {
annotation("javax.persistence.Entity")
}
allOpen {
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
annotation("javax.persistence.Embeddable")
}
```
- entity
```kotlin
@Entity
@Table(name = "users")
class UserEntity (
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null, // val은 getter만 var은 setter도 할당
@Column(name = "name")
var name: String? = null,
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "department")
var department: Department,
@Column(name = "inserted_at")
var insertedAt: ZonedDateTime? = ZonedDateTime.now(),
@Convert(converter = UserStatusConverter::class)
@Column(name = "status")
var status: UserStatus? = UserStatus.ACTIVATED,
)
enum class UserStatus(val status: String) {
ACTIVATED("activated"),
DEACTIVATED("deactivated"),
INACTIVATED("inactivated"),
INOPERATIVE("inoperative");
}
@Converter
class UserStatusConverter : AttributeConverter<UserStatus, String> {
override fun convertToDatabaseColumn(UserStatus): String {
return UserStatus.status
}
override fun convertToEntityAttribute(value: String): UserStatus {
return UserStatus.valueOf(value.toUpperCase())
}
}
```
- repository
```kotlin
@Repository
interface UserRepository : JpaRepository<UserEntity, Long> {
fun findAllByVacationStartAtAndVacationEndAtAndPublicKey(userId: Long): MutableList<Users> // bad
@Query(SELECT u FROM USER u WHERE u.vacationStartAt AND u.vacationEndAt AND u.publicKey)
fun findAllByVacationTimeAndPKey(userId: Long): MutableList<Users> // good
fun findByOrderById(pageable: Pageable): Page<User>
// count 쿼리가 중복적으로 발생하는걸 방지하기 위해 전체 사이즈를 미리 구해놓고 함께 전달
fun findByOrderById(pageable: Pageable, Integer totalEleme): Page<User>
}
```
- service
```kotlin
@Service
class UserService(
private val userRepository: UserRepository,
) {
fun findAll(): List<UserEntity> {
return userRepository.findAll()
}
}
```
#### JPA + Auditoring
jpa를 사용하다 보면 @CreatedBy @CreatedDate @UpdatedBy @LastModifiedDate등 annotation을 통해 Entity의 생성과정에서 셋팅되어야할 프로퍼티를 설정하고 싶을 수 있다.
- @CreatedDate and @LastModifiedDate
```kotlin
@SpringBootApplication
@EnableJpaAuditing // JpaAuditing을 설정해주고
class MainApplication
@Entity
@EntityListeners(AuditingEntityListener::class)
class SomeEntity {
val id: Long? = 0,
val name: String,
...
@CreatedDate
var createdDateTime: LocalDateTime = LocalDateTime.now()
// 위 처럼 사용해주면 되는데 한가지 유의할 점은 val 대신 var을 사용해줘야 setter가 할당된다
}
```
- @CreatedBy and @LastModifiedBy
- JpaAuditorConfig
```kotlin
@Configuration
@EnableJpaAuditing(modifyOnCreate = false)
class JpaAuditingConfig {
@Component
class CustomAuditorAware : AuditorAware<String> {
override fun getCurrentAuditor(): Optional<String> {
val authentication = SecurityContextHolder.getContext().authentication
val roles = authorities
if (roles has user) return authentication.name
else "Unknown User"
}
}
}
```
- Entity
```kotlin
@Entity
@EntityListeners(AuditingEntityListener::class)
class SomeEntity {
val id: Long? = 0,
val name: String,
...
@CreatedBy
var createdBy: String? = null
// 위 처럼 사용해주면 되는데 한가지 유의할 점은 val 대신 var을 사용해줘야 setter가 할당된다
}
```
#### MySQL
- build.gradle.kts
```kotlin
dependencies {
...
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("mysql:mysql-connector-java")
}
```
- application.yml
```yaml
datasource:
url: jdbc:mysql://ip:port/database_name
username: username
password: password
```
#### Spring Security
- build.gradle.kts
```kotlin
dependencies {
...
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
}
```
- WebSecurityConfig
```kotlin
@Configuration
@EnableWebSecurity
class WebSecurityConfig(
private val jwtFilter: JwtFilter
) : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.requestMatchers(RequestMatcher { request: HttpServletRequest? -> CorsUtils.isPreFlightRequest(request!!) }).permitAll()
.anyRequest().hasAuthority("user") // role이 아니라 authority
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
}
}
```
- JwtUtil
RSA256기반 public key를 사용한 token parsing 기법
kotlin 사용에 중점을 둔 프로젝트라 토큰 생성은 이곳에서 다루지 않음
```kotlin
class JwtUtils() {
val publicKey: String = "publicKey"
// 토큰발취
fun resolveToken(req: HttpServletRequest): String? {
val authHeader: String? = req.getHeader("Authorization")
var tokenString: String? = null
if (authHeader != null && authHeader.startsWith("Bearer ")) {
tokenString = authHeader.replace("Bearer ".toRegex(), "").trim()
}
return tokenString
}
// 토큰검증
fun validateToken(token: String?): Boolean {
return try {
val claims: Claims = getAllClaims(token)
return claims.expiration.after(Date())
} catch (e: Exception) {
false
}
}
// username으로 Authentcation객체 생성
fun getAuthentication(token: String): Authentication {
val claims: Claims = getAllClaims(token)
return UsernamePasswordAuthenticationToken(claims.subject, null, listOf(SimpleGrantedAuthority("user")))
}
// 모든 Claims 조회
private fun getAllClaims(token: String?): Claims {
val kf: KeyFactory = KeyFactory.getInstance("RSA")
val pubKeySpecX509EncodedKeySpec = X509EncodedKeySpec(Base64.getDecoder().decode(publicKey))
val key = kf.generatePublic(pubKeySpecX509EncodedKeySpec)
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.body
}
}
```
- JwtFilter
```kotlin
@Component
class JwtFilter(private val jwtUtils: JwtUtils) : GenericFilterBean() {
@Throws(IOException::class, ServletException::class)
override fun doFilter(request: ServletRequest?, response: ServletResponse?, filterChain: FilterChain) {
val token: String? = (request as HttpServletRequest?)?.let { jwtUtils.resolveToken(it) }
if (token != null && jwtUtils.validateToken(token)) {
val auth: Authentication = jwtUtils.getAuthentication(token)
SecurityContextHolder.getContext().authentication = auth
}
filterChain.doFilter(request, response)
}
}
```
#### Swagger
- build.gradle.kts
```kotlin
dependencies {
...
implementation("io.springfox:springfox-boot-starter:3.0.0")
implementation("io.springfox:springfox-swagger-ui:3.0.0")
}
```
- SwaggerConfig
```kotlin
@Configuration
@EnableSwagger2
class SwaggerConfig {
@Bean
fun api(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.useDefaultResponseMessages(false)
.select()
.apis(RequestHandlerSelectors.basePackage("path.to.api.package"))
.paths(PathSelectors.any())
.build()
}
```
- WebSecurityConfig
```kotlin
override fun configure(http: HttpSecurity) {
http
...
.antMatchers("/swagger-resources/**").permitAll() // swagger resource api filter 허용
}
// 정적 요소 필터 제외
override fun configure(web: WebSecurity) {
web.ignoring().antMatchers(
"/v2/api-docs",
"/swagger-resources/**",
"/swagger-ui/**",
"/webjars/**",
"/swagger/**"
)
}
```
#### Logging
- build.gradle.kts
```kotlin
dependencies {
...
runtimeOnly("io.github.microutils:kotlin-logging-jvm:3.0.2")
}
```
- logback.xml
```xml
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} MDC=%X{user} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
```
- service.kts
```kotlin
@Service
class Service(
private val someRepository: SomeRepository,
) {
private val logger = KotlinLogging.logger {}
fun logTest(arg1: String, arg2: String) {
logger.debug { "Hello World arg1=$arg1 arg2=$arg2" }
}
}
```