Programming/기타

MSA API 호출 성능개선 방안 - Redis 활용

armyost 2023. 9. 18. 12:56
728x90

기존의 Spring에서도 공통코드, 메뉴코드와 같이 데이터 변경이 잘없는 호출은 SpringCache를 활용하였다. 이 컨셉을 계승하여 MSA에 적용하는 사례를 소개코자 한다. 그리고 Redis Cache로 외부 Cache 저장소를 사용할 것이다.

 

Github 소스코드

https://github.com/armyost/ApiRedisTemplate

 

GitHub - armyost/ApiRedisTemplate: This is api call template with redis cache

This is api call template with redis cache. Contribute to armyost/ApiRedisTemplate development by creating an account on GitHub.

github.com

 

Application.yml

spring:
  cache: 
    type: redis
  redis:
    host: redis01.armyost.com
    port: 6379

 

RestApiTemplate.java (코드에는 DemoService 라고 되어있음)

 

아래는 Rest API 호출시 사용할 Template을 공통기능으로 만든것이다. 업무에서 규격에 맞춰 사용하면 된다.

 

아래 Cacheable에서 cacheManager가 contentCacheManager로 되어 있다. Redis config시 관련 CacheManager를 등록해주자. 

@Service
public class DemoService {
    private static final Logger logger = LoggerFactory.getLogger(DemoService.class);

    // You can set URL for cache.
    @Cacheable(value = "myAwesomeCache", cacheManager = "contentCacheManager", condition = "#url.startsWith('/realApplication/')", key = "#url + '::' + #param")
    public Map getRestData(String host, String url, Map param, HttpMethod method) {
        return getRestDataWithSession(host, url, param, method, null);
    }

    public Map getRestDataWithSession(String host, String url, Map param, HttpMethod method, String sessionId) {
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        if (sessionId != null) {
            headers.add(headers.COOKIE, "JSESSIONID=" + sessionId);
        }

        headers.setAccept(Arrays.asList(new MediaType[] { MediaType.APPLICATION_JSON }));

        String fullUri = host + url;
        String jsonParam = null;

        if (method.equals(HttpMethod.GET) || method.equals(HttpMethod.DELETE)) {
            fullUri += "?" + ofFromDataStr(param, true);
        } else {
            headers.setContentType(MediaType.APPLICATION_JSON);
            try {
                jsonParam = mapToJsonString(param);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

        HttpEntity request = new HttpEntity(jsonParam, headers);
        ResponseEntity<String> response = restTemplate.exchange(fullUri, method, request, String.class);

        Map resultMap = null;

        try {
            resultMap = jsonToMap(response.getBody());
        } catch (IOException e) {
            logger.error("!!! getRestData {} !!!", e.getMessage());
        }
        return resultMap;
    }

    private static String ofFromDataStr(Map<Object, Object> data, boolean needEncode) {
        if (data == null) {
            return "";
        }
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<Object, Object> entry : data.entrySet()) {
            if (builder.length() > 0) {
                builder.append("&");
            }
            builder.append(URLEncoder.encode(entry.getKey().toString(), StandardCharsets.UTF_8));
            builder.append("=");

            if (needEncode) {
                builder.append(URLEncoder.encode(entry.getValue().toString(), StandardCharsets.UTF_8));
            } else {
                builder.append(entry.getValue().toString());
            }
        }
        return builder.toString();
    }

    private static TreeMap jsonToMap(String json) throws JsonParseException, JsonMappingException, IOException {
        ObjectMapper mapper = new ObjectMapper();
        TreeMap<String, ArrayList<TreeMap>> map = mapper.readValue(json, TreeMap.class);
        return map;
    }

    private static String mapToJsonString(Map param) throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        return mapper.writeValueAsString(param);
    }

    private static Map listToMap(List<TreeMap> list, String key, String value) {
        Map retMap = new TreeMap();
        for (Map map : list) {
            String kValue = (String) map.get(key);
            String vValue = (String) map.get(value);
            retMap.put(kValue, vValue);
        }
        return retMap;
    }
}

 

 

RedisConfig.java

@EnableCaching
@Configuration
public class RedisConfig {

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

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

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @SuppressWarnings("deprecation")
    @Bean
    public CacheManager contentCacheManager() {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory());
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer())) // Value Serializer 변경
                .prefixKeysWith("Sample:") // Key Prefix로 "Sample:"를 앞에 붙여 저장
                .entryTtl(Duration.ofMinutes(30)); // 캐시 수명 30분
        builder.cacheDefaults(configuration);
        return builder.build();
    }
}

 

 

※ 실제로는 Cache 되는 시간도 다양한 옵션을 제공할 수 있도록, 여러 메서드를 시간별로 등록하여 사용하는 것이 더 좋을듯 하다.