전체 보기
🍀

HashSet 사용, Builder 끊어서 사용

작성일자
2023/08/12
태그
DIARY_DEVELOP
프로젝트
KDKD
책 종류
1 more property

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 간의 삭제 정책도 잘 따져봐야 했다.