서두
동작을 설명하기 이전에 지역 데이터를 스프링의 캐시 기반으로 조회를 하려고 했던 배경에 대해서 먼저 소개하겠습니다.
현재 진행 중인 프로젝트에서는 사용자들이 자신의 선호지역 혹은 자신이 검색하고자 하는 지역들을 선택 할 수 있습니다.
이를 클라이언트에서 불러오기 위한 방법을 2가지를 떠올렸습니다.
1. 먼저 시를 선택하면 시에 해당하는 id 값을 서버에 보내서 구를 가지고 오는 api를 호출한다.
2. 구가 도착하면 다시 구를 선택하고 구에 해당하는 id 값을 서버에서 보내서 동을 가지고 오는 api를 호출한다.
3. 마지막으로 동을 선택한다.
위와 같은 과정은 먼저 3번의 네트워크를 타야하고, 유저가 어떤 시, 구, 동을 선택할지 예상 할 수 없었습니다.
캐시를 사용한다고 하더라도 각 시에 호출, 구에 대한 호출, 동에 대한 값에 대해서 캐싱을 진행해야하는데, 캐시는 메모리에 담기며 속도는 매우 빠르지만 적은 공간을 차지하는 저장소입니다.
따라서, 캐시에 담아서 조회 성능을 향상 시키려는 목적 자체는 괜찮은 생각이였지만, 해결하기에 좋아보이지는 않았습니다.
이어서, 생각하게 된 두 번째 방법입니다.
처음부터 응답 시에 시, 구, 동 얻을 수 있는 api를 한 번 호출합니다. 그리고 최종적으로 동까지 선택을 해야 하는 플로우이기 때문에 동에만 id를 넣어두고, 실제로 서버에는 선택한 동에 대한 id를 보내줍니다.
다만, 시에 따라 구, 구에 따라 동이 나와야 하기 때문에 한 번에 보내주는 데이터는 다음과 같은 형태가 됩니다.
"data": [
{
"si": "서울특별시",
"gu": [
{
"gu": "동대문구",
"dong": [
{
"id": 333,
"dong": "답십리1동"
},
{
"id": 334,
"dong": "이문2동"
},
]
},
{
"gu": "중랑구",
"dong": [
{
"id": 317,
"dong": "망우본동"
},
{
"id": 318,
"dong": "면목3,8동"
},
{
"id": 319,
"dong": "면목본동"
}
]
},
}
],
해당 데이터를 클라이언트의 컬렉션에서 보관하고 선택에 따라서 그 값을 보여주는 형태입니다.
그리고 서버에서는 해당 지역데이터를 캐시에 보관하고, 호출 시에 같은 값을 보내주면 쿼리 호출에 대한 성능 개선이 이루어질 것으로 판단하였습니다. 또한 지역 데이터는 변할 가능성이 많지 않다고 판단하였기에 캐시에 보관하기 적절하다고 판단했습니다.
공공 지역 데이터 가져오기
이제 데이터를 DB에 저장을 해야합니다. 노가다(?)로 각 지역에 있는 데이터를 직접 INSERT 시켜 줄 수 있지만, 그것은 너무 비효율적인 방법인 것 같습니다. 해당 지역에 대한 행정 정보는 공공 데이터에서 제공해주고 있으므로 이를 이용해보려고 합니다.
https://data.seoul.go.kr/dataList/OA-21234/S/1/datasetView.do
저는 다음과 같은 행정 지역 데이터를 사용하였습니다.
해당 링크를 타고 들어가 아래로 내려가보면 다음과 같은 형식을 찾아 볼 수 있습니다.
현재 데이터베이스에서 사용하고 있는 regions 테이블의 컬럼은 (id, si, gu, dong) 으로 구성이 되어있습니다.
따라서, 현재 제공해주고 있는 값과 다르고 시도 명칭에도 서울특별시에 대한 데이터만 존재하는 것이 아닌 인천시, 경기도 등 사용하지 않을 다른 데이터들이 있는 것을 볼 수 있습니다.
이를 해결하기 위해 csv 파일로 내려받고 데이터를 수정해보겠습니다.
데이터를 다음과 같이 엑셀로 수정하고 csv 파일로 다시 저장을 하였습니다.
csv 데이터를 DB에 저장하기 (MySQL WorkBench)
현재 RDB를 MySQL을 사용하고 있기에 MySQL WorkBench를 사용하여 csv 데이터를 테이블에 반영해보도록 하겠습니다.
MySQL WorkBench에 들어와 테이블에서 우 클릭을 누르면 다음과 같은 여러 옵션들을 볼 수 있습니다.
해당 옵션을 누르면 다음과 같은 화면을 볼 수 있습니다.
다음과 같이 DB에 반영하고자 하는 csv 파일을 선택해주고 진행을 해줍니다.
만약 csv 파일이 정상적으로 되어있다면 정상적으로 테이블에 반영이 될 것이고 csv 파일에 문제가 있다면
다음과 같은 에러 문구를 마주치게 됩니다. 해당 이슈에 대한 해결 방법은 다음(추가 예정) 에서 확인 하실 수 있습니다.
스프링 캐시를 사용하여 데이터를 조회하기
스프링 캐시를 사용하기 위해 다음과 같은 사전 작업이 필요합니다.
1. 라이브러리 주입
스프링 부트를 사용 중이라면 다음과 같은 라이브러리를 사용하여 손 쉽게 구현이 가능합니다.
이어서, 인 메모리 캐시를 만들어보도록 하겠습니다.
2. 스프링 캐시 설정파일 적용
@EnableCaching
@Configuration
public class SpringCachingConfiguration {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager simpleCacheManager = new SimpleCacheManager();
simpleCacheManager.setCaches(List.of(new ConcurrentMapCache("regions")));
return simpleCacheManager;
}
}
캐시를 사용하기 위해 @EnableCaching을 설정해주고, 설정파일이 적용되도록 @Configuration 옵션을 넣어줍니다.
그리고 캐시 메모리를 만들기 위한 cacheManager 빈을 생성해줍니다.
setCaches에는 value값으로 별도의 캐시 저장소가 생성되게 됩니다.
3. 캐시가 적용되어야 할 메서드 정의
@Cacheable(value = "regions")
public List<AllRegionResponse> findAllRegions() {
var regions = regionRepository.findAll();
return RegionMapper.mapToAllRegionResponse(regions);
}
해당 메서드는 regionService 영역에 있는 메서드입니다.
@Cacheable 애노테이션을 사용하여 이전에 캐시 저장소로 지정했던 값을 명시해줍니다.
@Cacheable 이것말고도 다양한 옵션들을 가지고 있습니다.
https://livenow14.tistory.com/56
해당 블로그에 각 옵션에 대한 설명들이 명시되어 있으니 한 번 읽어보실 것을 권장드립니다.
이제 이어서 api를 호출해보도록 하겠습니다.
첫 번째 api를 호출했을 때는 service 영역의 메서드를 호출하였지만, 두 번째 호출에서는 service 영역의 메서드를 호출하지 않은 것을 볼 수 있습니다.
HTTP 테스트를 진행해도 동일한 값을 반환하게 됩니다. 하지만 아직 의심스럽습니다.
캐시에 정확하게 값이 담기는 지 확인하는 테스트를 작성해보도록 하겠습니다.
@SpringBootTest
public class RegionCacheTest {
@Autowired
private CacheManager cacheManager;
@Autowired
private RegionRepository regionRepository;
@Autowired
private RegionService regionService;
@DisplayName("이미 불러온 전체 지역 데이터는 한 번 만 불러온다.")
@Test
void regionCacheRead() {
// given
var regionA = Region.builder()
.si("서울특별시")
.gu("강남구")
.dong("삼성1동")
.build();
var regionB = Region.builder()
.si("서울특별시")
.gu("서초구")
.dong("양재1동")
.build();
regionRepository.saveAll(List.of(regionA, regionB));
// when
var allRegions = regionService.findAllRegions();
var regionCache = cacheManager.getCache("regions");
// regions 캐시에서 값을 가져오기
ConcurrentMap<Object, Object> cacheMap = (ConcurrentMap<Object, Object>) regionCache.getNativeCache();
// 모든 값에 대한 작업 수행
cacheMap.forEach((key, value) -> {
// value를 이용한 작업 수행
System.out.println("Value: " + value);
});
// then
assertThat(regionCache).isNotNull();
}
}
결과
테스트는 성공적으로 통과하며 Cache에 조회 한 값이 담겨있는 것을 확인해 볼 수 있습니다.
결론
이번 포스팅에서는 스프링에서 제공해주는 인 메모리 캐시를 한 번 적용해봤습니다. 현재 데이터는 변경 될 가능성이 있는 값이 별로 없기 때문에 동시성에 대한 고민을 많이 할 필요가 없었지만, 만약 변경이 잦은 내용이라면 캐시에 담아 둘 필요가 있을지에 대한 고민과 동시성에 대한 고민을 해야 할 필요가 있습니다.
또한, 현재 레디스를 메시지 브로커로 사용을 하고 있는데, 있는 그대로 레디스를 사용하는 것이 어떨까? 하는 고민도 들었지만 현재 단일 서버를 운영하고 있는 상태에서 레디스를 캐시 메모리로 사용하는 것은 오버 엔지니어링을 하는 걸 수도 있겠다는 생각이 들어 Local Cache를 사용하였습니다. 만약, 여러가지 다른 용도로 캐시를 사용 할 일이 많아진다면 redis로 바꿀 필요도 있을 것 입니다.
'프로그래머스 데브코스' 카테고리의 다른 글
최종 프로젝트 회고(2) - 멀티 모듈 (0) | 2023.12.08 |
---|---|
최종 프로젝트 회고 (1) - 일정 관리 (1) | 2023.11.30 |
Code Deploy를 이용한 멀티 모듈 애플리케이션 배포 CD 구축하기 (1) | 2023.09.26 |
프로그래머스 데브코스 14주차 회고 (0) | 2023.09.04 |
프로그래머스 데브코스 13주차 회고 (0) | 2023.08.27 |