diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index ccd76f0..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index 21ca89f..c1ee09f 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ out/ .jython_cache +.vscode # Local .terraform directories @@ -23,6 +24,7 @@ out/ # .tfstate files *.tfstate *.tfstate.* +terraform.exe # Crash log files crash.log diff --git a/README.md b/README.md index 3af3f87..3c2609d 100644 --- a/README.md +++ b/README.md @@ -1 +1,134 @@ -# movie_chatbot \ No newline at end of file +# 무비빔밥 +사용자 선호 및 위치 기반 영화관을 추천해주는 **지능형 고객 지원 챗봇** + + + + +- 🔗 서비스 링크 : http://d14hn9nv9zhgf7.cloudfront.net/ +- 📅 개발 기간 : 2024.07.17 ~ 2024.09.03 (7주) + +
+ +## 팀 구성 및 역할 + +
+Alyssa - 인공지능 담당 +
+ +- ChatGPT API를 활용한 응답 생성 + - 사용자 질문 데이터 전처리 + - 프롬프트 엔지니어링을 통한 영화관 추천 생성 +- 프로젝트 협업 관리 + +
+
+ +
+Ryan - 인공지능 담당 +
+ +- LLM ChatGPT api를 활용한 응답 생성 + - 사용자 질문에서 NER을 이용하여 Entity 추출 + - koBERT,kiwi를 이용한 RAG구축 후 + - FAISS를 이용한 Semantic Search, 및 Levenshtein distance 기반 검색 기능 개발 + - LLM 응답 정형화 + +
+
+ +
+Yohan - 풀스택 담당 +
+ +- 백엔드 + - Spring 애플리케이션 서버 개발 + - AI 모델 구동을 위한 FastAPI 서버 개발 + - OpenFeign을 사용해 서버 간 통신 구현 + - MySQL DB 조회 기능 개발 +- 프론트엔드 + - 날짜 선택, 지역 선택 화면 구현 + +
+
+ +
+Mir - 풀스택 담당 +
+ +- 영화 상영정보 크롤링 및 DB에 저장 +- 프론트엔드 + - 채팅 화면 구현 + - 질문-답변 채팅형태 구현 및 엔티티 관리 + - 매뉴얼/응답대기 메시지 및 재확인 버튼/체크박스 구현 + - 스타일 적용 + - 백엔드 연결 + +
+
+ +
+Bryan - 클라우드 담당 +
+ +- AWS 인프라 설계 +- Terraform을 활용해 AWS 구현 +- Ansible을 활용한 EC2 인스턴스 개발 환경 구축 +- Prometheus 및 Grafana를 활용한 인스턴스 모니터링 환경 구축 + +
+
+ +
+Eddy - 클라우드 담당 +
+ +- CI/CD 파이프라인 구축 +- Docker를 이용한 애플리케이션 이미지 만들기 및 배포 +- 전체적인 배포 환경에서의 애플리케이션 실행 테스트 + +
+
+ +
+ +## 주요 기능 +- 채팅, 버튼을 통한, 의사소통 기능 +- 영화 이름, 지역, 날짜, 시간 정보를 통해 조건에 맞는 영화관의 상영 정보 제공 기능 + +
+ +## Stack +- Frontend : +- Backend : +- Cloud : +- AI : + +
+ +## 아키텍처 +![Movie-chatbot1](https://github.com/user-attachments/assets/1e5d10f9-a22c-4588-90c4-65a51d475259) + + +
+ +## 프로젝트 수행 결과 + +- [풀스택](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%EA%B2%B0%EA%B3%BC_%ED%92%80%EC%8A%A4%ED%83%9D.md) +- [클라우드](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%EA%B2%B0%EA%B3%BC_%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C.md) +- [인공지능](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%EA%B2%B0%EA%B3%BC_%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5.md) + +
+ +## 트러블 슈팅 + +- [풀스택](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85_%ED%92%80%EC%8A%A4%ED%83%9D.md) +- [클라우드](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85_%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C.md) +- [인공지능](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%8A%B8%EB%9F%AC%EB%B8%94%EC%8A%88%ED%8C%85_%EC%9D%B8%EA%B3%B5%EC%A7%80%EB%8A%A5.md) + +
+ +## 회고 + +- [회고](https://github.com/KTB-19/movie_chatbot/blob/main/docs/%ED%9A%8C%EA%B3%A0.md) + +
diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..dfb18f1 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "backend", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/cloud/ansible/crawling-docker-compose.yml b/cloud/ansible/crawling-docker-compose.yml index bddca2b..a46f946 100644 --- a/cloud/ansible/crawling-docker-compose.yml +++ b/cloud/ansible/crawling-docker-compose.yml @@ -2,14 +2,18 @@ version: '3.8' services: movie-crawling: - image: prunsoli/movie-crawling:2.0.7 + image: prunsoli/movie-crawling:latest container_name: movie-crawling - + env_file: + - .env + environment: + - TZ=Asia/Seoul + cadvisor: image: gcr.io/cadvisor/cadvisor:v0.49.1 container_name: cadvisor ports: - - "9100:8080" + - "9090:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:ro diff --git a/cloud/ansible/backend-docker-compose.yml b/cloud/ansible/db-docker-compose.yml similarity index 86% rename from cloud/ansible/backend-docker-compose.yml rename to cloud/ansible/db-docker-compose.yml index 39e8978..727362a 100644 --- a/cloud/ansible/backend-docker-compose.yml +++ b/cloud/ansible/db-docker-compose.yml @@ -8,12 +8,14 @@ services: - "3306:3306" networks: - export_network + volumes: + - /home/ec2-user/mysql:/var/lib/mysql mysqld-exporter: image: prom/mysqld-exporter:main container_name: mysql_exporter ports: - - "9100:9104" + - "9090:9104" volumes: - /home/ec2-user/config.my-cnf:/cfg/config-my.cnf command: diff --git a/cloud/ansible/docker-install.yml b/cloud/ansible/docker-install.yml index 8bc94d6..d967e37 100644 --- a/cloud/ansible/docker-install.yml +++ b/cloud/ansible/docker-install.yml @@ -30,7 +30,6 @@ - name: Install docker-compose shell: cmd: curl -L "https://github.com/docker/compose/releases/download/v2.3.3/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - warn: false - name: Chmod docker-compose file: @@ -42,7 +41,7 @@ src: /usr/local/bin/docker-compose dest: /usr/bin/docker-compose state: link - + # 시간 바꾸기 - name: Set timezone to Asia/Seoul ansible.builtin.command: timedatectl set-timezone Asia/Seoul @@ -52,4 +51,23 @@ - name: Print the current timezone ansible.builtin.debug: - var: timedatectl_output.stdout \ No newline at end of file + var: timedatectl_output.stdout + + # Node Exporter 설치 + - name: Pull Node Exporter Docker image + ansible.builtin.docker_image: + name: quay.io/prometheus/node-exporter + tag: latest + source: pull + + - name: Run Node Exporter container + ansible.builtin.docker_container: + name: node_exporter + image: quay.io/prometheus/node-exporter:latest + state: started + restart_policy: always + network_mode: host + pid_mode: host + volumes: + - /:/host:ro,rslave + command: --path.rootfs=/host \ No newline at end of file diff --git a/cloud/ansible/work-instance.yml b/cloud/ansible/work-instance.yml index 2327696..0aa7421 100644 --- a/cloud/ansible/work-instance.yml +++ b/cloud/ansible/work-instance.yml @@ -1,5 +1,5 @@ - name: Deploy and run docker-compose on different hosts - hosts: backend, crawling + hosts: db, crawling become: yes tasks: @@ -14,9 +14,9 @@ src: "{{ item.src }}" dest: "{{ item.dest}}" with_items: - - { src: 'backend-docker-compose.yml', dest: '/home/ec2-user/docker-compose.yml'} + - { src: 'db-docker-compose.yml', dest: '/home/ec2-user/docker-compose.yml'} - { src: 'config.my-cnf', dest: '/home/ec2-user/config.my-cnf'} - when: "'backend' in group_names" + when: "'db' in group_names" - name: Copy docker-compose.yml and .env to crawling servers copy: diff --git a/cloud/terraform/environment/dev/front/.terraform.lock.hcl b/cloud/terraform/environment/dev/front/.terraform.lock.hcl deleted file mode 100644 index 0fa7c0c..0000000 --- a/cloud/terraform/environment/dev/front/.terraform.lock.hcl +++ /dev/null @@ -1,25 +0,0 @@ -# This file is maintained automatically by "terraform init". -# Manual edits may be lost in future updates. - -provider "registry.terraform.io/hashicorp/aws" { - version = "4.67.0" - constraints = "~> 4.16" - hashes = [ - "h1:5Zfo3GfRSWBaXs4TGQNOflr1XaYj6pRnVJLX5VAjFX4=", - "zh:0843017ecc24385f2b45f2c5fce79dc25b258e50d516877b3affee3bef34f060", - "zh:19876066cfa60de91834ec569a6448dab8c2518b8a71b5ca870b2444febddac6", - "zh:24995686b2ad88c1ffaa242e36eee791fc6070e6144f418048c4ce24d0ba5183", - "zh:4a002990b9f4d6d225d82cb2fb8805789ffef791999ee5d9cb1fef579aeff8f1", - "zh:559a2b5ace06b878c6de3ecf19b94fbae3512562f7a51e930674b16c2f606e29", - "zh:6a07da13b86b9753b95d4d8218f6dae874cf34699bca1470d6effbb4dee7f4b7", - "zh:768b3bfd126c3b77dc975c7c0e5db3207e4f9997cf41aa3385c63206242ba043", - "zh:7be5177e698d4b547083cc738b977742d70ed68487ce6f49ecd0c94dbf9d1362", - "zh:8b562a818915fb0d85959257095251a05c76f3467caa3ba95c583ba5fe043f9b", - "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9c385d03a958b54e2afd5279cd8c7cbdd2d6ca5c7d6a333e61092331f38af7cf", - "zh:b3ca45f2821a89af417787df8289cb4314b273d29555ad3b2a5ab98bb4816b3b", - "zh:da3c317f1db2469615ab40aa6baba63b5643bae7110ff855277a1fb9d8eb4f2c", - "zh:dc6430622a8dc5cdab359a8704aec81d3825ea1d305bbb3bbd032b1c6adfae0c", - "zh:fac0d2ddeadf9ec53da87922f666e1e73a603a611c57bcbc4b86ac2821619b1d", - ] -} diff --git a/cloud/terraform/environment/dev/front/s3CD.tf b/cloud/terraform/environment/dev/front/s3CD.tf index 4e2a514..1d70993 100644 --- a/cloud/terraform/environment/dev/front/s3CD.tf +++ b/cloud/terraform/environment/dev/front/s3CD.tf @@ -23,7 +23,7 @@ provider "aws" { } resource "aws_s3_bucket" "react_website" { - bucket = "ktb-movie-bucket" + bucket = "ktb-movie-bucket-${workspace}" } # S3 버킷의 공용 접근 차단 설정 diff --git a/cloud/terraform/environment/dev/main.tf b/cloud/terraform/environment/dev/main.tf index 3d46b95..c329f07 100644 --- a/cloud/terraform/environment/dev/main.tf +++ b/cloud/terraform/environment/dev/main.tf @@ -32,10 +32,10 @@ module "vpc" { module "backend" { source = "../../modules/instance" ami_id = "ami-0c2acfcb2ac4d02a0" - instance_type = "t3.small" + instance_type = "t3.large" ssh_key_name = "kakao-tech-bootcamp" subnet_id = module.vpc.private_subnet_ids[0] - security_groups_id = [aws_security_group.ssh.id, aws_security_group.mysql.id] + security_groups_id = [aws_security_group.ssh.id, aws_security_group.backend.id] workspace = "${terraform.workspace}-backend" } @@ -59,6 +59,86 @@ module "dev-host" { workspace = "Dev-Host" } +# ALB + +# ALB를 위한 보안 그룹 생성 +resource "aws_security_group" "alb" { + vpc_id = module.vpc.vpc_id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "moive-${terraform.workspace}-sg-alb" + } +} + +# ALB 생성 +resource "aws_lb" "alb" { + name = "${terraform.workspace}-alb" + internal = false + load_balancer_type = "application" + security_groups = [aws_security_group.alb.id] + subnets = module.vpc.public_subnet_ids + + tags = { + Name = "moive-${terraform.workspace}-alb" + } +} + +# ALB 리스너 생성 +resource "aws_lb_listener" "http" { + load_balancer_arn = aws_lb.alb.arn + port = "80" + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.backend.arn + } +} + +# 타겟 그룹 생성 +resource "aws_lb_target_group" "backend" { + name = "${terraform.workspace}-tg-backend" + port = 8080 + protocol = "HTTP" + vpc_id = module.vpc.vpc_id + + health_check { + path = "/" + protocol = "HTTP" + matcher = "200-299" + interval = 30 + timeout = 5 + healthy_threshold = 3 + unhealthy_threshold = 3 + } + + tags = { + Name = "moive-${terraform.workspace}-tg-backend" + } +} + +# 백엔드 인스턴스 등록 +resource "aws_lb_target_group_attachment" "backend" { + target_group_arn = aws_lb_target_group.backend.arn + target_id = module.backend.instance_id + port = 8080 +} + + # 보안 그룹 resource "aws_security_group" "ssh" { vpc_id = module.vpc.vpc_id @@ -88,7 +168,7 @@ resource "aws_security_group" "ssh" { } } -resource "aws_security_group" "mysql" { +resource "aws_security_group" "backend" { vpc_id = module.vpc.vpc_id ingress { @@ -98,6 +178,7 @@ resource "aws_security_group" "mysql" { cidr_blocks = ["0.0.0.0/0"] } + #mysql ingress { from_port = 3306 to_port = 3306 @@ -105,6 +186,14 @@ resource "aws_security_group" "mysql" { cidr_blocks = ["0.0.0.0/0"] } + #alb + ingress { + from_port = 80 + to_port = 8080 + protocol = "tcp" + security_groups = [aws_security_group.alb.id] + } + egress { from_port = 0 to_port = 0 diff --git a/cloud/terraform/environment/prod/main.tf b/cloud/terraform/environment/prod/main.tf index 8aadae1..4106e66 100644 --- a/cloud/terraform/environment/prod/main.tf +++ b/cloud/terraform/environment/prod/main.tf @@ -1,6 +1,5 @@ ####기본적인 terraform setup을 위한 tf 파일 # S3 버켓과 DynamoDB를 생성한다 - terraform { required_providers { aws = { @@ -20,118 +19,150 @@ terraform { required_version = ">= 1.2.0" } + module "vpc" { - source = "../../modules/vpc" - vpc_cidr = "192.168.0.0/16" - public_subnet_cidr = "192.168.1.0/24" - private_subnet_cidr = "192.168.2.0/24" - environment = terraform.workspace - public_subnet_count = 2 - private_subnet_count = 2 + source = "../../modules/vpc_v2" } - -## 인스턴스 - -module "front" { - source = "../../modules/instance" - ami_id = "ami-0c2acfcb2ac4d02a0" - instance_type = "t2.micro" - ssh_key_name = "kakao-tech-bootcamp" - subnet_id = module.vpc.public_subnet_ids[0] - security_groups_id = [aws_security_group.ssh.id] - workspace = "${terraform.workspace}-front" +module "security_groups" { + source = "../../modules/security_groups" + vpc_id = module.vpc.vpc_id } -module "name" { - source = "../../modules/instance" - ami_id = "ami-0c2acfcb2ac4d02a0" - instance_type = "t2.small" - ssh_key_name = "kakao-tech-bootcamp" - subnet_id = module.vpc.private_subnet_ids[0] - security_groups_id = [aws_security_group.ssh.id] - workspace = "${terraform.workspace}-backend" +module "dev-host" { + source = "../../modules/ec2" + subnets = [module.vpc.public_subnets[0]] + instance_type = "t3.small" + ami_image = "ami-05d768df76a2b8bd8" + private_ips = ["192.168.1.23"] + instance_count = 1 + security_groups = [module.security_groups.movie_default_sg_id] + tags = "dev-host" } module "crawling" { - source = "../../modules/instance" - ami_id = "ami-0c2acfcb2ac4d02a0" - instance_type = "t2.xlarge" - ssh_key_name = "kakao-tech-bootcamp" - subnet_id = module.vpc.public_subnet_ids[1] - security_groups_id = [aws_security_group.ssh.id] - workspace = "${terraform.workspace}-crawling" + source = "../../modules/ec2" + subnets = [module.vpc.public_subnets[1]] + instance_type = "c6i.large" + private_ips = ["192.168.2.26"] + instance_count = 1 + security_groups = [module.security_groups.movie_default_sg_id] + tags = "crawling" +} + +module "backend" { + source = "../../modules/ec2" + subnets = module.vpc.private_subnets + instance_type = "r7i.large" + private_ips = ["192.168.3.233", "192.168.4.134"] + instance_count = 2 + security_groups = [module.security_groups.movie_default_sg_id, module.security_groups.movie_backend_sg_id] + tags = "backend" } module "db" { - source = "../../modules/instance" - ami_id = "ami-0c2acfcb2ac4d02a0" - instance_type = "t2.small" - ssh_key_name = "kakao-tech-bootcamp" - subnet_id = module.vpc.private_subnet_ids[1] - security_groups_id = [aws_security_group.mysql.id,aws_security_group.ssh.id] - workspace = "${terraform.workspace}-db" + source = "../../modules/ec2" + subnets = module.vpc.db_subnets + instance_type = "t3.small" + private_ips = ["192.168.5.116"] + instance_count = 1 + security_groups = [module.security_groups.movie_default_sg_id, module.security_groups.movie_db_sg_id] + tags = "db" } -module "dev-host" { - source = "../../modules/instance" - ami_id = "ami-0c2acfcb2ac4d02a0" - instance_type = "t2.xlarge" - ssh_key_name = "kakao-tech-bootcamp" - subnet_id = module.vpc.public_subnet_ids[1] - security_groups_id = [aws_security_group.ssh.id] - workspace = "${terraform.workspace}-db" +module "alb" { + source = "../../modules/alb" + target_instances_ids = module.backend.instance_ids + vpc_id = module.vpc.vpc_id + alb-subnets_ids = module.vpc.public_subnets + security_groups_ids = [module.security_groups.movie_alb_sg_id] } +# Front 배포 +resource "aws_s3_bucket" "react_website" { + bucket = "ktb-movie-bucket-${terraform.workspace}" +} +# S3 버킷의 공용 접근 차단 설정 +resource "aws_s3_bucket_public_access_block" "react_website_public_access_block" { + bucket = aws_s3_bucket.react_website.id -# 보안 그룹 -resource "aws_security_group" "ssh" { - vpc_id = module.vpc.vpc_id - ingress { - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - } + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } +resource "aws_cloudfront_origin_access_control" "s3_oac" { + name = "ktb-movie-oac" + description = "OAC for ktb-movie S3 bucket" + origin_access_control_origin_type = "s3" - tags = { - Name = "moive-${terraform.workspace}-sg-ssh" - } + signing_behavior = "always" + signing_protocol = "sigv4" } -resource "aws_security_group" "mysql" { - vpc_id = module.vpc.vpc_id +resource "aws_cloudfront_distribution" "react_website_distribution" { + origin { + domain_name = aws_s3_bucket.react_website.bucket_regional_domain_name + origin_id = aws_s3_bucket.react_website.id + origin_access_control_id = aws_cloudfront_origin_access_control.s3_oac.id + + } + + enabled = true + is_ipv6_enabled = true + default_root_object = "index.html" - ingress { - from_port = 22 - to_port = 22 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] + default_cache_behavior { + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + target_origin_id = aws_s3_bucket.react_website.id + + forwarded_values { + query_string = false + cookies { + forward = "none" + } + } + + viewer_protocol_policy = "redirect-to-https" } - - ingress { - from_port = 3306 - to_port = 3306 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] + + viewer_certificate { + cloudfront_default_certificate = true } - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] + restrictions { + geo_restriction { + restriction_type = "none" + } } tags = { - Name = "moive-${terraform.workspace}-sg-mysql" + Name = "React Website Distribution" } +} + +resource "aws_s3_bucket_policy" "react_website_policy" { + bucket = aws_s3_bucket.react_website.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "cloudfront.amazonaws.com" + } + Action = "s3:GetObject" + Resource = "${aws_s3_bucket.react_website.arn}/*" + Condition = { + StringEquals = { + "AWS:SourceArn" = aws_cloudfront_distribution.react_website_distribution.arn + } + } + } + ] + }) } \ No newline at end of file diff --git a/cloud/terraform/environment/prod/variables.tf b/cloud/terraform/environment/prod/variables.tf index 1729329..c72a4ca 100644 --- a/cloud/terraform/environment/prod/variables.tf +++ b/cloud/terraform/environment/prod/variables.tf @@ -1,11 +1,11 @@ variable "aws_region" { description = "AWS 리전" - type = string - default = "ap-northeast-2" + type = string + default = "ap-northeast-2" } variable "name" { description = "프로젝트 이름" - type = string - default = "movie-chat" + type = string + default = "movie-chat" } \ No newline at end of file diff --git a/cloud/terraform/modules/alb/alb.tf b/cloud/terraform/modules/alb/alb.tf new file mode 100644 index 0000000..cfa9e6a --- /dev/null +++ b/cloud/terraform/modules/alb/alb.tf @@ -0,0 +1,65 @@ +variable "alb-subnets_ids" { + description = "alb에 등록할 subnet id" + type = list(string) +} + +variable "security_groups_ids" { + type = list(string) +} + +variable "vpc_id" { + type = string +} + +variable "target_instances_ids" { + type = list(string) +} + +# ALB 생성 +resource "aws_lb" "backend_alb" { + name = "backend-alb" + internal = false + load_balancer_type = "application" + security_groups = var.security_groups_ids + subnets = var.alb-subnets_ids + + tags = { + Name = "ktb-movie-backend-alb" + } +} + +resource "aws_lb_target_group" "backend_tg" { + name = "backend-tg" + port = 8080 + protocol = "HTTP" + vpc_id = var.vpc_id + + health_check { + path = "/health" + protocol = "HTTP" + matcher = "200-299" + interval = 30 + timeout = 5 + healthy_threshold = 3 + unhealthy_threshold = 3 + } +} + +resource "aws_lb_listener" "backend_listener" { + load_balancer_arn = aws_lb.backend_alb.arn + port = 80 + protocol = "HTTP" + + default_action { + type = "forward" + target_group_arn = aws_lb_target_group.backend_tg.arn + } +} + +# 인스턴스 등록 +resource "aws_lb_target_group_attachment" "backend" { + count = length(var.target_instances_ids) + target_group_arn = aws_lb_target_group.backend_tg.arn + target_id = var.target_instances_ids[count.index] + port = 8080 +} \ No newline at end of file diff --git a/cloud/terraform/modules/ec2/ec2.tf b/cloud/terraform/modules/ec2/ec2.tf new file mode 100644 index 0000000..9c66365 --- /dev/null +++ b/cloud/terraform/modules/ec2/ec2.tf @@ -0,0 +1,13 @@ +resource "aws_instance" "instances" { + count = var.instance_count + ami = var.ami_image + instance_type = var.instance_type + subnet_id = element(var.subnets, count.index) + key_name = "kakao-tech-bootcamp" + security_groups = var.security_groups + private_ip = var.private_ips[count.index] + + tags = { + Name = "ktb-movie-${var.tags}-instance-${count.index}" + } +} diff --git a/cloud/terraform/modules/ec2/outputs.tf b/cloud/terraform/modules/ec2/outputs.tf new file mode 100644 index 0000000..30ea4ae --- /dev/null +++ b/cloud/terraform/modules/ec2/outputs.tf @@ -0,0 +1,4 @@ +output "instance_ids" { + description = "The IDs of all the instances created" + value = aws_instance.instances[*].id +} \ No newline at end of file diff --git a/cloud/terraform/modules/ec2/variable.tf b/cloud/terraform/modules/ec2/variable.tf new file mode 100644 index 0000000..3788ea3 --- /dev/null +++ b/cloud/terraform/modules/ec2/variable.tf @@ -0,0 +1,35 @@ +variable "subnets" { + description = "서브넷 주소" + type = list(string) + nullable = true +} +variable "tags" { + type = string +} + +variable "instance_count" { + description = "생성할 인스턴스 개수" + type = number +} + +variable "instance_type" { + description = "인스턴스 타입" + type = string + default = "t2.micro" +} + +variable "security_groups" { + description = "Security Groups" + type = list(string) +} + +variable "private_ips" { + description = "할당한 private subnet" + type = list(string) +} + +variable "ami_image" { + description = "EC2 이미지" + type = string + default = "ami-0c2acfcb2ac4d02a0" +} \ No newline at end of file diff --git a/cloud/terraform/modules/security_groups/main.tf b/cloud/terraform/modules/security_groups/main.tf new file mode 100644 index 0000000..f6ef88d --- /dev/null +++ b/cloud/terraform/modules/security_groups/main.tf @@ -0,0 +1,92 @@ +variable "vpc_id" { + type = string +} + +resource "aws_security_group" "movie-default" { + vpc_id = var.vpc_id + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 9100 + to_port = 9100 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 9090 + to_port = 9090 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "ktb-movie-default-sg" + } +} + +resource "aws_security_group" "movie-db" { + vpc_id = var.vpc_id + + ingress { + from_port = 3306 + to_port = 3306 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "ktb-movie-db-sg" + } +} + +resource "aws_security_group" "movie-alb" { + vpc_id = var.vpc_id + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "ktb-movie-alb-sg" + } +} + + +resource "aws_security_group" "movie-backend" { + vpc_id = var.vpc_id + + ingress { + from_port = 8080 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "ktb-movie-backend-sg" + } +} \ No newline at end of file diff --git a/cloud/terraform/modules/security_groups/output.tf b/cloud/terraform/modules/security_groups/output.tf new file mode 100644 index 0000000..71466d3 --- /dev/null +++ b/cloud/terraform/modules/security_groups/output.tf @@ -0,0 +1,23 @@ +# Output for movie-default security group +output "movie_default_sg_id" { + description = "The ID of the movie-default security group" + value = aws_security_group.movie-default.id +} + +# Output for movie-db security group +output "movie_db_sg_id" { + description = "The ID of the movie-db security group" + value = aws_security_group.movie-db.id +} + +# Output for movie-alb security group +output "movie_alb_sg_id" { + description = "The ID of the movie-alb security group" + value = aws_security_group.movie-alb.id +} + +# Output for movie-backend security group +output "movie_backend_sg_id" { + description = "The ID of the movie-backend security group" + value = aws_security_group.movie-backend.id +} \ No newline at end of file diff --git a/cloud/terraform/modules/vpc/variables.tf b/cloud/terraform/modules/vpc/variables.tf index 791076c..85d4e2f 100644 --- a/cloud/terraform/modules/vpc/variables.tf +++ b/cloud/terraform/modules/vpc/variables.tf @@ -8,7 +8,6 @@ variable "environment" { type = string } - variable "public_subnet_count" { description = "The number of public subnets" type = number @@ -22,5 +21,5 @@ variable "private_subnet_count" { variable "availability_zone" { description = "AZ" type = list(string) - default = ["ap-northeast-2a"] + default = ["ap-northeast-2a","ap-northeast-2c"] } \ No newline at end of file diff --git a/cloud/terraform/modules/vpc/vpc.tf b/cloud/terraform/modules/vpc/vpc.tf index bbf7ba4..d080811 100644 --- a/cloud/terraform/modules/vpc/vpc.tf +++ b/cloud/terraform/modules/vpc/vpc.tf @@ -12,7 +12,7 @@ resource "aws_subnet" "public" { vpc_id = aws_vpc.this.id cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index) map_public_ip_on_launch = true - availability_zone = var.availability_zone[0] + availability_zone = var.availability_zone[count.index] tags = { Name = "movie-chat-${var.environment}-public-subnet-${count.index + 1}" @@ -24,7 +24,7 @@ resource "aws_subnet" "private" { vpc_id = aws_vpc.this.id cidr_block = cidrsubnet(var.vpc_cidr, 8, var.public_subnet_count + count.index) - availability_zone = var.availability_zone[0] + availability_zone = var.availability_zone[count.index % length(var.availability_zone)] tags = { Name = "movie-chat-${var.environment}-private-subnet-${count.index + 1}" @@ -86,6 +86,6 @@ resource "aws_eip" "movie-eip" { # Nat Gateway resource "aws_nat_gateway" "movie-nat-gw" { allocation_id = aws_eip.movie-eip.id - subnet_id = aws_subnet.public[1].id + subnet_id = aws_subnet.public[0].id connectivity_type = "public" } \ No newline at end of file diff --git a/cloud/terraform/modules/vpc_v2/output.tf b/cloud/terraform/modules/vpc_v2/output.tf new file mode 100644 index 0000000..93153f6 --- /dev/null +++ b/cloud/terraform/modules/vpc_v2/output.tf @@ -0,0 +1,30 @@ +output "vpc_id" { + value = aws_vpc.main_vpc.id +} + +# Public 서브넷 ID 출력 +output "public_subnets" { + description = "Public subnet IDs" + value = [ + for key, subnet in aws_subnet.subnets : subnet.id + if substr(key, 0, 6) == "public" + ] +} + +# Private 서브넷 ID 출력 +output "private_subnets" { + description = "Private subnet IDs" + value = [ + for key, subnet in aws_subnet.subnets : subnet.id + if substr(key, 0, 7) == "private" + ] +} + +# DB 서브넷 ID 출력 +output "db_subnets" { + description = "DB subnet IDs" + value = [ + for key, subnet in aws_subnet.subnets : subnet.id + if substr(key, 0, 2) == "db" + ] +} \ No newline at end of file diff --git a/cloud/terraform/modules/vpc_v2/vpc.tf b/cloud/terraform/modules/vpc_v2/vpc.tf new file mode 100644 index 0000000..03de66e --- /dev/null +++ b/cloud/terraform/modules/vpc_v2/vpc.tf @@ -0,0 +1,125 @@ + +# 공통 태그 정의 +locals { + common_tags = { + Project = "ktb-movie" + Owner = "Cloud-Team-Bryan" + CreatedBy = "Terraform" + CreatedDate = formatdate("YYYY-MM-DD", timestamp()) + } + + # 서브넷 정의 + subnets = { + public = { + cidr_blocks = ["192.168.1.0/24", "192.168.2.0/24"] + azs = ["ap-northeast-2a", "ap-northeast-2c"] + } + private = { + cidr_blocks = ["192.168.3.0/24", "192.168.4.0/24"] + azs = ["ap-northeast-2a", "ap-northeast-2c"] + } + db = { + cidr_blocks = ["192.168.5.0/24"] + azs = ["ap-northeast-2a"] + } + } + + subnet_config = { + public = local.subnets.public + private = local.subnets.private + db = local.subnets.db + } + + subnet_map = merge([ + for subnet_type, config in local.subnet_config : { + for idx, cidr in config.cidr_blocks : + "${subnet_type}-${idx}" => { + cidr_block = cidr + availability_zone = config.azs[idx] + public = subnet_type == "public" + } + } + ]...) +} + +# VPC 생성 +resource "aws_vpc" "main_vpc" { + cidr_block = "192.168.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = merge(local.common_tags, { + Name = "ktb-movie-main-vpc" + }) +} + +# 인터넷 게이트웨이 +resource "aws_internet_gateway" "igw" { + vpc_id = aws_vpc.main_vpc.id + + tags = merge(local.common_tags, { + Name = "ktb-movie-main-igw" + }) +} + +resource "aws_subnet" "subnets" { + for_each = local.subnet_map + + vpc_id = aws_vpc.main_vpc.id + cidr_block = each.value.cidr_block + availability_zone = each.value.availability_zone + map_public_ip_on_launch = each.value.public + + tags = merge(local.common_tags, { + Name = "ktb-movie-${each.key}-subnet" + }) +} + +# NAT Gateway +resource "aws_eip" "nat_eip" { + vpc = true + tags = merge(local.common_tags, { + Name = "ktb-movie-nat-eip" + }) +} + +resource "aws_nat_gateway" "nat_gateway" { + allocation_id = aws_eip.nat_eip.id + subnet_id = aws_subnet.subnets["public-0"].id + + tags = merge(local.common_tags, { + Name = "ktb-movie-nat-gateway" + }) +} + +# 라우팅 테이블 +resource "aws_route_table" "route_tables" { + for_each = toset(["public", "private"]) + + vpc_id = aws_vpc.main_vpc.id + + tags = merge(local.common_tags, { + Name = "ktb-movie-${each.key}-route-table" + }) +} + +# 라우팅 규칙 +resource "aws_route" "public_internet_gateway" { + route_table_id = aws_route_table.route_tables["public"].id + destination_cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.igw.id +} + +resource "aws_route" "private_nat_gateway" { + route_table_id = aws_route_table.route_tables["private"].id + destination_cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.nat_gateway.id +} + +# 서브넷 연결 +resource "aws_route_table_association" "subnet_routes" { + for_each = aws_subnet.subnets + + subnet_id = each.value.id + route_table_id = aws_route_table.route_tables[startswith(each.key, "public") ? "public" : "private"].id +} \ No newline at end of file diff --git a/crawling/main.py b/crawling/main.py index 8b5ddb7..528391b 100644 --- a/crawling/main.py +++ b/crawling/main.py @@ -24,25 +24,25 @@ def job(): print("inserted into database", flush=True) -def job_for7days(): - data_list = [] - divisions = [i for i in range(1, 18)] +# def job_for7days(): +# data_list = [] +# divisions = [i for i in range(1, 18)] - print("start crawling") +# print("start crawling") - for i in divisions: - data_list.extend(process_division.process_division(i, initial=7)) +# for i in divisions: +# data_list.extend(process_division.process_division(i, initial=7)) - print("final: ", len(data_list)) - database.insert_db.insert_data(data_list) - print("inserted into database(for 7 days)", flush=True) +# print("final: ", len(data_list)) +# database.insert_db.insert_data(data_list) +# print("inserted into database(for 7 days)", flush=True) if __name__ == '__main__': - # 최초 1회 실행 - job_for7days() + # # 최초 1회 실행 + # job_for7days() - # 매일 오전 9시마다 실행 + # # 매일 오전 9시마다 실행 schedule.every().day.at("09:00").do(job) while True: diff --git a/docs/img/1.png b/docs/img/1.png new file mode 100644 index 0000000..2815adf Binary files /dev/null and b/docs/img/1.png differ diff --git a/docs/img/2.png b/docs/img/2.png new file mode 100644 index 0000000..47d9ae3 Binary files /dev/null and b/docs/img/2.png differ diff --git a/docs/img/3.png b/docs/img/3.png new file mode 100644 index 0000000..68e6be7 Binary files /dev/null and b/docs/img/3.png differ diff --git a/docs/img/4.png b/docs/img/4.png new file mode 100644 index 0000000..514367d Binary files /dev/null and b/docs/img/4.png differ diff --git a/docs/img/Movie-chatbot-architecture.png b/docs/img/Movie-chatbot-architecture.png new file mode 100644 index 0000000..5453c77 Binary files /dev/null and b/docs/img/Movie-chatbot-architecture.png differ diff --git a/docs/img/c6i.large.png b/docs/img/c6i.large.png new file mode 100644 index 0000000..c1ab836 Binary files /dev/null and b/docs/img/c6i.large.png differ diff --git a/docs/img/c6i.xlarge.png b/docs/img/c6i.xlarge.png new file mode 100644 index 0000000..546e0ce Binary files /dev/null and b/docs/img/c6i.xlarge.png differ diff --git a/docs/img/erd.png b/docs/img/erd.png new file mode 100644 index 0000000..5b3f0a7 Binary files /dev/null and b/docs/img/erd.png differ diff --git a/docs/img/exception.png b/docs/img/exception.png new file mode 100644 index 0000000..8a9fc7a Binary files /dev/null and b/docs/img/exception.png differ diff --git a/docs/img/image (2).png b/docs/img/image (2).png new file mode 100644 index 0000000..b82146c Binary files /dev/null and b/docs/img/image (2).png differ diff --git a/docs/img/lighthouse.png b/docs/img/lighthouse.png new file mode 100644 index 0000000..d6ede92 Binary files /dev/null and b/docs/img/lighthouse.png differ diff --git "a/docs/img/screen 2024-09-10 \354\230\244\355\233\204 2.47.28.png" "b/docs/img/screen 2024-09-10 \354\230\244\355\233\204 2.47.28.png" new file mode 100644 index 0000000..0a0a7e6 Binary files /dev/null and "b/docs/img/screen 2024-09-10 \354\230\244\355\233\204 2.47.28.png" differ diff --git a/docs/img/t2.large.png b/docs/img/t2.large.png new file mode 100644 index 0000000..0b5ca08 Binary files /dev/null and b/docs/img/t2.large.png differ diff --git a/docs/img/t2.xlarge.png b/docs/img/t2.xlarge.png new file mode 100644 index 0000000..5f51a47 Binary files /dev/null and b/docs/img/t2.xlarge.png differ diff --git a/docs/img/test.png b/docs/img/test.png new file mode 100644 index 0000000..f341816 Binary files /dev/null and b/docs/img/test.png differ diff --git a/docs/img/validate.png b/docs/img/validate.png new file mode 100644 index 0000000..4f41e2c Binary files /dev/null and b/docs/img/validate.png differ diff --git "a/docs/\352\262\260\352\263\274_\354\235\270\352\263\265\354\247\200\353\212\245.md" "b/docs/\352\262\260\352\263\274_\354\235\270\352\263\265\354\247\200\353\212\245.md" new file mode 100644 index 0000000..5aba83b --- /dev/null +++ "b/docs/\352\262\260\352\263\274_\354\235\270\352\263\265\354\247\200\353\212\245.md" @@ -0,0 +1,31 @@ +## 1. RAG 기술 활용 + +⇒ 영화관 추천 전문 고객지원 챗봇으로 커스텀 + +- LLM ChatGPT api를 활용한 응답 생성 + - 사용자 질문에서 NER을 이용하여 Entity 추출 + - 9월 11일에 3시 강남에서 에일리언 보고싶어. + - {date}: 2024-09-11 {time}: 15:00 {region}: 강남{movieName}:에일리언 + - koBERT,kiwi를 이용한 RAG구축 후 + - FAISS를 이용한 Semantic Search, 및 Levenshtein distance 기반 검색 기능 개발 + - {movieName}:에일리언 -> 에일리언:로물루스 + - LLM 응답 정형화 +- LLM ChatGPT api를 활용한 응답 생성 + +ex) 오늘 판교에서 에이리언 보고 싶어 + +## 2. ChatGPT API를 활용한 응답 생성 + +1. 사용자의 질문에서 추출한 정보가 올바른지 사용자에게 다시 확인하는 기능 +- ex) ‘2024-09-09 18:00에 성남시 분당구에서 에이리언:로물루스를 보고 싶은 게 맞으신가요? +- {date} {time}에 {region}에서 {movieName}을 보고 싶은 게 맞으신가요? + +1. 사용자의 다양한 질문 형식을 자동으로 인식, 일관된 형식으로 변환해 답변하는 기능 +- 시간/날짜 형식 전처리(YYYY-MM-DD, HH:MM) +- 지역명 데이터 추가해 영화관 조회를 용이하게 함 ex) ‘판교’ → ‘경기도 성남시 분당구’ + +1. 사용자가 선택한 날짜, 장소, 영화명을 기반으로 최적의 영화관, 영화스케줄을 추천해주는 답변 생성 +- 사용자 맞춤 영화관 추천 + - 사용자가 입력한 위치 근처에서 교통 접근성이 좋은 영화관, 사용자가 보고 싶은 영화를 많이 상영하는 영화관을 추천 +- 영화관 주소 데이터를 추가: 영화관 근처 교통정보 제공 + - 추천된 영화관 근처 지하철역에서 이동경로(몇 번 출구에서 도보로 몇 분), 지하철역이 없을 경우 버스 정류장 정보 제공 \ No newline at end of file diff --git "a/docs/\352\262\260\352\263\274_\355\201\264\353\235\274\354\232\260\353\223\234.md" "b/docs/\352\262\260\352\263\274_\355\201\264\353\235\274\354\232\260\353\223\234.md" new file mode 100644 index 0000000..c2dc8b9 --- /dev/null +++ "b/docs/\352\262\260\352\263\274_\355\201\264\353\235\274\354\232\260\353\223\234.md" @@ -0,0 +1,245 @@ +# 프로젝트 아키텍처 + +![Movie-chatbot-architecture.png](./img/Movie-chatbot-architecture.png) + +# AWS 아키텍쳐 관련 + +## 인프라 구성 방법 + +Terraform을 활용해 직접 인스턴스들을 제어하지 않고, 코드로 생성 및 관리함 + +- Terraform 코드를 모듈화 시켜 보기 한눈에 알아 보기 쉽게 구성함 + + ![screen 2024-09-10 오후 2.47.28.png](/img%2Fscreen%202024-09-10%20%EC%98%A4%ED%9B%84%202.47.28.png) + +- 개발 환경과 프로덕션 환경을 나누어서 불필요한 리소스 소비를 줄였음 + +## 개발환경 세팅 + +모니터링 및 개발 인스턴스를 별도로 생성하였음 + +Ansible를 활용하여 여러 인스턴스의 환경 설정을 효율적으로 하였음 + +- Ansible을 활용하여 Docker, Docker-compose, Node_exporter를 세팅 하였다. + +## 모니터링 + +각 인스턴스에 Node_exporter를 실행해 CPU, 메모리 등 주요 리소스를 모니터링 함 + +인스턴스의 역할에 맞게 cAdvisor와 mysqld_exporter를 추가로 실행해, 역할에 맞는 리소스를 추가로 모니터링 함 + +- CI/CD + +github actions를 이용한 배포 과정 + +1. dev version workflows 작성 + + ```jsx + name: Deploy MovieChatBot to AWS + + on: + push: + branches: + - develop + + env: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} + AWS_REGION: ap-northeast-2 + + jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + # 2. JDK 설치 + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + java-version: '21' + distribution: 'adopt' + + # 3. Gradle 빌드 + - name: Build with Gradle + run: | + cd backend # gradlew 파일이 있는 디렉토리로 이동 + ./gradlew build + + - name: Login to Docker Hub + run: echo $DOCKER_HUB_PASSWORD | docker login -u $DOCKER_HUB_USERNAME --password-stdin + + # Frontend 빌드 및 S3 배포 + - name: Build and deploy Frontend + run: | + cd frontend + npm ci + echo "REACT_APP_ENDPOINT=${{ secrets.REACT_APP_ENDPOINT }}" >> .env + CI=false npm run build + aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} --delete + aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*" + + # Backend, AI 이미지 빌드 및 푸시 + - name: Build and push Docker images + run: | + cd backend + docker build -t $DOCKER_HUB_USERNAME/backend:latest . + docker push $DOCKER_HUB_USERNAME/backend:latest + + cd ../ai + docker build --build-arg OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" -t $DOCKER_HUB_USERNAME/ai:latest . + docker push $DOCKER_HUB_USERNAME/ai:latest + + # Backend 서비스 배포 (AWS Systems Manager 사용) + - name: Deploy Backend services + run: | + aws ssm send-command \ + --instance-ids ${{ secrets.BACKEND_EC2_HOST }} \ + --document-name "AWS-RunShellScript" \ + --parameters '{ + "commands": [ + "sudo docker stop backend ai || true", + "sudo docker rm backend ai || true", + "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest", + "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest", + "sudo docker run -d --name backend --network ec2-user_export_network -p 8080:8080 -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/moviedatabase?serverTimezone=Asia/Seoul -e SPRING_DATASOURCE_USERNAME=root -e SPRING_DATASOURCE_PASSWORD=qlalfqjsgh486 -e AI_SERVICE_URL=http://ai:8000/api/v1 ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker run -d --name ai --network ec2-user_export_network -p 8000:8000 -e PROJECT_NAME=ParseAI -e DATABASE_URL=mysql+aiomysql://root:qlalfqjsgh486@mysql:3306/moviedatabase ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest" + ] + }' + + - name: Cleanup + if: always() + run: | + docker logout + rm -f /tmp/ec2_key + ``` + +2. git repositroy push → docker build and push → ec2 인스턴스에서 docker pull → docker run 으로 배포 완료 +3. 배포 환경(main) workflows 추가 작성 + +```jsx +name: Deploy MovieChatBot to Main AWS + +on: + push: + branches: + - main + +env: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} + BACKEND_EC2_INSTANCE: ${{ secrets.BACKEND_EC2_INSTANCE }} + BACKEND_EC2_INSTANCE2: ${{ secrets.BACKEND_EC2_INSTANCE2 }} + AWS_REGION: ap-northeast-2 + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: ${{ env.AWS_REGION }} + + - name: Set up JDK 21 + uses: actions/setup-java@v2 + with: + java-version: '21' + distribution: 'adopt' + + - name: Build with Gradle + run: | + cd backend + ./gradlew build + + - name: Login to Docker Hub + run: echo $DOCKER_HUB_PASSWORD | docker login -u $DOCKER_HUB_USERNAME --password-stdin + + - name: Build and deploy Frontend + run: | + cd frontend + npm ci + echo "REACT_APP_ENDPOINT=${{ secrets.REACT_APP_ENDPOINT }}" >> .env + CI=false npm run build + aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} --delete + aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*" + + - name: Build and push Docker images + run: | + cd backend + docker build -t $DOCKER_HUB_USERNAME/backend:latest . + docker push $DOCKER_HUB_USERNAME/backend:latest + + cd ../ai + docker build --build-arg OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" -t $DOCKER_HUB_USERNAME/ai:latest . + docker push $DOCKER_HUB_USERNAME/ai:latest + + - name: Deploy Backend services + run: | + aws ssm send-command \ + --instance-ids ${{ secrets.BACKEND_EC2_INSTANCE }} \ + --document-name "AWS-RunShellScript" \ + --parameters '{ + "commands": [ + "sudo docker stop backend ai || true", + "sudo docker rm backend ai || true", + "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest", + "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest", + "sudo docker run -d --name backend --network ec2-user_export_network -p 8080:8080 -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase?serverTimezone=Asia/Seoul -e SPRING_DATASOURCE_USERNAME=root -e SPRING_DATASOURCE_PASSWORD=qlalfqjsgh486 -e AI_SERVICE_URL=http://ai:8000/api/v1 ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker run -d --name ai --network ec2-user_export_network -p 8000:8000 -e PROJECT_NAME=ParseAI -e DATABASE_URL=mysql+aiomysql://root:qlalfqjsgh486@${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest" + ] + }' + env: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DATABASE_EC2_PRIVATE_IP: ${{ secrets.DATABASE_EC2_PRIVATE_IP }} + + - name: Deploy Backend 2 services + run: | + aws ssm send-command \ + --instance-ids ${{ secrets.BACKEND_EC2_INSTANCE2 }} \ + --document-name "AWS-RunShellScript" \ + --parameters '{ + "commands": [ + "sudo docker stop backend ai || true", + "sudo docker rm backend ai || true", + "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest", + "sudo docker image rm ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest", + "sudo docker run -d --name backend --network ec2-user_export_network -p 8080:8080 -e SPRING_DATASOURCE_URL=jdbc:mysql://${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase?serverTimezone=Asia/Seoul -e SPRING_DATASOURCE_USERNAME=root -e SPRING_DATASOURCE_PASSWORD=qlalfqjsgh486 -e AI_SERVICE_URL=http://ai:8000/api/v1 ${{ secrets.DOCKER_HUB_USERNAME }}/backend:latest", + "sudo docker run -d --name ai --network ec2-user_export_network -p 8000:8000 -e PROJECT_NAME=ParseAI -e DATABASE_URL=mysql+aiomysql://root:qlalfqjsgh486@${{ secrets.DATABASE_EC2_PRIVATE_IP }}:3306/moviedatabase ${{ secrets.DOCKER_HUB_USERNAME }}/ai:latest" + ] + }' + env: + DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }} + DATABASE_EC2_PRIVATE_IP: ${{ secrets.DATABASE_EC2_PRIVATE_IP }} + + - name: Cleanup + if: always() + run: | + docker logout +``` + +- 컨테이너 배포 후 연결 및 배포환경 api 연결 + + ALB를 사용하여 프라이빗 서브넷에 배포한 backend api를 연결하는 것으로 진행하였음. + + - health check를 이용한 ALB 연결 확인 유무 +![image (2).png](/img%2Fimage%20%282%29.png) \ No newline at end of file diff --git "a/docs/\352\262\260\352\263\274_\355\222\200\354\212\244\355\203\235.md" "b/docs/\352\262\260\352\263\274_\355\222\200\354\212\244\355\203\235.md" new file mode 100644 index 0000000..727ff1d --- /dev/null +++ "b/docs/\352\262\260\352\263\274_\355\222\200\354\212\244\355\203\235.md" @@ -0,0 +1,54 @@ +- 크롤링 + - **Kobis에서 제공하는 지역별 및 날짜별 상영 스케줄 정보를 크롤링** + - 7일치의 정보를 크롤링 + 새롭게 올라온 날짜의 상영 스케줄 크롤링 + - 선택된 조건에 맞는 영화 상영 스케줄 정보 수집 + - 광역 선택 -> 기초 선택 -> 영화관 선택 -> 날짜 선택 순서로 크롤링 절차를 진행하여 사용자가 3~4회의 버튼 클릭으로 영화 상영 스케줄 정보를 수집하도록 구현 + - **DB 설계 및 데이터 저장** + - 크롤링한 상영 스케줄 데이터를 효율적으로 조회할 수 있도록 DB 테이블 설계 + - 상영 정보 조회 속도와 데이터 정합성을 고려하여 영화관, 영화 테이블의 정보를 기준으로 상영정보를 조회하도록 함 + ![erd.png](./img/erd.png) + - **크롤링 속도 개선을 위해 멀티프로세싱 적용** + - 여러 영화관의 상영 정보를 동시에 수집하여 시간 최적화 + - 시간 최적화 +- 프론트엔드 + - React를 사용한 사용자 인터페이스 구축 + - React 라이브러리를 사용한 컴포넌트 기반 UI 설계 및 구축 + - UI 모듈화를 통해 각 컴포넌트의 독립적 개발 및 유지보수 가능성 향상과 코드 재사용성 극대화 + + ![4.png](./img/4.png) + - **상태 관리 및 전역 상태 관리** + - useState를 활용한 동적 데이터(사용자 입력 값, 서버로부터 받은 데이터 등) 관리 + - useEffect를 통한 컴포넌트 생명주기 기반 데이터 페칭 및 DOM 업데이트 처리 + - useRef를 사용하여 최신 상태 유지와 즉시 참조 가능성 확보 + - 상태 변경에 따른 자동 렌더링 및 코드 간결화로 유지보수성 향상 + - Context API를 통한 전역 상태 관리 도입 + - 중복된 상태 전달 없이 필요한 데이터에 직접 접근할 수 있도록 개선하여 코드 가독성 향상 + ![3.png](./img/3.png) + - **백엔드 API와의 통신** + - 사용자 입력 기반 영화 정보(영화 이름, 지역, 날짜 등)를 처리하는 비동기 통신 구현 + - fetch API를 사용하여 백엔드 서버와 통신하며, 실시간 상영 스케줄 정보 및 응답값 반환 + - 네트워크 지연 없는 사용자 경험 최적화 + ![2.png](./img/2.png) + ![1.png](./img/1.png) + - 핵심 기능 + - 실시간 영화 상영 정보 제공: 사용자의 질문에 대한 응답 제공 및 사용자가 입력한 영화, 지역, 날짜 정보를 바탕으로 상영 시간 정보를 실시간으로 제공 + - 상태 기반 UI 업데이트: 사용자 입력 및 백엔드 응답에 따른 UI 실시간 변경 처리. 지역 선택 시 입력값과 응답값에 따른 동적 업데이트. + - 유연한 필터링 및 데이터 처리: 영화, 지역, 날짜 등의 선택에 따른 유동적인 데이터 필터링 및 항목 변경 시 즉각 처리. + + - lighthouse 지표 +![lighthouse.png](./img/lighthouse.png) + +- 백엔드 + - RESTful API 설계에 대한 이해 및 적용 (설계 및 API 명세서 작성) + - Swagger를 사용한 API 명세서 작성 (설계 및 API 명세서 작성) + - BDDMockito, JUnit5를 사용한 단위 테스트 작성 (테스트 작성) + ![test.png](./img/test.png) + - ExceptionHandler을 통한 공통 예외 처리 (예외 처리) + - 전역에서 발생하는 예외를 한 곳에서 처리함으로써, 예외 처리 로직을 모듈화하고 유지보수성 높임 + - 로깅을 통해, 모니터링과 디버깅이 수월 + ![exception.png](./img/exception.png) + - Validation 과정을 통해 데이터 유효성 검증 (검증 과정 추가) + - Pattern, Size 지정을 통해 요청 형식 제한 + - 데이터 무결성 보장 및 보안 강화 + ![validate.png](./img/validate.png) + - AI Vectorize 과정 스케줄링 \ No newline at end of file diff --git "a/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\354\235\270\352\263\265\354\247\200\353\212\245.md" "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\354\235\270\352\263\265\354\247\200\353\212\245.md" new file mode 100644 index 0000000..2093855 --- /dev/null +++ "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\354\235\270\352\263\265\354\247\200\353\212\245.md" @@ -0,0 +1,26 @@ +## chatgpt api를 활용한 최적의 응답 생성 방법 + +문제: 서비스 품질을 유지하려면 정해진 형식에 맞게 출력되어야 하는데 llm특성 상 매번 조금씩 다른 답변이 생성됨 + +테스트: python 코드로 vs 프롬프트 엔제니어링으로 + +- 프롬프트: 원하는 답변이 나오기는 하지만 세세한 것까지 자연어로 지시해야 하고, 수정이 용이하지 않음 +- python 코드: 프롬프트로 페르소나만 설정 + python 활용해서 응답 형식을 제한하면 형식에는 맞지만 이번 단계에서는 불필요한 답변(‘예매를 도와드릴까요?‘ 등)까지 생성됨 + +해결: 하이브리드 형식 + +- python 조건문으로 답변 틀 생성하고 / 프롬프트로 페르소나&답변 생성 방향&제한사항(절대 티켓 예매 관련해서는 언급하지 마세요)등 간단하게 설정 + +문제2: + +## 가공을 위한 정형화된 아웃풋 + +LLM을 활용한 응답에서 정형화의 문제가 있음. + +해결: 구체적인 프롬프트 엔지니어링을 통해 응답의 정형화 가능하였지만 아쉬운 점이 존재하였으며, 추후 structured outputs기능을 통해 고정된 답변을 생성하면 더 좋을 것으로 예상됨 + +## chatgpt api를 활용한 엔티티 추출 방식 + +엔티티 추출 후 RAG에서 검색 시 FAISS 검색만 하였을 때 의미 기반 Semantic Search만 사용할시 영화 제목은 의미 기반 검색이 효율이 떨어짐, + +해결: 따라서 Levenshtein distance를 이용한 검색을 추가하여 Hybride 방식의 검색 방식으로 변경하였을 때 효율이 급상승함. \ No newline at end of file diff --git "a/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\201\264\353\235\274\354\232\260\353\223\234.md" "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\201\264\353\235\274\354\232\260\353\223\234.md" new file mode 100644 index 0000000..e843de0 --- /dev/null +++ "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\201\264\353\235\274\354\232\260\353\223\234.md" @@ -0,0 +1,152 @@ +# 1. Mysql 도커 이미지로 EC2에서 Endtrypoint 에러 + +### 문제 + +- 도커 이미지로 만든 Mysql 이미지를 EC2 인스턴스에서 실행시 아래 에러가 났다. +- DB 이미지에 초기 테이블과 스키마를 설정해주기 위해 넣은 init.sql 파일의 권한 에러가 났다. + +```java +[Note] [Entrypoint]: /usr/local/bin/docker-entrypoint.sh: running /docker-entrypoint-initdb.d/init.sql +/usr/local/bin/docker-entrypoint.sh: line 75: /docker-entrypoint-initdb.d/init.sql: Permission denied +``` + +### 시도 + +1. 처음 에러를 보고 든 생각은 /usr/local/bin의 디렉토리만 보고 도커와 Mount된 Host 디렉토리에 대한 권한이 없는 줄 알고 해당 컨테이너와 Mount된 볼륨 권한을 설정해줬지만 에러가 났다. +2. 다시 에러 메시지를 살펴보니 이미지 생성 할때 넣어준 init.sql 파일이였다. + - init.sql을 로컬에서 생성해서 파일 읽기 권한이 root에만 있어서 도커가 실행되고 나서 접근할 수 없었다. + +### 해결 + +```docker +chmod 755 명령어를 dockerfile에 추가해 주었더니 성공 했다. +``` + +# 2. Python Crawling 이미지 생성 중 chrome browser 설치 문제 + +### 문제 + +- 풀스텍 팀원이 작성한 Python 코드를 인스턴스에 도커 이미지로 생성하려고 했다. + - 이전에 미리 코드 작성하면서 사용한 의존성들을 작성해달라고 했다. + - 아래와 같은 의존성들이 필요했다. + + ```docker + selenium + python-dotenv + coverage + pymysql + schedule + apscheduler + cryptography + ``` + +- 해당 의존성을 다 포함해서 이미지를 만들었지만, 코드가 작동이 되지 않았다. +- Selenium은 chrome broswer를 사용하는데, 당연히 로컬 환경에서는 깔려있어서 고려를 하지 못했다. + +### 시도한 것 + +- 우선 인스턴스에서 실행되는 도커에 bash로 접속하여, linux용으로 만들어진 chrome을 공식 홈페이지에서 wget으로 다운받아 코드를 실행 했더니 잘되어서 Dockerfile에 포함 시켜서 이미지를 빌드했다. + - RUN 명령어를 사용하여 설치하려고 했는데 수많은 에러가 났다. +- CMD 명령어로 시도를 해봤지만, python 실행 명령어를 사용할 수 없었다. +- shell 스크립트를 이미지에 포함해서 이미지를 만들어 실행했지만, 해결 할 수 없는 에러가 났다. + +### 해결 + +**결국 python이외의 아무것도 추가하지 않은 이미지를 실행시키고 bash에 붙어서 필요한 환경을 구축하고 사용한 명령어 그대로 Dockerfile에 작성했다.** + +- 그랬더니 성공했다! + +```docker +FROM python:3.12 + +# 작업 디렉토리 설정 +WORKDIR /app + +# 루트 디렉토리의 모든 파일을 컨테이너의 /app 디렉토리로 복사 +COPY . /app + +RUN apt-get update +RUN pip install -r requirements.txt +RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +RUN apt-get install -y ./google-chrome-stable_current_amd64.deb + +ENTRYPOINT [ "python", "main.py" ] +``` + +# 3. 크롤링 인스턴스의 적절한 type 설정 + +### 문제 + +- 크롤링이 멀티프로세싱을 이용함. +- 로컬에서 apple m2 pro 프로세서로 테스트시 하루치 데이터를 가져오는데 약 20분이 걸림 + - 로컬보다 cpu 개수가 낮은 인스턴스는 적합하지 않다고 판단. + - 별다른 테스트 없이 4 cpu 8gb 인스턴스인 c6i.xlarge를 사용 +- 하지만 가격이 너무 비싸 인스턴스 조정을 하고 싶었음 + +### 시도 한 것 + +- 모니터링을 공부하고 구축한 상황이라 여러 인스턴스를 만들고 동시에 실행 시켜 모니터링을 시도했음 +- Promethues를 활용하여 1일치 데이터를 크롤링에 걸리는 시간과 리소스를 비교 + +### 결과 + +**c6i.large (2core, 4gb, 0.096$)** + +![c6i.large.png](./img/c6i.large.png) + +**t2.large (2core, 8gb, 0.11$)** + +![c6i.xlarge.png](./img/c6i.xlarge.png) + +**t2.xlarge (4core , 8gb, 0.23$)** + +![t2.large.png](./img/t2.large.png) + +**c6i.xlarge (4core, 8gb, 0.19$)** + +![t2.xlarge.png](./img/t2.xlarge.png) + +**CPU Core 개수와 램의 스펙에 상관없이 일정하게 약 25분이 걸렸다. 예상과는 다른 결과에 놀랐지만, 가격이 제일 저렴하고, 효율적으로 리소스를 사용하는 `c6i.large` 인스턴스를 사용하기로 결정** + +# 4. 크롤링 이외의 시간에 사용되지 않는 인스턴스 + +### 문제 + +- c6i.large는 기본적인 인스턴스(t2.micro, t2.small)들에 비해 가격이 높은 편 +- 하지만 사용되는 시간보다 사용되지 않는 시간이 더 많음 +- 필요할 때만 리소스를 사용할 수 있는 방법을 찾아야함 + +### 시도한 것 + +- AWS lambda + EventBridge 를 이용한 방법 + - 하지만 labmda 함수는 최대 15분까지만 작동되는 한계점이 있어, 크롤링이 이 시간보다 더 많이 작동 되어 사용하지 못함. +- AWS Fargate와 EventBridge를 사용하는 방법 + - Container Image로 만든 크롤링 앱을 ECS 클러스터를 통해 Task로 정의하면, 정해진 시간에만 인스턴스를 활용하여 실행시킬 수 있음 + +### 결과 + +- 관련 지식 기반이 부족 및 시간 부족으로 인해 아직 구현하지 못했음. + +# **5. 인스턴스와 서브넷 등의 네트워크 관계에 대한 공부의 필요성** + +- 각 파트의 도커 이미지를 만들어 배포를 하기 전 까지는 수월하고 도커 컴포즈를 통한 로컬 테스트까지 순조롭게 진행하였음. +- 하지만 아키텍쳐에 맞게 배포를 하는 과정에서 프라이빗 서브넷, 퍼블릭 서브넷 등의 인스턴스와 서브넷 등의 정확한 의미를 깨닫지 못하고 배포를 하는 과정에서 IP주소 밑 같은 대역의 연결이 맞는건지 헷갈리기도 했었음 +- 결국 네트워크에 관한 서칭으로 각 서브넷에 대한 관계를 깨닫고 다시 배포를 실시하였고 성공하였음. + +# **6. CI/CD는 모든 상황에서 필요한 것인가?** + +- 항상 코드를 푸시할때마다 배포를 하게 된다면 과연 좋을까? +- 변경이 자주되는 코드(이미지)가 있는 반면에, 한 번 올려두면 절대 변하지 않을 코드(이미지)들도 존재한다. 따라서 항상 모든 이미지를 새로 배포하는 것이 아닌 프로젝트 특성을 고려해서 변경이 자주 되는 부분과 한 번 올려두면 끝인 부분을 구분해둬서 workflows를 구성하려고 하였다. + +# 7. Docker container 배포시 각 컨테이너의 연결 방법에 대한 고민 + +- 이번 프로젝트는 인스턴스를 2개로 나누어 각 컨테이너가 사용하는 리소스 량에 따라 인스턴스 스펙을 나누어 배포를 실시하였습니다. +- 이때 한 인스턴스에 backend, ai의 연결 관계에서는 docker의 network를 이용하여 연결을 진행하였습니다. +- Docker network를 사용하지 않으면은 매 배포마다 다른 IP를 부여받아서 연결이 항상 잘 되지 않았던 문제가 있었는데 같은 인스턴스 내에서 배포를 하여 연결하는 과정에서는 docker network를 사용하여 연결하는 방법을 이용하였습니다. +- 그렇다면 다른 인스턴스에서 연결 할 때, 서브넷이 다를때의 연결 방식 또한 다르다는 걸 알았고 그런 상황들에 대비하여 pipeline를 짤때 고려해야할 점이 더 있다는 것 또한 알게 되었습니다. +- 컨테이너들 간의 연결 시 인스턴스의 사용, 서브넷 등의 의해 연결 방식 또한 방법이 나뉘는 것을 알게 되었습니다. + +1. HTTPS, HTTP 도메인 관련 허용 +- 일반적으로 안전한 사이트를 위해 HTTPS를 사용합니다. +- HTTPS는 SSL/TLS 프로토콜을 사용하여 데이터를 암호화를 하게 됩니다. +- SSL 인증서를 구입하고, 웹 서버에 설치하는 과정이 필요합니다. SSL 인증서는 공인된 인증 기관(CA)으로부터 구입할 수 있으며, 인증서의 종류에 따라 가격과 보안 수준이 다릅니다. \ No newline at end of file diff --git "a/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\222\200\354\212\244\355\203\235.md" "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\222\200\354\212\244\355\203\235.md" new file mode 100644 index 0000000..b8eb71e --- /dev/null +++ "b/docs/\355\212\270\353\237\254\353\270\224\354\212\210\355\214\205_\355\222\200\354\212\244\355\203\235.md" @@ -0,0 +1,119 @@ +--- + +## 영화 상영 스케줄 크롤링 시간을 어떻게 단축할 수 있을까? + +문제1 + +- 영화 상영 정보에 대한 하루치 데이터를 크롤링 하는데 120분이 걸리는 상황 + +해결1 + +- 멀티 프로세싱을 사용해 40분으로 단축 (3배) + +문제 2 + +- 일주일치 데이터를 크롤링 하는데, 40 x 7 = 280분이 걸리는 상황 +- 추가 시간 단축을 위해 멀티스레딩 시도 했으나 context switching 문제 발생 + +해결 2 + +- 멀티프로세싱만 사용하기로 결정 +- 프로세스 최적화 + - 최대 가용한 cpu 수에 맞게 프로세스 수 설정 + - 크롤링 대상의 크기에 따라 프로세스 별 사이즈 설정 +- 새롭게 업데이트되는 하루치 데이터만 크롤링하기로 결정 +- ⇒25분으로 단축 (11배) + +--- + +## 인공지능 Python 코드를 구동하기 위한, 효율적인 아키텍처를 어떻게 설계할까? + +### 문제1 + +- 인공지능 팀에서 Python 코드를 작성하기 때문에, 이를 구동시키기 위한 방법이 고민되는 상황 +- 선택지 + - Spring 애플리케이션에서 Jython을 사용해 구동 + - Python 코드를 구동시키기 위한 서버를 따로 분리 + +### 해결 + +- Spring 애플리케이션에서 Jython을 사용해 구동하기로 결정 +- 서버를 분리할 시, 서버 간 통신과정 추가로 인해 지연 시간 증가 및 유지보수 비용 증가가 예상되었기 때문 + +### 문제2 + +- Jython이 Python 2.7까지 지원해서, 최신 라이브러리와의 호환성 문제 +- 외부 패키지를 설치하기 어려운 문제 + +### 해결 + +- 서버를 두 개로 분리해, FastAPI에서 구동해서 해결 +- 효과 + - 책임 분산 및 확장성 개선 + - AI 코드 실행에 대한 책임을 FastAPI가 맡음으로써, Spring 애플리케이션은 비즈니스 로직에 집중 가능하고, 이로 인해 확장성 및 유지보수성이 상승함 + - 장애 격리로 안정성 강화 + - AI 코드 실행이 리소스를 많이 소모하기 때문에, 이로 인한 장애 발생 시 Spring 애플리케이션에 문제 전염되는 것을 차단 + - 성능 및 처리 효율성 증가 + - FastAPI는 비동기 처리를 잘 지원해서, AI 작업을 보다 빠르고 효율적으로 처리 가능 + +--- + +## 여러 객체에 흩어져있는 정보를 어떻게 한 번에 묶을 수 있을까? + +### 문제 + +- 영화 상영 정보, 영화관 객체를 사용해 영화관 별 상영정보를 얻어야 하는 상황 + +### 해결 + +- stream의 groupingBy, mapping을 사용해 해결 + +```java +Map> timesPerTheaterNameMap = dto.stream() + .collect( + groupingBy( + d -> d.getTheater().getName(), + mapping(d -> d.getMovieInfo().getTime(), toList()) + ) + ); +``` + +--- + +## 영화 상영 스케줄 크롤링 시간을 어떻게 단축할 수 있을까? + +### 문제1 + +- 영화 상영 정보에 대한 하루치 데이터를 크롤링 하는데 120분이 걸리는 상황 + +### 해결1 + +- 멀티 프로세싱을 사용해 40분으로 단축 (3배) + +### 문제 2 + +- 일주일치 데이터를 크롤링 하는데, 40 x 7 = 280분이 걸리는 상황 +- 추가 시간 단축을 위해 멀티스레딩 시도 했으나 context switching 문제 발생 + +### 해결 2 + +- 멀티프로세싱만 사용하기로 결정 +- 프로세스 최적화 + - 최대 가용한 cpu 수에 맞게 프로세스 수 설정 + - 크롤링 대상의 크기에 따라 프로세스 별 사이즈 설정 +- 새롭게 업데이트되는 하루치 데이터만 크롤링하기로 결정 +- ⇒ 25분으로 단축 (11배) + +--- + +## 상태의 변경 사항을 즉시 반영할 수 없을까? + +### 문제 + +- React에서 상태 변경 시 useEffect만으로는 최신 상태 반영 불가 +- 변경된 값을 다른 곳에서 참조하면 최신 값이 아닌 이전 값이 참조되어 데이터 오류 발생 + +### **해결** + +- useEffect는 상태 변경을 감지하지만, 다른 함수나 이벤트에서 최신 상태값을 바로 참조할 수 없는 문제가 발생함을 파악 +- 이를 해결하기 위해 useRef를 함께 사용하여 상태 변경 사항이 즉시 데이터에 반영되도록 구현 \ No newline at end of file diff --git "a/docs/\355\232\214\352\263\240.md" "b/docs/\355\232\214\352\263\240.md" new file mode 100644 index 0000000..33d9765 --- /dev/null +++ "b/docs/\355\232\214\352\263\240.md" @@ -0,0 +1,97 @@ +### ‘성과’ 측면 (개발 달성도, 완성도 등) + +- Eddy + - 아쉬운 점 + - 뭔가 온전히 집중하는 프로젝트의 느낌이 아니었던 느낌.. 대면으로 했었으면 더 좋았을거같다. +- Yohan + - 잘한 점 + - 계획한 최소 기능 구현 완료 + - 아쉬운 점 + - 실 서비스 사용 테스트를 통해 예외 처리 개선 필요 +- Ryan + - 잘한 점 + - 계획한 최소 기능 구현 완료 + - 아쉬운 점 + - 아직 부족한 기능 mvp단계의 초기에서 멈춰서 아쉽다. +- Mir + - 잘한 점 + - 처음으로 웹 크롤링을 도전하여, 데이터를 성공적으로 수집하고 DB에 저장하는 과정을 **완성** + - 새로운 기술을 학습하고 실제로 적용하여 결과를 낸 것에 대해 성취감 + - 아쉬운 점 + - 프로젝트 초기에 설계한 대로 진행했으나, 기능이 추가되거나 변경되는 과정에서 **설계 수정이 필요했음. 이로 인해 코드가 덧붙여지면서 최종적으로 구현된 구조가 다소 아쉬웠음.** + - 처음부터 최종적인 기능 요구 사항을 더 명확히 설계했다면 더욱 깔끔한 구조를 유지할 수 있었을 것이라 생각 +- Bryan + - 잘한 점 + - 소규모 프로젝트에서도 최소한의 가용성을 가진 인프라를 구축한 것 + - **개발 환경과 프로덕션 환경을 나누어 개발 한것** + +- Alyssa + - 😃 **ChatGPT API를 다양한 방식으로 활용, 매번 일정한 답변을 생성하는 데 성공 → 서비스 품질 향상** + - 🥲 기획 단계에 시간을 좀 더 쏟았다면 하는 아쉬움 + +### ‘배움’ 측면 (배운 것, 향후 실무 활용 가능 정도 등) + +- Yohan + - 잘한 점 + - 공통 예외 처리 및 Validation에 대한 학습 및 도입을 통해 보다 안정성 있는 서비스 구현 + - 아쉬운 점 + - Docker 등 클라우드에 대한 학습 및 이해 필요 +- Ryan + - 잘한 점 + - AI-백엔드-프론트엔드/클라우드의 흐름과, Fast API와 같은 기술의 습득 및 langchain을 이용한 LLM가공 기술 획득 + - 아쉬운점 + - 백엔드와 프론트엔드 클라우드 기술의 흐름은 알았지만 아직 학습과 이해가 부족함. +- Alyssa: 😃 백엔드-AI-프론트엔드 등 **서버 간 데이터의 흐름**과 협업 방식을 습득 → 향후 실무에 적용 가능 +- Eddy : CI/CD에 관한 설계 방식 및 효율성을 따지고 보게 됨 +- Mir + - 잘한 점 + - 하나의 기능을 처음부터 끝까지 구현해 보는 경험 + - 프로젝트에서 특정 기능이 전체 구조와 어떻게 연결되는지, 그리고 다른 파트와 어떻게 협력하는지에 대한 깊은 이해를 함 + - 아쉬운 점 + - 새로운 기술을 배우면서 구현했지만, 내가 선택한 방식이 최선인지에 대한 피드백을 받을 기회가 부족했음. + - 좀 더 다양한 시각에서 논의하고 검토할 수 있었다면, 기술적으로 더 나은 선택을 할 수 있었을 것이라는 아쉬움이 있음. +- Bryan + - 잘한 점 + - 전체적인 AWS 인프라를 설계하는 법, Ec2, Vpc, S3 등 기본적인 AWS 서비스들을 이해할 수 있었다. + - **Terraform 과 Ansible을 활용하여 효율적으로 인프라를 구성하고 환경을 만드는 것을 배웠다.** + - 아쉬운 점 + - **모든 것이 처음이고 배우면서 했기에, 얇고 넓게 배운거 같아 아쉽다.** + - 실무에서는 이정도 규모가 아닌 몇배 몇십배는 큰 환경을 관리해야할텐데 어떻게 관리하는지에 대한 궁금증만 커져갔다. + +### ‘협업’ 측면 (팀 운영 관련 등) + +- Yohan + - 잘한 점 + - MVP 도입을 통해, 작은 목표에 집중해 빠르게 개발한 점 + - 매일 스크럼을 통해, 진행상황 공유한 점 + - 아쉬운 점 + - 다른 팀원이 간편하게 테스트할 수 있는 환경 구성 필요 + - 테스크 관리 및 문서화 할 수 있는 환경 필요 - Jira +- Ryan + - 잘한 점 + - 매일 짧게 회의하고 진행 상황을 공유한 것은 전반적인 상황을 팔로우하기 좋았음. + - 첫 프로젝트라 PR 발표가 매우 좋았음. + - 아쉬운 점 + - 첫 프로젝트라 자기 객관화가 부족하여 일정의 딜레이가 아쉬움. + - 문서화가 필요함. +- Bryan + - 잘한점 + - 팀 프로젝트를 처음 하는 팀원도 있고, 개발이 처음이 팀원도 있었지만, 서로 부족한 부분을 채워주며 함께 성장했고 완벽하지 않지만 프로젝트를 순탄하게 진행했음 + - **매일 데일리 스크럼과 회의를 하며 서로의 상황을 개발 진척도나 트러블을 나누었던 것이 너무 좋았음.** 이를 통해 팀이 방향을 잃지 않고 한 방향으로 올 수 있었던거 같음 + - 팀원 개인이 가진 능력(문서화, 내용 정리, 본질 잃지 않는 질문 등)이 매우 조화로웠음. + - 아쉬운점 + - 처음 설계를 하고 시작했지만, 모두 낯선 환경에 세밀한 설계가 되지 않아 **프로젝트 진행 중간에 많이 변경했다는 점이 아쉽다.** 아무것도 모르고 시작했기에 당연한 결과였기에 마냥 아쉽지 않고 이를 통해 배운게 많아서 오히려 좋다. +- eddy + - 잘한 점 + - 매일 짧게라도 회의를 하여 진행 상황을 공유한 것 + - 아쉬운 점 + - Jira 등의 진행 상황에 대한 문서화가 필요했을 것 같다. (실제로 세세한 진행상황을 잘 정리해두지 않아서 나중에 정리할때 헷갈리지 않을까…) +- Mir + - 잘한 점 + - **매일 회의**를 통해 팀원들과 진행 상황을 공유하고, 프로젝트에서 변경사항이나 문제에 대해 즉각적으로 논의한 점 + - 아쉬운 점 + - 시간 제약으로 인해 다른 팀원들의 코드를 완벽하게 이해하지는 못한 점 +- Alyssa + - 😃 매일 스크럼을 진행해 서로 진행 상황과 문제점을 공유한 것 + - 😃 애자일 방식을 도입해 개발/비개발 모든 측면에서 문제점이 있으면 → **바로 논의해 즉각적으로 수정하는 태도를 유지한 것** + - 🥲 각 스프린트가 원래 계획했던대로 기획→개발→테스트, 피드백→회고가 한 스프린트 내에 이뤄졌으면 좋았을 것 같다. 우리 팀에게 적절한 스프린트 주기를 찾는 과정이었다고 생각하는데, 이**번 프로젝트를 통해 찾은 적절한 주기를 앞으로 더 테스트해볼 수 없어 아쉽다.** \ No newline at end of file