같은 취미를 가진 사람들과 모임을 쉽게 만들고 관리할 수 있는 웹사이트입니다. 사용자는 모임을 생성하고, 멤버를 관리하며, 실시간 소통과 일정 관리를 할 수 있습니다.
이 웹사이트는 취미를 중심으로 모임을 쉽게 생성하고 관리할 수 있는 플랫폼입니다. 사용자는 다음과 같은 기능을 이용할 수 있습니다:
- 모임 정보 입력
- 회원 모집
- 실시간 소통
- 일정 및 모임 관리
사람들의 관심사가 다양해지면서 비슷한 관심을 가진 사람들과 경험을 공유하려는 욕구가 커지고 있습니다. 하지만 취미 모임을 직접 조직하고 관리하는 일은 홍보, 소통, 일정 관리 등 여러 면에서 어려움을 겪을 수 있습니다. 이 웹사이트은 이러한 과정을 간소화하는 데 목적이 있습니다.
2024-10-21 ~ 2024-11-22
이름 | 역할 |
---|---|
송민지 | 리더 |
조은형 | 부리더 |
고 결 | 팀원 |
번영덕 | 팀원 |
이정현 | 팀원 |
- Swagger를 사용하여 API 문서를 자동으로 생성하고, 개발자 간 명세 공유를 용이하게 했습니다.
SwaggerConfig
를 통해 문서화에 필요한 기본 정보(제목, 설명, 버전)를 설정- 보안 강화를 위해 개발 환경(dev) 에서만
Swagger
가 활성화되도록 설정
API 명세 자동화:
- RESTful API에 대한 요청/응답 스펙을 Swagger UI를 통해 시각화했습니다.
- 추가적인 어노테이션을 활용해 추가적인 API 설명을 문서화했습니다.
환경별 접근 제한:
application.yml
에서dev
프로파일에서만Swagger UI
에 접근할 수 있도록 조건부 활성화를 설정하여, 보안과 운영 환경의 간소화를 동시에 고려하였습니다. (dev 환경에서만 가능)
- 🔐 회원가입 및 로그인
- 이메일 회원가입: 이메일을 통해 회원가입
- 카카오 회원가입: 카카오를 통한 회원가입
- 로그인: 이메일 또는 카카오를 통한 로그인
- 회원 탈퇴: 비밀번호 입력 후 소프트 딜리트
- 👤 프로필 관리 (CUD)
- 프로필 정보 조회 (이미지, 닉네임)
- 프로필 이미지 등록
- 프로필 이미지 및 닉네임 변경
- 🎉 모임 관리 (CRUD)
- 생성, 수정, 삭제: 소규모 취미 모임 생성 및 수정, 삭제
- 카테고리별 조회: 선택한 카테고리별로 모임 조회
- 해시태그 조회: 해시태그를 통해 모임 검색
- title 조회: 제목을 통한 모임 검색
- 📝 공지/일정(게시판/스케쥴) 관리 (CUD)
- 게시판에서 공지 작성, 수정, 삭제
- 💬 댓글 관리 (CUD)
- 게시글에 댓글 작성, 수정, 삭제
- 🏷️ 해시태그 (CD)
- 콘텐츠를 분류할 수 있는 태그 생성 및 삭제
- 👍 좋아요 (CD)
- 소속된 모임에 좋아요/취소
-
기능 설명
사용자의 위치를 기준으로 이용자들이 원하는 거리에 맞게 모임을 추천합니다. -
구현 방법:
사용자의 위도와 경도 정보를 받아 서버에서 Redis에서 지원하는 GeoOperation을 사용해 거리 계산을 수행합니다.
- 모임 랭킹: zSetOperation자료구조를 이용하여 생성이 많이 된 지역 순으로 랭킹 확인을 할 수 있습니다.
사용자는 웹소켓을 통해 사용자가 속한 모임간 실시간으로 메시지를 주고받을 수 있습니다.
- 실시간 메시지 전송
- 사용자 인증
- 입/퇴장 안내 메세지
- 메시지 저장 및 조회
- 메시지 브로드캐스트
굵은 선
: 요청 메시지를 나타냅니다, 요청 메시지는 한 개체가 작업을 수행하도록 지시하거나 데이터를 요청할 때 사용됩니다.점 선
: 요청 메시지에 대한 응답 메시지를 나타냅니다, 요청 결과를 반환하거나 작업 완료 여부를 알려줍니다.
Client
: 사용자가 메시지를 전송합니다.WebSocket
: 클라이언트로부터 메시지를 받아ChatController
로 전달합니다.ChatController
: 메시지 요청을 처리하고 사용자 인증을 수행합니다.AuthenticatedUser
: 사용자 정보를 확인합니다.ChatService
: 메시지 처리 로직을 수행합니다.Database
: 메시지를 저장합니다.Redis
: 메시지를 퍼블리시하여 브로드캐스트합니다.ChatMessageListener
:Redis
로부터 메시지를 구독하고,WebSocket
을 통해 다른 사용자들에게 메시지를 브로드캐스트합니다.
퍼블리시
(Publish) : 메시지를 특정 채널에 등록해, 해당 채널을 구독하는 모든 리스너가 접근할 수 있도록 발행하는 행위입니다.브로드캐스트
(Broadcast) : 여러 대상에게 메시지를 동시 전달하는 행위로,WebSocket
을 사용하여 실시간으로 모든 클라이언트에게 메시지를 전송합니다.
기존 Redis Pub/Sub 만으로 구현된 초기 버전과 차이점
특징 | Redis Pub/Sub | 현재 시스템 |
---|---|---|
메시지 저장 | 저장되지 않음 (구독자가 없으면 메시지 유실) |
메시지를 DB에 저장하여 추후 조회 및 분석 가능 |
메시지 전송 | Redis가 구독자들에게 직접 전송 | Redis에서 메시지를 수신한 후, WebSocket으로 브로드캐스트 |
추가 로직 | 없음 | 사용자 인증, 메시지 유효성 검사, 방 존재 확인 등 처리 |
실시간성 | 빠름 | Redis와 WebSocket을 조합하여 빠른 응답 제공 |
확장성 | 단일 목적에 적합 (Pub/Sub 외 다른 기능 지원하지 않음) |
다른 시스템과 통합 가능 (예 : 채팅 알림, 통계 시스템) |
구독자가 없는 경우 | 메시지가 유실됨 | 메시지는 DB에 저장되므로 추후 클라이언트가 수신 가능 |
기존 Redis Pub/Sub만으로 보다 간단하게 구현한 실시간 채팅에서는
- 메세지 보존 : 구독자가 없을 경우 메세지가 증발해 데이터 보존이 어려움
- 확장성 : 추가적인 비즈니스 로직 처리/관리에 어려움
- 조회 분석 : 저장된 메세지 내역을 통해 조회나 분석하는 관리의 어려움
등의 한계가 있어 보다 고도화를 진행 하였습니다.
기능 설명
사용자 요청에 따라 쿠폰을 발급하고, 재고를 실시간으로 관리하며, 발급 상태를 사용자별로 저장하는 시스템입니다.
특징:
- 대량의 쿠폰 발급 요청을 처리할 수 있는 병렬 처리 방식.
- Redis를 활용한 빠른 데이터 접근과 TTL(Time-to-Live) 설정으로 데이터 만료 관리.
- 실패한 요청을 별도 큐에 저장하여 안정적으로 재처리 가능.
구현 방법
-
Redis를 활용한 대기열 처리:
대기열 생성: Redis의 List 구조(LPUSH, RPUSH)를 사용하여 사용자 요청을 큐에 저장. 대기열 처리: @Scheduled를 활용해 Redis에서 데이터를 일정 간격으로 가져와(RPOP) 병렬로 처리.
redisTemplate.opsForList().leftPush("couponQueue", jsonRequest); List<Object> batch = redisTemplate.opsForList().rightPop("couponQueue", BATCH_SIZE);
-
Redisson 락을 통한 중복 처리 방지:
분산 환경에서 여러 인스턴스가 동시에 작업하지 않도록 Redisson의 분산 락을 사용.
락이 없을 경우 작업을 건너뛰도록 구현.
RLock lock = redissonClient.getLock("couponQueueLock"); if (!lock.tryLock(10, TimeUnit.SECONDS)) { log.info("다른 인스턴스가 락을 보유 중입니다. 이번 작업은 건너뜁니다."); return; }
- 실시간 재고 관리:
Redis의 DECR 명령어를 사용해 쿠폰 재고를 관리.
재고 부족 시 요청을 실패 처리하고 대기열에서 제거.
Long remainingStock = redisTemplate.opsForValue().decrement("couponStock"); if (remainingStock == null || remainingStock < 0) { redisTemplate.opsForValue().increment("couponStock"); // 재고 복구 log.warn("쿠폰 발급 실패: 재고 부족"); }
- 발급 상태 저장 및 TTL 설정:
발급된 쿠폰의 상태(SUCCESS 또는 FAILED)를 사용자별로 Redis에 저장.
TTL(Time-to-Live) 설정을 통해 발급 상태 데이터를 24시간 유지.
redisTemplate.opsForValue().set("couponIssued:" + userId, status, 86400, TimeUnit.SECONDS);
- 실패 데이터 재처리:
처리 중 실패한 요청은 별도의 Redis 대기열(failureQueue)에 저장.
주기적으로 실패 데이터를 재처리하는 스케줄러를 실행
redisTemplate.opsForList().leftPush("failureQueue", failedRequest);
- 병렬 처리:
ExecutorService를 활용해 병렬 처리로 대량 요청을 효율적으로 처리.
작업 큐가 초과되면 호출자 스레드에서 작업을 실행하도록 정책 설정.
private final ExecutorService executorService = new ThreadPoolExecutor( 10, 100, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(500), new ThreadPoolExecutor.CallerRunsPolicy() );
주요 사용 기술
Redis: List 구조, DECR 명령어, TTL 설정. Redisson: 분산 락을 통한 데이터 충돌 방지. Java: Spring Scheduler, ExecutorService를 활용한 병렬 처리. JSON: 요청 데이터 직렬화/역직렬화(ObjectMapper).
시스템 동작 흐름
사용자 요청이 들어오면 Redis 대기열(couponQueue)에 저장. @Scheduled를 통해 주기적으로 대기열 데이터를 가져옴. Redisson 락으로 작업 충돌을 방지하며, 병렬로 요청 처리. 쿠폰 재고를 실시간으로 관리하며, 성공/실패 상태를 Redis에 저장. 실패한 요청은 failureQueue로 이동하고, 별도의 스케줄러로 재처리.
장점
고속 처리: Redis의 메모리 기반 처리로 대량의 요청도 빠르게 처리 가능. 확장성: Redisson 락과 병렬 처리를 통해 다중 인스턴스 환경에서도 안정적으로 동작. 안정성: 실패 데이터를 관리하고 재처리하여 데이터 손실 방지. 유연성: TTL 설정으로 불필요한 데이터 자동 삭제.
문제점
- Gather table에는 참조하는 테이블이 많습니다.
- map과 hashTag은 Eager Loding을 활용하였습니다.
- 검색속도 저하와 함께 N+1의 문제도 함께 발생하였습니다.
해결방안
- application-dev.yml파일에 batch size = 100으로 설정였습니다.
- map과 hashTag를 leftJoin().fetchJoin()으로 설정하였습니다.
- Gather table에 title로 새로운 인덱스를 생성하였습니다.
- Elastic Search를 도입하였습니다.
결과
- map과 hashTag를 leftJoin().fetchJoin()으로 설정하자 N+1 문제가 해결되었습니다.
- Elastic Search를 적용하고 검색하자 카테고리 검색 82.37%, 해시태그 검색 78.65%, 타이틀 검색 71.29% 개선 되었습니다.
- 일반검색 대비 Throughput 개선률이 약 4배이상 개선되었습니다.
검색 타입 | Batch 적용 개선율 | Elastic Search적용 개선율 | 전체 개선율 (일반 → DB Index) |
---|---|---|---|
카테고리 검색 | 313.85% | 8.37% | 350.00% |
HashTag 검색 | 318.46% | 9.19% | 356.92% |
문제점
- 클라이언트에서 위도,경도,nkm의 값을 받고 모임을 추천하려면 map의 모든 데이터를 하나하나 대조하는 구조였습니다..
- map의 데이터가 많아질 수록 데이터 검색속도가 많이 걸리는 것을 확인하였습니다.
- 느린 로딩 속도로 유저가 느끼는 불편함을 개선하고 운영 측면에서도 서버 비용을 감축할 필요가 있었습다.
해결방안
MySQL에 저장된 값을 Redis로 캐싱하여 GeoOperation 자료구조를 활용하여 주변 모임을 추천하였습니다.
결과
개선전 164ms에서 개선후 16ms로 약 90% 검색속도가 개선되었습니다.
기존 시스템에서는 MySQL에서 시/군/구 단위로 모임 데이터를 가져와 정렬하고, 이를 스케줄링 작업(@Scheduled
)으로 클라이언트에 제공하였습니다. 이 방식은
다음과 같은 문제가 발생했습니다:
- 서버 부하 증가: 스케줄링 작업이 많아질수록 데이터베이스 부하가 증가하여 성능이 저하될 수 있었습니다.
- 비효율적인 데이터 정렬: 시/군/구 데이터를 직접 집계 및 정렬하는 작업이 반복되면서 처리 속도가 저하되었습니다.
Redis ZSET(정렬된 집합) 구조를 활용하여 효율적으로 데이터를 관리하고 클라이언트에 제공하도록 개선하였습니다.
- ZSET 특징: ZSET은 각 데이터에 고유한
score
값을 할당하여 자동으로 정렬된 순서를 유지합니다. 이를 통해 추가적인 정렬 작업 없이도 **순위를 쉽게 확인 **할 수 있습니다.
-
데이터 집계 및 스케줄링 작업 간소화: Redis ZSET을 사용하여 별도의 스케줄링 작업 없이도 정렬된 데이터를 실시간으로 관리할 수 있습니다.
-
트래픽 분산: Redis의 고속 데이터 처리 특성을 이용해 MySQL 대신 Redis에서 순위를 관리함으로써 서버 부하를 효과적으로 분산시킬 수 있었습니다.
-
Spring batch: Spring batch를 사용하여 대용량의 데이터를 효율적이고 안정적으로 반복할 수 있을것으로 기대하하고 있습니다.
쿠폰 요청에서 member의 권한을 확인하는 로직 부분을 실행 시 쿼리가 추가적으로 연결되어있는 맵,모임,유저의 쿼리까지 같이 표시 되어 불필요한 쿼리가 조회되고 있었고 동시에 100건이 넘는 경우를 처리할때 너무 길어지는 점이 거슬렸다
해결방안
- 그 불편함을 해소하기 위하여 캐싱을 이용해보고자 하였다
@Cacheable(value = "memberPermissions", key = "#userId.toString()", unless = "#result == null") public Member getCachedMemberByUserId(UUID userId) { log.info("Fetching member from DB for userId: {}", userId); return memberRepository.findByUserId(userId) .orElseThrow(() -> new BaseException(ExceptionEnum.MANAGER_NOT_FOUND)); }
하지만 그래도 캐싱이 저장되지않아서 다른 방법을 생각해보았다
-> 이건 다른 팀원의 로직이라 다른 곳에 씌일 수 있으므로 보류
-> 다른 모든 로직들의 sql도 보이지 않으므로 반영하지않음
- MemberRepository에 query문으로 지정하여 원하는 부분만 표시 되게 반영하기
-> 다른사람의 로직은 안건드리면서 필요한 부분만 표시 할 수 있다는 점으로 반영하여 표시하였다
결과
결국 멤버의 권한을 조회할 때 memberRepository와 userReporitory를 둘 다 조회하지 않고 memberRepository에서 User와 member를 함께 조회하여 쿼리 단순화를 하게되었다
엄청난 양의 쿼리에서 필요한 부분만 조회될 수 있게 변경하였다!
- user: 사용자의 기본 정보(이메일, 닉네임, 프로필 이미지 등)와 소셜 로그인 관련 데이터 저장
- gather: 모임 정보를 저장하며, 모임의 카테고리, 좋아요 수 관리
- member: 특정 모임에 가입한 회원 정보를 저장하며, 회원의 권한(주최자, 관리자, 멤버 등) 관리
- schedule: 모임 내의 일정 정보를 저장하며, 일정 제목과 내용 포함
- board: 모임 내 공지사항과 게시글 정보를 관리
- comment: 게시글이나 일정에 대한 댓글 데이터를 저장
- likes: 모임의 좋아요(하트) 기능을 구현하기 위한 테이블
- agreement: 사용자 동의 항목(마케팅 동의, 개인정보 처리 방침 등)과 관련된 데이터 관리
- batch_job_instance 및 관련 테이블: Spring Batch를 활용한 배치 작업의 실행 정보와 로그를 저장
-
CI/CD 파이프라인:
GitHub Actions를 통해 Docker 이미지를 Amazon ECR에 업로드한 후, Amazon EC2를 사용해 수동으로 배포를 진행합니다.
배포 시 ACM(AWS Certificate Manager)를 이용해 SSL 인증서를 적용하였으며, Amazon Route 53과 연결하여 도메인을 설정했습니다. -
Database:
메인 데이터베이스로 MySQL(Amazon RDS)를 사용하며, 실시간 채팅, 랭킹, 이메일 인증 서비스는 Redis Cloud를 활용해 처리하고 있습니다.
- 프로젝트 기간(1개월)이 짧아 CI/CD 자동화 작업을 완료하지 못했습니다.
- 향후 AWS CodeDeploy나 Elastic Beanstalk를 활용하여 배포 자동화 프로세스를 구현할 계획입니다.
- 배포 자동화를 통해 운영의 효율성을 높이고, 더 빠르게 새로운 기능을 배포할 수 있도록 개선하고자 합니다.