본문 바로가기
SAN's history/kernel360

[kernel360] E2E Project 회고

by san.d 2024. 4. 4.

무려 5개월 전, 한 달 동안 했던 다 식어버린 E2E 회고..... 150일 후에 되돌아보기......

2023.10.24(화) ~ 2023.11.24(금), 5주

Front-End부터 Back-End까지의 구현 기술을 협업으로 경험해 보는 프로젝트로 기획, 설계 개발, 배포까지 구현한다. 웹서비스 전체를 구현하는 것을 목표로 서비스 요청 WAS와 DB를 활용한 처리 서버의 응답을 화면에 보여주기까지 구현하고 이를 배포하도록 한다.

Kernel Engine 

가입한 유저의 블로그 게시글과 관리자가 등록한 기술 블로그의 게시글을 키워드를 통해서 검색할 수 있는 검색엔진 서비스

운영체제의 그 커널 아니고요.... 커널360의 커널입니다.... 예예...ㅜㅜ 이름 달리 지을걸...

기획의도 

운이 좋게도 내가 만들고 싶었던 서비스가 채택되어 한번 더 팀장으로서 프로젝트를 이끌게 되었다. 

요즘은 많은 회사들이 개발 기록을 블로그로 유의미하게 남기고 있다. 그리고 kernel360의 크루들도 대부분 기술 블로그를 운영하고 있다. 내가 궁금한 기술을 적용해 본 회사들의 기록이나, 크루들의 기록을 쉽게 찾아보고 싶었고 그래서 이 서비스를 기획하게 되었다. 

운영자가 등록한 회사들의 기술 블로그와 사용자들이 등록한 기술 블로그를 기반으로 개발 관련 검색어를 서칭하고자 했다. 

기능

  • 키워드를 통해서 원하는 토픽이 들어간 게시글을 검색할 수 있다. 
  • 최근 많이 조회된 기술 블로그 리스트를 확인할 수있다. 

깃헙 

서비스의 erd나 아키텍처 같은 것은 github readme에 잘 정리해 놨기 때문에 레포 주소를 남기는 것으로 대신한다. 

https://github.com/anso33/kernel-engine


내가 KernelEngine에서 한 일

 리더 

바로 이전 프로젝트에서 생각했던 리더로서 개선사항은 다음과 같았다. 

그래서 앞으로 만약에 팀장이 되면 스프린트 주기를 총 프로젝트 시간 고려해서 현명하게 가져가서 팀원들이 적당한 성취감과 목표를 가질 수 있도록 하고, 진행 상황이 보이는 협업툴(github issue, project, pr)을 100% 활용하도록 독려해야겠다는 생각을 했다.

 

커널엔진 프로젝트를 하면서는 개선을 잘했다고 스스로 평가한다.  왜... 뭐요...ㅎ

1. 스크럼

 

프로젝트는 5주로 짧은 시간안에 화면부터 서버까지 모두 완성해야 했다. 정해진 시간 안에 계획한 기능을 모두 완성하려면 팀원들이 늘어지지 않도록 매일 목표 설정을 할 필요가 있다고 생각했고 이를 위한 도구로 스크럼을 사용했다. 

다만 개발 시간을 뺏지 않도록 최대한 간결하게, 정말 중요한 이슈와 오늘 할일은 무엇인지 정도를 매일 일과 시작 전에 10분 정도 공유하는 시간을 가졌다. 

2. 스프린트 회의, 마일스톤

스프린트 회의는 주 2회 정도로 스프린트 기간동안 할 일을 나누고 완료 상태를 공유하는 것이 주목적이었다. 

이때 스프린트마다 목표했던 Task의 완료상태를 한눈에 볼 수 있도록 마일스톤을 이용하여 아래와 같이 관리하였다. 

 

스프린트 회의에서 task에 대한 작업만 한 것은 아니다. 우리는 모두 처음 만나는 사람들이고 짧은 시간 안에 완성도 있는 서비스를 만들기 위해서는 팀원들의 성향을 파악하고 팀워크를 높을 수 있는 장치가 필요했다. 그래서 주 2회의 스프린트 회의 중 1회는 회고 시간을 가져서 나를 비롯한 팀원들이 이번 스프린트 동안 작업을 하면서 팀에 원하는 방향이 있는지, 불편한 점이 있었는지 공유하도록 했다. 물론 말한다고 다 적용할 수는 없지만 적어도 누가 무엇을 원하는지, 어떤 사람인지 보다 빠르게 파악할 수 있었다고 생각하고 나 나름대로는ㅎ 팀원들이 팀에 큰 불만없이 각자 원하는 것을 할 수 있는 환경 조성에 도움이 되었다고 생각한다. 

 

3. 자세한 Issue와 PR

맡은 도메인에 대해서 해야할 작업이 생겼다면 어떤 이유로 하고자 하는지 이슈를 생성하여 설명하도록 했다. 주말이나, 집에서 작업을 하더라도 이런 식으로 하면 왜 이 작업을 진행해야 하는지, 혹은 다른 사람과 하는 일이 겹치진 않는지, 필요 없는 일은 아닌지 다른 팀원들도 확인할 수 있고 무엇보다 해당 작업에 대한 기록이 남기 때문이다. 

 

작업을 마무리 했을 때는 아래와 같이 왜, 무엇을, 그래서 결과가 무엇인지를 자세하게 설명하여 팀원들이 어떤 코드인지 이해하고 리뷰할 수 있도록 했다. 


크롤러...라고 하기엔 rss 파싱기 제작기

커널엔진은 개발 블로그를 대상으로 검색어에 맞는 포스팅을 찾아주는 서비스이다. 커널엔진 자체 검색을 위해서는 등록된 포스팅에 대한 데이터를 가지고 있어야 한다. 따라서 우리는 등록된 블로그를 대상으로 크롤링을 하고자 했다. 

그렇지만 곧바로 멘붕과 좌절에 빠지는데.... 

 

바로 개발 블로그의 플랫폼이 tistory, velog, medium 부터 github까지, 이 외에도 다양한 종류가 있었고 심지어 같은 플랫폼에서도 사용자가 custom 한 블로그들이 존재한다는 것이었다..... 언제 다 크롤링해..... 아무리 머리를 싸매봐도 각 블로그마다 그에 맞는 custom crawler를 개발해야한다는 결론으로 수렴했다. 하지만 우리에게 주어진 시간은 한 달뿐..... 이때 우리는 rss라는 형식을 알게 되었다!

RSS란? 
RSS(Rich Site Summary)는 뉴스나 블로그 사이트에서 주로 사용하는 콘텐츠 표현 방식으로 웹 사이트 관리자는 RSS 형식으로 웹 사이트 내용을 보여줄 수 있다. 

즉, 정해진 태그로 내용을 나눠서 줄 수 있음.

출처 [ https://ko.wikipedia.org/wiki/RSS ]

 

보통 기술 블로그들은 rss 형식을 제공하고 있기 때문에 우리가 충분히 활용할 수 있었다. 우리는 rss 소스를 읽어오는 대신 커널엔진의 정책을 블로그를 등록하고자 하는 사용자는 개인 기술 블로그에서 rss 형식을 제공하도록 하고 rss 주소를 등록하도록 했다. 

 

물론!! rss 형식을 사용한다고 단 하나의 크롤러로 모든 블로그의 정보를 가져올 수 있는 것은 아니다. 블로그마다 Tag가 다른 경우도 존재하기 때문이다. 하지만 html 소스를 읽어오는 것보다는 훨씬 정형화되어 있어 변화에 대처하기 쉬웠기 때문에 최종적으로 rss 소스를 사용하고자 했다. 

 

rss 크롤러를 만들면서 목표를 다음과 같이 잡았다. 

1. 웬만큼 정형화된 tag 형식에 적용할 수 있는 대표 크롤러를 둔다.      

  • 블로그마다 제목이나 발행일, 본문 같은 내용을 각자 다른 rss 태그를 사용하여 저장하기도 하지만 그래도 85% 정도의 블로그가 2가지 형식을 공통적으로 쓰고 있었다!

2. tag만 등록하여도 custom rss crawler를 최대한 쉽고 빠르게 만들 수 있도록 지원하는 구조를 만든다. 

  • 나머지 15%의 블로그에 대해서 최대한 빠르게 custom crawler를 만들어 대응할 수 있도록 하고자 했다. 

rss 크롤러는 다음의 로직을 가진다. 

유저나 관리자가 등록한 블로그의 rss 소스를 읽어와 마지막으로 크롤링한 날짜에 게시된 포스트가 나올 때까지 게시된 포스트의 등록일을 하나씩 확인하며 저장한다. 포스트의 정보는 태그별로 파싱하여 게시글의 원본 url, 제목, 내용, 게시일과 작성 블로그를 저장했다. 

 

2번 목표를 달성하기 위해서 커스텀 크롤러는 다음의 과정으로 만들어지도록 설계했다. 

 

RssCrawler는 rss 파일을 읽어올 때 사용할 메서드가 정의되어 있다. 

 

그리고 이 인터페이스를 각 형식에 맞는 태그를 가지고 구현한 RssCrawler Impl가 존재한다. 

 

CustomRssCrawler는 최종적으로 각 블로그(유저)마다 가지는 크롤러 객체로 아래의 사진과 같이 블로그의 rss형식 문서, rssUrl, 유저 정보, 비즈니스 로직을 처리해 줄 Service 객체, 그리고 해당 블로그의 rss 형식에 맞춰 크롤링해 줄 크롤러를 멤버 변수로 가진다. 

 

크롤링할 태그 형식을 가지고 만들어진 크롤러(RssCrawler Impl)는 상태를 따로 가지고 있지 않고 여러 블로그가 같은 형식을 가질 수 있기 때문에 블로그마다 인스턴스를 생성하지 않고, 이미 만들어진 인스턴스를 재사용하도록 스프링빈으로 등록하여 관리했다. 하지만 실제 유저(블로그)의 커스텀 크롤러(CustomRssCrawler)는 필요할 때마다 인스턴스를 생성해서 쓰도록 했는데 이유는 다음과 같다. 

1. 블로그마다 그 정보에 대한 상태를 가지고 있어 동시성 이슈가 발생할 수 있음. 
2. 블로그마다 다른 상태를 가지고 있어 어차피 재사용에 대한 의미가 없음. 

따라서 그냥 일반 클래스로 두고 필요한 정보를 조립해서 사용할 수 있도록 구성했다. 다만 블로그의 rss 형식에 맞는 크롤러를 찾아주는 관리자가 필요한데 그 역할을 하는 것이 RssCrawlerFactory이다. 

..... 아..ㅋㅋ 솔직히 RssCrawlerFactory는 RssCrawler Impl을 찾기 위한 else if 떡칠이라서 이렇게 사진으로 넣기 창피한데...! 앞으로 개선할 거니까! 더 나아지는 과정 중 일부니까! ㅋㅋㅋㅋㅋ 아무튼아무튼 이 Factory를 통해서 블로그에 맞는 크롤러를 찾아준다.

이 Factory에서는 일단 가장 먼저 Crawler가 새로 생겨도 변화를 겪지 않도록 리팩토링 하고자 한다. 이걸 만들 때는 방법을 몰랐었는데 이제는 어떻게 해야 할지 알 것 같다. 아 To Be Continue.... 라구요.ㅎㅎㅋ...

 

현재는 이렇게 Custom 크롤러를 동작하고 있는데 이때도, 그리고 회고를 쓰면서도 느끼는 개선점은 다음과 같다. 

  1. 우선 코드를 좀 더 확장성 있게 개선하고 싶다.
  2. 사용자가 직접 내용에 대한 태그를 화면상에서 등록하면 서버단에서 새로운 크롤러를 생성하도록 리팩터링 한다.  
  3. 크롤링 중 발생할 수 있는 에러 처리 로직을 추가한다. 

크롤러에서 느끼는 개선점은 우선 다음과 같은데 이것부터 해결하고 난 다음에 또 다른 개선점을 찾아봐야겠다. 


Elastic Search 도입하기

커널 엔진에서는 최신순, 조회순, 정확도순이라는 3가지 종류의 검색 서비스를 제공하고 있다. 각 API를 어떤 방법으로 구현해야 최적의 결과를 도출할 수 있을지 고민해야 했다. 우리가 찾은 선택지는 MySQL에서 제공하는 단순 LIKE 기반 검색, 역시 MySQL에서 제공하는 전용 인덱스를 사용하는 Full-Text, 마지막으로 역인덱스를 사용하는 분산 검색 엔진 Elastic Search 였다.

 

결과적으로 '최신순', '조회순' 검색은 LIKE문을 선택하였다. 아직 가지고 있는 데이터가 10000건 이하로 많지 않아 연관성이 조금은 낮더라도 최대한 많은 데이터가 반환되는 것이 우선이라고 생각했기 때문이다. 그리고 속도 또한 아직은 LIKE 쿼리로 찾았을 때 크게 차이가 나지 않는다고 결론을 내렸다. LIKE와 Full-Text를 비교했을 때 약 10배 차이뿐이었다. 

하지만 '정확도' 검색에는 역인덱스를 사용하여 대용량 데이터에 대한 복잡한 검색 쿼리도 빠른 시간 안에 처리할 수 있다는 장점을 가진 Elastic Search를 사용하였다. 

 

프로젝트 내에서 나는 Elastic Search를 프로젝트에 도입하는 역할을 맡았다. Elastic Search를 구동하기 위한 DockerFile을 구성하고, 스프링 프로젝트에 ES 설정, Entity(Document) 설정을 맡았다. 

 

1. Docker를 이용한 ES 환경 설정

각자 맡은 도메인에 대한 개발을 할 시간도 부족한데 ES 환경을 설치할 시간이 없었고, 팀원들마다 컴퓨터 환경도 달라서 Docker를 이용하여 쉽게 ES 환경을 구성할 수 있도록 DockerFile을 작성했다. 

 

2. @Document 등록

ES를 처음 써봐서 어떻게 데이터를 연결하여 넣어야 할지 어려웠었다. 열심히 서칭 했었는데.... (아련) 아무튼아무튼, elasticsearch는 데이터를 json 문서로 직렬화된 복잡한 자료 구조를 한다. 따라서 특정 문장을 입력받으면 파싱을 통해서 문장을 단어 단위로 분리하여 저장하고 대문자를 소문자로 치환하거나 유사어 체크 등의 추가 작업을 통해서 텍스트를 저장한다.

 

도메인에 @Document를 붙여 ES에 넣을 데이터를 등록하고, @Document에 있는 필드를 어떤 타입으로 매핑할 건지 또, 어떤 형태소 분석기를 매핑할 것인지를 명세하는 json 형식을 이용했다. 

 

검색 API 구현 방법을 고민하면서 데이터의 양, 검색의 복잡성, 요구되는 응답 시간, 3가지를 고려해야 한다는 점을 배웠다.

 

검색 API 역시 개선점이 많이많이 있는데 ㅎㅎ 검색 질의에 대해서 MySQL의 native query를 사용했기 때문에 유지보수성이 떨어진다는 점, 쿼리에 대한 오류가 런타임시에 발생한다는 점을 개선해야 하고, 어쨌든 이 서비스는 계속 운영을 한다면 검색 대상이 되는 데이터가 갈수록 많아질 것이기 때문에 LIKE 쿼리로는 한계가 있을 것이다. 따라서 이를 Full-Text를 효율적으로 사용하여 개선해야 한다. 


KernelEngine을 구현하면서 고민했던 것들

팀 내에서 고민하고 다른 크루, 멘토님에게 질문하여 얻은 결과-모음집

 

▷ Dto 변환, Entity 변환 메서드를 어디에 구현해야 할까?

dto → entity, entity → dto를 수행하는 메서드를 어디에 위치시켜야 할지 팀 내에서 고민을 많이 했었다. 많은 질문과 고민 끝에 우리 팀에서는 메서드의 위치에 대한 기준을 다음과 같이 잡았다. 관심사가 Dto 입장에서 관심이 많을까… 엔티티 입장의 관심사가 많을까?

그리고 결론은 다음과 같았다. → dto가 많을 것 같다. dto 계층에 변환 메서드를 두자!

 

하지만 이렇게 해도 지금의 아키텍처에서는 dto 계층을 여러 계층에서 의존하게 되는 결과가 발생했다. 이를 해결하기 위해서는 좀 더 다양한 아키텍처를 공부해 보고 적용해봐야 할 것 같다. 


▷ 변수의 Type을 선택하는 기준이 뭘까?

  • Timestamp 클래스와 LocalDateTime 클래스 중 어떤 것을 사용해야 하지?
    • 고민 지점
      • Timestamp는 DB 서버의 시간을 따르고 LocalDateTime은 서버 컴퓨터의 시간을 따른다. 그런데 Entity 설정등을 하면서 시간에 대한 필드를 선언할 때 Timestamp로 선언을 할지, LocalDateTime으로 선언할지에 대한 기준이 명확하게 서지 않아서 타입을 선정하는 기준에 대한 궁금증이 생겼다. 
    • LocalDateTime vs Timestamp
      • 시간 데이터를 다루기는 LocalDateTime이 편하다. 서로 변환하는 건 어렵진 않음. 변환하는 컨버터라던지 그런 설정을 할 수 있다. 가급적 LocalDateTime으로 하면 가독성이 좋으니까 컨버터 달고 구현하는 편. 하지만 time을 자주 바꾸지 않으면 Timestamp도 노상관
      • 즉, 가독성, 연산을 위해 localdatetime 추천, 시간 조정 안 할 거면 timestamp 사용하는 것도 방법이라는 결론을 얻었다. 
  • Primitive, Reference 중 어떤 것을 사용해야 하지?
    • 팀 내 결론
      • Reference Type은 null이라는 표현하는 방법이 있을 뿐, null이 들어가지 않을 거면 Primitive 사용, data가 맞지 않아서 null이 리턴되는 경우도 있을 때는 편리하게 Reference Type을 사용하자!
    • Primitive Type
      • 메모리의 측면에서 효율적
    • Reference Type
      • Reference Type의 경우 객체이기 때문에 == 연산이 아닌 equals() 메소드를 이용해서 값을 비교해야 하는데 코드를 읽을 때 직관적으로 읽을 수 있음.
      • null 값에 대한 관리가 쉬움
      • 하지만 결국 객체를 생성하는 것이므로 메모리 측면에서 비효율

▷ DB 검색과 Elastic Search 성능 비교를 어떤 지표로 하면 좋을까?

  • 주요 api에 따라 어느 부분에서 부하가 발생하는지를 중심으로 성능 확인하기

API 구현 방법을 선택하는 기준

1.  DB에 request를 할 때 복잡한 쿼리를 날리는 것과 많은 양의 데이터를 한 번에 긁어오는 것 중 더 오래 걸리는 쿼리를 어떻게 확인하지?

  • 실행 계획을 확인해서 인덱스를 잘 타는지 등의 확인을 통해 시간을 확인해 보면 된다! ㅋㅋ 

2. 더 좋은 api 구현 방법을 고르고 싶을 때 어떤 점들을 고려해야 할까?

팀 내에서 생각해 본 고려할 포인트

  • DB 자체에 요청하는 효율을 따진다. 
  • api 요청 자체의 처리 시간을 측정한다. 

현업에서 일하는 개발자는 api 구현할 때 어떤 부분들을 고려하는지 알고 싶어 멘토님께도 여쭤보았다. 

  • DB 부하가 최대한 적게 가도록 한다.
    • DB는 내부적으로 엔진을 고칠 수 없다. 즉, 쿼리 레벨이 아닌 자체적인 성능 최적화에 관련해선 개발자의 자유도가 적음. 따라서 DB에 너무 많은 역할을 주지 않고, 커넥션을 오래 물고 있지 않도록 구현한다. 성능 개선 포인트는 쿼리, 자바 로직과 같이 다양하게 있기 때문이다. 
  • 테이블 설계만 잘해도 성능은 보장된다. 읽기 요청, 쓰기 요청이 존재할 텐데 읽기 요청의 성능과 쓰기 요청의 성능 중 둘 중 하나는 포기를 할 수밖에 없다.(트레이드 오프) 더 많이 필요한 요청에 맞춰서 설계하자!
    • 즉, 읽기 요청과 쓰기 요청을 분리하기! 단, 이렇게 했을 때는 데이터 정합성을 잘 고려해야 한다. 
  • Event Driven Architecture 공부해 보기!

개인적인 결론

  • 내가 만드는 서비스가 어떤 서비스인지를 명확히 이해하는 것이 우선인 것 같다. 만일 지금의 서비스가 오랫동안 유지보수되어야 하고 트래픽이 엄청나게 몰릴 것 같지 않다면 쿼리를 많이 쓰지 말고 가독성을 높이는데 집중,
  • 아 모르겠고 트래픽이 굉장히 많이 몰리는 서비스임!!! -> 쿼리를 사용하여 빠르게 가져오는데 집중

Batch 서버를 따로 두기 위해서 Multi-Module을 도입하면서 생겼던 문제들....

 

 


내가 만든 크롤러가 객체지향적일까?

나름 객체지향적, 확장성 높은 크롤러를 만들고 싶다고 애를 쓰긴 했는데.... 그래서 정말 객체지향적이고 확장성 있는 크롤러를 만들었는지 궁금해서 주변 사람들에게 코드리뷰를 부탁했다. 그리고 여기서 얻은 조언들을 통해 테스트 코드에 대한 새로운 시각을 얻었다.

  • 테스트 메서드 하나는 하나의 기능만 확인할 수 있어 코드의 간결함, 단일 책임, 객체지향성을 자연스레 추구할 수 있게 된다. 
  • 테스트 코드는 코드레별로 확인해 주는 기능과 리팩토링 효과가 커서 처음에는 오래 걸릴지라도 테스트 코드 작성하는 것 추천!

사실 테스트 코드는 그냥 내가 만든 기능이 잘 돌아가는지 확인하는 용도라고 생각했는데 이런 조언을 듣자 테스트 코드의 또 다른 역할을 알 수 있었고 이를 기반으로 크롤러 코드를 스스로 다시 점검할 수 있겠다고 생각했다. 

 

그 외에도 DDD에 대한 인사이트를 얻었다. 

  • 도식화(ddd 도메인 주도 아키텍처)를 통해서 로직을 좀 더 작은 단위로 쪼개고 어느 layer에 두는 게 좋을지 고민해봐요!

커널 엔진의 개선 방향

사용자가 직접 내용에 대한 태그를 화면상에서 등록하여 자동으로 크롤러가 생성되도록 한다. 

크롤링 배치 개선

크롤링한 데이터를 조회에 좀 더 유리한 NoSQL을 사용해 보고 성능 비교까지 해보기

검색에 full-text index를 적용하고 elastic search 성능 개선

10만 건 정도 데이터를 넣어놓고 부하테스트 해보기 

CI/CD 파이프라인 구축하기 

 

 

 

고생했다~~~~~~~ ><

'SAN's history > kernel360' 카테고리의 다른 글

[kernel360] 해커톤 회고  (1) 2023.11.01
[kernel360] boot-up 회고  (1) 2023.11.01

댓글