레시피 검색 시 블로그 검색 외부 API 를 호출하고 있었다.
블로그 검색 결과를 보여주기 위해서 썸네일이 필요했는데 외부 API 에서는 썸네일을 따로 전달해주지 않고 있었다.
이를 해결하기 위해 Jsoup 라이브러리를 활용해서 해당 url을 크롤링해서 썸네일 이미지를 추출하도록 했다.
그러나 크롤링 과정으로 인해 검색 결과를 보여줄 때 10초 정도 걸리면서 성능적으로 문제가 발생했다. (크롤링 과정이 10초 중 9초에 해당함)
사용자들에게 10초는 사실상 오류가 있다고 인지할만한 길이이고 로딩 화면을 보여준다고 해도 내부적으로 해결이 필요한 문제라고 생각했다.
검색 결과와 함께 썸네일을 보여주는 게 좋겠지만 없어도 기본 설정 이미지가 존재하고 제목과 설명을 같이 보여주고 있으므로 심각한 오류로 인식되진 않겠다고 생각했다.
그리고 크롤링한 결과를 저장한 이후에는 제대로 된 썸네일을 볼 수 있을테니 썸네일을 크롤링해서 저장하는 부분을 비동기로 처리하고자 했다.
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncExecutor-");
executor.initialize();
return executor;
}
}
@EnableAsync 어노테이션을 Application 클래스 위에 붙이는 방법도 존재@Async의 기본 설정은 SimpleAsyncTaskExecutor를 사용하도록 되어 있는데, 이것은 스레드 풀이 아니고 단순히 스레드를 만들어내는 역할을 하여 스레드를 관리할 수 없음@EnableAsync를 붙인 후, 스레드 설정 추가@Service
public class BlogRecipeThumbnailCrawlingService {
private final BlogRecipeRepository blogRecipeRepository;
public BlogRecipeThumbnailCrawlingService(BlogRecipeRepository blogRecipeRepository) {
this.blogRecipeRepository = blogRecipeRepository;
}
@Async
public void saveThumbnails(List<BlogRecipe> blogRecipes) {
System.out.println("thumbnail save");
for (BlogRecipe blogRecipe : blogRecipes) {
blogRecipe.changeThumbnail(getBlogThumbnailUrl(blogRecipe.getBlogUrl()));
}
blogRecipeRepository.saveAll(blogRecipes);
}
public String getBlogThumbnailUrl(String blogUrl) {
if (blogUrl.contains("naver")) {
return getNaverBlogThumbnailUrl(blogUrl);
} else if (blogUrl.contains("tistory")) {
return getTistoryBlogThumbnailUrl(blogUrl);
} else {
return "";
}
}
private String getNaverBlogThumbnailUrl(String blogUrl) {
try {
URL url = new URL(blogUrl);
Document doc = Jsoup.parse(url, 5000);
Elements iframes = doc.select("iframe#mainFrame");
String src = iframes.attr("src");
String url2 = "http://blog.naver.com" + src;
Document doc2 = Jsoup.connect(url2).get();
return doc2.select("meta[property=og:image]").get(0).attr("content");
} catch (Exception e) {
return "";
}
}
private String getTistoryBlogThumbnailUrl(String blogUrl) {
try {
Document doc = Jsoup.connect(blogUrl).get();
Elements imageLinks = doc.getElementsByTag("img");
String thumbnailUrl = null;
for (Element image : imageLinks) {
String temp = image.attr("src");
if (!temp.contains("admin")) {
thumbnailUrl = temp;
break;
}
}
return thumbnailUrl;
} catch (Exception e) {
return "";
}
}
}
Jsoup 을 이용하여 썸네일 추출을 위한 크롤링 작업 진행, 존재하지 않는 경우 빈 값 전달
비동기적으로 작동하게 하고 싶은 메소드에 @Async 어노테이션 적용