From 659f6f7d43aa63152de8539f0816d1778fa2a35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EA=B0=80=EC=98=81?= Date: Sat, 15 Nov 2025 22:22:02 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Docs:=20=ED=8A=B8=EB=9E=9C=EC=9E=AD?= =?UTF-8?q?=EC=85=98=20=EC=A0=84=ED=8C=8C=20=EC=86=8D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/README.md b/README.md index 8e0bb15..70237d2 100644 --- a/README.md +++ b/README.md @@ -306,3 +306,73 @@ Feign Client란 Netflix에서 개발한 Http Client다. (HttpClient는 Http 요 - 모니터링 대시보드 스크린샷 2025-11-08 오후 10 33 53 +
+ +# 트랜잭션 +- 트랜잭션은 시작 지점 & 종료 지점이 존재 +- 시작 방법은 1가지지만, 끝나는 방법은 commit과 rollback 2가지 + +# 트랜잭션 전파 속성 (Transaction Propagation) +- **전파 속성** : 트랜잭션이 이미 진행 중일 떄 추가 트랜잭션 진행을 어떻게 할지 결정하는 것 + +## 물리 트랜잭션 / 논리 트랜잭션 +- 트랜잭션은 커넥션 객체를 통해 처리하기 때문에, 1개의 트랜잭션을 사용하는 것은 하나의 커넥션 객체를 사용한다는 것 +- 실제 데이터베이스의 트랜잭션을 사용한다는 점에서 -> 물리 트랜잭션이라고도 함 + - 물리 트랜잭션 : 실제 커넥션에 롤백/커밋을 호출하는 것. 즉 해당 트랜잭션이 끝나는 것 + +- 스프링의 입장에서는 트랜잭션 매니저를 통해 트랜잭션을 처리하는 곳이 2군데이기 때문에, 실제 데이터베이스 트랜잭션과 스프링이 처리하는 트랜잭션 영역을 구분하기 위해 스프링은 논리 트랜잭션이라는 개념을 추가함 +- 예) 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션(=커넥션)을 사용하는 경우 + - 스크린샷 2025-11-15 오후 10 05 20 + - 2개의 트랜잭션 범위 존재 -> 따라서 개별 논리 트랜잭션이 존재. 하지만 실제로는 1개의 물리 트랜잭션 사용 + +따라서 개념을 정리하면 +- 물리 트랜잭션: 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위 +- 논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위 + +- 기존의 트랜잭션이 진행중일 때 또 다른 트랜잭션이 사용되면 복잡해짐 => 스프링: 논리 트랜잭션이라는 개념을 도입 + - 원칙 1. 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됨 + - 원칙 2. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됨 + +
+ +## 스프링의 트랜잭션 전파 속성 + +### 1. REQUIRED +스크린샷 2025-11-15 오후 10 16 12 +- 스프링이 제공하는 DEFAULT 전파 속성 +- 기본적으로 2개의 논리 트랜잭션을 묶어 하나의 물리 트랜잭션을 사용하는 것 (이전 예시 사진) +- 내부 트랜잭션은 기존에 존재하는 외부 트랜잭션에 참여(이어 나감)하게 되며, 외부 트랜잭션의 범위가 내부 트랜잭션까지 확장됨. 따라서 내부 트랜잭션은 새로운 물리 트랜잭션을 사용하지 않음 +- 커밋은 내부 1회, 외부 1회 총 2회 실행되고 물리 트랜잭션을 관리하는 외부 트랜잭션이 최종적으로 커밋될 때 실제로 커밋됨 +- 롤백또한 내부 트랜잭션에서 롤백해도 즉시 롤백 X, 물리 트랜잭션이 롤백될 때 실제 롤백처리 됨. + - 외부 트랜잭션의 롤백이 필요한 경우 : 내부 트랜잭션의 커밋/롤백 여부와 무관하게 물리 트랜잭션에서 롤백 시킴 + - 내부 트랜잭션에서 롤백이 필요한 경우(외부 t는 커밋 O) : 내부 트랜잭션에서 UnexpectedRollbackException 예외를 통해 롤백 필요 알림 -> 외부 트랜잭션에서 이를 바탕으로 롤백 + +### 2. REQUIRES_NEW +스크린샷 2025-11-15 오후 10 16 33 +- 외부/내부 트랜잭션을 완전히 분리하는 전파 속성 +- 2개의 물리 트랜잭션 사용. 각각 트랜잭션 별로 커밋과 롤백이 수행 +- 따라서 내부 트랜잭션 롤백이 외부 트랜잭션 롤백에 영향을 주지 않음 +- 서로 다른 물리 트랜잭션 == 각각의 디비 커넥션 사용 (== 1개의 HTTP 요청에 대해 2개의 커넥션 사용) + - 따라서 내부 트랜잭션 처리 중이 꺼내진 외부 트랜잭션은 대기 -> 커넥션 고갈 시킬 수 있기 때문에 주의 + +### 3. SUPPORTS +- 이미 시작된 트랜잭션이 있으면 참여 / 없으면 트랜잭션 없이 진행 +- 트랜잭션이 없지만 해당 경계 내에서 커넥션이나 Hibernate Session 등을 공유 가능 + +### 4. MANDATORY +- REQUIRED와 유사. 이미 시작된 트랜잭션이 있으면 참여 +- 하지만 트랜잭션이 없다면 생성하는 것이 아니라 예외를 발생 + - 따라서 혼자서 독립적으로 트랜잭션을 실행하면 안되는 경우에 사용 + +### 5. NOT_SUPPORTED +- 트랜잭션 사용 X +- 이미 진행 중인 트랜잭션이 있으면 보류시킴 + +### 6. NEVER +- 트랜잭션을 사용하지 않도록 강제 +- 이미 진행 중인 트랜잭션도 존재하면 안됨. 있다면 예외 발생 + +### 7. NESTED +- 이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션 시작 + - 독립적인 트랜잭션을 만드는 REQUIRES_NEW와 다름 +- 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋/롤백에는 영향을 받지만, 자신의 커밋/롤백은 부모에 영향 X From 309f1d42045e4784b41108bc913ae9cb98ea9900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=84=9C=EA=B0=80=EC=98=81?= Date: Sat, 15 Nov 2025 22:46:35 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Docs:=20=EC=9D=B8=EB=8D=B1=EC=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/README.md b/README.md index 70237d2..4d285a6 100644 --- a/README.md +++ b/README.md @@ -376,3 +376,76 @@ Feign Client란 Netflix에서 개발한 Http Client다. (HttpClient는 Http 요 - 이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션 시작 - 독립적인 트랜잭션을 만드는 REQUIRES_NEW와 다름 - 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋/롤백에는 영향을 받지만, 자신의 커밋/롤백은 부모에 영향 X + +
+ +
+ +# 데이터베이스 인덱스 +## 1. Clustered index +- 데이터베이스 테이블의 물리적인 순서를 인덱스의 키 값 순서대로 정렬하는 것 +- 테이블 당 하나만 존재 + - 테이블의 데이터가 하나의 순서로만 정렬될 수 있기 때문 +- 데이터의 물리적 순서와 인덱스 순서가 같음 -> 빠른 검색 가능 +- 실제 데이터와 함께 저장되기 때문에 불필요한 디스크 공간을 추가적으로 유발하지 않음 +- 기본적으로 MySQL 에서 InnoDB 스토리지 엔진을 사용하는 경우 테이블은 기본 키를 클러스터드 인덱스로 자동으로 사용 + +## 2. Non-Clustered Index +image +- 데이터가 저장된 테이블과 별도의 공간에 위치. 데이터의 위치를 가리키는 포인터를 사용해 데이터를 찾음 + - 별도의 인덱스 테이블에 추가된 인덱스 값과 해당 데이터의 포인터가 저장됨 +- 테이블의 데이터와 다른 순서로 정렬될 수 있음 +- 인덱스가 테이블의 데이터와는 다른 별도의 저장 공간에 저장되며, 인덱스가 테이블 데이터와 다른 구조를 가짐 +- 하나의 테이블에 대해 여러 개의 Non-Clustered Index 생성 가능 + - 따라서 다양한 검색 요구 사항에 맞춰 여러 인덱스 사용 가능 +- Non-Clustered Index는 특정 컬럼에 대한 검색 최적화 가능 +- PK 외에 다른 유니크 인덱스가 설정되는 경우, Non-Clustered Index로 unique index가 배치됨 + +## 3. B-Tree Index +image + +- 모든 리프 노드가 같은 레벨이기 때문에 데이터베이스에서 검색/삽입/삭제 등의 동작이 일정 시간 안에 이뤄짐 +- 키 값들이 정렬된 상태로 유지되어 빠르고 효율적인 검색이 가능 +- 데이터 삽입/삭제 시 트리의 균형을 자동으로 맞추기 때문에 동적인 데이터 조정이 이뤄짐 + +## 4. Hash Index +image +- 해시 테이블 기반 +- 빠른 데이터 검색을 위해 사용되는 인덱스 유형 중 하나 +- 특정 키 값을 해시 함수를 통해 해시 코드로 변환 +- 하지만 DB 인덱스에서 해시 테이블이 사용되는 경우는 제한적 + - 해시가 등호(=) 연산에만 특화되었기 때문이며 + - 해시 함수는 값이 1이라도 달라지면 완전히 다른 해시 값을 생성하는데, 이러한 특성에 의해 부등호 연산(>, <)이 자주 사용되는 데이터베이스 검색을 위해서는 해시 테이블이 적합하지 않음 + +## 5. Bitmap Index +- 각각의 값에 대해 비트 배열 생성 (해당 값에 행 존재 시 : 1, 없으면 : 0) +- 예) 각 성별에 대해 비트 배열을 생성할 때, 성별 속성에 대한 남자 비트맵은 1 0 0 1 0 + - 따라서 해당 테이블 내에서 남자를 탐색하는 경우 비트맵 인덱스만 보고도 1, 4번째가 남자임을 알 수 있음 + +## 6. Full-Text Index +- 데이터베이스 내의 텍스트 데이터에서 키워드 검색을 가능하게 하는 인덱스 유형 +- 대량의 텍스트 데이터에서 특정 단어나 문구를 빠르게 찾아내는 기능 +- 주로 뉴스 기사, 책, 블로그 글 등의 텍스트 데이터를 검색할 때 사용 +- 텍스트 데이터를 분석해 중요 데이터(키워드)를 추출하고, 이 단어들의 데이터베이스 내 위치를 기록하여 컴색 쿼리가 수행되는 경우 해당 인덱스를 사용해 빠르게 데이터를 검색함 + +### 장점 +- 게시물 내용을 효율적으로 검색 가능 +- 복잡한 쿼리 지원 + - 문구 검색, 불용어(별로 중요하지 않은 단어, 조사 등..을 후순위로 처리), 동의어 처리 등 복잡한 텍스트이 검색 요구사항 지원 +### 단점 +- 저장공간을 많이 사용함 +- 텍스트 데이터가 자주 변경되면 업데이트 비용이 큼 +- 대안이 많다! + +## 7. Composite Index +- 두 개 이상의 컬럼을 결합해 생성한 인덱스 +- 특정 쿼리 연산에서 여러 컬럼을 동시에 사용할 때 검색 성능을 향상시키기 위해 사용 +- 지정된 컬럼들의 조합으로 구성되며, 인덱스 내에서는 해당 컬럼들이 정의된 순서대로 데이터를 정렬함 + +### 장점 +- 여러 컬럼을 조건으로 하는 쿼리에서 검색 성능이 크게 향상 +- 정렬 그룹화 최적화 가능 +- 인덱스 커버링 + - 쿼리가 인덱스에 포함된 컬럼만을 사용하는 경우, 실제 데이터에 접근하지 않고 인덱스에서 결과를 바로 가져올 수 있음 +### 단점 +- 별도 공간, 유지관리 비용 From 1da73450895198a44d6b363aa6c75d608b4265f2 Mon Sep 17 00:00:00 2001 From: caminobelllo Date: Sat, 15 Nov 2025 23:26:05 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Refactor:=20Cinema=20domain=20N+1=20-=20@En?= =?UTF-8?q?tityGraph=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/theater/repository/CinemaRepository.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) 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 074250f..fa2f223 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); } From bf164b805490c8ea24ddff51ff0ecf3fd069ad6a Mon Sep 17 00:00:00 2001 From: caminobelllo Date: Sat, 15 Nov 2025 23:28:59 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Refactor:=20Product=20N+1=20-=20JOIN=20FETC?= =?UTF-8?q?H=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../store/repository/ProductOrderItemRepository.java | 10 ++++++++++ .../domain/store/service/ProductOrderQueryService.java | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) 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 68e80f3..1e28e34 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 ab54dd7..cc38874 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(), From 2e67819352ee4299b79bf26da44df82ee802629a Mon Sep 17 00:00:00 2001 From: caminobelllo Date: Sat, 15 Nov 2025 23:33:01 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Refactor:=20Seat=20-=20=EC=97=B0=EC=82=B0?= =?UTF-8?q?=20=EC=A3=BC=EC=B2=B4=EA=B0=80=20Java->=20DB=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../theater/repository/SeatRepository.java | 19 +++++++++++++ .../theater/service/SeatQueryService.java | 27 ++++++++++--------- 2 files changed, 34 insertions(+), 12 deletions(-) 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 a09f600..0546bc5 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 243f4cf..81a87cb 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); } }