-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
검색어 자동 완성 API 개발 및 성능 개선 (#95) #102
검색어 자동 완성 API 개발 및 성능 개선 (#95) #102
Conversation
} | ||
|
||
private void saveAllSubstring(List<String> allDisplayName) { | ||
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); //병렬 처리를 위한 스레드풀을 생성하는 과정 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
availableProcessors()
를 통해 현재 가용가능한 스레드풀을 가져오는걸로 알고있습니다. 이 작업이 맨처음에 실행되는걸로 알고있는데, 그러면 10개가 가용가능하다면 10개를 모두 사용할거고, 이 시간동안에는 다른 작업이 불가능한건가요? 현재는 이 작업이 매우 짧은시간내에 이루어져 큰 영향은 없어보이지만, 호기심 삼아 물어봅니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
우선 굉장히 좋은 고민 거리를 주셔서 감사합니다. 제가 처음에 구현했을 때는 스레드풀을 만든다고 해서 모든 스레드를 점유해서 사용한다기보다는 공유해서 사용할 것이라고 너무 당연하게만 생각했었는데 이에 대한 근거가 부족한 것 같아서 많이 찾아봤습니다.
일단 nexFixedThreadPool() 메서드를 뜯어보면 뭔가 답이 나오지않을까해서 들어가봤는데 저기 인자로 넘겨준 availableProcessors()이 corePoolSize, maximumPoolSize의 값으로 들어가더군요. corePoolSize는 초기 Pool 내 thread 값이고 maximumPoolSize는 Pool에 예약된 task가 많은 경우 최대로 늘어날 수 있는 thread 수 였습니다.
이것만으로는 ThreadPool이 모든 Thread를 점유하는 것인지 아닌지에 대한 근거를 찾을 수 없어서 제가 JVM 관련된 debug 로그를 모두 켜놓고 아래와 같이 직접 실행시켜봤습니다.

하이라이트 되어있는 StoreCertification 관련된 부분은 완전히 다른 서비스 단에서 PostConstruct 애너테이션을 활용한 로직 내 실행되는 내용입니다. 그리고 lettuce.core에서 처리하고 있는 ZADD와 같은 로직들은 이번에 개발한 검색어 자동 완성 기능을 위해 Redis에 쿼리를 실행시키는 로그입니다.
이를 보고나니 JVM 자체가 멀티 스레드 기반이고 기대한 것처럼 ThreadPool을 사용한다해서 그 스레드를 모두 점유해서 사용하는 것이 아닌 공유해서 사용하는 것으로 확인되었습니다~
저도 다시 한번 고민해볼 수 있는 좋은 피드백 주셔서 감사합니다 ㅎㅎ
|
||
@Value("${spring.data.redis.port}") | ||
private int port; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dev, prod 는 보안상 접속시에 비밀번호를 사용하고 있는데 비밀번호 설정이 필요해보입니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dev, prod 환경에서 비밀번호를 사용하고 있으니 Spring 프로젝트 내 Redis관련한 Config에서 password 사용하도록 변경해달라는 말씀이신거죠? ㅎㅎ
일단 수정 반영해놓았으니 확인 부탁드립니다!
⭐️ Issue Number
🚩 Summary
사용자들이 검색 기능을 사용할 때 더욱 편리하게 검색할 수 있도록 검색 키워드를 입력받는 중에 자동으로 해당 키워드와 매칭될만한 검색어를 완성해서 추천해주는 검색어 자동 완성 API를 개발합니다.
매우 빈번하게 조회 관련 쿼리가 발생하는 성능이 중요한 API인지라 조회 성능이 뛰어난 인메모리 DB인 Redis를 구축하여 사용하였습니다.
가게명에 대한 검색어 자동 완성 기능이기 때문에 StoreService 단에 PostConstruct 애너테이션 활용해서 자동 완성 기능을 위한 초기 데이터들을 Redis에 넣어두는 로직을 구현해 놓았습니다.
[검색어 자동 완성 Flow]
이번 PR을 구현하고 Refactoring하면서 굉장히 많은 기술적인 경험을했는데 이와 관련된 내용을 제 개인 기술 블로그에 총 3개의 포스팅으로 분할하여 작성하였습니다.
Spring Project에서 158초 걸리던 Batch 작업을 병렬 처리하여 0.009초로 줄여본 이야기
Spring 프로젝트에서 Redis를 사용하여 빠른 검색어 자동 완성 구현하기 (1)
Spring 프로젝트에서 Redis를 사용하여 빠른 검색어 자동 완성 구현하기 (2)
🛠️ Technical Concerns
검색어 자동 완성 로직
위와 같은 흐름으로 구현한다면 쉽게 자동 완성 로직을 구현할 수 있을 것만 같았습니다만, 여기서 몇가지 주의해야 될 부분이 있었습니다.
우선 첫째로, 모든 단어들을 음절 단위로 쪼개놓았기 때문에 불완전한 형태의 키워드들이 함께 조회됩니다. 예를 들어 ‘아이폰’이라는 값이 조회 대상으로 DB에 저장되어 있어서 이를 음절 단위로 쪼개 모두 저장한다고 치면 [아, 아이, 아이폰] 이런 식으로 쪼개져 저장될 것입니다. 이때 사용자가 ‘아’까지만 검색한다면 ‘아’, ‘아이’, ‘아이폰’이 모두 자동 완성 검색어로 노출될 것입니다. ‘아’가 노출되서는 안된다는 것은 너무나 자연스럽게 이해가 되지만 ‘아이’가 노출되는 것은 괜찮지 않느냐라고 생각하실 수도 있습니다. 하지만 ‘아이’는 자동 완성으로는 노출되서는 안되는 키워드입니다.
왜냐하면 처음 DB에 저장되어 조회 대상이 되었던 이름은 ‘아이폰’이었고 이 ‘아이폰’의 ‘아이’라는 글자와 ‘아이’라는 단어만 놓고 봤을 때 바로 떠오르는 단어와는 관련이 없기 때문입니다. 물론 ‘아이폰’이라는 키워드 외에 다른 단어들과 같은 경우, 특히 합성어 같은 경우에 음절 단위로 끊어 단어의 일부분만 잘라봤을 때 말도 되고 기존 단어와 문맥도 일치하는 단어가 존재할 수도 있습니다만 이 부분까지 고려하기에는 생각해야되는 부분이 너무 많아지기 때문에 저는 처음 조회 대상이 되는 완벽한 단어만 자동 완성 키워드로 노출되도록 설정해줄 것입니다. 이 로직은 완벽한 단어 뒤에 ‘*‘과 같은 문자를 두어 함께 저장하고 추후에 비즈니스 로직에서 ‘*‘가 포함되어 있는 단어만 자동 완성 검색어로 노출시켜주는 방식으로 간단히 구현했습니다.
둘째로, 음절 단위로 저장된 데이터 간의 경계값에 주의해야된다는 점입니다. 보통 이런 자동 완성 기능을 구현하게 되면 최대 자동 완성 검색어 노출 개수를 지정해서 노출하게 될텐데 아무리 오름차순으로 데이터가 정렬되어 있다고 하더라도 Limit 이내에 완전 다른 뜻을 가지는 경계값과 같은 단어가 뒤이어 존재하게 되면 어색한 검색어 예측을 하게 됩니다. 이런 단어들이 사용자에게 노출되면 사용자는 당연하게도 검색어 자동 완성 기능에 신뢰를 잃게 될 것입니다. 이를 막기 위해서 다양한 방식이 사용될 수 있지만, 저는 가장 심플하게 사용자가 입력한 검색어가 포함되는 경우에만 자동 완성 검색어로 노출될 수 있도록 구현하였습니다.
대규모 Batch Job 병목 이슈
위 코드가 바로 병목이 걸려 문제가 발생하던 코드인데 간단히 상황 설명을 드리면 다음과 같습니다.
검색어 자동 완성 기능 구현을 위해 MySQL DB에 저장된 모든 가게명에 대해서 음절 단위로 1글자씩 잘라낸 뒤 모든 Substring을 Redis에 저장해두는 1회성 Batch Job이 필요했고 그 로직 중 일부가 위 코드입니다. 참고로 저는 위 로직을 가게와 관련된 서비스 단인 StoreService에 대한 Bean이 만들어진 직후 딱 1번만 Atomic하게 실행시키기 위해 PostConstruct 애너테이션을 활용하여 위 로직을 StoreService의 init() 로직에서 실행시켰습니다.
저희 Production DB에 총 가게가 약 51200개 정도 있고 가게 이름이 평균 6글자 정도라고 가정하면, 총 51200*6 = 307200번 정도의 Redis 연산이 순차적으로 실행되는 결과를 낳게됩니다. 그 결과 약 158초 정도의 시간이 걸렸고 일회성 Job이긴 하지만 이에 대한 개선이 반드시 필요하다고 생각이 들었습니다.
그래서 결국 Redis Pipelining과 Multi threads 기반의 병렬 프로그래밍을 적용시켜보며 성능 분석을 진행한 결과 병렬 처리를 했을 때 가장 짧은 시간인 0.009초로 개선할 수 있었습니다. 이에 대한 내용 역시 위에 제 기술 블로그에 잘 정리해놓았습니다.
📋 To Do