ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • API Latency를 줄이는 방법 (Part. 0)
    극락코딩 2023. 8. 28. 20:13
    API의 응답속도 및 지연에 민감한 서비스인 경우, 어떤 방식으로 latency를 줄일 수 있을까?

     

     

    최근에 API의 latency가 민감한 서비스를 개발하게 되었다. 사용자 경험상, 아무리 늦더라도 60~80ms안에는 API의 응답을 받아야 했다. 그렇다면, latency를 줄이기 위한 방법으로는 무엇이 있을까?

     

     

     

    일단, 몇가지 상황을 제시한다.

    1. 해당 API는 TPS 500이 넘는다고 가정한다. (그 만큼 Call이 많은 서비스)
    2. 해당 서비스는 다수의 쿼리를 조회하고, Redis 및 Client 호출이 많다. (Wrapping API라고 생각하자)
    3. 해당 서비스는 에러가 발생하더라도, 이를 방어할 수 있는 로직이 필요하다. (에러 발생보다는, 로그를 남기고 넘어가는 방향으로 진행)

     

     

    그럼, Project Setup을 진행해보자.

    New Project Setup

    초기 설정은 다음과 같이 진행한다. 원하는 네이밍으로 진행..

    개인적으로 dependency 설정을 gradle에서 하는 걸 좋아하기 때문에, 설정 없이 바로 프로젝트를 생성한다.

     

     

    Dependency는 다음과 같이 잡자. 전체 코드는 아래 깃헙 링크를 들어가면 확인 가능하다.

    object DependencyVersion {
        const val KOTLIN_LOGGING_VERSION = "3.0.0"
        const val LOGBACK_ENCODER = "7.2"
    }
    
    dependencies {
        /** spring starter */
        implementation("org.springframework.boot:spring-boot-starter-web")
        implementation("org.springframework.boot:spring-boot-starter-validation")
        implementation("org.springframework.boot:spring-boot-starter-data-jpa")
        implementation("org.springframework.boot:spring-boot-starter-data-redis")
        implementation("org.springframework.boot:spring-boot-starter-webflux")
        kapt("org.springframework.boot:spring-boot-configuration-processor")
    
        /** kotlin */
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    
        /** logger */
        implementation("io.github.microutils:kotlin-logging-jvm:${DependencyVersion.KOTLIN_LOGGING_VERSION}")
        implementation("net.logstash.logback:logstash-logback-encoder:${DependencyVersion.LOGBACK_ENCODER}")
    
        /** mysql */
        runtimeOnly("mysql:mysql-connector-java")
        
        /** etc */
        developmentOnly("org.springframework.boot:spring-boot-devtools")
    }

     

    다음으로는, Database 및 Redis Setup을 진행한다.

    (local에 mysql과 redis가 설치되었기를..)

     

    application.yml은 다음과 같이 설정한다.

    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/reduce_api_latency?useUnicode=true&charset=utf8mb4&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Seoul
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password:
        hikari:
          minimum-idle: 10
          maximum-pool-size: 20
      jpa:
        show-sql: true
        hibernate:
          ddl-auto: none
        open-in-view: false
      redis:
        host: localhost
        port: 6379

     

    DDL 쿼리는 다음과 같다. Local에서 실행하시길

    -- CREATE TABLE
    CREATE DATABASE reduce_api_latency CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
    
    CREATE TABLE `test_1`
    (
        `id`          bigint NOT NULL AUTO_INCREMENT,
        `title`       varchar(255) DEFAULT NULL,
        `description` varchar(255) DEFAULT NULL,
        `created_at`  datetime     DEFAULT CURRENT_TIMESTAMP,
        `modified_at` datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
    
    CREATE TABLE `test_2`
    (
        `id`          bigint NOT NULL AUTO_INCREMENT,
        `title`       varchar(255) DEFAULT NULL,
        `description` varchar(255) DEFAULT NULL,
        `created_at`  datetime     DEFAULT CURRENT_TIMESTAMP,
        `modified_at` datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
    
    CREATE TABLE `test_3`
    (
        `id`          bigint NOT NULL AUTO_INCREMENT,
        `title`       varchar(255) DEFAULT NULL,
        `description` varchar(255) DEFAULT NULL,
        `created_at`  datetime     DEFAULT CURRENT_TIMESTAMP,
        `modified_at` datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
    
    CREATE TABLE `test_4`
    (
        `id`          bigint NOT NULL AUTO_INCREMENT,
        `title`       varchar(255) DEFAULT NULL,
        `description` varchar(255) DEFAULT NULL,
        `created_at`  datetime     DEFAULT CURRENT_TIMESTAMP,
        `modified_at` datetime     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
        PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

     

    이제 짜잘한 설정을 진행한다.

     

    Jpa Auditing Config

    @Configuration
    @EnableJpaAuditing
    class JpaAuditingConfig

     

    Redis Config

    @EnableCaching
    @Configuration
    @EnableConfigurationProperties(RedisProperties::class)
    class RedisConfig(
        private val properties: RedisProperties
    ) : CachingConfigurerSupport() {
        @Bean
        fun redisConnectionFactory(): RedisConnectionFactory {
            return LettuceConnectionFactory(properties.host, properties.port)
        }
    
        @Bean
        fun redisTemplate(): RedisTemplate<String, Any> {
            return RedisTemplate<String, Any>().apply {
                this.setConnectionFactory(redisConnectionFactory())
                this.keySerializer = StringRedisSerializer()
                this.valueSerializer = GenericJackson2JsonRedisSerializer()
                this.hashKeySerializer = StringRedisSerializer()
                this.hashValueSerializer = GenericJackson2JsonRedisSerializer()
            }
        }
    }

     

    BaseEntity

    @MappedSuperclass
    @JsonIgnoreProperties(value = ["createdAt, modifiedAt"], allowGetters = true)
    @EntityListeners(AuditingEntityListener::class)
    abstract class BaseEntity(
        @Column(columnDefinition = "datetime default CURRENT_TIMESTAMP")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS", timezone = "Asia/Seoul")
        var createdAt: ZonedDateTime = ZonedDateTime.now(),
    
        @Column(columnDefinition = "datetime default CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS", timezone = "Asia/Seoul")
        var modifiedAt: ZonedDateTime = ZonedDateTime.now()
    ) {
        @PrePersist
        fun prePersist() {
            createdAt = ZonedDateTime.now()
            modifiedAt = ZonedDateTime.now()
        }
    
        @PreUpdate
        fun preUpdate() {
            modifiedAt = ZonedDateTime.now()
        }
    }

     

    Test Entity 1 ~ 4

    @Entity
    @Table(name = "test_1") // 요것만 바꾸기~
    class Test4(
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        val id: Long = -1L,
    
        val title: String? = null,
    
        val description: String? = null,
    ) : BaseEntity()

     

    Test Repository 1 ~ 4

    @Repository
    interface Test1Repository : JpaRepository<Test1, Long> {
    }

     

    여기까지 셋업했다면, 기본적인 구조는 다 작성한 것이다.

    다음으로는 테스트를 위한 service와 conroller를 추가한다.

     

    @RestController
    class TestController(
        private val testService: TestService
    ) {
    
    }
    
    @Service
    class TestService {
    }

     

    Mysql 및 Redis Setup이 끝났다. 그리고 기본 골격도 다 잡았다.

    추가적으로 외부 호출을 진행하기 위한 WebCliente도 추가하자. (이건 레포에서 확인하기..)

     

    다음 포스팅부터는 예제 API를 작성하고, 이를 개선하는 것을 다루기로 하자...

     

     

     

     

     

    Reference

    gitHub

     

    '극락코딩' 카테고리의 다른 글

    spring version up을 진행할때 참고하기  (0) 2023.11.06
    API Latency를 줄이는 방법 (Part. 1)  (0) 2023.08.29
    Slack Message 발송  (1) 2023.08.27
    @Transactional vs TransactionTemplate  (0) 2023.08.22
    온디맨드가 뭔디?  (1) 2023.08.20
극락코딩