기술부채가 늘어나기 시작한, 소프트콘의 프로젝트 국케이원 제작기 (1)

정연진·5일 전
5
post-thumbnail

https://viewership.softc.one
소프트콘 뷰어십은 인터넷 방송 데이터를 이용해 사용자들에게 필요한 데이터를 제공하는 플랫폼으로, 2023년 10월부터 지금까지 열심히 데이터를 모아가고 있습니다.

소프트콘 뷰어십은 DAU 5~6천명 / MAU는 7만명 / 한달 PV 159만을 기록중인 대한민국에서 어느정도 인지도가 있는 뷰어십 사이트로 성장하였습니다.


( 2023년 9월 제작되었던 기획 피그마 )

하지만, 처음 기획에서 사용자들이 원하고 필요한 다양한 데이터가 추가되기 시작했고 그때마다 땜질을 하며 기능을 추가하였던 상황에서 더이상 현재 상태로 뷰어십 프로젝트를 만들어가기엔 코드가 너무 지저분해 질것으로 예상되었습니다.

미뤄왔던 기술부채가 이제 감당하기 힘든 상황으로 치닫고 있었죠.


1. 문제

사이트가 커지면서 프론트, 백엔드 모든 부분에서 문제를 일으키기 시작했습니다. 그저 문제에 지나지 않는다면 모를까 비용 역시 매우 가파르게 상승하고 있었습니다.

가. Vercel, 그리고 배포

( Vercel 계산서, 2024년 4월 )

해당 영수증의 분석툴 가격인 $90은 선택옵션으로, 배포와 호스팅에 전혀 관련없는 비용입니다.

기존 Vercel은 Team Seat를 기준으로 가격을 측정하고 있었습니다. 계산서를 보면 아시겠으나 분석툴을 제외한 배포 비용은 한달 $20 에 불과할 정도로 굉장히 저렴한 상황이였습니다.

Vercel은 서버리스 시스템을 이용하기에 약간의 Cold Boot와 더불어 어느정도 페이지 딜레이가 있으나 가격과 관리측면을 생각한다면 충분히 감당할만한 수준이였죠.

하지만, 올해 6월 Vercel은 새로운 가격정책을 발표합니다.
Data Cache를 전세계 Edge로 보내고 이를 빠르게 받아오는 Fast Origin Transfer와 더불어 여러 비용을 세분화하여 받기 시작했죠.

소프트콘 뷰어십은 Caching 기능을 굉장히 적극적으로 이용하고 있었고, 이러한 가격정책 변화는 굉장히 큰 타격으로 다가왔습니다. Vercel도 이점을 알고 어느정도 유예기간을 주고 10월부터 적용한다는 안내를 보내왔죠.

그리고 저번달, 굉장히 충격적인 영수증이 발행되었습니다.

배포, 호스팅 비용으로 무려 $142.. 팀 시트 $20을 추가해 $162달러..
환율이 1,472원인점을 감안하면 무려 프론트엔드에 24만원이 발생해버리는 상황이 찾아왔습니다.

EC2의 medium 인스턴스를 한달간 써도 5만원이 나올랑 말랑한 상황에서 24만원은 정말 말도 안되는 금액이였습니다.


물론, Vercel의 방화벽 서비스와 공격 방어 모드와 같은 서비스도 있고 모니터링 서비스도 괜찮이 좋긴 했으나 Cloudflare 라는 훌륭한 대안이 있는 상황에서 핵심 기능은 아니였습니다.

소프트콘 뷰어십은 대한민국에서 주로 쓰는 플랫폼입니다. 전세계 캐싱을 해주는 엣지 배포 기능의 이점은 저희 서비스에서 큰 이점이 아니였고 오히려 Cold Boot로 인한 딜레이가 마이너스로 다가오기 시작하는 시점이였습니다.

이제 배포 방식을 바꾸고, 정말 Classic한 웹서버 방식으로 돌아갈때가 왔습니다.

나. Next 15의 등장

소프트콘 뷰어십은 Next.js 13의 App router로 시작했습니다.

아직 App Router에 버그도 많고, 개발자들 사이에 부정적인 시선이 훨씬 강하던 시절 미래는 App Router일 것이다라는 판단에 과감하게 Page router를 사용하지 않기로 했었죠.

그리고 크게 변경되지 않을것이다 라는 생각하에 App Router에 정착하기 시작했습니다.

Next.js 14가 등장하고 13에서 14로 마이그레이션 하는 과정은 사실상 버전 수정에 불과할 정도로 적었습니다. 14.2 버전으로 올라오며 안정성도 많이 확보되고 프론트 사이드의 캐싱인 Data Cache역시 적극적으로 활용할 수 있었죠.

여기서, Next 15 가 등장합니다.


( Vercel, Next15글 일부 )

캐싱 의미론

Next.js App Router는 의견이 반영된 캐싱 기본값과 함께 출시되었습니다. 이러한 기본값은 가장 성능이 뛰어난 옵션을 기본값으로 제공하면서도 필요한 경우 선택적으로 해제할 수 있도록 설계되었습니다.

여러분의 피드백을 기반으로, Partial Prerendering(PPR) 같은 프로젝트와 상호작용하거나 fetch를 사용하는 타사 라이브러리와의 호환성을 고려하여 캐싱 휴리스틱을 다시 평가했습니다.

Next.js 15부터는 GET Route Handlers와 Client Router Cache의 캐싱 기본값을 기본적으로 캐싱됨에서 기본적으로 캐싱되지 않음으로 변경합니다. 이전 동작을 유지하고 싶다면 계속해서 캐싱을 선택적으로 활성화할 수 있습니다.

Next.js의 캐싱 개선은 앞으로 몇 달 동안 지속적으로 이루어질 예정이며, 자세한 내용은 추후 공유하겠습니다.
( 번역, ChatGPT )

Next 15는 기본 캐싱 정책을 force-cache 에서 no-cache 로 변경했습니다. 즉 Data Cache를 이용한 프론트 사이드의 캐싱은 이제 기본이 아니기에 Next 15로 업그레이드 하기 위해선 현재 api에 force-cache 를 설정해줘야 한다는 이야기였죠.

이와 더불어 use cache 의 추가로 기존 사용중인 unstable_cache 는 걷어내야 했고, 계정의 결제 기능등은 작동이 제대로 되지 않기도 했습니다.

( searchParams, params 는 이제 비동기로 요청해야하는 것도 있지만 이는 업그레이드시 지원하는 codemon이 알아서 처리해주니 넘어가겠습니다 )

소프트콘 뷰어십에 존재하는 대부분의 코드가 수정이 발생했고, 이미 기존에 덕지덕지 추가했던 코드와 꼬일대로 꼬여버린 sass 를 이용한 디자인 트리등 기술 부채가 기하급수적으로 쌓여버리게 되었습니다.

다. 불편한 화면 레이아웃

초기엔 깔끔한 디자인을 위해 가로 1200px의 고정 레이아웃을 이용했었지만 실제 사용자들이 보고싶은 데이터가 많은 경우엔 억지로 늘린 확장 레이아웃을 이용하고 있습니다.

누가봐도 굉장히 어색하고 이상하지만 기능을 위해 디자인을 포기한 레이아웃이 나오며 굉장히 불편한 디자인이 등장하기 시작했습니다.

특히 데이터만 봐야하는 페이지에선 표시하는 데이터에 비해 공간이 협소해 데이터 표시가 점점 지저분해지는 일도 발생하기 시작했습니다.

생각해보면 위 사진에서 표기된 숫자는 필요 없는 상황입니다. 요약된 데이터가 해당 페이지 최상부에 이미 존재하는 상황이였거든요.

초기에 고려하지 못햇던 사안들이 점점 눈에 띄기 시작했습니다. 그저 요구사항을 충족하기 위한 개발이 진행되어 버렸고, 디자인이 망가지기 시작했죠.

라. 사용하기 편했지만, 리소스가 많이 드는 Prisma

소프트콘 뷰어십 서버는 ORM으로 Prisma를 사용하고 있습니다.

Prisma를 써본 사람들은 알겠으나, 사용하기 편하고 커스텀하기 어렵습니다. 기본 제공해주는 기능이 꽤 많고 docs 정리가 정말 잘되어 있으나 소프트콘 뷰어십에서 구현하고자 하는 데이터는 기본 제공보다 더 많은 기능들이 필요했죠.

이번 Prisma 6 즈음에 도입된 TypeSQL 역시 이부분을 어느정도 도와주기는 하지만, 실제로 사용해보면 코드의 복잡성만 더욱 올라가는 결과가 발생합니다.

어떤 부분에선 TypeSQL을 이용해 따로 파일을 빼고, 어떤 부분엔 Prisma 쿼리를 그대로 이용하는 등 생각이상으로 코드가 복잡해집니다.

물론, 기존 방식인 queryRaw를 이용해 그대로 쓰는것보다는 낫지만.. 결국 땜질 처방일수 밖에 없습니다.

drizzle ORM

Prisma를 대항하는 새로운 ORM으로, Prisma의 Not SQL-LIKE 가 아닌 SQL-LIKE 이자 Not SQL-LIKE 를 지향합니다.

이게 뭔소리냐구요?
drizzle은 Prisma의 방식과 더불어 SQL-LIKE 방식으로 개발할 수 있습니다. 두가지 방식의 코드를 이용해 입맛에 맞춰 사용할 수 있는데요,

// NOT SQL-LIKE
const parties = await this.db.query.parties.findMany();

// SQL-LIKE
const parties = await this.db.select().from(schema.members);

두 방식의 코드 모두 사용할수 있고, Prisma의 npx prisma generate 없이 스키마 파일을 직접 import 하기 때문에 바로 사용할수 있는 장점이 있습니다.

결과적으로 코드가 매우 깔끔하고 관리가 용이해지는 것과 별도로 내가 쓰는 코드가 SQL로 어떻게 실행될지 확신할 수 있게 됩니다.

Prisma의 경우 JOIN 전략이 변경되면서 하나의 요청에 2개의 SQL을 사용하기도 하는 등 1 ORM 요청이 1 SQL 쿼리라는 것을 확실한 수 없는 경우가 있었고, 더군다나 JOIN의 형태 조차 사용자가 쉽게 결정할 수 없는 문제가 있는 상황이죠.


( Prisma 공식 문서 )

중첩 읽기 (Nested Reads)

중첩 읽기를 통해 데이터베이스의 여러 테이블에서 관련 데이터를 읽을 수 있습니다. 예를 들어, 사용자와 해당 사용자의 게시물을 함께 조회할 수 있습니다. 다음과 같은 작업이 가능합니다:

include를 사용하여 쿼리 응답에 사용자의 게시물이나 프로필 같은 관련 레코드를 포함할 수 있습니다.
중첩된 select를 사용하여 관련 레코드에서 특정 필드만 포함할 수 있습니다. 또한, include 안에 select를 중첩시킬 수도 있습니다.

관계 로드 전략 (Relation Load Strategies) - (프리뷰)

버전 5.8.0부터 PostgreSQL 데이터베이스에서 Prisma Client가 관계 쿼리를 실행하는 방식을 쿼리 단위로 결정할 수 있는 relationLoadStrategy 옵션이 도입되었습니다.

버전 5.10.0부터는 이 기능이 MySQL에서도 사용 가능합니다.

https://www.prisma.io/docs/orm/prisma-client/queries/relation-queries
Prisma는 기본적으로 관계요청시 LATERAL JOIN를 이용합니다. 소프트콘 뷰어십과 같이 1억행이 가뿐히 넘어가는 저희 DB에서 부하가 어떨지에 대한 이야기는 넘어가더라도 이것을 개발자가 직접 선택할수 없다는 것은 굉장히 불안합니다.

매우 간단한 INNER JOIN을 이용해 데이터를 꾸며보고 싶지만, Prisma에선 자체적인 조사를 통해 우리는 LATERAL JOIN을 사용한다라고 이야기 하고 있죠.
https://www.prisma.io/blog/prisma-orm-now-lets-you-choose-the-best-join-strategy-preview

INNER JOIN 을 사용하기 위해선, 결국 Prisma의 장점을 버리고 직접 SQL을 짜는 rawSQL 혹은 TypedSQL 을 사용할 수 밖에 없습니다.

const get_all_vote = await this.db
  .select(selectField)
  .from(schema.votes)
  .where(whereField)
  .innerJoin(
    schema.members,
    eq(schema.votes.member_id, schema.members.member_id),
  )
  .innerJoin(schema.parties, eq(schema.members.party_id, schema.parties.id))
  .innerJoin(schema.bills, eq(schema.votes.bill_id, schema.bills.bill_id))
  .groupBy(
    schema.votes.member_id,
    schema.parties.name,
    schema.members.name,
    schema.members.party_id,
    schema.members.election_number,
    schema.members.district,
    schema.members.name_eng,
  )
  .limit(limit)
  .offset((offset - 1) * limit)
  .orderBy(
    createDrizzleOrderBy({
      sortString: sort,
      selectField,
    }),
  );

그에 반해 drizzle 은 사실 SQL을 알고 있다면 너무나도 익숙한 구조로 사용할 수 있습니다. 내가 원하는 방법의 JOIN을 이용하고 내가 원하는 SQL 쿼리를 확정적으로 만들 수 있습니다.

Prisma는 배우기 쉽고 대부분의 결정을 내가 아닌 Prisma가 처리해주지만, 이제 내가 작성한 코드가 내가 생각한 SQL로 만들어 진다는 것이 더 중요한 상황이 찾아 왔습니다.

결국,
Prisma는 놓아줄때가 된것 같습니다.

2. 함부로 넘어갈순 없다, 테스트베드로 부터

이제 문제점은 확인했습니다. 해결할 시간이 찾아왔습니다.

무턱대고 이러한 문제점을 해결하는것은 당연히 좋지 않습니다. 소프트콘 뷰어십은 하루에도 5,000명 이상의 사용자가 사용하는 것과 별도로 구독 서비스를 통해 유료 회원들에게 상품을 판매하고 데이터를 보여주고 있는 상황입니다.

문제점이 있다 한들 테스트 없이 한번에 넘어가는 것은 제 오랜 개발 역사에서 좋은 결과를 내지 않았다는 것을 잘 알고 있습니다.

비슷한 구조지만 규모는 좀 작고 테스트 해볼수 있는 환경이 있..
잠시만요 무슨일이 생긴것 같은데

...

헉..
이게 무슨일이죠!?

( 프로젝트 국케이원 제작기 2부에서 계속 )


잠깐!
국회 정보 플랫폼, 국케이원은 아래 링크에서 미리 만나보실 수 있습니다!
https://assembly.softc.one/

profile
소프트콘 뷰어쉽 절찬리 운영중! https://viewership.softc.one

1개의 댓글

2부 글 너무 궁금합니다 ㅇ0ㅇ

답글 달기