Project Detail

Everlive

Everlive — Global Live Streaming Platform

두 달이라는 짧은 기간 동안 프론트, 백엔드를 모두 맡아 글로벌 스트리밍 서비스의 MVP를 완성하고 첫 라이브 공연을 성공적으로 운영했습니다.

  • Vimeo 기반 라이브 스트리밍 및 라이브 채팅 구축
  • PortOne, Eximbay 결제 연동
  • 글로벌 다국어 커뮤니티 UX 구축

팬들이 세계 어디에서든 라이브 공연을 즐길 수 있도록 실시간 스트리밍과 티켓 커머스를 통합한 플랫폼입니다. 짧은 기간 안에 결제, 권한 검증, 다국어 지원까지 구축했습니다.

STREAMING

글로벌 라이브 스트리밍

Vimeo Player API 위에 라이브 시청 권한 관리와 다국어 UX를 더해 전 세계 팬들이 안정적으로 공연을 감상할 수 있게 했습니다.

Vimeo API, 실시간 권한 검증
COMMERCE

티켓 커머스 & 결제

PortOne, Eximbay를 통합해 카드, 해외 결제까지 아우르는 주문/결제 플로우를 구현했습니다.

PortOne, Eximbay, 주문 관리
OPS

운영 자동화

Redis 캐시와 세션 검증으로 권한 중복을 막고, 글로벌 팬 대상 운영 공지를 빠르게 전달할 수 있도록 구성했습니다.

Redis, Admin UX, 알림
Everlive 프론트오피스 — 메인 대시보드 1
1 / 5

프로젝트 개요

Everlive는 아이돌 공연을 위한 티켓 판매 및 실시간 스트리밍 플랫폼으로, 전 세계 팬들이 공연을 실시간으로 즐길 수 있도록 설계되었습니다. 이 플랫폼은 글로벌 트래픽을 처리하기 위한 확장성과, 원활한 사용자 경험을 제공하는 데 초점을 맞췄습니다.

주요 특징

  • 실시간 스트리밍 기능: Vimeo 동영상 스트리밍 API를 활용한 실시간 스트리밍 서비스 제공
  • 티켓 관리 시스템: 온라인 티켓 구매 기능 구현
  • 안정적인 결제 시스템: PortOne을 활용한 다양한 결제 게이트웨이 연동
  • 실시간 채팅 기능: 팬과 아티스트 간 소통을 위한 채팅 및 댓글 기능 제공
  • 다국어 지원: 영어, 한국어 지원으로 글로벌 유저 접근성 강화

주요 책임 및 성과

  • RDS 전체 테이블 설계
  • 프론트/백오피스 화면 전체 개발
  • PortOne 결제 연동 및 주문 관리 시스템 구축
  • 조회가 잦은 데이터 관리를 위한 Redis 활용
  • 다국어 지원
  • Amazon S3를 활용한 정적 콘텐츠 파일 관리(고해상도 이미지, GIF 등)
  • 라이브 비디오 스트리밍 연계(Vimeo) (※ 비디오 스트리밍 기능을 직접 구현한 것은 아닙니다! 계약 전 개발 범위에 비디오 스트리밍 기능 개발까지 포함이였으나, 클라이언트의 사정으로 계약금이 1/3로 줄어들어 간단히 Vimeo를 활용하는 방향으로 변경됐습니다.)
  • 유튜브 연계를 통한 동영상 제공
  • 아티스트 응원 메시지 기능 구현
  • 공지사항, QnA, FAQ 구현

기술 스택

  • Back-end: Java 17, Spring Boot(Web)
  • Front-end: TypeScript, React, Redux-Toolkit, Styled Components
  • Database: MySQL
  • Payment: PortOne, Eximbay
  • Infra: AWS EC2, Amazon RDS, Amazon S3, Amazon ElastiCache(Redis), Docker Compose
  • Live Streaming: Vimeo

담당 역할

Everlive 프로젝트에서 프론트 오피스와 백오피스 모두에서 프론트엔드와 백엔드 개발을 맡아 실시간 스트리밍 플랫폼을 구축하였습니다. RDS 테이블 설계, 실시간 시청 시 짧은 시간 단위로 지속적인 사용자 권한 체크(Redis 활용), 결제 시스템 연동, 다국어 지원, 상품/티켓/아티스트 등등 모든 기능을 개발해 글로벌 팬 커뮤니티에 최적화된 서비스를 제공하였습니다. 첫 라이브 공연 시 시청자가 그리 많지는 않았지만 크리티컬한 문제는 발생하지 않았으며, 이후 차은우가 나오는 라이브 공연 시 시청자가 급증하였을 때도 안정적으로 서비스가 운영되었음을 Everline 사측에서 전달받았습니다.

배운 점

적은 개발 기간과 적은 인원으로 프로덕션 배포 및 운영

약 두 달이라는 제한된 짧은 기간 안에 총 두 명의 개발자로 풀스택 개발부터 출시까지 경험해본 유일한 프로젝트입니다. 물론 Everlive의 경우 그동안 경험해왔던 프로젝트들과 비교해보면 기능이 한정적이고 비교적 간단한 프로젝트였지만, 적은 인원으로 짧은 시간 동안 프로젝트를 완성해야 했던 점에서 많은 것을 배울 수 있었습니다.
같이 프로젝트를 진행하게 된 동료는 6년차 백엔드 개발자로, 응용 서버 개발에 대한 지식이 많았지만, 프론트엔드 개발에 대한 지식은 전무했습니다. 따라서 저 혼자 프론트엔드 구조 설계와 개발을 모두 맡아 진행해야 했고, 백엔드의 경우에도 동료가 AWS 인프라 구축 및 Firebase를 활용한 로그인 기능 개발을 담당했으므로 백엔드의 거의 모든 기능을 혼자서 개발해야 했습니다. 일은 너무 많은데 시간은 부족해 동료와 상의해 일부 리액트 페이지를 맡아 처리해 줄 것에 양해를 구하고, 빠르게 개발을 진행했습니다.
리액트의 State는 무엇이고, 상태 관리는 어떻게 해야 하는지, UseEffect를 활용한 라이프사이클을 어떻게 관리해야 하고 전역 상태관리를 위한 Redux-Toolkit은 어떻게 활용해야 하는지 등의 기술을 빠르게 알려주었고, 이를 통해 짧은 시간 안에 동료에게 프론트 생태계와 개발 지식을 전달해줄 수 있었습니다. 지식을 전수하면서 다시금 React 생태계를 되짚어보고, 이를 통해 동료와 함께 더욱 깊이있게 이해할 수 있었던 좋은 기회였습니다. 시간은 빠듯해도 해당 동료와 기술 얘기 하는 순간 순간이 재미있었습니다.

스프링 부트를 활용한 체계적인 개발

영풍문고 리뉴얼 프로젝트 이후로 두 번째 스프링 부트 기반 프로젝트였는데, 이번 작업을 진행하면서 스프링 부트의 안정성과 확장성을 다시금 느낄 수 있었습니다. Nodejs와 Express로 개발할 때에는 빠른 개발이 가능하다는 장점이 있지만, 경험이 적고, 누군가에게 배울 수 있는 상황이 아니라면 쉽게 안티 패턴 범벅의 프로젝트로 변모할 가능성이 높기도 합니다. 반면 스프링 부트 기반으로 개발한 웹 애플리케이션의 경우 빠른 개발보다는 프레임워크의 테두리 안에서 스프링 팀이 제안하는 방향으로 개발하다 보면 코드에 최소한의 규칙과 패턴이 녹아들어 어떤 개발자가 와도 빠르게 코드와 구조를 이해하고 적용할 수 있다는 장점이 있다는 것을 알았습니다.
물론 이는 개발자의 경험과 성숙도에 따라 코드 퀄리티가 다르겠지만, 프레임워크가 주는 안정성의 이점이 개인적으론 대단하다고 생각합니다. 이번 계기로 스프링 부트의 장점과 이를 활용해 어떻게 빠르게 개발을 진행할 수 있는지 직접 경험해보고 배울 수 있었습니다. 예를 들어, Spring Data JPA라는 성숙한 도구를 활용해 RDS 테이블과 엔터티를 손쉽게 매핑하고, 쿼리 메서드를 통해 간단하게 데이터를 가져올 수 있었고, application.yaml 설정에서 테스트를 위한 스키마 정의 및 시딩 데이터 파일 경로만 설정해주면 로컬 테스트 환경에서 앱 실행 시 테스트 데이터를 손쉽게 생성할 수 있다는 점 등이 있습니다. 그리고 코드 레벨에서는 Controller → Service → Repository로 나뉘는 Layered Architecture 구조를 제안하는 스프링 Web MVC 팀의 가이드를 통해 각 레이어의 역할과 책임을 명확히 구분하고, 이를 통해 코드의 가독성과 유지보수성을 높일 수 있다는 점도 있습니다. (전통적인 Layered Architecture의 경우 외부 요인과 도메인 간의 강한 결합이 생긴다는 문제가 있긴 합니다. 이 문제는 사이드 프로젝트인 TravelAdvisor에서 풀어내려고 노력했습니다.)

PortOne을 활용한 주문/결제 프로세스 개발

PortOne은 결제대행사(PG, Payment Gateway)를 통합해 관리하는 솔루션으로, 신용카드, 간편결제, 해외결제 등 다양한 결제수단을 제공합니다. Everlive 회원 중 해외 시청자 비중이 아주 높으므로 PG로 전 세계 대상 안전결제를 제공하는 Eximbay를 선택했습니다. 주문/결제 흐름은 다음과 같습니다. (예외 케이스 처리는 코드로 설명드리겠습니다.)
  1. [프론트] 결제 페이지에서 주문서 생성 요청
  2. [백엔드] 주문서 생성 및 PG 결제창 생성에 필요한 데이터를 응답으로 전달
1/** 2 * 주문 요청에 대한 주문서 생성 3 * 4 * @param orderRequest 주문 요청 데이터 5 * @param firebaseTokenOrNull 파이어베이스 JWT 토큰 6 * @return OrderAggregate PG 화면 생성에 필요한 데이터 7 * @throws FirebaseAuthException 8 */ 9public OrderAggregate createOrder(PostOrderRequest orderRequest, String firebaseTokenOrNull) throws FirebaseAuthException { 10 /* 11 1. 회원 조회 12 -> 가입되지 않은 회원인 경우 "가입되지 않은 회원입니다." 예외 반환 13 2. 상품 조회 14 -> 상품이 없는 경우 "존재하지 않는 상품입니다." 예외 반환 15 3. 해당 상품과 연결된 티켓 조회 16 -> 해당 상품에 연결되지 않은 티켓인 경우 "존재하지 않는 티켓입니다." 예외 반환 17 4. 주문서 생성 18 5. 선택된 티켓 아이템과 quantity 를 기반으로 금액 계산 19 6. 주문서 저장 20 7. 주문번호, 금액, 사용자 이메일 등 프론트에서 PG 화면 호출에 필요한 데이터 반환 21 */ 22 23 FirebaseToken ftoken = getFirebaseToken(firebaseTokenOrNull); 24 Member verifiedMember = memberCache.findById(Long.parseLong(ftoken.getUid())) 25 .orElseThrow(() -> new BizException("가입되지 않은 회원입니다.")); 26 27 Product product = productRepository.findByIdAndJoinTicketItemAndShowFlag(orderRequest.productId(), StringBoolEnum.Y) 28 .orElseThrow(() -> new BizException("존재하지 않는 상품입니다.")); 29 product.validateProductActivated(); 30 31 TicketItem ticketItem = ticketItemRepository.findById(orderRequest.ticketItemId()) 32 .orElseThrow(() -> new BizException("존재하지 않는 티켓입니다.")); 33 ticketItem.validateTicketItemBuyable(); 34 35 Order order = Order.of( 36 37 ... 주문서 생성에 필요한 데이터 ... 38 39 ); 40 41 // 주문서 금액 계산 42 order.calculateOrderProductTicketItem(); 43 44 Order savedOrder = orderRepository.save(order); 45 46 return OrderAggregate.of(savedOrder, userCode, product.getProductThumbnailUrl()); 47}
회원 정보의 경우 조회 빈도가 매우 잦으므로 Redis에 캐싱해 두고 이를 가져다 씁니다. 만약 cache miss가 발생한 경우 RDS에서 가져와 캐싱합니다.
  1. [프론트] 전달받은 데이터로 PG 결제창 생성 후 결제 완료 시 결제가 정상적으로 처리됐는지 검증하기 위해 사후 검증 API 호출
1/** 2 * PortOne을 활용한 PG 결제 Custom Hooks 3 * 4 * @returns OrderCreatedResponse 5 */ 6const usePortOnePayment = (): Payment => { 7 const dispatch = useDispatch() 8 9 ... PG 객체 초기화 로직 ... 10 11 // PG 결제창 생성 12 const processPayment = (orderRequest: Order, /* [A] */ onResponse) => { 13 const req = { 14 ...orderRequest, 15 currency: DEFAULT_CURRENCY, 16 language: DEFAULT_LANGUAGE, 17 pg: PG_MID, 18 bypass: { 19 ... 각종 변수 ... 20 }, 21 } 22 23 const { IMP } = window 24 IMP.request_pay(req, (response: PaymentResponse) => { 25 onResponse(response) 26 }) 27 } 28 29 return { initializePayment, processPayment } 30} 31 32/*** [B] 호출자 콜백 함수 ***/ 33/** 34 * PG 결제 처리 직후 결제 사후 검증 요청 콜백 함수 35 * 36 * @param response 결제 처리 결과 37 */ 38const onPGResponseHandler = (response: PaymentResponse) => { 39 40 ... 검증 로직 ... 41 42 validatePaymentSuccessful(requestData) 43 .unwrap() 44 .then((result) => { 45 46 ... 검증 로직 ... 47 48 dispatch( 49 setState({ 50 message: '구매 완료했습니다. 내 인벤토리 페이지로 이동합니다.', 51 redirectTo: routerPath.MY_TICKETS, 52 }), 53 ) 54 }) 55 .catch((error) => { 56 return dispatch(setMessage('결제에 실패했습니다.')) 57 }) 58}
[A] 를 보시면, 호출측에서 processPayment 함수를 호출할 때 onResponse 콜백 또한 파라미터로 받도록 합니다. 즉, 결제를 마치면 호출측에서 정의한 함수가 실행될 수 있도록 합니다. 콜백 함수 이름은 [B] onPGResponseHandler 입니다. 결제를 정상적으로 마쳤는지 알기 위해 사후 검증을 위한 백엔드 API를 호출합니다. 정상적으로 결제 처리된 경우 결제를 마치고 인벤토리 페이지로 이동하고, 실패한 경우 백엔드에서 주문서 상태를 실패로 처리하고 회원에 결제 실패를 알립니다.
  1. [백엔드] 회원이 결제를 마치고 결제 결과가 정상적인지 파악하기 위해 결제 사후 검증 수행
1/** 2 * 결제 사후 검증 3 * 4 * @param request 5 */ 6public PostPaymentValidationResponse validatePostPayment(PostPaymentValidationRequest request) { 7 log.info("PostPortoneWebHookRequest request impUid({}), merchantUid({})", request.impUid(), request.merchantUid()); 8 9 // [A-1] 10 PortonePostPaymentValidationResponse portonePostPaymentValidationResponse = portoneApiCaller.getCompletedPayment(request); 11 12 Order savedOrder = orderRepository.findById(portonePostPaymentValidationResponse.response().merchant_uid()) 13 .orElseThrow(() -> new BizException("주문서가 존재하지 않습니다.")); 14 15 // [B] 16 velidateIsEqualSavedOrderAmountAndPortoneAmount( 17 savedOrder.getTotalPaymentAmount(), portonePostPaymentValidationResponse.response().amount()); 18 19 ... 주문서 및 티켓 유효성 검증 ... 20 21 return PostPaymentValidationResponse.of(... 응답 데이터 ...); 22} 23 24/** 25 * [A-2] 26 * 결제 사후검증을 위한 PortOne 측으로부터 결제 정보 조회 27 */ 28public PostPrepareBeforePaymentResponse getCompletedPayment(PostPaymentValidationRequest request) { 29 String accessToken = portoneTokenApiCaller.getToken(); 30 31 HttpHeaders headers = new HttpHeaders(); 32 headers.setBearerAuth(accessToken); 33 HttpEntity requestEntity = new HttpEntity<>(headers); 34 35 String urlTemplate = UriComponentsBuilder 36 .fromUriString(portoneUtil.getPostPrepareBeforePaymentRequestUrl(impUid)) 37 .buildAndExpand(request.impUid()) 38 .toUriString(); 39 40 var responseEntity = restTemplate.exchange( 41 urlTemplate, 42 HttpMethod.GET, 43 requestEntity, 44 PortonePostPaymentValidationResponse.class 45 ); 46 47 return responseEntity.getBody(); 48}
클라이언트에서 결제가 완료되면 결제가 정상적으로 이루어졌는지 알기 위해 백엔드로 결제 사후 검증을 요청합니다. [A-1], [A-2] 를 보시면 백엔드에서 PortOne으로 요청을 바이패스하는 것을 보실 수 있습니다. 사후 검증이란, 결제를 마친 후 PortOne 서버에 저장된 주문 정보와 회원이 진행한 결제 정보(금액 등)를 비교해 정확히 일치하는지 확인하는 과정입니다. [B] 주문서의 결제 금액(savedOrder)과 PortOne 서버에 저장된 결제 금액(portonePostPaymentValidationResponse)이 서로 정확히 일치하는지 검증합니다. 일치하지 않는 경우 예외 메시지를 전파하고, 주문서 상태를 실패 상태로 전환합니다. [C] 금액/주문서/티켓 유효성 검증​을 마치면 주문서를 주문 완료 상태로 전환하고, 회원의 인벤토리에 구매한 티켓을 지급합니다.
KCP, 카카오페이, 페이코 등 각종 PG사와 직접 연동하는 대신, PG 통합 관리 플랫폼인 PortOne을 활용하여 매우 짧은 시간 안에 주문/결제 프로세스를 완성할 수 있었습니다. PortOne은 각종 PG의 요구사항을 대신 처리하고, 불필요한 복잡성을 감추어 추상화된 인터페이스만 제공해 개발 효율성을 크게 높여줍니다. 역시 기능이 이쁘게 잘 포장된 서비스는 많은 사람들에게 편리함과 가치를 제공한다는 것을 다시금 몸소 느낍니다.

라이브 공연 시청 중인 회원들의 주기적인 권한 체크 및 중복 세션 충돌 처리

라이브 시청 가능 시간이 되면 해당 티켓을 구매한 회원은 공연을 실시간으로 관람할 수 있게 됩니다. 여기서 발생할 수 있는 문제로는, 하나의 계정에서 티켓을 구매하고 여러 사람이 각각 본인의 디바이스로 접속해 동시에 시청하는 계정 공유의 문제입니다. 에버라인 규정상 계정당 동시시청 가능 세션 개수는 1개로 제한됩니다. 이를 해결하기 위해 Redis에 현재 로그인 중인 회원의 세션을 보관하고, 각 유저당 하나의 세션만 유지되도록 합니다. 만약 다른 곳에서 로그인에 성공한 경우 이미 로그인 된 기존의 세션을 지우고 새로운 세션을 삽입합니다. 그리고 만료된 세션으로 공연을 관람 중인 회원은 "다른기기에서 로그인되어 로그아웃 합니다." 라는 알림을 띄우고 로그아웃 처리 합니다. 코드는 다음과 같습니다.
1const { data: liveVideoDetail, error: liveVideoDetailError } = 2 useValidateHasLiveVideoAuthorizationQuery( 3 { 4 ticketItemInventoryId: location.state?.ticketItemInventoryId, 5 liveUrl: location.state?.liveUrl, 6 }, 7 { 8 // [A] 9 refetchOnMountOrArgChange: true, 10 // [B] 11 pollingInterval: 30000, 12 }, 13 ) 14 15export const liveVideoDetailApi = createApi({ 16 17 ... 생략 ... 18 19 endpoints: (build) => ({ 20 // [C] 21 validateHasLiveVideoAuthorization: build.query< 22 Response<LiveVideoDetail>, 23 Partial<LiveVideoDetail> 24 >({ 25 query: (params: LiveVideoDetail) => ({ 26 url: `ticket-inventory/live/${params.ticketItemInventoryId}/auth/${params.liveUrl}`, 27 }), 28 }), 29 }), 30}) 31 32export const { useValidateHasLiveVideoAuthorizationQuery } = liveVideoDetailApi
회원이 해당 공연 티켓에 권한이 있는지 주기적으로 확인합니다. API 호출 도구로는 Redux Toolkit Query(RTK Query)를 활용했습니다. RTK Query의 createApi 메서드를 사용하여 liveVideoDetailApi​를 정의했습니다. 이 API는 정의한 validateHasLiveVideoAuthorization 엔드포인트를 생성하며, useValidateHasLiveVideoAuthorizationQuery 라는 이름으로 API 요청 훅을 외부로 노출합니다.
  • [A]: 라이브 공연 관람 페이지에 접근하거나 useValidateHasLiveVideoAuthorizationQuery의 첫 번째 파라미터에 변동사항이 발생하면 API를 자동으로 호출합니다. 이를 통해 지속해서 티켓 권한 체크가 가능하게 됩니다.
  • [B]: 30초마다 주기적으로 API를 호출해 공연 티켓 권한을 체크합니다.
  • [C]: build.query 메서드를 사용하여 데이터를 가져오는 API 호출을 구성하며, URL은 동적으로 생성됩니다. URL은 티켓의 고유 ID와 라이브 URL을 포함하여 해당 리소스에 대한 권한을 확인합니다.
1/** 2 * 회원이 라이브 공연 티켓에 권한이 있는지 검증합니다. 3 */ 4public LiveVideoValidationAggregate validateHasLiveVideoAuthorization(Long ticketItemInventoryId, 5 String liveUrl, 6 String firebaseTokenOrNull) { 7 ... 검증 로직 ... 8 9 // [A-1] 중복 세션 검사 10 Auth authResult = memberService.validateTokenDuplicated(firebaseTokenOrNull); 11 12 TicketItemInventory myItem = ticketItemInventoryRepository.findByMemberIdAndLiveUrl( 13 ticketItemInventoryId, verifiedMember.getId(), liveUrl) 14 .orElseThrow(() -> new BizException("해당 티켓에 권한이 없습니다. 먼저 구매해주세요.")); 15 16 // [B] 구매한 티켓에 라이브 접근 권한이 있는지 검증 17 myItem.getTicketItem().validateHasLiveAuthorization(); 18 19 // 공연이 아직 진행 중인지 검증 20 myItem.getTicketItem().validateLiveVideoExpiredNotYet(); 21 22 return LiveVideoVerification.of(myItem, authResult.getToken()); 23} 24 25/** 26 * 전달받은 토큰과 Redis에 저장된 데이터를 비교하여 중복 세션 여부를 검사합니다. 27 * 28 * @param newToken 새로 전달받은 JWT 토큰 29 * @return Auth 객체 (사용자 정보와 세션 상태 포함) 30 */ 31public Auth validateTokenDuplicated(String newToken) { 32 try { 33 // Firebase 토큰 검증 및 UID 추출 34 FirebaseToken firebaseToken = validateFirebaseToken(newToken); 35 36 long memberId = Long.parseLong(firebaseToken.getUid()); 37 String existingToken = memberCache.findTokenByMemberId(memberId); 38 // [A-2] 기존 토큰이 존재하고, 새 토큰과 값이 서로 다른 경우 중복된 세션으로 간주 39 if ((!existingToken != null || !existingToken.isBlank()) && !newToken.equals(existingToken)) { 40 throw new BizException("다른기기에서 로그인되어 로그아웃 합니다."); 41 } 42 43 // Auth 객체 반환 44 return Auth.of(...) 45 } catch (FirebaseAuthException e) { 46 if (AuthErrorCode.EXPIRED_ID_TOKEN.equals(ex.getErrorCode())) { 47 throw new BizException("비디오 권한 체크 중 만료된 세션이 확인됐습니다. 토큰 재발급이 필요합니다."); 48 } 49 ... 나머지 예외 처리 ... 50 } catch (Exception e) { 51 ... 나머지 예외 처리 ... 52 } 53}
  • [A-1]: 기존에 로그인 된 회원 세션이 존재하는지 확인합니다.
  • [A-2]: Redis에 저장된 회원의 세션이 이미 존재하고, 새 토큰과 값이 서로 다른 경우 다른 기기에서 로그인 된 것으로 간주해 로그아웃 진행을 알립니다.
  • [B]: 티켓에는 세 가지 타입이 있습니다. VIDEO(촬영본 접근 권한), LIVE(라이브 공연 접근 권한), COMPLEX(촬영본, 라이브 공연 둘 다에 대한 접근 권한)​가 있으며, VIDEO → LIVE → COMPLEX 순으로 가격이 비싸집니다. 이 검증 로직에서 구매한 티켓에 LIVE 혹은 COMPLEX 권한이 있는지 검증합니다.
회원들의 티켓 권한 체크를 매 30초마다 진행하는 과정에서 Redis를 활용해 최적화 하였으며, 많은 회원이 몰리는 라이브 공연에서도 서버가 충분히 버틸 수 있음을 에버라인 측에서 확인 받았습니다.

Tech Stack

라이브 스트리밍 서비스의 핵심 기능을 빠르게 검증하기 위해 Spring Boot, PortOne, AWS 조합으로 MVP를 완성했습니다.

BACKEND

Streaming Platform Core

5

라이브 스트리밍을 위한 인증, 권한 검증, 티켓 재고 동기화를 담당했습니다.

Java 17Spring BootSpring Data JPARedisFirebase Auth

COMMERCE

Commerce & Payments

5

글로벌 결제 흐름과 주문/배송 관리, 환불 플로우를 빠르게 구축했습니다.

PortOneEximbayREST 주문 APIWebhook ValidationRedux Toolkit

OPS

Infrastructure & Ops

5

AWS 인프라 구성과 모니터링, 배포 자동화를 통해 공연 당일 안정성을 확보했습니다.

AWS EC2Amazon RDSAmazon S3Amazon ElastiCacheDocker Compose