Booking Recommendation System


인턴 기간 동안에 개발한 예약 추천 시스템에 대해 저희에게 간단히 설명해 주실 수 있나요?

이번 인턴 기간 동안에 개발한 예약 추천 시스템은 과거 예약 데이터를 사용해 네이버 예약 사용자들이 특정 업체 선택 시 선택한 업체와 가장 유사한 업체, 선택한 업체의 사용자들이 가장 선호하는 업체와 업종을 추천해 주는 시스템을 만들었습니다.

인턴 기간 동안에 개발한 추천 시스템을 통해 얻은 점이 있나요?

  1. DB부터 Front-End까지의 End-To-End 개발로 전체적인 시스템 아키텍처에 대한 이해가 높아질 수 있는 계기가 되었습니다.

    지금까지 많은 프로젝트를 진행하면서 서비스에 필요한 서버 단이나 DB 단만 개발해 왔습니다. 하지만 이번 인턴 기간을 통해 시스템을 처음부터 끝까지 End-To-End로 개발해 전체적인 시스템 아키텍처에 대한 이해가 높아질 수 있었습니다.

  2. 서비스를 만들기 위해 시스템의 구현 뿐만 아니라 운영도 아주 중요한 요소라는 점을 깨닫는 계기가 되었습니다.

    학생 때는 시스템이 정상동작하는 것만 신경썻는데, 막상 인스턴스가 많은 시스템을 구현해보니 DB Lock에 의한 일시적인 에러나 서버 인스턴스에 문제가 생기는 등 운영상의 이슈가 발생했습니다. 예전에는 해당 이슈가 일어난 인스턴스들을 재시작하면 문제가 해결되는 줄 알았으나 이는 일시적인 문제 해결일 뿐이고, 서비스를 위해서는 시스템의 정상적인 운영도 꼭 필요하다는 점을 깨닫게 되었습니다.

  3. 기본기의 중요성에 대해서 깨닫는 계기가 되었습니다.

    추천 시스템을 개발하면서 Spring Boot Web을 제외한 대부분의 인스턴스들은 처음 학습하였습니다. 새로운 기술들을 빠르게 학습하고 적용하기 위해서는 전체 시스템을 이해할 수 있는 능력인 기본기가 가장 중요하다고 느끼게 되었고 현재에도 운영체제(공룡책)을 정독하고 있습니다.

추천 시스템 개발간에 가장 어려웠던 점은 무엇인가요??

추천 시스템을 개발하면서 가장 어려웠던 점은 추천 알고리즘을 기획하고 설계했던 부분이 가장 어려웠던 것 같습니다.

제 서비스에서 추천 알고리즘의 성능 평가는 사용자들의 객관적인 지표가 존재하지 않기 때문에 주관적으로 평가가 되어지는데, 좋은 추천 알고리즘을 위해서는 대부분의 사람들이 선호할만한 업체나 업종을 추천하는 알고리즘을 개발했어야만 했고, 이러한 알고리즘을 개발하기 위해서 사용되어지는 모든 로직과 상수값, 기준점들이 다 마땅한 이유가 있어야만 했습니다. 이러한 고민 속에서도 추천 알고리즘이 제 자신만의 색깔을 잃어버리면 안됬었습니다. 종합해보자면 저는 좀 더 특별하고, 재미있고, 성능 좋은 알고리즘을 만들기 위해서 많이 고민했던 것 같습니다. 당연히 추천 서비스는 처음 해보는 분야라서 추천 알고리즘의 기획과 설계가 많이 어려웠지만 A4용지로 20장도 넘게 그림을 그리는 것을 통해서 머리속의 생각들을 정리할 수 있었고 이를 통해 재미있고 성능 좋은 알고리즘이 나올 수 있었던 것 같습니다.

추천 시스템을 개발하시면서 분명히 시간이 부족했을텐데… 어떠한 부분을 더 챙기고 싶었나요?

더 개발하고 싶은 부분은 너무 많았습니다. 추천 알고리즘 개선부터 Redis나 Batch의 장애 대응 고도화, 배치에 병렬 프로그래밍 도입 등 다양한 부분을 더 개발 하고 싶었는데, 일단 기간 내에 서비스를 완성하는 것이 가장 중요하니 어느 정도 개발된 부분에 대해서는 만족했어야만 했습니다. 이러한 많은 부분 중에서 그래도 가장 관심있는 부분은 장애 대응 고도화입니다. 현재 Batch나 Redis에 장애 대응 전략을 세워서 개발하였지만 아직 부족한 부분이 몇가지 존재해서 현재에도 계속 공부하고 있습니다.

어떻게 장애 대응을 고도화 시키고 싶으시나요?

일단 현재 Batch Process에 장애 대응을 위해서 Skip, Retry, Back-off 전략을 구현한 상태이며 Batch Process 실패시 알람을 보내는 클라이언트 부분을 더 개발하고 싶습니다. 두 번째로는 현재 Redis 장애 대응을 위해 Master-Replica 운영 모델을 도입해서 적용 했는데, 좀 더 자동화 된 장애 대응을 위해서 Redis Sentinel이나 Redis Cluster 운영 모델을 도입하고 싶습니다. 아니면 Health Checker도 괜찮구요.

Health Checker란 무엇인가요?

Health check는 서버의 상태를 주기적으로 체크하여 서버의 상태가 Health Checker와 통신이 불가능할 경우 서버에 특정 조치를 취해 서비스를 원활하게 제공하기 위해 이용되는 방법입니다. 서버의 상태를 주기적으로 점검하는 방법으로는 Link, ARP, ICMP, TCP, Script 방식이 있으며, Health Checker에서는 일정 시간 간격으로 서버에 Open과 Close를 반복적으로 진행하여 서비스 제공 가능 유무를 점검합니다. 아마 제가 구현하고 싶은 방식은 가장 익숙한 TCP 방식이지 않을까 생각하고 있습니다.

헬스체크를 위해 사용되는 패킷의 종류, 패킷을 전송하는 주기와 서버의 응답을 기다리는 시간, 응답이 수신 되지 않았을 때 다시 패킷을 보내주는 횟수는 사용자의 설정에 따라 동작합니다. 전송 주기가 짧을 수록 서버의 장애 여부를 정확하게 파악할 수 있지만, 이는 네트워크의 부하가 될 수 있음으로 Context를 잘 고려해서 설정 해야만 합니다.

네이버 예약에 추천 시스템이 왜 필요할까요?

현 시대에 사는 사람들은 인터넷에 너무나 많은 정보가 존재하기 때문에 자신이 어떠한 정보를 원하는지 조차도 잘 알지 못합니다. 이러한 상황은 네이버 예약 사용자들에게도 동일하게 발생한다고 생각합니다. 너무나 많은 업체 및 상품들이 등록되어 있어서 사용자들은 어떤 업체를 예약해야될지 잘 알지 못합니다. 이러한 불편함을 해결해주고자 수 많은 업체를 필터링해서 사용자가 선호할만한 업체를 추천해주는 시스템이 네이버 예약에 꼭 필요하다고 생각합니다.

추천 시스템의 기대 효과는 무엇인가요?

추천 시스템의 대표적인 기대 효과로는 네이버 예약 사용자들의 서비스 만족도 증가 효과를 기대할 수 있구요, 또한 추천 시스템으로부터 발생한 다양한 지표를 활용해서 새로운 이벤트를 기획할 수도 있습니다. 이 뿐만 아니라 평소라면 검색하지 못할 다양한 업체를 추천 시스템을 통해서 추천해줌으로써 사용자의 예약의 폭 확장을 불러일으킬 수도 있습니다. 결국 추천 시스템을 통해 예약 서비스의 성장을 이룰 수 있다고 생각합니다.

여러 추천 대상 중 왜 업체를 추천하게 되었나요?

예약 추천 시스템을 기획해 보았을 때 추천 대상은 크게 업종과 업체, 상품으로 나눌 수 있었습니다. 이들간의 관계를 살펴보면 업종은 업체를 포함하고 업체는 상품을 포함합니다. 업종을 추천하게 된다면 사용자들은 같은 업종에 속한 수 많은 업체 중 특정 업체를 선택해야만 하는 불편함이 발생했고, 상품을 추천하기에는 동일한 업체임에도 불구하고 등록된 상품들의 다양성이 너무 심해서 알고리즘의 성능이 좋지 않을것 같았습니다. 이러한 이유에서 업체 추천이 가장 적절하다고 생각해서 업체를 추천하는 시스템을 기획하게 되었습니다.

패치

정지 없이 서버 패치를 할 수 있는 방법

  1. 새 버전으로 준비된 서버들을(site B) 미리 준비한다.
  2. 기존 서버로의 추가 유입을 막고(Drain) 새 버전 서버로 유입되도록 바꿉니다(Redirect)
  3. Draining과 Redirecting을 계속하여 두 개의 Site를 뒤집는(Flip) 방식으로 서비스의 중단 없이 패치를 적용합니다.

  4. Flip이 완료되면 모든 접속이 새 Site로 붙게되며 기존 Site는 deactivate 됩니다.

장애

장애는 피할 수 없다. 장애에 견디고 복구 가능한지가 중요하다.

모든 서비스는 Health Check 프로토콜을 구현하고 있습니다. Health Check가 실패한 경우를 모니터링하여 장애를 감지합니다.

SPOF 를 피하기 위해 동종 서비스들은 개념적으로 Pool에 담겨 있고 장애 감지시 해당 노드가 Pool 에서 제거됩니다.

단일 장애점(single point of failure, SPOF)은 시스템 구성 요소 중에서, 동작하지 않으면 전체 시스템이 중단되는 요소를 말한다

그리고 서비스 종류별 Pool을 두고 있습니다.

만약 예상치 못한 이유로 서비스 전체나 상당부분이 불안정한 경우가 발생한다면 서버들을(Site) 다시 배포합니다.


Recommendation Algorithm


업체와 업종 추천을 위해서 사용한 알고리즘에 대해 저희에게 알려주실 수 있나요?

일단 수 많은 업체와 업종 중 특정 대상을 추천하려면 추천 대상간에 랭킹 작업이 필요합니다. 저는 랭킹 작업을 진행하기 위해서 랭킹 점수를 업체 간의 접근성과 유사도, 만족도의 곱으로 정의했고 업체간의 유사도를 구하기 위해서 두 가지의 추천 알고리즘을 사용하였습니다.

선택한 업체와 가장 유사한 업체를 추천하기 위해 업체와 속성간의 관계를 기반으로 업체를 추천하는 알고리즘인 키워드 기반의 Content Based Filtering을 사용했구요. 선택한 업체의 사용자들이 가장 선호하는 업체를 추천하기 위해 업체와 사용자의 관계를 기반으로 하는 알고리즘인 Collaborative Filtering을 사용하였습니다.

키워드 기반의 Content Based Filtering이란 무엇인가요?

일단 Content Based Filtering이란 업체와 속성간의 관계를 기반으로 가장 유사한 업체를 필터링하는 알고리즘을 의미합니다. 저는 업체 정보를 분석해 키워드로 나눈 뒤 업체들의 속성을 산출했으며 산출된 속성들과 업체들간의 관계를 행렬로써 표시해 업체간의 유사도를 구할 수 있었습니다.

Content Based Filtering의 한계점으로는 콘텐츠가 많아 질수록 속성을 추출하기 어려워진다는 문제점이 존재한다.

Collaborative Filtering이란 무엇인가요?

Collaborative Filtering이란 사용자와 업체간의 관계를 기반으로 사용자들이 가장 선호하는 업체를 필터링하는 알고리즘 입니다. 과거 예약 데이터를 사용하면 특정 업체를 예약한 사용자들을 알 수 있고, 사용자들의 특정 업체 선호도를 알 수 있습니다. 이러한 관계를 행렬로써 표시해 업체간의 유사도를 구할 수 있었습니다.

협업 필터링의 한계점으로 콜드 스타트, 계산 효율 저하, 롱 테일 등의 문제가 있음

업종 추천은 어떠한 방법으로 개발 하셨나요?

업종 추천을 위해서 시간, 순서, 기간에 따른 사건의 규칙을 분석하는 방법인 순차 분석을 사용하였습니다. 순차 분석을 위해 특정 업체를 예약한 사용자들의 데이터를 분석 했구요, 사용자들의 개인별 업체 예약 시점을 기준으로 특정 기간 내의 예약 데이터를 분석함으로써 예약한 업체가 속해있는 업종과 다른 업종이 특정 기간 내에 얼마나 자주 예약되는지를 산출 할 수 있었습니다. 이러한 관계를 통해 업종간의 관계를 산출할 수 있었구요 특정 업종에 속한 업체 선택시 관련된 업종을 추천할 수 있는 알고리즘을 설계할 수 있었습니다.

추천 알고리즘의 성능 문제나 Cold Start 문제를 해결하기 위해서 Hybrid Filtering은 고려하지 않았나요?

물론 Hybrid Filtering의 도입도 검토해 보았습니다. Hybrid Filtering은 Collaborative Filtering과 Content Based Filtering으로 표현되어지기 때문에 Cold Start나 Long Tail 등의 문제를 해결할 수 있습니다. 하지만 제 서비스는 사용자 없기 때문에 알고리즘의 성능은 개인의 주관으로 평가되어질 수 밖에 없고 제가 적절하다고 생각했던 알고리즘이 남들이 보기에는 적절하지 않을 수도 있겠다고 생각이 들었습니다.

이러한 이유에서 선택한 업체와 유사하면서 선택한 업체의 사용자들이 선호하는 업체를 추천하는 것 보다는 이를 분리해서 선택한 업체와 유사한 업체, 선택한 업체의 사용자들이 선호하는 업체로 나누는 것이 인턴 평가 때 팀원분들이 업체 추천에 대해서 더 쉽게 공감할 수 있겠다고 생각해서 Hybrid FIltering 대신 Content Based Filtering과 Collaborative FIltering으로 나누어서 개발하였습니다.

추천 알고리즘 대신 머신러닝을 사용하지 않은 이유는 무엇인가요?

일단 머신러닝을 사용하지 않은 가장 큰 이유는 시간적인 문제 때문에 사용하지 않았습니다.

총 6주간의 인턴 기간 동안 추천 서비스 기획과 설계에 2주를 사용하고 남은 4주 동안에 추천 시스템을 구현하기 위해 수 많은 기술들을 배우고 적용했어야만 했습니다. 머신러닝을 한번도 접해보지 않은 상황에서 알고리즘을 세우고, 데이터를 전처리하고 학습시켜 모델을 만들고 이를 테스트하고 실제 프로세싱에 적용하기에는 시간이 부족할 거라고 생각했습니다. 또한 로직으로써 추천 알고리즘을 풀어내도 충분히 재미있고 성능 좋은 결과를 낼 수 있을것 같다는 자신감도 조금 있었구요!! 이러한 이유에서 머신러닝 대신 추천 알고리즘을 사용했습니다.

형태소 분석기인 KOMORAN을 사용한 이유는 무엇인가요?

업체와 속성간의 관계 행렬을 구하기 위해 업체 정보를 형태소 분석을 통해 키워드로 만들고 싶어서 형태소 분석기인 KOMORAN을 사용했습니다. 또한 KOMORAN은 Maven Repository에 등록되어 있기 때문에 간단히 메이븐 설정 파일을 수정하는 것 만으로도 쉽게 프로젝트에 가져올 수 있다는 장점이 있어서 KOMORAN을 사용하였습니다.

Cosine Similarity를 사용한 이유는 무엇인가요?

추천 알고리즘을 사용한다면 업체와 속성 또는 사용자 간의 관계를 벡터로 표현할 수 있습니다. 이러한 벡터간의 유사도 즉, 업체간의 유사도를 구하기 위해 Cosine Similarity를 사용했으며, 이는 벡터의 크기가 아닌 방향의 유사도를 판단하는 함수입니다. 실제로 벡터간의 크기의 유사도를 판단하는 방법인 Euclidean, Manhattan, Minkowski Distance 방법보다 Cosine Similarity가 제 로직에서는 훨씬 성능이 좋았습니다.

FastText를 사용한 이유는 무엇인가요?

업체와 속성간의 관계 행렬을 구하기 위해 텍스트 유사도가 필요했었고, 텍스트 유사도를 구하기 위해 텍스트를 백터로 임베딩할 필요가 있었습니다. 즉, 텍스트를 백터로 임베딩 하기 위해서 텍스트의 표현 및 분류를 학습할 수 있도록 하는 오픈 소스 라이브러리인 FastText를 사용했으며, 미리 한국어로 학습된 모델을 사용했습니다.

알고리즘 vs 모델

알고리즘

어떤 문제의 해결을 위하여 입력된 자료를 토대로 하여 원하는 출력을 유도하여 내는 규칙

모델

주어진 데이터를 가지고 알고리즘을 통해서 학습을 시켜서 모델을 도출

  • 알고리즘 : y = wx + b
  • 모델 : y = 2x + 2 (w와 b를 머신러닝이 구해준다.)

PostgreSQL


PostgreSQL을 사용한 이유가 있으시나요?

수 많은 관계형 DBMS 중 PostgreSQL만을 사용하게 된 이유는 없습니다. MSSQL이나 MySQL, MariaDB등 다양한 RDBMS를 고려해보기도 했는데요, 일단 운영 데이터를 가져와서 사용해야 하기 때문에 RDBMS는 무조건적으로 사용해야 했었고 그렇다면 오픈 소스임에도 불구하고 다른 DB에 비해서 좋은 성능과 다양한 기능을 제공해주는 PostgreSQL을 사용해보면 어떨까? 하는 생각에 PostgreSQL을 사용하게 되었습니다.

PostgreSQL의 특징에 대해서 알고 있으신가요?

일단, 오픈 소스임에도 불구하고 좋은 성능과 다양한 기능을 제공해주는 것으로 알고 있습니다. Microsoft Azure VM 환경에서 여러 DB간에 단순 쿼리 성능을 비교한 지표를 본적이 있는데요 물론 Oracle DB에 비해서는 PostgreSQL의 쿼리 성능이 떨어지긴 하지만 다른 DB에 비해서는 확실히 성능이 좋다는 것을 볼 수 있었습니다. 기능적인 측면에서도 신뢰도를 최우선으로 하여 다양한 인덱싱 기법과, 트랜잭션 및 ACID, 동시성 성능을 높여주는 MVCC 기능, 다양하고 유연한 REPLICA 방식 지원, 다양한 언어에 제공되는 인터페이스, 잘 만든 문서나 메뉴얼 등을 지원해주는 것으로 알고 있습니다.

어떠한 인덱싱 기법을 지원해주는가요?

PostgreSQL은 기본적으로 B-Tree 인덱스를 지원하구요, 이 뿐만 아니라 복합 인덱스, GIN(Generalized Inverted Index), Gist(Generalized Inverted Search Tree) 인덱스 타입을 제공해주는 것으로 알고 있습니다.

동시성 성능을 높여주는 MVCC 기능은 뭔가요?

MVCC란 Multi-Version Concurrency Control의 줄임말로써, 동시 접근을 허용하는 데이터베이스에서 동시성을 제어하기 위해 사용하는 방법 중 하나라고 알고 있습니다. PostgreSQL에 접근하는 사용자는 접근한 시점의 데이터베이스 Snapshot을 읽고, 데이터에 대한 변경이 완료될 때 이를 새로운 버젼으로 가정해 이전 버젼의 데이터와 비교해서 변경된 내용을 기록하는 방법으로 동시성을 제어합니다. 이를 MVCC라고 하구요.

Lock이 존재하지 않기 때문에 일반적인 RDBMS보다 확실히 빠르게 작동하지만, 버젼 이슈가 생길 수 있다는 점을 주의해야만 합니다.


PostgreSQL

  • 오라클에 준하는 기능을 가지고 있다.
  • MVCC, Point in Time Recovery 등의 특징이 존재한다.
  • INSERT, SELECT, UPDATE 쿼리 성능이 비교적 좋은 편이다.



  • PostgreSQL Instance Objects Hierarchy Architecture



  • Oracle구조와 가장 유사한 DB이다.
  • 성능도 비슷하다.

Redis


추천 시스템에서 Redis를 사용하신 이유가 무엇인가요?

Redis를 사용하게 된 대표적인 이유는 인메모리 Key-Value 데이터 저장소로써, 추천 데이터를 캐싱하기 위해서 사용하였습니다. 뿐만 아니라 Sorted Set과 같은 다양한 자료구조도 지원해주고, Slave-Replica나 Redis Sentinel과 같은 여러 운영 모델도 지원해주기 때문에 시스템을 더 안정적이고 성능 좋게 만들고 싶어서 Redis를 사용하게 되었습니다.

캐싱(Caching)이란 무엇인가요?

캐싱이란 특정 데이터를 빠르게 접근할 수 있도록 임시적인 저장 위치에 데이터를 저장해두는 방법을 의미합니다. 캐싱을 통해서 속도가 빠른 장치와 속도가 느린 장치 사이에서 발생하는 병목 현상을 줄일 수 있습니다.

병목(bottleneck) 현상은 전체 시스템의 성능이나 용량이 하나의 구성 요소로 인해 제한을 받는 현상을 말합니다.

레디스의 Key - Value 구조는 어떻게 잡으셨나요?

Redis에 데이터를 저장하기 위한 키는 크게 키워드 기반의 업체 추천과, 사용자 기반의 업체 추천, 업종 추천, 업종별 업체 추천으로 나누었구요 이들 모두 계층적으로 구성하였습니다. Value는 추천 업체 아이디와 추천 점수를 Json 형태의 문자열로써 저장하였습니다.

추천 데이터 저장을 위해 어떠한 자료구조를 사용하셨나요??

추천 데이터를 저장하기 위해 SortedSet 자료구조를 사용하였습니다. 특정 업체나 업종의 추천 대상들을 저장하기 위해서 Set이나 List와 같은 자료구조를 사용해야만 했으며, 레디스에 저장시 추천 점수별로 정렬 되면 매번 어플리케이션 레이어에서 부담해야하는 정렬 작업이 줄어들어 훨씬 성능면에서 좋아질 것 같아서 SortedSet 자료구조를 사용했으며 정렬을 위한 score는 추천 점수를 사용하였습니다.

Redis의 운영 모델에 대해서 설명해주세요.

레디스 운영 모델은 크게

  • 하나의 Redis 인스턴스만을 사용하는 Standalone 모델,
  • Master 인스턴스의 데이터를 비동기적으로 여러개의 Replica 인스턴스로 복제하는 Master - Replica 모델,
  • 레디스의 높은 고가용성을 위해 모니터링, 알림, 자동화 된 장애 대응을 지원하는 Redis Sentinel 모델,
  • 다양한 장애 대응 및 파티셔닝을 제공하는 Redis Cluster 모델 등

다양한 운영 모델이 존재합니다.

Redis Cluster란 무엇인가요?

Redis Cluster란 레디스의 높은 가용성을 위한 하나의 운영모델을 의미하구요, 자체적으로 Hash Slot을 사용해 샤딩을 지원하고 자체적인 Primary, Secondary Failover를 통해 자동화 된 장애 대응을 지원합니다.

데이터 분산 방법

  • Application
    • Consistent Hashing
      • 일관된 해싱(Consistent hashing)은 웹서버의 개수가 변동하는 가운데 요청을 분산하는 방법을 말한다. 해시테이블의 크기가 변할 때, 평균적으로 K/n의 키만 재매핑되면 된다.
    • Sharding (Horizontal Partitioning)
      • 같은 테이블 스키마를 가진 데이터를 다수의 데이터베이스에 분산하여 저장하는 방법을 의미합니다.
  • Redis Cluster
    • Hash 기반으로 Slot을 0 ~ 16384로 구분하여 자신의 Slot에 해당하는 Node로 Redirect 시킨다.
    • 장점
      • 자체적인 Primary, Secondary Failover
      • Slot 단위의 데이터 관리.
    • 단점
      • 메모리 사용량이 더 많음
      • Library 의존성

Redis 장애 대응을 위해 어떻게 처리 하셨죠?

Redis 장애 대응을 위해 하나의 Master와 두 개의 Replica로 이루어진 Master - Replica 운영 모델을 도입하여 Master가 다운될 시 Replica를 통해 Read를 하는 장애 대응 전략을 구축 했습니다. 이러한 전략으로는 장애 대응이 완벽하다고 말 할 수는 없으며, Redis Sentinel 이라던지, Health Checker를 도입해서 노드 재시작 및 노드 자동 승격 기능 등을 구현해보고 싶습니다.

Redis의 구조는 알고 계시나요?

Redis는 ANSI C를 사용해서 작성되었으며 Disk를 사용해서 persistence를 지원한다. redis에서는 데이터를 저장하는 방법으로 snapshot(RDB) 방식과 AOF(Append on file) 방식을 사용한다.

  • snappshot(RDB) 방식은 특정 순간에 메모리에 있는 내용 즉, snapshot을 disk에 옮겨 담는 방식이다. (restart 시간이 빠르지만 특정 snapshot 이후 변경된 데이터는 유실된다.)
  • AOF 방식은 redis의 모든 write/update 연산 자체를 모두 log 파일에 기록해서 서버 재시작시 기록된 write/update를 재실행하는 방법이다. (데이터 유실이 발생하지 않지만 restart시 느리다.)

Redis는 사용자 명령어를 Single Thread로 처리하기 때문에 Long-Time 명령 수행시 다른 명령어들은 처리할 수 없는 상태가 되기 때문에 꼭 주의해야 합니다.

Redis Java Client인 Lettuce는 무엇인가요?

Lettuce는 Netty 기반의 고성능 비동기 Redis 클라이언트로써 다른 Redis 클라이언트인 Jedis보다 훨신 빠릅니다. 또한 레디스의 다양한 운영 모델과 커넥션을 지원하구요 무엇보다도 잘 만들어진 공식 문서가 매력적입니다.

Database Partioning

데이터베이스를 여러 부분으로 분할하는 것을 데이터베이스 파티셔닝(DB Partioning)이라고 한다.

데이터베이스 파티셔닝은 중요한 튜닝기법으로 데이터가 너무 커져서, 조회하는 시간이 길어질 때 또는 관리 용이성, 성능, 가용성 등의 향상을 이유로 행해진다. 데이터베이스가 분할된 각 부분을 파티션이라고 부른다.

Database Sharding (수평 분할, Horizontal Partioning)

테이블을 수평으로 쪼개서 각 행을 다른 DB에 분산시키는 것이다. 분할된 각 부분을 샤드(Shard)라고 한다. 즉, 각 샤드의 스키마 구조는 동일하다.

Vertical Partioning(수직 분할)

테이블을 수직으로 쪼개서 테이블의 일부 열을 다른 DB로 빼내는 형태로 분할한다. 정규화는 본질적으로 수직 분할에 관련된 과정이다. 특정 테이블에서 자주 참조되는 열을 수직 분할시켜 해당 데이터를 캐싱할 수도 있다.


이슈


Spring Data Redis Contribution 과정에 대해서 알려주실 수 있나요?

redis는 버젼 5.0부터 slave라는 용어가 replica로 변경되었고 Redis 클라이언트인 Lettuce-core는 버젼 5.2.0부터 코드 레벨의 Slave를 Replica로 변경하였습니다. 하지만 최근 Spring Data Redis Doc을 보면 Lettuce-core 버젼 5.2.0 이상을 사용함에도 불구하고 여전히 Deprecated된 SLAVE_PREFERRED를 통해서 특정 기능의 사용을 안내 합니다.

저는 이 부분이 잘못되었다고 생각해 직접 Spring Data Redis 프로젝트에 이슈를 발급하였고 Deprecated 된 부분을 모두 고친 뒤 PR을 날렸습니다. 현재 PR이 머지가 되었으며 해당 이슈가 해결되었습니다.

많은 사용자가 존재하는 Spring Project에 조그마한 부분이지만 기여할 수 있었다는 점에서 매우 뿌듯하고 행복합니다.

OutOfMemoryException 이슈에 대해서 공유해주실 수 있나요?

추천 알고리즘을 위해 업체와 사용자 관계를 벡터 값으로 표현해 Integer 객체로 저장 했는데요, Batch Processing 간에 모든 업체와 모든 사용자간의 관계를 벡터 값으로 표현 했을 경우 수십, 수백억 개의 Interger 객체가 생겼었습니다. 이러한 무분별한 Integer 객체 생성으로 인해 객체를 저장하는 메모리인 Heap의 공간이 부족했었고 결국 JVM의 메모리가 부족하다는 OutOfMemoryError가 발생하였습니다.

저는 이러한 이슈를 해결하기 위해 업체의 벡터 값을 저장하기 위한 자료구조를 Integer List 에서 Int 배열 즉, Wrapper 타입에서 Primitive 타입으로 리팩토링 하였고, 이를 통해 Heap 메모리에 저장되는 데이터의 사이즈를 줄여 OutOfMemoryError를 해결할 수 있었을 뿐만 아니라 Wrapper 타입으로 저장 시 발생하는 객체 생성, 참조 등의 부하를 줄여 Batch Processing 성능을 개선할 수 있었습니다.

Primitive 와 Wrapper 타입은 각각 무엇이고, 언제 써야하는지 알려주세요.

Primitive 타입은 Java에 키워드로 등록된 8개의 비객체형 원시 타입을 의미하구요, Wrapper 타입은 primitive type을 Wrapping해 객체로 다루기 위해서 사용하는 클래스를 의미합니다.

단순하고 반복적인 계산을 위해 Wrapper 타입의 객체가 계속 생성되는 경우 성능상 Primitive 타입을 사용하는 것이 더 좋고, DB에서 데이터를 가져오는 경우나, primitive 타입의 default 값으로부터 혼란이 발생하기 쉬운 경우 Wrapper 타입을 사용하는 것이 더 좋습니다.

JVM

자바 가상 머신으로 자바 바이트 코드를 실행할 수 있는 주체이다.

운영체제 위에서 동작하는 프로세스로 자바 코드(.java)를 컴파일해서 얻는 바이트 코드(.class)를 해당 운영체제가 이해할 수 있는 기계어로 바꿔 실행시켜주는 역할을 한다.

JVM의 구성을 살펴보면 크게 4가지(Class Loader, Execution Engine, Garbage Collector, Runtime Data Area)로 나뉜다.



1. Class Loader

컴파일러를 통해서 생성된 바이트코드(.class)를 엮어서 JVM이 운영체제로부터 할당받은 메모리 영역인 Runtime Data Area로 적재하는 역할을 Class Loader가 한다.

2. Execution Engine

Class Loader에 의해 메모리에 적재된 클래스(바이트 코드)들을 기계어로 변경해 명령어 단위로 실행하는 역할을 한다. 명령어를 하나 하나 실행하는 인터프리터(Interpreter)방식이 있고 JIT(Just-In-Time) 컴파일러를 이용하는 방식이 있다.

JIT 컴파일러는 적절한 시간에 전체 바이트 코드를 네이티브 코드로 변경해서 Execution Engine이 네이티브로 컴파일된 코드를 실행하는 것으로 성능을 높이는 방식이다.

3. Garbage Collector

Garbage Collector(GC)는 Heap 메모리 영역에 생성(적재)된 객체들 중에 참조되지 않는 객체들을 탐색 후 제거하는 역할을 한다.

4. Runtime Data Area

JVM은 프로그램 실행간에 사용되어지는 다양한 Runtime Data Area를 제공한다. 이러한 Data Area 중 몇몇은 JVM이 시작할 때 생성되어지고 JVM이 종료되어질 때 소멸된다. 하지만 다른 몇몇의 Data Area는 스레드마다 할당되어진다. 스레드마다 할당되어지는 Data Area는 스레드 생성시 생성되어지고 스레드 종료시 소멸되어진다.

4.1 The pc Register

JVM은 한번에 많은 스레드의 실행을 지원할 수 있습니다. 각각의 JVM 스레드는 자신의 pc(program counter) register를 갖고 있습니다. 언제든지(At any point), JVM 스레드는 단일 메서드 즉(namely), 해당 스레드에 대한 현재 메서드의 코드를 실행할 수 있습니다. 해당 메소드가 native code가 아닌 경우 pc 레지스터에는 현재 실행중인 JVM 명령어의 주소가 포함됩니다. 만약 현재 스레드에 의해서 실행되고 있는 메서드가 native라면, JVM의 pc 레지스터의 값은 정의되지 않을것 입니다. JVM의 pc 레지스터는 특정 플랫폼에서의 native pointer 또는 return Address를 잡을만큼 꽤 큽니다.

4.2 Java Virtual Machine Stacks

각각의 JVM 스레드들은 Java Virtual Machine Stack을 가지고 있고 스레드와 동시에 생성됩니다. Java Virtual Machine Stack은 프레임(Frame)을 저장합니다. JVM Stack은 C와 같은 기존 언어의 스택과 유사합니다. JVM 스택은 지역 변수와 부분적인 결과를 보유하고, 메소드 호출과 반환에서 역할을 수행합니다. JVM 스택은 프레임을 푸시하고 팝하는 것을 제외하고 직접 조작하지 못하므로 프레임에 heap이 할당될 수 있습니다. JVM 스택의 메모리는 연속적일(contiguous) 필요가 없습니다.

스레드에서의 연산이 JVM 스택이 허용하는 것 보다 더 큰 Stack이 필요한 경우 JVM은 StackOverFlowError을 발생합니다.

JVM스택이 동적으로 확장되어지고 확장을 위해서 이용가능한 메모리가 불충분함에도 확장을 시도하거나, 새로운 스레드를 위해 JVM 스택의 초기화를 하기위해 메모리가 불충분할 경우 JVM은 OutOfMemoryError를 던집니다.

4.3 Heap

JVM은 모든 JVM 스레드 사이에서 공유되어지는 heap 영역을 갖습니다. heap은 모든 클래스의 인스턴스와 배열에 대한 메모리가 할당되는 run-time data area 입니다.

heap은 JVM이 시작되어질 때 생성됩니다. 객체들의 heap 저장소는 자동 저장소 관리 시스템(Garbage Collector)에 의해 회수됩니다. 객체들은 절대 명시적으로 할당이 해제되지 않습니다. JVM은 특정 유형의 자동 저장소 관리 시스템을 가정하지 않으며 저장소 관리 기술은 구현자의 시스템 요구사항에 따라서 선택되어질 수 있습니다. 힙은 고정된 사이즈가 될 수 있으며 또는 연산에 의해서 확장되어질 수 있습니다. 만약 더 큰 heap이 필요하지 않다면 축소되어질 수도 있습니다. 힙 메모리는 연속적(contiguous)일 필요가 없습니다.

JVM은 초기 heap 사이즈를 조절할 수 있도록 프로그래머 또는 사용자에게 제공해주고, 뿐만 아니라 동적으로 힙을 확장 또는 축소할 수 있도록 최대 및 최소 힙 사이즈를 제어할 수 있습니다.

만약 연산이 자동 저장소 관리 시스템으로부터 이용가능한 heap 보다 더 많은 heap을 필요로 한다면, JVM은 OutOfMemoryError를 던집니다.

4.4 Method Area

JVM은 모든 JVM 스레드 사이에서 공유되어지는 method area를 가집니다. method area는 기존 언어의 컴파일된 코드를 위한 저장 영역과 유사하거나 운영 체제 프로세스의 텍스트 세그먼트와 유사합니다. method area는 run-time constant pool, 필드, 메소드 데이터, 메소드와 생성자를 위한 코드와 같은 클래스별 구조를 저장합니다. 이는 인스턴스 초기화와 인터페이스 및 클래스 초기화에서 사용되어지는 특별한 메소드들도 포함합니다.

method area는 JVM이 시작할 때 생성되어지며, 논리적으로 heap의 일부분이지만 간단한 구현에서는 Garbage collector 또는 압축을 선택하지 않을 수 있습니다. 이러한 스펙은 컴파일된 코드를 관리하기 위해서 사용되어지는 정책 또는 method area에 대한 위치를 요구하지 않습니다. method area는 고정된 크기일 수 있고 연산에서 요구된다면 확장되어질 수 있습니다. 그리고 불필요하다면 축소될 수도 있습니다. method area를 위한 메모리는 연속적인 공간을 필요하지 않습니다.

4.5 Run-Time Constant Pool

run-time constant pool은 클래스 파일에있는 constant_pool table에 대한 클래스별 또는 인터페이스별 runtime 표현입니다. run-time constant pool은 컴파일 타임에 알려진 숫자 리터럴부터 런타임에 해결되어야하는 메서드 및 필드 참조에 이르기까지 여러 종류의 상수들을 포함합니다. run-time constant pool은 일반적인 기호 테이블보다 더 넓은 범위의 데이터를 포함하지만 기존 프로그래밍 언어의 기호 테이블과 유사한 기능을 제공합니다.

각각의 run-time constant pool은 JVM method area로부터 할당되어 집니다. 클래스 또는 인터페이스를 위한 run-time constant pool은 클래스와 인터페이스가 JVM에 의해서 생성되어질 때 생성됩니다.

클래스와 인터페이스가 생성될 때 JVM method area에서 이용가능한 메모리보다 더 많은 run-time constant pool 생성을 요구한다면 JVM은 OutOfMemoryError를 발생할 겁니다.

4.6 Native Method Stacks

JVM은 native methods(자바 프로그래밍 언어 이외의 언어로 작성된 메서드)를 지원하기 위해 구어적으로(colloquially) C 스택이라고 하는 기존 스택을 사용할 수 있습니다. Native method stacks은 C와 같은 언어에서 JVM 명령어의 집합을 위한 인터프리터의 구현으로부터 사용되어질 수 있습니다. native methods를 로드할 수 없고, 기존 스택에 의존하지 않는 JVM 구현은 native method 스택을 제공할 필요가 없습니다. 만약 제공되어진다면 native method stacks는 일반적으로 스레드가 생성될 때 스레드마다 할당되어집니다.

5. Frames

프레임은 데이터와 부분적인 결과를 저장하기 위해 사용되어 집니다. 뿐만 아니라 동적인 linking을 수행하고 메소드를 위해 값을 리턴하고 예외를 dispatch 하기위해서 사용되어집니다.

새로운 프레임은 메서드가 호출되어질 때 생성되어지고 메서드 호출이 완료되어질 때 소멸됩니다. 프레임들은 프레임을 생성하는 스레드의 JVM stack으로부터 할당되어집니다. 각각의 프레임들은 자신의 지역변수들에 대한 배열과, 피연산자 stack, 현재 메소드의 클래스의 run-time constnat pool의 참조를 가지고 있습니다.

Frame은 디버깅 정보와 같이 추가적인 구현 스펙 정보와 함께 확장되어집니다.

지역 변수 배열 및 피연산자 스택의 크기는 컴파일 타임에 결정이 되어지고, 프레임과 관련있는 메서드에 대한 코드가 제공되어집니다. 프레임 데이터 구조의 사이즈는 오직 JVM 구현에 의존하며 이러한 구조의 메모리는 메서드 호출과 동시에 할당되어집니다.

메서드를 실행하기 위한 프레임은 제어를 위한 스레드가 활성화 되는 시점에서 활성화 됩니다. 이러한 프레임을 현재 프레임(current frame)이라고 하고 이러한 메소드는 현재 메소드라고 합니다. 현재 메서드가 정의되어진 클래스를 현재 클레스라고 합니다. 지역 변수와 피연산자 스택의 연산은 일반적으로 현재 프레임을 참조합니다.

메서드가 다른 메서드를 호출하거나 해당 메서드가 완료한다면 해당 프레임은 현재 상태가 아닙니다. 메서드가 호출될 때 새로운 프레임이 생성되고 현재 프레임이 되며 제어는 세로운 메서드로 이동합니다. 메서드가 리턴되어지면 현재 프레임은 메서드 호출의 결과를 이전 프레임으로 다시 전달합니다. 현재 프레임은 버려지고 이전의 프레임이 현재 프레임이 될것 입니다.

스레드에 의해서 생성되어지는 프레임은 해당 스레드에 로컬이고 어떠한 다른 스레드에 의해서 참조되어질 수 없습니다.

6. Garbage Collection

Runtime Data Area의 Heap 부분은 5개의 영역(eden, survivor1, survivor2, old, permanent)으로 나뉜다. heap의 영역을 5개로 나눈 이유는 효율적으로 GC가 일어나게 하기 위함이다.

GC는 Minor GC와 Major GC로 나뉜다.

  • Minor GC: New 영역에서 일어나는 GC
    1. 최초에 객체가 생성되면 Eden 영역에 생성된다.
    2. Eden 영역에 객체가 가득차게 되면 첫 번째 GC가 일어난다.
    3. Survivor1 영역에 Eden 영역의 메모리를 그대로 복사한다. 그리고 Survivor1 영역을 제외한 다른 영역의 객체를 제거한다.
    4. Eden 영역도 가득차고 Survivor1 영역도 가득차게 된다면, Eden 영역에 생성된 객체와 Survivor1 영역에 생성된 객체 중에 참조되고 있는 객체가 있는지 검사한다.
    5. 참조 되고있지 않은 객체는 내버려 두고 참조되고 있는 객체만 survivor2 영역에 복사한다.
    6. survivor2 영역을 제외한 다른 영역의 객체들을 제거한다.
    7. 위의 과정중에 일정 횟수이상 참조되고 있는 객체들을 survivor2에서 Old영역으로 이동시킨다.
    8. 해당 과정의 지속적인 반복
  • Major GC(Full GC): Old 영역에서 일어나는 GC
    1. Old 영역에 있는 모든 객체들을 검사하며 참조되고 있는지 확인한다.
    2. 참조되지 않은 객체들을 모아 한 번에 제거한다.
      • Minor GC 보다 시간이 훨씬 많이 걸리고 실행중에 GC를 제외한 모든 쓰레드가 중지된다.

Major GC(Full GC)가 일어나면, Old 영역에 있는 참조가 없는 객체들을 표시하고 그 해당 객체들을 모두 제거하게 된다. 그러면서 Heap 메모리 영역에 중간중간 구멍(파편화)이 생기는데 이 부분을 위해 재구성을 하게 된다(디스크 조각모음처럼 조각난 메모리를 정리함) 따라서 메모리를 옮기고 있는데 다른 쓰레드가 메모리를 사용해버리면 안되기 때문에 모든 쓰레드가 정지하게 되는 것이다.

성능 및 가독성 개선을 위해 쿼리 분리 및 인덱싱 작업을 어떻게 진행하셨나요??

업종 추천을 위해 사용자가 특정 업체를 예약한 시점을 기준으로 특정 기간 동안의 예약 데이터를 가져와야 하는 쿼리가 필요했습니다. 이를 위해 처음 작성한 쿼리는 조인한 테이블이 많고 WHERE절도 많아서 성능도 좋지 않고 가독성도 매우 떨어졌습니다. 이를 해결하기 위해 쿼리의 개수가 무분별하게 많아지지 않는 선에서 쿼리를 분리했고, 쿼리의 개수가 늘어나 발생하는 지연을 줄이고자 인덱싱 작업을 진행 하였습니다. 이를 통해 쿼리의 성능 및 가독성이 증가될 수 있었습니다.

DB 인덱스란 무엇인가요?

DB 인덱스는 데이터베이스 테이블의 동작 속도를 높여주는 자료 구조를 의미합니다. 마치 책의 앞 부분에 존재하는 목차와 같습니다.

인덱싱은 어떠한 경우에 사용하면 좋을까요?

인덱싱은 테이블에 저장된 데이터의 양이 많고 SELECT가 UPDATE보다 잦은 경우, 인덱스를 사용하고자 하는 컬럼의 값이 다양한 경우에 데이터베이스에 인덱스를 추가한다면 성능 개선의 효과를 볼 수 있습니다.

UPDATE나 INSERT가 자주 일어나는 테이블에 수 많은 인덱스를 설정하게 된다면 매번 데이터가 바뀔 때마다 인덱스를 다시 재구성해주어야하는 오버헤드가 발생하기 때문에 성능상의 이슈가 발생할 수 있습니다. 즉, 인덱스는 SELECT 할 때 빛을 발합니다.

여러 컬럼의 값이 존재하는 경우 보통 여러 컬럼을 활용해서 데이터를 검색하곤 합니다. 이러한 과정에서 매번 PRIMARY KEY로 FULL SEARCH 할 수 없기 때문에 자주 검색되는 컬럼에 인덱스를 걸어주면 성능 향상의 효과를 기대할 수 있습니다.

그렇다면 어떠한 컬럼에 인덱스를 설정하는게 좋을까요?

인덱스를 설정하면 좋을 컬럼은

  • WHERE 절과 같은 조건 절에 자주 활용되는 컬럼
    • 당연히 자주 활용되는 컬럼이 인덱스로 설정되면 성능 향상을 기대할 수 있겠지?
  • 카디널리티가 높은(컬럼의 중복되는 값이 많이 없는) 컬럼
    • 카디널리티가 낮다면 어차피 full search 할테고, 이점이 많이 사라지는게 아닐까? 되도록이면 인덱스를 통해서 많은 row 들을 필터링한다면 그게 성능이 좋은게 아닐까?

복합 인덱스는 무엇인가요?

복합 인덱스란 인덱스에 컬럼이 두개 이상 걸려 있는 경우를 의미하구요, 보통 조건문에 걸리는 컬럼이 많은 경우에 사용됩니다. 복합 인덱스에서 컬럼을 지정할 때 보통 카디널리티가 높은 순에서 낮은 순으로 구성하는 것이 성능이 더 좋습니다.

어떠한 자료구조를 사용하여서 인덱싱 작업을 진행 했나요?

저는 PostgreSQL에서 인덱싱 작업시 default로 지원해주는 B-Tree 자료구조를 사용해서 인덱스를 설정하였습니다.

Cardinality

Cardinality란 특정 컬럼에 존재하는 값의 중복도를 나타내는 상대적인 표현이다.

  • 중복도가 ‘낮으면’ Cardinality가 ‘높다’고 표현한다.

  • 중복도가 ‘높으면’ Cardinality가 ‘낮다’고 표현한다.

ex) 주민등록번호는 중복되는 값이 없으므로 카디널리티가 높고, 이름은 종복되는 값이 많으므로 주민등록번호에 비해 상대적으로 카디닐러티가 낮다고 할 수 있다.


API


API 서버 구현을 위해 어떠한 기술을 사용하셨나요?

API 서버 구현을 위해 Spring Boot Web을 사용했구요, Front로부터 업체 추천 요청이 들어왔을 경우 Redis에 캐시된 추천 업체 데이터를 읽어와서 이를 사용해서 PostgreSQL로부터 업체 정보를 읽어와 API 를 통해서 추천 데이터를 제공합니다.

API 를 설계 및 구현 하면서 어려웠던 점이 있으시나요?

API 구현보다는 설계가 더 어려웠던 것 같습니다. 설계 중에서 가장 어려웠지만 재미있었던 점은 “어떻게 하면 좀 더 확장성 및 유지보수성이 좋은 API를 만들 수 있을까?” 에 대해서 고민했던 점입니다. 현재는 제 API서버의 클라이언트는 크게 한 대이지만, 추후에 수 많은 클라이언트가 제 API를 가져다 사용했을 경우 쉽게 확장할 수 있고 쉽게 유지보수가 가능하게 끔 만들어 보고 싶었던 생각에서부터 생겼던 고민이었던 것 같습니다.

그래서 API는 잘 설계 되었나요?

네! 제 API를 사용할 여러 클라이언트들의 입장에서 생각하고 고민한 결과 API 설계는 잘 되었던 것 같습니다. 제 API는 크게 업체 정보 API, 상품 정보 API, 업종 추천 API, 업체 추천 API로 각 API들은 되도록 Restful하게 만들었습니다.

Restful API가 무엇인지 저희에게 설명해주실 수 있나요?

Restful API는 REST 아키텍쳐 스타일을 따르는 API를 의미합니다. REST는 분산 하이퍼미디어 시스템(예, 웹)을 위한 소프트웨어 아키텍처 스타일을 의미하구요 이를 위해 6가지의 제약조건이 존재합니다.

  • Uniform Interface: 구성요소(클라이언트, 서버 등) 사이의 인터페이스는 균일(uniform)해야 한다.
    • 시스템 아키텍처가 단순화되고, 구현과 서비스가 분리되므로 독립적인 진화가 가능하다.
    • 제약조건
      • identification of resources: 리소스가 URI로 식별
      • manipulation of resources through representations: representation 전송을 통해서 resource를 조작해야 한다.
      • self-descriptiove messages: 메시지는 스스로 설명해야한다.
        • ex) Content-Type, API
      • hypermedia as the engine of application state (HATEOAS): 애플리케이션의 상태는 항상 hyperlink를 통해서 전이가 되어야 한다.
  • Layered System: 아키텍처는 계층(hierarchical layers)적으로 구성이 가능해야하며, 각 레이어에 속한 구성요소는 인접하지 않은 레이어의 구성요소를 볼 수 없어야 한다.
  • Cacheable: 클라이언트는 응답을 캐싱할 수 있어야 한다.
    • 효율, 규모 확장성, 성능 개선
  • Client-Server: 클라이언트-서버 스타일은 아키텍처를 단순화시키고 작은 단위로 분리(decouple)함으로써 클라이언트-서버의 각 파트가 독립적으로 개선될 수 있도록 해준다.
    • 사용자 인터페이스에 대한 관심을 데이터 저장에 대한 관심으로부터 분리 (web-server - api server)
  • Stateless: 클라이언트와 서버의 통신에는 상태가 없어야 한다. 모든 요청은 필요한 모든 정보를 담고 있어야 한다.
    • 상태를 저장할 필요가 없으므로 규모확장성(Scalability)가 개선된다.
  • Code-On-Demand (Optional): 서버가 네트워크를 통해 클라이언트에 프로그램을 전달하면 그 프로그램이 클라이언트에서 실행될 수 있어야 한다.
    • 이 제약조건은 팔수는 아니며 Javascript나 Java applet을 의미한다.

SOAP(Simple Object Message Protocol)

SOAP는 일반적으로 널리 알려진 HTTP, HTTPS, SMTP 등을 통해 XML 기반의 메세지를 컴퓨터 네트워크 상에서 교환하는 프로토콜입니다.

GraphQL

GraphQL은 API를 위한 데이터 쿼리 언어입니다. 이를 통해서 클라이언트가 직접 필요한 데이터를 정의하고 서버는 요청된 데이터를 정확하게 반환합니다.

SOAP vs REST vs GraphQL




Batch


추천 시스템에서 Batch Processing을 왜 사용하셨나요?

Web은 실시간 처리를 중요시하기 때문에 사용자로부터 요청이 들어온 순간에 전체 수 십, 수 백만개의 업체 중 추천 대상을 산출해서 응답을 보내는 작업은 시간이 오래걸려서 적절하지 못하다고 생각했습니다. 이를 위해서 사용자가 요청을 보내기 전에 미리 수 많은 예약 데이터를 분석해서 추천 대상을 캐싱해놓고 싶어 Batch Processing 즉, 일괄 처리를 도입하였습니다.

Batch Processing을 위해서 Spring Batch를 사용하게 된 이유가 있나요?

Batch Processing을 위해서 Spring Batch를 사용하게 된 대표적인 이유는 Spring Batch의 기본적인 프로세스만 이해한다면 비즈니스 로직에 집중할 수 있다는 점에서 매력을 느껴 Spring Batch를 사용하였습니다. 이 외에도 트랜잭션 관리, 장애 대응 전략 등 다양한 기능을 제공해 줘서 Spring Batch를 사용하였습니다. 또한 사실 Spring Framework에서 적용하고 있는 철학이나, 전략, 핵심 기술들이 익숙해서 Spring Batch를 사용했던 이유도 있긴 합니다.

Spring Batch의 구조에 대해서 저희에게 간단히 설명 해주실 수 있나요?

Spring Batch에는 하나의 배치 작업 단위인 Job이 존재하구요, Job은 독립적이고 순차적인 단계를 의미하는 Step의 집합으로 구성되어집니다. Step은 Chunk 지향으로 데이터를 처리할 수 있는 Reader, Processor, Writer로 구성되어질 수 있고, 간단하게 하나의 execute 메서드를 실행하는 Tasklet으로도 구성되어질 수 있습니다.

chunk 기반의 프로세싱은 무엇인지에 대해서 알려주실 수 잇나요?

Spring Batch에서 구현하는 Chunk 지향 프로세싱은 Spring Batch 버젼 2.0부터 지원해주는 기능으로써 하나의 트랜잭션 내에서 ItemReader에 의해 한 번에 하나의 아이템을 읽어서 Chunk를 만들고 ItemProcessor에 의해 청크를 프로세싱한 후 aggreagate 시킵니다. 아이템을 읽은 횟수가 설정한 commit interval과 동일할 때 aggregated 된 전체 청크는 ItemWriter에 의해서 write되어지고 트랜잭션은 커밋되어지는 프로세싱을 의미합니다.

chunk-oriented Processing과 Tasklet은 각각 언제 쓰이는게 적절하나요?

chunk 지향 프로세싱은 청크 단위로 한번에 하나씩 데이터를 읽어서 처리하고 전송하는 과정이 필요한 경우에 사용하는 것이 적절할 것 같구요, Tasklet은 간단한 스크립트나 단순한 SQL 문을 호출하는 등 Job 수행에는 꼭 필요한 과정이지만 굳이 청크 기반으로 데이터를 처리할 필요가 없는 경우에 사용하는게 적절할 것 같습니다.

Batch에서 어떠한 기준으로 Job과 Step을 나누셨죠?

추천 대상을 산출하기 위해 사용하는 추천 알고리즘 별로 Job을 나누었으며 추천을 위해선 랭킹 작업이 필요한데 랭킹을 매기기 위해 데이터를 가공할 필요가 있는 경우 랭킹 Step 이전에 데이터 프로세싱 Step들을 구성하였습니다.

Spring Batch 메타 테이블 구조에 대해서 알려주실 수 있나요?

JOB

  • BATCH_JOB_INSTANCE
    • JOB이 실행될 때 생성되는 JOB_INSTANCE에 관한 정보를 저장하고 있습니다. 엄연히 말하자면 JOB_INSTANCE는 JOB이 실행될 때마다 생성되지 않고 JOB_PARAMETER 값에 따라 새로 생성됩니다.
    • JOB_PARAMETER에 의해서 동일한 JOB의 여러 JOB_INSTANCE간에 구분이 이루어집니다.
  • BATCH_JOB_EXECUTION
    • JOB(JOB_INSTANCE)을 실행하기위한 단일 시도의 기술적인 개념들을 저장합니다.
    • EXECUTION은 실패 또는 성공으로 끝날 수 있지만, EXECUTION이 성공적으로 완료되지 않는한 해당 EXECUTION에 해당하는 JOB_INSTANCE는 완료된 것으로 간주하지 않습니다.
    • JOB_INSATNCE의 단일 실행을 나타내는 JOB_EXECUTE에 관한 정보를 담고 있습니다.
    • JOB_INSTANCE와 JOB_EXECUTION은 부모(1)와 자식(N) 관계입니다.
  • BATCH_JOB_EXECUTION_PARAMS
    • JOB_PARAMETER에 대한 모든 정보를 기록하고 있습니다.
  • BATCH_JOB_EXECUTION_CONTEXT
    • EXECUTION_CONTEXT와 관련된 모든 정보를 기록합니다.
    • 1개의 JOB_EXECTION에 각 JOB_EXECUTION_CONTEXT가 있으며 특정 작업 실행에 필요한 모든 데이터를 포함합니다. 일반적으로 JOB_INSTANCE가 중지된 위치에서 다시 시작할 수 있도록, 실패(Fail)이후 지점에 State를 나타냅니다.
  • BATCH_JOB_EXECUTE_SEQ, BATCH_JOB_SEQ
    • JOB_EXECUTE와 JOB의 순서 관리 테이블입니다.

Step

  • BATCH_STEP_EXECUTION
    • STEP을 실행하기위한 단일 시도에 관련된 정보를 저장합니다. 각 STEP이 실행될 때마다 STEP_EXECUTION이 생성되고 저장됩니다.
    • 이전 STEP이 실패해서 다음 STEP의 실행 또한 실패한다면, 어떠한 STEP_EXECUTION도 persist(영속화) 되어지지 않습니다. STEP_EXECUTION은 실제로 해당 STEP이 시작될 때만 생성됩니다.
    • 각각의 STEP_EXECUTION은 해당 STEP의 참조와 관련된 커밋과 롤백 개수, 시작과 종료 시간등 트랜잭션과 관련된 데이터를 포함합니다.
  • BATCH_STEP_EXECUTION_CONTEXT
    • STEP의 EXECUTION_CONTEXT와 관련된 모든 정보를 저장합니다.
    • STEP_EXECUTION 당 1개의 STEP_EXECUTION_CONTEXT가 있으며 개발자가 batch 실행에서 유지해야하는 모든 데이터(재시작을 위한 정적인 정보 또는 통계 정보)를 포함합니다.
  • BATCH_STEP_EXECUTION_SEQ
    • STEP_EXECUTION의 시퀀스관리 테이블입니다.

JobRepository는 무엇인가요?

JobRepository란 Batch의 Stereotype 인 JobExecution, StepExectuion 등을 위한 영속성 메커니즘입니다. JobRepository는 JobLauncher, Job, Step 구현을 위한 CRUD 작업을 제공합니다. Job이 시작되어질 때, repository로 부터 JobExecution을 가져오고, 실행 과정에서 StepExecution과 JobExecution 구현을 repository로 전달하여 유지합니다.

Fault Tolerant 시스템을 위해 Batch에 어떠한 장애대응 전략을 도입하셨나요??

Batch Processing 간에 NPE나, JSONException 등이 발생했을 경우, 해당 chunk 처리를 최대 10회 skip 하는 전략인 Skip 정책을 도입했습니다. 그리고 DB lock이나, 다른 일시적인 이유에 의해서 TransientDataAccessException이나 RedisBusyException 등이 발생했을 경우 300ms의 시간을 두고 최대 3회 재시도하는 정책인 Retry 정책과 Back-off 정책을 도입하였습니다.

Batch Process Scheduling을 위해서 무엇을 사용하셨나요?

Batch Process 스케줄링을 위해서 BatchJobLaunchScheduler 클래스를 구현해 매일 아침 7시에 Batch Process가 동작할 수 있도록 Spring Scheduler를 사용해서 스케줄링을 구현하였습니다.

Spring Scheduler를 구현했을 경우 단점이 뭘까요?

만약 배치 작업이 너무 거대해서 클러스터로 구성하여 스프링 스케줄러로 스케줄링 하였을 때, 하나의 배치 노드에서 전체 클러스터의 스케줄링 작업을 수행한다는 것이 어색하다. 만약 해당 스프링 스케줄러 기능이 포함된 배치 노드가 다운되었다면 배치 실행을 안할 것인가..? 즉, 장애대응에 완벽한 구조를 가지고 있지 않다. 그나마 좀 더 완벽한 장애대응을 가지는 CI 툴의 사용을 고려해 보자.

CI 툴을 Scheduler로 도입했을 시 장점

CI툴은 대부분 Slack이나, Email, SMS 등 다양한 Integration을 제공하고 실행 이력, 로그 관리, Dashboard등 UI가 굉장히 잘 되어 있습니다. 뿐만 아니라 REST API, Scheduling, 수동 실행 등 다양한 실행 방법을 제공하기 때문에 CI 도구를 Scheduler로 도입했을 경우 많은 장점들이 존재합니다.

Spring Scheduler

  • 장점
    • 스프링에서 자체적으로 지원하는 기능으로써 애플리케이션 내부에서 쉽게 구현할 수 있어 비용이 적게든다.
    • 자바 어플리케이션을 띄우고 주기적으로 실행할 수 있으므로 스케줄된 메소드를 정시에 실행 가능하다.
  • 단점
    • 스케줄러 interval이 매우 짧을 경우 작업 수행시간이 interval 시간보다 길어질 상황이 생길 수 있다.
      • Task 수행을 멀티스레딩으로 설정하는 것을 통해 직접 커스터마이징 가능
    • interval 시간보다 긴 작업이 존재한다면 다른 작업들도 제 시간에 실행되지 않는 문제가 발생할 수 있다.
      • 비동기 애노테이션(@Async)로 아직 작업이 끝나지 않았더라도, 새로운 스레드를 할당해 작업을 수행할 수 있다.
    • 멀티 서버일 경우 스케줄러가 중복 실행되는 문제
      • ShedLock 잠금으로 해결 가능
        • ShedLock은 스케줄링된 task가 최대 동시에 한번만 실행가능하도록 만들어진 라이브러리입니다.
        • 만약 테스크가 하나의 노드에서 실행되는 경우, 다른 노드로부터 같은 테스크의 실행을 막기위해 잠금을 획득합니다. 즉, 하나의 작업이 이미 한 노드에서 실행중인 경우 다른 노드에서의 실행은 대기하지 않고 단순히 건너 뜁니다.

Front-End (업데이트 예정)


리액트를 사용한 이유는 무엇인가요??

리액트를 사용해서 UI를 확장성 및 재사용성이 높은 컴포넌트의 집합으로 개발하기 위해 사용하였습니다. 또한 리액트에서 사용할 수 있는 스타일 라이브러리나 상태 관리 라이브러리를 사용하고 싶어서 리액트를 사용하기도 하였습니다.

프론트를 개발하면서 어려운 점은 없으셨나요?

백앤드 개발에 집중하다 보니 프로젝트 막바지에 프론트앤드 개발에 투자할 시간이 상대적으로 많이 없었는데요, 이러한 상황에서 리액트도 처음 사용하다보니 리액트에서 제공해주는 기능 하나 하나를 완벽히 이해하고 가져다 쓰기에는 어려운 점이 많았습니다. 일단 프론트 앤드는 이상 없이 요구사항 전부를 구현 했으나, 리액트의 장점과 특징들을 제대로 활용하지 못하고 그냥 가져다 사용한 것만 같아서 생산성과 유지 보수성이 높은 코드를 작성하는게 가장 어려웠습니다.

리액트의 대표적인 특징은 무엇인가요?

리액트의 대표적인 특징으로는 UI를 상태를 가지는 간단한 컴포넌트의 집합으로 표현할 수 있고, Virtual DOM을 사용하여 변화가 필요한 곳만 렌더링 할 수 있다는 특징을 가지고 있습니다. 또한 리액트는 엄청나게 큰 생태계를 가지고 있어 다양한 기능들이 새로 추가되고 유지보수 되고 있으며 이를 통해 개발자들이 좀 더 쉽게 UI를 개발할 수 있게 해줍니다.