반응형
Recent Posts
Recent Comments
관리 메뉴

개발잡부

[Redis] API 성능개선 ( redis 캐시 적용하기 ) - 실전 본문

JAVA/springboot

[Redis] API 성능개선 ( redis 캐시 적용하기 ) - 실전

닉의네임 2023. 12. 19. 15:57
반응형

일단 삽질 부터 정리를 하자면..

왜 삽질을 정리하냐 물어보신다면..   "결과가 좋지 않으니 시간낭비를 하지 말자" 라는 의미로

 

첨에 성능개선의 방향을 메소드 별 캐싱, 즉 동적인 결과를 반환하는 메소드 외에  검색키워드에만 영향을 받아 캐싱이 되어도 무방한 정적인 데이터를 처리하는 메소드를 캐싱 해버린다. 

 

 

이렇게 캐싱할 메소드를 정해놓고

 

 

데이터 처리 하는 로직을  component 에 이관하고 component 를 캐싱하려고 했으나..

메소드 캐싱을 할수록 시간이 증가하는 기적이 .. 20~50ms 씩 증가..

 

위의 구조라면 저것들을 다 캐싱하는 순간.. 

 

그래서 캐싱은 1번으로 끝내고 가능하다면 최전방으로 배치한다. 의 전략 

 

최초 호출인 /search 의 호출을 캐싱해버리는..

/search 호출은 상품정보, 필터정보, 부가정보 등등 여러 정보를 가지고 있다. 그래서 쿼리 한번에 ES 조회를 8번 정도 하는

 

expire time 이 5분 이니까 전체 검색로그를 기준으로 메소드 X 검색어 X storeID 로 키가 몇개까지 생성될 수 있는지 예측 해보니

 

 

특별한 일이 없으면 max 5000개

 

작업해보잣

CacheKey.java 캐시의 config 정보를 저장 

package kr.co.homeplus.totalsearch.core.config;

public class CacheKey {

    public static final int DEFAULT_EXPIRE_SEC = 60; // 1 minutes

    public static final int FILTER_EXPIRE_SEC = 300; // 5 minutes
    public static final String FILTER = "filter";

    public static final int BANNER_EXPIRE_SEC = 300; // 5 minutes
    public static final String BANNER = "banner";

    public static final int TOTAL_EXPIRE_SEC = 300; // 5 minutes
    public static final String TOTAL = "total";
}

 

RedisConfig.java

package kr.co.homeplus.totalsearch.core.config;


import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator;
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.ReadFrom;
import io.lettuce.core.RedisURI;
import io.lettuce.core.resource.ClientResources;
import kr.co.homeplus.search.api.model.Category;
import kr.co.homeplus.search.api.model.Element;
import kr.co.homeplus.search.api.model.RsvInfo;
import kr.co.homeplus.search.api.model.SellerInfo;
import kr.co.homeplus.search.api.response.BaseAdmin;
import kr.co.homeplus.totalsearch.common.search.response.AttributeElement;
import kr.co.homeplus.totalsearch.common.search.response.SearchInfo;
import kr.co.homeplus.totalsearch.common.search.response.SearchResponse;
import kr.co.homeplus.totalsearch.core.constants.CacheConstants;
import kr.co.homeplus.totalsearch.core.properties.ElasticCacheProperties;
import kr.co.homeplus.totalsearch.total.response.v1.HomeTotalItem_v1_0;
import kr.co.homeplus.totalsearch.total.response.v1.TotalResponse_v1_0;
import kr.co.homeplus.totalsearch.total.response.v1.UserOrderProductInfo_v1_0;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.cache.CacheKeyPrefix;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.session.data.redis.config.ConfigureRedisAction;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;


@RequiredArgsConstructor
@EnableCaching
@Configuration
public class RedisConfig {

    private final String LOCAL = "local";
    private final Environment env;
    private final RedisProperties redisProperties;
    private final ElasticCacheProperties elasticCacheProperties;

    @Bean
    public ConfigureRedisAction configureRedisAction() {
        return ConfigureRedisAction.NO_OP;
    }

    @Bean
    @Primary
    public RedisConnectionFactory itemListConnectionFactory() {
        return redisConnectionFactory(CacheConstants.ITEMLIST);
    }

    //redis master/slave connection config
    public RedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration(String clusterName) {
        //master
        RedisStaticMasterReplicaConfiguration redisStaticMasterReplicaConfiguration = new RedisStaticMasterReplicaConfiguration(elasticCacheProperties.getMaster(clusterName).getHost(), elasticCacheProperties.getMaster(clusterName).getPort());

        //slave
        elasticCacheProperties.getSlave(clusterName).forEach(slave -> redisStaticMasterReplicaConfiguration.addNode(slave.getHost(), slave.getPort()));

        //password
        redisStaticMasterReplicaConfiguration.setPassword(redisProperties.getPassword());

        return redisStaticMasterReplicaConfiguration;
    }

    public RedisConnectionFactory redisConnectionFactory(String clusterName) {
        return new LettuceConnectionFactory(redisStaticMasterReplicaConfiguration(clusterName), lettuceClientConfiguration());
    }

    public LettuceClientConfiguration lettuceClientConfiguration() {
        LettuceClientConfiguration clientConfiguration = new LettuceClientConfiguration() {
            //spring profiles 에 맞게 useSSL 설정
            @Override
            public boolean isUseSsl() {
                return Arrays.stream(env.getActiveProfiles()).noneMatch(e -> e.equalsIgnoreCase(LOCAL));
            }

            @Override
            public boolean isVerifyPeer() {
                return true;
            }

            @Override
            public boolean isStartTls() {
                return false;
            }

            @Override
            public Optional<ClientResources> getClientResources() {
                return Optional.empty();
            }

            @Override
            public Optional<ClientOptions> getClientOptions() {
                return Optional.empty();
            }

            @Override
            public Optional<String> getClientName() {
                return Optional.empty();
            }

            @Override
            public Optional<ReadFrom> getReadFrom() {
                //return Optional.empty();
                return Optional.of(ReadFrom.SLAVE_PREFERRED);
            }

            @Override
            public Duration getCommandTimeout() {
                return Duration.ofSeconds(RedisURI.DEFAULT_TIMEOUT);
            }

            @Override
            public Duration getShutdownTimeout() {
                return Duration.ofMillis(100);
            }
        };

        return clientConfiguration;
    }


    @Bean(name = "cacheManager")
    public RedisCacheManager cacheManager() {


        PolymorphicTypeValidator typeValidator = BasicPolymorphicTypeValidator
                .builder()
                .allowIfSubType(Object.class)
//                .allowIfSubType(AttributeElement.class)
//                .allowIfSubType(Element.class)
                .build();

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper
                .activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);


        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues()
                .entryTtl(Duration.ofSeconds(CacheKey.DEFAULT_EXPIRE_SEC))
                .computePrefixWith(CacheKeyPrefix.simple())
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper())))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));

        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();


        cacheConfigurations.put(CacheKey.TOTAL, RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues()
                .entryTtl(Duration.ofSeconds(CacheKey.TOTAL_EXPIRE_SEC))
                .computePrefixWith(CacheKeyPrefix.simple())
//                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper())))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(itemListConnectionFactory()).cacheDefaults(configuration)
                .withInitialCacheConfigurations(cacheConfigurations).build();
    }


    private ObjectMapper objectMapper() {
        PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator
                .builder()
                .allowIfBaseType(Element.class)
                .allowIfSubType(Object.class)
                .allowIfSubType(ArrayList.class)
                .allowIfSubType(Element.class)
                .allowIfSubType(TotalResponse_v1_0.class)
                .allowIfSubType(AttributeElement.class)
//                .allowIfSubType(SearchResponse.class)
                .allowIfSubType(BaseAdmin.class)
                .allowIfSubType(Long.class)
                .allowIfSubType(RsvInfo.class)
                .allowIfSubType(SearchInfo.class)
                .allowIfSubType(UserOrderProductInfo_v1_0.class)
                .allowIfSubType(Category.class)
                .allowIfSubType(SellerInfo.class)
                .allowIfSubType(HomeTotalItem_v1_0.class)
                .build();
        ObjectMapper mapper = new ObjectMapper();

        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE);
        mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);

//        mapper.registerSubtypes(Element.class);
//        mapper.registerSubtypes(AttributeElement.class);
//        mapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
//        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
//        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);




        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
        mapper.registerModule(new JavaTimeModule());
//        mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);
        mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.EVERYTHING);
        return mapper;
    }
}

 

첫번째 삽질은

Serialize 가 되지 않는 직렬화 에러 

implements Serializable

 

이런식으로 직렬화를 시켰더니 나중엔 공통 프레임워크나 high level의 Response 메소드에서는 인터페이스를 붙일수가 없어서 

삽질.. 

1.  ES 의 request 캐싱 query 캐싱 등으로 풀어보려고 하다가 포기.

2. API response 객체만 캐싱 하려고 했으나 자바에서 로직처리가 캐시가져오는 속도보다 2-3배 빠름.. 포기

3. 다시 메소드 캐싱으로 돌아옴 

 

해결책은 드릅게 간단함

.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper())))

 

RedisCacheManager 에서 마이클젝슨으로 직열화 하겠다고 하면 되는거였음..

 

그래서 끝날줄 알았으나.. 

 

이런 미친 감자 

여기서 재대로 삽질..

 

Missing type id when trying to resolve subtype of ... 

AttributeElement가 Element 의 서브타입이 아니라는 젝슨의 에러

 

테스트 했던 키워드에서는 속성값이 없어서 문제가 없었는데 감자로 테스트 해보니 속성값이 있었고  그 속성값을 역직렬화 하는 과정에서 subtype 에러가 

 

package kr.co.homeplus.search.api.model;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import io.swagger.annotations.ApiModelProperty;

import java.io.Serializable;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
import org.elasticsearch.action.search.SearchResponse;

@Getter
@Setter
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "type",
        visible = true
)
@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class)
@JsonSubTypes({
        @JsonSubTypes.Type(value = AttributeElement.class, name = "AttributeElement")
})
@JsonTypeName("Element")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Element {

    @ApiModelProperty(value = "순서")
    private Integer sequence;
    @ApiModelProperty(value = "코드값")
    private String code;
    @ApiModelProperty(value = "노출명")
    private String name;
    @ApiModelProperty(value = "설명")
    private String explanation;
    @ApiModelProperty(value = "하위 속성")
    private List<Element> sub;
}

 

어노테이션은 따로 정리하고 일단 에러의 내용대로 AttributeElement 를 Element 의 subType 으로 지정해 주었다. 

그랬더니  subType 관련 에러는 사라졌는데 POJO의 에러..

 

@JsonTypeInfo 어노테이션이 붙어버리니 POJO 가 아니라서 type 을 생성할 수 없다.

 

대충 아래와 같은 애러

missing type id property 'type' for pojo property

 

POJO 와 subType 사이에서 삽질을 .. 

 

AttributeElement.java 에다가도 이것 저것 똥칠을 해봤으나 실패

package kr.co.homeplus.search.api.model;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import io.swagger.annotations.ApiModelProperty;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.EXISTING_PROPERTY,
        property = "subType",
        visible = true
)
@JsonSubTypes({
//        @JsonSubTypes.Type(value = ArrayList.class, name = "ArrayList"),
        @JsonSubTypes.Type(value = Element.class, name = "Element")
})
@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class)
@JsonTypeName("AttributeElement")
@JsonInclude(Include.NON_NULL)
public class AttributeElement {

    @ApiModelProperty("순서")
    private Integer sequence;
    @ApiModelProperty("유형")
    private String type;
    @ApiModelProperty("코드값")
    private String code;
    @ApiModelProperty("노출명")
    private String name;
    @ApiModelProperty("이미지 URL")
    private String imgUrl;
    @ApiModelProperty("이미지 넓이")
    private String imgWidth;
    @ApiModelProperty("이미지 높이")
    private String imgHeight;
    @ApiModelProperty("색상 유형")
    private String colorType;
    @ApiModelProperty("색상 코드")
    private String colorCode;
    @ApiModelProperty("설명")
    private String explanation;
    @ApiModelProperty("하위 속성")
    private List<AttributeElement> sub;

    @ApiModelProperty("우선순위")
    private Integer priority;


    @ApiModelProperty("상단필터 노출여부")
    private String topFilter;
    @ApiModelProperty("정렬타입")
    private String attrSortType;
    @ApiModelProperty("속성타입")
    private String attrDispType;
}

 

하다 하다 여기에도 동을 막 칠해 보았으나  다 실패 하고 

package kr.co.homeplus.totalsearch.total.response.v1;

import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

import java.util.ArrayList;
import java.util.List;
import kr.co.homeplus.search.api.model.AttributeElement;
import kr.co.homeplus.search.api.model.Category;
import kr.co.homeplus.search.api.model.Element;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ApiModel("검색 필터 + 본문 정보")
//@JsonTypeInfo(
//        use = JsonTypeInfo.Id.CLASS,
//        include = JsonTypeInfo.As.WRAPPER_ARRAY,
//        property = "type",
//        visible = true,
//        defaultImpl = Element.class
//
//)
//@JsonSubTypes({
//        @JsonSubTypes.Type(value = AttributeElement.class, name = "AttributeElement"),
//        @JsonSubTypes.Type(value = Element.class, name = "Element"),
//        @JsonSubTypes.Type(value = Category.class, name = "Category"),
//        @JsonSubTypes.Type(value = ArrayList.class, name = "ArrayList")
//})
//@JsonIdentityInfo(generator = ObjectIdGenerators.StringIdGenerator.class)
//@JsonTypeName("TotalResponse_v1_0")
@JsonInclude(Include.NON_NULL)
public class TotalResponse_v1_0<I> extends TotalItemResponse_v1_0<I> {

    @ApiModelProperty(value = "필터 정보 - 카테고리")
    private List<Category> categoryList;

    @ApiModelProperty(value = "필터 정보 - 하위 카테고리")
    private List<Category> categorySubList;

    @ApiModelProperty(value = "필터 정보 - 브랜드")
    private List<Element> brandList;

    @ApiModelProperty(value = "필터 정보 - 파트너")
    private List<Element> partnerList;

    @ApiModelProperty(value = "필터 정보 - 상품속성")
    private List<AttributeElement> attributeList;

    @ApiModelProperty(value = "필터 정보 - 가격")
    private List<Element> priceRangeList;

    @ApiModelProperty(value = "필터 정보 - Mall Type")
    private List<Element> mallTypeList;

    @ApiModelProperty(value = "필터 정보 - 도수")
    private List<Element> alcoholRangeList;

    @ApiModelProperty(value = "uda 임시 리스트")
    @JsonIgnore
    private List<AttributeElement> udaList;

    @ApiModelProperty(value = "리스트 뷰타입(1단형/2단형)")
    private String displayType;

}

 

여기저기 물어보던 중 우리 회사 최고 엘리트가 역시  해결해 줌 

@ApiModelProperty(value = "필터 정보 - 상품속성")
private List<Element> attributeList;

 

@ApiModelProperty(value = "필터 정보 - 상품속성")
private List<AttributeElement> attributeList;

 

위를 아래로 바꿨는데 솔찌기 아직도 .. 이해가 안간다.. 

 

그래도 필터정보의 속성이 바꼈으니 데이터 검증을 진행해보았다. 

 

 

반응형

'JAVA > springboot' 카테고리의 다른 글

[java] API method cache - 적용  (0) 2023.10.31
[java] API method cache  (1) 2023.10.29
[java] API - 검색 api 성능 개선 final  (2) 2023.10.15
[java] API - cache method  (0) 2023.10.15
[java] API - redis cache  (2) 2023.10.09
Comments