Project
Web
DevOps
Music
Projects - July 3, 2024
현재 Showpang 이라는 프로젝트를 진행중이다. Showpang은 사용자가 장르
, 무드
, 템포
를 선택하면 그에 맞는 음악을 생성하여 스트리밍 할 수 있는 웹 서비스이다.
원래부터 음악에 관심이 많았고, 작곡해본 경험도 몇몇 있던지라 음악과 개발을 엮으면 더욱 재미있을 것 같아 참여하게 되었다. 작곡(+α; 잡?무) 부분을 맡게 되었고, 추후에 AI 기능을 접목시킬 예정이라 일단은 python으로 화성확 + 확률 기반의 작곡 알고리즘을 만들게 되었다!
1~2 주에 한 번 만나서 개발하는 것이었고, 프로젝트에 참여한 인원 대부분이 꽤나 바쁜 탓에, 솔직히 그리 애자일한 개발이 진행되지는 못했다. 하지만, 그럼에도 불구하고 나름 잘 개발했고, 모였을 때 다양한 분야의 기술적 대화를 나눌 수 있었다.
아래의 내용들은 23년 12월부터 24년 6월까지의 전반적인 개발 내용을 요약하고, 그 중 중요하다고 생각한 것들을 추린 것이다.
python의 pretty_midi 모듈을 이용해서 midi 파일을 만드는 방식으로 진행하였다.
먼저, 주어진 장르, 무드에 따라 randomness
라는 값을 변화시켜서 멜로디의 규칙성을 조절했다.
4/4박자 기준으로, randomness
가 낮을 수록 정박(4분음표)에 음이 나오도록 하였고, 높을 수록 무작위하게 (높은 엔트로피) 나올 수 있도록 하였다. 중요한 것은, 균등하게 확률이 퍼지는 것이 아니라, 한 박의 subdivision 내에서도 정박성을 유지하게끔 하면서 퍼지게 하는 것이었다.
따라서 이러한 randomness
에 따른 멜로디의 등장 확률 분포를 만들 필요가 있었다.
# 패턴의 확률분포를 만듦.
# 큰 randomness는 더욱 분산이 큰 분포를 만듦 = 높은 엔트로피.
def _make_probability_distribution(self):
depth = np.log2(self.division).astype(np.uint8) - 1
weights = np.zeros(depth)
r = self.randomness
primary = -0.5*(r - 2)
def h(x, a): return x/a
# 정박이 아닌 박의 확률을 구하기 위해 interpolate.
# somthing magical happens (?)
weights[0] = primary
for i in range(depth - 1):
weights[i+1] = (1-h(r, 2**i))*(1-primary) + h(r, 2**i)*primary
pd = np.zeros(self.bar_length * self.division)
for i in reversed(range(depth)):
step = (self.division // self.measure[1]) // 2**i
pd[::step] = weights[i]
self._pd = pd
간단한 interpolation 을 이용해서 이를 구현할 수 있었다.
이 이외에도, 다음 멜로디를 부드럽게 연결시키기 위해서 정규분포를 사용하였다. 또한, 현재 코드에 있는 음이 선택될 확률을 높였다.
spring 벡엔드 서버에서 음악 생성 요청을 받고, 생성된 음악을 응답해야 했기 때문에 FastAPI 서버를 구축했다.
다음과 같은 순서로 음악 생성 요청이 처리된다.
일단 적은 수의 요청이 들어왔을 때에는 문제 없이 저 과정들이 수행되었다. 하지만, 많은 요청이 비동기로 들어왔을 때 문제가 발생했다.
학생신분이었던 우리는 최대한 무과금으로 프로젝트를 진행하기 위해서 aws 의 프리티어를 이용하고 있었다. 하지만, 프리티어의 ec2 인스턴스는 vCPU 1개, 메모리도 1GB 의 가난한 스펙이었기 때문에 매 요청 처리 시 cpu utilization 이 100% 가까이 기록되었다. 그러다 보니, 서버가 이를 버티지 못하고 죽는 경우도 허다했다.
병목 지점을 찾기 위해 성능 모니터링을 진행했고, ffmpeg 가 그 원인임을 알게 되었다. ffmpeg 의 cpu 활용률을 줄이려니 응답속도가 확연히 느려졌고, 이 방법 또한 수많은 요청을 견뎌내기는 힘들어 보였다.
이를 어떻게 해결했는지는 다음 포스트에.
음악 스트리밍은 HLS(HTTP Live Streaming) 를 사용하기로 결정했다. 기본적으로 스트리밍은 유저에게 온전한 mp3 파일을 보내지 않기 때문에 유저에게 쪼개진 음악 파일(.ts) 과 그 순서를 명시한 플레이리스트 파일(.m3u8) 을 전달하여 원활한 스트리밍이 될 수 있도록 구현하였다.
하지만 동일한 플레이리스트 파일을 요청하면 인증되지 않은 사용자도 스트리밍을 할 수 있다는 문제가 있었다. 따라서, 처음에 유저의 인증 여부를 확인하고 딱 한 번만 사용할 수 있는 요청 url 을 알려주는 방식을 생각했다.
AWS 의 cloudfront 에는 presigned url 기능이 있는데, 이를 활용하면 s3의 데이터에 일정 시간만 접근할 수 있는 url 을 만들 수 있다. 하지만, 접근한 횟수 기반으로 요청을 제한하는 기능은 없었다. 따라서, 외부 redis 서버를 이용해서 해당 url 을 해싱하여 캐싱하는 방법을 사용했다. (마치 분산 락 처럼?) 대략적인 워크플로우는 다음과 같다.
redis 에 저장할때는 timeout 을 걸어주어 일정 시간이 지나면 없어지게 했다.
실제로 잘 작동함을 확인했지만, 굉장히 먼 길을 돌아가는 느낌을 받았다. 외부 redis 를 써야하는 문제점도 있고, 분명히 더 간단하게 해결할 수 있을 것 같았다. 또한, 직접 테스트 해보진 않았지만 동시성과 관련된 문제도 고려해봐야 할 것 같았다.
따라서, 팀원들과 논의 끝에 signed cookie 를 사용하기로 결정하였다. 이것이 훨씬 간단하고, 인증여부에 따른 권한 부여도 훨씬 쉬웠기 때문이다. 이에 대한 자세한 이야기도 추후 포스트에.
우리는 상당히 가난한 대학생이었고, 그래서 aws free tier 를 사용했다. 하지만, 프리 티어에서 쓸 수 있는 ec2 인스턴스는 cpu 1 코어에 1GB 메모리라는 상당히 저조한 사양을 가지고 있었기 때문에, 도커 이미지를 빌드하는 것 조차 불가능했다.
따라서, 배포를 위해 github actions 에서 도커 이미지를 빌드하고, 이후 ec2 에서는 이미지를 풀 받는 방식으로 개발했다.
추후에 테스트도 붙여서 보다 괜찮은 CI/CD 파이프라인을 구축할 계획이다.
프론트엔드는 next.js 를 사용했는데, 단순하게 도커 이미지로 만드니 2GB 라는 무시무시한 크기를 가지게 되었다. (spring 도 500MB 정도인데 ㄷㄷ)
원인을 분석해보니, 최종 배포에 필요하지 않은 파일이나 의존성 등이 포함되어서 그랬던 것이었다. 따라서, next.js 공식 문서에서 추천하는 도커 빌드 방법을 참고하여 도커 파일을 구성하였다.
next.js/examples/with-docker/Dockerfile at canary · vercel/next.js
The React Framework. Contribute to vercel/next.js development by creating an account on GitHub.
GitHub
FROM node:20-alpine AS base
# === Layer 1: 의존성 및 node_modules 설치
# 의존성 설치
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /src
# 노드 패키지 설치
COPY . ./
RUN yarn --frozen-lockfile
RUN rm -rf ./.next/cache
# === Layer 2: next 빌드 ===
# 프로젝트 빌드
FROM base AS builder
WORKDIR /src
COPY /src/node_modules ./node_modules
COPY . ./
RUN yarn build
# === Layer 3: 프로젝트 실행 ===
FROM base AS runner
WORKDIR /src
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# COPY --from=builder /src/public ./public # 나중에 public 폴더가 있으면 추가
COPY /src/.next/standalone ./
COPY /src/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
이렇게 Dockerfile 을 바꿈으로써 이미지 크기가 줄어들었고, 도커허브에 푸시/풀 하는 시간이 줄어들어서 전체적인 배포 시간이 단축되었다!
그 외에도 사소하게 FastAPI 로 만들었던 음악 생성 서버의 도커라이징을 개선했다.
python 외에도 ffmpeg, fluidsynth 같은 외부 프로그램을 사용했기 때문에, 이것들도 도커 이미지에 넣었어야 했는데, 매번 ubuntu 위에 ffmpeg, fluidsynth 를 까는 것이 큰 시간적 낭비라고 생각했다.
따라서, 먼저 python, ffmpeg, fluidsynth 가 있는 이미지(크게 수정되지 않으므로)를 준비하고, 이를 매 배포마다 풀 받아서 여기에 필요한 모듈과, 변경된 코드를 올리는 방식으로 수정했다.
결과적으로 배포에 걸리는 시간이 많이 단축되었다!
아직 갈길이 무척이나 멀지만, 일단 다양한 문제들을 직접 경험해보고, 이를 극복해나가는 과정이 상당히 뜻깊었다.
특히, 기존에는 아예 모르던 데브옵스쪽을 한 번 건들여보니 전반적인 개발 및 배포 과정이 그려진 것 같은 느낌이 들었다. 나중에 2차 정리도 기대하시라!