IWished
article thumbnail

개요: WAS 실습

Achievement Goals

  • API 문서를 작성할 수 있습니다.
  • Fastify를 이용해 DB와 통신하는 서버를 만들 수 있습니다.
  • PostgreSQL을 이용하여 DB를 구성할 수 있습니다.
  • GitHub을 활용하여 팀원들과 협업합니다.

Day1

DevOps 부트캠프가 진행한 지 벌써 한 달이 지났고, 실습과제가 시작됐다.

주제는 쇼핑몰과 LMS(학습 관리 시스템) 두 개가 있고, 우리 팀은 LMS 주제로 진행이 되었다.

만들어야 할 애플리케이션의 조건은 7개의 최소 요구사항을 가지고 있었고, 다음과 같다.

 

LMS(학습 관리 시스템)

  • 사용자는 모든 수업을 조회할 수 있다
  • 사용자는 특정 분류의 수업을 조회할 수 있다(예: 강의자/ 수업명 / 수업코드 등)
  • 사용자는 수업을 수강신청 할 수 있다
  • 사용자는 모든 수강중인 수업을 조회할 수 있다
  • 사용자는 이메일 정보와 같은 개인정보를 변경할 수 있다
  • 사용자의 타입이 강의자일 경우 새로운 수업을 생성할 수 있다
  • 사용자는 수업에 대한 수강신청을 취소할 수 있다

1. ERD 구성

우리 팀은 백엔드를 경험 했던 사람이 총 3명이 있었고, 서로 의견을 공유하며 수월하게 테이블 생성과 테이블 간의 관계를 설정할 수 있었다.

1.1 테이블

요구사항을 분석해 class(수업), users(사용자), users_class(수강정보) 테이블을 구성했다.

문제사항 1

이 때, 사용자가 수강자인지 강의자인지 구분을 해야 했기에, users 테이블에 역할을 부여할지, 역할 테이블을 따로 만들어서 관리를 할지 고민을 했고, 이 과정에서 role(역할), role_user(유저 역할 정보) 테이블이 생성이 됐다.

 

토의 결과

우리는 토의를 한 결과, 수강정보 데이터가 1년 혹은 상/하반기로 나눠지며 상반기에서 하반기로 이동 시 상반기 정보 테이블은 제거되고 신규 생성이 되는 것으로 가정을 세웠고, 역할의 수 또한 수강자, 강의자로 제한되는 등 역할의 수가 많아지지 않을 것으로 예상했다.

그러하여 role, role_user 테이블을 삭제하고 user테이블에 is_professor 컬럼을 추가하여 역할을 관리하는 것으로 했다.

문제사항 2

나는 class 테이블 내에 강사명을 명시하는 professor_name 컬럼을 생성할 시 사용자가 수업정보를 검색할 때, 강사명으로 검색을 한다면, users 테이블을 조회하지 않더라도 professor_name 컬럼을 통해 class 테이블만 조회하여 처리할 수 있지 않을까 라는 생각을 했다.

 

토의 결과

우리는 오랜 시간 토의한 결과, users 테이블과의 조인을 통한 데이터 검색을 하도록 결정지었다.

이는, 정규식 조건을 지키면서 코드 구현 과정에서 SQL의 join문을 사용해 하나의 쿼리로 유저 정보까지 받아 올 수 있게 하여, professor_name 이외의 user 정보 또한 모두 가져오는 것이 좋을 것 같아 professor_name 컬럼은 삭제하였다.

 

1.2 테이블 관계

users & class 테이블의 일대다 관계

한 명의 사용자(강의자)가 여러 개의 수업을 진행할 수 있지만, 각 수업에는 한 명의 강의자만 있을 수 있기 때문에 일대다 관계를 지어준다.

즉, class 테이블의 user_id는 중복 값을 가질 수 있지만(한 명이 여러 개의 수업 진행), users 테이블의 기본 키 id(사용자의 고유한 id)는 중복 값을 가질 수 없다.

 

users & class & users_class 테이블의 각각 일대다 관계

여러 명의 사용자가 각각 여러 개의 수업을 수강신청 할 수 있기에 각각 일대다 관계를 지어준다.

즉, users_class 테이블의 user_id와 class_id는 중복 값을 가질 수 있고, users 테이블의 기본 키 id(사용자의 고유한 id)와 class 테이블의 기본 키 id(수업별 고유한 id)는 중복 값을 가질 수 없다.

 


2. API 문서 작성

https://neat-apartment-b02.notion.site/API-b6d629abd43d434dac26db36dc3c2cf0

 

API 문서

REST API Reference

neat-apartment-b02.notion.site


Day2

Project Setting

  • fastify 프로젝트 생성 및 DB 연결

MileStone1

  • DB 구성 및 서버 연결

MileStone2

  • API 문서에 따른 서버 구현

1. 데이터베이스 준비

PostgreSQL

  • ElephantSQL 접속 - 인스턴스 생성
  • 테이블 생성

2. fastify 프로젝트 생성

팀 레포지토리 클론 후 이동

fastify generate .

 

 

fasify 플러그인을 활용하여 데이터베이스와 연결

  • \plugins\postgres.js 파일 생성
'use strict'

const fp = require('fastify-plugin')

const {
  DATABASE_USER,
  DATABASE_PASSWORD,
  DATABASE_HOST,
  DATABASE_NAME
} = process.env

module.exports = fp(async function (fastify, opts) {
  
  fastify.register(require('@fastify/postgres'), {
    connectionString: `postgres://${DATABASE_USER}:${DATABASE_PASSWORD}@${DATABASE_HOST}/${DATABASE_NAME}`
  })
})
  • .env 파일
DATABASE_USER=아이디
DATABASE_PASSWORD=패스워드
DATABASE_HOST=호스트
DATABASE_NAME=아이디
  • \routes\aricle\index.js 파일 생성
'use strict'

module.exports = async function (fastify, opts) {
  fastify.get('/', async function (request, reply) {

    const client = await fastify.pg.connect()
    try {
      const { rows } = await client.query(
        'SELECT * FROM public.users'
      )
        
      reply.code(200).send(rows)
    } finally {

      client.release()
    }
  })
}
  • 경로 접속 시

성공


3. API 기능 구현

내가 맡은 API는 다음과 같다

  • 사용자는 모든 수업을 조회할 수 있다
  • 사용자는 특정 분류의 수업을 조회할 수 있다(강의자, 수업명, 수업코드 등)

강의 조회 GET /class

사용자는 모든 수업을 조회할 수 있다. 요청이 성공적으로 서버에 전달되면 200 OK를 반환하며, 요청 처리 중 발생한 오류는 응답 객체의 error필드에 나타납니다.

 

Request parameters

Name Type Description Required
professer_name (query) String 강의자의 이름 X
title (query) String 강의 제목 X

 

Response

HTTP Result Code가 OK일 때 반환하는 정보입니다.

Field Type Desc
id Integer 파라미터로 수신한 class_id
user_id Integer 수업 강의자의 user_id
title String 수업의 제목

 

[
    {
        "id" : 1,
    "user" : {
        "id": 4,
        "name": "이상윤"
     },
    "title" : "자바스크립트 1"
     },
     {
        "id" : 5,
    "user" : {
        "id": 4,
        "name": "한성"
     },
    "title" : "자바 언어의 이해"
     },
]

 

3.1 기능 구현

routes\class\read.js

  • fastify autoload의 dir를 routes 폴더로 register 하여, routes\class 폴더 내의 js파일의 "/" path는 class로 지정된다.

강의 전체 조회

'use strict'

module.exports = async function (fastify, opts) {

  fastify.get("/", async function (request, reply) {
    const client = await fastify.pg.connect();

    try {
      const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id"'
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
    } finally {
      client.release();
    }
  })
}

 

특정 강의 조회

처음엔, GET /class/:professor_name, GET /class/:title

강사명으로 조회, 수업 이름으로 검색을 각각 구현하려 했지만,

/class/:professor_name, /class/:title 이 모두 /class/:parameter 로 구분되어 사용하지 못함을 알게 되었고,

때문에, if문으로 강사명 파라미터와 수업명 파라미터를 구분하는 것으로 코드를 작성했다.

...
  fastify.get("/:parameter", async function (request, reply) {
    const professorName = request.query.professor_name; // professor_name (path) = 강의자의 이름
    const lectureTitle = request.query.title; // title (path) = 강의 제목
    const client = await fastify.pg.connect();

    try {
      // 강의자 이름과 강의 제목으로 검색 시
      if (professorName && lectureTitle) {
        const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE name = $1 AND title = $2',
          [professorName, lectureTitle]
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
      }
      // 강의자 이름으로 검색 시
      else if (professorName) {
        const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE name = $1',
          [professorName]
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
      }
      // 강의 제목으로 검색 시
      else if (lectureTitle) {
        const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE title = $1',
          [lectureTitle]
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
      }
    } finally {
      client.release();
    }
  });
}

이 과정에서, 그럼 parameter가 없는 상황도,

즉, 전체 조회 또한 패스를 나눌 필요가 없지 않을까 싶어서 합쳤다.

 

최종 구현 코드

'use strict'

module.exports = async function (fastify, opts) {
  fastify.get("/", async function (request, reply) {
    const professorName = request.query.professor_name; // professor_name (path) = 강의자의 이름
    const lectureTitle = request.query.title; // title (path) = 강의 제목
    const client = await fastify.pg.connect();

    try {
      // 강의자 이름과 강의 제목으로 검색 시
      if (professorName && lectureTitle) {
        const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE name = $1 AND title = $2',
          [professorName, lectureTitle]
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
      }
      // 강의자 이름으로 검색 시
      else if (professorName) {
        const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE name = $1',
          [professorName]
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
      }
      // 강의 제목으로 검색 시
      else if (lectureTitle) {
        const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE title = $1',
          [lectureTitle]
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
      }
      // 검색 필터 없을 때
      else {
        const { rows } = await client.query(
          'SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id"'
        );

        const result = rows.map((v) => {
          return {
            id: v.id,
            user: { id: v.user_id, name: v.name },
            title: v.title,
          };
        });

        reply.code(200).send(result);
      }
    } finally {
      client.release();
    }
  });
}

 

3.2 SQL문

전체 수업 조회 SQL문

SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id"

class 테이블 조회 시 users 테이블의 Primary key idclass 테이블user_id 컬럼을 join 하여 class 테이블의 강의자(user)가 누구인지 알 수 있도록 함

 

특정 수업 조회 SQL문

SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE name = $1 AND title = $2;
SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE name = $1;
SELECT * FROM "class" JOIN "users" ON "users"."id" = "class"."user_id" WHERE title = $1;

WHERE name = $1 AND titlt = $2;

  • 파라미터의 매개변수가 professor_name(변수명 : name) 과 title 두 개가 있다면, 그에 알맞은 수업이 있는지 조회
    • 두 매개변수를 모두 충족하는 값이 없다면 빈 array 출력
    • 두 매개변수를 모두 충족하는 값이 있다면 알맞은 값을 모두 출력

 

WHERE name = $1

  • 파라미터의 매개변수가 professor_name(변수명 : name) 하나만 있다면, 강사명에 알맞은 수업을 조회
    • 매개변수를 충족하는 값이 없다면 빈 array 출력
    • 매개변수를 충족하는 값이 있다면 알맞은 값을 모두 출력 (검색한 강사이름에 알맞는 모든 수업 조회)

 

WHERE title = $1

  • 파라미터의 매개변수가 title 하나만 있다면, 수업명에 알맞은 수업을 조회
    • 매개변수를 충족하는 값이 없다면 빈 array 출력
    • 매개변수를 충족하는 값이 있다면 알맞은 값을 모두 출력 (검색한 강의명에 알맞는 모든 수업 조회)


Section 1을 마치며...

나는 백엔드 공부를 한 번 했지만, 이후로 오래 쉬어 전부 잊어버렸다고 생각하고 있었다.

하지만 API 문서를 작성하고 ERD를 설계하는 과정에서 이전에 여러 번 해봤던 작업인지라 어렵다고 느껴지진 않았고,

NodeJS를 사용해보지 않은 입장에서 NodeJS가 굉장히 흥미롭게 느껴졌다.

또한, 설계대로 구현하는 과정에서 이전에 다른 프로젝트를 했을 때와 완전 다르다고 느껴지진 않아, 어려웠지만 아예 하지 못할 것 같다는 생각은 들지 않았던 것 같다.

어려운 부분들은 다른 팀원들의 도움을 받아 잘 해결해 나갈 수 있었고 함께 트러블슈팅을 하고 의논을 하며 진행을 하다 보니 즐거웠다.

 

이때 껏 이론 공부 할 땐, 몸을 배배 꼬아가며 가끔 소리를 지르고 싶을 때도 있었지만, 실습이 재밌어서 버틸 수 있었던 것 같다.

 

하지만, 결과적으로 팀원들의 도움이 없었다면 확실히 이번 실습과제는 어려웠을 것 같다.

새로운 언어에 익숙하지도 않고, 참조할 레퍼런스도 어떤 걸 찾아야 할지 몰라 많이 헤맸다.

나는 이상한 곳에 꽂히는 습성도 있어, 각각의 기능들을 병합시켜 실행시키는 과정에서도 폴더 정리에 대해 고민을 엄청 했던 것 같다.

기능별 폴더(path 지정 또한 가능하기에)를 만들어 그곳에 관련 기능을 넣을 것인가, 아니면 그저 각각의 기능들에 알맞는 path를 지정해 폴더 외부에서 정리를 할 것인가.

지금 당장은 큰 의미가 없는 작업이었지만, 그땐 그냥 거기에 꽂혔었다. 정리병이 있는 것 같다.

이 과정은 고맙게도 다른 팀원 분이 개인시간에 모두 기능별로 정리를 해두셨다.

고로 난 그저 고민 하는 감자가 되었던 것.

 

이전에는 정규수업 시간 외에 다른 것을 공부하거나, 예습을 하곤 했지만 점점 벅차기도 하고 지금 하는 것에 집중이 더 필요할 것 같다는 생각이 들었다.

요즘에는 또 몸이 좋지 않아 늦잠을 잘 뻔도 했고(새벽까지 논 것도 아닌데 억울하다. 게으른 사람으로 비춰지고 싶진 않은데) 두 번 정도 팀원들의 연락에 겨우 눈을 뜨기도 했다. 죄송하고 고마운 마음이다. 그래도 지각은 한 번도 안했다고!

이후에는 복습을 더 철저히 하고, 건강 관리 또한... 노력해야겠다. 의사 선생님, 저를 책임져 주세요. 제발요.

 

그래도 내가 공부하고 싶었던 인프라와 클라우드에 한 발짝 더 다가갈 수 있을 것 같아 마음만은 기쁘다.

다만, 지금은 쉬고 싶다. 시간아 달려라. 주말을 내게 데려와줘.