[좌충우돌 산악회 홈페이지 만들기 #2] TypeORM을 활용한 DB 연동과 Cross-env 및 Dotenv 세팅, 예제 컨트롤러/서비스 개발!
* 이 글은 정보제공용 글이 아닌 일기처럼 쓰는 개발 일지입니다! 비판 환영!
1. 서론
이제 본격적으로 Nest.js를 활용해서 초석을 다질 차례이다. 앞으로의 개발은 하기의 공식 도큐먼트를 최대한 참고하여 작성할 예정이다.
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac
docs.nestjs.com
매번 그렇듯, 프로젝트를 세팅하는 단계가 가장 까다롭고 오래 걸리는 것 같다.
나로서는 귀찮게 느껴지긴 하지만, 초기에 잘 다져두면 두고두고 유용하게 사용할 수 있으니 열심히 해야겠다.
이번에 가장 기초적으로 진행할 작업은 아래와 같다.
1. 프로젝트 생성
당연히 프로젝트를 생성해야 개발을 할 수 있다. Nest.js 공식 도큐먼트를 참조하여 CLI 패키지를 다운로드 받고, 프로젝트를 구성해본다.
2. TypeORM을 활용한 DB 연동
Nest.js에서는 ORM으로 TypeORM을 사용한다고 한다. 뭐 DB에 직접 쿼리를 날리는 방법은 분명 존재하겠지만, Raw Query로 매번 매작업마다 DB에 쿼리를 한다는건 미친짓일 것이다.
쿼리를 직접 작성하지 않고도 객체지향적으로 DB에 다양한 작업을 할 수 있다는 것은 유지보수 및 생산성의 증가로 이어지며, 무엇보다 개발자 본인이 본연의 비즈니스 로직에만 집중할 수 있음을 시사한다. (나 같은 경우는 회사에서 전자정부프레임워크로 개발된 Spring에 추가 기능을 구현할 때 SQL Mapper인 Mybatis를 통해 개발을 진행했어야 했는데, 내가 기능 개발을 하는건지, SQL을 짜는건지 여러모로 현타 오는 시간을 가진 적이 있었다.)
자, 그런데 여기서 문제가 발생한다. 개발 환경은 다양하다. development, staging, production 등 여러가지가 있는데, 환경 별로 구동되어야하는 Config가 모두 다르다.
가령, 데브 환경에서는 DB 명이 alpine_club_dev 일 수도 있고, 배포 환경에서는 alpine_club 처럼 데이터베이스명도 다르고, DB에 접근하기 위한 username, password, host, port도 모두 다를 것이다.
그래서 나는 이러한 환경을 분리하는 작업을 수행해야한다.
3. cross-env와 dotenv를 활용한 Config 분리
나는 이번 개발에서 환경을 두 가지로 분리하고자 한다. 하나는 데브 환경이고, 다른 하나는 추후 개발이 완료되었을 때 사용할 배포 환경이다.
dotenv는 찾아보니 내가 지정한 외부 환경 변수를 불러와 사용할 수 있도록 해주는 패키지이고, cross-env는 실행 스크립트(npm run start:dev 등)를 동작하는 시점에 내가 지정한 특정 환경변수를 주입할 수 있도록 해주는 친구라고 한다.
이 부분도 구현이 필요할 것이다.
4. 예제 Controller 및 Service, Entity와 Repository 개발 및 동작 테스트
1번과 2번 작업을 완료하고 나면, ORM과 DB가 성공적으로 연동되었는지, CRUD 작업이 정상동작하는지 확인이 필요할 것이다.
일단 구현해보고, 추후 적용이 필요한 사항들을 고려해봐야할 것 같다.
2. 구현!
1. 가장 먼저 프로젝트를 만들어보자
아, 참고로 나는 Intellij를 통해 개발한다.
공식 도큐먼트에 따르면, 다음과 같은 커맨드를 수행시키라고 한다.
$ npm i -g @nestjs/cli
$ nest new alpine_club_nestjs
가장 먼저 Global하게 NestJS CLI를 설치한다. 설치해야만 다양한 Nest 관련한 커맨드를 사용할 수 있다.
그 다음은 'nest new' 커맨드를 사용해 프로젝트를 생성한다. 나의 경우는 'alpine_club_nestjs'로 지정하였다.
프로젝트가 생성이 완료되었으면, 이제 TypeORM과 DB를 연동해볼 차례이다.
2. TypeORM과 DB 연동
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac
docs.nestjs.com
공식 도큐먼트가 다음 커맨드를 실행하라고 알려줬다.
$ npm install --save @nestjs/typeorm typeorm@0.2 mysql2
설치 커맨드에서 typeorm이 0.2 버전인 것을 확인할 수 있는데, 공식 도큐먼트에서는 0.3버전에서는 상당히 많은 부분이 변경되었다고 한다. 뭐 이에 대한 설명도 나와 있기는 한데, 도큐먼트에서는 0.3 버전 쓸 사람은 써라~ 라는 어감으로 적혀있는 것 같아서 순순히 0.2 버전으로 진행해보고자 한다.
이제 프로젝트에서 전역적 모듈을 관리하는 app.module.ts에 관련 정보를 설정해주면 된다.
간단하다. 모듈 내 imports는 필요한 모듈을 적용하기 위해 사용하는 것으로 보인다.
app.module은 전역적으로 모듈을 관리한다고 하는데, 여기서 TypeOrmModule의 forRoot 메소드를 적용하면 전역적으로 ORM이 적용되는 듯하다.
그리고 ORM 옵션 안에 'autoLoadEntities'가 true인데, default는 false라고 한다.
false일 경우, entities라는 키를 명시하고, Value로 Array를 받는데, 그 Array 안에 사용할 Entity들을 모두 적어줘야한다고 한다.
만일, 프로젝트에 Example1, Example2 라는 Entity가 있다고 가정해보자면,
{
...
entities: [Example1, Example2]
...
}
와 같이 명시해주어야한다고 한다. 근데 나는 Entity를 생성 할 때마다 명시하기 귀찮아서 일단은 autoLoadEntities를 true로 지정해두었다.
또, synchronize 옵션을 true로 지정해두었는데, 이건 매 서버 실행시마다 생성되었거나 변경된 Entity가 있으면 이를 감지해서 실제 DB에 반영해준다고 한다.
어.. 근데 ORM의 Option을 정의하긴 했는데, 뭔가 마음에 걸린다. username도 그렇고, password, database 도 환경에 따라 모두 변하는 값들이다. 이제 cross-env와 dotenv를 활용해서 이 부분을 변경해보자.
3. Cross-env와 dotenv를 활용한 외부 환경 변수 주입
먼저 패키지부터 설치해본다.
$ npm i --save cross-env dotenv
나의 경우는 cross-env를 통해 실행 스크립트 동작 시점에 NODE_ENV 라는 환경 변수를 주입해줄거고, 이를 바탕으로 NODE_ENV가 dev일 경우 .env.dev 파일을, prod일 경우 .env.prod 파일을 통해 환경변수를 관리할 것이다.
물론, .env라는 파일도 만들어서 실행하는 환경에 상관 없이 공통 환경변수를 관리하는 친구도 만들 것이다.
.env.dev 파일 안에는 개발 환경에서 사용하는 DB 정보가 담겨있다. .env 파일 안에는 사실 뭘 넣어야할지 아직 못 정했다.
이렇게 만든 env 파일 내의 환경변수는 process.env.환경변수명 으로 사용하면 된다고 한다.
그리고 만든 env파일을 사용하기 위해서 Nest.js의 app.module에 처리를 해주어야 한다고 공식 도큐먼트가 다시 나에게 말해줬다.
Documentation | NestJS - A progressive Node.js framework
Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Progamming), FP (Functional Programming), and FRP (Functional Reac
docs.nestjs.com
하기 코드와 같이 ConfigModule을 사용하여 세팅을 해주면 된다고 한다.
여기서 envFilePath는 불러올 env 파일의 이름들이며, 배열 형태로 받을 수 있다고 한다.
또한, isGlobal 옵션을 true로 지정하면, 모~든 모듈에게 공통적으로 해당 env 파일 내의 환경변수가 적용되어서 어디서든 꺼내쓸 수 있다고 한다.
어.. 근데 envFilePath 키의 Value에서 두 번째 env 파일에서 process.env.NODE_ENV가 눈에 보인다.
만일 내가 실행 스크립트로 npm run start:dev를 실행하면 .env.dev가, 그리고 npm run start:prod를 실행하면 .env.prod를 로드하도록 처리한 것이다.
이건 cross-env를 활용해서 실행스크립트를 동작시키는 시점에 NODE_ENV 변수에 특정 데이터를 담아서 실행시켜주면 된다.
package.json 내의 start:dev와 start:prod 스크립트에 'cross-env NODE_ENV=dev 이후_실행할_스크립트' 와 같이 작성해두었다. 해당 스크립트는 NODE_ENV 변수에 dev를 할당시키겠다는 의미다.
그럼 이제 앱이 실행되며 app.module.ts 내의 imports를 읽어나가다가 ConfigModule에서 .env와 .env.dev 를 환경변수로 로드할 것이다!
그럼 이제 TypeORM의 Config에서도 해당 환경변수를 사용할 수 있다.
참 깔끔해졌다. 이제 Git에 업로드해도 내 DB 정보를 타인이 알 수 없을 것이다! 따라서 .env 파일은 Git에 푸시할 때 반드시 제외해야한다! 민감한 정보를 담아둘 공간이기 때문이다.
이제 본격적으로 예제 컨트롤러와 서비스, 엔티티를 만들어보자.
4. 예제 컨트롤러/서비스, Entity, Repository 만들고 동작 테스트해보기
Nest.js에서는 CRUD 를 수행할 수 있는 Controller와 서비스, DTO, Entity 껍데기를 자동으로 생성해주는 스크립트가 있다.
$ nest g resource example
여기서 g는 generate의 약어, resource는 CRUD에 필요한 것들을 생성해주는 키워드이다. 나는 example이라는 이름으로 생성했다.
example 디렉토리 안에 다양한 소스 파일들이 생성되었다.
이제 엔티티를 만들어보자! 엔티티 내에는 간단히 ID, Title, Content 필드로만 구성해보려고 한다.
@Entity() 데코레이터를 사용했는데, 이건 해당 클래스가 엔티티라는 것을 Nest.js에 알려주기 위함이다.
옵션으로는 name을 사용했으며, 이는 DB 내에 tb_example이라는 테이블로 저장된다.
@PrimaryGeneratedColumn() 은 DB에서 Auto Increment가 적용된 PK 필드를 나타낸다.
@Column() 은 DB 내의 필드를 나타낸다. comment, length, default, type 등 다양한 옵션을 적용할 수 있다.
엔티티를 만들었으니, Example의 TypeORM 모듈에 해당 엔티티를 등록해주자. 등록해주지 않으면 TypeORM은 해당 엔티티를 인식할 수 없다.
등록 완료 이후, 서버를 실행하면 MySQL에 내가 만든 테이블과 필드들이 동일하게 생성됨을 확인할 수 있다.
이제 엔티티가 완성되었으니, 컨트롤러를 살펴보자.
위의 사진은 컨트롤러 파일인데, 첫 번째 밑줄에 @Controller() 데코레이터를 사용함으로써 해당 클래스가 컨트롤러임을 명시할 수 있다.
이때, 해당 데코레이터에 'example'이 적혀있는데, 이는 엔드포인트를 나타낸다. 만일, 해당 컨트롤러의 create 메소드를 호출하려면 'POST http://localhost:3000/example' 로 호출해야할 것이다.
create 메소드 상단의 @Post()는 해당 API가 사용하는 메소드(GET, POST, PATCH, DELETE 등)를 나타낸다. 여기서도 동일하게 데코레이터 안에 다른 Path를 명시할 수 있다. 만일 @Post('test') 라면 API를 호출 할때에는 'POST http://localhost:3000/example/test' 로 요청해야할 것이다.
두 번째 밑줄에서의 @Body() 는 외부에서 전달받을 인자가 있음을 나타낸다. 공식 도큐먼트에 따르면 이외에도 @Param, @Req, @Res 등 다양한 데코레이터를 상황에 따라 사용할 수 있다고 한다. 아무튼 @Body() 데코레이터에 CreateExampleDto를 받는다.
즉, 외부로부터 CreatedExampleDto에 명세된 데이터를 전달받아 실제 비즈니스 로직을 처리할 것이다.
매우 간단하다. 나는 외부로부터 title과 content만 body로 전달받을 것이다.
이제 Service에서 비즈니스 로직을 처리해보자.
생성자에서 Entity와 관련된 다양한 CRUD 작업을 수행하기 위한 Repository를 주입해준다. 이렇게하면 Example Repository를 사용할 수 있다.
그리고 create 메소드 안에서 createExampleDto를 전달받아 Repository를 통해 실제 DB에 Insert한다.
여기서 든 생각인데, 트랜잭션도 다음 시간에 적용해봐야겠다.
아무튼, 이렇게하면 간단히 작업이 완료된다. 여기까지는 JPA랑 비슷한 것 같다.
Postman으로 잘 호출된다.
DB에도 잘 들어간다 ㅎㅎ
그럼 이제 조회도 해보자.
이번엔 @Get 데코레이터에 :id 가 적혀있다. 이건 Path Variable이다. example/1 로 접근하면 findOne의 @Param('id')가 url에서 '1'만 추출해서 id 변수에 넣어줄 것이다. 그리고 이것을 exampleService의 findOne 함수의 인자로 전달한다.
그러면 Service는 해당 값을 전달받아서 Repository를 통해 작업을 수행해주기만 하면 된다.
여기서 exampleRepository.findOne() 메소드는 TypeORM에서 제공해주는 조회 전용 메소드이다.
인자로 옵션을 전달할 수 있는데, 나의 경우 where 절에 id에 해당하는 데이터를 가져오도록 지정하였다.
지금은 기본 메소드로 조회를 처리하는데, .createQueryBuilder로 Spring의 QueryDSL처럼 유사하게 사용할 수 있는 것 같다.
1번 아이디를 지닌 데이터를 조회해보았는데, 잘 나온다 ㅎㅎ
참, 갑자기 API의 모든 Prefix에 공통으로 api/v1을 추가하고 싶어졌다. 이건 main.ts에서 한 줄만 추가해주면 된다.
이제 다시 api/v1/ 을 붙여서 다시 호출해보자.
잘 된다 ㅎㅎ
3. 다음 시간에는 뭘 할까
1. 트랜잭션을 적용해보자.
2. 공통 Response를 처리하기 위한 Interceptor를 만들어보자.
3. Exception Handler 를 만들어보자.
이외에 또 생각나는 거 있으면 더 생각해보자!