영호

분산 환경에서 로컬 캐시의 정합성을 어떻게 보장할까 본문

DB

분산 환경에서 로컬 캐시의 정합성을 어떻게 보장할까

0h0 2024. 8. 25. 01:45

들어가면서

글로벌 캐시를 사용하다보니  분산환경에서 로컬캐시를 사용했을 때 어떻게 캐시 데이터 정합성을 맞출 수 있을지 고민해본 내용을 정리하려고 합니다.

 

실무에서 활용한 내용이 아닌 개인적으로 고민해본 내용이라 잘못된 부분이 있을 수 있습니다. 언제든지 댓글로 알려주시면 감사하겠습니다 ㅎ

 

글에서 다룰 내용

  • 로컬 캐시를 언제 활용할 수 있을지
  • 분산 환경에서 로컬 캐시의 정합성을 어떻게 맞출 수 있을지

로컬 캐시를 언제 활용할 수 있을까

저는 로컬캐시를 캐시 서버의 트래픽을 분산시킬 때 사용할 수 있을 것 같습니다. 혹은 조회 성능이 캐시 서버를 쓰는 것보다 빨라야 할 경우에 사용할 수 있을 것 같습니다.

 

데이터를 캐싱한다고 하면 redis 같은 별도의 캐시 서버를 많이 활용합니다. 하지만 글로벌 캐시만을 활용해 캐싱 작업을 진행한다면 데이터가 많아질수록 부하가 발생할 것입니다.

 

예를 들어, 조회요청이 많은 주식 종목이란 공통적인 데이터를 글로벌 캐시에 추가한다고 가정해보겠습니다. 이때, 기존 캐시 서버에 다른 데이터들도 캐싱되어 있을 경우 새롭게 추가된 주식 종목 캐싱으로 인해 캐시 서버에 부하가 발생할 수 있습니다. 이때, 해당 데이터를 각 WAS의 로컬 캐시에 관리할 경우 캐시 서버로의 트래픽을 줄일 수 있을 것 같습니다.

 

이처럼, 로컬캐시를 활용해 캐시서버의 부하를 분산시킬 수 있을 것 같습니다. 다만, 개인 데이터의 경우 로컬캐시에 담기에는 데이터가 많을 수도 있기 때문에, 개인 데이터를 로컬 캐시에 관리할 때는 주의가 필요할 것 같습니다.

캐시 종류

캐시는 크게 글로벌 캐시, 로컬 캐시 2종류가 있습니다.

 

글로벌 캐시

별도의 캐시서버를 두어 데이터를 조회하는 방식입니다. 대표적으로 redis가 있습니다.

 

로컬 캐시

WAS별로 데이터를 캐싱하여 사용하는 방식입니다. 이로 인해, 분산환경에서 사용 시 각 WAS별로 데이터 정합성을 맞춰줘야 되는 어려움이 있습니다.

그러나 조회 성능 측면에서는 별도의 네트워크 작업이 필요없기 때문에 글로벌 캐시보다 속도가 빠릅니다.

분산환경에서 로컬 캐시 주의점

로컬 캐시는 각 WAS에서 데이터를 캐싱하고 있는 것이기 때문에 1번 WAS를 통해 원본 데이터에 수정이 발생할 경우 다른 WAS들의 캐시 데이터에도 이를 적용 해줘야됩니다. 즉, 데이터의 정합성을 맞춰줘야 됩니다.

어떻게 정합성을 맞출 수 있을까

2가지 정도 방법이 있을 것 같습니다. TTL 설정, invalidation message propagation

하나씩 살펴보겠습니다.

 

TTL 설정

캐싱되어 있는 데이터가 유효한 시간을 정해주는 방법입니다. TTL이 지난 데이터는 캐시에서 사라지고 추후 조회 요청이 발생하면 최신 데이터를 조회하여 캐시를 채우는 방식입니다. 해당 방법은 데이터가 주기적으로 바뀔 때 유용할 것 같습니다.

 

예를 들어, 어제의 게임 점수 랭킹을 보여주는 기능이 있다고 가정해보겠습니다. 그렇다면 해당 캐시 데이터의 TTL을 하루로 잡으면 날이 바뀔 때 마다 기존 캐시 데이터는 제거되고, 최신 데이터가 캐싱되면서 빠른 조회 성능을 챙길 수 있습니다.

 

Invalidation Message Propagation

해당 방법은 캐시 데이터에 수정이 발생할 경우 기존 캐시 데이터는 유효하지 않다는 것을 다른 WAS에 전파시켜 기존 캐시 데이터를 무효화시키는 방법입니다.

 

자바 진영의 cache 라이브러리 중 하나인 caffeine 측의 issue에 보면 분산 환경에서 어떻게 데이터를 동기화 하는지에 대한 질문이 있습니다. 이에 대한 대답으로 redis pub/sub 활용하면 좋다는 답변이 있습니다.

 

이처럼 데이터 수정이 발생한 WAS가 변경된 캐시 데이터의 key가 이제 유효하지 않다는 메시지를 다른 WAS에게 전파하는 방식입니다.

이 방법은 데이터의 변경 주기가 일정하지 않고 유저 액션에 의해 일어나는 경우 적절하다고 생각합니다.

 

write-back, write-through

추가적으로 write-back, write-through 등의 방법도 있는데 이는 분산 환경의 로컬 캐시에서는 적합하지 않다고 생각합니다. 글로벌 캐시의 경우 여러 WAS가 동일한 캐시 서버를 바라보기 때문에 이 경우에는 가능할 것 같습니다.

 

하지만, 분산 환경의 로컬 캐시에선 힘들 것 같다고 생각합니다. 왜냐하면, 두 방법 모두 캐시에 데이터를 업데이트하고 이를 기반으로 DB에 추가적인 변경이 발생합니다. 이때, 데이터 변경 요청을 받지 않은 서버들은 원본 DB의 변경사실을 모른채 계속해서 캐싱된 데이터를 내려줄 것입니다. 그렇기 때문에 분산 환경의 로컬 캐시에서는 다른 WAS들의 캐시 데이터를 무효화시킬 필요가 있어보입니다.

Invalidation Message Propagation 예제 코드

employeeService

@Slf4j
@Service
@RequiredArgsConstructor
public class EmployeeService {

    private final EmployeeRepository employeeRepository;
    private final CacheManager cacheManager;
    private final RedisPublisher redisPublisher;

    @Transactional
    public Long save(EmployeeCreateDto employeeDto) {
        Employee employee = new Employee(employeeDto.getName(), employeeDto.getPhone(), employeeDto.getAge());
        return employeeRepository.save(employee).getId();
    }

    @Transactional
//    @CacheEvict(value = "employees", allEntries = true) 단일 환경에서 활용 가능
    public Long modify(EmployeeModifyDto employeeDto) {
        Employee employee = employeeRepository.findById(employeeDto.getEmployeeId())
                .orElseThrow(() -> new IllegalArgumentException("Employee not found"));

        employee.modify(employeeDto.getName(), employeeDto.getPhone());

				// TransactionalEventListener를 활용해 commit 이후 발행하도록 분리 가능
        redisPublisher.publish(ChannelTopic.of("employee-invalidation"), employeeDto.getEmployeeId());
        return employee.getId();
    }

    @Transactional(readOnly = true)
    @Cacheable("employees")
    public List<Employee> find(Integer id) {
        return findEmployeeById(id);
    }
}

더 알아볼 내용

  • write-back, write-through

'DB' 카테고리의 다른 글

[DB] 트랜잭션이란?  (0) 2022.05.09
Comments