diff --git a/src/main/java/com/seong/shoutlink/domain/common/Trie.java b/src/main/java/com/seong/shoutlink/domain/common/Trie.java index 395e1b5..d7ce272 100644 --- a/src/main/java/com/seong/shoutlink/domain/common/Trie.java +++ b/src/main/java/com/seong/shoutlink/domain/common/Trie.java @@ -1,46 +1,48 @@ package com.seong.shoutlink.domain.common; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import lombok.Getter; @Getter public class Trie { - @Getter static class Node { private final char c; - private final Map children = new HashMap<>(); + private final Map children = new ConcurrentHashMap<>(); private boolean isWord; public Node(char c) { this.c = c; } - public void setWord(boolean word) { - isWord = word; + public void settingWord() { + isWord = true; } public boolean hasChildren(char c) { return children.containsKey(c); } - public boolean isWord() { - return isWord; + public Node nextNode(char c) { + Node nextNode = children.putIfAbsent(c, new Node(c)); + return Optional.ofNullable(nextNode) + .orElseGet(() -> children.get(c)); } public void addSuggestions(String word, List suggestions, int count) { + if(isWord) { + suggestions.add(word); + } + if(suggestions.size() >= count) { + return; + } children.forEach((character, childNode) -> { String suggestionsWord = word + character; - if (childNode.isWord()) { - suggestions.add(suggestionsWord); - } - if (suggestions.size() >= count) { - return; - } childNode.addSuggestions(suggestionsWord, suggestions, count); }); } @@ -53,22 +55,19 @@ public void addSuggestions(String word, List suggestions, int count) { private final Node root = new Node(' '); - public synchronized void insert(String word) { - if(word.length() > MAX_WORD_LENGTH) { + public void insert(String word) { + if (word.length() > MAX_WORD_LENGTH) { return; } - Node currentNode = root; for (char c : word.toCharArray()) { - Map children = currentNode.getChildren(); - currentNode = children.getOrDefault(c, new Node(c)); - children.put(c, currentNode); + currentNode = currentNode.nextNode(c); } - currentNode.setWord(true); + currentNode.settingWord(); } public List search(String prefix, int count) { - if(prefix.length() > MAX_PREFIX_LENGTH) { + if (prefix.length() > MAX_PREFIX_LENGTH) { prefix = prefix.substring(ZERO, MAX_PREFIX_LENGTH); } Node currentNode = root; @@ -76,17 +75,13 @@ public List search(String prefix, int count) { if (!currentNode.hasChildren(c)) { return List.of(); } - Map children = currentNode.getChildren(); - currentNode = children.get(c); + currentNode = currentNode.nextNode(c); } return findWords(prefix, currentNode, Math.min(MAX_SUGGESTION, count)); } private List findWords(String word, Node currentNode, int count) { List suggestions = new ArrayList<>(); - if (currentNode.isWord()) { - suggestions.add(word); - } currentNode.addSuggestions(word, suggestions, count); return suggestions; } diff --git a/src/main/java/com/seong/shoutlink/domain/domain/repository/DomainRepositoryImpl.java b/src/main/java/com/seong/shoutlink/domain/domain/repository/DomainRepositoryImpl.java index e1e65a0..116a5a6 100644 --- a/src/main/java/com/seong/shoutlink/domain/domain/repository/DomainRepositoryImpl.java +++ b/src/main/java/com/seong/shoutlink/domain/domain/repository/DomainRepositoryImpl.java @@ -29,8 +29,10 @@ public Optional findByRootDomain(String rootDomain) { @Override public Domain save(Domain domain) { - return domainJpaRepository.save(DomainEntity.create(domain)) + Domain savedDomain = domainJpaRepository.save(DomainEntity.create(domain)) .toDomain(); + domainCacheRepository.insert(savedDomain.getRootDomain()); + return savedDomain; } @Override @@ -38,7 +40,6 @@ public List findRootDomains(String keyword, int size) { return domainCacheRepository.findRootDomains(keyword, size); } - @Override public void synchronizeRootDomains() { domainJpaRepository.findRootDomains().forEach(domainCacheRepository::insert); } diff --git a/src/main/java/com/seong/shoutlink/domain/domain/service/DomainRepository.java b/src/main/java/com/seong/shoutlink/domain/domain/service/DomainRepository.java index 701d5d5..ebf89a9 100644 --- a/src/main/java/com/seong/shoutlink/domain/domain/service/DomainRepository.java +++ b/src/main/java/com/seong/shoutlink/domain/domain/service/DomainRepository.java @@ -14,8 +14,6 @@ public interface DomainRepository { List findRootDomains(String keyword, int size); - void synchronizeRootDomains(); - DomainPaginationResult findDomains(String keyword, int page, int size); Optional findById(Long domainId); diff --git a/src/main/java/com/seong/shoutlink/global/config/EventConfig.java b/src/main/java/com/seong/shoutlink/global/config/EventConfig.java index 0e04c32..c8b1f9b 100644 --- a/src/main/java/com/seong/shoutlink/global/config/EventConfig.java +++ b/src/main/java/com/seong/shoutlink/global/config/EventConfig.java @@ -1,6 +1,7 @@ package com.seong.shoutlink.global.config; import com.seong.shoutlink.domain.common.EventPublisher; +import com.seong.shoutlink.domain.domain.repository.DomainRepositoryImpl; import com.seong.shoutlink.domain.domain.service.DomainUseCase; import com.seong.shoutlink.domain.link.service.LinkBundleUseCase; import com.seong.shoutlink.domain.tag.service.TagUseCase; @@ -26,8 +27,10 @@ public LinkBundleEventListener linkBundleEventListener(LinkBundleUseCase linkBun } @Bean - public DomainEventListener domainEventListener(DomainUseCase domainUseCase) { - return new DomainEventListener(domainUseCase); + public DomainEventListener domainEventListener( + DomainUseCase domainUseCase, + DomainRepositoryImpl domainRepository) { + return new DomainEventListener(domainUseCase, domainRepository); } @Bean diff --git a/src/main/java/com/seong/shoutlink/global/config/SchedulerConfig.java b/src/main/java/com/seong/shoutlink/global/config/SchedulerConfig.java index 94460e8..e8a9bca 100644 --- a/src/main/java/com/seong/shoutlink/global/config/SchedulerConfig.java +++ b/src/main/java/com/seong/shoutlink/global/config/SchedulerConfig.java @@ -1,6 +1,6 @@ package com.seong.shoutlink.global.config; -import com.seong.shoutlink.domain.domain.service.DomainRepository; +import com.seong.shoutlink.domain.domain.repository.DomainRepositoryImpl; import com.seong.shoutlink.global.scheduler.DomainScheduler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -11,7 +11,7 @@ public class SchedulerConfig { @Bean - public DomainScheduler domainScheduler(DomainRepository domainRepository) { + public DomainScheduler domainScheduler(DomainRepositoryImpl domainRepository) { return new DomainScheduler(domainRepository); } } diff --git a/src/main/java/com/seong/shoutlink/global/event/DomainEventListener.java b/src/main/java/com/seong/shoutlink/global/event/DomainEventListener.java index 93746f6..8e227a9 100644 --- a/src/main/java/com/seong/shoutlink/global/event/DomainEventListener.java +++ b/src/main/java/com/seong/shoutlink/global/event/DomainEventListener.java @@ -1,9 +1,12 @@ package com.seong.shoutlink.global.event; +import com.seong.shoutlink.domain.domain.repository.DomainRepositoryImpl; import com.seong.shoutlink.domain.domain.service.DomainUseCase; import com.seong.shoutlink.domain.domain.service.request.UpdateDomainCommand; import com.seong.shoutlink.domain.link.service.event.CreateLinkEvent; import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.event.TransactionalEventListener; @@ -12,10 +15,16 @@ public class DomainEventListener { private final DomainUseCase domainUseCase; + private final DomainRepositoryImpl domainRepository; @TransactionalEventListener @Transactional(propagation = Propagation.REQUIRES_NEW) public void updateDomainInfo(CreateLinkEvent event) { domainUseCase.updateDomain(new UpdateDomainCommand(event.linkId(), event.url())); } + + @EventListener(ApplicationReadyEvent.class) + public void warmUpCache() { + domainRepository.synchronizeRootDomains(); + } } diff --git a/src/main/java/com/seong/shoutlink/global/scheduler/DomainScheduler.java b/src/main/java/com/seong/shoutlink/global/scheduler/DomainScheduler.java index 70aee82..fcb3386 100644 --- a/src/main/java/com/seong/shoutlink/global/scheduler/DomainScheduler.java +++ b/src/main/java/com/seong/shoutlink/global/scheduler/DomainScheduler.java @@ -1,13 +1,13 @@ package com.seong.shoutlink.global.scheduler; -import com.seong.shoutlink.domain.domain.service.DomainRepository; +import com.seong.shoutlink.domain.domain.repository.DomainRepositoryImpl; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.annotation.Scheduled; @RequiredArgsConstructor public class DomainScheduler { - private final DomainRepository domainRepository; + private final DomainRepositoryImpl domainRepository; @Scheduled(cron = "0 0 * * * *") public void synchronizeRootDomains() { diff --git a/src/test/java/com/seong/shoutlink/base/BaseIntegrationTest.java b/src/test/java/com/seong/shoutlink/base/BaseIntegrationTest.java index 04ddcd7..2aa817e 100644 --- a/src/test/java/com/seong/shoutlink/base/BaseIntegrationTest.java +++ b/src/test/java/com/seong/shoutlink/base/BaseIntegrationTest.java @@ -5,7 +5,7 @@ import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -public class BaseIntegrationTest { +public abstract class BaseIntegrationTest { @Autowired private DatabaseCleaner databaseCleaner; diff --git a/src/test/java/com/seong/shoutlink/domain/domain/repository/DomainRepositoryImplTest.java b/src/test/java/com/seong/shoutlink/domain/domain/repository/DomainRepositoryImplTest.java new file mode 100644 index 0000000..7870049 --- /dev/null +++ b/src/test/java/com/seong/shoutlink/domain/domain/repository/DomainRepositoryImplTest.java @@ -0,0 +1,36 @@ +package com.seong.shoutlink.domain.domain.repository; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.seong.shoutlink.base.BaseIntegrationTest; +import com.seong.shoutlink.domain.domain.Domain; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class DomainRepositoryImplTest extends BaseIntegrationTest { + + @Autowired + private DomainRepositoryImpl domainRepository; + + @Nested + @DisplayName("save 호출 시") + class SaveTest { + + @Test + @DisplayName("성공: 루트 도메인 문자열이 캐싱된다.") + void cachingRootDomain() { + //given + Domain domain = new Domain("asdf"); + + //when + domainRepository.save(domain); + + //then + List rootDomains = domainRepository.findRootDomains("as", 10); + assertThat(rootDomains).containsExactly("asdf"); + } + } +} diff --git a/src/test/java/com/seong/shoutlink/domain/domain/repository/StubDomainRepository.java b/src/test/java/com/seong/shoutlink/domain/domain/repository/StubDomainRepository.java index 7d5585a..7af6702 100644 --- a/src/test/java/com/seong/shoutlink/domain/domain/repository/StubDomainRepository.java +++ b/src/test/java/com/seong/shoutlink/domain/domain/repository/StubDomainRepository.java @@ -76,11 +76,4 @@ public DomainLinkPaginationResult findDomainLinks(Domain domain, int page, int s public List findRootDomains(String keyword, int size) { return searchAutoComplete.search(keyword, size); } - - @Override - public void synchronizeRootDomains() { - for (Domain domain : memory.values()) { - searchAutoComplete.insert(domain.getRootDomain()); - } - } }