| 💡 Better | 💬 Opinion | 📂 Organize | ✨ Simple | 🤝 Together |
|---|---|---|---|---|
| 더 나은 성장과 개선 | 솔직한 의견 공유 | 체계적 관리와 정리 | 누구나 쉽게 사용 가능 | 함께 만드는 협업 경험 |
BOOST는 대학생 팀 프로젝트에서 더 나은 성장을 돕고, 솔직한 의견을 자유롭게 나누며,
체계적인 관리와 누구나 쉽게 접근 가능한 환경 속에서 팀원 모두가 함께 협업할 수 있게 만들어주는 협업 툴입니다.
- 대학생 팀 프로젝트를 보다 효율적이고 체계적으로 관리할 수 있도록 돕는 협업 툴입니다.
- 칸반(Kanban) 보드 형태의 직관적인 인터페이스를 제공하여, 팀원들은 할 일을 쉽게 테스크(Task)로 만들고 담당자를 지정할 수 있습니다.
- 작업물이 업로드되면 팀원들이 확인하고 승인할 수 있으며, 원하는 부분에 마커를 달아 구체적인 피드백을 남길 수 있습니다.
- 말하기 어려운 피드백을 서비스의 마스코트 Boo가 부드럽게 변환하여 대신 전하게 할 수 있습니다.
- 복잡한 일정 관리 없이도 팀 프로젝트의 진행 상황을 한눈에 확인하고, 원활한 협업을 경험할 수 있습니다.
2025/09 ~ 2025/11 (약 3개월)
🌐 BOOST 메인 서비스
📘 BOOST API Swagger 문서
- 🛠️ 기술 스택
- 📐 아키텍처
- 🚀 CI/CD 파이프라인
- ✨ 주요 기능
- 🔐 인증 및 인가 플로우
- 📊 ERD
- ⚙️ 기술적 노력 & 설계 고민
- 🧪 Test Coverage (Jacoco)
- 📂 폴더 구조
- 🔃 협업 룰, 컨벤션, 브랜치 전략
- 👥 BOOST 팀원 소개
| 구분 | 기술 |
|---|---|
| 개발 언어 | |
| 서버 프레임워크 | |
| 빌드/의존성 관리 | |
| 데이터베이스 | |
| ORM | |
| 보안/인증 | |
| API 문서화 | |
| 외부 API | |
| 테스트 | |
| 컨테이너/배포 | |
| CI/CD | |
| 개발 환경 | |
| 협업 도구 |
GitHub Actions를 통해 AWS ECS에 자동 배포되는 파이프라인입니다.
- 배포 트리거:
deploy태그가 GitHub 원격 저장소에 푸시될 때 실행됩니다. - 대상 환경: Production
- 배포 방식: Blue/Green 배포 (AWS ECS)
- AWS ECS를 사용한 Blue/Green 배포 전략을 사용합니다.
- 이 방식은 신규 버전('Green')을 먼저 배포하고, Load Balancer의 테스트 리스너를 통해 상태 확인을 완료합니다.
- 모든 검증이 통과하면, 프로덕션 트래픽(리스너)이 기존 'Blue' 환경에서 'Green' 환경으로 즉시 전환됩니다.
- 보안:
- AWS 인증: OIDC 방식으로 안전한 임시 자격 증명 사용
- 환경 변수: AWS Secrets Manager를 통한 중앙 관리
- 핵심 장점:
- 무중단 배포: 트래픽 전환은 순간적으로 일어나므로 사용자는 배포 과정을 인지하지 못합니다.
- 신속한 롤백: 'Green' 환경 배포 중 문제가 감지되거나 트래픽 전환 후 정상적인 상태가 아님이 확인된 경우, 자동으로 트래픽을 즉시 'Blue' 환경(기존 버전)으로 되돌립니다. 기존 'Blue' 환경은 트래픽 전환이 안정화된 후에 종료되므로 롤백이 매우 안전하고 빠릅니다.
deploy태그 푸시- Checkout
- JDK 21 설정
- Gradle 빌드
- AWS OIDC 인증
- ECR 로그인
- Docker 이미지 빌드
- ECR 푸시
- ECS 태스크 정의 렌더링
- ECS 서비스 배포
- 서비스 안정화 대기
- 개인 칸반보드 형태로 직관적인 작업 생성 및 관리 가능
- 프로젝트별 개인 테스크를 한눈에 확인하고, 우선순위에 따라 효율적으로 정리
- 상태(진행 전 / 진행 중 / 검토 중 / 완료) 변경을 통해 작업 현황을 명확하게 관리
- 팀 단위 칸반보드를 통해 팀원들과 실시간으로 작업 진행 상황을 공유
- 각 작업의 담당자, 진행 상태, 마감일 등을 한곳에서 관리하여 협업 효율 극대화
- 상태 변경 시 팀원들이 즉시 확인할 수 있어 투명한 협업 환경 구축
- 참여 코드를 통해 안전하게 프로젝트 참여 가능
- 24시간마다 코드가 리프레시되어 보안 유지
- 코드 공유로 간편하게 팀원 초대
- 각 팀원의 작업 상태 한눈에 확인 가능
- 점수가 가장 높은 팀원에게 왕관 아이콘 부여
- 개인의 기여도를 시각적으로 표시하여 협업 동기 부여
- 팀원의 작업물에 댓글 가능
- 피드백 작성 시 원하는 위치에 마커 지정 후 댓글 작성 기능
- 거친 표현이나 세게 들릴 수 있는 말투를 AI가 자연스럽고 부드럽게 변환
- 솔직한 피드백은 그대로 유지하면서, 상대방이 기분 좋게 받아들일 수 있도록 조정
- 팀 내 건강한 커뮤니케이션 문화 형성에 도움
- 업로드된 작업물 확인 및 승인
- 리뷰 요청 시 검토 중으로 할 일이 이동하고, 모든 승인을 다 받으면 완료 처리 가능
- 마감일이 얼마 남지 않은 작업 알림
- 검토가 필요한 작업 발생 시 알림
- 모든 승인 완료 시 알림
- 팀원들이 중요한 일정을 놓치지 않도록 실시간으로 안내
- 직관적인 인터페이스로 누구나 쉽게 사용 가능
- 귀여운 아바타 지정 가능
- 프로젝트를 진행하면서 나온 작업 파일들을 한곳에서 모아보기 가능
- 팀원 간 공유용 자유로운 메모 작성 가능
- 프로젝트 관련 아이디어, 링크, 참고 자료 정리
- 간단한 개인 기록용으로도 사용 가능
카카오 로그인부터 토큰 재발급, 로그아웃까지의 전체 인증 흐름을 시각적으로 정리했습니다.
ERD 핵심 요소 설명
-
BaseEntity필드 타입 설명 idUUID기본 키 (자동 생성) createdAtLocalDateTime생성 시각 ( @CreatedDate)updatedAtLocalDateTime마지막 수정 시각 ( @LastModifiedDate)- 모든 엔티티의 기본 클래스
- JPA Auditing으로 생성/수정 시각 자동 관리
- 삭제/복구가 필요 없는 엔티티에서 사용
-
SoftDeletableEntity필드 타입 설명 deletedboolean삭제 여부 ( true면 논리 삭제 상태)deletedAtLocalDateTime삭제된 시각 BaseEntity확장 클래스- 물리 삭제 대신 논리 삭제(soft delete) 처리
reactivate()로 복구 가능- 삭제/복구가 필요한 엔티티에서 상속을 받아서 사용
-
ProjectMembership필드 타입 설명 projectProject소속된 프로젝트 memberMember참여 중인 멤버 roleProjectRole프로젝트 내 역할 (예: OWNER, MEMBER 등) notificationEnabledboolean알림 수신 여부 SoftDeletableEntity상속 (프로젝트 탈퇴 시 논리 삭제)Project와Member사이의 관계를 나타내는 중간 엔티티- 단순 N:M 매핑이 아닌, 역할/알림 설정을 함께 관리
-
ProjectJoinCode필드 타입 설명 joinCodeString프로젝트 참여 초대 코드 projectProject관련된 프로젝트 statusCodeStatus코드 상태 (ACTIVE, EXPIRED, REVOKED 등) expiresAtLocalDateTime코드 만료 시각 BaseEntity상속 (삭제 대신 상태 전환으로 관리)- 프로젝트 초대 링크/코드 관리용 엔티티
- 상태 및 만료 시간 기반으로 유효성 검증 (
isActive,isExpired)
-
BoostingScore필드 타입 설명 projectMembershipProjectMembership점수가 귀속되는 프로젝트-멤버 관계 taskScoreInteger작업(Task) 수행 점수 commentScoreInteger댓글(Comment) 활동 점수 approveScoreInteger승인(Approve) 관련 점수 totalScoreInteger총합 점수 calculatedAtLocalDateTime점수 계산 시각 - 멤버의 프로젝트 내 활동 점수를 관리 (
ProjectMembership단위로 누적) - (작업/댓글/승인)에 따라 1시간마다 스케줄링으로 자동 갱신되어 최신 점수를 유지
- 멤버의 프로젝트 내 활동 점수를 관리 (
-
WebPushSubscription필드 타입 설명 memberMember구독한 사용자 tokenString브라우저 푸시 토큰 (고유) deviceInfoString디바이스 정보 (중복 방지용) webPushUrlString푸시 엔드포인트 URL publicKeyString클라이언트 공개 키 authKeyString인증 키 SoftDeletableEntity상속- 사용자별 웹 푸시 구독 정보 저장
- webPushUrl: 브라우저가 푸시 서버(Google FCM 등)에 등록할 때 제공하는 고유 엔드포인트 URL
- publicKey: 푸시 메시지 암호화를 위한 브라우저 측 공개 키 (서버는 이걸로 암호화함)
- authKey: 메시지 무결성과 인증을 보장하기 위한 추가 키
저희 프로젝트를 진행하면서 서비스 품질 향상과 성능 개선을 위해 진행한 기술적 시도 및 과정을 정리했습니다.
Refresh Token 보안 강화 및 Stateless 로그아웃 전략
Access Token은 수명이 짧지만, Refresh Token은 수명이 깁니다.
만약 Refresh Token이 JavaScript로 접근 가능한 localStorage나 일반 쿠키에 저장될 경우,
XSS 공격에 탈취당하면 사용자의 계정이 장기간 위험에 노출됩니다.
- Access Token (수명 30분)
JavaScript가 API 요청 헤더에 담을 수 있도록 응답 Body에 담아 전달했습니다. - Refresh Token (수명 7일): XSS 공격을 원천적으로 차단하기 위해, JavaScript가 접근할 수 없는 HttpOnly 속성의 보안 쿠키로 발급했습니다.
- 추가로
SameSite=Strict,Secure=true옵션을 부여하여 CSRF 공격과 HTTP 통신을 방어했습니다.
HttpOnly쿠키는 브라우저 보안 정책에 의해document.cookie로 접근이 불가능합니다.- 이 방식은 XSS 공격자가 스크립트를 삽입하더라도,
가장 중요한 Refresh Token의 탈취를 막을 수 있는 가장 표준적이고 강력한 웹 보안 전략입니다.
- XSS 공격으로부터 Refresh Token을 안전하게 보호하여 인증 시스템의 전반적인 보안 수준을 크게 향상시켰습니다.
- Access Token은 편리하게 사용하되, 위험은 짧은 만료 기간으로 최소화하는 이중 토큰 전략을 완성했습니다.
JWT는 서버가 상태를 저장하지 않는 Stateless를 전제로 합니다.
이로 인해 사용자가 로그아웃을 해도, 서버는 이미 발급된 Access Token을 강제로 무효화할 방법이 없습니다.
토큰이 유효기간(30분) 동안 탈취되어 사용될 수 있는 보안적 딜레마가 발생했습니다.
- (고려한 방식)
Access Token을 Redis 같은 DB에 '블랙리스트'로 저장하여,
모든 요청마다 토큰이 블랙리스트에 있는지 검사하는 Stateful 방식. - (선택한 방식)
JWT의 Stateless 이점을 유지하기로 결정했습니다.
대신, 로그아웃 시 DB에 저장된 Refresh Token을 즉시 폐기했습니다.
- '블랙리스트' 방식은 모든 API 요청마다 추가적인 DB 조회가 필요해, JWT의 가장 큰 장점인 성능과 서버 확장성을 포기해야 하는 트레이드오프가 발생했습니다.
- Refresh Token을 즉시 폐기하는 Stateless 방식은, 비록 Access Token의 짧은 유효기간 동안은 위험이 존재하지만,
- 그 이후에는 공격자가 새로운 토큰을 발급받을 수 없도록 접근을 원천 차단할 수 있습니다. 성능과 보안 사이의 합리적인 절충안이라고 판단했습니다.
- Stateless 아키텍처의 성능과 확장성을 유지하면서, Refresh Token 폐기를 통해 실질적인 로그아웃 기능을 구현했습니다.
로그아웃 딜레마를 해결했음에도, 수명이 7일인 Refresh Token 자체가 탈취당하면 여전히 위험했습니다.
공격자는 7일간 지속적으로 새 Access Token을 발급받을 수 있습니다.
- Refresh Token Rotation (RTR) 전략을 도입했습니다.
- 토큰 재발급 요청 시, 새로운 Access Token과 함께 '완전히 새로운 Refresh Token'을 발급합니다.
- 동시에, DB에 저장된 '기존 Refresh Token'은 즉시 폐기(또는 새 토큰으로 교체)합니다.
- RTR은 Refresh Token을 일회용처럼 사용하게 만듭니다.
- 만약 토큰이 탈취되더라도, 공격자나 실제 사용자 중 단 한 명만 성공적으로 재발급을 받을 수 있습니다.
- 이후 무효화된 토큰으로 재발급 요청이 들어오면, 서버는 예외를 발생시켜 이를 탈취 시도로 감지하고 해당 사용자의 모든 세션을 강제 종료시키는 등의 후속 조치를 할 수 있습니다.
- 기존의 '토큰 유출 시 7일간 위험' 상태에서, '토큰 유출 시 즉시 감지 및 무력화 가능' 상태로 보안 수준을 대폭 향상시켰습니다.
Task 조회 성능 개선을 위한 데이터베이스 인덱스 도입
- 프로젝트 내 태스크가 증가하면서 태스크 목록 조회 시 성능 저하 발생
- TaskRepository의 주요 쿼리들이 복합 조건(프로젝트+상태+정렬)을 사용하나 인덱스 미적용
- "내 태스크" 조회 시 task_assignees 조인 테이블 풀스캔으로 인한 지연
- 마감일 알림 기능에서 전체 테이블 스캔 발생
- 프로젝트별 태스크 조회: 프로젝트 ID + 상태 필터링 + 생성일/마감일 정렬
- 내 태스크 조회: 멤버 ID로 assignees 조인 + 상태 필터링 + 정렬
- 멤버별 태스크 집계: 프로젝트 내 모든 멤버의 상태별 태스크 카운팅
- 마감일 알림: 특정 날짜 + 완료되지 않은 태스크 검색
| 인덱스명 | 컬럼 구성 | 대상 쿼리 |
|---|---|---|
idx_tasks_project_status_created |
project_id, status, created_at, id |
프로젝트+상태별 조회 및 생성일 정렬 |
idx_tasks_project_status_duedate |
project_id, status, due_date, id |
프로젝트+상태별 조회 및 마감일 정렬 |
idx_tasks_duedate_status |
due_date, status |
마감일 알림 조회 |
| 인덱스명 | 컬럼 구성 | 대상 쿼리 |
|---|---|---|
idx_task_assignees_member_task |
member_id, task_id |
멤버별 태스크 조회 (MEMBER OF 쿼리) |
- 프로젝트 태스크 목록 조회: 테이블 풀스캔 → 인덱스 스캔으로 변경되어 O(n) → O(log n) 성능 향상
- 내 태스크 조회: 조인 테이블 풀스캔 제거로 멤버당 할당된 태스크만 효율적으로 접근
- 정렬 처리: 인메모리 정렬 대신 인덱스 순서 활용으로 추가 정렬 비용 제거
- 마감일 알림: 특정 날짜 범위 검색 시 인덱스 범위 스캔으로 대폭 개선
- 태스크 수가 증가해도 조회 성능 선형적으로 유지
- 프로젝트 멤버가 많아져도 개인 태스크 조회 속도 안정적
- 동시 사용자 증가 시 DB 부하 감소
- 태스크 목록 로딩 속도 향상으로 UI 반응성 개선
- 검색 및 필터링 기능 사용 시 즉각적인 응답
- 대시보드 및 통계 화면 로딩 시간 단축
Task 엔티티 낙관적 락(Optimistic Lock) 도입
여러 리뷰어가 동시에 같은 Task를 승인할 때 Lost Update 문제 발생
- 사용자A와 B가 동시에 Task 조회 (approvers: 빈 리스트)
- 사용자A가 승인 추가 후 저장 (approvers: [A])
- 사용자B가 승인 추가 후 저장 (approvers: [B])
- 결과: 사용자A의 승인이 사라지고 데이터 정합성이 깨짐
Task 엔티티에 @Version 필드 추가
- JPA의 @Version 어노테이션을 사용해 버전 필드 선언
- JPA가 자동으로 버전을 관리하고 충돌 감지
- 충돌 시 OptimisticLockException 발생
- GlobalExceptionHandler에서 409 Conflict 응답 처리
데이터 정합성 보장
- 동시 수정 시 먼저 커밋된 트랜잭션만 성공
- Lost Update 문제 방지
- 승인자 목록, 상태 변경 등 비즈니스 로직의 일관성 유지
높은 성능 & 확장성
- DB 레벨의 락을 사용하지 않아 읽기 성능 우수
- 여러 사용자가 동시에 조회해도 성능 저하 없음
- 데드락 위험 없음
웹 푸시 알림 시스템 설계 및 비동기 이벤트 처리
- 조별 과제나 협업 과정에서는 팀원들이 자료를 확인하거나 검토해야 하는 상황이 자주 발생합니다.
- 이를 원활하게 진행하기 위해, 모두가 항상 휴대하고 있는 모바일 기기를 통해 즉각적인 알림을 전달할 수 있는 방법이 필요하다고 판단했습니다.
- 처음에는 카카오톡 알림톡을 활용하려 했으나, 사업자 등록이 필수라는 제약이 있어 대안을 모색하던 중 웹 푸시(Web Push) 기술을 선택하게 되었습니다.
- 별도의 앱 설치 없이 브라우저에서의 알림 허용만으로 알림 전송 가능
- 사용자가 브라우저를 열지 않아도 실시간 알림 전달 가능
- Chrome, Edge, Firefox 등 대부분의 브라우저 지원(IOS의 경우 웹앱으로 사용 가능)
- 협업 시 팀원들에게 자료 확인·검토 알림을 빠르게 전달 가능
알림 등록 과정 설명
-
세션 발급 (
create상태)- 클라이언트가 웹 푸시를 요청하면 서버는 TTL 5분짜리 세션을 생성한다.
- 생성된 세션 정보는
create상태로 저장된다.
-
QR 코드 생성 및 표시
- 클라이언트는 발급된 세션 정보를 기반으로 QR 코드를 생성한다.
- 이 QR 코드를 사용자에게 표시하여 모바일 기기에서 인식할 수 있도록 한다.
-
모바일 연결 (
connect상태 전환)- 사용자가 모바일로 QR을 스캔하면, 클라이언트의 요청으로 해당 세션이
connect상태로 변경된다.
- 사용자가 모바일로 QR을 스캔하면, 클라이언트의 요청으로 해당 세션이
-
웹 푸시 등록 및 저장 (
register로 상태 전환)- 사용자가 모바일 웹에서 알림 설정하기 버튼을 클릭하게 되면
웹 푸시 알림이 활성화되면서 해당 세션이register상태로 변경된다. - 즉, 모바일 기기와 브라우저 간의 연결이 성립된다.
- 사용자가 모바일 웹에서 알림 설정하기 버튼을 클릭하게 되면
-
브라우저에서의 페이지 전환
- 브라우저는 주기적으로 서버에 polling을 수행해 세션 상태를 확인한다.
- 세션 상태가
register로 변경되면, 클라이언트는 웹 푸시 구독 정보를 등록한다. - 서버는 전달받은 구독 정보를 DB에 저장한다.
알림 전송 흐름 설명
-
구독 정보 저장 과정은 위의 웹 푸시 알림 등록 과정을 축약한 것 입니다.
-
프론트에서 알림 전송 요청
- 알림을 보내고 싶은 시점에 브라우저에서 서버에 “알림 보내기” 요청
- 요청에는 알림 내용(payload)만 포함
-
서버에서 푸시 서버로 전송
- 서버는 DB에서 구독 정보를 조회
- 푸시 서버로 메시지 전송 요청
- 푸시 서버는 메시지를 브라우저로 전달
-
브라우저 Service Worker가 알림 표시
- 백그라운드에서 메시지 수신
- 알림을 구독한 모바일 기기에 푸시 알림 표시
- 알림 클릭 시 웹페이지 이동 가능
Polling vs WebSocket 선택 이유
-
고민한 이유
모바일에서 QR 코드를 스캔해 연결된 후, 프론트에서 자동 페이지 리다이렉트를 구현하고자 했습니다.
하지만 모바일과 브라우저는 서로 상태를 알 수 없기 때문에 문제가 발생하였고
Polling과 WebSocket 두 가지 방법을 고민했습니다. -
Polling 선택 이유
- 구현 난이도가 낮아 기능 우선 개발에 적합
- 실시간성이 극도로 중요한 서비스가 아니며, 현재 동시 사용자가 많지 않음
- 서버 부하가 발생할 수 있지만, 알림 연결 수준에서는 충분히 감당 가능
-
WebSocket 고려
- 실시간성이 뛰어나고 서버와 브라우저 간 상태 동기화가 즉시 가능
- 추후 성능 개선이나 동시 접속자 증가 시 업그레이드 가능
-
결론
현재는 빠른 구현과 안정성을 위해 Polling을 선택하고,
나중에 필요에 따라 WebSocket으로 전환하도록 하였습니다.
알림 전송 로직 설계 및 비동기 처리 이유
-
목적
알림 전송 과정은 특정 서비스 로직 실행 → 알림 저장 → 푸시 전송 의 세 단계를 각각 독립된 비동기 흐름
으로 처리하여, 서비스의 안정성과 응답성을 동시에 확보하였습니다.
- 알림 발송이 필요한 특정 서비스(
TaskService.Java)
if (request.status() == TaskStatus.REVIEW) {
taskEventPublisher.publishTaskReviewEvent(project.getId(), task.getId());
}-
설명
특정 서비스 로직이 실행된 이후, 조건에 만족되면 알림 발송이 필요하다면 알림 이벤트를 발행합니다.
예를 들어 Task 상태가
REVIEW로 변경되면,TaskReviewEvent가 발행됩니다.
-
NotificationEventHandler.Java@Async @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) public void handleTaskReviewEvent(TaskReviewEvent event) { notificationService.notifyTaskReview(event.projectId(), event.taskId(), NotificationType.REVIEW); }
-
비동기로 실행한 이유
- Task 상태 변경 트랜잭션과 알림 발송은 서로에게 영향을 주어서는 안된다고 생각하였습니다.
- 알림 발송 실패가 Task 업데이트에 영향을 주면 비즈니스 일관성이 깨질 것이라고 판단하였습니다.
- 따라서
@Async+AFTER_COMMIT조합으로 트랜잭션 커밋 후 별도 스레드에서 실행하도록 설계하였습니다.
-
NotificationService.Java@Transactional(readOnly = true) public void notifyTaskReview(UUID projectId, UUID taskId, NotificationType type) { // ...프로젝트 및 태스크 조회 for (Member member : members) { try { notificationSenderService.saveAndSendNotification(member, type.title(), type.message(task.getTitle())); } catch (Exception e) { log.error("Failed to send review notification to member: {}", member.getId(), e); } } }
-
설명
- 알림을 받을 대상(
Member)을 필터링 후, 개별적으로saveAndSendNotification호출
- 알림을 받을 대상(
-
설계 의도
- 한 명의 알림 발송 실패가 다른 사람에게 영향 주지 않도록 각 멤버별 독립 실행을 하기 위해서 따로 트랜잭션을 독립적인 단위로 실행하고자 하였습니다. 아래에서 더 자세한 설명하도록 하겠습니다.
-
NotificationSenderService.Java@Transactional(propagation = Propagation.REQUIRES_NEW) public void saveAndSendNotification(Member member, String title, String message) { Notification notification = Notification.create(member, title, message); notificationRepository.save(notification); if (member.isNotificationEnabled()) { notificationEventPublisher.publishNotificationSavedEvent(member, title, message); } }
-
해당 메서드를 부모 트랜잭션의 영향을 받지 않도록 하기 위해 별도의 컴포넌트로 분리하였습니다.
-
기존 서비스 로직 내에서 멤버별 알림을 순차적으로 처리할 때, 하나의 트랜잭션 안에서 모두 실행되면 특정 멤버의 알림 저장 또는 발송 과정에서 예외가 발생할 경우 전체 트랜잭션이 롤백될 위험이 있었습니다.
-
따라서 해당 메서드에
@Transactional(propagation = Propagation.REQUIRES_NEW)를 적용하여 각 멤버별 알림 저장을 독립적인 트랜잭션으로 실행하도록 설계하였습니다. -
REQUIRES_NEW 사용 이유
- 멤버별 알림 저장은 독립적인 단위로 실행되어야 함
- 기존 Task 알림 루프나 다른 멤버의 알림과 트랜잭션을 공유할 필요 없음
- 실패 시 rollback 범위를 최소화하여 부분 성공 허용
NotificationEventHandler.Java
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleNotificationSavedEvent(NotificationSavedEvent event) {
webPushClient.sendNotification(event.member(), event.title(), event.message());
}- 비동기로 처리한 이유
- 서비스 자체에서 알림을 표현해주기 위한 알림 저장(DB)은 비교적 빠르게 처리되지만, 실제 WebPush 전송은 네트워크 I/O로 인해 시간이 더 걸리는 작업입니다.
- 그래서 DB 트랜잭션과 전송 로직을 분리하여 사용자 응답 속도 향상 및 안정성 확보하였습니다.
- 결과
- 알림 저장 → 커밋 후 → 별도 스레드에서 Web Push 전송
- 네트워크 오류나 푸시 실패 시에도 데이터 무결성은 보장됨
-
이벤트 기반 구조
→ 비즈니스 로직과 알림 로직을 분리하여 결합도를 최소화하고 유지보수성을 향상시켰습니다.
-
@Async + AFTER_COMMIT 조합
→ 트랜잭션 커밋 이후 비동기 실행으로 처리하여 트랜잭션 안정성과 사용자 응답 속도를 모두 확보했습니다.
-
REQUIRES_NEW 트랜잭션 적용
→ 멤버별 알림을 독립된 트랜잭션으로 처리하여 부분 실패를 허용하고, 다른 알림에 영향을 주지 않도록 했습니다.
-
2단계 비동기 처리 (Service → Notification)
→ 서비스 로직 이후 알림 처리 단계를 분리하여 에러 상황을 격리하고 장애 전파를 방지했습니다.
-
에러 로그만 기록, 실패 무시
→ 알림 전송 실패가 핵심 비즈니스 로직에 영향을 주지 않도록 하고, 핵심 도메인 로직의 안정성을 보호했습니다.
Jacoco를 활용하여 테스트 코드 커버리지를 측정하고 관리하여 코드의 품질을 향상시켰고 84%의 테스트 커버리지를 달성하였습니다.
- 단위 테스트 기반 커버리지 측정
📁 boost/
├── 📁 .github/ # (GitHub Actions - CI/CD 설정)
├── 📁 build/ # (빌드 결과물)
├── 📁 gradle/
├── 📄 build.gradle # 📜 프로젝트 의존성 및 빌드 설정
├── 📄 Dockerfile # 🐳 Docker 컨테이너 빌드 스크립트
├── 📄 ecs-taskdef.json # (AWS ECS 작업 정의)
├── 📄 README.md
└── 📁 src/
├── 📁 main/
│ ├── 📁 java/
│ │ └── knu/team1/be/boost/
│ │ ├── BoostApplication.java # 🚀 메인 애플리케이션
│ │ │
│ │ ├── 🧩 [feature_domain]/
│ │ │ ├── controller/ # API 엔드포인트 (API Interface + Controller)
│ │ │ ├── service/ # 비즈니스 로직
│ │ │ ├── repository/ # DB 데이터 접근 (JPA Repository)
│ │ │ ├── dto/ # 데이터 전송 객체 (Request/Response DTOs)
│ │ │ ├── entity/ # DB 테이블과 매핑 (JPA Entity)
│ │ │ └── ... # (필요시 exception, scheduler 등)
│ │ │
│ │ ├── 🛡️ security/ # Spring Security (공통 보안 설정)
│ │ │ ├── SecurityConfig.java
│ │ │ ├── filter/ # (JwtAuthFilter, JwtExceptionFilter)
│ │ │ ├── handler/ # (CustomAuthenticationEntryPoint)
│ │ │ └── util/ # (JwtUtil)
│ │ │
│ │ └── 🌍 common/ # 공통 모듈 (여러 도메인에서 사용)
│ │ ├── config/
│ │ ├── entity/ # 공통 엔티티 속성
│ │ ├── exception/ # 공통 예외 처리
│ │ └── policy/ # 공통 접근 정책
│ │
│ └── 📁 resources/
│ ├── application.yml # 📋 공통 설정
│ ├── application-dev.yml # 💻 개발 환경 설정
│ ├── application-dev-env.yml # 🔑 개발 환경 변수용 YAML (Git 무시)
│ └── application-prod.yml # ☁️ 운영 환경 설정
│
└── 📁 test/
└── 📁 java/
└── knu/team1/be/boost/
├── BoostApplicationTests.java
│
└── 🧪 [feature_domain]/ # 도메인별 테스트 코드
├── controller/ # (ExampleControllerTest)
└── service/ # (ExampleServiceTest)
- 작업 시에는 반드시 이슈 템플릿에 맞게 이슈를 생성하고 작업을 진행한다.
- PR 제목은 커밋 컨벤션 + 간단 설명 형식
- 코드 리뷰 시 Approve / Request Changes로 피드백
- 기능 구현 시 작은 단위로 commit & PR
- 코드 스타일과 포맷은 Google Java style guide 기준 준수
### 😊 리뷰 규칙을 지킵시다
코드 리뷰는 `Pn`룰에 따라 작성하기.
Reviewer가 피드백을 남길 때 Assignee에게 얼마나 해당 피드백에 대해 강조하고 싶은 지 표현하기 위한 규칙입니다.
- `P1` : 꼭 반영해 주세요 (Request Changes) - 이슈가 발생하거나 취약점이 발견되는 케이스 등
- `P2` : 반영을 적극적으로 고려해 주시면 좋을 것 같아요 (Comment)
- `P3` : 이런 방법도 있을 것 같아요~ 등의 사소한 의견입니다 (Chore)
<Gitmoji>(#이슈번호) <작업 요약>
<상세 설명>
| 이모지 | 의미 | 예시 |
|---|---|---|
| ✨ | 새로운 기능 추가 | ✨ 팀원 API 추가 |
| 🐛 | 버그 수정 | 🐛 알람 기능 버그 수정 |
| ♻️ | 리팩토링 | ♻️ 메소드 분리 |
| 🎨 | 코드 스타일 변경 | 🎨 코드 스타일 적용 |
| 📝 | 문서 파일 추가 및 수정 | 📝 README 업데이트 |
| ✏️ | 단순 오타 수정 | ✏️ typo 수정 |
| ✅ | 테스트 추가 | ✅ TaskDetail 테스트 추가 |
| 🔥 | 코드 제거 | 🔥 사용하지 않는 메소드 제거 |
| 🩹 | 단순한 에러 수정 | 🩹 API 응답 메시지 오류 수정 |
| 🚑️ | 핫픽스 | 🚑️ 특정 에러 핫픽스 |
| 🔧 | 설정 변경 | 🔧 Config 파일 수정 |
| 🚀 | 배포 관련 수정 | 🚀 CI/CD workflow 작성 |
| 브랜치 | 용도 | 설명 |
|---|---|---|
main |
배포용 | 항상 안정된 버전 유지 |
develop |
개발 통합 | 기능 완료 후 merge |
feat/#이슈-<이름> |
기능 개발 | 새로운 기능 개발 시 사용 |
refactor/#이슈-<이름> |
코드 리팩토링 | 코드 리팩토링 시 사용 |
fix/#이슈-<이름> |
버그 수정 | 버그 발생 해결 시 사용 |
deploy/#이슈-<이름> |
배포 관련 | 배포 관련 작업 시 사용 |
💡 PR은 반드시 리뷰 후 merge 진행
| 이진호 | 김원호 | 김혜민 | 서영진 | 유다연 |
|---|---|---|---|---|
@treasure-sky |
@kmwh |
@hyemomo |
@seoyoungjin23 |
@daaoooy |
| Backend | Backend | Frontend | Backend | Frontend |


















