Skip to content

gitaehee/spring-cgv-23rd

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

61 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

spring-cgv-23rd

CEOS 23기 백엔드 스터디 - CGV 클론 코딩 프로젝트

2주차 DB 접근기술, MVC 아키텍처

1️⃣ DB를 모델링해봐요!

ERD 다이어그램 영화관 ||--o{ 상영관

상영관 ||--o{ 좌석

상영관 ||--o{ 상영일정

영화 ||--o{ 상영일정

상열일정 ||--o{ 예매내역

좌석 ||--o{ 상영일정

사용자 ||--o{ 예매내역

3️⃣ CGV의 4가지 HTTP Method API 만들어요

postman 연결

5️⃣ Global Exception를 만들어봐요

없는 아이템 not found로 나오게

6️⃣ Swagger 연동 후 Controller 통합 테스트를 해봐요

swagger 연동 성공

Post Post

Get 전체 Get 전체

Get 한 개 Get 한 개

Delete Delete

다시 Get 해보면 Get 다시

7️⃣ Service 계층의 단위 테스트를 진행해요

테스트 완료
3주차 보안 & Spring Security

(저번주에 이어서..)

ERD 새 다이어그램 erd 다시 그림 h2 h2 실행 h2 connect 하면 이렇게 뜸 h2 테이블 명 한 번 더 보고 인서트 준비 h2 테스트 데이터 insert postman 영화관 post postman 영화 post postman 매점 post

1️⃣JWT 인증(Authentication) 방법에 대해서 알아보기

1. JWT 인증 방식이란?

JWT -> 로그인한 사용자인지 확인하기 위해 사용하는 토큰 방식

사용자가 로그인하면 서버가 토큰을 만들어 주고, 사용자는 이후 요청마다 이 토큰을 함께 보내서 자신을 인증함

Access Token
  • 실제 API 요청할 때 쓰는 토큰
    • 보통 유효기간이 짧다
Refresh Token
  • Access Token이 만료됐을 때 새 Access Token을 받기 위해 쓰는 토큰
    • Access Token보다 보통 더 오래 간다
JWT 방식의 장점
  • 서버가 로그인 상태를 따로 많이 저장하지 않아도 된다
    • 모바일 앱이나 프론트/백 분리 구조에 잘 맞는다
JWT 방식의 단점
  • 토큰이 탈취되면 위험할 수 있다
    • 한 번 발급된 토큰은 바로 무효화하기가 어렵다

2. 세션과 쿠키

쿠키

쿠키 -> 브라우저에 저장되는 작은 데이터

로그인 정보를 유지할 때 많이 사용된다.

세션

세션 -> 서버가 로그인 상태를 저장하는 방식

브라우저는 보통 쿠키에 담긴 세션 ID를 보내고, 서버는 그걸 보고 누가 로그인했는지 확인

장점
  • 서버가 로그인 상태를 직접 관리해서 제어하기 쉽다
    • 로그아웃 처리나 강제 만료가 비교적 쉽다
단점
  • 사용자가 많아지면 서버가 세션을 계속 관리해야 해서 부담이 생긴다

3. OAuth

OAuth -> 다른 서비스의 계정을 이용해 로그인하거나 권한을 위임하는 방식 ex)

  • 카카오 로그인
    • 구글 로그인
    • 네이버 로그인

즉, 우리 서비스가 사용자의 비밀번호를 직접 받지 않고 다른 서비스의 인증 기능을 빌려 쓰는 것

장점
  • 회원가입/로그인이 편하다
    • 비밀번호를 우리 서비스가 직접 관리하지 않아도 된다
단점
  • 구현이 조금 더 복잡하다
    • 외부 서비스 정책에 영향을 받을 수 있다

4. 방식 비교

방식 설명 장점 단점 주로 쓰는 곳
JWT 토큰을 이용해 사용자를 인증하는 방식 서버 부담이 비교적 적고, 프론트/백 분리 구조에 잘 맞음 토큰 탈취 시 위험하고, 바로 무효화하기 어려움 모바일 앱, REST API, SPA
세션 + 쿠키 서버가 로그인 상태를 저장하고, 브라우저는 쿠키로 세션 ID를 보냄 서버가 로그인 상태를 직접 관리해서 제어하기 쉬움 서버가 세션을 계속 관리해야 해서 부담이 생길 수 있음 전통적인 웹 서비스
OAuth 다른 서비스 계정으로 로그인하거나 권한을 위임하는 방식 소셜 로그인이 가능하고, 비밀번호를 직접 관리하지 않아도 됨 구현이 더 복잡하고 외부 서비스에 영향을 받을 수 있음 카카오 로그인, 구글 로그인, 네이버 로그인

5. 정리

  • JWT: 토큰으로 로그인 상태를 확인하는 방식
    • Access Token: 실제 요청에 사용하는 토큰
    • Refresh Token: Access Token을 다시 발급받기 위한 토큰
    • 세션: 서버가 로그인 상태를 저장하는 방식
    • 쿠키: 브라우저에 저장되는 데이터
    • OAuth: 구글/카카오 같은 외부 계정으로 로그인하는 방식
auth 회원가입
4주차 Java와 OOP

동시성 해결 방법에 대해 조사하고 적용하기

동시성 해결 방법 조사

  1. synchronized

    • JVM 내부에서만 동작하는 락 방식
    • 단일 서버에서는 사용할 수 있지만, 서버가 여러 대면 한계가 있다.
    1. DB Lock

      • DB row에 락을 걸어 동시에 수정되지 않도록 제어한다.
      • 재고 차감처럼 같은 데이터를 동시에 수정하는 경우에 적합하다.
    2. Unique 제약

      • DB에서 중복 저장 자체를 막는 방식이다.
      • 같은 상영 회차의 같은 좌석처럼 절대 중복되면 안 되는 데이터에 적합하다.
    3. Redis 분산락

      • 여러 서버 간 동시성 제어에 사용할 수 있다.
      • 다만 Redis 인프라가 필요해 현재 프로젝트 규모에서는 과하다고 판단했다.

문제 상황

CGV 서비스에서는 여러 사용자가 동시에 상품을 주문하거나 좌석을 예매할 수 있다.
이때 재고가 중복 차감되거나 같은 좌석이 중복 예약되는 문제가 발생할 수 있다.

적용 방법

대상 적용 방법 이유
상품 재고 비관적 락 PESSIMISTIC_WRITE 같은 재고에 동시 주문이 들어올 수 있기 때문
좌석 예매 (screening_id, seat_id) unique 제약 같은 상영 회차의 같은 좌석은 한 번만 예약되어야 하기 때문

변경 내용

  • TheaterItemStockRepository에 락 조회 메서드 추가
    • ItemOrderService에서 재고 조회 시 락 메서드 사용
    • TheaterItemStock에서 재고 검증과 차감 처리
    • Reservation에 unique 제약 추가
    • ReservationService에서 중복 예약 예외 처리
    • 동시성 테스트 추가

테스트

테스트 결과
재고 1개에 동시 주문 2건 요청 시 1건만 성공 성공
같은 좌석에 동시 예매 2건 요청 시 1건만 성공 성공

결제 시스템 연동

구분 대상 특징
티켓팅 좌석 예매 Reservation 좌석을 먼저 선점하고, 결제 성공 시 예매 확정
커머스 매점 주문 ItemOrder 결제 성공 후 재고 차감 및 주문 확정

티켓팅과 커머스는 결제 흐름이 다르기 때문에 각각 다른 방식으로 처리했다.

결제 연동 방식

외부 결제 서버 연동에는 OpenFeign을 사용했다.

방식 특징
Feign 인터페이스 기반으로 외부 API를 간단하게 호출할 수 있음
RestClient/WebClient 세밀한 제어는 가능하지만 요청/응답 코드가 많아짐

이번 프로젝트에서는 결제 서버 명세가 정해져 있고, API 호출 구조가 단순하기 때문에 Feign을 선택했다.

적용 파일:

  • PaymentFeignClient
    • PaymentGateway
    • PaymentGatewayImpl
    • PaymentProperties
    • payment/dto/*

커머스 결제 흐름

매점 주문은 결제가 성공한 뒤 재고를 차감하도록 구성했다.

주문 요청
→ 주문 생성(PENDING_PAYMENT)
→ 결제 서버 즉시 결제 요청
→ 결제 성공 시 재고 차감
→ 주문 상태 PAID 변경

Feign Client / Http Client 장단점

방식 장점 단점
Feign Client 선언형 인터페이스 방식이라 코드가 간결하고 API 명세를 매핑하기 쉽다. 리트라이, 커넥션 제어 등 세밀한 설정은 추가 구성이 필요하다.
RestClient / WebClient 요청 흐름을 직접 제어하기 쉽고, 세밀한 커스터마이징에 유리하다. 요청/응답 처리와 에러 파싱 코드를 직접 작성해야 해서 구현량이 많아진다.

이번 과제 적합:

  • Feign Client: 명세 기반 연동이 빠르다.
  • RestClient / WebClient: 학습에는 좋지만 구현량이 많다.
5주차 Deploy

프로젝트 마무리

1. 진행 내용

  • 외부 결제 API 연동 방식을 OpenFeign에서 RestClient로 변경했다.
  • RestClient의 onStatus()를 사용해 결제 API 응답 상태 코드별 예외 처리를 추가했다.
  • 결제 API Secret Key를 캐싱하도록 변경하고, 동시 요청 상황을 고려해 volatilesynchronized 기반 더블 체크 락킹을 적용했다.
  • 결제 관련 DTO를 record로 변경해 불변 객체로 관리하도록 수정했다.
  • 결제 API 호출이 트랜잭션 내부에서 실행되지 않도록 예약 결제 로직의 트랜잭션을 분리했다.
  • 만료된 예약 삭제 로직을 별도 트랜잭션으로 분리하고, RESERVATION_EXPIRED 에러 코드를 추가했다.
  • 매점 주문에서도 재고 선점/차감, 결제 호출, 결제 결과 반영을 분리했다.
  • 결제 실패 시 주문 상태를 실패로 변경하고, 선점한 재고를 복구하도록 처리했다.
  • 여러 상품 주문 시 데드락을 줄이기 위해 itemId 기준으로 정렬한 뒤 재고 락을 획득하도록 수정했다.
  • 재고 락 조회에 PESSIMISTIC_WRITE와 락 타임아웃을 적용했다.
  • IllegalArgumentException으로 처리하던 예외를 CustomExceptionErrorCode 기반으로 통일했다.
  • 컨트롤러에서 반복되던 Long.parseLong(userDetails.getUsername()) 로직을 제거하고, CustomUserDetailsgetUserId()를 사용하도록 변경했다.
  • JWT subject가 숫자가 아닐 경우를 대비해 INVALID_TOKEN_SUBJECT 예외 처리를 추가했다.
  • 좌석 예매 중복 방지를 위해 좌석 조회 시 PESSIMISTIC_WRITE 비관적 락을 적용했다.

2. 수동 배포

도커 이미지 생성 도커 이미지 생성

도커 허브에 올리기 도커 허브에 올리기

ec2 접속 ec2 접속

배포 완료 배포 완료

도커 컴포즈 방식으로 변경 도커 컴포즈 방식으로 변경

3. CI/CD

github actions으로 자동 배포 적용 깃허브 액션

느낀 점

  • 팸키들을 폴더 내 ssh에 저장해두는 게 마음이 편해서 좋은 방법 같았다
  • EC2안에서 컨테이너가 생성이 잘 안 됐는데 arm이랑 x86 아키텍처가 맞지 않았던 문제였다
    • -> 이미지 다시 만들면 해결 됨
  • 펨키 안 보이면 명령어로 cat ~/.ssh/ceos-cgv-key.pem하고 -----BEGIN ... PRIVATE KEY----- 줄도 다 포함해서 전체 다 복사해야함
6주차 부하테스트

1. CGV 서비스 아키텍처 구조도 그려보기

구조도에서 병목 지점 예상 구조도

2. 부하테스트 결과 분석하기

k6 실행 테스트 API: GET /api/movies 1차 테스트

  • 최대 VU: 200
  • 총 요청 수: 13,620건
  • 처리량: 75.4 req/s
  • 평균 응답시간: 147.02ms
  • p95 응답시간: 156.78ms
  • 최대 응답시간: 909.71ms
  • CPU: 0.13%
  • Memory: 310.1MiB / 908.7MiB
  • Memory %: 34.13%
  • PIDS: 50
  • NET I/O: 16.2MB / 28MB

K6 대시보드 대시보드 대시보드 대시보드

  • Iteration Rate: 36/s
  • HTTP Request Rate: 30.5/s
  • HTTP Request Duration: 149ms
  • HTTP Request Failed: 0.0%
  • Received Rate: 14.5 kB/s
  • Sent Rate: 10.6 kB/s

느낀점

  • k6실행할 때 EC2의 .env안에 JWT_SECRET값이 없었어서 애를 먹었다
7주차 캐싱 & 로깅

1. 캐시 도입하기

기존에는 GET /api/movies 요청이 들어올 때마다 DB에서 영화 목록을 다시 조회하는데, 영화 목록은 (다른 데이터들 보다) 자주 바뀌는 데이터가 아니라고 생각하여, 매번 DB를 조회하는 것보다 한 번 조회한 결과를 잠깐 저장해두고 다시 사용하는 것이 더 효율적이라고 판단하였다. 그래서 영화 조회를 담당하는 MovieService에 캐시를 적용했다.

적용 위치

캐시는 MovieService에 적용했다.

파일 메서드 적용 내용 설명
MovieService.java findAll() @Cacheable 전체 영화 목록을 캐시에 저장
MovieService.java findById(Long id) @Cacheable 영화 하나를 조회한 결과를 캐시에 저장
MovieService.java create() @CacheEvict 새 영화가 추가되면 기존 캐시 삭제

동작 방식

  • Caffeine을 사용하여 서버 내부에 데이터를 임시로 저장하는 방식으로 캐시 적용
  • 처음 영화 목록 조회 시 DB에서 데이터 조회
  • 조회한 영화 목록 결과를 캐시에 저장
  • 이후 같은 영화 목록 조회 요청이 들어오면 DB를 다시 조회하지 않음
  • 캐시에 저장된 영화 목록 데이터를 바로 반환
  • 영화가 새로 추가될 경우 기존 캐시에 새 영화가 반영되지 않을 수 있음
  • create() 메서드에 @CacheEvict 적용
  • 새 영화 추가 시 기존 영화 캐시 삭제
  • 다음 영화 목록 조회 요청 시 DB에서 최신 영화 목록을 다시 조회
  • 다시 조회한 최신 결과를 캐시에 저장

2. 로그 리팩토링 하기

GlobalExceptionHandler 로그 추가

CustomException이 발생했을 때 errorCodemessage가 로그에 남도록 수정했다.

  • 기존: 에러 응답만 반환
  • 개선: 에러 응답 반환 + 서버 로그 기록

Grafana 대시보드 지표

  • API 요청 수
  • API 평균 응답 시간
  • 4xx / 5xx 에러 수
  • ErrorCode별 발생 횟수
  • 매점 주문 성공 수
  • 결제 실패 수
  • 락 획득 실패 수

About

CEOS 23기 백엔드 스터디 - CGV 클론 코딩 프로젝트

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • Java 99.2%
  • Other 0.8%