export default function UserMenu() { const { data: session, status } = useSession();
if (status === 'loading') return <Skeleton />; return session ? <ProfileMenu user={session.user} /> : <LoginButton />; } 장점 사용자 경험 개선: 즉각적인 UI 반응.
세션 동기화: 탭 간 상태 일관성 유지.
5. NextAuth.js 설정: 확장성 확보 권장 구성 typescript // auth.config.ts import NextAuth from 'next-auth'; import Google from 'next-auth/providers/google'; import Credentials from 'next-auth/providers/credentials';
export const authOptions = { providers: [ Google({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), Credentials({ async authorize(credentials) { // 커스텀 로그인 로직 (예: 이메일/비밀번호) }, }), ], callbacks: { async jwt({ token, user }) { if (user) token.role = user.role; // 역할 정보 추가 return token; }, async session({ session, token }) { session.user.role = token.role; // 클라이언트에 역할 전달 return session; }, }, }; 보안 강화 설정 Cookie 옵션: Secure, HttpOnly, SameSite=Strict 적용.
세션 갱신: updateAge: 24 * 60 * 60 (24시간마다 갱신).
종합 비교 및 권장 조합 방법 사용 사례 보안 성능 구현 복잡도 미들웨어 전역 인증 검사 ⭐⭐⭐ ⭐⭐⭐ 낮음 서버 컴포넌트 역할 기반 접근 제어, SEO 최적화 ⭐⭐⭐ ⭐⭐⭐ 중간 API 라우트 백엔드 API 보호 ⭐⭐⭐ ⭐⭐ 중간 클라이언트 실시간 UI 업데이트 ⭐ ⭐⭐ 낮음 최적화 조합 전략 미들웨어로 기본 인증 검사 → 서버 컴포넌트에서 세부 권한 확인 → API 라우트에서 이중 검증.
클라이언트 컴포넌트는 보조적 UI 업데이트용으로만 사용.
NextAuth.js 설정: JWT + 세션 콜백으로 역할 정보 확장.
보안 추가 권장 사항 HTTPS 강제: next.config.js에서 HTTPS 리디렉션 설정.
환경 변수 관리: NEXTAUTH_SECRET 반드시 설정.
로그인 시도 제한: IP 기반 차단 또는 CAPTCHA 통합.
2FA(이중 인증): 중요한 기능에 대해 추가 인증 계층 적용.
이 조합을 통해 Next.js 애플리케이션의 인증 시스템을 견고하게 구축할 수 있으며, 확장성과 유지보수성도 함께 확보됩니다.
안녕하세요! 오늘은 AI 개발자와 머신러닝 연구자들이 많이 활용하는 RunPod 클라우드 GPU 서비스의 가격 비교와 가성비 분석에 대해 알아보겠습니다. 특히 VRAM 대비 가격을 중심으로 어떤 GPU가 여러분의 AI 워크로드에 가장 적합한지 살펴보겠습니다.
RunPod이란?
RunPod는 AI 개발, 머신러닝 훈련, 추론 등을 위한 GPU 클라우드 서비스로, 다양한 NVIDIA 및 AMD GPU를 온디맨드 방식으로 제공합니다. 특히 최신 GPU 모델을 경쟁력 있는 가격에 이용할 수 있어 프리랜서 개발자부터 스타트업, 연구 기관까지 널리 사용되고 있습니다.
VRAM 대비 가격 분석
AI 모델, 특히 대형 언어 모델(LLM) 및 이미지 생성 모델을 실행할 때 가장 중요한 자원은 **VRAM(비디오 메모리)**입니다. 아래는 RunPod에서 제공하는 주요 GPU의 VRAM 대비 시간당 비용을 분석한 표입니다.
주요 GPU별 상세 분석
1. NVIDIA A40 (최고의 VRAM 가성비)
VRAM: 48GB GDDR6
시간당 비용: $0.35 (할인가)
1GB당 비용: $0.0073
특징:
TensorCore 지원으로 딥러닝 성능 향상
High 가용성으로 안정적인 작업 가능
9 vCPU와 50GB RAM으로 전처리 작업도 원활
A40은 VRAM 대비 가격이 가장 우수한 GPU로, 중대형 AI 모델을 다루는 데 적합합니다. 특히 LoRA 파인튜닝과 같은 메모리 집약적 작업에서 뛰어난 가성비를 보여줍니다.
2. AMD MI300X (최대 VRAM 용량)
VRAM: 192GB HBM3
시간당 비용: $1.99 (할인가)
1GB당 비용: $0.0104
특징:
압도적인 192GB VRAM 용량
24 vCPU와 283GB RAM으로 강력한 성능
낮은 가용성(Low)이 단점
MI300X는 초대형 언어 모델(LLM) 작업에 이상적인 선택입니다. Llama-70B, Claude, GPT-4급 모델을 양자화 없이 전체 정밀도로 로드할 수 있습니다.
3. NVIDIA L40 vs L4 비교
L40:
48GB VRAM, $0.84/hr, $0.0175/GB/hr
Transformer 엔진 최적화로 어텐션 메커니즘 가속
8 vCPU와 94GB RAM으로 복잡한 데이터 파이프라인 지원
L4:
24GB VRAM, $0.37/hr, $0.0154/GB/hr
저전력 설계로 추론에 최적화
12 vCPU와 50GB RAM으로 경량 작업에 적합
L40은 훈련 및 고급 추론에, L4는 효율적인 배포 및 서빙에 더 적합합니다.
AI 워크로드별 최적 GPU 추천
대형 언어 모델(LLM) 작업
전체 모델 로드: AMD MI300X (192GB) 또는 H200 SXM (141GB)
LoRA/QLoRA 파인튜닝: A40 (48GB) 또는 L40 (48GB)
양자화 기반 추론: L4 (24GB) 또는 RTX A5000 (24GB)
이미지 생성 모델
고해상도 생성: RTX A6000 (48GB) 또는 A40 (48GB)
실시간 이미지 생성: RTX 4090 (24GB)
효율적 배치 처리: RTX 4000 Ada (20GB)
멀티모달 AI 작업
비전-언어 모델: L40 (48GB) 또는 A40 (48GB)
오디오-텍스트 변환: RTX A5000 (24GB)
비디오 처리: RTX 4090 (24GB) 또는 RTX A6000 (48GB)
경제적인 RunPod 사용 전략
1. 스팟 인스턴스 활용
RunPod의 스팟 인스턴스는 온디맨드 가격보다 15-40% 저렴합니다. 중단 허용 작업에는 항상 스팟 인스턴스를 활용하는 것이 경제적입니다.
2. 작업 특성에 맞는 GPU 선택
훈련 작업: VRAM이 큰 A40, L40, MI300X
추론 작업: 추론 최적화된 L4, RTX 4000 Ada
개발 및 실험: 비용 효율적인 RTX A4000, RTX 3090
3. 모델 최적화 기법 적용
양자화(Quantization): INT8/INT4 최적화로 VRAM 요구량 감소
모델 프루닝(Pruning): 불필요한 가중치 제거
모델 샤딩(Sharding): 여러 GPU에 모델 분산
VRAM과 RAM의 역할과 중요성
VRAM의 역할
모델 가중치 저장
어텐션 캐시 및 KV 캐시 관리
중간 활성화값 저장
텐서 연산 가속
시스템 RAM의 역할
데이터 전처리 및 배치 구성
모델 로딩 준비
결과 후처리 및 저장
VRAM 오버플로 시 임시 저장소
대형 AI 모델을 효율적으로 실행하려면 VRAM과 RAM 간의 균형이 중요합니다. RunPod의 모든 GPU는 적절한 비율의 RAM을 제공하지만, 특별한 작업에는 더 많은 RAM이 필요할 수 있습니다.
결론: 가성비 최고의 GPU는?
VRAM 대비 가격만 고려한다면 NVIDIA A40이 명확한 승자입니다. 하지만 워크로드 특성과 실제 성능을 고려하면:
최고의 종합 가성비: NVIDIA A40
대규모 LLM 작업용: AMD MI300X
효율적인 추론용: NVIDIA L4
중소규모 프로젝트용: RTX A4000
자신의 AI 워크로드 요구사항과 예산 제약을 고려하여 최적의 GPU를 선택하는 것이 중요합니다.
마지막 팁: RunPod 사용 시 비용 절감 방법
사용하지 않는 인스턴스는 즉시 중지하기
도커 이미지 최적화로 시작 시간 단축
체크포인트 저장으로 작업 연속성 확보
볼륨 스토리지 최적화로 스토리지 비용 절감
자동화 스크립트로 유휴 시간 최소화
여러분은 어떤 GPU를 사용하고 계신가요? 특별한 AI 프로젝트에 적합한 GPU에 대한 조언이 필요하시면 댓글로 남겨주세요! 다음 글에서는 RunPod vs Lambda Labs vs Vast.ai 가격 비교 분석을 진행할 예정입니다. 구독과 좋아요 부탁드립니다! 👍
2030년을 상상해보세요. 불과 10년 전만 해도 공상과학 영화에서나 볼 법한 일들이 우리의 일상이 되어버린 세상입니다. AI 비서와 대화하며 하루를 시작하고, 자율주행차를 타고 출근하는 모습이 더 이상 낯설지 않죠.
이 글에서는 2030년의 모습을 생생하게 그려보려 합니다. 스마트홈과 AI의 보편화, 서비스의 자동화, 가상 인물의 보편화와 그에 따른 정보의 신뢰성 문제, 그리고 역설적으로 부활하는 오프라인 커뮤니티의 중요성까지. 또한 이러한 변화들이 우리의 노동 시장과 사회 구조에 어떤 영향을 미치는지도 고민해보았습니다.
미래는 언제나 불확실하지만, 그 속에서 기회와 도전을 발견하는 것은 우리의 몫입니다. 2030년의 세상이 어떤 모습일지, 그리고 그 속에서 우리는 어떻게 살아가게 될지, 함께 들여다보시죠. 이 여정이 여러분에게 미래에 대한 흥미로운 통찰을 제공하고, 다가올 변화에 대비할 수 있는 영감을 줄 수 있기를 바랍니다.
1. 스마트홈과 AI 비서의 보편화
a) 건강 관리: AI 비서가 사용자의 수면 패턴, 활동량, 식습관을 분석하여 맞춤형 건강 조언을 제공합니다. "오늘은 스트레스 지수가 높아 보이네요. 10분간 명상을 추천드립니다."
b) 에너지 효율: AI가 날씨와 사용자의 일정을 고려해 집의 온도와 조명을 자동으로 조절합니다. "오후 3시에 귀가 예정이시네요. 실내 온도를 미리 조절해 놓겠습니다."
c) 약자 돌봄: 노인이나 아이들의 활동을 모니터링하고 이상 징후를 감지합니다. "할머니께서 30분째 움직임이 없으십니다. 확인해 보시는 게 어떨까요?"
d) 사물인터넷 최적화를 위한 운영체제 출현: 콘센트 및 기기와 자연스럽게 연동되어 조절되는 신세대 인터페이스 개발. 전세계적으로 이 규격이 통일될 수도. 자동차, 휴대폰, 아파트, 컴퓨터 등의 기기들과 모두 자연스럽고 효율적으로 통신.
2. 서비스의 자동화
초개인화 서비스의 발달, 개인과 가정의 데이터를 활용한 인공지능 서비스들의 다수 출현.
a) 자동 식료품 보충: 냉장고가 내용물을 파악하고 부족한 식료품을 자동으로 주문합니다. "우유가 다 떨어졌네요. 내일 아침 배송으로 주문해 놓았습니다.", "내일 저녁에 돼지 목살을 구워먹고 싶어" -> 목살 등을 주문합니다.
b) 스마트 옷장: 날씨와 일정에 맞는 옷을 추천하고, 세탁이 필요한 옷을 자동으로 세탁 서비스에 보냅니다. "오늘 비 올 확률이 90%네요. 방수 자켓을 추천드립니다."
c) 자동 차량 정비: 차량이 자체 진단을 통해 정비가 필요한 부분을 감지하고 정비소와 예약을 잡습니다. "타이어 공기압이 낮습니다. 내일 오전 10시에 근처 정비소 예약을 해두었습니다."
3. 가상 인물과 정보의 신뢰성 문제
a) 가짜 리뷰, 가짜 뉴스 확대: 정보 검증 시스템의 중요성이 더욱 부각됩니다. 신뢰있는 정보인가에 대한 중요성 증가.
b) 유튜브 및 SNS: 실제 인물과 구분할 수 없는 가상 인물들로 영상 대부분을 차지.
4. 오프라인 커뮤니티의 부활
가상 현실 기술, 비대면 서비스와 자동화의 확산으로 인해 사람들은 점점 더 온라인에서 시간을 보내게 되었습니다. 그러나 역설적으로 이러한 상황이 오프라인 커뮤니티에서 취미과 경험을 공유하고자 하는 수요를 높이고 있습니다. 사람들은 직접적인 대면 접촉과 실제 경험을 갈망하게 되었고, 이는 새로운 형태의 오프라인 모임과 활동의 증가로 이어지고 있습니다.
a) 테크 프리 카페: 전자기기 사용이 금지된 공간이 인기를 끕니다. 사람들은 이곳에서 직접 대화를 나누고 아날로그 게임을 즐깁니다.
5. 노동 시장의 변화
제조업을 비롯한 여러 산업 분야에서 공장 자동화와 AI의 도입으로 인력 수요가 크게 줄어들었습니다. 이로 인해 부의 양극화가 심화되었고, 기본소득제도의 필요성이 대두되고 있습니다. 일부에서는 '인공지능세' 등의 새로운 과세 방안을 통해 이 문제를 해결하려는 움직임도 있습니다.
a) AI 교육 센터: 일자리를 잃은 노동자들을 위한 AI와 협업할 수 있는 기술 교육 센터가 정부 지원으로 설립됩니다.
b) 사회 공헌 포인트 제도: 기업들이 자동화로 절감한 비용의 일부를 사회에 환원하는 제도가 도입되어, 시민들의 자원봉사 활동과 연계됩니다. 시민들은 봉사활동을 통해 소득을 얻거나 생계를 유지합니다.
c) 4일제 근무의 보편화: AI와 자동화로 인한 생산성 향상으로, 많은 기업들이 4일 근무제를 도입합니다. 이에 따라 여가 산업이 크게 성장합니다.
sqlc라 최고라는 말을 듣고 적용해보았다!.. 이 도구는 SQL 스키마와 쿼리를 기반으로 타입 안전한 Go 코드를 생성해주어, 안정성과 개발 생산성을 높여준다고 합니다. 이번 글에서는 sqlc를 사용하여 PostgreSQL 데이터베이스와 상호작용하는 방법에 대해 살펴보겠습니다.
2. sqlc 설정
sqlc를 사용하기 위해서는 먼저 sqlc.yaml 설정 파일을 생성해야 합니다. 이 파일에는 데이터베이스 엔진, 쿼리 파일, 스키마 파일, 그리고 생성된 Go 코드의 출력 위치 등을 지정합니다.
schema.sql 파일에 데이터베이스 테이블 구조를 정의합니다. 예를 들어, authors 테이블을 다음과 같이 정의할 수 있습니다
CREATE TABLE authors (
id BIGSERIAL PRIMARY KEY,
name text NOT NULL,
bio text
);
테이블 정보를 담은 DDL을 실행해 줍니다. 자동으로 마이그레이션을 해주고, 마이그레이션 파일을 저장해주는 라이브러리가 있다고 하는데, 다음에 적용해볼 예정입니다.
4. 쿼리 작성
query.sql 파일에 필요한 데이터베이스 쿼리를 작성합니다. sqlc는 이 쿼리들을 분석하여 Go 함수로 변환합니다.
-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = $1 LIMIT 1;
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
-- name: CreateAuthor :one
INSERT INTO authors (
name, bio
) VALUES (
$1, $2
)
RETURNING *;
-- name: UpdateAuthor :exec
UPDATE authors
set name = $2,
bio = $3
WHERE id = $1;
-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1;
5. 코드 생성
sqlc 설정이 완료되면, 다음 명령어를 실행하여 Go 코드를 생성합니다
sqlc generate
이 명령은 db/ 폴더 내에 query.sql.go, models.go, db.go 파일을 생성합니다. 이 파일들에는 데이터베이스 SQL 구문을 실행하기 위한 함수들이 포함되어 있습니다.
6. 생성된 코드 사용
생성된 코드를 사용하여 데이터베이스 작업을 수행할 수 있습니다. 예를 들어
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.26.0
// source: query.sql
package db
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createAuthor = `-- name: CreateAuthor :one
INSERT INTO authors (
name, bio
) VALUES (
$1, $2
)
RETURNING id, name, bio
`
type CreateAuthorParams struct {
Name string
Bio pgtype.Text
}
func (q *Queries) CreateAuthor(ctx context.Context, arg CreateAuthorParams) (Author, error) {
row := q.db.QueryRow(ctx, createAuthor, arg.Name, arg.Bio)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
const deleteAuthor = `-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = $1
`
func (q *Queries) DeleteAuthor(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, deleteAuthor, id)
return err
}
const getAuthor = `-- name: GetAuthor :one
SELECT id, name, bio FROM authors
WHERE id = $1 LIMIT 1
`
func (q *Queries) GetAuthor(ctx context.Context, id int64) (Author, error) {
row := q.db.QueryRow(ctx, getAuthor, id)
var i Author
err := row.Scan(&i.ID, &i.Name, &i.Bio)
return i, err
}
const listAuthors = `-- name: ListAuthors :many
SELECT id, name, bio FROM authors
ORDER BY name
`
func (q *Queries) ListAuthors(ctx context.Context) ([]Author, error) {
rows, err := q.db.Query(ctx, listAuthors)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Author
for rows.Next() {
var i Author
if err := rows.Scan(&i.ID, &i.Name, &i.Bio); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const updateAuthor = `-- name: UpdateAuthor :exec
UPDATE authors
set name = $2,
bio = $3
WHERE id = $1
`
type UpdateAuthorParams struct {
ID int64
Name string
Bio pgtype.Text
}
func (q *Queries) UpdateAuthor(ctx context.Context, arg UpdateAuthorParams) error {
_, err := q.db.Exec(ctx, updateAuthor, arg.ID, arg.Name, arg.Bio)
return err
}
import (
"context"
"your-project/db"
)
func main() {
// 데이터베이스 연결 설정
conn, err := pgx.Connect(context.Background(), "your-database-url")
if err != nil {
log.Fatal(err)
}
defer conn.Close(context.Background())
queries := db.New(conn)
// 새 저자 생성
author, err := queries.CreateAuthor(context.Background(), db.CreateAuthorParams{
Name: "John Doe",
Bio: pgtype.Text{String: "A prolific writer", Valid: true},
})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Created author: %v\n", author)
// 저자 목록 조회
authors, err := queries.ListAuthors(context.Background())
if err != nil {
log.Fatal(err)
}
for _, a := range authors {
fmt.Printf("Author: %s\n", a.Name)
}
}
sqlc를 사용하면 데이터베이스 스키마, SQL 쿼리와 Go 코드 사이의 불일치를 줄이고, type-safe을 보장하며, 개발 생산성을 향상시킬 수 있습니다. 또한 데이터베이스 스키마 변경 시 자동으로 코드를 업데이트할 수 있어 유지보수가 용이하다고 합니다.
결론적으로, sqlc는 Go와 PostgreSQL을 함께 사용하는 프로젝트에서 강력한 도구가 될 수 있습니다. 타입 안전한 데이터베이스 액세스 코드를 자동으로 생성함으로써 개발자는 비즈니스 로직에 더 집중할 수 있게 되며, 데이터베이스 관련 오류를 크게 줄일 수 있는 장점이 있다고 합니다.
생성형 AI로 일적으로나 생활적으로 도움을 많이 받고 있는 요즘, 업무 차원에서 개발 속도를 어떻게 하면 더욱 향상시킬까 하는 고민이 많은데요.
하지만 때로는 우리가 선택한 개발 및 배포 환경이 개발 속도를 방해하곤 합니다. 저 역시 최근에 그런 경험을 했습니다.
1. 기존 배포 방식의 단점
이전에 저는 docker + GitHub Actions + ECS + ECR 조합으로 배포 파이프라인을 구축했습니다. 이 방식은 코드를 푸시하는 순간 자동으로 배포가 이루어지고 github에서 종합적으로 관리할 수 있다는 장점이 있었죠. 하지만 실제로 develop 브랜치에 푸시되고 github action으로 docker 이미지가 빌드되고, ECS의 Task가 생성될 때까지, 즉, 서버에 변경사항이 반영되기까지 무려 5분이나 걸렸습니다.
처음에는 '자동화됐으니 괜찮아'라고 생각했지만, 점점 이 긴 대기 시간(코드를 변경하고 서버에 반영되는 시간)이 답답하게 느껴졌습니다. 특히 릴리즈 환경이 아니라 소규모로 개발하는 경우에는 이 5분의 대기 시간이 개발 속도에 상당한 영향을 미쳤습니다. 작은 변경사항 하나 를 커밋하고 테스트하려고 해도 커피 한 잔 마시고 올 시간이 있었으니까요.
물론 여러 명이 협업할 때는 이런 자동화된 파이프라인이 분명한 장점이 있습니다. 하지만 개인 프로젝트나 빠른 개발이 필요한 상황에서는 오히려 족쇄가 되는 느낌이었습니다.
2. 새로운 배포 방식: ssh config와 shell script의 활용
이러한 문제를 해결하기 위해 저는 좀 더 단순하면서도 빠른 배포 방식을 고안했습니다. 바로 ssh config와 shell script를 활용한 방법입니다.
새로운 방식의 흐름은 다음과 같습니다:
1. 로컬 환경에서 Docker 이미지를 빌드합니다. (I love M2 Pro 맥북❤️)
2. 빌드된 이미지를 Docker hub에 푸시합니다.
3. ssh를 통해 AWS EC2 서버에 원격으로 접속합니다.
4. 서버에서 최신 Docker 이미지를 풀(pull)받습니다.
5. 풀받은 이미지로 컨테이너를 실행합니다.
이 모든 과정을 shell script로 자동화했더니, 놀랍게도 전체 배포 시간이 28초 정도로 줄어들었습니다! 기존 방식의 약 5분(300초)에서 1/10 수준으로 단축된 것이죠.
28초 ㅁㅊㄷ..!
3. 결론
개발 환경과 배포 방식은 항상 트레이드오프가 있습니다. 팀 규모, 프로젝트의 성격, 개발 단계 등에 따라 최적의 방식이 달라질 수 있습니다. 이번 경험을 통해 저는 상황에 맞는 유연한 접근이 중요하다는 것을 깨달았습니다. 때로는 '최신' 기술 스택보다 단순하지만 효율적인 방식이 더 나을 수 있다는 것도 알게 되었고요.
여러분도 현재 사용 중인 개발 및 배포 프로세스를 한 번 점검해보는 것은 어떨까요? 어쩌면 작은 변화로 큰 효율을 얻을 수 있을지도 모릅니다.
인프라에 대해 공부해보다가 Go + Docker + ECS + Fargate + ECR + Github Action으로 CI/CD를 구성했다는 블로그 글을 보고, 따라해 보았다.
2. Dockerfile 만들기
Dockerfile은 Docker 이미지를 생성하기 위한 설정 파일입니다. Go + gin backend를 위한 Dockerfile을 만들어 보겠습니다.
FROM golang:alpine AS builder
ENV GO111MODULE=on \
CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64
WORKDIR /build
COPY go.mod go.sum main.go ./
RUN go mod download
RUN go build -o main .
WORKDIR /dist
RUN cp /build/main .
FROM scratch
COPY --from=builder /dist/main .
ENTRYPOINT ["/main"]
3. ECS 만들기 (Fargate vs EC2)
Amazon ECS(Elastic Container Service)는 컨테이너화된 애플리케이션을 쉽게 실행하고 관리할 수 있는 완전 관리형 컨테이너 오케스트레이션 서비스입니다. ECS를 사용할 때 두 가지 주요 실행 모드가 있습니다 (Fargate, EC2). 생성할 때 두 개 중 하나를 선택할 수 있습니다. 저는 Fargate를 선택했습니다.
Fargate
서버리스 옵션으로, 인프라 관리 없이 컨테이너를 실행할 수 있습니다. 확장성이 뛰어나고 관리가 쉽습니다.
EC2
사용자가 직접 EC2 인스턴스를 관리하며, 더 많은 제어와 커스터마이징이 가능합니다.
예전에는 EC2 + Code Deploy 조합으로 배포 인프라 구성을 하곤 했는데, 이번에는 Fargate를 시도해보고자 합니다.
Fargate는 인프라 관리 부담을 줄이고 싶은 경우에 적합하며, EC2는 더 세밀한 제어가 필요한 경우에 선택한다고 합니다.
클러스터 (Cluster) 개념
ECS의 최상위 리소스 그룹입니다. 여러 개의 EC2 인스턴스 또는 Fargate 작업을 논리적으로 그룹화합니다. (저는 Fargate를 사용했습니다.) 한 클러스터 내에서 여러 서비스와 태스크를 실행할 수 있습니다.
태스크 (Task) 개념
애플리케이션의 최소 배포 단위입니다. 하나 이상의 컨테이너로 구성됩니다. 태스크 정의(Task Definition)를 통해 실행할 컨테이너, 사용할 리소스 등을 정의합니다.
서비스 (Service)
지정된 수의 태스크를 클러스터에서 동시에 실행하고 관리합니다. 태스크가 중지되면 자동으로 새 태스크를 시작하여 지정된 개수를 유지합니다. (저는 1개를 지정해서 최대 1개만 실행됩니다.) 로드 밸런서와 연결하여 트래픽을 분산할 수 있다고 합니다. (요금 주의)
컨테이너 (Container)
Docker 이미지를 기반으로 실행되는 격리된 실행 환경입니다. (ECR에 push하고 태그로 관리됩니다) go + gin 으로 구성된 프로젝트를 Docker로 빌드했습니다. 태스크 내에서 하나 이상의 컨테이너가 함께 실행될 수 있습니다.
4. 인바운드 규칙 편집
8080 포트 열기 (인바운드 규칙 편집)
ECS 태스크가 실행되는 Fargate 태스크에 연결된 보안 그룹의 인바운드 규칙을 편집해야 합니다. ECS -> 클러스터 -> 서비스 -> 보안 그룹으로 이동합니다. 해당 보안 그룹을 선택하고 "인바운드 규칙 편집"을 클릭합니다.
유형: 사용자 지정 TCP 포트 범위: 8080 소스: 필요에 따라 특정 IP 범위 또는 모든 트래픽(0.0.0.0/0)을 선택
저는 8080포트를 열었으므로, 8080포트를 외부에서 접근 가능하도록 개방했습니다.
이렇게 설정하면 외부에서 8080 포트를 통해 ECS에서 실행 중인 go backend app에 접근할 수 있게 됩니다. 단, 보안을 위해 가능한 한 접근을 제한적으로 설정하는 것이 좋습니다. 또한, 태스크 정의에서도 컨테이너 포트 매핑을 설정해야 합니다. 태스크 정의의 컨테이너 설정에서 8080 포트를 호스트 포트와 매핑해야 합니다.
4. ECR 만들기 (Docker 이미지 저장소)
Amazon ECR(Elastic Container Registry)은 Docker 컨테이너 이미지를 안전하게 저장, 관리 및 배포할 수 있는 완전 관리형 Docker 컨테이너 레지스트리입니다. ECR을 사용하면 프라이빗 레포지토리를 생성하여 Docker 이미지를 저장하고, ECS와 쉽게 통합할 수 있습니다.
5. Github Action (develop branch에 머지 시 워크플로우 실행)
develop 브랜치에 머지가 되었을 때 (이벤트 발생), Docker 이미지가 빌드되고 ECR에 push(업로드)된 다음에, 해당 도커 이미지를 ECS에서 실행되게 할 것입니다. (8080포트로 접근 가능하게)
GitHub Actions를 사용하면 이러한 CI/CD 파이프라인을 구축할 수 있습니다. develop 브랜치에 머지될 때 자동으로 빌드, 테스트, 배포 과정을 실행하도록 설정할 수 있습니다.
aws.yml 코드
name: Deploy to Amazon ECS
on:
push:
branches: [ "develop" ]
env:
AWS_REGION: ap-northeast-2 # set this to your preferred AWS region, e.g. us-west-1
ECR_REPOSITORY: fye-backend-repository # set this to your Amazon ECR repository name
ECS_SERVICE: fye-backend-service # set this to your Amazon ECS service name
ECS_CLUSTER: fye-cluster # set this to your Amazon ECS cluster name
ECS_TASK_DEFINITION: ./fye-task.json # set this to the path to your Amazon ECS task definition
# file, e.g. .aws/task-definition.json
CONTAINER_NAME: fye-backend-container # set this to the name of the container in the
# containerDefinitions section of your task definition
permissions:
contents: read
jobs:
deploy:
name: Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
- 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: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
# Build a docker container and
# push it to ECR so that it can
# be deployed to ECS.
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ env.ECS_TASK_DEFINITION }}
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
상단에 있는 env: 부분에서 지역과 ecs, ecr 관련 정보들을 넣습니다. json은 태스크 탭에서 다운로드할 수 있습니다. 그리고 iam을 생성해서 github secrets에 access key와 secret key를 등록하여 github action이 aws의 다양한 서비스를 조작할 수 있게 설정합니다. 저는 iam에 아래의 6개 권한을 추가했습니다. 그리고 .csv로 추출하여 github의 secret에 키를 등록했습니다.
6개 권한 추가인프라 구성 성공!, 수많은 시도들..
6. 마무리 및 소감
No Docker + Code Deploy로 했을 때는 1~2분이면 서버 배포가 끝났는데, Docker + ECS로 해보니 생각보다 오래 걸려서 당황했다..
Docker Build 하는데만 48s, Deploy ECS Task Definition 하는 데만 4m 16s가 걸렸다. 총 5분..
다수가 협업할 때는 장점이 충분히 있을 것 같지만, 1인이나 소규모로 할 때는 No Docker + Code Deploy를 사용하는 게 속도면에서 좋을 것 같다.
아침은 든든한 황태국으로 시작했어요. 영양 가득한 식사로 하루를 열고, 우리 가족은 숙소에서 30분 거리인 선유도로 향했습니다. 선유도의 아름다운 자연 경관은 정말 감탄이 절로 나왔어요. 자연 그대로의 소금기를 담은 공기와 피톤치드를 마시며 몸과 마음이 회복되는 기분이었죠. 선유도에서는 여유롭게 드라이브를 즐기며 곳곳에서 사진을 찍었어요. 유람선을 탈까 고민했지만, 결국 차로 돌아다니는 것으로 만족했답니다. 차 안에서 보는 풍경도 충분히 아름다웠거든요.
2. 오후: 맛있는 생선과 리트리버
점심 시간이 되어 격포해수욕장과 채석강으로 향했어요. 더운 날씨에 시원한 슬러시로 더위를 식히며 걸어다녔어요. 점심은 근처 식당에서 고등어와 가자미 생선 정식을 먹었는데, 정말 맛있었어요.
식사 후 근처 카페에 들렀는데, 카페 안에 진짜 몸집이 큰 리트리버가 낮잠을 자고 있었거든요! 너무 놀라서 안에서 먹으려다가 가지고 나가기로 결정했답니다!
3. 저녁: 탁구 & 김치찌개
숙소로 돌아와 잠깐 낮잠을 자고 있는데, 엄마가 갑자기 탁구를 치자고 하셨어요. 숙소에 탁구장이 있어서 탁구 대결로 오후 시간을 보냈죠.
저녁으로는 직접 끓인 김치찌개와 배달의 민족으로 주문한 BHC 치킨을 먹었어요. 여행 중이지만 집에서 먹는 것 같은 편안함이 좋았답니다.
4. 예상치 못한 업무
여행 중에도 일을 했다고요? 네, 맞아요. 원래 이번 달 초까지 해야 했었던 중요한 업무가 7월 말까지로 연장되었는데, 오늘이 월요일인지라 외부에서 업무 요청이 여러 번 왔었어요. 휴가라 일을 해야하나 싶었지만, 이미 오늘 놀건 다 놀았고 회사에서 중요한 프로젝트니, 개발 & 배포하고 저녁에는 숙소에서 업무에 집중했네요. 다행히 성공적으로 마무리 했답니다! 하지만 그래도 오늘 충분히 즐거운 여행이었어요. 일과 삶의 균형을 찾는 것, 어렵지만 중요한 것 같아요. 내일은 마지막 날이니 적극적으로 숙소 주변을 돌아다녀봐야겠어요. 내일은 3일차 이야기로 찾아뵐게요. 다들 좋은 밤 되세요!
안녕하세요, 오늘은 가족들과 함께 떠난 변산 여행 2박 3일 여행의 첫날 후기를 소개해드리려고 합니다. (feat. NH농협생명 변산수련원)
1. 오전 - 금산 출발
아침 일찍 금산을 출발했습니다. 가는 길에 눈에 띄는 점이 있었어요. 도로 위에 흙이 쌓여있고, 인삼농사 막이 무너져 있는 모습이 보였습니다. 포크레인도 유난히 많이 보였죠.
궁금해서 인터넷으로 뉴스를 찾아보니, 최근 집중호우로 인해 금산 지역에 큰 피해가 있었다고 합니다. 공공시설 354건, 사유시설 350건의 피해가 접수되었고, 총 피해액이 357억 원에 달한다고 하네요. 우리가 출발할 때 보았던 모습들이 바로 이 피해의 흔적이었던 거죠.
2. 점심 - 변산 도착
점심 무렵 변산에 도착했습니다. 배가 고파 먼저 근처 맛집인 군산식당으로 향했어요. 이곳에서 꽃게탕과 백합찜을 주문했습니다. 꽃게탕은 처음 먹어보는 메뉴였는데, 정말 담백한 맛이 맛있었어요. 사장님 말씀으로는 담백한 맛을 유지하기 위해 오전에 미리 한 번 끓여놓은 거라고 하더라고요. 백합찜도 처음 먹어봤는데, 속살이 정말 알찼습니다.
3. 오후 - 체크인과 내소사 방문
식사 후 NH농협생명 변산수련원에 체크인했습니다. 우리 일행은 두 개의 방을 잡았어요. 시설이 좋았고, 바로 앞에 바다가 보이는 전망도 정말 좋았습니다.
잠깐의 낮잠 후, 우리는 내소사로 향했습니다. 내소사는 드라마 '대장금' 촬영지로도 유명하다고 하더라고요. 절에 들어서자마자 피톤치드가 충전되는 기분이였어요. 마음이 편안해지는 기분이었습니다. 다만 일요일 오후라 그랬는지 주변 상가들은 대부분 문을 닫았어요. 오디 음료? 파는 곳이 몇개 있더라고요. (오디 농사를 짓나?)
Golang으로 대량 API 요청 처리: Excel 파일에서 데이터 읽기부터 병렬 처리까지
안녕하세요, 오늘은 Go 언어를 사용하여 Excel 파일에서 데이터를 읽어와 대량의 API 요청을 처리하는 방법에 대해 알아보겠습니다. 이 프로그램은 입력할 Excel 파일에서 데이터를 읽어와 body에 넣어서 API에 요청을 보내고, 응답을 파일에 저장하는 과정을 수행합니다.
1. 과정
Excel 파일에서 데이터 추출하기 (API request body에 넣을 값들)
API 요청 (병렬 처리를 위한 고루틴 사용 - WaitGroup)
응답 데이터 정렬 by index
결과를 파일로 저장 (csv)
로깅 (응답 데이터, 응답 시간 로그 파일 생성)
2. Excel 파일에서 원하는 데이터 추출하기
먼저, Excel 파일에서 데이터를 읽어오는 코드를 만듭니다.
package main
import (
"encoding/csv"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"time"
"github.com/tealeg/xlsx"
)
const (
FILE_PATH = "./sample.xlsx"
SHEET_INDEX = 1
COLUMN = 'F'
START_ROW = 2
END_ROW = 186
PREVIEW_LENGTH = 500
)
func currentDateString() string {
return time.Now().Format("20060102")
}
func readExcelColumn(filePath string, column rune, startRow, endRow int) ([]string, error) {
xlFile, err := xlsx.OpenFile(filePath)
if err != nil {
return nil, fmt.Errorf("error: file '%s' not found", filePath)
}
if SHEET_INDEX >= len(xlFile.Sheets) {
return nil, fmt.Errorf("error: sheet index out of range")
}
sheet := xlFile.Sheets[SHEET_INDEX]
columnIndex := int(column - 'A')
var columnData []string
for i := startRow - 1; i < endRow; i++ {
if i >= len(sheet.Rows) {
break
}
cell := sheet.Rows[i].Cells[columnIndex]
columnData = append(columnData, cell.String())
}
return columnData, nil
}
func processCellData(cellData string) string {
parts := strings.Split(cellData, "\n")
if len(parts) > 1 {
return strings.TrimSpace(parts[1])
}
return strings.TrimSpace(parts[0])
}
func processColumnData(columnData []string) []string {
var processedData []string
for _, cell := range columnData {
processedData = append(processedData, processCellData(cell))
}
return processedData
}
func saveToCSV(data []string, outputFile string) error {
file, err := os.Create(outputFile)
if err != nil {
return fmt.Errorf("error writing to file: %v", err)
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
for _, item := range data {
if err := writer.Write([]string{item}); err != nil {
return fmt.Errorf("error writing to file: %v", err)
}
}
fmt.Printf("Data has been saved to %s\n", outputFile)
return nil
}
func previewFile(filePath string, length int) error {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return fmt.Errorf("error reading file for preview: %v", err)
}
fmt.Println("Preview of saved data:")
fmt.Println(string(data)[:length])
return nil
}
func main() {
currentDate := currentDateString()
outputFile := fmt.Sprintf("./body_%s.csv", currentDate)
columnData, err := readExcelColumn(FILE_PATH, COLUMN, START_ROW, END_ROW)
if err != nil {
log.Fatalf("Failed to read Excel column: %v", err)
}
if len(columnData) == 0 {
log.Println("No data to process. Check the Excel file and column specification.")
return
}
processedData := processColumnData(columnData)
if err := saveToCSV(processedData, outputFile); err != nil {
log.Fatalf("Failed to save to CSV: %v", err)
}
if err := previewFile(outputFile, PREVIEW_LENGTH); err != nil {
log.Fatalf("Failed to preview file: %v", err)
}
}
3. 읽어온 데이터를 바탕으로 다수의 API 요청 보내기
WaitGroup을 사용해서 동시성을 활용해 API를 병렬적으로 실행합니다.
동시 작업 추적
WaitGroup은 완료되어야 할 고루틴(goroutine)의 수를 추적합니다. 이는 여러 고루틴이 병렬로 실행될 때 유용합니다.
작업 완료 대기
Main Go routine 이 모든 작업이 완료될 때까지 기다릴 수 있게 해줍니다. 이를 통해 모든 병렬 작업이 끝날 때까지 프로그램이 종료되지 않도록 합니다.
카운터 메커니즘
WaitGroup은 내부적으로 카운터를 사용합니다. 이 카운터는 다음 메서드들로 조작됩니다:
- Add(delta int): 대기해야 할 고루틴의 수를 증가시킵니다. - Done(): 고루틴 하나가 완료되었음을 알립니다 (내부적으로 Add(-1)과 동일). - Wait(): 카운터가 0이 될 때까지 블록합니다.
그리고 1번 요청을 먼저 했다고 해도 30번째 요청의 응답이 먼저 들어올 수 있으므로 index를 지정해서 모든 요청이 완료된 순간, index를 기준으로 오름차순 정렬을 하게 했습니다.
이로써, 동시성 프로그래밍을 활용하여 30개의 API 요청 시 응답시간을 270s -> 9s로 (1개 요청 시 보통 9s) 시간을 단축하였습니다.
package main
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"
"strconv"
"sync"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// 상수 정의
const (
INPUT_FILE = "body_20240714.csv" // 입력 CSV 파일 이름
API_URL = "http://localhost:8000/api/v1/xxxx" // API URL
ASSISTANT_CONTENT = "" //
DATE_FORMAT = "20240712" // 날짜 형식
OUTPUT_CSV_FILE = "response_20240712.csv" // 출력 CSV 파일 이름
)
// CSV 파일 읽기 함수
func readCSV(filename string, logger *zap.Logger) ([]string, error) {
logger.Info("Reading CSV file", zap.String("filename", filename)) // CSV 파일 읽기 시작 로그
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("Error opening file: %w", err) // 파일 열기 오류 시 에러 반환
}
defer file.Close()
reader := csv.NewReader(file)
var data []string
for {
record, err := reader.Read()
if err != nil {
break // 더 이상 읽을 레코드가 없으면 종료
}
if len(record) > 0 {
data = append(data, record[0]) // CSV 레코드의 첫 번째 필드만 저장
}
}
logger.Info("Successfully read records from CSV file", zap.Int("records", len(data))) // 성공적으로 읽은 레코드 수 로그
return data, nil
}
// API 요청 함수
func makeAPIRequest(userContent string, wg *sync.WaitGroup, results chan<- [2]string, index int, logger *zap.Logger) {
defer wg.Done() // 작업이 완료되면 WaitGroup의 작업 카운터를 줄임
logger.Info("Making API request", zap.Int("index", index), zap.String("userContent", userContent)) // API 요청 시작 로그
startTime := time.Now() // 요청 시작 시간 기록
requestData := []map[string]string{
// 원하는 데이터 요청 형식
}
jsonData, err := json.Marshal(requestData)
if err != nil {
results <- [2]string{fmt.Sprintf("Error marshalling JSON: %v", err), fmt.Sprintf("%d", index)}
logger.Error("Error marshalling JSON", zap.Int("index", index), zap.Error(err)) // JSON 변환 오류 로그
return
}
req, err := http.NewRequest("POST", API_URL, bytes.NewBuffer(jsonData))
if err != nil {
results <- [2]string{fmt.Sprintf("Error creating request: %v", err), fmt.Sprintf("%d", index)}
logger.Error("Error creating request", zap.Int("index", index), zap.Error(err)) // HTTP 요청 생성 오류 로그
return
}
req.Header.Set("Content-Type", "application/json") // 요청 헤더 설정
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
results <- [2]string{fmt.Sprintf("Error making API request: %v", err), fmt.Sprintf("%d", index)}
logger.Error("Error making API request", zap.Int("index", index), zap.Error(err)) // API 요청 오류 로그
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
results <- [2]string{fmt.Sprintf("Error reading response: %v", err), fmt.Sprintf("%d", index)}
logger.Error("Error reading response", zap.Int("index", index), zap.Error(err)) // 응답 읽기 오류 로그
return
}
var response map[string]interface{}
if err := json.Unmarshal(body, &response); err != nil {
results <- [2]string{fmt.Sprintf("Invalid response format: %s", string(body)), fmt.Sprintf("%d", index)}
logger.Error("Invalid response format", zap.Int("index", index), zap.String("body", string(body))) // 응답 형식 오류 로그
return
}
if output, ok := response["output"].(string); ok {
results <- [2]string{output, fmt.Sprintf("%d", index)}
duration := time.Since(startTime) // 요청 소요 시간 계산
logger.Info("Received valid response", zap.Int("index", index), zap.String("response", output), zap.Duration("duration", duration)) // 유효한 응답 로그 및 소요 시간 기록
} else {
results <- [2]string{"No output field in response", fmt.Sprintf("%d", index)}
logger.Warn("No output field in response", zap.Int("index", index)) // 응답에 output 필드가 없음 로그
}
}
// CSV 파일에 데이터 저장 함수
func saveToCSV(filename string, data [][2]string, logger *zap.Logger) error {
logger.Info("Saving data to CSV file", zap.String("filename", filename)) // CSV 파일 저장 시작 로그
file, err := os.Create(filename)
if err != nil {
return fmt.Errorf("Error creating CSV file: %w", err) // 파일 생성 오류 시 에러 반환
}
defer file.Close()
writer := csv.NewWriter(file)
defer writer.Flush()
for _, value := range data {
if err := writer.Write([]string{value[0]}); err != nil {
return fmt.Errorf("Error writing record to CSV file: %w", err) // 레코드 작성 오류 시 에러 반환
}
}
logger.Info("Successfully saved data to CSV file") // 데이터 저장 성공 로그
return nil
}
func fileLogger(filename string) *zap.Logger {
config := zap.NewProductionEncoderConfig()
config.EncodeLevel = zapcore.CapitalColorLevelEncoder
config.EncodeTime = zapcore.ISO8601TimeEncoder
fileEncoder := zapcore.NewJSONEncoder(config)
consoleEncoder := zapcore.NewConsoleEncoder(config)
logFile, _ := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
writer := zapcore.AddSync(logFile)
defaultLogLevel := zapcore.DebugLevel
core := zapcore.NewTee(
zapcore.NewCore(fileEncoder, writer, defaultLogLevel),
zapcore.NewCore(consoleEncoder, zapcore.AddSync(os.Stdout), defaultLogLevel),
)
logger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
return logger
}
func main() {
filename := "logs.log"
logger := fileLogger(filename)
defer logger.Sync()
var startIndex, endIndex int
var err error
// 터미널에서 시작 인덱스와 끝 인덱스를 입력받음
fmt.Print("startIndex(0~184): ") // 0 -> 2
_, err = fmt.Scan(&startIndex)
if err != nil {
logger.Fatal("Error reading start index", zap.Error(err)) // 시작 인덱스 읽기 오류 시 프로그램 종료
}
fmt.Print("endIndex(1~185): ")
_, err = fmt.Scan(&endIndex)
if err != nil {
logger.Fatal("Error reading end index", zap.Error(err)) // 끝 인덱스 읽기 오류 시 프로그램 종료
}
// CSV 파일에서 질문 읽기
questions, err := readCSV(INPUT_FILE, logger)
if err != nil {
logger.Fatal("Error reading CSV file", zap.Error(err)) // CSV 파일 읽기 오류 시 프로그램 종료
}
// 끝 인덱스가 0이거나 질문 수보다 큰 경우 질문 수로 설정
if endIndex == 0 || endIndex > len(questions) {
endIndex = len(questions)
}
var wg sync.WaitGroup
results := make(chan [2]string, endIndex-startIndex)
// 시작 인덱스부터 끝 인덱스까지 API 요청 생성
for i := startIndex; i < endIndex; i++ {
wg.Add(1) // WaitGroup 카운터 증가
go makeAPIRequest(questions[i], &wg, results, i, logger) // 고루틴으로 API 요청 실행
}
wg.Wait() // 모든 고루틴이 완료될 때까지 대기
close(results) // 채널 닫기
var responseData [][2]string
for response := range results {
responseData = append(responseData, response) // 응답 데이터 수집
}
// 응답 데이터를 index 기준으로 오름차순 정렬
sort.Slice(responseData, func(i, j int) bool {
indexI, _ := strconv.Atoi(responseData[i][1])
indexJ, _ := strconv.Atoi(responseData[j][1])
return indexI < indexJ
})
// 정렬된 데이터의 텍스트 내용만 CSV 파일에 저장
var sortedTextData [][2]string
for _, value := range responseData {
sortedTextData = append(sortedTextData, [2]string{value[0]})
}
// 응답 데이터를 CSV 파일에 저장
if err := saveToCSV(OUTPUT_CSV_FILE, sortedTextData, logger); err != nil {
logger.Fatal("Error saving to CSV", zap.Error(err)) // CSV 저장 오류 시 프로그램 종료
}
logger.Info("Processing complete", zap.String("outputFile", OUTPUT_CSV_FILE)) // 처리 완료 로그
fmt.Printf("Processing complete. Results saved to %s\n", OUTPUT_CSV_FILE) // 처리 완료 메시지 출력
}
간단한 백엔드를 만들일이 있어서 고민을 하다가, 유튜브 알고리즘에 뜬 아래 두 동영상을 보고 Golang Backend를 공부해야겠다고 다짐했습니다. 약 4년전, 공군 개발병 복무 시절, Golang + go gin 으로 간단한 REST API를 사지방에서 만들어보고 초당 몇번의 요청이 가능한지 테스트해보고 빨라서 놀랐던 기억이 있었는데, 그 시절이 떠오르네요. [Golang 도입, 그리고 4년 간의 기록 / Golang과 함께 서버 레이턴시를 500배 개선한 후기] https://www.youtube.com/watch?v=75X_eBW0mog
간단한 백엔드 개발을 위한 언어를 선택하는 과정에서 가장 중요하게 생각한 것은 성능이 빠른지와 단순한지였습니다. Golang은 다음과 같은 매력적인 장점들이 있습니다.
빠른 컴파일 속도 Golang의 빠른 컴파일 속도는 개발 생산성을 크게 향상시킵니다. 공군 시절 Spring 프로젝트를 수정하고 컴파일 하는데 1분 가까이 걸려서 멍때렸던 기억이 있었는데, 아직 경험해보지 못했지만 go는 프로젝트가 커져도 빠른 컴파일이 된다고 합니다. 뛰어난 동시성 처리 Go routine과 채널을 통한 동시성 제어는 Golang의 큰 장점 중 하나입니다. 요즘 Sass(Software as a service)가 많아지면서 Third party app(GPT etc..)과 API 연동할 일이 많은데 이때 강점을 가집니다. 언어의 단순성 간결하고 읽기 쉬운 문법으로 빠르게 학습할 수 있습니다. 사실 다른 빠른 언어들도 있지만, 개인적으로 go 언어가 이해가 잘되었습니다. 성능 최적화 메모리와 CPU 사용을 쉽게 최적화할 수 있다고 합니다. 고성능 애플리케이션 개발에 많이 사용된다고 합니다.
물론, Golang에도 몇 가지 단점이 있습니다..
에러 처리의 복잡성 모든 에러를 명시적으로 처리해야 해서 코드가 길어질 수 있습니다. GC 관련 이슈 대규모 시스템에서는 가비지 컬렉션으로 인한 성능 저하가 발생할 수 있습니다. 제네릭 지원 부족 제네릭 기능의 부재로 인해 코드 재사용성이 떨어질 수 있습니다. 특정 패턴 강제 에러 처리 등에서 특정 패턴을 따라야 하는 경우가 많습니다.
3. 선택한 기술 스택
Golang 커뮤니티를 탐색하면서 많은 개발자들이 추천하거나 github star가 많은 라이브러리를 찾았습니다. 제가 선택한 기술 스택은 다음과 같습니다:
Go Gin 경량화되고 빠른 웹 프레임워크로, RESTful API 개발에 적합합니다. SQLC SQL 쿼리를 Go 코드로 변환해주는 도구로, 타입 안정성과 성능을 모두 잡을 수 있습니다. PostgreSQL with RDS 요즘 많이 쓰는 관계형 데이터베이스입니다. Docker 개발 환경의 일관성을 유지할 수 있습니다. Github Action CI/CD easy..!
4. 특이했던 점
NestJS에서 사용했었던 schema.prisma처럼 migration 폴더에 데이터베이스 정보(PK, FK, Constraint, relation, type 등)를 기술하는 것. @Controller, @Service 같은 어노테이션이 없는것. pointer star (*), & 등 C 같은 느낌. 특이한 for문 등. 내일은 AWS RDS 데이터베이스 만들고, 연결해봐야겠다. 그리고 메모장같은 간단한 CRUD를 만들어보고 GPT나 Claude API 연동 시도해봐야겠다..!
안녕하세요! 다들 오늘 하루 어떻게 보내셨나요? 제 오늘은 정말 다채로운 하루였습니다. 평소에는 다들 재택근무를 해서 회사에 가지 않다가, 오늘은 전사회의가 있는 날이라 모두가 출근했어요. 그래서 다양한 일정들과 활동이 겹치면서 정신없이 지나갔네요. 오래만에 이렇게 일정이 많은 날도 있구나 싶어 기록해두고 싶어졌습니다.
구글에서 가져온 이미지
1. 점심 식사
요즘 장마철이라 계속 비가 와서 습하고, 날씨가 좋지 않았는데, 다행히 오늘은 맑았어요. 오랜만에 직원분들이 한자리에 모여 점심을 먹었습니다. 인턴 분이 추천한 지하에 있는 한식집에 갔는데, 반찬도 푸짐하고 은근히 맛있더라고요. 오랜만에 모여서 그런지 다들 못다한 이야기를 했습니다. (컨퍼런스 다녀온 이야기, 근황, 하는 일은 어떤지.. 등)
김치찌개 + 제육볶음 👍
2. 즉흥적인 탁구 경기
점심 식사 후에 카페를 갈까 하다가 갑자기 회사 옥상에 있는 탁구대가 생각나서 셋이서 탁구를 치기로 했어요. 놀랍게도 다들 실력이 대단하더라고요. 3명이 번갈아가며 11점 내기로 2판씩 했는데, 커트, 드라이브를 너무 잘하셨어요. 제가 생각보다 실력이 부족하다는 걸 깨달았습니다. 드라이브가 자꾸 공이 떠서 넘어가서 실점을 많이했습니다. 연습을 더 해야겠어요. 🥲
탁구대
3. 산업안전보건교육
오후 1시 30분부터는 외부 기업에서 진행하는 산업안전보건교육을 들었습니다. (국가에서 주관?) 건강박수 같은 레크레이션이나 재테크 지식(절세 등) 공유 같은 부분은 재미있었는데, 마지막에 연금 보험 상품 가입을 권유하셨는데 개인적으로는 보험보다 미국 주식 투자가 더 매력적이게 보여서 가입은 사양했습니다.
4. 전사회의
3시 30분부터는 전사회의가 있었습니다. 회사의 방향성과 계획, 그리고 우리가 만들어가야 할 조직문화에 대해 이야기를 나눴어요. 건의사항도 듣고 앞으로의 계획도 공유하는 시간이었습니다.
5. 저녁 식사
5시 30분에는 우리 회사 합류를 고민 중인 외국인 연구원분과 직원분들과 함께 인도 음식점에서 저녁을 먹었어요. 식당에서는 카잔 케밥을 처음 먹어봤는데 고기가 정말 맛있더라고요.박사님의 연구실이 제가 학부 때 수업을 들었던 과목의 교수님과 같다는 걸 알고 놀랐습니다. 그리고 대학원 생활에 대한 이야기를 영어로 해주시는데, 발음이 생각보다 좋으셔서 꽤 알아들을 수 있었습니다. 다만 영어로 대화하는 게 익숙하지 않아 듣기만 하고, 영어로 많은 말을 하지 못한 게 아쉬웠습니다. 영어 말하기를 더 공부해야겠어요. (OPIC?)
"nice to meet you, i'm cheolheeLee" "i have a question!!" "how long does it take to publish the paper?" "is it related with XXX?, there are lots of fund" "oh yes, yes" (제일 많이 한 듯..) "my second major is mobility software and AI too." "See you soon" "Bye, Bye" "Good luck" ...
사실 머리에 떠오르는 구체적인 질문은 더 많았지만, 질문할 타이밍이 안맞아서 입으로 뱉지 못해서 아쉬웠습니다.
6. NestJS 백엔드 오프라인 스터디
마지막으로 저녁 7시 30분부터는 NestJS 백엔드 오프라인 스터디에 참석했어요. 다양한 배경을 가진 분들과 IT 관련 이야기를 나누며 즐거운 시간을 보냈습니다. 특히 교통사고를 당하셔서 생사에 기로에 놓이셨다가, 회복하고 나신 후에 코딩을 배우시는 분의 열정에 감동받았어요. 상대적으로 나이 때문에 스터디에 참가하시기 주저하셨지만, 실리콘밸리에서는 50대도 공부하신대요 라고 위로해드렸습니다 👏. 또 "병원 생활 4년 사이에 IE가 사라졌어요"(인터넷 익스플로러)는 말을 "아이가 사라졌다" (아이 실종..!)는 말로 잘못 들어서 웃겼었던!
7. 마무리
이렇게 점심회식, 탁구 대결, 기업교육, 전사회의, 저녁회식, 스터디까지... 정말 많은 일정을 소화한 하루였습니다. 피곤하긴 하지만, 이렇게 다양한 경험을 할 수 있어 기억에 남는 하루였네요. 앞으로도 이런 날들이 가끔은 있었으면 좋겠습니다. 읽어주셔서 감사합니다 🙇♂️
안녕하세요, 여러분! 👋 오늘은 제가 트위터를 구경하다 우연히 발견한 신기하고 유용한 정보를 여러분과 공유하려고 해요. 2024년에 인기있는 60가지 AI 도구들이에요! 🚀💼 저도 지금 하나씩 사용해보고 있는 중인데, 몰랐던 도구들이 진짜 많기도하고 신기해서 여러분께 꼭 알려드리고 싶었어요! (제가 사용해본 것 형광펜 표시해볼게요)
1. 아이디어 발상 도구들 💡
Claude, ChatGPT, Gemini, Bing Chat, Perplexity 같은 AI 챗봇들이 아이디어 뱅크 역할을 해줘요! 상상력의 한계를 넓혀보세요!
2. 웹사이트 제작 도구들 🌐
@wegic_ai, Unicorn, 10Web, Framer, Dora로 멋진 웹사이트를 뚝딱 만들 수 있어요. 코딩 실력이 없어도 괜찮아요!
요즘 이 도구들을 하나씩 사용해보면서 매일 새로운 인사이트를 발견하고 있어요. ("AI로 이런 기능도 만들 수 있구나!, 어떻게 개발했지..?") 여러분도 한번 도전해보세요! 처음엔 어색할 수 있지만, 조금씩 익숙해지다 보면 어느새 AI 마법사가 되어 있을 거예요. 😉🧙♂️
코딩 입문 때부터 계속 쓰던 VS Code에서 벗어나기가 두려웠지만, 한 번 사용해보자는 실험 정신도 있었고 열정적인 추천에 솔깃해서 바로 구독하고 설치해봤는요. 일하다 말고 툴을 바꾸는 게 좀 도전적이라고 생각했지만.. 근데 VS code의 기능과 UI/UX가 매우 비슷해서 이질감을 못느꼈어요. 코드 생성형 AI 모델 선택도 가능하더라고요. Claude-3.5도 지원이 되어서 최근에 핫한 이 모델을 선택해서 쓰고 있어요.
3. 좋았던 점
명령어 생성 및 터미널 디버깅
기존에는 에러가 발생하면 터미널 내용을 복사해서 구글에 검색하곤 했는데, [Option + D] 단축키를 통해 터미널을 디버깅하고 설명해주는 게 편했어요.
터미널에 커서를 옮기고 [Command + K]를 누르면 아래 채팅이 뜨고 원하는 명령어를 입력하고 Enter를 누르면 터미널에 명령어가 작성됨.
변경하고 싶은 코드 부분을 드래고하고 [Command + L]를 누르면, 채팅 사이드바가 뜬다(코드가 입력됨).
코드가 결과로 나오고, 상단의 [Apply] 클릭하면 수정될 부분 보여주고, [Accept]/[Reject]를 선택할 수 있다.
4. 마무리
VS Code와 GitHub Copilot도 아직도 사람들이 많이 사용하는 유용한 툴이지만, Cursor IDE와 Claude 3.5 Sonnet 조합은 꽤 신선한 같아요. 앞으로 계속 사용할 것 같네요. 여러분도 한번 시도해보는 건 어떨까요?(+구독료는 덤) 새로운 경험에 푹 빠질지도 몰라요! 😉
Claude...! 너란 녀석
여러분은 어떤 도구를 쓰고 있나요? 각 도구의 좋았던 경험을 공유해주시면 정말 좋을 것 같아요! 자, 이제 저는 Cursor와 함께 코딩 여정을 시작하러 가볼게요. 다들 즐거운 코딩하세요! 읽어주셔서 감사합니다! 👋
아래 명령어에 파일명(./docker.sh)을 넣고 실행하면, 원격 레포지토리에 push 하면 해당 파일에 대한 기록이 모두 사라진다..! 실수로 중요한 정보를 원격 레포지토리에 올린 경우 유용하게 사용할 수 있을 것 같다. 그리고 .gitignore을 통해 원격 레포지토리에 올라가지 않도록 잘관리하자!
OpenAI는 실시간 대화, Q&A, 텍스트 생성 등을 지원하는 다중 모드 대형 언어 모델인 GPT-4o를 공개했습니다. OpenAI는 생성형 AI 시대를 주도하는 기업입니다. OpenAI의 성공과 인기를 이끄는 핵심은 GPT-3와 GPT-4를 포함한 대형 언어 모델(LLM)인 GPT 시리즈와 회사의 ChatGPT 대화형 AI 서비스입니다.
OpenAI는 2024년 5월 13일 봄 업데이트 행사에서 새로운 대표 멀티모달 언어 모델인 GPT-4 Omni(GPT-4o)를 발표했습니다. 행사에서는 모델의 직관적인 음성 응답 및 출력 기능을 보여주는 여러 비디오도 공개되었습니다.
GPT-4o란 무엇인가요?
GPT-4o는 OpenAI LLM 기술 포트폴리오의 대표 모델입니다. 여기서 O는 Omni를 의미하며, 이는 단순한 마케팅 용어가 아니라 텍스트, 비전, 오디오의 여러 매체를 아우르는 모델의 특성을 나타냅니다.
GPT-4o 모델은 OpenAI가 2023년 3월 처음 출시한 GPT-4 LLM의 새로운 진화를 보여줍니다. 이는 GPT-4의 첫 번째 업데이트도 아닌데, 2023년 11월에 GPT-4 Turbo가 처음 등장하며 모델이 한 차례 향상된 바 있습니다. GPT는 Generative Pre-Trained Transformer의 약자입니다. 트랜스포머 모델은 생성형 AI의 기본 요소로, 새로운 출력을 이해하고 생성할 수 있는 신경망 아키텍처를 제공합니다.
GPT-4o의 기능은 무엇인가요?
GPT-4o가 출시될 당시, 이 모델은 기능성과 성능 면에서 모든 OpenAI 모델 중 가장 뛰어났습니다. GPT-4o가 할 수 있는 여러 가지 기능은 다음과 같습니다:
실시간 상호작용: GPT-4o 모델은 실시간으로 지연 없이 음성 대화를 나눌 수 있습니다.
지식 기반 Q&A: 이전의 모든 GPT-4 모델과 마찬가지로, GPT-4o는 방대한 지식 기반을 학습하여 질문에 답변할 수 있습니다.
텍스트 요약 및 생성: GPT-4o는 텍스트 요약 및 생성 등 일반적인 텍스트 LLM 작업을 수행할 수 있습니다.
멀티모달 추론 및 생성: GPT-4o는 텍스트, 음성, 비전을 하나의 모델로 통합하여 다양한 데이터 유형을 처리하고 응답할 수 있습니다. 모델은 오디오, 이미지, 텍스트를 동일한 속도로 이해할 수 있으며, 오디오, 이미지, 텍스트로 응답을 생성할 수 있습니다.
언어 및 음성 처리: GPT-4o는 50개 이상의 다양한 언어를 처리할 수 있는 고급 기능을 가지고 있습니다.
감정 분석: 이 모델은 텍스트, 오디오, 비디오의 다양한 모달리티에서 사용자 감정을 이해할 수 있습니다.
음성의 뉘앙스: GPT-4o는 감정적인 뉘앙스를 담은 음성을 생성할 수 있어, 민감하고 뉘앙스 있는 소통이 필요한 응용 프로그램에 효과적입니다.
오디오 콘텐츠 분석: 이 모델은 음성 언어를 생성하고 이해할 수 있으며, 음성 인식 시스템, 오디오 콘텐츠 분석 및 인터랙티브 스토리텔링에 적용될 수 있습니다.
실시간 번역: GPT-4o의 멀티모달 기능은 한 언어에서 다른 언어로 실시간 번역을 지원할 수 있습니다.
이미지 이해 및 비전: 모델은 이미지와 비디오를 분석할 수 있으며, 사용자가 업로드한 시각적 콘텐츠를 이해하고 설명하며 분석할 수 있습니다.
데이터 분석: 비전 및 추론 능력을 통해 데이터 차트에 포함된 데이터를 분석할 수 있습니다. GPT-4o는 분석이나 프롬프트에 따라 데이터 차트를 생성할 수도 있습니다.
파일 업로드: 지식 컷오프를 넘어, GPT-4o는 파일 업로드를 지원하여 사용자가 특정 데이터를 분석할 수 있습니다.
메모리 및 맥락 인식: GPT-4o는 이전 상호작용을 기억하고 긴 대화에서도 맥락을 유지할 수 있습니다.
큰 컨텍스트 창: 최대 128,000개의 토큰을 지원하는 컨텍스트 창을 통해, GPT-4o는 긴 대화나 문서에서도 일관성을 유지할 수 있어 상세한 분석에 적합합니다.
환각 감소 및 향상된 안전성: 이 모델은 부정확하거나 오해의 소지가 있는 정보를 최소화하도록 설계되었습니다. GPT-4o는 사용자가 안전하게 이용할 수 있도록 강화된 안전 프로토콜을 포함하고 있습니다.