도커(Docker)?

 도커는 컨테이너 기반의 오픈소스 가상화 플랫폼이다. 여기서 컨테이너는 격리된 공간에서 프로세스를 실행하는 기술을 말하는데, 이 기술을 사용하면 하나의 컨테이너에 애플리케이션과 그것을 실행하는 데 필요한 모든 종속성을 포함한다.

 다시말해, 컨테이너환경에서 구축한 것은 어떤 환경에서도 똑같이 작동한다는 특징이 있다.

 

컨테이너 방식을 사용하면 얻을 수 있는 이득은 매우 많지만 몇가지만 말해보자면

  1. 의존성 충돌 문제의 해결
  2. 개발과 배포 환경의 일치
  3. 훨씬 쉬운 수평확장
  4. 각 서버에 새로운 내용을 배포하는 난이도 하락

일 것이다.

 이 장점들 모두 컨테이너안에 구축된 환경이 컨테이너 밖과 완전히 격리되는 특징으로부터 생긴다.

 그렇기 때문에 각기 다른 환경에서도 같은 컨테이너를 사용해 배포한다면, 똑같이 작동할 수 있다.

 

즉, 아래와 같은 끔찍한 상황을 미연에 방지할 수 있다.

도커의 주요 키워드

 

이미지 (Image)

컨테이너를 생성하기 위한 템플릿으로, 애플리케이션과 종속성을 포함한다. 이미지는 Dockerfile을 통해 만들어지며, 다른 사용자와 공유할 수 있다. 즉, 내 컨테이너 내부환경을 다른 사용자와 나눌 수 있다.

 

레지스트리 (Registry)

컨테이너를 생성하기 위한 템플릿으로, 애플리케이션과 종속성을 포함한다. 이미지는 Dockerfile을 통해 만들어지며, 다른 사용자와 공유할 수 있다. 즉, 내 컨테이너 내부환경을 다른 사용자와 나눌 수 있다.

 

Dockerfile 

도커 이미지를 생성하기 위한 스크립트 파일로, 애플리케이션과 실행 환경을 설정한다. Dockerfile에는 베이스 이미지, 필요한 소프트웨어 설치, 애플리케이션 코드 복사 등의 명령들이 들어간다.

 

Docker Hub

도커 이미지를 공유하는 공식 저장소로, 다양한 오픈소스 프로젝트 및 기업용 이미지를 찾을 수 있다. 개인이나 팀이 공유하고 싶은 이미지를 업로드하고 다운로드할 수 있는 플랫폼이다.

https://hub.docker.com/

 

Docker Hub Container Image Library | App Containerization

Deliver your business through Docker Hub Package and publish apps and plugins as containers in Docker Hub for easy download and deployment by millions of Docker users worldwide.

hub.docker.com

 

Docker Engine

도커 이미지를 실행하고 관리하는 소프트웨어다. 도커 엔진은 Dockerfile을 사용하여 이미지를 빌드하고, 이미지를 컨테이너로 실행한다.

 

Docker Compose

여러 컨테이너로 구성된 애플리케이션을 정의하고 관리하는 툴이다.

YAML 파일 형식으로 작성된 docker-compose.yml을 사용하여 서비스, 네트워크, 볼륨 등의 설정을 작성해두면, 한 번의 명령어로 편리하게 애플리케이션을 시작하거나 중지할 수 있다.

 

볼륨(Volume)

컨테이너의 데이터를 영구적으로 저장하는 방식이다.

컨테이너는 일시적이고 변경 가능하다. 여기서 볼륨을 사용하면 컨테이너가 삭제되거나 재시작될 때 데이터가 손실되지 않도록 데이터를 호스트 시스템에 저장할 수 있다.

물론 볼륨은 여러 컨테이너에서 공유하는 것도 가능하다.

 

도커 설치

우분투에서 도커 엔진을 설치하려면 아래 명령어를 입력해 도커 패키지를 설치해준다.

sudo apt update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

설치가 다 됐다면 한번 아래 명령어를 사용해서 제대로 설치 된건지 확인해본다.

sudo docker --version

sudo docker run hello-world

 

docker-compose 설치

우분투에서 도커 컴포즈를 설치하려면 아래 명령어를 입력해 설치해주면 된다.

 sudo apt-get update
 sudo apt-get install docker-compose-plugin

설치가 다 됐다면 한번 아래 명령어를 사용해서 제대로 설치 된건지 확인해본다.

 docker compose version

결과가 제대로 나오면 성공이다.

1차 팀 프로젝트 소개

섹션1. 팀프로젝트 주제는 쇼핑몰이었다.

프로젝트의 목표는 요구사항을 충족하는 DB와 API를 설계하고 설계에 맞는 ERD와 API명세를 작성하는 것으로 시작해, 명세된 기능을 fastify를 사용해 웹서버를 구현해 제작한 API가 잘 작동하는지 확인하는 것이다.


필수 요구사항

  • 사용자는 모든 상품을 조회할 수 있다
  • 사용자는 특정 분류의 상품을 조회할 수 있다(상품분류, 브랜드명, 가격, 상품명)
  • 사용자의 타입이 판매자인 경우 자신의 상품을 등록할 수 있다
  • 사용자는 상품을 장바구니에 담을 수 있다
  • 사용자는 자신의 장바구니를 조회할 수 있다
  • 사용자는 자신의 장바구니에 있는 상품의 수량을 변경시킬 수 있다
  • 사용자는 상품을 자신의 장바구니에서 제외할 수 있다

추가 요구사항

  • 회원가입, 로그인, 로그아웃을 할 수 있다.
  • 관리자는 모든회원의 정보를 조회할 수 있다.
  • 사용자는 자신의 정보를 조회할 수 있다.
  • 관리자는 회원을 삭제할 수 있다.

DB 설계

쇼핑몰 프로젝트 ER-Diagram

필요한 테이블은 크게 users, products, Cart테이블에 더해서 product 컬럼 내용의 중복이 많을 수 있는 category와 brand는 따로 테이블을 만들어서 관리한다.

Brands와 Categories는 이 설명만으로 충분하기 때문에 나머지 테이블을 살펴보면

  • users 테이블은 pk인 id, 그리고 유저가 어떤타입(일반, 관리자, 판매자)인지 나타내는 type과 password, email 컬럼으로 구성된다.
  • products 테이블은 pk인 productid, 그리고 판매자(user)의 id인 sellerId, 해당 브랜드의 id인 brandid, 해당되는 카테고리의 id인 categoryid를 각 해당테이블과의 외래키로, 나머지는 상품명인 productName과 가격인 price, 마지막으로 상품 설명인 desc로 이뤄진다.
  • Cart 테이블은 pk인 id, 그리고 장바구니 주인(user)의 id인 userId, 장바구니에 담긴 상품의 id인 productid를 각 해당테이블과의 외래키로, 나머지는 담긴 상품의 양인 qty로 이뤄진다.

API 명세

Method Path Request Header Request Body Response Status Code Response Body
GET /product Content-Type: application.json - 200 OK [ { "sellerID": 1, "brandID": 1, "categoryID": 1, productName": "테스트 상품.", "price: 50000", "desc": "테스트용 상품입니다." }, { "sellerId": 2, "brandId": 2, "categoryId: 2, "productName": "테스트 상품2", "price": 500000, "desc": "테스트용 상품2입니다." } ]
GET /product/:productId Content-Type: application.json - 200 OK 404 Not Found { { "sellerID": 1, "brandID": 1, "categoryID": 1, productName": "테스트 상품.", "price: 50000", "desc": "테스트용 상품입니다." } }
POST /product Content-Type: application.json, Authorization { "brandId": 1, "categoryId": 1, "productName": "테스트 상품.", "price": 50000, "desc": "테스트용 상품입니다." } 201 OK 400 Bad Request 403 Forbidden { "sellerID": 1, "brandID": 1, "categoryID": 1, productName": "테스트 상품.", "price: 50000", "desc": "테스트용 상품입니다." }
PUT /product Content-Type: application.json, Authorization { "brandId": 2, "categoryId": 2, "productName": "테스트 상품 수정.", "price": 50000, "desc": "테스트용 상품 수정입니다." } 201 OK, 400 Bad Request 403 Forbidden { "brandId": 2, "categoryId":2, "productName": "테스트 상품 수정.", "price":50000, "desc": "테스트용 상품 수정입니다." }
DELETE /product/:productId Content-Type: application.json, Authorization { "productID": 1 } 200 OK 204 No Content 400 Bad Request { "Success", "Bad Request" }
GET /cart Content-Type: application.json - 200 OK [ { "cartId": 1, "productId": 1, "productName": "상품명", "brandName": "브랜드명", "categoryName": "분류명", "price": 1000, "desc": "설명" } ]
POST /cart Content-Type: application.json, Authorization { "productId": 1 } 201 OK "Success"
PUT /cart/{cartId} Content-Type: application.json, Authorization { "productID": 1, "Qty": 1 } 200 OK 204 No Content 400 Bad Request "Success", "Bad Request"
DELETE /cart/{cartId} Content-Type: application.json, Authorization { "productID": 1 } 200 OK 204 No Content 400 Bad Request "Success", "Bad Request"
GET /user Content-Type: application.json Authorization - 200 OK [ { "userId": 2, "email": test@test.com, "password": 1234 }, { "userId": 3, "email": test2@test.com, "password": 1234 } ]
GET /user/:userId Content-Type: application.json Authorization - 200 OK 204 No Content { "userId: 2, "email": test@test.com "password": 1234 }
POST /signUp Content-Type: application.json, Authorization { "email": test@test.com "password": 1234 } 201 Created 400 Bad Request "Created", "Bad Request"
POST /signIn Content-Type: application.json { "email": test@test.com "password": 1234 } 200 OK 401 Unauthorized "OK", "Unauthorized"
PUT /user/:userId Content-Type: application.json, Authorization { "email": test@test.com "password": 1234 } 200 OK 400 Bad Request "Success", "Bad Request"
DELETE /user/:userId Content-Type: application.json, Authorization - 200 OK 400 Bad Request "Success", "Bad Request"

 API 명세서를 작성하면서 어떻게 API를 구성해야 구조가 직관적이어서 구현할 때 보기 편할지 고민을 많이 했다.

 그 결과 결국 상품, 사용자, 장바구니 별로 API 경로를 구분해 작성하는 것이 구현에 있어서도, 테스트에 있어서도 가장 직관적일 것이라고 판단해 API url을 정의했다. 그리고 request body값과 response값의 정의는 개발작업 중 편의를 위해서 개발 도중 변경하는 노선을 택했다.


구현작업 중 마주했던 이슈들 

초반 프로젝트 방향성 정리 부족으로 인한 의사소통 문제

 

 처음 프로젝트를 시작할때 서비스를 만듦에 있어서 확실히 정하고 넘어가야 하는 부분에 대한 충분한 의사소통이 없었던 점이 아쉽다.

 프로젝트를 진행하면서 이 프로젝트의 부분간의 연관점을 어떻게 다룰 것인가에 대해서 미리 정하지 않아서 서로가 서로의 작업 끝나는 것을 기다리면서 시간을 허비하거나 부분간의(ex. 깃/깃허브는 어떻게 사용할지) 유기적인 상호작용이 제대로 되지 않는 문제가 있었다. 

 

의사소통 문제로 인한 작업량 불균형 문제

 

앞서 언급했던 의사소통 문제의 연장선으로, 팀원들 간의 논의가 이뤄지지 않은 문제들이 속속 튀어나오면서 개발에 익숙한 팀원들이 프로젝트를 진행하게 되는 상황이 발생해버렸다.  그바람에 개발에 익숙하지 않았던 팀원들이 작업에 제대로 참여하지 못하게 됐고, 결과적으로 역량강화를 위해서 했어야하는 경험들을 팀원들 모두가 충분히 하지는 못했다고 생각한다.

 

어떻게 해야 해결할 수 있을까?

 앞으로는 프로젝트를 진행할 때 시간을 조금 할애하더라도 팀원들간의 차이를 고려해 프로젝트의 어떤작업을 어떻게 진행할지 확실히 해두고 다음 단계로 넘어가야겠다고 다짐했다. 

 그리고 프로젝트 진행 중에도 작업이 버거워보이는 팀원들에게 먼저 도와주러 가는 태도를 더 적극적으로 가져야겠다는 생각이 들었다.

 


 

깃허브 오류 : git pull 명령어 사용시 에러( Need to specify how to reconcile divergent branches. )

 

이 오류는 클론 받아온 로컬 브랜치와 원격 브랜치가 충돌이 있는 상태에서 merge(병합)하려고 할 때 발생한다. 그래서 이 문제를 해결하려면 먼저 충돌을 해결한 뒤에 병합을 진행해야 하는데, 해결법은 다음과 같다.

  1. 로컬 브랜치의 변경 사항을 커밋해준다.
  2. 로컬 브랜치 - 원격 브랜치의 동기화를 위해서 pull을 수행해준다.
  3. 충돌이 발생한 파일을 찾아서 수동으로 충돌을 해결해준다. 충돌은 <<<<<<< HEAD, =======, >>>>>>> 같은 마커로 표시된다.
  4. 충돌이 해결된 파일을 다시 커밋하고, 병합을 완료하면 해결된다.

Postgre 데이터베이스 오류 : 보낸쿼리에서 컬럼명을 찾지 못하는 문제 에러 (column "productname" does not exist)

 

`update public.products set productName = COALESCE($1, productName), price = COALESCE($2,parsedPrice), "desc" =COALESCE($3,"desc") where id=$4`, 
              [productName, parsedPrice, desc, productId]

 이 쿼리문이 Postgre 데이터베이스에서 컬럼명을 찾지 못하는 오류였다.

 이 오류는 알고보니 'productName'컬럼명처럼 대소문자를 구분해서 작성한 구문이 있어서 생기는 오류였다.

 Postgre는 기본적으로 컬럼 이름을 소문자로 해석하는데, 그래서 나는 분명 컬럼명을 대소문자로 구분해 적었지만 Postgre는 컬럼명을 모두 소문자로 해석해서 일어난 오류이다.

 

 

이 오류를 해결하는 법은 매우 간단하다. 아래처럼대소문자를 구분해 적어둔 구문에다가 쌍따옴표를 씌워주면 된다.

`update public.products set "productName" = COALESCE($1, "productName"), price = COALESCE($2,price), "desc" =COALESCE($3,"desc") where id=$4`, 
            [productName, parsedPrice, desc, productId]

이렇게 쿼리문을 작성해주면 오류가 깔끔하게 해결되는 것을 볼 수 있다.

 

 


데브옵스 부트캠프에서 첫 프로젝트 작업이었다. 프젝 진행중에 생기는 상황을 보면서 미리 논의를 하는게 얼마나 중요한지 조금이나마 께닫는 시간이 된 것 같다. 복잡하게 흘러가는 상황에 따라오지 못하는 팀원들을 보며 안타까웠고, 앞으로도 진행할 다른 프로젝트들에 있어서는 이번 프로젝트와 같은 문제를 반복하지 않도록 신경 많이 써야겠다. 

 그리고 회고를 쓰면서 아쉬웠던 점은 구현작업 도중에 마주쳤던 오류, 이슈들에 대한 기록들을 소홀하게 했다는 것이다. 많은 에러와 싸우며 완성한 프로젝트임에도 이 해결과정을 남기지 못하는 점이 너무나도 아쉽다. 앞으로는 오류 마주칠 때마다 입맛다시면서 기록해야겠다 ㅎㅎㅎ

CI/CD 파이프라인은 소프트웨어 개발 프로세스에서 개발, 테스트 및 배포를 자동화하기 위한 과정을 말한다.

이 파이프라인은 개발자들이 소프트웨어 개발에 대한 변경 사항을 더욱 쉽고 빠르게 반영할 수 있도록 도와준다.

CI/CD 파이프라인

왜 CI/CD 파이프라인을 사용해야 할까?

크게 4가지 이유로 정리해볼 수 있을 것 같다.

  • CI/CD 파이프라인을 통해 개발자들은 빠르게 개발하고 배포할 수 있다. 그러면 개발자들이 더 많은 시간을 코드 작성에 집중하고, 소프트웨어의 배포 주기를 단축하여 사용자들이 새로운 기능을 더욱 빠르게 사용할 수 있다.
  • CI/CD 파이프라인을 사용하면, 오류가 최소화되고 S/W 배포의 안정성이 향상된다. 이는 개발자들이 소프트웨어에 대한 변경 사항을 신속하게 확인할 수 있기 때문에, 버그 및 오류가 더 빠르게 발견되어 해결될 수 있다.
  • CI/CD 파이프라인은 개발 및 배포 프로세스의 자동화를 통해, 사람의 작업을 최소화해 인적 오류를 예방한다. 이는 개발자들이 더욱 안정적이고 신뢰성 있는 소프트웨어를 배포할 수 있도록 해준다.
  • CI/CD 파이프라인은 소프트웨어 개발자와 운영자 간의 협업을 원활하게 만들어준다. 이전까지는 개발자와 운영자가 각자의 업무에만 전념하다가, 개발된 소프트웨어가 운영에 문제가 발생하는 경우가 많았지만, CI/CD 파이프라인을 사용한다면 개발자와 운영자가 함께 작업하고, 이를 자동화하는 방식으로 문제를 예방하고 해결할 수 있다.

 

CI/CD 파이프라인의 각 단계

  1. Plan : 소프트웨어에 대한 기획, 설계를 하는 단계입니다. 여러 문서작업이 수반된다.
  2. Code : 소스 코드를 저장하고 관리하는 단계입니다. 대표적으로 Git이 쓰인다.
  3. Build: 소스 코드를 컴퓨터가 이해할 수 있는 실행 가능한 형태로 변환한다.
  4. Test: 빌드된 소프트웨어가 정상적으로 동작하는지 확인한다.
  5. Release: 코드의 품질과 보안을 검사한다.
  6. Deploy: 빌드된 소프트웨어를 실제 서버에 배포한다.
  7. Operate: 소프트웨어가 실제로 동작하며, 소프트웨어가 제대로 동작하는지 모니터링한다.

각 단계는 일련의 자동화된 작업으로 구성된다.

 소스 코드가 변경되면, CI 파트가 시작된다. 이 때, CI 파트에서는 소스 코드를 빌드하고, 테스트를 수행하며, 코드에 문제가 없는지 확인한다.

 테스트가 통과되면, CD 파트가 시작된다. CD 파트에서는 빌드된 소프트웨어가 배포 가능한 형태인지 확인하고, 배포될 수 있도록 필요한 작업을 수행한다.

+ Recent posts