신입 개발자의 첫 홀로서기 프로젝트

Jihye Woo
29CM TEAM
Published in
16 min readMar 11, 2022

안녕하세요!! 29CM 를 첫 직장으로 삼고서 5개월차에 접어든 병아리 개발자 우지혜입니다🐣

오늘은 어려운 기술 이야기 대신, 가볍게!! 신입 개발자로서 처음 프로젝트를 온전히 맡아 진행하면서 겪었던 어려움과 해결과정을 공유드리러 왔습니다~~

배포 전

🏁 최대 혜택가 프로젝트 시이작~!

상품 가격에 추가로 적용할 수 있는 모든 혜택 정보를 상품 페이지에 제공하고자 ‘최대 혜택가’라는 기능을 개발하게 되었는데요!

이전까지는 할인을 적용한 최종 가격 확인을 위해서는 주문서 페이지를 방문해야 했지만, 최대 혜택가 기능으로 상품 상세화면에서도 유저가 직접 모든 혜택들을 조합해 볼 수 있게 되었습니다!

최대 혜택가 적용 전후 사진도 함께 들고 왔어요✨

최대 혜택가 적용 전과 후

가장 먼저 했던 작업은 기획서를 보고 개괄적인 체크리스트를 작성하는 것이었습니다.

  • 프로모션 테이블에 1:N으로 상세 할인 조건 테이블 붙이기 (어드민 작업 필요)
  • 유저 서비스에 마일리지 정보만 조회하는 API 추가
  • 쿠폰 발급 시, 고려해야하는 조건 파악
  • 최대 혜택가 계산식 작성
  • 최대 혜택가 기능 전용 서비스 구현 및 계산 로직 작성
  • 등등.. ( + 그리고 코틀린 배우기🐥)

체크리스트를 보면 간단해보였습니다.

하지만 본격적으로 개발에 착수해보니 어잌후.. 생각보다 쉽지 않았습니다.

💡 도메인 파악의 어려움

보시다시피, 혜택을 제공하는 주체가 다양합니다. 최대 혜택가에서는 크게 4개의 영역 (상품, 쿠폰, 결제수단 및 카드사 별 프로모션 혜택, 보유 마일리지) 에 대한 혜택 정보를 제공하고 있죠!

개발자 측면에서 보자면, 여러 도메인을 조합하는 과정이 필요하고 각 도메인에 대한 지식이 필요한 기능입니다.

  • 상품 할인 (상품 도메인)
  • 쿠폰 할인 (쿠폰 도메인)
  • 프로모션 할인 (프로모션 도메인)
  • 보유 마일리지 (유저 도메인)

그래서 처음 프로젝트를 맡았을 때 혹시나 도메인 지식이 부족해서 놓치는 일이 없을까, 특히 쿠폰이나 프로모션 쪽에서 내가 모르는 사각지대의 조건 값들이 있지는 않을까 걱정이 있었습니다.

물론 기획에서 어느 정도 도메인에 대해 기술해주고 있지만, 개발하는 과정에서 새롭게 추가되는 사항들도 많았고, 엣지 케이스들이 많이 등장해서 빠진 부분이 없는지 지속적으로 체크해야 했습니다.

고려해야할 무수한 조건들.. (근데 이게 다가 아니에요)

그래서 처음엔 엣지 케이스들을 파악하기위해, 유저 입장에서 최대 혜택가 기능을 사용한다 생각하고 무작정 머릿속으로 시뮬레이션해봤습니다! 하지만 생각보다 유저가 처해있는 상황과 각기 다른 혜택들의 조건에 따라 달라지는 케이스들이 많았고 머릿속으로 모든 것들을 그려내기는 벅찼습니다.

🔎 그래서 어떻게 파악했냐면요..

엣지 케이스들을 파악하기 위해서는 어떡해야할지 고민이 많았고, 제가 선택했던 해결방법은 의사 결정 테이블이었어요!

그걸 하나의 테이블 형태로 표기해보니 모든 상황들이 한 눈에 들어오더라구요! 상태 값이나 조건의 조합이 어떤 결과를 내놓아야 하는지 시각적으로 파악하기 훨씬 쉬워졌습니다.

끄적거렸던 손그림 테이블을 몇 개를 예시로 첨부합니다..ㅎㅎ

코드 작업 시간보다도, 조건 값을 확인하는 시간이 더 오래 걸렸던 것 같아요 ㅎㄷㄷ (이 작업 할 때, 초콜릿을 유독 많이 찾았던 기억이..🍫)

테이블아 너 덕분에 살았어 고마웠다

👩‍🔧 피드백과 테스트의 무한 굴레

코드 작업을 1차로 마치고서 기능을 검증하는 시간도 필요했습니다

하지만 위에서 말했던 대로, 단순히 코드레벨의 테스트를 통해서 검증할 수 없는 것들이 많았습니다

여러가지 도메인과 다양한 상태들이 복합적으로 작용하는 기능이라서, 여러가지 방면에서 체크해야할 요소들이 많았고 QA 테스트와 내부 피드백을 통해서도 놓친 부분들을 찾아나가는 과정을 거쳤답니다✨💯

QA 환경에 기능을 오픈하고, 스태프 분들께 피드백을 요청드렸습니다! 감사하게도 29 동료분들이 꼼꼼하게 의견을 주셨고, 개선 및 수정해야할 부분들은 체크리스트로 만들어서 보수작업을 진행했습니다

다양하고 상세한 피드백을 받고, 엣지 케이스들을 발굴해냈습니다

그리고 LIVE 배포 직전에도 최대 혜택가 프로젝트를 담당했던 스탭들끼리 좀 더 자세하게 테스트 항목 나눠 인수 테스트를 디바이스 환경 별로 진행했습니다.

QA 테스트 리스트와 다시 수정해야 할 체크 리스트

그렇게 라이브에 배포하고서 다 끝났다고 생각했지만.. 역시나 문제가 발생했습니다

배포 후

🐢 성능 개선의 필요성

29CM는 기존의 큰 모놀리식 서비스를 작게 분리하여 도메인 단위의 작은 서비스들이 REST API를 통해 필요한 데이터를 주고받습니다.

최대 혜택가는 여러 도메인들이 얽혀있기 때문에 다른 서비스로의 호출 구간이 많습니다. 그리고 최대 혜택가 API 호출 건수도 많을 것이라 예상하고 있어서 성능 개선을 고려해야 했습니다.

그래서 라이브 배포를 하고서 점진적으로 성능 개선을 해야겠다.. 라고 마음 먹었지만…

그런 느긋한 소리를 할 때가 아니었습니다😱

배포 하자마자 모니터링 alert에 제가 만든 최대 혜택가 API가 시도때도 없이 등장하고..

내 목은 타들어가고.. (또 다시 초콜릿을 찾기 시작했던 때..)

당장 성능 개선이 필요한 상황이었습니다.

📀 성능 개선 1 : 커버링 인덱스

희창님의 도움으로 가장 먼저 확인해봤던건 인덱스였습니다.

실행 계획 시각화 툴 사용법 (only for Postgresql)

1. EXPLAIN 키워드를 통해 실행 계획을 조회하되, json 형태로 리턴받는다.
2. json 형식의 실행 계획 결과를
실행 계획 시각화 사이트에 붙여넣는다.

OMG.. 문제가 되는 쿼리의 실행계획을 확인해보니 쿼리 수행 비용이 1028이었습니다.

쿼리를 실행하는 코드로 당장 달려가보았습니다.

쿼리에서는 전체 컬럼을 조회해서 가져오기 때문에 INDEX SCAN을 탈 수 밖에 없었고, 인덱스 로직을 다시 보니 몇 개의 컬럼 정보들만 필요하다는 것을 확인했습니다.

INDEX SCAN : 대상 데이터를 찾기 위해 인덱스 뿐만 아니라, Disk 영역의 Data Page까지 접근해야 하는 경우를 말합니다. 인덱스를 참고해서 특정 Data Page만을 조회하므로 전체 데이터를 조회하는 TABLE SCAN보다 속도가 빠릅니다.

INDEX ONLY SCAN : 인덱스 트리를 타기만 하면 원하는 모든 데이터를 조회할 수 있는 경우를 말합니다. Data Page를 조회할 필요가 없으므로 (즉, Disk 영역에 접근하지 않아도 되므로) INDEX SCAN 보다 속도가 빠릅니다.

∴ 속도 : TABLE SCAN > INDEX SCAN > INDEX ONLY SCAN

그래서 인덱스 트리만을 조회해도 필요한 정보를 모두 가져올 수 있도록 커버링 인덱스를 적용하기로 했습니다.

커버링 인덱스를 타도록 하기 위해서는 SELECT, WHERE, ORDER BY, GROUP BY 등 쿼리에 사용되는 모든 컬럼들이 모두 인덱스를 구성하도록 만들어야 합니다.

현재 혜택 계산 로직에서 필요한 값은 coupon(쿠폰 엔티티), couponUseEndAt(쿠폰 유효기간), isCouponUsing(쿠폰 사용 여부) 세 가지였기 때문에 세 가지 컬럼값을 인덱스에 포함해야 했습니다.

그리고 커버링 인덱스를 만들기 위해서는 SELECT 절 뿐만 아니라, WHERE 절에서 사용되는 컬럼 또한 인덱스에 포함을 시켜야합니다. 해당 쿼리에서는 userId(쿠폰 발급 유저)와 isDeleted(쿠폰 삭제 여부)가 WHERE 절에 사용되므로 두 컬럼값까지 포함해서 복합 인덱스를 생성했습니다.

CONCURRENTLY 옵션

원래 인덱스를 생성할 때는 해당하는 테이블을 읽기 전용으로 잠그게 되지만, CONCURRENTLY 옵션을 추가해주면 인덱스를 만들면서도 insert, update, delete 작업을 허용합니다 (물론 잠궈놓고 인덱스를 생성할 때보다는 오래걸리겠지만, 운영 환경에서 인덱스를 만들 때는 반드시 고려해야하는 사항) [참고]

그리고 기존에 coupon_issue 테이블의 전체 정보를 조회해오는 SELECT 절을 필요한 컬럼값만 프로젝션하도록 아래와 같이 변경했습니다

그 결과 해당 쿼리를 사용하는 최대 혜택가 API의 request duration time이 매우 개선되었습니다.

그리고 수정된 쿼리의 실행계획을 다시 살펴보니, INDEX ONLY SCAN을 타고 있었고 cost는 1000에서 7.03으로 확 줄어들었습니다. (예에에 🎉🎉)

🤔 리인덱싱 비용에 대한 고민

원래 인덱스를 구성하는 특정 컬럼의 값이 자주 변경된다면, 그만큼 인덱스를 재구성해야하기 때문에 인덱스 관리 비용이 늘어나게 됩니다.

저의 경우에는 인덱스를 구성하는 컬럼 중, 쿠폰 사용 여부를 판단하는 컬럼이 업데이트가 발생할 가능성이 높기 때문에 관리 비용에 대해서 생각할 수 밖에 없었습니다. 하지만 그럼에도 불구하고 해당 컬럼을 포함해서 커버링 인덱스를 사용했을 때의 이점이 너무 컸기 때문에 양쪽 사이에서 고민이 많았습니다.

그래서 매주 금요일 진행하는 백엔드 엔지니어링 시간에 이 고민을 털어놓게 되었고, 준영님이 Postgresql 의 include라는 특수한 키워드를 공유해주셨습니다! ✨🙇‍♀️👏 [관련 공식 문서]

인덱스 생성 시 특정 컬럼을 include 절에 지정해놓으면, 인덱스 트리에서 해당 컬럼은 non-leaf node에 포함되지 않고 leaf node에만 저장됩니다. 즉, 인덱스 탐색 키 역할은 하지 않지만, 인덱스 트리에 포함되어있기 때문에 테이블 전체를 탐색하기 위해 디스크 영역까지 접근할 필요가 없어지게 되는거죠!

include 절에 대해 잘 설명해놓은 링크를 첨부합니다 🔗
https://use-the-index-luke.com/blog/2019-04/include-columns-in-btree-indexes#include

아쉬운 점은, postgresql에 특화된 기능이라서 차후에 MySQL로 DB를 변경하게 될 경우에는 다른 대안을 찾아봐야할 것 같아요🥺

🧐 성능 개선 2 : Heap Dump 분석

인덱스로 성능 개선을 한 뒤에도 계속 프로모션 서버가 삐그덕거리는 문제가 발생했습니다.

다함께 모니터링 중! (언제나 든든한 29 동료들 ❤️)

모니터링 툴로 확인을 해보니, 메모리가 계속 차오르다가 결국 OutOfMemory가 터져서 프로모션 서버가 죽는 현상이 발생했고 메모리와 관련된 문제가 있다는 것을 확인할 수 있었습니다.

하지만, 코드를 확인해봐도 메모리를 극단적으로 많이 잡아먹는 로직은 없어보였습니다. 그렇다고 GC가 작동을 아예 안하는 건 아닌데, GC가 해제하는 메모리에 비해, 메모리가 차오르는 속도가 더 컸습니다. (뭔가 새로 할당되는 메모리가 많았던거죠🤔)

며칠간 원인을 발견하지 못해, 원인 찾기에 현상금까지 걸렸습니다 ☠️

그래서 Heap Dump를 떠서 원인을 파악해보기로 했습니다.

확인해보니 쿼리 캐싱이 메모리 릭의 주범이었습니다.

하이버네이트 내부에서 실행되는 쿼리를 저장할 때, in 절의 갯수가 달라지면 다른 쿼리로 인식하고 그때마다 쿼리를 캐싱하고 있었거든요👀

예를 들어, 파라미터 갯수가 다른 것만 빼면 모든게 동일한 쿼리가 있다고 할지라도 in 절의 파라미터 갯수가 다르다는 이유로 달리 캐싱을 해주게 됩니다.

따라서 쿼리에서 다루고 있는 파라미터 갯수가 최대 n개까지라고 가정하면, 총 n개의 쿼리가 캐싱이 되는거죠 🚨

그렇게 캐싱된 쿼리들이 QueryPlanCache를 점유하게 되면서 메모리가 부족해지고 결국 메모리 릭이 발생하게 되었습니다

in (?)          --> 캐싱
in (?, ?) --> 캐싱
in (?, ?, ?) --> 캐싱
in (?, ?, ?, ?) --> 캐싱

다행히도 하이버네이트 설정 중에서 in_clause_parameter_padding이라는 옵션이 있었고 해당 옵션을 true로 켜주면서 문제를 해결할 수 있었습니다. (디폴트 값이 false라서 사용하고 싶을 경우 직접 true로 옵션 값을 변경해주어야 합니다)

해당 옵션을 적용하면, 아래와 같이 in 절 쿼리를 2의 제곱단위로 생성 및 캐싱하게 됩니다

in (?, ?, ?)     --> in (?, ?, ?, ?) 캐싱 
in (?, ?, ?, ?) --> in (?, ?, ?, ?) 재사용

예를 들어, 위와 같이 쿼리 캐싱이 되어있다고 가정했을 때, in절 인자로 1,2,3이 있는 쿼리를 실행하게되면 맨 마지막 인자(3)를 패딩해서 쿼리를 재사용합니다

in (1, 2, 3) --> in (1, 2, 3, 3) 재사용 — 캐시 hit

Hibernate Query Plan Cache에 대해 잘 설명되어있는 블로그를 공유합니다
https://americanopeople.tistory.com/377

힙 덤프 분석은 잘 모르는 부분이 많아서 헤메고 있었는데, 이번에도 역시나 많은 29 동료분들이 도와주어 무사히 해결할 수 있었습니다🙏

📝 회고해봅시다

하나의 프로젝트를 맡아서 진행했다고 하지만 사실 코드만 혼자 작성했을 뿐, 함께 이루어냈다는 느낌이 강했던 것 같아요! 29 동료분들의 많은 관심과 리뷰, 피드백이 더해져서 프로젝트를 잘 마무리할 수 있었습니다!

특히 개인적으로 성능 개선을 하면서 커버링 인덱스를 적용해본 경험이 매우 좋았고, 도메인에 대한 이해도가 좀 더 높아져서 앞으로의 개발 작업에도 도움이 많이 될 것 같아요! 프로젝트를 믿고 맡겨주신 덕분에 좋은 경험을 할 수 있었습니다🙏

성능도 여전히 고민

아직 성능 측면에서도 걱정스러운 부분이 있습니다. 아무리 성능을 개선했다고 하더라도 타 서비스의 의존도가 높기 때문에(상품 서비스와 유저 서비스 호출) 둘 중 한군데에서라도 장애 혹은 성능 문제가 나면 최대 혜택가 API도 영향을 받게됩니다. 모니터링 alerting에서 앞으로도 반갑지 않은 재회를 하게 될 가능성이 농후하죠

차후에는 정적인 데이터는 캐싱을 고려해보면 좋을 것 같습니다. 특히 외부 호출이 있는 상품(아이템) 부분은 캐싱을 할 때 이점이 훨씬 커질 것 같아요. 물론 Cache Hit을 최대화할 수 있는 방향으로 캐싱 전략은 잘 짜야하겠죠!

예를 들어, 특정 시간대에 인기 상품의 상세 페이지 트래픽이 급격하게 증가하면, 당연히 특정 상품에 대한 최대 혜택가 API 트래픽도 함께 증가합니다. 이때 최대 혜택가가 같은 아이템 정보를 동일하게 호출하는 구간이 발생하는데, 그걸 캐싱하게 되면 매우 효율적으로 캐시를 사용할 수 있습니다👍 (준영님, 호길님이 조언해주신 부분🙏)

대신 아이템의 할인률이 변동될 가능성이 있기 때문에, 캐시 만료시간(TTL)을 짧게 가져가는 전략이 좋을 것 같아요

📉 기획은 항상 변할 수 있다

기획은 항상 변할 수 있고, 개발자는 항상 거기에 대비해야한다는 점을 깊이 느꼈습니다.
최대 혜택가 기획이 한 번 홀딩된 적이 있었는데, 기획이 수정되고 난 뒤 다시 개발을 시작할 때 이전에 작성한 코드들을 파악하는 데 시간이 꽤 걸렸습니다. 변경이나 확장이 발생할 만한 지점을 미리 설계 단계에 생각해보고서 개발에 착수하면 좋았을 것 같다는 아쉬움도 있습니다.

🍯 회사 분위기 맛보기

프로젝트를 진행하면서 회사 분위기나 문화에 대해서도 많이 느꼈어요!

자연히 29 자랑도 좀 하고 싶어졌구요❤️

적극적인 커뮤니케이션

그리고 코로나라서 재택인 경우가 많아서 소통이 힘들거라고 생각했지만, 놉놉! 쓰레드에 도움 요청을 하면 댓글이 우수수🍃 선물&예약 스쿼드가 아니더라도 많이 관심 가져주시고, 의견을 주셨어요!

어떤 포지션에 있든, 연차가 얼마나 되었든간에 상관없이 자유롭게 의견을 낼 수 있는 분위기라는 느낌이었습니다! 이상한 질문이 아닐까 싶은 것들도 제대로 잘 들어주고 답변해주셔서 매번 너무 감사드렸어요🙏

최대 혜택가 리뷰

메모리릭이 발생했을 때도 다들 각자의 방법으로 분석해주시기도하고, 관련된 문서나 링크가 있으면 공유하는 등 매우 활발히 의견이 오고 갔습니다. 덕분에 덤프 분석하는 법도 배우고, GC 나 최적화에 좀 더 관심이 생겨 공부하게되었습니다🙏

메모리릭 에러 당시 매우 핫했던 쓰레드

그리고 조직이 스쿼드 단위로 운영되다보니 PO, UX 디자이너 그리고 FE 개발자 분들과 소통할 일이 많습니다. 그래서 개발 외적으로도 의견을 자유롭게 이야기해서 반영할 수 있고, 자연히 맡고있는 도메인에 대해서 오너십을 가지게 되었던 것 같아요!

☕️ 소소한 리프레시 : 커피내기 가위바위보

개발에 꼭 필요한 커피는 항상 가위바위보 내기를 통해서 공급(?)받고 있습니다. 이 시간이 심장 쫄깃하면서도 제일 재밌어요✨ 중독성 갑

어쩌면 전 가위바위보하러 출근하는걸지도..

(참고로 전 거의 이겨서 맨날 커피를 얻어먹곤합니다 감사드려요 여러분)

#가위바위보 슬랙 채널 살짝 오픈!

가위바위보도 하고, 가끔은 보드게임도 같이 하면서 친해진 덕분에 좀 더 빨리 적응할 수 있었습니다ㅎㅎ (최근에 한 구스구스덕도 꿀잼)

!!물론 가위바위보나 게임은 모두 자율참석!!

👋 이제 마무리

오늘은 다른 글들과 달리 간단하게 신입이 겪었던 프로젝트 진행 스토리와 회사 분위기를 살짝 보여드렸습니다! 다음에 또 만나요❤️

[함께 성장할 동료를 찾습니다]

29CM ((주)무신사)는 3년 연속 거래액 2배의 성장을 이루었습니다.

이제 더 큰 성장을 위해 기존 모놀리틱 서비스 구조를 마이크로서비스 구조로 전환하고, 앵귤러 기반 프론트엔드 코드를 리액트로 전환하는 등의 기술적인 시도를 진행하고 있습니다. 모바일 앱 내부 구조도 모듈러 아키텍처로 개선하는 과정에 있습니다. 함께 성장하고 유저 가치를 만들어낼 동료 개발자분들을 찾습니다.

🚀 29CM 채용 페이지 : https://www.29cmcareers.co.kr/

📌채용이 완료되면 공고가 닫힐 수 있으니 빠르게 지원해주세요!

--

--