diff --git a/README.md b/README.md
index 8e0bb157..4d285a66 100644
--- a/README.md
+++ b/README.md
@@ -306,3 +306,146 @@ Feign Client란 Netflix에서 개발한 Http Client다. (HttpClient는 Http 요
- 모니터링 대시보드
+
+
+# 트랜잭션
+- 트랜잭션은 시작 지점 & 종료 지점이 존재
+- 시작 방법은 1가지지만, 끝나는 방법은 commit과 rollback 2가지
+
+# 트랜잭션 전파 속성 (Transaction Propagation)
+- **전파 속성** : 트랜잭션이 이미 진행 중일 떄 추가 트랜잭션 진행을 어떻게 할지 결정하는 것
+
+## 물리 트랜잭션 / 논리 트랜잭션
+- 트랜잭션은 커넥션 객체를 통해 처리하기 때문에, 1개의 트랜잭션을 사용하는 것은 하나의 커넥션 객체를 사용한다는 것
+- 실제 데이터베이스의 트랜잭션을 사용한다는 점에서 -> 물리 트랜잭션이라고도 함
+ - 물리 트랜잭션 : 실제 커넥션에 롤백/커밋을 호출하는 것. 즉 해당 트랜잭션이 끝나는 것
+
+- 스프링의 입장에서는 트랜잭션 매니저를 통해 트랜잭션을 처리하는 곳이 2군데이기 때문에, 실제 데이터베이스 트랜잭션과 스프링이 처리하는 트랜잭션 영역을 구분하기 위해 스프링은 논리 트랜잭션이라는 개념을 추가함
+- 예) 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션(=커넥션)을 사용하는 경우
+ -
+ - 2개의 트랜잭션 범위 존재 -> 따라서 개별 논리 트랜잭션이 존재. 하지만 실제로는 1개의 물리 트랜잭션 사용
+
+따라서 개념을 정리하면
+- 물리 트랜잭션: 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위
+- 논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위
+
+- 기존의 트랜잭션이 진행중일 때 또 다른 트랜잭션이 사용되면 복잡해짐 => 스프링: 논리 트랜잭션이라는 개념을 도입
+ - 원칙 1. 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됨
+ - 원칙 2. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됨
+
+
+
+## 스프링의 트랜잭션 전파 속성
+
+### 1. REQUIRED
+
+- 스프링이 제공하는 DEFAULT 전파 속성
+- 기본적으로 2개의 논리 트랜잭션을 묶어 하나의 물리 트랜잭션을 사용하는 것 (이전 예시 사진)
+- 내부 트랜잭션은 기존에 존재하는 외부 트랜잭션에 참여(이어 나감)하게 되며, 외부 트랜잭션의 범위가 내부 트랜잭션까지 확장됨. 따라서 내부 트랜잭션은 새로운 물리 트랜잭션을 사용하지 않음
+- 커밋은 내부 1회, 외부 1회 총 2회 실행되고 물리 트랜잭션을 관리하는 외부 트랜잭션이 최종적으로 커밋될 때 실제로 커밋됨
+- 롤백또한 내부 트랜잭션에서 롤백해도 즉시 롤백 X, 물리 트랜잭션이 롤백될 때 실제 롤백처리 됨.
+ - 외부 트랜잭션의 롤백이 필요한 경우 : 내부 트랜잭션의 커밋/롤백 여부와 무관하게 물리 트랜잭션에서 롤백 시킴
+ - 내부 트랜잭션에서 롤백이 필요한 경우(외부 t는 커밋 O) : 내부 트랜잭션에서 UnexpectedRollbackException 예외를 통해 롤백 필요 알림 -> 외부 트랜잭션에서 이를 바탕으로 롤백
+
+### 2. REQUIRES_NEW
+
+- 외부/내부 트랜잭션을 완전히 분리하는 전파 속성
+- 2개의 물리 트랜잭션 사용. 각각 트랜잭션 별로 커밋과 롤백이 수행
+- 따라서 내부 트랜잭션 롤백이 외부 트랜잭션 롤백에 영향을 주지 않음
+- 서로 다른 물리 트랜잭션 == 각각의 디비 커넥션 사용 (== 1개의 HTTP 요청에 대해 2개의 커넥션 사용)
+ - 따라서 내부 트랜잭션 처리 중이 꺼내진 외부 트랜잭션은 대기 -> 커넥션 고갈 시킬 수 있기 때문에 주의
+
+### 3. SUPPORTS
+- 이미 시작된 트랜잭션이 있으면 참여 / 없으면 트랜잭션 없이 진행
+- 트랜잭션이 없지만 해당 경계 내에서 커넥션이나 Hibernate Session 등을 공유 가능
+
+### 4. MANDATORY
+- REQUIRED와 유사. 이미 시작된 트랜잭션이 있으면 참여
+- 하지만 트랜잭션이 없다면 생성하는 것이 아니라 예외를 발생
+ - 따라서 혼자서 독립적으로 트랜잭션을 실행하면 안되는 경우에 사용
+
+### 5. NOT_SUPPORTED
+- 트랜잭션 사용 X
+- 이미 진행 중인 트랜잭션이 있으면 보류시킴
+
+### 6. NEVER
+- 트랜잭션을 사용하지 않도록 강제
+- 이미 진행 중인 트랜잭션도 존재하면 안됨. 있다면 예외 발생
+
+### 7. NESTED
+- 이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션 시작
+ - 독립적인 트랜잭션을 만드는 REQUIRES_NEW와 다름
+- 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋/롤백에는 영향을 받지만, 자신의 커밋/롤백은 부모에 영향 X
+
+
+
+
+
+# 데이터베이스 인덱스
+## 1. Clustered index
+- 데이터베이스 테이블의 물리적인 순서를 인덱스의 키 값 순서대로 정렬하는 것
+- 테이블 당 하나만 존재
+ - 테이블의 데이터가 하나의 순서로만 정렬될 수 있기 때문
+- 데이터의 물리적 순서와 인덱스 순서가 같음 -> 빠른 검색 가능
+- 실제 데이터와 함께 저장되기 때문에 불필요한 디스크 공간을 추가적으로 유발하지 않음
+- 기본적으로 MySQL 에서 InnoDB 스토리지 엔진을 사용하는 경우 테이블은 기본 키를 클러스터드 인덱스로 자동으로 사용
+
+## 2. Non-Clustered Index
+
+- 데이터가 저장된 테이블과 별도의 공간에 위치. 데이터의 위치를 가리키는 포인터를 사용해 데이터를 찾음
+ - 별도의 인덱스 테이블에 추가된 인덱스 값과 해당 데이터의 포인터가 저장됨
+- 테이블의 데이터와 다른 순서로 정렬될 수 있음
+- 인덱스가 테이블의 데이터와는 다른 별도의 저장 공간에 저장되며, 인덱스가 테이블 데이터와 다른 구조를 가짐
+- 하나의 테이블에 대해 여러 개의 Non-Clustered Index 생성 가능
+ - 따라서 다양한 검색 요구 사항에 맞춰 여러 인덱스 사용 가능
+- Non-Clustered Index는 특정 컬럼에 대한 검색 최적화 가능
+- PK 외에 다른 유니크 인덱스가 설정되는 경우, Non-Clustered Index로 unique index가 배치됨
+
+## 3. B-Tree Index
+
+
+- 모든 리프 노드가 같은 레벨이기 때문에 데이터베이스에서 검색/삽입/삭제 등의 동작이 일정 시간 안에 이뤄짐
+- 키 값들이 정렬된 상태로 유지되어 빠르고 효율적인 검색이 가능
+- 데이터 삽입/삭제 시 트리의 균형을 자동으로 맞추기 때문에 동적인 데이터 조정이 이뤄짐
+
+## 4. Hash Index
+
+- 해시 테이블 기반
+- 빠른 데이터 검색을 위해 사용되는 인덱스 유형 중 하나
+- 특정 키 값을 해시 함수를 통해 해시 코드로 변환
+- 하지만 DB 인덱스에서 해시 테이블이 사용되는 경우는 제한적
+ - 해시가 등호(=) 연산에만 특화되었기 때문이며
+ - 해시 함수는 값이 1이라도 달라지면 완전히 다른 해시 값을 생성하는데, 이러한 특성에 의해 부등호 연산(>, <)이 자주 사용되는 데이터베이스 검색을 위해서는 해시 테이블이 적합하지 않음
+
+## 5. Bitmap Index
+- 각각의 값에 대해 비트 배열 생성 (해당 값에 행 존재 시 : 1, 없으면 : 0)
+- 예) 각 성별에 대해 비트 배열을 생성할 때, 성별 속성에 대한 남자 비트맵은 1 0 0 1 0
+ - 따라서 해당 테이블 내에서 남자를 탐색하는 경우 비트맵 인덱스만 보고도 1, 4번째가 남자임을 알 수 있음
+
+## 6. Full-Text Index
+- 데이터베이스 내의 텍스트 데이터에서 키워드 검색을 가능하게 하는 인덱스 유형
+- 대량의 텍스트 데이터에서 특정 단어나 문구를 빠르게 찾아내는 기능
+- 주로 뉴스 기사, 책, 블로그 글 등의 텍스트 데이터를 검색할 때 사용
+- 텍스트 데이터를 분석해 중요 데이터(키워드)를 추출하고, 이 단어들의 데이터베이스 내 위치를 기록하여 컴색 쿼리가 수행되는 경우 해당 인덱스를 사용해 빠르게 데이터를 검색함
+
+### 장점
+- 게시물 내용을 효율적으로 검색 가능
+- 복잡한 쿼리 지원
+ - 문구 검색, 불용어(별로 중요하지 않은 단어, 조사 등..을 후순위로 처리), 동의어 처리 등 복잡한 텍스트이 검색 요구사항 지원
+### 단점
+- 저장공간을 많이 사용함
+- 텍스트 데이터가 자주 변경되면 업데이트 비용이 큼
+- 대안이 많다!
+
+## 7. Composite Index
+- 두 개 이상의 컬럼을 결합해 생성한 인덱스
+- 특정 쿼리 연산에서 여러 컬럼을 동시에 사용할 때 검색 성능을 향상시키기 위해 사용
+- 지정된 컬럼들의 조합으로 구성되며, 인덱스 내에서는 해당 컬럼들이 정의된 순서대로 데이터를 정렬함
+
+### 장점
+- 여러 컬럼을 조건으로 하는 쿼리에서 검색 성능이 크게 향상
+- 정렬 그룹화 최적화 가능
+- 인덱스 커버링
+ - 쿼리가 인덱스에 포함된 컬럼만을 사용하는 경우, 실제 데이터에 접근하지 않고 인덱스에서 결과를 바로 가져올 수 있음
+### 단점
+- 별도 공간, 유지관리 비용
diff --git a/src/main/java/com/ceos22/cgv_clone/domain/store/repository/ProductOrderItemRepository.java b/src/main/java/com/ceos22/cgv_clone/domain/store/repository/ProductOrderItemRepository.java
index 68e80f34..1e28e342 100644
--- a/src/main/java/com/ceos22/cgv_clone/domain/store/repository/ProductOrderItemRepository.java
+++ b/src/main/java/com/ceos22/cgv_clone/domain/store/repository/ProductOrderItemRepository.java
@@ -2,10 +2,20 @@
import com.ceos22.cgv_clone.domain.store.entity.ProductOrderItem;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ProductOrderItemRepository extends JpaRepository {
+ // 주문 아이템을 찾을 때 Product 정보도 JOIN FETCH 같이
+ @Query("""
+ SELECT poi FROM ProductOrderItem poi
+ JOIN FETCH poi.product
+ WHERE poi.productOrder.id = :orderId
+ """)
+ List findByProductOrderIdWithProduct(@Param("orderId") Long orderId);
+
List findByProductOrderId(Long orderId);
}
diff --git a/src/main/java/com/ceos22/cgv_clone/domain/store/service/ProductOrderQueryService.java b/src/main/java/com/ceos22/cgv_clone/domain/store/service/ProductOrderQueryService.java
index ab54dd72..cc388746 100644
--- a/src/main/java/com/ceos22/cgv_clone/domain/store/service/ProductOrderQueryService.java
+++ b/src/main/java/com/ceos22/cgv_clone/domain/store/service/ProductOrderQueryService.java
@@ -137,7 +137,7 @@ public ProductOrderDetailDto getOrder(Long orderId) {
ProductOrder order = productOrderRepository.findDetailsById(orderId)
.orElseThrow(() -> new CustomException(ErrorCode.ORDER_NOT_FOUND));
- List items = productOrderItemRepository.findByProductOrderId(orderId);
+ List items = productOrderItemRepository.findByProductOrderIdWithProduct(orderId);
return new ProductOrderDetailDto(
order.getId(),
diff --git a/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/CinemaRepository.java b/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/CinemaRepository.java
index 074250f2..fa2f2237 100644
--- a/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/CinemaRepository.java
+++ b/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/CinemaRepository.java
@@ -2,8 +2,8 @@
import com.ceos22.cgv_clone.domain.common.enums.Region;
import com.ceos22.cgv_clone.domain.theater.entity.Cinema;
+import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
-import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
@@ -13,11 +13,7 @@ public interface CinemaRepository extends JpaRepository {
List findByRegion(Region region);
- @Query("""
- SELECT c FROM Cinema c
- JOIN FETCH c.auditoriums
- WHERE c.id = :id
- """)
+ @EntityGraph(attributePaths = {"auditoriums", "auditoriums.auditoriumType"})
Optional findByIdWithAuditoriums(@Param("id") Long cinemaId);
}
diff --git a/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/SeatRepository.java b/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/SeatRepository.java
index a09f600b..0546bc52 100644
--- a/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/SeatRepository.java
+++ b/src/main/java/com/ceos22/cgv_clone/domain/theater/repository/SeatRepository.java
@@ -1,5 +1,6 @@
package com.ceos22.cgv_clone.domain.theater.repository;
+import com.ceos22.cgv_clone.domain.theater.dto.SeatStatusDto;
import com.ceos22.cgv_clone.domain.theater.entity.Seat;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
@@ -22,4 +23,22 @@ public interface SeatRepository extends JpaRepository {
@Query("SELECT s FROM Seat s WHERE s.id IN :ids")
List findAllByIdWithLock(@Param("ids") List ids);
+
+ @Query("""
+ SELECT new com.ceos22.cgv_clone.domain.theater.dto.SeatStatusDto(
+ s.id,
+ s.rowNo,
+ s.columnNo,
+ (bs.id IS NOT NULL)
+ )
+ FROM Seat s
+ LEFT JOIN BookingSeat bs
+ ON s.id = bs.seat.id AND bs.screening.id = :screeningId
+ WHERE s.auditorium.id = :auditoriumId
+ ORDER BY s.rowNo ASC, s.columnNo ASC
+ """)
+ List findSeatStatusByAuditoriumAndScreening(
+ @Param("auditoriumId") Long auditoriumId,
+ @Param("screeningId") Long screeningId
+ );
}
diff --git a/src/main/java/com/ceos22/cgv_clone/domain/theater/service/SeatQueryService.java b/src/main/java/com/ceos22/cgv_clone/domain/theater/service/SeatQueryService.java
index 243f4cf1..81a87cb3 100644
--- a/src/main/java/com/ceos22/cgv_clone/domain/theater/service/SeatQueryService.java
+++ b/src/main/java/com/ceos22/cgv_clone/domain/theater/service/SeatQueryService.java
@@ -29,18 +29,21 @@ public List getSeatMap(Long screeningId) {
.orElseThrow(() -> new CustomException(ErrorCode.SCREENING_NOT_FOUND));
Long auditoriumId = screening.getAuditorium().getId();
- List seats = seatRepository.findAllByAuditoriumIdSorted(auditoriumId);
+// List seats = seatRepository.findAllByAuditoriumIdSorted(auditoriumId);
+//
+// Set reservedSeatIds = new HashSet<>(
+// bookingSeatRepository.findByScreeningId(screeningId)
+// .stream().map(bs -> bs.getSeat().getId()).toList()
+// );
+//
+// return seats.stream()
+// .map(s -> new SeatStatusDto(
+// s.getId(), s.getRowNo(), s.getColumnNo(),
+// reservedSeatIds.contains(s.getId())
+// ))
+// .toList();
- Set reservedSeatIds = new HashSet<>(
- bookingSeatRepository.findByScreeningId(screeningId)
- .stream().map(bs -> bs.getSeat().getId()).toList()
- );
-
- return seats.stream()
- .map(s -> new SeatStatusDto(
- s.getId(), s.getRowNo(), s.getColumnNo(),
- reservedSeatIds.contains(s.getId())
- ))
- .toList();
+ // 단일 쿼리 호출로 변경
+ return seatRepository.findSeatStatusByAuditoriumAndScreening(auditoriumId, screeningId);
}
}