본문 바로가기

프리온보딩 백엔드/TIL(Today I Learned)

[Assignment 7] Cardoc(카닥) TIL

  • 진행 날짜 - 2021.11.22 pm 16:00 ~ 2021.11.29 pm 14:00
  • 과제 필수 포함 사항 

 

[필수 포함 사항]

  • READ.ME 작성
    • 프로젝트 빌드, 자세한 실행 방법 명시
    • 구현 방법과 이유에 대한 간략한 설명
    • 완료된 시스템이 배포된 서버의 주소
    • 해당 과제를 진행하면서 회고 내용 블로그 포스팅
  • Swagger나 Postman을 이용하여 API 테스트 가능하도록 구현

 

 [배경 및 공통 요구사항]

  • 데이터베이스 환경은 별도로 제공하지 않습니다. RDB중 원하는 방식을 선택하면 되며, sqlite3 같은 별도의 설치없이 이용 가능한 in-memory DB도 좋으며, 가능하다면 Docker로 준비하셔도 됩니다.
  • 단, 결과 제출 시 README.md 파일에 실행 방법을 완벽히 서술하여 DB를 포함하여 전체적인 서버를 구동하는데 문제없도록 해야합니다.
  • 데이터베이스 관련처리는 raw query가 아닌 ORM을 이용하여 구현합니다.
  • Response Codes API를 성공적으로 호출할 경우 200번 코드를 반환하고, 그 외의 경우에는 아래의 코드로 반환합니다.

 

  • 과제 Github 리포지토리
 

GitHub - WonDongGyun/assignment-7-cardoc: 위코드 x 원티드 프리온보딩 7번째 과제

위코드 x 원티드 프리온보딩 7번째 과제. Contribute to WonDongGyun/assignment-7-cardoc development by creating an account on GitHub.

github.com


 

🏫 사용한 프레임워크 & 라이브러리                                                       

  • Nest JS    
  • config     
  • supertest
  • typeorm mysql2
  • class-validator & class-transformer
  • passport-local & passport-jwt
  • morgan
  • swagger

 

 

 

 

 

 

💯 구현 목록                                                                                       

[구현한 API 목록]

✔️ 사용자 생성 API

✔️ 로그인 API

✔️ 사용자가 소유한 타이어 정보를 저장하는 API

✔️ 사용자가 소유한 타이어 정보 조회 API

 

테스트 코드

  • ✅ e2e Test
  • ✅ Unit Test

 

 

 

📋 Database Modeling                                                                         

 

 

 

 

💭 Project Review                                                                                

프리온보딩 백엔드 코스 마지막 과제로 차량 관리 어플리케이션을 만들고 있는 카닥이라는 기업의 과제를 진행하였습니다. 이번 과제는 팀 과제가 아니라 개인별로 진행하는 과제여서 일주일 동안 모든 과정을 혼자 생각하고 진행하였습니다.  해당 과제에서 요구한 API는 3개 정도밖에 없어서 처음에는 금방 만들 줄 알았는데, 끝나고 보니 제출 날 새벽 7시였습니다. 역시 쉬운 과제는 없군요.  🤒

 

 

💬 데이터 베이스 설계  

과제에서 요구한 API로는 사용자 생성 API, 사용자가 소유한 타이어 정보를 저장하는 API, 사용자가 소유한 타이어 정보를 조회하는 API가 있었습니다. 그래서 저는 사용자 테이블, 차량 정보를 저장하는 테이블, 타이어 테이블 이렇게 3가지의 테이블을 구성하였고, 추가적으로 공통 코드 테이블을 만들었습니다. 

 

사용자 테이블의 경우에는 id와 password 말고는 별다른 요구사항이 없었기 때문에 id, password, createdAt, updatedAt 이렇게 4개의 컬럼으로 테이블을 구성하였습니다. id의 경우에는 넉넉하게 varchar 15를 주었고, password의 경우에는 bcrypt 암호화를 적용해서 저장할 것이었기 때문에 최댓값을 부여하였습니다. 

 

차량 정보를 저장하는 테이블의 경우에는 trimId 말고는 별다른 정보가 없다는 것에 의아해 하실 수도 있을 것 같습니다. 해당 과제에서는 카닥에서 제공하는 자동차 정보 조회 API가 주어졌었는데, 확인해보니 해당 API에 차량과 관련된 정보가 엄청나게 많았고, 이 과제는 타이어 API를 설계하고 구현하는 것이었기 때문에 차종 ID 값인 trimId만 저장하는 것이 좋겠다고 판단하였습니다. 사용자가 여러 대의 차종을 가질 수 있다고 생각하여서 사용자 테이블과는 1:M의 관계를 갖습니다.

 

기업에서 제공해준 API의 일부 입니다.

 

 

 

타이어 테이블 같은 경우에는 처음에는 차량 정보를 저장하는 테이블에 합쳐서 만들 생각이었습니다. 하지만 생각을 해보니 하나의 차종은 여러개의 타이어를 가질 수 있었기 때문에 그냥 분리해서 따로 테이블을 만들고 1:M의 관계를 갖도록 하였습니다. 이 테이블도 다른 테이블처럼 만들기 간단하면 좋았겠지만 과제에서 타이어에 대한 요구사항이 있었습니다. 요구사항은 다음과 같습니다.

 

타이어 정보는 205/75R18의 포맷이 정상이다. 205는 타이어 폭을 의미하고 75R은 편평비, 그리고 마지막 18은 휠 사이즈로써 {폭}/{편평비} R {18}과 같은 구조이다. 위와 같은 형식의 데이터일 경우만 DB에 항목별로 나누어 서로 다른 Column에 저장하도록 한다.

 

처음에는 해당 요구사항을 보고 폭, 편평비, 휠 사이즈를 저장할 수 있는 컬럼만 있으면 되겠다고 생각했습니다. 하지만 제공해주신 자동차 정보 조회 API를 보다 보니 요구사항에 적혀있는 타이어 포맷과는 다른 포맷들이 존재했습니다. 타이어 포맷이 서로 다른 것을 보고 타이어에 대해 잘 알지 못하면 이번 과제는 제대로 진행할 수 없을 것이라고 판단하였고, 타이어 포맷 정보를 찾기 시작했습니다. 마침 넥센 타이어 홈페이지에 타이어 포맷을 잘 알려주는 글이 있어서 해당 글을 참고하여 칼럼을 만들어 보기로 하였습니다.

 

출처: https://www.nexentire.com/kr/information/tire_information/guide/orthography.php

 

타이어 포맷에는 보시는바와 같이 여러 가지 포맷이 존재하는데, 카닥은 이 중에서도 ISO 호칭을 따르며 TR 호칭법을 사용하여 표기하는 것 같았습니다. 그래서 TR 호칭법에 따라 타이어의 용도, 타이어의 성질, 폭, 편평비, 휠 사이즈를 저장할 수 있도록 칼럼을 만들었습니다. 

 

공통 코드 테이블은 다른 테이블에서는 코드 이름만 필요하기 때문에 따로 관계를 연결하지는 않았습니다. 다른 테이블에서 공통 테이블을 사용하려면 codeId를 가지고 있으면 됩니다. 해당 테이블을 사용하여 타이어가 앞쪽 타이어인지 뒤쪽 타이어 인지 구분할 수 있도록 하였습니다. 코드를 사용하지 않고 타이어 테이블에 문자열로 저장하는 방식을 사용할 수도 있겠지만 그렇게 하지 않았던 이유는 만약에 다른 이유로 해당 문자열을 다른 문자열로 바꿔서 표기해야 하는 경우가 발생하면 타이어 테이블에 있는 모든 문자열을 바꿔야 하기 때문입니다. 그래서 코드 테이블을 활용하여 표기하는 방식을 선택하였습니다.

 

 

 

 

 

💬 사용자가 소유한 타이어 정보를 저장하는 API  

인증 토큰을 발급하고 이후의 API는 인증된 사용자만 호출할 수 있다라는 조건이 붙어있었으나, 해당 API 요구사항에는 id와 trimId를 같이 받고 있었고 한 번에 5명까지의 사용자에 대한 요청을 받을 수 있었습니다. 이 부분에서 해당 API는 로그인 없어도 5명의 사용자를 입력할 수 있는 API라고 생각했고, 해당 API에 한해서만 인증 없이 사용할 수 있게 구현하였습니다.

 

그리고 이 과제는 타이어 정보를 저장하고 조회하는 타이어 API를 설계하고 구현하는 것이었기 때문에, trim 정보와 tire 정보는 제공해주신 자동차 정보 조회 API를 사용하여 한 번에 저장할 수 있도록 하였습니다. 그래서 현재 해당 API의 controller는 다음과 같이 코딩되어 있습니다. @saveUserTrimModel()이라는 커스텀 데코레이터를 사용하여 형식에 맞는지 검증하게 하였고, 만약 1개 ~ 5개 사이의 요청이 아니라면 에러를 발생하게 만들었습니다.

 

// TrimController

@Controller("trim")
export class TrimController {
	constructor(
		private readonly httpService: HttpService,
		private readonly trimService: TrimService
	) {}

	@Post("")
	@ApiOperation({
		summary: "사용자 차종 저장 API",
		description: "사용자가 소유한 자동차 정보 및 타이어 정보를 저장합니다."
	})
	async userTrim(
		@saveUserTrimModel()
		saveUserTrimDtoModel: SaveUserTrimDto[]
	) {
		const response = [];

		for (const saveUserTrimDto of saveUserTrimDtoModel) {
			const url = process.env.TRIM_API + saveUserTrimDto.trimId;
			const res = await lastValueFrom(this.httpService.get(url));
			response.push(
				await this.trimService.saveUserTrim(
					saveUserTrimDto,
					res.data.spec.driving
				)
			);
		}

		return response;
	}
}

 

 

 

 

자동차 정보 조회 API를 사용해서 한번에 저장하다보니 트랜잭션 설정 없이는 데이터의 무결성이 지켜지지 않았습니다. 예를 들어 trim 데이터는 성공적으로 trim 테이블에 저장되었으나 tire 데이터가 잘못되어 오류가 발생해 타이어 테이블에 저장되지 않은 경우, trim 데이터는 있지만 tire 데이터는 없는 상황이 발생하게 됩니다. 따라서 trim service에서는 각기 다른 리포지토리에서 불러오는 메소드들을 하나의 트랜잭션으로 묶어서, 하나가 오류가 발생하면 전부 rollback 되도록 하였습니다. rollback 되지 않는다면 마지막 작업이 성공적으로 수행되고 나서 commit 하게 됩니다.

 

 

// TrimService

@Injectable()
export class TrimService {
	constructor(
		private readonly trimRepository: TrimRepository,
		private readonly tireRepository: TireRepository,
		private readonly userRepository: UserRepository
	) {}

	async saveUserTrim(saveUserTrimDto: SaveUserTrimDto, res) {
		const queryRunner = await getConnection().createQueryRunner();
		await queryRunner.startTransaction();

		const findUser: User = await this.userRepository.findUser(
			saveUserTrimDto.id
		);

		try {
			const createTrim: Trim = await this.trimRepository.saveUserTrim(
				queryRunner.manager,
				findUser,
				saveUserTrimDto.trimId
			);

			await this.tireRepository.saveTrimTire(
				queryRunner.manager,
				createTrim,
				res
			);

			await queryRunner.commitTransaction();
			return createTrim;
		} catch (err) {
			await queryRunner.rollbackTransaction();
		} finally {
			await queryRunner.release();
		}
	}
}

 

 

 

 

 

💬 네이버 클라우드 사용하기 (ft. jenkins) 

혼자하는 프로젝트니까 배포도 제가 해야겠죠? 마침 Naver Cloud 쪽에서 프리온보딩 과정을 진행하는 사람들에게 30만 원 상당의 쿠폰을 협찬해주셔 가지고 AWS가 아니라 네이버 클라우드를 사용해보기로 했습니다. 그동안 AWS로만 배포해서 네이버 클라우드로 배포하는 방식이 낯설었는데, 한국 클라우드 플랫폼이다 보니 AWS에서는 볼 수 없던 친절함 가득한 한글 문서에서 감동을 받았습니다. AWS도 한국어를 지원해주지만 그렇게 친절하지는 않았거든요. 네이버 클라우드는 유튜브에 동영상도 있어서 쉽게 따라 할 수 있어서 좋았습니다. 

 

배포 완료!

 

 

 

이번에는 전부터 하고 싶었던 배포 자동화를 해보기로 했습니다. 예전부터 컴퓨터에서 열심히 코드 고쳐서 git push를 하고, 바꾼 코드를 받기 위해 매번 클라우드 서버에서 git pull를 하는게 귀찮았거든요. 그래서 요번에는 Jenkins라는 도구를 사용해서 배포 자동화를 만들어 보았습니다. Jenkins를 선택한 이유는 처음으로 해보는 배포 자동화라서 설치 및 사용이 간단하고 무료이며 인터넷에서 정보를 찾기 쉬운 도구를 찾고 있었기 때문입니다. 이것말고도 엄청나게 장점이 많다고 하는데 처음 사용해보는 것이니 꼭 필요한 기능만 사용해서 만들었습니다.

 

그런데 설정하는게 쉬울 줄 알았으나 생각보다 엄청나게 힘들었습니다. Jenkins 서버에 권한 문제로 오류가 나지 않나, ssh key값 전부 설정했다고 생각했는데 Nest js 서버에는 public key를 등록하지 않았다던가... 이런 자잘 자잘한 문제 때문에 엄청 오래 걸렸지만 결국에는 성공했습니다. 더 이상 클라우드 서버에서 git pull를 하지 않아도 되니까 너무 기분이 좋네요.  😀

 

 

 

이것땜에 하루를 날렸지 뭐야

 

 

 

👩‍💻반성점 OR 다음 프로젝트부터 고치고 싶은 것 

요번 프로젝트에서 몇가지 아쉬운 점이 있었다면, 시간은 많이 주어졌지만 뭔가 시간에 쫓기면서 개발한 부분이 있었습니다. 왜 나는 시간 관리를 제대로 하지 못했는가...... 그리고 에러 처리 부분이 미흡한 부분이 있었습니다. 최대 5명의 사용자를 입력해서 타이어 정보를 입력하는 API를 만들 때, null을 반환할 것이 아니라 제대로 에러 메시지를 내보냈어야 했는데 그 부분을 제대로 처리하지 못했습니다. 코드가 깔끔하지 못했던 부분도 존재했고요. 그래서 마지막 과제가 끝난 지금 남은 시간 동안은 이 프로젝트를 조금씩 다듬으면서 부족했던 부분을 고칠 것 같습니다.