목차
개요
이전 포스팅에서도 볼수 있다 싶이
디자인 변화에 따른 회의를 통한 여러 아이디어들의 비교와 선정을 하였는데 이번 포스팅에서는 이를 적용하고 나름 어떻게 효율적으로 코드를 작성했나(?) 를 중점적으로 포스팅 해보려합니다.
또한 따로 개발 서버를 두고 있지 않고있기 때문에 api의 버전명시를 통해서 현재 운영 서버에 새로운 프레젠테이션 구조를 가진 api 를 같이 배포를 하고 이에 대한 캐시 전략까지 확인해보기로합니다.
아이디어 4를 적용한 서비스 로직 구현
먼저 아이디어4의 BusinessSchedule 엔티티에서 연속된 일정을 효율적으로 처리하기 위해 도입된 새로운 로직과 이에 따른 변경 사항을 다루고자 합니다.
기존의 로직에서는 일정의 BlockType(Start, Middle, End, Single)을 결정할 때 날짜만을 api 를 통하여 전달했기 때문에, 연속된 일정을 구분하는 작업이 주로 프론트엔드에서 이루어졌습니다.
따라서 아이디어4를 구현할때 연속된 일정들을 정확하게 구분하기 위해 날짜뿐만 아니라 ChangeType과 Description도 함께 고려하도록 BlockType 결정 로직을 개선했으며, 인덱스 기반 리스트 순회 방식을 Iterator로 대체하여 코드의 가독성과 유지보수성을 높였습니다.
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();
- 리스트가 비어 있는 경우(hasNext()가 false), 빈 리스트 scheduleDTOs를 반환하게 하여 처리할 일정이 없는 경우를 처리한다.
- 이후에 List 의 iterator()을 호출하여 Iterator 로 만들어주고
- iterator.next()를 호출하여 리스트의 첫 번째 BusinessSchedule 객체를 current 변수에 할당하게 한다.
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;
}
- isPreviousLinked는 이전 일정이 연속된 일정인지 여부를 나타내는 플래그로, 처음 값은 이전 값이 없기 때문에 false로 설정한다.
- while (iterator.hasNext()) 루프는 리스트의 모든 요소를 순회할 때까지 반복한다.
- 루프 내부에서 iterator.next()를 호출하여 다음 일정(next)을 가져오게한다.(Iterator을 쓴 이유 중 하나. 표현이 직관적이다.)
- isNextLinked 변수는 현재 일정(current)과 다음 일정(next)이 연속되는지 판단한다.
- 두 일정의 날짜가 연속(즉, current.getDate().plusDays(1)이 next.getDate()와 동일)
- ChangeType과 Description이 동일
- getBlockType(isPreviousLinked, isNextLinked) 메서드는 현재 일정의 BlockType을 결정한다.
- scheduleDTOs.add(BusinessScheduleWithBlockTypeDTO.from(current, blockType))를 호출하여 current 일정에 대한 BusinessScheduleWithBlockTypeDTO를 생성하고 리스트에 추가한다.
- isPreviousLinked 플래그를 현재의 isNextLinked 값으로 업데이트해서 다음 루프에서 현재 일정이 이전 일정과 연속되는지 판단하는 데 사용한다.
// 마지막 일정 처리
BlockType lastBlockType = isPreviousLinked ? BlockType.END : BlockType.SINGLE;
scheduleDTOs.add(BusinessScheduleWithBlockTypeDTO.from(current, lastBlockType));
return scheduleDTOs;
}
- 루프가 끝나면 마지막 일정이 처리되지 않았기 때문에, 마지막 일정에 대한 BlockType을 결정하고 리스트에 추가한다.
- isPreviousLinked가 true라면 마지막 일정은 연속된 일정의 끝(END)으로 간주되며, 그렇지 않으면 독립적인 일정(SINGLE)으로 지정한다.
마지막으로 객체 생성 시에는 DTO 내부에 정적 팩토리 메서드(from)를 도입하였는데
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 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;
}
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 내부에 변환 로직을 구현한 이유
- BlockType은 프론트엔드에서 UI 표현을 위해 사용하는 정보로, 비즈니스 로직이나 데이터 저장하지 않는다. 또한 다른 도메인의 서비스단과 상호작용이 없고 프레젠테이션 단에서만 사용되기 때문에 DTO 클래스 내에 메서드 형태로 도입하여 책임을 분리했다.
- 개념 객체를 통해 명시 해줄수도 있었다. 하지만 이후에 서버나 또다른 api에서 BlockType을 재사용할 일이 없다고 판단하여 안에 숨겨놓는 캡슐화를 진행했다.
정적 팩토리 메서드를 통해 구현한 이유
- 해당 기능을 구현하는 상황에서는 생성자보다 더 명확하고 관리하기 쉬운 객체 생성 방식이라고 판단하였다.
- 또한 생성자로 각 속성을 나열하는 것보다 객체 + DTO 내부에서만 선언되어있는 enum 타입 의 조합으로 해당 메서드가 동작한다는 것을 표현하고 싶었다.
캐시 전략에 대한 고민과 선택
운영 중인 애플리케이션을 안정적으로 유지하면서 새로운 기능을 개발하고 테스트하기 위해서 기존의 api와 캐시를 삭제하지 않고 그대로 두었고, 같은 DB에 접근하는 API를 엔드포인트의 버저닝부분을 /v2 로 변경하여 추가 구현을 해주었다.
따라서 v1과 v2 API를 동시에 운영하는 상황에서 적절한 캐시 전략이 필요하다고 생각이 들었습니다. 사실 많이 복잡하지는 않았지만 실제 운영 서버에 테스트를 위한 api 와 운영중인 api를 캐싱 기능을 포함하여 모두 올려보는게 처음이라서 글을 써본다.
최고의 선택은 테스트를 위한 서버를 따로 두거나 현재 채택되지 않은 api의 경우엔 캐시기능을 굳이 안넣는게 좋을것 같다.
캐시를 어떻게 관리할지에 대한 고민은 크게 두 가지 측면에서 이루어졌는데
첫째, v1과 v2의 DTO 구조가 서로 다르다는 점.
이로 인해 동일한 데이터를 캐시하더라도 두 버전이 서로 다른 형식으로 데이터를 사용하기 때문에, 캐시를 분리하여 관리하는 것이 불가피했다.
둘째, 각 버전의 캐시를 어떻게 무효화할지에 대한 고민이 있었다.
특정 데이터가 수정, 추가, 삭제될 때, v1과 v2 모두에서 해당 데이터의 캐시가 무효화되어야 했기 때문이다.
이를 해결하기 위해 다음과 같은 전략을 채택했다.
- 버전별 독립 캐시 사용: v1과 v2의 DTO 구조 차이를 반영하여, 각 버전에서 별도의 캐시를 운영.
- 동일한 무효화 타이밍 적용: 데이터의 추가, 수정, 삭제 시점에 v1과 v2의 캐시를 동시에 무효화하도록 설정.
마무리
현재는 운영중인 페이지와 새로운 디자인으로 개발되고 있는 개발 서버에서도 같은 서버 url을 통해서 각각의 api 가 확실하게 작동되고 있다.
운영을 하면서 느끼는건데 은근히 캐시전략을 수립할때 ~ 되겠지 라는 가벼운 마음으로 다가갔다가는 꼭 놓치는 부분이 나와서 서비스에 불편을 주는 상황이 한번 씩 생기는것같다. 따라서 위에 있는 나름의 흐름도(?) 를 만들어서 관리를 하는게 이용자도 불편을 겪지 않아서 좋고 나 또한 일을 두번해야되는 상황이 발생하지 않을수 있어서 좋은것 같다.
캐시전략은 항상 꼼꼼히,,
'Dayner 프로젝트' 카테고리의 다른 글
Dayner에서 구매 이력을 관리하는 방법 [1] (feat: 개인정보보호법, 원장 데이터) (4) | 2024.08.31 |
---|---|
영업시간 디자인 변경에 대응한 리팩토링 일지 [1] (feat: 연결된 일정 구현하기) (0) | 2024.08.22 |
Dayner 2차 리팩토링 계획(feat: 멀티 모듈화) (0) | 2024.04.03 |