HashSet 이용해 Tag 업데이트 로직 성능 최적화
서론
우리 서비스는 Tag 기능이 있다. Tag를 업데이트 할 때 평소 개발할 때처럼 id(PK)를 이용할 수 없고 name을 이용해야 하는 상황이 주어졌다. 프론트로부터 TagName 배열을 받기 때문이었다. 기존의 TagName 배열과 새로 들어온 TagName 배열을 비교해 추가할 배열과 삭제할 배열을 찾아내야 했다. 이를 위해 집합 개념을 이용했다. 이때 각 원소를 접근해야 했기에 List를 그대로 사용하기보단 HashSet을 이용해 성능 이슈를 최대한 해결하고자 했다.
•
개념 코드
◦
배열의 크기가 크다면 교집합 배열 따로 안 만드는 방식이 더 나을 수 있지만, 우린 tagName 배열 크기가 최대 원소 10개로 제한되어 있으므로 2번 방식 채택함
◦
1번 방법
// 배열 A에만 있는 원소를 찾습니다. A-B
Set<Integer> onlyInA = new HashSet<>(setA);
onlyInA.removeAll(setB);
// 배열 B에만 있는 원소를 찾습니다. B-A
Set<Integer> onlyInB = new HashSet<>(setB);
onlyInB.removeAll(setA);
Java
복사
◦
2번 방법
// 두 배열 모두에 있는 원소를 찾습니다. A ∩ B
Set<Integer> intersection = new HashSet<>(setA);
intersection.retainAll(setB);
// 배열 A에만 있는 원소를 찾습니다. A - (A ∩ B)
Set<Integer> onlyInA = new HashSet<>(setA);
onlyInA.removeAll(intersection);
// 배열 B에만 있는 원소를 찾습니다. B - (A ∩ B)
Set<Integer> onlyInB = new HashSet<>(setB);
onlyInB.removeAll(intersection);
Java
복사
본론
•
개념 코드 적용한 기본 로직
// 현재 태그 목록 조회
List<Tag> currentTags = tagRepository.findByUrl(url);
// 현재 태그 이름 리스트 (A)
Set<String> currentTagNames = new HashSet<>();
for(Tag tag : currentTags) {
currentTagNames.add(tag.getName());
}
// 들어온 태그 이름 리스트 (B)
if(tagNames == null) {
tagRepository.deleteAll(currentTags); // 들어온 태그 이름 리스트가 null인 경우 저장된 태그를 모두 삭제
return;
}
Set<String> incomingTagNames = new HashSet<>(tagNames);
// 존재하는 모든 태그 이름 리스트 -> A ∩ B
Set<String> existingTagNames = new HashSet<>(currentTagNames);
existingTagNames.retainAll(incomingTagNames);
// 삭제되는 태그 이름 리스트 -> A - B -> A - (A ∩ B)
Set<String> tagNamesToDelete = new HashSet<>(currentTagNames);
tagNamesToDelete.removeAll(existingTagNames);
// 추가되는 태그 이름 리스트 -> B - A -> B - (A ∩ B)
Set<String> tagNamesToAdd = new HashSet<>(incomingTagNames);
tagNamesToAdd.removeAll(existingTagNames);
Java
복사
•
가독성 고려해 리팩토링한 로직
public void updateTagList(List<String> tagNames, Url url, Member member) {
// 현재 태그 목록 조회
List<Tag> currentTags = tagRepository.findByUrl(url);
// 들어온 태그 이름 리스트가 빈 배열인 경우 저장된 태그를 모두 삭제하고 early return
if(tagNames.isEmpty()) {
tagRepository.deleteAll(currentTags);
return;
}
// 현재 태그 이름 집합 (A)
Set<String> currentTagNames = Optional.ofNullable(currentTags)
.map(tags -> tags.stream()
.map(Tag::getName)
.collect(Collectors.toSet()))
.orElse(Collections.emptySet());
// 들어온 태그 이름 집합 (B)
Set<String> incomingTagNames = new HashSet<>(tagNames);
// 존재하는 모든 태그 이름 집합 -> A ∩ B
Set<String> existingTagNames = new HashSet<>(currentTagNames);
existingTagNames.retainAll(incomingTagNames);
// 삭제되는 태그 이름 집합 -> A - B -> A - (A ∩ B)
Set<String> tagNamesToDelete = new HashSet<>(currentTagNames);
tagNamesToDelete.removeAll(existingTagNames);
// 추가되는 태그 이름 집합 -> B - A -> B - (A ∩ B)
Set<String> tagNamesToAdd = new HashSet<>(incomingTagNames);
tagNamesToAdd.removeAll(existingTagNames);
// 삭제
tagRepository.deleteAll(
currentTags.stream()
.filter(tag -> tagNamesToDelete.contains(tag.getName()))
.collect(Collectors.toList())
);
// 추가
saveTagList((List<String>) tagNamesToAdd, url, member);
}
Java
복사
Builder 끊어서 사용해 로직 간결하게 만들기
•
builder를 쓰다 보면 분기점에서 아래와 같은 코드를 짜게 되어 반복되는 코드를 쓰게 되는 경우가 종종 있다.
if(...) {
Url url1 = Url.builder()
.name("hi")
.isWatchedLater(true)
.build();
} else {
Url url2 = Url.builder()
.name("hi")
.isWatchedLater(false)
.build();
}
Plain Text
복사
•
이를 아래와 같이 builder 타입 변수를 만들어 리팩토링하면 중복되는 코드를 줄이고 간결하게 만들 수 있다. 이 방법을 검색해 찾아내기가 어려웠다. 관련 글들이 많이 없는 거 같다. 왜 그런진 쓰다 보니 알 수 있었다. 이렇게 쓸 바엔 그냥 정적 팩토리 메소드 방식을 쓰는 게 훨씬 가독성도 좋고 확장성도 좋기 때문이다.
Url.UrlBuilder urlBuilder = Url.builder();
urlBuilder.name("hi");
if(...) {
urlBuilder.isWatchedLater(true);
} else {
urlBuilder.isWatchedLater(false);
}
Url url = urlBuilder.build();
Plain Text
복사
후,, DDD 플젝을 단순 crud라 생각해서 만만하게 봤다가 큰 코 다쳤다 ㅋㅋ
DB가 폴더 구조를 나타내느라 Nested 구조로 이뤄져있어 API 구현도 꽤 챌린지 했다
또, Url의 Tag 기능의 경우 수정 정책이 독특해 재밌는 코드를 짜볼 수 있었다. 개발과 알고리즘의 합작품 느낌?
이외에 Url, Category, Tag 간의 삭제 정책도 잘 따져봐야 했다.