[일상] 첫 대형 프로젝트 마무리 회고록

루나·2023년 2월 22일
0

첫 회사에서 처음으로 맡아본 프로젝트가 마무리됐다.
항상 풀스택을 목표로 했었지만 진짜 실무에서 풀스택으로 밑바닥 설계부터 끝까지 맡는다는건 역시 생각보단 고난했다.
반년이 조금 넘은 지금 회고록을 작성해보기로 했다.


0. 개요

사내에서 기존에 사용하던 웹 관련 프론트, 백엔드의 기술스택을 마이그레이션하게 됐다.
상세하게 프론트는 Angular에서 React로, 백엔드는 Flask에서 NestJS로 옮기게 됐다.
이런 선택을 하게된 이유는 아래와 같다.

  • 기존 레거시 프로젝트(Angular, Flask)는 외주 업체에서 전달받았지만 아무 문서도 없었고 이쪽 회사에 맞춰서 개발이 된 상태도 아니었기 때문에 불필요한 코드도 굉장히 많았으며 아키텍처도 존재하지 않고 하나의 함수에서 라우팅, 비즈니스 로직, DB 제어, 응답을 모두 수행하는 등 유지보수가 굉장히 힘든 상태의 코드였다.
  • 소수의 인원(1~2명)으로 웹 전반의 유지보수 + 요구사항 개발을 진행해야했지만 기술스택이 TypeScript(Angular) / Python(Flask)로 분리되어 있어 개발비용 측면에서 불리한 상황이었다.
  • 모바일 프론트의 경우 React Native로 개발되고 있었으며 웹 프론트도 React로 변경하면 비슷한 프레임워크(라이브러리) 환경이기 때문에 좀 더 이점을 취할 수 있을 것이라 판단되었다.
  • 같은 이유로 백엔드도 NestJS로 변경하면 웹 프론트 + 앱 프론트 + 백엔드 셋 다 TypeScript로 모든 서비스를 하나의 언어스택으로 구성할 수 있었다.

결정적으로 회사에서 서비스를 확장하려고 하는 단계였으며 DB, 서버, 프론트에 대규모 수정사항이 생길 구조적 변경 요구사항이 생기게 됐다.
현재 상태를 유지하면서 추가 개발을 진행하는 것은 장기적으로 고려해봤을때 결국은 기술부채를 계속해서 늘리기만 할 뿐이라 판단했으며 이에 마이그레이션 작업을 제안하여 진행하게 됐다.

1. 백엔드 서버

앞서 말했듯이 레거시 서버는 라우팅, 비즈니스 로직, DB 제어, 응답 등 모든 로직이 하나의 함수에서만 이루어져 있었으며 당연하게도 재사용성은 미비했고 중복 로직이 다수 존재했다.
API 문서 또한 존재하지 않았기 때문에 처음에 분석하고 재설계하는 과정에서 굉장히 많이 고생했던 기억이 아직도 난다.
DB 구조에서도 무분별한 인덱스 설정과 사용되지 않는 컬럼, 잘못 사용되는 컬럼 등 많은 문제점이 존재하는 것을 확인할 수 있었다.

같은 TypeScript로 구성할 수 있는 Express를 선택할 수도 있었겠지만 이미 위의 문제점들을 겪어본 입장에서 장기적으로도 아키텍처가 지켜질 수 있도록 프레임워크 차원에서 지원받고 싶어 NestJS를 선택했으며 어떻게 만들어야 유지보수, 추가 기능 개발에 용이할 수 있을지 찾아보았고 클린 아키텍처라는 것을 공부해 도입해보기로 결정했다.

1-1. 클린 아키텍처

각 레이어를 뚜렷하게 정의하여 나누고 분리된 의존성을 토대로 따로따로 개발하며 이를 이용한 이점으로 테스트 코드를 작성해보고.. 처음엔 모든 것이 좋은 느낌이었다. 하지만 얼마 안가 여러 고민들이 깊게 자리하게 됐으며 일주일 내내 구조만 계속 변경했던 시기도 있었다.

  • 도메인(엔티티)레이어를 잘못 이해하고 사용하고 있었다. 그저 멤버 변수만 존재하는, 실질적으론 타입과 다름없는 도메인들만 생겨났다.
    • 사실 전체 서비스가 DB에서 단순 CRUD 작업만 진행하는 것이 70% 이상이었으며 비즈니스 도메인이 거의 없다는 점도 한몫 했었다. 어찌보면 이 아키텍처를 선택한 것부터 잘못됐던 걸까? 싶었다.
    • 그렇다보니 '진짜 비즈니스 로직' 이 있어도 유스케이스 레이어에서 작성하게 되는 일이 잦아졌고 이게 정말 도메인을 지키는, 도메인을 중심으로 사용하는 아키텍처가 맞나..? 싶어졌다.

클린 아키텍처로 구성했다는 많은 깃 레포, 블로그 자료 등을 참조해보며 생각해본 결과 이런 의문들이 계속 생겨나는 근본적인 이유는 '이미 DB가 완성되어 있었으며 사실상 DB 스키마에 의존하여 단순한 CRUD만 진행하는 서비스'였기 때문에 더더욱 그런쪽으로 밖에 생각하지 못하게 됐던 것 같았다.
이를 교정하기 위해 '만들면서 배우는 헥사고날 아키텍처 설계와 구현' 이라는 책을 많이 참고했었으며 덕분에 클린 아키텍처를 온전하게 사용하는 법을 터득했다.

  • DTO를 어떻게 구성하고 어디서 선언한 뒤 어떻게 넘겨줘야 할지 굉장히 심하게 고민했었다.
    • 처음엔 인터페이스로만 선언한 뒤 객체 리터럴로 반환, 전달을 진행했었지만 런타임 시점에서 사라지는 타입으로 DTO를 전달하고 사용하는 것이 옳은걸까..? 타입에 대한 유효성 검증을 아예 진행하지 않는 것이 과연 올바른걸까...? 라는 고민이 생겨났다.
    • 위 고민 끝에 클래스 형식으로 DTO를 대부분 바꾸던 중 클래스로 만들어 사용하는 것도 깔끔하고 납득되는 해답이 아니었기 때문에 더 혼란스러웠다. 정의상 DTO는 아무 로직도, 함수도 포함하지 않아야 된다는데 그렇다면 더더욱 클래스일 이유가 없는게 아닐까? 멤버변수만 있는 클래스가 인터페이스(타입)과 도대체 뭐가 다른걸까?

결국 해당 프로젝트 안에서 DTO를 어떻게 만들고 사용할지에 대한 컨벤션을 정했으며 명확한 기준을 나눠서 인터페이스와 클래스를 적절히 섞어서 사용하게 되었다.
통일성 관점에서도 별로 좋진 않은 것 같고 클래스에 static 메서드(안쪽 레이어의 DTO로 변환하는 함수)가 포함되어 있었기 때문에 DTO의 정의랑도 잘 맞지 않는 거 같았지만 DTO 관련 논쟁은 우리 뿐만이 아니라 다른 사람들도 많이 겪는 문제인 것으로 보아 확실하게 구분지어 정의할 순 없는 영역이라 생각했고 여러 시행착오 끝에 도달한 그나마 납득 가능한 정의들이었다.
해당 방식은 pvarentsov/typescript-clean-architecture 레포지터리에서 참조했다.

클린 아키텍처를 효과적으로 사용하기 위해선 고려해봐야 되는 점이 생각보다도 굉장히 많았으며 이런 개념이 생겨난 이유가 많이 와닿았다. 역시 최우선적으로 지켜야 될것은 '의존성의 흐름, 방향과 제어' 라고 생각해 그것을 중점으로 뒀으며 결과적으로 나름 납득 가능한 선에서의 아키텍처를 정의해냈고 이로 인해 유닛 테스트를 작성하는데에 많은 이점을 가져올 수 있었다.

1-2. TypeORM..........

보통 NestJS로 프로젝트를 구성할 때 TypeORM을 많이 사용하며 공식문서에서도 이를 기반으로 설명하고 있다.
실제로 프로젝트를 구성했을 단계에서 여러 ORM을 비교해봤을 때 TypeORM이 여러면에서 적합하다고 판단되어 도입했지만 정말. 정말... 여러 문제가 많았다........

  • 타입스크립트를 사용하는데도 타입에 안전하지 않다.
    • select 옵션으로 특정 컬럼들을 지정하는 순간 TypeORM 엔티티와 결과 값의 타입은 완전히 달라지게 된다. undefined.....
    • 쿼리빌더를 사용할 때 해당 엔티티에 대한 타입을 자동완성으로 지원해주지 않는다. 사실상 생 쿼리와 다름없는 매직 스트링을 입력하게 되며 이 경우 ORM을 사용하는 이점이 소실되며 오히려 생 쿼리를 쿼리빌더 문법으로 사용하기 위해 찾아내는 작업을 해야해서 일을 두번씩 하는 느낌이었다. (특히 andWhere 안에서 orWhere를 사용하는 복잡한 조건에서 'Brackets' 라는 객체를 생성해 사용해야 했는데 이 경험이 굉장히 좋지 않았다.)
  • 굉장히 많은 버그가 존재했다. 정말 많이 존재했다.
    • 거의 항상 이슈에서 검색해봤던 거 같다. 제일 기억에 남는 것은 join이 들어가는 where 쿼리에서 inner join이 기본 값으로 적용되는 점이었다. 해당 문제는 12월 4일 0.3.11버전으로 업데이트되면서 해결됐지만 이런 비슷한 문제들을 굉장히 많이 겪어 지쳐갔다.
  • 클린(레이어드) 아키텍처에서 사용할 때 트랜잭션 처리에 관해서 곤란했다.
    • 여러 모듈이 엮여 있는 트랜잭션 작업을 수행하기 위해선 유스케이스 레이어에서 트랜잭션을 알아야 했으며 이를 알게되는 순간 유스케이스 레이어가 인프라 레이어의 세부사항을 알게되는 문제점을 갖고 있다.
    • 이를 해결하기 위한 typeorm-transactional-cls-hooked 등 라이브러리가 존재하긴 했으나 어떤 선택지도 적절하진 않았다. (사실 이 문제는 TypeORM만의 문제점은 아닌 것 같긴 하다.)

프로젝트의 중반부터는 정말 하루에도 몇번씩이나 TypeORM... 또 TypeORM 너야..... 라는 생각을 달고 살았던 것 같다. 이쯤부터 Prisma를 잠깐씩 공부해보고 있었는데 물론 프리즈마도 깊게 파고 들어가면서 프로젝트를 진행해보면 문제점이 생기긴 하겠지만 그래도... 정말 TypeORM에 비해서는 천사처럼 느껴질 정도로 사용 경험이 굉장히 좋았다.

개인적으로 TypeORM은 결국 스프링 JPA의 엄청난 하위호환일 뿐이라는 느낌을 굉장히 많이 들게했으며 이걸 사용할 바에는 차라리 스프링으로, JPA를 사용해서 구현하는게 낫겠다 싶었다..
그에 비해 Prisma는 확실하게 독자적인 방식을 채택하며 좀 더 TypeScript, NodeJS스럽다 라는 느낌을 많이 받을 수 있어 만족스러웠다.
비록 NestJS도 TypeORM도 스프링 환경을 참조해서 만들었다곤 해도 결국 독자적인 방향을 추구하지 않는다면 '그거 쓸바엔 그냥 스프링 쓰면 되는 거 아냐? 어차피 스프링에 비해서 기능도 굉장히 빈약하고 불안정하던데' 에 그치지 않을까 라고 생각한다.

2. 인프라/데브옵스

입사 후 3~4주차. 아직도 그때의 괴로운 기억이 선명하게 남아있었다.
레거시 프로젝트의 배포 프로세스를 로컬에서 테스트해보고 실제 배포를 진행했던 날이었는데 정말 끔찍한 경험이었다.

  • 배포에 필요한 모든 프로세스를 개발자가 '매번 수동으로' 직접 구성해야 했다.
    • 로컬에서 빌드를 진행하고, 새 버전의 이미지를 생성해 ECR에 업로드하고, ECS에 새로운 Task를 개정하고, Service를 업데이트 하는데 인스턴스의 가용 자원, 고정되어있는 포트 등의 문제도 발생했다.
    • 하나의 ECS 클러스터, 하나의 서비스, 심지어 하나의 태스크에 모든 컨테이너(서버, 프론트, traefik, Worker, 심지어 레디스까지!)에 대한 정의가 구성되어 있었으며 당연하게도 하나의 수정사항이 생기면 엮여있던 모든 서비스들이 다운됐다. 실제로.....

덕분에 정말 이것만은 개선을 꼭 해내야겠다 라고 마음을 먹었었으며 결과는 아래와 같다.

배포 프로세스

  • 그림에는 없지만 PR이 올라오면 Github Action에서 빌드, 테스트, 린트 등 CI 파이프라인을 거치도록 구축했다.
  • CodePipeline을 활용해서 ECS Blue/Green 무중단 배포 CD 파이프라인을 구축했다.
    • 당연히 이전과는 다르게 클러스터와 서비스, 태스크를 나눠 각각의 수정사항이 다른 서비스에 영향을 미치지 않도록 구성했다.

AWS의 여러 기능들을 처음 써보게 되었으며 보름 가까이 코드와는 멀어져 공부만 했던 것 같다.
인프라, 데브옵스 관련 정보글들은 찾아보면 항상 추상적으로 설명만 하고 구체적인 설정 예제가 없어 굉장히 막막하기도 했고 제대로 구축하고 있는게 맞을까 잘못되면 어떡하지 하는 걱정에 겁이나서 하루종일 손도 못댔던 기억이 난다.
더군다나 AWS 콘솔 환경이 신버전으로 업그레이드하고 있는 중이었으며 구버전과 신버전을 번갈아가면서 특정 기능이 존재하는지 찾아봐야 했다......
그래도 결국 공식문서에는 모든 답이 있었고(그렇게 친절하진 않았지만) 결국은 완전히 구축을 해내 실제 배포가 진행되고 수정사항이 생겼을 때도 main 브랜치에 push가 되는 순간 파이프라인이 돌고 n분 뒤에 새 버전으로 배포가 성공적으로 진행되는 것을 확인했을 때의 성취감은 정말...
직접 구축하고 나니 그 수많은 정보글들이 왜 추상적으로만 알려줬는지 이해가 됐었다.

전체 서비스 아키텍처 (간략)


배포 환경에서의 아키텍처도 일부 수정된 부분이 있었는데 원래 레거시 구조에선 컨테이너로 띄워져있던 traefik이 로드밸런싱을 담당하고 있었으며 라우팅은 가비아를 사용했었다.
여기서 컨테이너들이 고정된 포트를 사용했기 때문에 당연히 오토스케일링이나 무중단 배포는 사실상 불가능한 구조였다.

이를 해결하기 위해 AWS의 여러 기능들을 검토해서 적극적으로 사용해 개선했으며 현재 서비스는 언제든 확장이 편리하게 가능한 상태로 개선해냈다.

profile
백엔드 개발자

0개의 댓글