private String create() {
int length = 6;
Random random;
try {
random = SecureRandom.getInstanceStrong();
} catch (
NoSuchAlgorithmException e) { //SecureRandom.getInstanceStrong()에서 사용하는 알고리즘을 JVM 에서 지원하지 않을 때
random = new SecureRandom();
}
StringBuilder builder = new StringBuilder();
for (int i = 0; i < length; i++) {
builder.append(random.nextInt(10));
}
return builder.toString();
}
이메일로 인증 코드를 발급하는 기능을 구현하기 전, 인증 코드를 어떤 DB에서 관리할 것인가에 대한 설계 과정이 필요하였다.
인증 코드는 휘발되어도 괜찮은 데이터인가?
개인적으로 인증 코드가 너무 자주 휘발되는 것은 당연히 프로그램의 신뢰성을 떨어뜨릴 것이다.
그러나, 인증코드는 어차피 짧은 시간 동안만 유효하고 혹여 서버 오류로 발급된 코드가 누락되더라도 다시 발급받으면 되는 데이터라고 생각했다.
따라서, 인증코드를 휘발되어도 괜찮은 데이터라고 판단하였다.
로컬 캐시 Vs 글로벌 캐시
인증코드가 휘발되어도 괜찮다는 판단을 내린 후 무엇으로 이를 관리할지 고민을 했을 때, Redis를 생각하였다.
인메모리 기반으로 인증코드를 빠르게 조회할 수 있다.
TTL 기능으로 만료 키 삭제에 대한 별도의 로직이 필요 없다.
분산 구조에 대해 확장성이 뛰어나다는 점과 다중 서버 환경에서 일관성을 유지할 수 있다는 장점도 있지만 현재 프로젝트는 분산 시스템은 고려하지 않고 있었기 때문에 우선 해당 장점은 고려하지 않았다.
??? : 로컬 캐시를 사용해 보는 건 어떤가요
인증코드를 Redis를 사용해서 관리하려고 한다는 계획을 밝혔을 때, 한 팀원 분이 그렇다면 로컬 캐싱을 사용하는 것은 어떤가라고 의견을 제시해 주셨다.
로컬 캐시(ehcache, caffeine cache)에 대한 내용을 찾아봤을 때, Redis 선택 시 고려했던 TTL 같은 기능도 있었고 글로벌 캐시보다 조회속도가 더 빠르다는 장점이 있었다. 위와 같이 인증코드의 특성상 휘발되어도 괜찮기 때문에 로컬 캐시를 사용해도 괜찮을 것 같다는 생각이 들었다.
??? : 여러분은 CI 관점을 놓치고 있습니다
기술 사용에 대한 의사결정이 타당한지 튜터님의 의견을 구하였다. 튜터님께서는 우리가 고려한 내용이 모두 맞지만 지속적인 배포(CI) 관점을 놓치고 있다는 조언을 해주셨다.
지속적인 배포의 관점에서 로컬 캐시와 글로벌 캐시가 어떤 시점에 데이터가 휘발되는지를 놓치고 있다.
최근에는 지속적인 배포를 중시한다. 어떤 기업은 1시간에 6번을 배포하는 경우도 있다.
이때, 로컬 캐시를 사용하게 되면 배포를 할 때마다 데이터가 휘발된다. 반면, 글로벌 캐시는 Redis 서버가 다운되지 않는다면 배포를 할 때마다 데이터가 휘발되지는 않는다.
따라서 우리는 처음 생각했던 방법인 글로벌 캐시 Redis로 발급된 인증코드를 관리하기로 하였다.
//문제은행 도메인 주소
final String domain = "https://cbtbank.kr";
//문제은행에서 정보처리기사 문제 목록이 있는 페이지
final String listCbtUrl = domain + "/category/정보처리기사";
Document listPage = Jsoup.connect(listCbtUrl).get();
Elements links = listPage.select("a[href^=/exam/]");
List<String> examUrls = new ArrayList<>();
for (Element link : links) {
String relativeUrl = link.attr("href");
String fullUrl = domain + relativeUrl;
examUrls.add(fullUrl);
}
정보처리기사 회차별 문제 링크 HTML 구성을 바탕으로 <a herf = "/exam/">에 속하는 요소들을 Element 객체로 가져와 내부 속성인 url만 추출하여 아래와 같은 링크 주소들을 리스트에 저장하였다.
문제 가져오기
public static ParsedQuiz parseQuiz(Element box) {
ParsedQuiz pq = new ParsedQuiz();
String rawTitle = box.selectFirst("p.exam-title").text();
pq.question = rawTitle.replaceFirst("^\\d+\\.", "").trim();
Elements liItems = box.select("ol.circlednumbers > li");
for (int i = 0; i < liItems.size(); i++) {
String choiceText = liItems.get(i).text();
pq.choices += choiceText;
if (liItems.get(i).hasClass("correct")) {
pq.answer = (i + 1) + "." + choiceText;
}
}
Elements replies = box.select("li.reply-item");
for (Element reply : replies) {
if (reply.attr("data-info").contains("depth:0")) {
Element comment = reply.selectFirst("div.reply-comment");
if (comment != null) {
pq.commentary = comment.text();
break;
}
}
}
return pq;
}
위와 같은 원리로 HTML 문을 분석하고 필요한 데이터를 추출하였다. 출력을 통해 데이터를 확인하면 아래와 같은 결과가 나온다.
크롤링한 데이터를 필요한 형태로 Json 파일 만들기
사실 크롤러를 프로젝트 내에 구현하여 크롤링한 데이터를 동일한 프로젝트 내에서 바로 적용할 수도 있었을 것이다. 그러나 나는 이를 별도의 프로젝트에서 구현하였다. 그 이유는 크롤링한 문제의 중복 제거에 있다.
중복된 문제 제거 방법 (feat. 고마워요 G선생)
프로젝트 설계 단계에서 우리 팀은 크롤링한 문제의 중복을 제거하는 로직을 어떻게 짜면 좋을까 하는 고민을 하였고, 이에 대해 튜터님께 조언을 구하였다.
중복 제거는 AI가 잘 제거해 줍니다.
그 결과, 크롤링은 자동으로 하고 중복은 AI가 한 후 중복이 제거된 파일을 DB에 등록하는 일종의 반자동 시스템이 된 것이다.
크롤링한 데이터를 Json 파일에 저장
Json 파일로 저장하기 위한 라이브러리를 찾아보니 gson과 Jackson이 있었다. gson과 jackson은 Json 문자열과 java 객체 간의 변환을 쉽게 해 주어 Json 데이터 처리에 사용되는 대표적인 라이브러리이다. gson은 Google이 개발하였고 jackson은 FasterXML이 개발하였다.
gson Vs Jackson
gson은 google이 개발한 라이브러리답게 쉽고 간단하다는 것이 주요 장점이다.
장점
쉽게 사용할 수 있다.
코드가 간결하다.
구성이 가볍다
그러나 Jackson에 비해 성능이 다소 떨어지고, 기능이 제한적이라는 단점이 있었다.
Jackson의 장점은 성능이 더 뛰어나고 Spring과 호환이 잘 된다는 것이 주요 장점이다.
두 라이브러리의 장단점을 본 후, 현재 크롤러 프로젝트에서는 크게 중요하게 고려할 만한 부분은 없었지만 문제의 개수가 매우 많을 수 있다는 점과 추후 혹시 모를 Spring 사용을 고려할 때, 호환성이 높고 대용량 데이터 처리에 더 적합한 Jackson 라이브러리를 사용하기로 하였다.
Json 파일로 저장
어떤 라이브러리를 사용할지 고민한 것에 비해 실제 크롤러에서 Json 파일로 저장하는 코드는 굉장히 간단하다.
ObjectMapper mapper = new ObjectMapper();
mapper.writerWithDefaultPrettyPrinter().writeValue(new File("quizzes.json"), quizzes);
그 결과 Json에 저장된 파일은 다음과 같다.
이렇게 Json 파일로 뽑아내어 GPT를 통해 중복을 제거한 문제를 어떻게 DB에 저장했는지는 다음 글에서 다루도록 하겠다!