일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Analyzer
- TSLA
- Elastic
- mysql
- Query
- Aggregation
- Docker
- NORI
- aggs
- Cache
- KNN
- dbeaver
- 테슬라
- java
- elasticsearch cache
- API
- java crawler
- file download
- IONQ
- aqqle
- api cache
- redis
- ann
- Elasticsearch
- 양자컴퓨터
- 아이온큐
- request cache
- vavr
- JPA
- Selenium
- Today
- Total
아빠는 개발자
[java] API 성능개선 본문
일단 삽질 부터 정리를 하자면..
왜 삽질을 정리하냐 물어보신다면.. "결과가 좋지 않으니 시간낭비를 하지 말자" 라는 의미로
첨에 성능개선의 방향을 메소드 별 캐싱, 즉 동적인 결과를 반환하는 메소드 외에 검색키워드에만 영향을 받아 캐싱이 되어도 무방한 정적인 데이터를 처리하는 메소드를 캐싱 해버린다.
이렇게 캐싱할 메소드를 정해놓고
데이터 처리 하는 로직을 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 > API' 카테고리의 다른 글
[java] API method cache (1) | 2023.10.28 |
---|---|
[java] API Controller에서 데이터를 받아오는 방법 (1) | 2023.10.28 |
[java] API - geo distance (0) | 2023.10.21 |
[java] API - 검색 api 성능 개선 final (0) | 2023.10.14 |
[java] API - redis cache for method (0) | 2023.10.13 |