dev notes

영업시간 디자인 변경에 대응한 리팩토링 일지 [2] (feat: 버전별 API 캐시 전략)

2024-08-257 min read
공유

개요#

1편에서 여러 아이디어를 비교하고 나니 남은 건 결국 "이걸 운영 서버에 어떻게 무리 없이 붙일까"였습니다. 별도 개발 서버를 두지 않는 환경이라 새 디자인용 API를 따로 놀릴 수 없었고, 기존 기능을 깨지 않으면서 같이 배포해야 했습니다.

그래서 API 버전을 명시하는 방식으로 운영 서버에 새 API를 같이 올렸고, 그에 맞는 캐시 전략도 같이 손봤습니다.

아이디어 4를 적용한 서비스 로직 구현#

먼저 아이디어4의 BusinessSchedule 엔티티에서 연속된 일정을 효율적으로 처리하기 위해 도입된 새로운 로직과 이에 따른 변경 사항을 다루고자 합니다.

기존 로직에서는 API를 통해 날짜만 전달했기 때문에, BlockType(Start, Middle, End, Single)을 결정하는 작업이 주로 프론트엔드에서 이루어졌습니다.

아이디어 4를 구현하면서 연속된 일정을 정확하게 구분하기 위해 날짜뿐만 아니라 ChangeType과 Description도 함께 고려하도록 BlockType 결정 로직을 개선했습니다. 또한 인덱스 기반 리스트 순회를 Iterator로 대체해 코드 가독성을 높였습니다.

java
private List<BusinessScheduleWithBlockTypeDTO> createScheduleDTOs(List<BusinessSchedule> businessSchedules) {
    List<BusinessScheduleWithBlockTypeDTO> scheduleDTOs = new ArrayList<>();
    Iterator<BusinessSchedule> iterator = businessSchedules.iterator();
 
    if (!iterator.hasNext()) {
        return scheduleDTOs;
    }
 
    BusinessSchedule current = iterator.next();
    boolean isPreviousLinked = false;
 
    while (iterator.hasNext()) {
        BusinessSchedule next = iterator.next();
 
        boolean isNextLinked = current.getDate().plusDays(1).isEqual(next.getDate()) &&
                current.getChangeType().equals(next.getChangeType()) &&
                current.getDescription().equals(next.getDescription());
 
        BlockType blockType = getBlockType(isPreviousLinked, isNextLinked);
 
        scheduleDTOs.add(BusinessScheduleWithBlockTypeDTO.from(current, blockType));
        isPreviousLinked = isNextLinked;
        current = next;
    }
 
    BlockType lastBlockType = isPreviousLinked ? BlockType.END : BlockType.SINGLE;
    scheduleDTOs.add(BusinessScheduleWithBlockTypeDTO.from(current, lastBlockType));
 
    return scheduleDTOs;
}

DTO 정적 팩토리 메서드(from) 도입 구조

DTO 정적 팩토리 메서드(from) 도입 구조

  • isPreviousLinked는 이전 일정이 연속된 일정인지 여부를 나타내는 플래그
  • isNextLinked 변수는 현재 일정(current)과 다음 일정(next)이 연속되는지 판단
  • 두 일정의 날짜가 연속이고, ChangeType과 Description이 동일하면 연속 일정으로 판단
  • getBlockType(isPreviousLinked, isNextLinked) 메서드로 현재 일정의 BlockType을 결정

마지막으로 DTO 내부에 정적 팩토리 메서드(from)를 도입했습니다.

java
public static class MonthlyScheduleDTO {
    private final List<BusinessScheduleWithBlockTypeDTO> businessScheduleDTOList;
 
    public MonthlyScheduleDTO(List<BusinessSchedule> businessScheduleList) {
        this.businessScheduleDTOList = createScheduleDTOs(businessScheduleList);
    }
 
    private enum BlockType {
        START, MIDDLE, END, SINGLE
    }
 
    private record BusinessScheduleWithBlockTypeDTO(int day, String changeType, String description, BlockType blockType) {
        public static BusinessScheduleWithBlockTypeDTO from(BusinessSchedule businessSchedule, BlockType blockType) {
            return new BusinessScheduleWithBlockTypeDTO(
                    businessSchedule.getDate().getDayOfMonth(),
                    businessSchedule.getChangeType().getTitle(),
                    businessSchedule.getDescription(),
                    blockType
            );
        }
    }
 
    private static BlockType getBlockType(boolean isPreviousLinked, boolean isNextLinked) {
        BlockType blockType;
        if (isPreviousLinked && isNextLinked) {
            blockType = BlockType.MIDDLE;
        } else if (isPreviousLinked) {
            blockType = BlockType.END;
        } else if (isNextLinked) {
            blockType = BlockType.START;
        } else {
            blockType = BlockType.SINGLE;
        }
        return blockType;
    }
}

DTO 내부에 변환 로직을 구현한 이유:

  1. BlockType은 프론트엔드의 UI 표현을 위한 정보로, 비즈니스 로직이나 데이터 저장과는 무관합니다.
  2. 서버나 다른 API에서 BlockType을 재사용할 일이 없다고 판단해서 DTO 안에 캡슐화했습니다.

정적 팩토리 메서드를 통해 구현한 이유:

  1. 생성자보다 더 명확하고 관리하기 쉬운 객체 생성 방식이라고 판단했습니다.
  2. 객체와 DTO 내부에서만 선언된 enum 타입의 조합으로 해당 메서드가 동작한다는 것을 표현하고 싶었습니다.

캐시 전략에 대한 고민과 선택#

운영 중인 애플리케이션을 안정적으로 유지하면서 새로운 기능을 개발하고 테스트하기 위해 기존 API와 캐시는 그대로 두고, 같은 DB에 접근하는 API를 엔드포인트 버저닝 부분만 /v2로 변경해 추가 구현했습니다.

v1과 v2 API를 동시에 운영하다 보니 적절한 캐시 전략이 필요했습니다. (Spring Cache Abstraction)

캐시 관리에 대한 고민은 크게 두 가지:

  1. v1과 v2의 DTO 구조가 서로 다르다는 점 → 캐시를 분리하여 관리
  2. 각 버전의 캐시를 어떻게 무효화할지 → 데이터 수정/추가/삭제 시 v1, v2 모두 무효화

v1/v2 버전별 캐시 분리 및 무효화 전략

v1/v2 버전별 캐시 분리 및 무효화 전략

  1. 버전별 독립 캐시 사용: v1과 v2의 DTO 구조 차이를 반영하여, 각 버전에서 별도의 캐시를 운영.
  2. 동일한 무효화 타이밍 적용: 데이터의 추가, 수정, 삭제 시점에 v1과 v2의 캐시를 동시에 무효화하도록 설정.

지금은 운영 중인 페이지와 새로운 디자인으로 개발 중인 서버가 같은 서버 URL을 쓰면서도 각자 필요한 API를 정상적으로 받고 있습니다.

돌아보면 여기서 진짜 중요했던 건 버전 분리 자체보다, 그에 따라 캐시를 어떻게 나눌지까지 같이 보는 일이었습니다. 운영하다 보면 캐시 전략을 가볍게 봤다가 꼭 한 번씩 놓치는 부분이 생기는데, 이번에도 결국 그걸 다시 확인한 셈이었습니다.

Connected Notes