Spring

[AWS + Spring] EB + ElastiCache Redis + Spring Boot 연동 (2)

BigBeen 2023. 1. 15. 22:03
반응형

이전 포스팅에선 Elasticache Redis Cluster를 생성하고 bastion host를 통해 EC2에서 연결까지 확인 해봤다.

이번 포스팅에선 Spring Boot에 연동 후 Beanstalk에 배포한 뒤 API 테스트를 진행 해볼 것이다.

 

본 포스팅은 Elastic Beanstalk + Aurora RDS 환경에서 테스트를 진행한다.

* 해당 Beanstalk 구축 및 RDS 구축 예제는 해당 링크 참고 *

 

 

Spring Boot 에선 Spring Data Redis를 통해서 Lettuce, Jedis라는 두 가지 오픈소스 Java 라이브러리를 사용할 수 있다.

이 2가지로 Redis에 접근할 수 있는데, Lettuce는 별도의 설정이 필요하지 않고, Jedis는 별도의 의존성이 필요하다.

 

또한 원래 Jedis 를 많이 사용했으나 여러 가지 단점(멀티 쓰레드 불안정, Pool 한계 등)과

Lettuce의 장점(Netty 기반 비동기 지원 가능) 때문에 Lettuce 로 추세가 넘어가고 있었다.

결국 Spring Boot 2.0 부터 Jedis가 기본 클라이언트에서 deprecated 되고 Lettuce가 탑재 되었다.

 

그리고 Spring Data Redis는 Redis를 사용하기 위한 2가지 접근 방식

RedisTemplate와 Redis Repository를 제공한다.

 

이 포스팅에선 Lettuce와 RedisTemplate를 사용할 것이다.

 

 

💡 Spring-Data-Redis

 - 여러가지 Redis 드라이버(Jedis, Lettuce 등등)를 추상화하여 사용

 - RedisTemplate를 이용하여 Redis 작업, 직렬화 작업 등의 여러 작업을 지원

 

💡 RedisTemplate

- RedisTemplate는 redis 서버에 redis 커맨드를 수행하기 위한 high-level-abstractions를 제공한다.

- RedisTemplate가 제공하는 Serialize, Deserialize로 String을 사용하는 StringRedisTemplate를 통해

   redis 서버에 CRUD를 할 수 있는 operation interface를 제공한다.

 

💡 RedisRepository

- domain Objects를 Redis Hash 자료구조로 변환, secondary indexes, TTL 등을 적용할 수 있다.


📌 시작하기

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

spring-data-redis dependency 추가

 

 

Endpoint 확인

Aws Elasticache > Redis Cluster > 엔드포인트 주소 확인

 

 

project-path/src/main/resource/application-develop.yml

server:
  port: 5000

spring:
  datasource:
    replication:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: 마스터계정
      password: 마스터계정 패스워드
      write:
        name: write
        url: jdbc:mysql://리전 클러스터(라이터 인스턴스) 엔드포인트:3306/test
      reads:
        - name: read1
          url: jdbc:mysql://리전 클러스터(리더 인스턴스) 엔드포인트:3306/test
  redis:
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 2
    port: 6379
    host: [Elasticache Redis Cluster Endpoint]
    password: password
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher

logging:
  level:
    root: info
    org:
      hibernate:
        SQL: debug
        type:
          descriptor:
            sql:
              BasicBinder: trace
      springframework:
        jdbc:
          core:
            JdbcTemplate: debug
            StatementCreatorUtils: trace

이 포스팅은 Aurora RDS를 기반으로 진행한다. Redis 설정 부분만 참고 하시면 된다.

host 주소에는 위에서 확인한 레디스 클러스터의 엔드포인트 주소를 적으면 된다.

 


Redis Configuration

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String redisPort;

    @Value("${spring.redis.password}")
    private String redisPassword;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisHost);
        redisStandaloneConfiguration.setPort(Integer.parseInt(redisPort));
        redisStandaloneConfiguration.setPassword(redisPassword);
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

@Value Annotation으로 application-develop.yml에 적어둔 host, port, password 값을 얻어온다.

Redis Repository가 아닌 RedisTemplate 사용할 것이기 때문에 StringRedisSerializer를 사용한다.

 

 

💡 RedisConnectionFactory

 - RedisConnectionFactory는 새로운 Connection이나 이미 존재하는 Connection을 리턴한다.

 - 예외 변환도 가능하며 예외 발생 시 SpringDataAccessException으로 전환한다.

 - Jedis, Lettuce 등등 Redis-client를 선택해서 연결을 생성할 수 있다.


JsonComponent 생성

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;

@Slf4j
@Component
public class JsonComponent {

    private ObjectMapper objectMapper;

    @PostConstruct
    public void init() {
        objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
    }

    public String objectToJson(Object obj) {
        String jsonStr = "";
        try {
            jsonStr = objectMapper.writeValueAsString(obj);
        } catch (Exception e) {
            log.error(ExceptionUtils.getStackTrace(e));
            new RuntimeException("ParsingException", e);
        }
        return jsonStr;
    }

    public <T> T jsonToObject(String json, Class<T> clazz) {
        T obj = null;
        try {
            obj = objectMapper.readValue(json, clazz);
        } catch (Exception e) {
            log.error(ExceptionUtils.getStackTrace(e));
            new RuntimeException("ConvertException", e);
        }
        return obj;
    }
}

 

 

Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling...

 

객체의 항목 중 LocalDateTime이 있는 항목의 값을 ObjectMapper를 써서 가져올 경우 위와 같은 에러가 발생한다.

Java 8에서 추가된 LocalDateTime 항목을 제대로 직렬화 또는 역직렬화를 못하기 때문이다.

 

위 컴포넌트는 jsr310 이슈를 해결 하기 위해 ObjectMapper에 JavaTimeModule을 추가하여 재정의 한 Component다.

objectToJson: object를 json으로 변환 시켜주는 메서드 (for set data)

jsonToObject: json을 T Class로 변환 시켜주는 메서드 (for get Data)


RedisComponent 생성

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

@Slf4j
@RequiredArgsConstructor
@Component
public class RedisComponent {

    final RedisTemplate<String, String> redisTemplate;
    final JsonComponent jsonComponent;

    public <T> T getObjectByKey(String key, Class<T> clazz) {
        ValueOperations<String, String> vop = redisTemplate.opsForValue();
        String jsonString = vop.get(key);
        if (StringUtils.isNotEmpty(jsonString)) {
            return jsonComponent.jsonStringToObject(jsonString, clazz);
        } else {
            return null;
        }
    }

    public void setObjectByKey(String key, Object obj) {
        ValueOperations<String, String> vop = redisTemplate.opsForValue();
        vop.set(key, jsonComponent.objectToJsonString(obj));
        redisTemplate.expire(key, 7, TimeUnit.DAYS);
    }
}

Data를 쓰거나 얻을 수 있는 메서드를 재사용 하기 위해 만든 Redis Component다.

위에서 만든 JsonComponent로 Data Value를 컨트롤하고, RedisTemplate를 이용해

Redis에 데이터를 저장하거나 조회 할 수 있다.

 

* RedisTemplate에 expire() 메서드를 이용해 Expired Date를 설정할 수 있다.


TestController 생성

@RestController
@RequestMapping("/api/test")
@RequiredArgsConstructor
public class TestController {
	
    private final TestService service;
    
    @GetMapping
    public String get() {
    	return service.get();
    }
    @PostMapping
    public String create() {
        service.create();
        return "ok";
    }
}

테스트를 위한 간단한 Controller를 생성한다.

 

 

TestService 생성

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class TestService {

    private final RedisComponent redisComponent;

    public String get() {
        return redisComponent.getObjectByKey("test", String.class);
	}
    
    @Transactional
    public void create() {
        redisComponent.setObjectByKey("test", "Success Connection Test");    
    }
}

단순 String을 생성하고 조회하는 Service를 생성한다.

 

RedisComponent는 JPA를 이용한 Entity도 다룰 수 있다.

본 포스팅은 테스트 목적이기 때문에 단순 String으로 테스트를 진행했지만, 꽤 유용하게 쓸 수 있는 컴포넌트다.


자, 이제 모든 설정과 구현이 끝났다. Elastic Beanstalk에 배포 하기 위해 Jar 패키징을 해보자.

 

Maven-Plugin으로 패키징해서 jar 파일을 생성한다.

Build가 성공적으로 끝나면 target/classes/{패키지명} 안에 projectName-0.0.1.jar 파일이 생성 된다.

다시 Aws Console로 이동해보자.

 

Elastic Beanstalk > 환경 > Test-env 페이지에서 '업로드 및 배포'를 클릭한다.

 

 

 

'파일 선택'을 클릭해서 생성한 프로젝트의 Jar 파일을 업로드하고 배포한다.

(Spring Active Profile은 develop인 상태)

 

 

배포 후 재기동이 완료 됐으면 API 요청을 보내보자.

 

 

 

 

POST 요청으로 Data를 생성하고

GET 요청으로 Redis에서 Data를 성공적으로 조회 하는 것을 확인할 수 있다.

 

반응형