- 진행 날짜 - 2021.11.04 pm 17:00 ~ 2021.11.06 am 10:00
- 과제 필수 포함 사항
음악 스트리밍 서비스에는 3가지 요소 뮤지션 곡 앨범 이 존재합니다.
앨범 페이지, 뮤지션 페이지, 곡 페이지에 인접 정보들 (ex, 곡의 뮤지션, 곡의 앨범)을 표현할 수 있도록 CRUD API를 구성해주세요.
이 페이지들에 대한 DB를 구성할 때 곡 - 뮤지션 연결과 곡 - 앨범 연결은 내부 운영팀에서 직접 연결 가능하지만, 뮤지션 - 앨범 정보까지 태깅하기엔 내부 운영 리소스가 부족한 상황으로 가정해보겠습니다.
이때, 뮤지션 - 곡 이 연결되어있고 곡 - 앨범 이 연결되어있다면 뮤지션 - [곡*] - 앨범 연결되어있다고 판단할 수 있는데요. 이 특성을 이용해서 뮤지션의 앨범을 보여주는 Read API, 특정 앨범의 뮤지션 목록을 보여주는 Read API를 만들어주세요.
- neo4j DB 테이블 요구사항
- 뮤지션, 곡, 앨범은 각각의 테이블 (musician, song, album)로 구성되어야합니다.
- 앨범 안에는 여러 곡이 속해있을 수 있습니다.
- 한 곡에는 여러 뮤지션이 참여할 수 있습니다.
- 한 곡은 앨범 1개에만 들어가있습니다.
- 뮤지션은 여러 앨범을 갖고 있을 수 있습니다.
- 뮤지션, 앨범, 곡 데이터는 위 relation을 테스트할 수 있을만큼 임의로 생성해주시면 좋습니다.
- 13팀 과제 Github 리포지토리
🏫 사용한 프레임워크 & 라이브러리
- Nest JS
- config
- graphql
- neo4j-driver
- data-loader
- supertest
- class-validator & class-transformer
💯 구현 목록
- 화면별 Read API 요구사항 (GraphQL)
곡 페이지
✅ 해당 곡이 속한 앨범을 가져오는 API
✅ 해당 곡을 쓴 뮤지션 목록을 가져오는 API
앨범 페이지
✅ 해당 앨범을 쓴 뮤지션 목록 가져오는 API
✅ 해당 앨범의 곡 목록을 가져오는 API
뮤지션 페이지
✅ 해당 뮤지션의 모든 앨범 API
✅ 해당 뮤지션의 곡 목록 가져오는 API
- Create, Update, Delete API 요구사항 (RESTful API)✅ 앨범 생성 API✅ 뮤지션 - 곡 연결✅ 곡 - 앨범 연결✅ 뮤지션 - 앨범 연결/연결해제 API 는 필요하지 않습니다. (구현 X)
- 뮤지션 - 곡 연결과 곡 - 앨범 연결이 되어있으면 GraphDB (neo4j) 에서 뮤지션 - [*] - 앨범 연결 여부를 뽑을 수 있습니다. 이 특성을 Read API에서 활용해주세요.
✅ 곡 - 앨범 연결 해제 API
✅ 뮤지션 - 곡 연결 해제 API
✅ 뮤지션 생성 API
✅ 곡 생성 API
Test
- ✅ Unit Test - GraphQL
- 🔺 Unit Test - Domain
- ✅ E2E Test
📋 Graph Database Modeling
💭 Project Review
프리온보딩 백엔드 코스 2차 과제로 마피아 컴퍼니 기업에서 내주신 과제를 진행하였습니다. 요번 2차 과제는 마피아 컴퍼니, 프레시 코드 기업의 과제 중 하나의 과제를 선택해서 진행하는 형식이었는데, 아무래도 마피아 컴퍼니 쪽의 과제가 더 재밌어 보인다는 의견이 있어서 해당 기업에서 내주신 과제를 선택하게 되었습니다. 요번 과제는 1차 과제와는 다르게 한 팀에서 팀을 나눠서 진행하는 것이 아니라 전체 팀원이 하나의 과제에 도전하는 방식으로 진행되었습니다. 그래서 총 6명의 인원이 하나의 리포지토리를 만들고 과제를 진행하였습니다.
💬 What is Graph DB?
이번 과제는 특이하게도 관계형 데이터베이스나 비 관계형 데이터베이스가 아니라 neo4j라는 그래프 데이터베이스를 활용하는 프로젝트였습니다. 그래프 데이터베이스를 한 번도 사용해 본 적이 없었기 때문에 어떤 형태로 데이터베이스에 저장되고 어떤 방식으로 모델링을 해야 하는지 알아야 할 필요성이 있었습니다.
그래프 데이터베이스란 관계를 저장하고 탐색하도록 만들어진 데이터베이스를 말합니다. 그래프 데이터베이스는 노드(node)라는 정점을 사용하여 데이터 엔티티를 저장하고, 에지(Edge)를 사용하여 두 노드 간의 관계를 저장합니다. 에지는 항상 시작 노드, 끝 노드, 유형, 방향 값을 가지며 각 노드 간의 관계, 동작, 소유자 등의 값을 갖게 됩니다. 하나의 노드가 가질 수 있는 관계의 수나 종류에는 제한이 없습니다.
다음 그림은 소셜 네트워크 그래프 데이터베이스의 사례로, 사람들과 그 사이의 관계를 보면 어떤 사람의 친구가 누구고, 친구의 친구가 누군지 알 수 있습니다.
💬 Graph DB Modeling
그래프 데이터베이스의 경우에는 어떻게 모델링을 할 수 있을까요? 그래프 데이터베이스에서는 다음과 같이 노드의 속성과 관계를 사용하여 모델링할 수 있습니다. 과제에서는 각 노드들의 속성에 관한 정보는 알려주지 않았지만, 6명이 열심히 머리를 맞대고 토론한 결과 다음과 같이 기본 속성을 지정해서 넣어주기로 하였습니다.
- 뮤지션 노드는 id, name, company 속성을 갖습니다.
- 곡 노드는 id, genre, runningTime 속성을 갖습니다.
- 앨범 노드는 id, name, releaseDate 속성을 갖습니다.
그래프 데이터베이스 모델링은 기존의 ERD를 만드는 방식처럼 만들 수도 있지만, 저희는 neo4j 공식문서에 나와 있는 모델링 방식처럼 노드의 속성과 관계를 작성해주는 방식으로 모델링을 하기로 하였습니다. 그래프 데이터베이스는 관계형 데이터베이스와는 다르게 1:N이라고 하지 않고 관계를 맺는다고 표현을 합니다. 뮤지션은 곡을 여러 곡 포함할 수 있다고 과제 조건에 명시되어 있기 때문에 뮤지션 노드는 곡 노드와는 HAVE 관계를 맺고 있다고 표현을 하였고, 앨범은 곡을 여러 곡 포함할 수 있다고 명시되어 있기 때문에 앨범 노드는 곡 노드와는 CONTAIN 관계를 맺고 있다고 표현하였습니다.
- neo4j 모델링 공식문서
💬 Nest Js + neo4j
모델링이 끝나고 다음으로 한 작업은 Nest Js를 사용하여 graphql과 neo4j를 사용할 수 있는 환경을 구축하는 것이었습니다. Nest Js에 graphql 환경을 구축하는 것은 어렵지 않았지만 neo4j는 처음 해보는 것이었기 때문에 어떻게 Nest Js에 neo4j 데이터베이스를 연결하는지 찾아봐야 했습니다.
먼저 Nest Js와 neo4j 데이터베이스를 연결하기 위해서는 neo4j 데이터베이스를 설치해야 하며, 가장 쉽게 하는 방법은 neo4j Desktop을 사용해서 neo4j 데이터베이스를 만들어 주는 것입니다. 아래의 사이트에 들어가셔서 neo4j Desktop을 다운 받아 줍니다.
- neo4j Desktop 다운로드
설치한 neo4j Desktop을 실행시켜서 들어가면 기본적으로 설치되어 있는 Movie DBMS가 있습니다. Start 버튼을 클릭하여 구동할 수 있고, Add 버튼을 클릭하여 neo4j 사이트에서 다른 사용자가 만든 neo4j 데이터베이스에 연결할 수도 있습니다. 현재 저희 팀이 사용하고 있는 DBMS는 Remote DBMS라고 되어 있는 데이터베이스입니다.
neo4j 데이터베이스 연결에 성공하셨다면 다음은 Nest Js와 neo4j 데이터베이스를 연결할 차례입니다.
먼저 neo4j-driver를 받아줍시다.
npm i --save neo4j-driver
관계형 데이터베이스를 사용하는 경우에는 TypeOrm을 사용하여 쉽게 연결 설정도 하고 쿼리도 쉽게 작성할 수 있지만, Nest Js에서 그래프 데이터베이스를 사용하는 경우에는 현재 이러한 기능을 지원하는 OGM이 존재하지 않습니다. neo4j driver를 adam cowley님은 현재로서는 OGM을 만드실 계획이 없다고 하십니다. 그래서 직접 neo4j driver 설정을 해줘야지만 Nest Js와 neo4j 데이터베이스를 연결할 수 있습니다.
NestJs와 neo4j 데이터베이스 연결 설정은 아래의 리포지토리에서 확인하시거나, 저희 팀의 리포지토리를 참고해주세요.
💬 GraphQl Schema First
요번 프로젝트는 6명이서 프로젝트를 진행하게 된 만큼 효과적으로 업무를 분담할 필요가 있었습니다. 저는 예전에 graphql을 다뤄본 적이 있었기 때문에 제가 graphql 관련 설정과 graphql query를 만드는 역할을 담당하기로 했습니다. 그래서 어떻게 해야 graphql을 처음 접해보는 팀원들이 쉽게 이해할 수 있을까 생각하면서 설정을 하기로 하였고, schema first 방식으로 만들기로 했습니다.
graphql 스키마 설정 방식에는 code first 방식과 shcema firsrt 방식이 존재합니다. code first 방식은 작성된 Resolver에 따라 자동으로 schema file이 생성되는데 반해, schema file 방식은 사용자가 직접 graphql schema file을 작성해야 합니다. schema first 방식은 직접 작성해야 하기 때문에 SDL(Schema Definition Language)과 Resolver가 정확히 일치해야 한다는 단점이 있습니다. 하지만 shema file을 미리 직접 작성하기 때문에 이것을 사용해서 graphql에 익숙하지 않은 사람들이 이해하기 쉬우며 의사소통 수단으로 사용할 수 있다는 장점이 있습니다. graphql을 다뤄본 사람과 다뤄보지 못한 사람들이 섞여서 한 팀을 이룬 경우에 최고의 방법이라고 생각하여 schema first 방식을 사용하였습니다.
💬 GraphQl Data - loader
graphql도 N + 1 문제를 가지고 있습니다. Musician과 연관된 Song을 가져오기 위해 다음과 같은 ResolverField를 사용합니다.
@ResolveField(() => [Song])
song(@Parent() musician: Musician) {
return this.readService.readHaveSong(musician);
}
ResolverField에서는 다음과 같이 readHaveSong() 메소드를 사용하고 있는데, 만약 10개의 musician과 관련된 Song을 찾는다고 하면 11번의 쿼리가 수행됩니다. musician을 찾는 쿼리(1번) + 각 musician id와 관계된 Song을 찾는 쿼리(10번) 총 11번을 수행하게 되죠.
async readHaveSong(musician) {
const haveSong = await this.neo4jService
.read(
`MATCH (m:MUSICIAN)-[HAVE]->(s:SONG) WHERE m.id = '${musician["id"]}' RETURN s`
)
.then((res) =>
res.records.map((row) => new Song(row.get("s")).toJson())
);
return haveSong;
}
이러한 문제를 해결하기 위해 graphql은 data-loader를 지원합니다. 그래서 data-loader를 적용하면 위의 쿼리를 다음과 같이 사용자가 지정한 쿼리로 모아서 단 한 번의 쿼리로 실행할 수 있게 해 줍니다.
@Injectable()
export class HaveSongDataLoader {
constructor(private readonly neo4jService: Neo4jService) {}
generateDataLoader() {
return new DataLoader<any, any>(async (musician_id) => {
console.log(musician_id);
const loader = await this.neo4jService
.read(
`MATCH (m:MUSICIAN)-[HAVE]->(s:SONG) WHERE m.id = [${musician_id}] RETURN s`
)
.then((res) =>
res.records.map((row) => new Song(row.get("s")).toJson())
);
loader.map((element) => console.log(element));
return null;
});
}
}
문제는 현재 Nest Js v8과 nestjs-dataloader v7.0.1 버전이 제대로 호환되지 않아 Rxjs가 충돌하며 발생하는 버그가 있습니다. 이러한 버그가 있어서 결국에는 data-loader를 적용시키지 못하고 성능 향상을 하지 못했습니다. 다른 작업이 밀려있던 터라 일단 미뤄두고 다른 작업을 하기로 했습니다. 결국에는, 프로젝트를 끝낼 때까지 이 문제를 해결하지 못했습니다. 프로젝트가 끝나고 혼자서 data-loader 없이 graphql N + 1 문제를 해결해 봐야 할 것 같습니다.
Property 'intercept' in type 'DataLoaderInterceptor' is not assignable to the same property in base type 'NestInterceptor<any, any>'.
Type '(context: ExecutionContext, next: CallHandler<any>) => Observable<any>' is not assignable to type '(context: ExecutionContext, next: CallHandler<any>) => Observable<any> | Promise<Observable<any>>'.
Type 'Observable<any>' is not assignable to type 'Observable<any> | Promise<Observable<any>>'.
Type 'import("C:/Users/mm/Desktop/Study/sparta/99/Assignment_2_MAPIA/node_modules/neo4j-driver/node_modules/rxjs/internal/Observable").Observable<any>' is not assignable to type 'import("C:/Users/mm/Desktop/Study/sparta/99/Assignment_2_MAPIA/node_modules/rxjs/dist/types/internal/Observable").Observable<any>'.
The types of 'operator.call' are incompatible between these types.
👩💻 반성점 OR 다음 프로젝트부터 고치고 싶은 것
아무래도 처음 사용해보는 기술이라 neo4j 그래프 데이터베이스와 cypher 쿼리문을 이해하는데 좀 시간이 걸린 것 같습니다. 예상치 못한 각종 버그가 발생하기도 해서 요번에도 만들고 싶었던 부분까지는 제대로 작업을 못했습니다. 현업에서 만약 이런 경우가 있다면 해당 라이브러리를 사용하지 않고도 비슷하게 구현은 할 수 있을 정도는 되어야 할 텐데, 아직 미숙해서 구현하지 못한 게 너무 아쉽습니다. 해당 부분은 프로젝트가 끝나고도 코드를 수정해보려고 합니다.
'프리온보딩 백엔드 > TIL(Today I Learned)' 카테고리의 다른 글
[Assignment 5] Humanscape(휴먼스케이프) TIL (0) | 2021.11.18 |
---|---|
[Assignment 4] 8PERCENT(8퍼센트) TIL (0) | 2021.11.14 |
[Assignment 3] RED BRICK(레드브릭) TIL (0) | 2021.11.11 |
[Assignment 1.5] 어쩌다 개발이 재밌어진 건에 대하여 (2) | 2021.11.06 |
[Assignment 1] AIMMO(에이모) TIL (6) | 2021.11.04 |