Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
7298534
Chore: Spring Boot initial
caminobelllo Nov 13, 2025
13f4690
Chore: 공통 응답 구조 설정
caminobelllo Nov 13, 2025
2b23b2b
Feat: Member domain 추가
Immmii Nov 13, 2025
85664df
Feat: Spring Security JWT 추가
Immmii Nov 13, 2025
e9f9de3
Feat: 회원가입 기능 추가
Immmii Nov 13, 2025
fa0c0ef
Chore: SecurityConfig JWT 설정 추가
Immmii Nov 13, 2025
c989532
Feat: RefreshToken 발급 및 로그아웃 기능 추가
Immmii Nov 13, 2025
d14e0c8
Fix: RefreshToken 수정
Immmii Nov 13, 2025
7a32e25
Chore: 회원 필드 추가
Immmii Nov 14, 2025
fde5909
Chore: DTO 패키지 구조 변경
Immmii Nov 14, 2025
5f34138
Remove: Exception 중복 삭제
Immmii Nov 14, 2025
f78afeb
Chore: Member -> User 변경 및 User 스펙 추가
Immmii Nov 18, 2025
797951f
Refactor: Spring Security & JWT 리팩토링 및 RefreshToken 저장소 Mysql -> Redi…
Immmii Nov 18, 2025
57f890e
Chore: ErrorCode 추가
Immmii Nov 18, 2025
a655541
Refactor: User 회원가입 & 로그인 api 리팩토링
Immmii Nov 18, 2025
62090f0
Rename: Exception 파일명 변경
Immmii Nov 18, 2025
a123f14
Feat: Login Exception 추가
Immmii Nov 18, 2025
912e00e
Refactor: 로그아웃 api 리팩토링
Immmii Nov 18, 2025
af87073
Chore: LoginSucceseCode 추가
Immmii Nov 18, 2025
41b258b
Chore: Update .gitignore
caminobelllo Nov 18, 2025
ed9cf43
Chore: Add ci workflow
caminobelllo Nov 18, 2025
941222b
Chore: Add cd workflow
caminobelllo Nov 18, 2025
40062fd
Chore: CI/CD workflows 작성
caminobelllo Nov 18, 2025
1257aff
Chore: change instance repo name
caminobelllo Nov 18, 2025
63c5e3c
Chore: cd instance 레포지토리명 수정
caminobelllo Nov 18, 2025
45f7902
Feat: Team, Part 조회 api 추가
Immmii Nov 19, 2025
7447408
Feat: 만료된 AccessToken 재발급 api 추가
Immmii Nov 19, 2025
b178065
Chore: TokenGenerateHelper import 수정
Immmii Nov 19, 2025
3e4cc90
Chore: GlobalExceptionHandler 로깅 추가
Immmii Nov 20, 2025
128f42d
Fix: GlobalExceptionHandler 에러 핸들링 순서 변경
Immmii Nov 20, 2025
5d294dd
Chore: RedisConfig 생성
caminobelllo Dec 17, 2025
aad448c
Feat: 후보자 엔티티 생성
caminobelllo Dec 18, 2025
e7c65d7
Feat: 투표 dto 생성
caminobelllo Dec 18, 2025
25dab6e
Feat: 투표 repository 생성
caminobelllo Dec 18, 2025
c465614
Feat: 투표 service 로직 구현
caminobelllo Dec 18, 2025
8f28fa9
Feat: 투표 adaptor 구현
caminobelllo Dec 18, 2025
b2c52f3
Feat: 투표 usecase 구현
caminobelllo Dec 18, 2025
0350abb
Feat: 투표 controller 구현
caminobelllo Dec 18, 2025
6e546af
Feat: db 스케줄러 설정
caminobelllo Dec 18, 2025
240635c
Chore: 오류 코드 추가
caminobelllo Dec 18, 2025
3188c4d
Chore: 배포용 flyway 의존성 추가
caminobelllo Dec 18, 2025
a27abcf
Chore: 초기 데이터 쿼리 작성
caminobelllo Dec 18, 2025
b3f44e9
Feat: 투표 기능 구현
caminobelllo Dec 18, 2025
a78beb4
Chore: cors & swagger 설정
caminobelllo Dec 20, 2025
abfc226
Chore: cors & swagger 설정
caminobelllo Dec 20, 2025
a3ba008
Chore: 시드 데이터 변경
caminobelllo Dec 20, 2025
33a1882
Chore: seed sql 변경
caminobelllo Dec 20, 2025
7efff21
Fix: 팀명 수정
caminobelllo Dec 20, 2025
0efd46d
Chore: dependency 추가
caminobelllo Dec 20, 2025
d5da338
Fix: 팀명 수정사항 반영
caminobelllo Dec 20, 2025
1202302
Docs: create README.md
caminobelllo Dec 22, 2025
a969db8
Docs: update README.md
caminobelllo Dec 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.idea
*.iml
*.ipr
*.iws
.gradle
.git
*.log
.DS_Store
45 changes: 45 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CEOS vote CD - Deploy to EC2

on:
workflow_run:
workflows:
- "CEOS vote CI - Build & Push to ECR"
types:
- completed

jobs:
deploy-to-ec2:
name: Deploy to EC2
runs-on: self-hosted
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
ECR_REGISTRY: ${{ secrets.ECR_REGISTRY }}

if: ${{ github.event.workflow_run.conclusion == 'success' }}

steps:
- name: Deploy on EC2 Instance
run: |
cd ~/ceos-vote || { echo "Directory not found"; exit 1; }

echo "Creating .env file..."
echo "DB_USERNAME=${{ secrets.DB_USERNAME }}" > .env
echo "DB_PASSWORD=${{ secrets.DB_PASSWORD }}" >> .env
echo "DB_ROOT_PASSWORD=${{ secrets.DB_ROOT_PASSWORD }}" >> .env
echo "JWT_SECRET_KEY=${{ secrets.JWT_SECRET_KEY }}" >> .env
echo "JWT_ACCESS_TOKEN_VALIDITY_MS=${{ secrets.JWT_ACCESS_TOKEN_VALIDITY_MS }}" >> .env
echo "JWT_REFRESH_TOKEN_VALIDITY_MS=${{ secrets.JWT_REFRESH_TOKEN_VALIDITY_MS }}" >> .env

echo "Logging in to ECR..."
aws ecr get-login-password --region ${{ secrets.AWS_REGION }} | docker login --username AWS --password-stdin ${{ secrets.ECR_REGISTRY }}

echo "Pulling latest image..."
docker compose pull app

echo "Restarting services..."
docker compose up -d app

echo "Cleaning up..."
docker image prune -af

echo "✅ Deployment successful!"
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CEOS vote CI - Build & Push to ECR

on:
push:
branches:
- storix

jobs:
build-and-push:
name: Build and Push to ECR
runs-on: ubuntu-24.04

steps:
- name: Checkout GitHub repository
uses: actions/checkout@v3

- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: ${{ secrets.AWS_REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

- name: Login to AWS ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

- name: Build & push image to ECR
env:
IMAGE_NAME: ${{ secrets.ECR_REGISTRY }}/${{ secrets.ECR_REPOSITORY }}
IMAGE_TAG: ${{ github.sha }}

run: |
docker build -t $IMAGE_NAME:$IMAGE_TAG .
docker push $IMAGE_NAME:$IMAGE_TAG

# latest tag
docker tag $IMAGE_NAME:$IMAGE_TAG $IMAGE_NAME:latest
docker push $IMAGE_NAME:latest
48 changes: 48 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
HELP.md
.gradle
build/
!**/src/main/**/build/
!**/src/test/**/build/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/

*.pem
*.env

### 추가가 필요한 파일은 예외로 추가하기 ###
*.yml
!ci.yml
!cd.yml
*.properties

!gradle/wrapper/gradle-wrapper.jar
!gradle/wrapper/gradle-wrapper.properties
27 changes: 27 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 1. Build Stage
FROM amazoncorretto:17-alpine-jdk AS builder

ENV TZ=Asia/Seoul
WORKDIR /app

COPY gradlew ./gradlew
COPY gradle ./gradle
COPY build.gradle ./
COPY settings.gradle ./

RUN ./gradlew dependencies

COPY . .

RUN ./gradlew build -x test --no-daemon


# 2. Run Stage
FROM amazoncorretto:17-alpine-jdk

ENV TZ=Asia/Seoul
WORKDIR /app

COPY --from=builder /app/build/libs/*.jar app.jar

ENTRYPOINT ["java","-Duser.timezone=Asia/Seoul","-jar","app.jar"]
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,47 @@
# spring-vote-22nd
ceos back-end 22nd voting service project
# 투표 서비스 합동 과제 - STORIX

## 1️⃣ 기술 스택
- Spring Boot & Java 17
- Redis (ZSET, String)
- MySQL 8.0
- Flyway : DB 형상 관리 도구. 배포 시마다 테이블이 초기화되는 것을 막고 스키마 변경 이력을 코드단에서 관리
- GitHub Actions (Self-hosted) : EC2 리소스가 제한적인 프리티어 환경에서, 외부 빌드 서버의 결과물을 안전하고 빠르게 가져오고 배포 스크립트를 단순화하기 위함
- Swagger (OpenAPI 3.0)

<hr />


## 2️⃣ 기술 고려 사항

### 1. 로그인 & 회원가입
• Spring Security + JWT 기반 Stateless 인증 방식을 구현
• RefreshToken은 Redis(Hash)로 캐싱하고 재발급 및 로그아웃 기능을 구현
• JwtAuthenticationFilter로 토큰이 전달되면 인증 객체를 생성하여 SecurityContext에 저장하도록 구현

• 회원 도메인 아이디·이메일에 Unique 제약조건을 적용하여 중복 가입 방지 및 에러 핸들링을 구현

### 2. Write-Back 패턴을 통한 성능 최적화

- 🚀 **문제 상황: RDB의 I/O 병목 현상**
- 단시간에 많은 write 요청을 가정. 기존의 RDB 방식은 투표가 발생할 때마다 UPDATE 쿼리를 실행하여 Row-Level Lock을 유발하고, 이로 인해 대기 시간이 길어지며 DB 커넥션 풀 고갈 위험

- 💡 **해결책: Redis를 활용한 Write-Back 전략 도입**
- 이를 해결하기 위해 인메모리 기반의 Redis를 1차 저장소로 활용하는 Write-Back(Write-Behind) 패턴을 도입

- **실시간 집계 (ZSET)**: 후보자별 득표수는 순위 산정이 필요 -> 정렬된 집합 자료구조인 Sorted Set (ZSET)을 사용
- 중복 투표 방지 (SET): 빠른 조회 가능

- ✅ 결과: DB에 직접 쿼리를 날리는 대신 Redis에서 메모리 연산으로 처리


### 3. 데이터 정합성과 영속성 보장

- 🚀 **문제 상황: 인메모리 데이터의 휘발성 위험**
- Redis는 메모리에 데이터를 저장하므로, 투표 데이터 유실 위험
- Redis와 MySQL 간의 정합성 문제

- 💡 **해결책 1: Redis AOF 적용**: 데이터 유실 방지를 위해 Redis의 영속성 옵션 중 AOF 방식을 사용
- AOF 선택 이유: 스냅샷(RDB) 방식은 특정 주기마다 저장하므로 마지막 저장 이후의 데이터가 유실될 수 있음. 반면, AOF는 모든 쓰기 연산 명령어를 로그 파일에 기록하므로, 서버 재시작 시 로그를 재실행하여 데이터 복구하기 때문에 투표 데이터 보존에 더 적합하다고 판단

- 💡 **해결책 2: 주기적인 RDB 동기화 (Scheduler)**: Redis <-> RDB간 동기화 로직을 구현
- 스케줄러 활용: Spring Scheduler를 이용해 일정 주기마다 Redis의 투표 데이터를 읽어 MySQL에 일괄 업데이트 수행
67 changes: 67 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.7'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.storix'
version = '0.0.1-SNAPSHOT'
description = 'Demo project for Spring Boot'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

// MySQL
runtimeOnly 'com.mysql:mysql-connector-j'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.7.0'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'

// Flyway
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

// validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
}

tasks.named('test') {
useJUnitPlatform()
}
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
7 changes: 7 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading