---
# System prepended metadata

title: Kotlin
tags: [playground]

---

# 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
![](https://i.imgur.com/SGbUBfC.png)

#### 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" }
        }
    }
    ```