4줄 짜리 로직 만들어 내려고(getExistEntityInList 메서드) 3시간여를 모자란 머리 쥐어 짜며 야근.상황 이해해야 코드 설명도 와닿을거라 (그리고 조금 지난 후 제 자신도 왜 이런거 짰나 기억 되살릴 수 있을 듯 하여) 우선 배경부터 잠깐.
1. 카프카에서 데이터 꺼내 DB 에 건건이 입력하던거를 성능 개선을 위해 데이터 모아두었다가 배치로 처리하게 구현했었습니다.
2. 로그성 데이터이지만 데이터 활용 때문에 몇 개의 속성값(키값)이 동일한 데이터(이하 '같은 아이템'이라고 칭하겠음)이 DB 에 이미 있다면 최종 동일 아이템에 변경된 값만 덮어써서 insert 하는(즉, 기존 정보에 변경값이 계속 누적 추가되는, update를 가장한 insert) 방식입니다. 예를 들어 기존에 (A, B, 1, 2, 3) 값으로 된 아이템이 있고 신규로 (A, B, 1, null, 4) - A, B 는 PK - 가 들어왔다면 (A, B, 1, 2, 4) 가 입력되어야 합니다.
3. 앞 의 내용을 구현하기 위해 이런 식으로 구현했었습니다.dbData = getExistDataInDB(newData);
addDataToQueue(newData, dbData);newData 가 DB 에 존재하는 아이템인지 확인해서 DB 에 있으면 dbData 에 newData 를 합치고, 아니면 newData 를 그대로 큐에 넣어 쌓아둔 후 스케쥴링으로 구현해 둔 메서드에서 정기적으로 이 큐 데이터를 배치로 DB 에 넣습니다.4. 잘 동작했습니다. 그런데, 오늘 테스트하다 보니 데이터에 구멍이 생긴 건이 발견되었습니다. 여기서 잠깐, 글 읽기 멈추고 왜 그랬을지 진단해보세요. 1 ~ 3 잘 읽어보면 큰 구멍이 있습니다.
.
.
.
.
발견하셨나요?
스케줄링, 한 주기에 동일한 아이템이 여러 건 들어오면 어찌 될까요?추리하거나 눈치채신 분이 계실겁니다.
큐에 쌓기 전에 DB 에 해당 아이템이 있는지 검색해서 있다면 그 속성 값들 합친다고 했었죠. 그런데, 동일 아이템 여런 건이 같은 스케쥴링 주기에 들어오면 그 모든 건이 똑같은 DB 최종 아이템을 기준으로 누적될테니 중간 아이템의 변경값들이 날라가버리는 사태가 발생합니다.5. 버그 해결하려고 만든게 이 코드입니다. 이하에서 코드 간략하게 설명하겠습니다.
a. DB 에 데이터 있는지 조회 전 우선 List.contains 메서드 이용해서 큐 용도로 만들어 놓은 List 에 신규 데이터와 같은 아이템이 있는지 검사를 합니다(위 소스에는 없지만 contains 메서드 사용을 위해 해당 entity 에는 equals 메서드 구현해놓았습니다)
b. 리스트에 들어있는 값들을 구글 guava 라이브러리에서 제공하는 reverse 메서드를 이용하여 reverse sort 했습니다. 같은 아이템이 여러 건 있을 때 가장 마지막 아이템 값을 쉽고, 빠르게 찾으려는 목적입니다. jdk 자체에 Collections.sort(list, Collections.reverseOrder()) 제공되기는 하지만 원본 리스트 자체를 바꿔 버립니다. 제 경우 원본 list 는 유지해야 해서 guava 제공 메서드를 사용했습니다.
c. 역시나 guava 에서 제공하는 Iterables.find 메서드를 이용해서 앞에서도 언급했던 Entity 에 구현해놓은 equals 메서드로 동일 아이템을 찾았습니다. Predicate 부분은 원래 조금 다르게 짰는데 IDE 에서 자동으로 람다 식으로 깔끔하게 변환해 주네요.
d. Spring 에서 제공하는 BeanUtils.copyProperties 를 이용해서 찾은 빈에 신규 빈의 속성을 복사해넣었습니다. 이때 신규 데이터의 null 값인 속성은 덮어쳐지지 않도록 getNullPropertyNames 라는 메서드를 옵션으로 주었습니다. getNullPropertyNames 는 stackoverflow 에서 발견한 소스 그대로 쓴 건데 코드는 저도 자세히 읽어보지 않았습니다.
e. 일단 소스 적용해 본 바로는 기대한대로 동작하는 듯합니다.
private Object getLastEntityInList(List entites, Object entity) {
Object lastEntity = null;
if (entites.contains(entity)) {
List evtAlrtHstsR = Lists.reverse(entites);
lastEntity = Iterables.find(evtAlrtHstsR, (Predicate<?>) hst -> hst.equals(entity));
if (lastEntity != null) {
BeanUtils.copyProperties(entity, lastEntity, Util.getNullPropertyNames(entity));
}
}
return lastEntity;
}
/* Util */
public static String[] getNullPropertyNames(Object source) {
final BeanWrapper wrappedSource = new BeanWrapperImpl(source);
return Stream.of(wrappedSource.getPropertyDescriptors())
.map(FeatureDescriptor::getName)
.filter(propertyName -> wrappedSource.getPropertyValue(propertyName) == null)
.toArray(String[]::new);
}
/* 호출 */
EvtAlrtHst dbAlrtHst = (EvtAlrtHst) getLastEntityInList(evtAlrtHstList), evtAlrtHst);
if (dbAlrtHst == null) {
dbAlrtHst = evtAlrtHstRepository.findAlertInfo(trmCd, alrtObjId, alrtCd, evntDt);
}
'Slack 채널 정리' 카테고리의 다른 글
파일 크기 기준으로 분할 압축된 로그에서 정보 찾기 (0) | 2019.12.02 |
---|---|
Spring Boot 에서 hessian 사용하기 (0) | 2019.12.02 |
Javers (0) | 2019.11.29 |
리눅스 fuser 명령어 (0) | 2019.11.29 |
git HTTP Basic: Access denied 에러 (0) | 2019.11.29 |