Notice
Recent Posts
Recent Comments
Link
«   2026/08   »
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
Tags
more
Archives
Today
Total
관리 메뉴

나의 개발 일상 기록

대용량 트래픽 환경에서 서버 부하를 줄이기 위한 캐싱 적용 본문

ETC

대용량 트래픽 환경에서 서버 부하를 줄이기 위한 캐싱 적용

느린 거북이 2021. 7. 5. 18:05

Caching 적용

 

1. Cache를 적용할 대상

  • 반복적으로 동일한 결과를 나오는 기능의 반환값
  • 서버 자원을 많이 사용하는 작업 또는 시간이 오래 걸리는 작업 (API 호출, 데이터베이스 조회 쿼리 등)
  • 자주 조회되는 데이터
  • 입력값과 출력값이 일정한 데이터

캐싱된 데이터는 데이터 갱신으로 인해 DB와 불일치가 발생할 수 있습니다. 그렇기 때문에 데이터 Update가 잦게 일어나거나 데이터 불일치시 비즈니스 로직 상 문제가 발생할 수 있는 기능은 캐싱 대상으로 적합하지 않습니다.

 

2. Local Cache vs Global Cache

 

  • Local Cache

Local Cache란 캐시 전략 중에 하나이며 서버에 저장하는게 아닌 클라이언트 로컬 영역에 캐시 데이터를 저장하는 것을 의미합니다. Local Cache를 이용하게 된다면 서버로 데이터 통신을 위한 대기시간이 없기 때문에 응답속도가 빠릅니다.또한 데이터의 쓰기가 한번씩만 발생하기 때문에 네트워크 트래픽 또한 줄일수 있다는 장점이 있습니다. 그러나 각각의 was마다 캐시를 저장하기 때문에 하나의 장비에만 저장이 됩니다. 그리고 서버에 저장하는 것이 아니기 때문에 다른 서버에서 참조하기가 어렵습니다.

  • Global Cache

Global Cache란 Local Cache와 다르게 서버에 캐시 데이터를 저장합니다. 따라서 여러 대의 서버를 이용할 때 서버 간의 데이터 동기화가 용이하며 트래픽의 분산 또한 가능합니다. 그러나 서버와 네트워크 통신이 이루어지기 때문에 Local Cache보다 속도가 느리다는 단점이 존재합니다.

3. Scale-Up vs Scale-Out Caching 전략

  • Scale-Up 환경에서는 서버가 1개이기에 Local Cache로 데이터 처리가 가능합니다. 캐시 된 데이터가 서버 사이에 일관되지 않아도 되기때문입니다. 즉, 서버 간 캐싱 데이터를 참조할 필요가 없습니다.
  • Scale-Out 환경에서는 여러 서버에서 캐시 서버를 참조해야 합니다. 서버 간 데이터를 공유해야 하는 환경입니다. 즉, 캐싱된 데이터가 일관되려면 서버 간 캐싱 데이터를 참조해야 합니다.

 

SPRING BOOT 환경 Caching 적용

 

1. @EnableCaching 적용

 

Spring boot를 실행하는 상위 클래스에 @EnableCaching Annotation을 적용해야합니다. 해당 Annotation을 적용하면 Spring에서 Cache에 관련한 어노테이션을 스캔하여 Caching 적용을 진행합니다.

@EnableCaching // Spring에서 Caching을 사용하겠다고 선언
public class MarketServerApplication {

	public static void main(String[] args) {
		SpringApplication.run(MarketServerApplication.class, args);
	}
}

 

2. SpringCacheManager에 RedisCacheManager 주입 (RedisConfig.java)

 

RedisCacheManager는 SpringFramework의 CacheManager를 구현합니다. Spring에서는 RedisCacheManager를 Bean으로 등록하면 기본 CacheManager를 RedisCacheManager로 사용합니다.

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

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

    @Value("${expire.default}")
    private long defaultExpireSecond;
    
    /**
     * Redis Cache를 사용하기 위한 cache manager 등록.
     * 커스텀 설정을 적용하기 위해 RedisCacheConfiguration을 먼저 생성한다.
     * 이후 RadisCacheManager를 생성할 때 cacheDefaults의 인자로 configuration을 주면 해당 설정이 적용된다.
     * RedisCacheConfiguration 설정
     * disableCachingNullValues - null값이 캐싱될 수 없도록 설정한다. null값 캐싱이 시도될 경우 에러를 발생시킨다.
     * entryTtl - 캐시의 TTL(Time To Live)를 설정한다. Duraction class로 설정할 수 있다.
     * serializeKeysWith - 캐시 Key를 직렬화-역직렬화 하는데 사용하는 Pair를 지정한다.
     * serializeValuesWith - 캐시 Value를 직렬화-역직렬화 하는데 사용하는 Pair를 지정한다.
     * Value는 다양한 자료구조가 올 수 있기 때문에 GenericJackson2JsonRedisSerializer를 사용한다.
     *
     * @param redisConnectionFactory Redis와의 연결을 담당한다.
     * @return
     */
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory,
                                               ObjectMapper objectMapper) {
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig()
                .disableCachingNullValues()
                .entryTtl(Duration.ofSeconds(defaultExpireSecond))
                .serializeKeysWith(RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(new StringRedisSerializer())).serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer(objectMapper)));

        return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory)
                .cacheDefaults(configuration).build();
    }
}

 

3. Redis를 Cache 저장소로 사용하기 위한 설정(application.properties)

# redis
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
spring.data.redis.repositories.enabled=false

# expire
expire.default=36288000
expire.orderLog=3600

 

4. ResdisTemplate select, Insert, Delete

  • RedisTemplate에 Key와 value를 저장 및 조회, 삭제를 합니다.
@Repository
public class OrderLogDao {

	@Autowired
	private RedisTemplate<String, Object> redisTemplate;
	
	@Autowired
	private ObjectMapper objectMapper;
	
	@Value("${expire.orderLog}")
	private long orderLogExpireSecond;
	
	
	/**
	 * redis list에 변경된 주문상태를 추가한다.
	 * RedisKeyFactory로 주문코드, 내부 키를 이용해 키를 생산산 후 주문정보이력을 저장시킨다.
	 * 
	 * @param orderDTO
	 * @param orderCd
	 */
	public void addOrder(OrderDTO orderDTO) {
		final String key = RedisKeyFactory.ORDER_KEY;
		
		redisTemplate.watch(key);
		
		try {
			redisTemplate.multi();
			redisTemplate.opsForList().rightPush(key, orderDTO);
			redisTemplate.expire(key, orderLogExpireSecond, TimeUnit.SECONDS);
			
			redisTemplate.exec();
			
		}catch (Exception e) {
			redisTemplate.discard(); // 트랜잭션 종료시 unwatch()가 호출된다
		    System.out.println(e.getMessage());
		    throw e;
		}
	}
	
	/**
	 * 주문이력 정보를 모두 조회한다.
	 * 
	 * @param key
	 * @return
	 */
	public List<OrderDTO> findAll(String key) {
		List<OrderDTO> orderList = redisTemplate.opsForList()
			.range(key, 0, -1)
			.stream()
			.map(item -> objectMapper.convertValue(item, OrderDTO.class))
			.collect(Collectors.toList());
				
	    return orderList;
    }
	
	/**
	 * 주문이력을 전부 삭제한다. rddis에서 해당 키에있는 내용을 모두 삭제한다.
	 * 
	 * @param key
	 * @return
	 */
	public boolean deleteByOrder(String key) {
		return redisTemplate.delete(key);
	}
}

 

5. 주문이력 저장

  • OrderServiceImpl에서 주문상태코드 변경 시 OrderDTO정보를 RedisTemplate에 Key와 value를 저장합니다.
/**
 * 주문상품의 주문상태코드를 변경한다.
 */
@Override
@Transactional(rollbackFor = RuntimeException.class)
public void updateOrderStatus(OrderDTO orderDTO) {
	
	String orderCd = orderDTO.getOrderCd();
	String orderStatusCd = orderDTO.getOrderStatusCd();
		
	int result = orderMapper.updateOrderStatus( orderCd, orderStatusCd);
		
	if(result != 1) {
		log.error("Update ERROR! {}", orderCd);
		throw new RuntimeException("Update ERROR! 주문번호를 확인해주세요.\n" + "orderCd : " + orderCd);
	}else {
		//현재시간 계산
		SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		Date time = new Date();
		
		orderLogDao.addOrder(new OrderDTO(orderCd, orderStatusCd, format.format(time)));
	}
	
	// 배송완료 시 주문수량 count 저장, OSC007 - 배송완료
	if("OSC007".equals(orderStatusCd)) {
		int orderCnt = productDao.getProductCntInfo(RedisKeyFactory.ORDER_CNT_KEY, orderDTO.getItemCd());
		productDao.addProductCntInfo(RedisKeyFactory.ORDER_CNT_KEY, orderDTO.getItemCd(), orderCnt + orderDTO.getOrderCnt());
	}
	
	// 환불완료 시 주문수량 count 변경, OSC009 - 환불완료
	if("OSC009".equals(orderStatusCd)) {
		int orderCnt = productDao.getProductCntInfo(RedisKeyFactory.ORDER_CNT_KEY, orderDTO.getItemCd());
		productDao.addProductCntInfo(RedisKeyFactory.ORDER_CNT_KEY, orderDTO.getItemCd(), orderCnt - orderDTO.getOrderCnt());
	}
	
	//사용자 아이디 get
	String userId = userService.findById(orderDTO.getLoginNo());
	
	//푸시메시지 전송
	sendPushMessage(userId, orderStatusCd);
}

 

 

6. Spring batch

  • 배치스케줄러를 통해 1시간마다 RedisTemplate에 저장된 주문상태이력을 조회 후 리스트에 저장합니다.
  • 리스트의 사이즈가 0이상일 경우 RedisTemplate에 저장된 이력정보를 삭제합니다.
  • 조회 한 정보를 DB에 저장합니다.
public class OrderLogServiceImpl{
	
	@Autowired
	private OrderMapper orderMapper;
	
	@Autowired
	private OrderLogDao orderLogDao;
	
	/**
	 * 일정시간마다 주문상태를 log 테이블에 insert 및 주문정보이력 삭제
	 */
	@Scheduled(cron = "0 0 0/1 * * *") // 1시간마다 실행
	public void InsertLogBySchedule() {
		List<OrderDTO> orderList = orderLogDao.findAll(RedisKeyFactory.ORDER_KEY);
		if(orderList.size() > 0) {
			orderLogDao.deleteByOrder(RedisKeyFactory.ORDER_KEY);
			orderMapper.insertOrderLog(orderList);
		}
	}
	
}

 

 

 

7. Redis Cli

 

처음 Redis를 사용하여 부족한 점이 있습니다.

Comments