| 회원가입 | 로그인 | 취향 선택 |
|---|---|---|
![]() |
![]() |
![]() |
| 메인화면 최신영화 | 메인화면 추천영화 | 영화 검색 |
|---|---|---|
![]() |
![]() |
![]() |
| 영화 상세조회 | 리뷰 작성 | 영화 스크랩 |
|---|---|---|
![]() |
![]() |
![]() |
| 내가 좋아한 영화 | 내가 작성한 리뷰 | 리뷰 수정 및 삭제 |
|---|---|---|
![]() |
![]() |
![]() |
| 영화관 선택 화면 | 영화 좌석 예매 화면 | 결제화면 |
|---|---|---|
![]() |
![]() |
![]() |
| 마이페이지 | 비밀번호 변경 | 아이디 찾기 |
|---|---|---|
![]() |
![]() |
![]() |
| 채팅방 리스트 | 채팅 화면 | 로그아웃 |
|---|---|---|
![]() |
![]() |
![]() |
-
기존에는 유사한 구조의 리뷰 관련 테이블이 2개로 나뉘어 있었으나, 중복을 줄이고 유지 보수를 용이하게 하기 위해 하나의 테이블로 통합하였습니다.
-
또한, 영화 스크랩과 리뷰 스크랩은 다른 도메인이므로 구분하여 관리할 수 있도록 별도의 테이블로 분리하였습니다.
-
MovieScrap: 사용자(User)와 영화(Movie) 간의 스크랩 관계 (User 1:N MovieScrap, Movie 1:N MovieScrap)
-
ReviewScrap: 사용자(User)와 리뷰(Review) 간의 스크랩 관계 (User 1:N ReviewScrap, Review 1:N ReviewScrap)
기존의 예매 테이블은 단순히 날짜 및 좌석 정보만 저장하고 있어, 실제 영화 예매 시스템의 흐름을 반영하기 어려웠습니다. 이에 따라 일반적인 영화 예매 플로우를 기준으로 필요한 테이블들을 재정의하였습니다.
사용자와 관리자가 실시간으로 소통할 수 있는 1:1 채팅 기능을 구현하였습니다. MongoDB를 사용해 비관계형 데이터 구조로 채팅 데이터를 저장하고 있으며, 가독성을 위해 ERD 형식으로 시각화하였습니다. Oracle 기반으로 기존 설계되었던 데이터베이스와의 직접적인 연동은 없습니다.
기존에는 Spring Security의 세션 기반 인증을 사용하였으나, JWT 기반 인증 방식으로 전환한 이유는 다음과 같습니다.
-
본 프로젝트는 웹뿐만 아니라 모바일 애플리케이션과의 연동 가능성까지 고려하여 설계되었습니다.
-
세션 기반 인증은 서버 측 상태 저장이 필요하고 확장성에 한계가 있는 반면, JWT는 Stateless 구조로 클라이언트가 토큰을 보관하며, 모바일 환경에서도 효율적인 인증 처리가 가능합니다.
결과적으로 JWT 기반 인증을 통해 유지 보수성과 확장성을 높일 수 있었습니다.
- 기존 추천 API /main/recommend는 Spark ALS 기반 연산을 요청마다 수행함
- 평균 응답 18.4초 → 사용자가 매번 기다려야 하는 심각한 UX 문제
- 원인: ALS 연산(학습/예측)이 CPU·I/O를 많이 사용하며, 매 요청 시 재계산하는 구조
첫 요청: 캐시 MISS → ALS로 추천 계산 → Redis 저장(24h TTL)
이후 요청: 캐시 HIT → Redis에서 즉시 응답
| 항목 | 캐시 미적용 | Redis 캐시 적용 |
|---|---|---|
| 평균 응답 시간 | 18,426ms(~18초) | 3ms |
| 속도 향상 | - | 🔥 6000배 |
-
키 전략: recommend:{userId}
-
TTL: 24시간 (데이터 신선성 확보 + 과도한 재계산 방지)
-
패턴: Cache-aside (애플리케이션이 캐시를 직접 관리)
-
Controller → MovieCacheRecommenderService#getRecommendations(userId) 호출
-
Redis 조회(HIT면 즉시 반환)
-
MISS면 Spark ALS로 계산 → JSON 직렬화해 Redis에 저장(TTL=1day)
-
이후 동일 사용자 요청은 캐시에서 반환(3ms)
public List<String> getRecommendations(int userId) {
String key = "recommend:" + userId;
String cachedJson = (String) redisTemplate.opsForValue().get(key);
if (cachedJson != null) {
System.out.println("[CACHE HIT] " + key);
try {
return objectMapper.readValue(cachedJson, List.class);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 파싱 실패", e);
}
}
System.out.println("[CACHE MISS] " + key);
// 미스 → 계산 후 JSON 저장
List<String> result = recommendMovies(userId);
try {
redisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(result), 1, TimeUnit.DAYS);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON 직렬화 실패", e);
}
return result;
}
}
java.lang.IllegalAccessError: class org.apache.spark.storage.StorageUtils$ cannot access class sun.nio.ch.DirectBuffer
- JDK 9+ 부터 내부 패키지 접근 제한 → Spark 2.x와 충돌
--add-exports java.base/sun.nio.ch=ALL-UNNAMED
Spark 내부 클래스에서 DirectBuffer 접근 허용
| 주제 | 담당자 | 관련 문서 |
|---|---|---|
| 세션 기반 인증에서 JWT로 전환 회고 | 허민영 | 바로가기 🔗 |
| Spring Security + JWT 사용자 인증(1) - 로그인 & 토큰 발급 | 허민영 | 바로가기 🔗 |
| Spring Security + JWT 사용자 인증(2) - Spring Security 설정 및 필터 적용 | 허민영 | 바로가기 🔗 |
| Spring Security + JWT 사용자 인증(3) - Refresh Token을 활용한 인증 갱신 | 허민영 | 바로가기 🔗 |
| 분야 | 기술 |
|---|---|
| Language | Java 17 |
| Framework | Spring Boot 2.7.18 |
| ORM | MyBatis |
| Database | OracleDB, MongoDB |
| Security | Spring Security, JWT (JJWT 0.11.5) |
| WebSocket | Spring WebSocket |
| Data Store | Redis |
| ML/추천엔진 | Apache Spark (spark-core, spark-mllib) |
| Validation | Hibernate Validator |
| Test | JUnit, Spring Security Test |
| Build Tool | Maven |

























