Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
143 changes: 143 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,3 +306,146 @@ Feign Client란 Netflix에서 개발한 Http Client다. (HttpClient는 Http 요
- 모니터링 대시보드
<img width="1436" height="966" alt="스크린샷 2025-11-08 오후 10 33 53" src="https://github.com/user-attachments/assets/32cfcb4e-4a36-4eb8-97b7-e033abe092d1" />

<hr />

# 트랜잭션
- 트랜잭션은 시작 지점 & 종료 지점이 존재
- 시작 방법은 1가지지만, 끝나는 방법은 commit과 rollback 2가지

# 트랜잭션 전파 속성 (Transaction Propagation)
- **전파 속성** : 트랜잭션이 이미 진행 중일 떄 추가 트랜잭션 진행을 어떻게 할지 결정하는 것

## 물리 트랜잭션 / 논리 트랜잭션
- 트랜잭션은 커넥션 객체를 통해 처리하기 때문에, 1개의 트랜잭션을 사용하는 것은 하나의 커넥션 객체를 사용한다는 것
- 실제 데이터베이스의 트랜잭션을 사용한다는 점에서 -> 물리 트랜잭션이라고도 함
- 물리 트랜잭션 : 실제 커넥션에 롤백/커밋을 호출하는 것. 즉 해당 트랜잭션이 끝나는 것

- 스프링의 입장에서는 트랜잭션 매니저를 통해 트랜잭션을 처리하는 곳이 2군데이기 때문에, 실제 데이터베이스 트랜잭션과 스프링이 처리하는 트랜잭션 영역을 구분하기 위해 스프링은 논리 트랜잭션이라는 개념을 추가함
- 예) 외부 트랜잭션과 내부 트랜잭션이 하나의 물리 트랜잭션(=커넥션)을 사용하는 경우
- <img width="646" height="235" alt="스크린샷 2025-11-15 오후 10 05 20" src="https://github.com/user-attachments/assets/88706482-39e9-4942-93db-5a7bc5e2211f" />
- 2개의 트랜잭션 범위 존재 -> 따라서 개별 논리 트랜잭션이 존재. 하지만 실제로는 1개의 물리 트랜잭션 사용

따라서 개념을 정리하면
- 물리 트랜잭션: 실제 데이터베이스에 적용되는 트랜잭션으로, 커넥션을 통해 커밋/롤백하는 단위
- 논리 트랜잭션: 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위

- 기존의 트랜잭션이 진행중일 때 또 다른 트랜잭션이 사용되면 복잡해짐 => 스프링: 논리 트랜잭션이라는 개념을 도입
- 원칙 1. 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋됨
- 원칙 2. 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백됨

<br />

## 스프링의 트랜잭션 전파 속성

### 1. REQUIRED
<img width="631" height="230" alt="스크린샷 2025-11-15 오후 10 16 12" src="https://github.com/user-attachments/assets/cf184616-856d-432d-958a-798c3c8ac5ef" />
- 스프링이 제공하는 DEFAULT 전파 속성
- 기본적으로 2개의 논리 트랜잭션을 묶어 하나의 물리 트랜잭션을 사용하는 것 (이전 예시 사진)
- 내부 트랜잭션은 기존에 존재하는 외부 트랜잭션에 참여(이어 나감)하게 되며, 외부 트랜잭션의 범위가 내부 트랜잭션까지 확장됨. 따라서 내부 트랜잭션은 새로운 물리 트랜잭션을 사용하지 않음
- 커밋은 내부 1회, 외부 1회 총 2회 실행되고 물리 트랜잭션을 관리하는 외부 트랜잭션이 최종적으로 커밋될 때 실제로 커밋됨
- 롤백또한 내부 트랜잭션에서 롤백해도 즉시 롤백 X, 물리 트랜잭션이 롤백될 때 실제 롤백처리 됨.
- 외부 트랜잭션의 롤백이 필요한 경우 : 내부 트랜잭션의 커밋/롤백 여부와 무관하게 물리 트랜잭션에서 롤백 시킴
- 내부 트랜잭션에서 롤백이 필요한 경우(외부 t는 커밋 O) : 내부 트랜잭션에서 UnexpectedRollbackException 예외를 통해 롤백 필요 알림 -> 외부 트랜잭션에서 이를 바탕으로 롤백

### 2. REQUIRES_NEW
<img width="635" height="199" alt="스크린샷 2025-11-15 오후 10 16 33" src="https://github.com/user-attachments/assets/fecc864b-517a-4432-9cbb-2bea4feab819" />
- 외부/내부 트랜잭션을 완전히 분리하는 전파 속성
- 2개의 물리 트랜잭션 사용. 각각 트랜잭션 별로 커밋과 롤백이 수행
- 따라서 내부 트랜잭션 롤백이 외부 트랜잭션 롤백에 영향을 주지 않음
- 서로 다른 물리 트랜잭션 == 각각의 디비 커넥션 사용 (== 1개의 HTTP 요청에 대해 2개의 커넥션 사용)
- 따라서 내부 트랜잭션 처리 중이 꺼내진 외부 트랜잭션은 대기 -> 커넥션 고갈 시킬 수 있기 때문에 주의

### 3. SUPPORTS
- 이미 시작된 트랜잭션이 있으면 참여 / 없으면 트랜잭션 없이 진행
- 트랜잭션이 없지만 해당 경계 내에서 커넥션이나 Hibernate Session 등을 공유 가능

### 4. MANDATORY
- REQUIRED와 유사. 이미 시작된 트랜잭션이 있으면 참여
- 하지만 트랜잭션이 없다면 생성하는 것이 아니라 예외를 발생
- 따라서 혼자서 독립적으로 트랜잭션을 실행하면 안되는 경우에 사용

### 5. NOT_SUPPORTED
- 트랜잭션 사용 X
- 이미 진행 중인 트랜잭션이 있으면 보류시킴

### 6. NEVER
- 트랜잭션을 사용하지 않도록 강제
- 이미 진행 중인 트랜잭션도 존재하면 안됨. 있다면 예외 발생

### 7. NESTED
- 이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션 시작
- 독립적인 트랜잭션을 만드는 REQUIRES_NEW와 다름
- 중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋/롤백에는 영향을 받지만, 자신의 커밋/롤백은 부모에 영향 X

<br />

<hr />

# 데이터베이스 인덱스
## 1. Clustered index
- 데이터베이스 테이블의 물리적인 순서를 인덱스의 키 값 순서대로 정렬하는 것
- 테이블 당 하나만 존재
- 테이블의 데이터가 하나의 순서로만 정렬될 수 있기 때문
- 데이터의 물리적 순서와 인덱스 순서가 같음 -> 빠른 검색 가능
- 실제 데이터와 함께 저장되기 때문에 불필요한 디스크 공간을 추가적으로 유발하지 않음
- 기본적으로 MySQL 에서 InnoDB 스토리지 엔진을 사용하는 경우 테이블은 기본 키를 클러스터드 인덱스로 자동으로 사용

## 2. Non-Clustered Index
<img width="1710" height="638" alt="image" src="https://github.com/user-attachments/assets/e1bf2415-09d1-4f2d-a64d-fcb4ce2af2b2" />
- 데이터가 저장된 테이블과 별도의 공간에 위치. 데이터의 위치를 가리키는 포인터를 사용해 데이터를 찾음
- 별도의 인덱스 테이블에 추가된 인덱스 값과 해당 데이터의 포인터가 저장됨
- 테이블의 데이터와 다른 순서로 정렬될 수 있음
- 인덱스가 테이블의 데이터와는 다른 별도의 저장 공간에 저장되며, 인덱스가 테이블 데이터와 다른 구조를 가짐
- 하나의 테이블에 대해 여러 개의 Non-Clustered Index 생성 가능
- 따라서 다양한 검색 요구 사항에 맞춰 여러 인덱스 사용 가능
- Non-Clustered Index는 특정 컬럼에 대한 검색 최적화 가능
- PK 외에 다른 유니크 인덱스가 설정되는 경우, Non-Clustered Index로 unique index가 배치됨

## 3. B-Tree Index
<img width="963" height="253" alt="image" src="https://github.com/user-attachments/assets/e220e999-53ab-4bde-a88d-e8abe921edfb" />

- 모든 리프 노드가 같은 레벨이기 때문에 데이터베이스에서 검색/삽입/삭제 등의 동작이 일정 시간 안에 이뤄짐
- 키 값들이 정렬된 상태로 유지되어 빠르고 효율적인 검색이 가능
- 데이터 삽입/삭제 시 트리의 균형을 자동으로 맞추기 때문에 동적인 데이터 조정이 이뤄짐

## 4. Hash Index
<img width="315" height="230" alt="image" src="https://github.com/user-attachments/assets/b48cfd53-3322-4064-8a02-250ab71e4cfd" />
- 해시 테이블 기반
- 빠른 데이터 검색을 위해 사용되는 인덱스 유형 중 하나
- 특정 키 값을 해시 함수를 통해 해시 코드로 변환
- 하지만 DB 인덱스에서 해시 테이블이 사용되는 경우는 제한적
- 해시가 등호(=) 연산에만 특화되었기 때문이며
- 해시 함수는 값이 1이라도 달라지면 완전히 다른 해시 값을 생성하는데, 이러한 특성에 의해 부등호 연산(>, <)이 자주 사용되는 데이터베이스 검색을 위해서는 해시 테이블이 적합하지 않음

## 5. Bitmap Index
- 각각의 값에 대해 비트 배열 생성 (해당 값에 행 존재 시 : 1, 없으면 : 0)
- 예) 각 성별에 대해 비트 배열을 생성할 때, 성별 속성에 대한 남자 비트맵은 1 0 0 1 0
- 따라서 해당 테이블 내에서 남자를 탐색하는 경우 비트맵 인덱스만 보고도 1, 4번째가 남자임을 알 수 있음

## 6. Full-Text Index
- 데이터베이스 내의 텍스트 데이터에서 키워드 검색을 가능하게 하는 인덱스 유형
- 대량의 텍스트 데이터에서 특정 단어나 문구를 빠르게 찾아내는 기능
- 주로 뉴스 기사, 책, 블로그 글 등의 텍스트 데이터를 검색할 때 사용
- 텍스트 데이터를 분석해 중요 데이터(키워드)를 추출하고, 이 단어들의 데이터베이스 내 위치를 기록하여 컴색 쿼리가 수행되는 경우 해당 인덱스를 사용해 빠르게 데이터를 검색함

### 장점
- 게시물 내용을 효율적으로 검색 가능
- 복잡한 쿼리 지원
- 문구 검색, 불용어(별로 중요하지 않은 단어, 조사 등..을 후순위로 처리), 동의어 처리 등 복잡한 텍스트이 검색 요구사항 지원
### 단점
- 저장공간을 많이 사용함
- 텍스트 데이터가 자주 변경되면 업데이트 비용이 큼
- 대안이 많다!

## 7. Composite Index
- 두 개 이상의 컬럼을 결합해 생성한 인덱스
- 특정 쿼리 연산에서 여러 컬럼을 동시에 사용할 때 검색 성능을 향상시키기 위해 사용
- 지정된 컬럼들의 조합으로 구성되며, 인덱스 내에서는 해당 컬럼들이 정의된 순서대로 데이터를 정렬함

### 장점
- 여러 컬럼을 조건으로 하는 쿼리에서 검색 성능이 크게 향상
- 정렬 그룹화 최적화 가능
- 인덱스 커버링
- 쿼리가 인덱스에 포함된 컬럼만을 사용하는 경우, 실제 데이터에 접근하지 않고 인덱스에서 결과를 바로 가져올 수 있음
### 단점
- 별도 공간, 유지관리 비용

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정리 너무 잘해주셨네요!

Original file line number Diff line number Diff line change
Expand Up @@ -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<ProductOrderItem, Long> {

// 주문 아이템을 찾을 때 Product 정보도 JOIN FETCH 같이
@Query("""
SELECT poi FROM ProductOrderItem poi
JOIN FETCH poi.product
WHERE poi.productOrder.id = :orderId
""")
List<ProductOrderItem> findByProductOrderIdWithProduct(@Param("orderId") Long orderId);

List<ProductOrderItem> findByProductOrderId(Long orderId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ public ProductOrderDetailDto getOrder(Long orderId) {
ProductOrder order = productOrderRepository.findDetailsById(orderId)
.orElseThrow(() -> new CustomException(ErrorCode.ORDER_NOT_FOUND));

List<ProductOrderItem> items = productOrderItemRepository.findByProductOrderId(orderId);
List<ProductOrderItem> items = productOrderItemRepository.findByProductOrderIdWithProduct(orderId);

return new ProductOrderDetailDto(
order.getId(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,11 +13,7 @@ public interface CinemaRepository extends JpaRepository<Cinema, Long> {

List<Cinema> findByRegion(Region region);

@Query("""
SELECT c FROM Cinema c
JOIN FETCH c.auditoriums
WHERE c.id = :id
""")
@EntityGraph(attributePaths = {"auditoriums", "auditoriums.auditoriumType"})
Optional<Cinema> findByIdWithAuditoriums(@Param("id") Long cinemaId);

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -22,4 +23,22 @@ public interface SeatRepository extends JpaRepository<Seat, Long> {

@Query("SELECT s FROM Seat s WHERE s.id IN :ids")
List<Seat> findAllByIdWithLock(@Param("ids") List<Long> 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<SeatStatusDto> findSeatStatusByAuditoriumAndScreening(
@Param("auditoriumId") Long auditoriumId,
@Param("screeningId") Long screeningId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,21 @@ public List<SeatStatusDto> getSeatMap(Long screeningId) {
.orElseThrow(() -> new CustomException(ErrorCode.SCREENING_NOT_FOUND));

Long auditoriumId = screening.getAuditorium().getId();
List<Seat> seats = seatRepository.findAllByAuditoriumIdSorted(auditoriumId);
// List<Seat> seats = seatRepository.findAllByAuditoriumIdSorted(auditoriumId);
//
// Set<Long> 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<Long> 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);
}
}