본문 바로가기
CodeStatesBootCamp/Review

Project Section - Main Project Final Week4 Review : 커튼콜

by JKROH 2023. 7. 30.
반응형

   한 달이라는 짧은 시간이 눈 깜짝할 새 지나가고, 프로젝트는 발표까지 모두 마무리가 되었다. 전반적으로 만족스러운 한 달 동안 프로젝트 기간이었지만, 구현에 치여 생각의 과정을 블로그에 기록으로써 남겨놓지 못 한 부분이 못내 아쉬웠다. 그래서 오늘 이 자리를 빌어 랭킹 기능을 구현했을 때와 로그를 남겼던, 프로젝트를 진행하며 가장 기억에 남는 두 순간을 기록해 놓고자 한다. 지금은 기억에 남지만, 결국 기록하지 않으면 기억에서 잊혀지기 마련이다. 그리고 작게나마 팀원분들에게 감사하는 시간도 가질 예정이다.

 

 랭킹은 어려워

- 랭킹 탄생의 배경

 도메인 설계는 어렵다. 처음 프로젝트를 기획할 때, 우리의 기획 중 어느 부분은 도메인 레벨에서 다뤄야할만큼 중요한지를 고려하고 전체 아키텍처와 디자인 설계를 하는 것은 정말 중요하다. 그런데, 기존에는 기획하지 않았던 것을 추가로 넣는 것은 더더욱이 어려움을 이번 프로젝트를 진행하며  알게 되었다.

 

 랭킹은 원래 없던 기획이었다. 우리가 제작한 칵테일 레시피 제공 서비스는 사용자가 칵테일 레시피를 등록하고, 댓글을 통해 사용자 간 소통을 하는 정도의 기능이 전부였다. 이것저것 기능의 수를 늘리느라 깊이 있는 사고를 할 시간이 없게 만드느니, 간단한 CRUD에 최대한 집중해서 깊이 있는 탐구를 하자는게 목표였다.

 

 그런데 막상 1차 구현을 마치고 나니, 메인 화면이 너무 텅비어있었다. 팀 자체적으로 메인 화면에 어떤 기능을 추가하는 것이 좋을까 이야기하는 시간이 있었고, 랭킹을 제안해보았다. 우리 사이트는 애초에 간편한 칵테일 레시피를 제공해주는 것을 기조로 한다. 프로젝트 이름이 '편의점 한잔' 인 것도 이에 기인한다. 그런데 생각해보니 우리가 제공하는 간단한 칵테일 레시피를 찾는 사람들, 즉 우리가 제공하는 서비스의 주 이용 고객은 분명 칵테일을 잘 모르는 사람들이 대다수일 것이었다. 칵테일을 잘 알고 좋아하는 사람들이 편의점에서 살 수 있는 간단한 재료들로 만드는 칵테일 레시피를 찾아오지는 않을 것이다.

 

 문제는 이렇게 우리 서비스를 이용하는 '칵테일을 잘 모르는 사람들'이 어떤 칵테일이 맛있는지, 어떤 칵테일이 사람들에게 인기가 많은지 알고있겠냐는 것이었다. 이러한 생각에서 기인해 나는 메인 화면에 칵테일 랭킹을 출력해주자고 제안하였고, 팀원 분들도 이 생각을 좋게 받아들여주셔서 랭킹 파트를 제작하기 시작했다. 오케이. 이제 랭킹을 만들 때다. 그런데, 랭킹을 만들어보자고 마음 먹은 순간부터 문제가 생겼다. '랭킹은 도대체 무슨 기준으로 정해야하는가'의 문제가 있었다. 

 

 처음에는 별점을 기준으로 하자는 이야기가 오갔다. 그러나 이는 평균으로 주어지는 별점에서 발생할 수 있는 통계의 함정 때문에 기각되었다. 예를 들어, 1명만 5점을 준 칵테일은 무조건 별점 기준 상위권에 있을거니까.(물론 별점 등록 횟수 칼럼이 있어 충분히 통제 가능한 변인이었이만, 당시에는 그런 생각을 하지 못했다) 그럼 무엇을 기준으로 순위를 정할까 생각을 해보니, 북마크 기능이 좋은 방안이 될 것 같았다. 북마크는 별점과 함께 칵테일에 대한 사용자들의 선호가 직접적이고 주체적으로 작용하는 가장 큰 바로미터 역할을 할 수 있었기 때문이다. 우리는 북마크 횟수를 통해 랭킹을 정하기로 결정했다.

 

- 근데... 어떻게 만들어?

 자 이제 북마크 등록 횟수를 기반으로 한 랭킹을 제작할 시간이 되었다. 그런데 문제는, 이걸 어떻게 구현해야할지를 모르겠다는 것이었다. 정확히는 어떻게 구현해야할지를 모르겠다기보다, 지나치게 구현할 수 있는 방법이 많았다. 백엔드 팀장의 역할을 맡은 나였기에, 랭킹은 내가 담당하겠다고 당당하게 말해놓은 터였는데, 어느 방법이 랭킹이라는 기능을 구현하고 표현하기에 가장 적절할지를 정하는 것부터 난관이었다.

 

 처음에는 랭킹 테이블을 만들었다. 화면에 출력하기 위한 정보인 '칵테일 id', '칵테일의 이미지 url', '칵테일 이름' 정보와 해당 칵테일이 몇 번 북마크 되었는지를 카운팅 하기위한 '북마크 수'를 테이블의 칼럼으로 담았다. 이렇게 설정하고 나니 기능 구현은 쉽게 가능했다. 북마크가 될 때마다, 랭킹 테이블도 조회를 하고 새롭게 북마크 수를 업데이트 해주는 것으로 랭킹의 구현이 끝났다. 기능은 잘 작동했다, 그런데 정작 내 코드를 다시보니 '이게 맞나?' 라는 생각이 들었다.

 

 북마크를 하면 해당 칵테일이 이미 북마크 되었는지를 확인하기 위해 랭킹 테이블을 조회하고, 랭킹 테이블을 업데이트 하기까지 한다. 북마크라는 하나의 기능이 동시에 두 개의 테이블에 영향을 미치는 것은 조회가 두 번 발생하기 때문에 성능 차원에서 문제가 있었다. 또한 랭킹을 하나의 엔티티라고 부르기에는 다른 엔티티들과 레벨이 너무 맞지 않았다. 칵테일, 북마크, 댓글, 사용자 엔티티는 모두 하나의 테이블의 이름을 차지할만한큼의 가치를 지닌 객체들이었다. 그들은 생성되고 삭제되는 과정에 있어 다른 객체의 명령이 필요하지 않다. 그러나 랭킹은 스스로 생성되지도, 삭제되지도 못한다. 북마크가 되면 생성되거나 수정되거나 삭제될 뿐이다.

 

 그래서 랭킹 테이블의 삭제를 결정했다. 대신, 북마크 테이블과 칵테일 테이블을 JOIN한 뒤 적절히 정렬해서 랭킹을 결정하는 방법을 선택했다.(처음에는 칵테일 테이블에 북마크 된 수를 저장하는 칼럼을 추가해볼까도 생각해보았지만 결국 처음 방법과 같은 결인 것 같아 사용하지 않았다) JOIN 테이블의 칵테일 정보를 가지고 데이터들을 묶어서 카운팅하면 그게 곧 랭킹이 되니까. 문제는, 그래서 SQL의 COUNT를 JPA에서는 어떻게 사용하는지를 몰랐다는 점이다. 카운트를 사용하는 것은 새롭게 Interface를 생성하는 등의 구현 방법을 요구했다. 동시에 JPQL 작성법과 작동 원리를 더욱 깊이 알아야했고, 여러 블로그들을 탐방하며 공부했다.

 

 최종적으로 구현하을 마치고나니 꽤 그럴듯한 코드가 완성된 것 같았다. 그래봐야 초보적인 코드고 여전히 수정할 부분이 많겠지만, 불필요한 테이블의 수도 줄이고, JPA 및 JPQL에 대한 이해도도 높아졌다. 이후, 로그인한 사용자를 위한 '연령대와 성별을 고려한 랭킹'을 구현하며 또 한 번 리팩토링을 거쳐 현재의 랭킹 컴포넌트가 완성되었다. JPQL에 WHERE절을 적용하는 것은 이미 검색에서 사용했던 기능이라 어렵지 않았다. 다시 돌아보니, 랭킹을 구현하는 과정은 초기의 목표였던 'CRUD에 집중'이라는 목표를 가장 잘 달성하게 해주었던 시간이 아니었다 싶다.

 

- 어떻게 수정해볼까?

 기능 구현은 완성됐지만, 수정해볼만한 점은 남아있다. 가장 크게 다가오는 부분은 '이걸 매번 조회하는게 맞나'라는 점이다. 현재는 메인 페이지에 접근할 때마다 새롭게 테이블을 조회해서 정보를 가져온다. 이는 실시간성을 고려하여 랭킹을 설계했기 때문이다. 현재 우리에게 주어진 데이터는 칵테일 100여개, 북마크 수십여개에 불과하다. 이들을 조인하고 조회하는 데에는 그렇게 많은 시간이 필요하지 않다. 심지어 랭킹 컴포넌트가 메인 화면에서 바로 보이는 것도 아니다. 메인화면에서 살짝 스크롤을 내려야 사용자들에게 노출된다. 다시 말해, 사용자들에게 노출되기까지 아주 잠깐이나마 시간적 여유를 확보할 수 있다는 것이다. 결과적으로, 사용자들에게 정보가 노출되기까지 걸리는 시간과 사용자들에게 주어지는 순위의 정확성의 트레이드 오프에서 실시간성이 보장되어 더 정확한 순위를 제공한다는 점에 무게가 실릴 수 있었다.

 

 그런데 만일 서비스가 확장되고, 데이터 수가 수만개, 수십만개로 늘어난다면 어떨까? 메인 화면에 접근할 때마다 새롭게 테이블을 조인하고 조회하는 로직은 지금에 비해 수십배의 시간을 요구할 것이다. 결국 배치를 이용해 랭킹을 미리 계산해놓고 제공하는 편이 오히려 더 나은 사용자 경험을 유도할 수 있을 것이다. 물론 우리 서비스가 이렇게 성장할 가능성은 굉장히 낮지만, 그럼에도 배치를 공부하고 적용해보는 경험은 한 번 정도는 해보는게 좋을 것 같아 프로젝트 고도화 단계에서 적용해볼 예정이다.

 

로그 관리의 필요성에 관하여

- 어? 안되는데요?

 프로젝트의 전반적인 1차 구현이 끝나고 QA를 진행하면서 아마 가장 많이 나온 말이 아니었을까 싶다. 당연히 오류가 발생하는 상황이 많았다. 세부적인 예외 상황에 대한 처리가 거의 안되있었을테니 말이다. 그래서 우리는 미처 처리하지 못했던 문제들을 QA를 통해 해결하고자 했다. 그런데 문제는 어디가 어떻게 안되는지를 알 수가 없었다는 것이다. 특히, 콘솔에 출력되는 에러를 이해할 수 없을 때는 정말 큰 문제였다. 대부분의 경우에야 콘솔 로그를 읽고 오류를 그래서 뭔가 오류가 발생할 때마다, 천천히 해결할 수 있었지만, 그렇지 않은 경우는 답이 없었다. 결국 오류가 발생할 때마다 우리는 다른 모든 QA를 멈추고 해당 부분을 처음부터 다시해야 했다. 당연히 QA에는 진전이 있을 수가 없었다.

 

 또한, 과거에 발생한 오류를 모두 기억하는 것은 불가능하다. 당장 어저께 어떤 기능을 사용하다 오류가 발생했는지를 기억하는 것도 어렵다. 남아있는 로그가 없다보니, 오류 처리에 대한 재차 피드백이 불가능했다. 아무리 생각해도 이런 식으로 QA가 진행되는 것은 너무나 비효율적이었다. QA를 빠르게 마쳐야 다음 스텝으로 넘어갈 수 있는데, 하루를 꼬박 QA에 투자해도 원하는 만큼의 진전을 얻지 못했다. 결국 우리는 로그를 남기는 것의 필요성을 느꼈다.

 

- 로그는 어디까지 남겨야할까?

 로그를 남기기로 결정한 뒤, 대대적인 로그 작업에 들어갔다. 처음에는 컨트롤러 레벨에서만 로깅을 적용해보았다. 아무런 로그를 남기지 않던 때보다 훨씬 QA속도가 진전됐다. 어느 API를 이용했을 때 문제가 발생했는지를 알 수 있었고, 이를 통해 해당 기능을 이리저리 둘러보며 어디에 문제가 있는지를 파악했다. 그런데, 여전히 부족했다. 하나의 기능은 여러 세부 로직으로 나뉘어 있었고, 우리가 남기는 로그 정도로는 어느 로직에서 에러가 발생하는지는 알 수 없었다. 결국, 서비스 레이어 전반에 걸쳐 로그를 남길 수 있게 코드를 수정했다.

 

 서비스 레이어에 특정 기능이 성공할 때마다 로그를 남기도록 코드를 수정하고 나니, 이제 어느 기능이 수행되지 않아서 에러가 발생했는지를 파악할 수 있었다. 이후에는 요청을 보낸 사용자에 따라 로그를 나누고, 어떤 Request를 보냈는지 등을 저장하는 수준까지 로그를 발전시켰으나, 이 때문에 Dispatcher Servlet 단계에서 Request가 수정되어 몇몇 기능에 에러를 발생시켰다. 서비스 레이어에 로그를 적용시키는 것만으로도 우리가 원하는 정도까지의 로깅은 달성했으니 해당 기능을 수정하기보다는 지금 당장은 사용하지 않기로 결정했다.

 

 그러나 여전히 하나의 문제가 남아있었다. 우리는 여전히 로그를 저장하지 못하고 있었고, 에러 상황에 대한 재피드백을 하지 못했다. 로그를 따로 저장할 수 있는 방법이 필요했다. 찾아보니 자바 진영에서는 Logback이라는 기술을 통해 로그를 저장할 수 있는 기능을 제공하고 있었다. 최종적으로 Logback을 적용하면서 로그 작업을 마쳤다. 이제 우리는 어느 부분에서 오류가 나는지를 보다 쉽게 파악할 수 있었고, 저장된 로그 파일을 통해 과거의 오류 이력을 관리할 수 있게 되었다.

 

- 어떻게 발전시켜볼까?

 로그의 중요성은 알았다. 실제로 적용시켜 보기도 했다. 그러나 로그 관리는 더욱 발전시킬 방법이 무궁무진할 것이다. 당장 우리의 경우만 보더라도, 발생한 로그파일을 여전히 EC2 인스턴스 내부에 저장하고 있기 때문에 로그를 확인하려면 EC2에 접속해야 한다. 이를 쉽게 개선할 수 있는 방법으로는 로그를 따로 S3에 저장하는 방법이 있을 것이고, 좀 더 기술적으로 들어간다면 로그 관리 전용 서버에 로그를 쏴주는 방법이 있을 수 있겠다. 

 

 또, Dispatcher Servlet 레벨에서 발생하는 에러에 대해서는 여전히 로그를 남기지 못하고 있다. 분명히 남기는 방법이 있을 것인데, 프로젝트의 마감 시한이 있다보니 여기까지는 찾아보지 못했다. 단순히 로그를 남기는 방법을 공부할 뿐만 아니라, Web과 WAS에 대한 추가적인 학습을 통한 더 깊은 이해를 바탕으로 해당 부분의 오류를 바라본다면 이 역시 해결할 수 있는 분제라고 생각된다.

 

 로그를 처리하는 다른 방법도 찾아보았다. AWS에서 제공하는 모니터링 서비스인 CloudWatch를 통해 로그를 관리할 수도 있고, 현업에서는 Elastic Search나 Logkit 프레임워크를 사용한다고도 하는데 이 부분에 대해서도 추가적으로 탐구해볼만 할 것 같다. 현업에서는 동시에 수십만 건, 수백만 건의 요청이 들어올 것이고 이를 관리하기 위한 로그를 남기는 것은 높은 기술적 난이도를 요구할 것이다. 이를 처리하기 위해서라도 로그에 대해 더 공부해볼 필요성이 있다.

 

마치며

 프로젝트를 진행하며 했던 모든 고민과 생각을 기록한다는 것은 사실 쉽지 않다. 예를 들어, 어떤 컴포넌트들로 서비스 레이어를 구성할 것인가? 등도 고민의 시간을 거쳐 이뤄진다. 아니면, 패키지 단위를 어떻게 나눌까? 등도 고민의 영역이다. 그러나 이렇게 암묵적으로 지나갈 수 있는 것들을 모두 남겨놓는 것은 쉽지 않기에, 가장 기억에 남았던 두 고민을 남겨 놓았다. 남겨놓고 나니 팀원분들과 함께 으쌰으쌰했던 당시의 순간이 떠올라 감회가 새롭기도 하다.

 

 팀원 얘기가 나온 김에, 감사의 인사를 전하고자 한다. 처음 팀이 구성된 순간부터 프로젝트가 모두 마쳐진 지금까지, 모든 팀원분들이 각자의 자리에서 각자의 역할을 충실히 이행해주셨기에 성공적인 프로젝트 마무리가 될 수 있지 않았나 싶다. 평소에 그렇게 섬세한 타입도 아니고, 업무적인 대화에 있어서는 서로의 감정이 상하지 않는 선에서 최대한 직접적이고 효율적으로 대화하는 것을 선호하다보니 팀원분들에게 얘기치 못한 상처를 준 부분이 있지는 않을까 걱정이 되기도 한다. 

 

 그럼에도 우리가 한 번의 다툼 없이 끝까지 올 수 있었던건, 나라는 사람을 이해해주고 인정해준 팀원분들 덕분이 아닌가 싶다. 이 자리를 빌어 다시금 감사의 말을 전한다.(물론 이 글이 그들에게 닿을지는 모르겠다) 동시에 앞으로 있을 고도화 작업에서도 잘 부탁드린다는 말씀을 드린다. 앞으로 우리가 어떻게 프로젝트를 더 발전시킬지, 얼마나 더 많은 뜨거운 식사()자리를 가지고 어떻게 관계를 이어갈지는 알 수 없지만, 어떤 일이 발생해도 그들의 무궁한 영광과 발전을 기원하는 바이다.

반응형

댓글