사례로 배워보는 디자인패턴 #5 - 추상화를 통한 파티션 테이블 자동 갱신하기 (1)
<현황>
- 다양한 도메인에서 대량의 데이터 분석이 매일 DataLake에서 이루어지고 있음
- 각 도메인별 데이터 분석결과물은 유형에 따라서 일/주/월 간격으로 생성이 됨 ( Partition by date )
- 이 결과물은 RDB에 적재되고 Application은 이를 참조하여 다시 다양한 외부 데이터 / 메타 등과 결합하고 비지니스 로직을 처리
- 최종 데이터 컨텐츠를 서비스형태로 사용자에게 제공함
<해결해야하는 점>
1. 각 도메인별로 생성되는 주기가 다르기 때문에 서비스에서 일괄로 최종 적재일자를 파악하기 어려움 (Latest Partition)
2. Application에서 스케쥴링을 통하여 적재일자 갱신작업을 수행하고 있으나 이때 에러가 발생할 경우 다음 스케쥴까지 기다려야 함
3. API / DB 호출은 속도가 느리기 때문에 최대한 캐시를 활용해야 하지만 1,2번과 맞물려 캐시 갱신타이밍을 잡기 어려움
4. 각 서비스 별로 Latest Partition을 관리하다 보니 코드의 복잡성이 증가하고 테스트하기도 어려움
<개요>
- 실제 쿼리를 수행하는 Repository를 멤버로 가지며 각 테이블의 파티션 일자를 관리하는 Wrapper Class 정의
- Wrapper Class에 대해서Abstract형태로 메소드를 추상화하고 각 테이블별로 최신파티션일자를 관리하도록함
- AbstractPartitionedRepository
- setLatestPartition / getLatestPartition: 구현체는 도메인별 테이블마다 특성을 반영하여 작성
- PartitionSyncService
- AbstractPartitionedRepository타입에 대해서 Bean주입을 받아서 List형태로 관리 하도록 함(dependency injection)
- 도메인별 구현없이 AbstractPartitionedRepository 타입에 대해서는 통합으로 동기화 작업 수행
- AbstractPartitionedRepository
<AS-IS>
- 각 테이블에 대한 Repository 클래스 와 최신 파티션 일자 저장/갱신 로직 을 구현하고 있는 Biz Service
- Service Layer에서 주기적으로 소유하고 있는 Repository의 메소드를 호출하여 최종 적재일자를 가져오는 형태
@Service
@Slf4j
public class BizAService {
private MonthlyMarketShareRepository monthlyMarketShareRepository;
private MonthlyUsageStatRepository monthlyUsageStatRepository;
private MonthlyPointRepository monthlyPointRepository;
private MonthlyDropoffRepository monthlyDropoffRepository;
private MonthlyDropoffCombRepository monthlyDropoffCombRepository;
private String monthlyMarketShareLatestPartition;
private String monthlyUsageLatestPartition;
private String monthlyPointLatestPartition;
private String monthlyDropoffLatestPartition;
private String monthlyDropoffCombLatestPartition;
@Scheduled(cron = "0 */20 * * * *")
private void setLatestPartition() {
monthlyMarketShareLatestPartition = monthlyMarketShareRepository.findFirst...().getExecYm();
monthlyUsageLatestPartition = monthlyUsageStatRepository.findFirst...().getExecYm();
monthlyPointLatestPartition = monthlyPointRepository.findFirst...().getExecYm();
monthlyDropoffLatestPartition = monthlyDropoffRepository.findFirst...().getExecYm();
monthlyDropoffCombLatestPartition = monthlyDropoffCombRepository.findFirst...().getExecYm();
}
- 저장되어 있는 최종 파티션 일자를 DB 조회시 조회조건으로 사용
public MarketStatResponse getMarketShareStat(String districtCode) {
List<MonthlyMarketShare> monthlyMarketShareList = monthlyMarketShareRepository.findistrictCode, monthlyMarketShareLatestPartition);
<TO-BE>
1. 최종 파티션 관리 및 일자값 저장을 위한 추상화 클래스
- getLatestPartition을 위한 Implementation은 각 도메인 테이블별로 수행
public abstract class AbstractPartitionedRepository {
protected PartitionExecDt partitionExecDt;
public abstract PartitionExecDt getLatestPartition();
public void setLatestPartition(){
this.partitionExecDt = getLatestPartition();
}
public PartitionExecDt getPartitionExecDt(){
return this.partitionExecDt;
}
}
2. 최종 파티션 일자에 대한 테이블 별 상세구현을 Repository와 함께 Wrapper로 처리하면 Service Layer에서는 더이상 파티션 조건을 신경쓰지 않아도 됨
public class MonthlyMarketShareRepositoryWrapper extends AbstractPartitionedRepository {
MonthlyMarketShareRepository monthlyMarketShareRepository;
@Override
public PartitionExecDt getLatestPartition() {
MonthlyMarketShare monthlyMarketShare = this.monthlyMarketShareRepository.findFirstByExecYmNotNullOrderByExecYmDesc();
return PartitionExecDt.builder()
.execDt(monthlyMarketShare.getExecYm())
.build();
}
// Service Layer에서는 필요한 값만 전달하고 파티션조건에 대한 처리는 Wrapper에서 담당한다.
public List<MonthlyMarketShare> findAllBySigCodeAndExecYm(String districtCode){
return this.monthlyMarketShareRepository.findAllBySigCodeAndExecYm(districtCode, this.partitionExecDt.getExecDt());
}
3. Biz Serivce에서는 Logic구현에 대해서만 처리하도록 하며, 스케쥴링을 통한 동기화 작업에 대해서는 공통 관심사로 분리하여 통합
@Service
@Slf4j
public class BizAService {
private MonthlyMarketShareRepositoryWrapper monthlyMarketShareRepositoryWrapper;
private MonthlyUsageStatRepositoryWrapper monthlyUsageStatRepositoryWrapper;
private MonthlyPointRepositoryWrapper monthlyPointRepositoryWrapper;
private MonthlyDropoffRepositoryWrapper monthlyDropoffRepositoryWrapper;
private MonthlyDropoffCombRepositoryWrapper monthlyDropoffCombRepositoryWrapper;
public class PartitionSyncService {
@Autowired
List<AbstractPartitionedRepository> repositoryList;
@Autowired
public PartitionSyncService(List<AbstractPartitionedRepository> repositoryList){
this.repositoryList = repositoryList;
}
@Scheduled(fixedDelay = 1000 * 60 * 10)
public void setPartition(){
for(AbstractPartitionedRepository abstractPartitionedRepository : repositoryList){
abstractPartitionedRepository.setLatestPartition();
}
}
<테스트 코드 비교>
AS-IS : 각 서비스에 대해서 파티션 일자 세팅 및 Repository 조회조건에서도 동일하게 테스트
@BeforeEach
public void init(){
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(bizService, "monthlyMarketShareLatestPartition", "202406");
...
...
...
}
@Test
void testGetMarketShareStat() {
// Repository 호출시 파티션 조건명시 필요
given(monthlyMarketShareRepository.findAllBySigCodeAndExecYm("0000000000", "202406"))
.willReturn(Lists.newArrayList(kickboardTestData.monthlyMarketShare));
...
...
...
MarketStatResponse expected = bizTestData.getMarketStatResponse();
MarketStatResponse actual = bizService.getMarketShareStat("0000000000");
TO-BE : given형태로 mock객체에 파티션일자 세팅, Repository 조회조건에서는 더이상 관심사가 아님.
@Test
void testGetMarketShareStat() {
// ServiceLayer의 관심사가 아님
given(monthlyMarketShareRepositoryWrapper.getLatestPartition())
.willReturn(PartitionExecDt.builder().execDt("202406").build());
// Repository 호출시 파티션 조건 필요없음
given(monthlyMarketShareRepositoryWrapper.findAllBySigCodeAndExecYm("0000000000"))
.willReturn(Lists.newArrayList(kickboardTestData.monthlyMarketShare));
...
...
MarketStatResponse expected = bizTestData.getMarketStatResponse();
MarketStatResponse actual = bizService.getMarketShareStat("0000000000");
<효과>
- Service Layer에서 적재 파티션 일자에 대한 관심사 분리 ( Repository Layer )
- Repository가 추가될 때마다 동기화에 대한 작업을 신경쓰지 않아도 됨
- Abstract타입 일괄호출을 통한 수동반영 가능
- 개발 및 테스트 용이성
- 캐시 Point 설정 용이성
- 해당 리팩토링을 통해서 Biz 관심사와 공통 관심사를 분리하여 코드를 보다 간결하게 만들 수 있었다.
- 다음 글에서는 이 작업을 통해서 관심사가 깔끔하게 분리되었을때 Cache에 대한 적용 (Key값, 갱신주기 등) 이 더 효율적으로 진행될 수 있음을 살펴본다.