ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring boot - API 서버 만들기 ( ft. Kotlin ) - ( 1 )
    Spring/Spring Boot 2021. 8. 30. 11:07
    반응형

    코틀린을 사용하여 API 를 만드는 방법을 처음부터 하나하나 따져 기록 해놓으려고 한다. 이유는 막상 구축하려고했을때 중간중간 기억이 안나거나 헷갈리는 부분들이 존재 하기 때문에 처음부터 차근차근 기록하려고한다. 

    환경은 intellij 로 할것이고 jdk 설치나 이런건 생략 한다. 

     

    Initializr 를 최대한 활용한다. 

     필요한 라이브러리들을 선택해준다. 화면에 표시된것 이외에 필요한게 있다면 더 선택해주면 된다. 이프로젝트는 단순 샘플용이고 JPA 기반에 단순한 기능들만 일단 만들것이기 때문에 별도로 다른것들은 선택하지 않았다. 

    선택하고나면 새로운 창으로 띄울것인지 물어볼것이고 프로젝트가 열리면 기본적으로 intellij 에서 gradle 프로젝트 세팅을 짧게 진행한다. 

    처음 해줘야 할것은 일단 환경 설정을 마무리 해주는것이다. 미리 어느정도 하지 않았을경우 코딩을 바로 할수 없다. 

    그래서 일단 gradle 설정부터 하게 된다. 

     

     

    import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
    
    plugins {
        val kotlinVersion = "1.5.21"
    
        kotlin("jvm") version kotlinVersion
        kotlin("kapt") version kotlinVersion
        kotlin("plugin.jpa") version kotlinVersion
        kotlin("plugin.allopen") version kotlinVersion
        kotlin("plugin.noarg") version kotlinVersion
        kotlin("plugin.spring") version kotlinVersion
    
        id("org.springframework.boot") version "2.5.3"
        id("io.spring.dependency-management") version "1.0.11.RELEASE"
        id( "com.ewerk.gradle.plugins.querydsl") version "1.0.10"
        
    }
    
    allOpen {
        annotation("javax.persistence.Entity")
        annotation("javax.persistence.MappedSuperclass")
        annotation("javax.persistence.Embeddable")
    
    //    // mongodb
    //    annotation("org.springframework.data.mongodb.core.mapping.Document")
    }
    
    repositories {
        mavenCentral()
        jcenter()
    }
    
    group = "com.example"
    version = "0.0.1-SNAPSHOT"
    java.sourceCompatibility = JavaVersion.VERSION_11
    
    repositories {
        mavenCentral()
    }
    kapt {
        annotationProcessor("com.querydsl.apt.jpa.JPAAnnotationProcessor")
    //    annotationProcessor("org.springframework.data.mongodb.repository.support.MongoAnnotationProcessor")
    }
    
    dependencies {
        implementation("org.projectlombok:lombok:1.18.18")
        val kotlinxSerializationJsonVersion = "1.1.0"
        val p6spyVersion = "1.6.3"
        val springmockkVersion = "3.0.1"
        val s3mockVersion = "0.2.6"
        val javaUuidGeneratorVersion = "4.0.1"
    
        implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
        implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    //    implementation("org.springframework.boot:spring-boot-starter-data-mongodb")
        implementation("org.springframework.boot:spring-boot-starter-validation")
    //    implementation("org.springframework.boot:spring-boot-starter-data-rest") // swagger 2 사용시 해당 라이브러리를 사용할경우 충돌로 인해 문제 발생
        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")
        implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
        implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationJsonVersion")
        testImplementation("org.jetbrains.kotlin:kotlin-test")
    
        // RestTemplate에서 PATCH를 지원하기 위해 HttpClient를 이용
        implementation("org.apache.httpcomponents:httpclient")
    
        developmentOnly("org.springframework.boot:spring-boot-devtools")
        testImplementation("org.springframework.boot:spring-boot-starter-test")
    
        //DB
        implementation("mysql:mysql-connector-java")
        implementation("com.querydsl:querydsl-jpa") // querydsl 설정
        implementation("com.vladmihalcea:hibernate-types-52:2.10.4")
        kapt(group = "com.querydsl", name = "querydsl-apt", classifier = "jpa") // querydsl 설정
        annotationProcessor(group = "com.querydsl", name = "querydsl-apt", classifier = "jpa")
        implementation("com.github.gavlyukovskiy:p6spy-spring-boot-starter:${p6spyVersion}")// JPA 로그 출력
        //h2
        implementation("com.h2database:h2")
    
        // JWT
        implementation( "io.jsonwebtoken:jjwt-api:0.11.2")
        runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
        runtimeOnly( "io.jsonwebtoken:jjwt-jackson:0.11.2")
    
        // UUID
        implementation("com.fasterxml.uuid:java-uuid-generator:$javaUuidGeneratorVersion")
    
        //swagger
        implementation( group="io.springfox" , name="springfox-swagger-ui" , version="2.9.2")
        implementation(group="io.springfox",name="springfox-swagger2",version="2.9.2")
    
        // 테스트
        testImplementation("org.springframework.boot:spring-boot-starter-test")
        testImplementation("com.ninja-squad:springmockk:${springmockkVersion}")
        testImplementation("io.findify:s3mock_2.13:${s3mockVersion}")
    
        // Utills
        implementation("org.apache.commons:commons-lang3")
    }
    
    tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
        kotlinOptions {
            freeCompilerArgs = listOf("-Xjsr305=strict")
            jvmTarget = "11"
        }
    }
    
    tasks.getByName<Jar>("jar") {
        enabled = false
    }
    
    // val jar: Jar by tasks
    // val bootJar: BootJar by tasks
    // bootJar.enabled = true
    // jar.enabled = false
    
    tasks.withType<Test> {
        /*
        filter{
            includeTestsMatching("com.sellermill.common.utils.PathUtilsTest.*")
        }
        dependsOn("cleanTest")
        testLogging {
            events("passed", "skipped","failed")
        }
        */
        useJUnitPlatform()
    //    exclude("**/*")
    }

    위 gradle 설정 사항을 적용하고 실행했을때 별 문제 없다면 1차 성공이다. 

    간략하게 설정 사항을 설명 하자면 

    plugins 

     전반적인 메인 설정들이 들어가 있다. 대부분 프로젝트 생성시 자동생성으로 만들어 지는 부분인데 여기서 allopen 은 꼭 넣어줘야 코틀린에서 상속을 사용할때 문제가 생기지 않는다.  allopen무엇때문에 필요한지는 아래에서 더자세히 설명하겠다. 

    중간에 보면 plugin.noarg 이 있는데 이것을 사용한 이유는 JPA 를 사용시 객체는 무조건 기본생성자가 필요하다. 하지만 코틀린은 주생성자는 존재하지만 기본생성자는 존재하지 않는데 이것을 해결하기 위해서 선언한것이다. 이걸 사용시 자바 바이너리 코드로 컴파일시 자동으로 기본 생성자를 생성해주기 때문에 해당 이슈가 생기지 않는다. 

    allOpen 

    코틀린의 특성(?) 때문에 생긴 문제다. 코틀린에서는 클래스의 기본 상속 제어자가 final이기 때문에 지연로딩으로 설정해도 프록시를 만들지 못해 지연로딩이 되지 않는 문제가 발생한다. 결국엔 지연로딩이 필요한 JPA 를 사용하기 때문인데 해당 설정을 해줄경우 @Entity 애너테이션이 붙은 클래스들은 final이 아는  open으로 변경된다.

     

    kapt 

    코틀린 프로젝트를 컴파일 할 때는 javac가 아닌 kotlinc로 컴파일을 하기 때문에 Java로 작성한 Annotation이  동작하지 않는다. 그렇기 때문에 코틀린에서는 이러한 Annotation 처리를 하기 위해 KAPT(Kotlin Annotation Processing Tool)를 제공한다. 이것을 사용하기 위한 설정이다. 

     

    dependencies

     이곳을 많이 사용하게 되는데 라이브러리의 디펜전시를 선언하는 부분이다. 내가 사용하고자 하는 기능들을 추가할때 이곳에 추가 해주면 된다. 간단히 순차적으로 살펴보면 스프링 부트사용시 최초 사용하겠다고 설정했던 것들이 처음 한단락으로 정렬을 해놓았고 그다음은 코틀린설정들이 있다. 대부분 명칭만 봐도 어느 용도인지는 유추가 가능하다. 그리고 다음 기타적으로 http 통신을 위한 라이브러리 , 데이터베이스사용을 위한 라이브러리 ( QueryDSL 설정 ) , jwt , uuid , 테스트 사용을 위한 것들만 기본적으로 선언 해놓았다. 

     

    tasks.withType  해당부분은 코틀린 컴파일에 해당하는 설정 부분이다. 프로젝트 생성시 자동으로 입려되는 부분이기도하다. 

     

    tasks.getByName  jar 파일의 이름을 구성할때 사용

     

    tasks.withType<Test>프로젝트 구동시 테스트 케이스에 대한 실행 여부를 설정할수 있는데 **/* 이렇게 설정시 모든 테스트 케이스를 실행하지 않겠다는 의미이다. 

    Gradle 설정이 됬으면 일단 기본적인 설정은 완료 됬다고 본다. 그다음 해야 할 스텝은 내 개인적인 생각으로는 yaml 설정과 패키지 구조를 잡는것이라고 생각한다. 일단 yml 부터 설정을 시작한다. 일반적으로는 예제들을 찾아 봤을때 application.properties 로 많이 사용하는데 이것은 자동 프로젝트 생성할때 디폴트로 잡히기 때문에 그런것 같다. 

     실제로 서비스를 개발하면서 application.properties  보다는 application.yml 을 더 많이 사용했고 실제로 yml 이 좀더 관리나 가독성 면에서 좋다. 

    application.yml 를 단독으로 쓰는것 보단 프로필을 나눠서 쓰는것이 좋다. 이유는 서비스를 배포하다보면 테스트, 릴리즈 , 프로덕션등으로 구분이 필요하게 된다. 각 환경별로 설정이 다르고 접속해야하는 데이터베이스도 달라지기 때문에 초반부터 나누어서 관리하는것이 좋다. 

    yml 은 스프링에서 기본적으로 우선순위를 가질수 있는데 jar 파일과 같은 폴더에 config 폴더를 생성하고 그곳에 별도의 yml 을 넣어줄경우 jar 안에 패키징되있는 yml 이 있더라도 우선순위에 의해서 무시 되게 된다. 그렇기 때문에 환경에 따라서 config 폴더를 생성해서 관리할지 , 아니면 실행시 java 명령 커맨드에 포함해서 사용할지 잘 생각해서 사용해야한다. 

    ## 서비스 설정
    server:
      port: ${HOST_PORT:9001}
      # kill -15 : 정상 종료
      # kill -9 : 강제 종료
      shutdown: graceful
    
    spring:
      profiles:
        active: local
      datasource:
        url: jdbc:mysql://localhost:3306/company?serverTimezone=Asia/Seoul&useSSL=false&rewriteBatchedStatements=true
        username: test
        password: test
        driver-class-name: com.mysql.cj.jdbc.Driver
      data:
        mongodb:
          uri: mongodb+srv://test:test1234@cluster0.kdbnf.mongodb.net/test?retryWrites=true&w=majority
          database: test
      jpa:
        hibernate:
          ddl-auto: none
          naming:
            physical-strategy: com.vladmihalcea.hibernate.type.util.CamelCaseToSnakeCaseNamingStrategy
        properties:
          hibernate:
            default_batch_fetch_size: 100
        database: mysql
        database-platform: com.sellermill.common.config.MySQLCustomDialect
    
      main:
        allow-bean-definition-overriding: true
      jackson:
        serialization:
          FAIL_ON_EMPTY_BEANS: false
    
    # jwt 설정 정보
    security:
      jwt:
        secret-key: saseveea3dev2011a12zs33zaedfeeaa #글자 자리수가 너무 작을경우 사용 불가
        expire-time:
          access-token: 1800   # 30M
          refresh-token: 2592000  # 30D

     간단하게 세부 설명을 하자면 port: ${HOST_PORT:9001}  부분은 서버의 포트를 정해주는 부분인데 이부분은 취향에 맞게 설정 해주면 되고 단순하게 port: 9001 로 해주어도 된다. 여기서 별도로 변수화 시킨것은 도커를 이용하여 배포하는 환경을 사용해서 그런데 이런부분들도 환경에 맞게 사용하면 될거 같다. 

    spring:
        profiles:
          active: local

    이부분이 중요한데 프로필을 정하는 부분이다 여기에 설정한 프로필 값을 넣어주고 재시작시 해당 설정값 yml 을 읽어 오게 된다. 

    datasource  : 데이터베이스를 설정하는 부분 

    allow-bean-definition-overriding :  이 옵션은  스프링 2.1 부터 bean 객체의 오버라이딩옵션이 기본 false 로 잡혀 있기 때문에 기본값을 true 로 변경 해주기 위한 부분

    그이외에는 딱히 특별한 설정이 없기 때문에 넘어 간다. 

    그 다음은 간단한 로그 설정을 해야한다. 이전글에서 Logback 을 이용한 로그 분리 글을 작성 해두었다. 

    https://ellune.tistory.com/34?category=769021

     

    Logback - 특정 이름별로 로그 분리 하기

    Logback 이 로그 파일을 쌓는 방식을 기본적으로는 패키지 명이나 로그 레벨 단위로 나누거나 하는 경우가 많다. 이번에 특이하다고 하긴 그렇지만 특정 이름별로 로그를 분리 할 이유가 생겼다.

    ellune.tistory.com

    logging:
      config: classpath:logs/logback-dev.xml

     yml 중에 application-dev.yml 을 작성하고 해당 설정을 넣어주면 개발환경에서의 로그정책을 적용 가능하다. xml 을 작성한뒤 이런식으로 프로필 yml 마다 로그를 별도로 적용할경우 환경별로 관리가 용이해진다. 

    이정도 설정이 되면 프로젝트에 실제로 API 를 만들수 있는 환경이 완성 된것이다. 이후에는 API 를 만들기위 한 기본적인 패키지 설계 및 구성을 하고 간단한 API 들을 실제로 만들어 볼것이다. 

    Next!!

    반응형
Designed by Tistory.