Compare commits

...

4 Commits

Author SHA1 Message Date
5e572c842c Merge pull request '[#72] 로그 레벨 재조정' (#73) from refactor/#72 into main
Reviewed-on: #73
2025-11-08 06:02:09 +00:00
7a236a8196 chore: 미사용 마크다운 제거 2025-10-21 09:44:34 +09:00
0756e21b63 refactor: 로그 레벨 재조정 2025-10-21 08:05:20 +09:00
162e5bbc79 [#70] 중복 조회 로직에 로컬 캐시 도입 (#71)
<!-- 제목 양식 -->
<!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) -->

## 📝 관련 이슈 및 PR

**PR과 관련된 이슈 번호**
- #70

##  작업 내용
<!-- 어떤 작업을 했는지 알려주세요! -->
- Spring Cache + Caffeine 도입
- 테마 조회 및 수정 / 삭제 로직에 캐시 적용

## 🧪 테스트
<!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! -->
- P95 응답 시간 약 14% 개선
- Hikari Pool Connection & Tomcat Threads에서의 개선

## 📚 참고 자료 및 기타
<!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! -->

Reviewed-on: #71
Co-authored-by: pricelees <priceelees@gmail.com>
Co-committed-by: pricelees <priceelees@gmail.com>
2025-10-17 10:47:15 +00:00
34 changed files with 246 additions and 983 deletions

View File

@ -1,3 +0,0 @@
# issue-pr-template
공통으로 사용하게 될 이슈, PR 템플릿 저장소

845
query.md
View File

@ -1,845 +0,0 @@
## Auth
**로그인**
```sql
-- 회원
-- 이메일로 회원 조회
SELECT
u.id
FROM
users u
WHERE
u.email = ?
LIMIT 1;
-- 연락처로 회원 조회
SELECT
u.id
FROM
users u
WHERE
u.phone = ?
LIMIT 1;
-- 회원 추가
INSERT INTO users (
created_at, created_by, email, name, password, phone, region_code,
status, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
-- 회원 상태 이력 추가
INSERT INTO user_status_history (
created_at, created_by, reason, status, updated_at, updated_by,
user_id, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?
);
```
### Payment
**결제 승인 & 저장**
```sql
-- 결제 정보 추가
INSERT INTO payment ( approved_at, method, order_id, payment_key, requested_at, reservation_id, status, total_amount, type, id
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
-- 결제 상세 정보 추가
INSERT INTO payment_detail ( payment_id, supplied_amount, vat, id
) VALUES ( ?, ?, ?, ?
);
-- 카드 결제 상세 정보 추가
INSERT INTO payment_card_detail ( amount, approval_number, card_number, card_type, easypay_discount_amount, easypay_provider_code, installment_plan_months, is_interest_free, issuer_code, owner_type, id
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**결제 취소**
SQL
```sql
-- 예약 ID로 결제 정보 조회
SELECT
p.id,
p.approved_at,
p.method,
p.order_id,
p.payment_key,
p.requested_at,
p.reservation_id,
p.status,
p.total_amount,
p.type
FROM
payment p
WHERE
p.reservation_id = ?;
-- 추가
-- 취소된 결제 정보 추가
INSERT INTO canceled_payment (
cancel_amount, cancel_reason, canceled_at, canceled_by,
card_discount_amount, easypay_discount_amount, payment_id,
requested_at, transfer_discount_amount, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
### Region
**모든 시/도 조회**
```sql
SELECT DISTINCT
r.sido_code,
r.sido_name
FROM
region r
ORDER BY
r.sido_name;
```
**시/군/구 조회**
```sql
SELECT
r.sigungu_code,
r.sigungu_name
FROM
region r
WHERE
r.sido_code = ?
GROUP BY
r.sigungu_code, r.sigungu_name
ORDER BY
r.sigungu_name;
```
**지역 코드 조회**
```sql
SELECT
r.code
FROM
region r
WHERE
r.sido_code = ? AND r.sigungu_code = ?;
```
### Reservation
**Pending 예약 생성**
```sql
-- schedule 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- theme 조회
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
-- 예약 추가
INSERT INTO reservation (
created_at, created_by, participant_count, requirement,
reserver_contact, reserver_name, schedule_id, status,
updated_at, updated_by, user_id, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**확정**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.id = ?;
-- 일정 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 예약 확정
UPDATE
reservation
SET
participant_count = ?, requirement = ?, reserver_contact = ?,
reserver_name = ?, schedule_id = ?, status = ?,
updated_at = ?, updated_by = ?, user_id = ?
WHERE
id = ?;
-- Schedule 확정
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**취소**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.id = ?;
-- 일정 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 취소 예약 추가
INSERT INTO canceled_reservation (
cancel_reason, canceled_at, canceled_by,
reservation_id, status, id
) VALUES (
?, ?, ?, ?, ?, ?
);
-- 예약 취소
UPDATE
reservation
SET
participant_count = ?, requirement = ?, reserver_contact = ?,
reserver_name = ?, schedule_id = ?, status = ?,
updated_at = ?, updated_by = ?, user_id = ?
WHERE
id = ?;
-- 일정 활성화
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**회원 예약 조회**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.user_id = ? AND r.status IN (?, ?);
-- 일정 조회 -> 각 예약별 1개씩(N개)
SELECT
s.id,
st.id AS store_id,
st.name AS store_name,
s.date,
s.time,
t.id AS theme_id,
t.name AS theme_name,
t.difficulty,
t.available_minutes,
s.status
FROM
schedule s
JOIN theme t ON t.id = s.theme_id
JOIN store st ON st.id = s.store_id
WHERE
s.id = ?;
```
**예약 상세 조회**
```sql
-- 예약 조회
SELECT
r.id, r.created_at, r.created_by, r.participant_count, r.requirement,
r.reserver_contact, r.reserver_name, r.schedule_id, r.status,
r.updated_at, r.updated_by, r.user_id
FROM
reservation r
WHERE
r.id = ?;
-- 회원 연락처 정보 조회
SELECT
u.id, u.created_at, u.created_by, u.email, u.name, u.password,
u.phone, u.region_code, u.status, u.updated_at, u.updated_by
FROM
users u
WHERE
u.id = ?;
-- 결제 정보 조회
SELECT
p.id, p.approved_at, p.method, p.order_id, p.payment_key,
p.requested_at, p.reservation_id, p.status, p.total_amount, p.type
FROM
payment p
WHERE
p.reservation_id = ?;
-- 결제 상세 정보 조회
SELECT
pd.id,
CASE
WHEN pbt.id IS NOT NULL THEN 1 -- bank_transfer
WHEN pcd.id IS NOT NULL THEN 2 -- card
WHEN pep.id IS NOT NULL THEN 3 -- easypay
WHEN pd.id IS NOT NULL THEN 0 -- etc
END AS payment_type,
pd.payment_id, pd.supplied_amount, pd.vat,
pbt.bank_code, pbt.settlement_status,
pcd.amount, pcd.approval_number, pcd.card_number, pcd.card_type,
pcd.easypay_discount_amount, pcd.easypay_provider_code,
pcd.installment_plan_months, pcd.is_interest_free, pcd.issuer_code,
pcd.owner_type,
pep.amount AS easypay_amount,
pep.discount_amount AS easypay_discount_amount,
pep.easypay_provider_code AS easypay_provider
FROM
payment_detail pd
LEFT JOIN payment_bank_transfer_detail pbt ON pd.id = pbt.id
LEFT JOIN payment_card_detail pcd ON pd.id = pcd.id
LEFT JOIN payment_easypay_prepaid_detail pep ON pd.id = pep.id
WHERE
pd.payment_id = ?;
-- 취소 결제 정보 조회
SELECT
cp.id, cp.cancel_amount, cp.cancel_reason, cp.canceled_at,
cp.canceled_by, cp.card_discount_amount, cp.easypay_discount_amount,
cp.payment_id, cp.requested_at, cp.transfer_discount_amount
FROM
canceled_payment cp
WHERE
cp.payment_id = ?;
```
### Schedule
**날짜, 시간, 테마로 조회**
```sql
SELECT
s.id,
st.id AS store_id,
st.name AS store_name,
s.date,
s.time,
t.id AS theme_id,
t.name AS theme_name,
t.difficulty,
t.available_minutes,
s.status
FROM
schedule s
JOIN theme t ON t.id = s.theme_id AND (? IS NULL OR t.id = ?)
JOIN store st ON st.id = s.store_id AND st.id = ?
WHERE
s.date = ?
```
**감사 정보 조회**
```sql
-- 일정 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 작업자 조회(createdBy, updatedBy)
SELECT
a.id, a.account, a.created_at, a.created_by, a.name, a.password,
a.permission_level, a.phone, a.store_id, a.type, a.updated_at,
a.updated_by
FROM
admin a
WHERE
a.id = ?;
```
**일정 생성**
```sql
-- 날짜, 시간, 테마가 같은 일정 존재 여부 확인
SELECT EXISTS (
SELECT 1
FROM schedule s
WHERE
s.store_id = ?
AND s.date = ?
AND s.theme_id = ?
AND s.time = ?
);
-- 시간이 겹치는 같은 날의 일정이 있는지 확인
SELECT
s.id,
st.id AS store_id,
st.name AS store_name,
s.date,
s.time,
t.id AS theme_id,
t.name AS theme_name,
t.difficulty,
t.available_minutes,
s.status
FROM
schedule s
JOIN theme t ON t.id = s.theme_id AND (? IS NULL OR s.theme_id = ?)
JOIN store st ON st.id = s.store_id AND st.id = ?
WHERE
s.date = ?
-- 일정 추가
INSERT INTO schedule (
created_at, created_by, date, status, store_id,
theme_id, time, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**일정 수정**
```sql
-- 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 수정
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**일정 삭제**
```sql
-- 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 삭제
DELETE FROM schedule
WHERE id = ?;
```
**상태 → HOLD 변경**
```sql
-- 조회
SELECT
s.id, s.created_at, s.created_by, s.date, s.status, s.store_id,
s.theme_id, s.time, s.updated_at, s.updated_by
FROM
schedule s
WHERE
s.id = ?;
-- 수정
UPDATE
schedule
SET
date = ?, status = ?, store_id = ?, theme_id = ?, time = ?,
updated_at = ?, updated_by = ?
WHERE
id = ?;
```
### Store
**매장 상세 조회**
```sql
-- 조회
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
-- 지역 정보 조회
SELECT
r.code, r.sido_code, r.sido_name, r.sigungu_code, r.sigungu_name
FROM
region r
WHERE
r.code = ?;
-- 감사 정보 조회(createdBy, updatedBy)
SELECT
a.id, a.account, a.created_at, a.created_by, a.name, a.password,
a.permission_level, a.phone, a.store_id, a.type, a.updated_at,
a.updated_by
FROM
admin a
WHERE
a.id = ?;
```
**매장 등록**
```sql
-- 이름 중복 확인
SELECT s.id FROM store s WHERE s.name = ? LIMIT 1;
-- 연락처 중복 확인
SELECT s.id FROM store s WHERE s.contact = ? LIMIT 1;
-- 주소 중복 확인
SELECT s.id FROM store s WHERE s.address = ? LIMIT 1;
-- 사업자번호 중복 확인
SELECT s.id FROM store s WHERE s.business_reg_num = ? LIMIT 1;
-- 추가
INSERT INTO store (
address, business_reg_num, contact, created_at, created_by,
name, region_code, status, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**매장 수정**
```sql
-- 조회
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
-- 수정
UPDATE
store
SET
address = ?, business_reg_num = ?, contact = ?, name = ?,
region_code = ?, status = ?, updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**비활성화(status = DISABLE)**
```sql
-- 조회
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
-- 수정
UPDATE
store
SET
address = ?, business_reg_num = ?, contact = ?, name = ?,
region_code = ?, status = ?, updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**모든 매장 조회**
```sql
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.status = 'ACTIVE'
AND (? IS NULL OR s.region_code LIKE ?);
```
**개별 매장 상세 조회**
```sql
SELECT
s.id, s.address, s.business_reg_num, s.contact, s.created_at,
s.created_by, s.name, s.region_code, s.status, s.updated_at,
s.updated_by
FROM
store s
WHERE
s.id = ? AND s.status = 'ACTIVE';
```
### Theme
**생성**
```sql
-- 이름으로 조회
SELECT
t.id
FROM
theme t
WHERE
t.name = ?
LIMIT 1;
-- 추가
INSERT INTO theme (
available_minutes, created_at, created_by, description, difficulty,
expected_minutes_from, expected_minutes_to, is_active, max_participants,
min_participants, name, price, thumbnail_url, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
```
**Active인 모든 테마 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.is_active = TRUE;
```
**테마 목록 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t;
```
**감사 정보 포함 개별 테마 상세 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
```
**개별 테마 조회**
```sql
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
```
**삭제**
```sql
-- 조회
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
-- 삭제
DELETE FROM theme WHERE id = ?;
```
**수정**
```sql
-- 조회
SELECT
t.id, t.available_minutes, t.created_at, t.created_by, t.description,
t.difficulty, t.expected_minutes_from, t.expected_minutes_to,
t.is_active, t.max_participants, t.min_participants, t.name,
t.price, t.thumbnail_url, t.updated_at, t.updated_by
FROM
theme t
WHERE
t.id = ?;
-- 수정
UPDATE
theme
SET
available_minutes = ?, description = ?, difficulty = ?,
expected_minutes_from = ?, expected_minutes_to = ?, is_active = ?,
max_participants = ?, min_participants = ?, name = ?, price = ?,
thumbnail_url = ?, updated_at = ?, updated_by = ?
WHERE
id = ?;
```
**인기 테마 조회**
```sql
SELECT
t.id, t.name, t.description, t.difficulty, t.thumbnail_url, t.price,
t.min_participants, t.max_participants,
t.available_minutes, t.expected_minutes_from, t.expected_minutes_to
FROM
theme t
JOIN (
SELECT
s.theme_id, count(*) as reservation_count
FROM
schedule s
JOIN
reservation r ON s.id = r.schedule_id AND r.status = 'CONFIRMED'
WHERE
s.status = 'RESERVED'
AND (s.date BETWEEN :startFrom AND :endAt)
GROUP BY
s.theme_id
ORDER BY
reservation_count desc
LIMIT :count
) ranked_themes ON t.id = ranked_themes.theme_id
```
### User
**회원가입**
```sql
-- 이메일 중복 확인
SELECT
u.id
FROM
users u
WHERE
u.email = ?
LIMIT 1;
-- 연락처 중복 확인
SELECT
u.id
FROM
users u
WHERE
u.phone = ?
LIMIT 1;
-- 추가
INSERT INTO users (
created_at, created_by, email, name, password, phone, region_code,
status, updated_at, updated_by, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
);
-- 상태 변경 이력 추가
INSERT INTO user_status_history (
created_at, created_by, reason, status, updated_at, updated_by,
user_id, id
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?
);
```
**연락처 정보 조회**
```sql
SELECT
u.id, u.created_at, u.created_by, u.email, u.name, u.password,
u.phone, u.region_code, u.status, u.updated_at, u.updated_by
FROM
users u
WHERE
u.id = ?;
```

View File

@ -8,6 +8,10 @@ dependencies {
// API docs // API docs
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13") implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
// Cache
implementation("org.springframework.boot:spring-boot-starter-cache")
implementation("com.github.ben-manes.caffeine:caffeine")
// DB // DB
runtimeOnly("com.h2database:h2") runtimeOnly("com.h2database:h2")
runtimeOnly("com.mysql:mysql-connector-j") runtimeOnly("com.mysql:mysql-connector-j")

View File

@ -3,8 +3,14 @@ package com.sangdol.roomescape
import org.springframework.boot.Banner import org.springframework.boot.Banner
import org.springframework.boot.SpringApplication import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.cache.annotation.EnableCaching
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import java.util.* import java.util.*
@EnableAsync
@EnableCaching
@EnableScheduling
@SpringBootApplication( @SpringBootApplication(
scanBasePackages = ["com.sangdol.roomescape", "com.sangdol.common"] scanBasePackages = ["com.sangdol.roomescape", "com.sangdol.common"]
) )

View File

@ -20,7 +20,7 @@ class AdminService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findCredentialsByAccount(account: String): AdminLoginCredentials { fun findCredentialsByAccount(account: String): AdminLoginCredentials {
log.info { "[findCredentialsByAccount] 관리자 조회 시작: account=${account}" } log.debug { "[findCredentialsByAccount] 관리자 조회 시작: account=${account}" }
return adminRepository.findByAccount(account) return adminRepository.findByAccount(account)
?.let { ?.let {
@ -28,14 +28,14 @@ class AdminService(
it.toCredentials() it.toCredentials()
} }
?: run { ?: run {
log.info { "[findCredentialsByAccount] 관리자 조회 실패: account=${account}" } log.debug { "[findCredentialsByAccount] 관리자 조회 실패: account=${account}" }
throw AdminException(AdminErrorCode.ADMIN_NOT_FOUND) throw AdminException(AdminErrorCode.ADMIN_NOT_FOUND)
} }
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findOperatorOrUnknown(id: Long): Auditor { fun findOperatorOrUnknown(id: Long): Auditor {
log.info { "[findOperatorById] 작업자 정보 조회 시작: id=${id}" } log.debug { "[findOperatorById] 작업자 정보 조회 시작: id=${id}" }
return adminRepository.findByIdOrNull(id)?.let { admin -> return adminRepository.findByIdOrNull(id)?.let { admin ->
Auditor(admin.id, admin.name).also { Auditor(admin.id, admin.name).also {

View File

@ -33,7 +33,7 @@ class AuthService(
request: LoginRequest, request: LoginRequest,
context: LoginContext context: LoginContext
): LoginSuccessResponse { ): LoginSuccessResponse {
log.info { "[login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" } log.debug { "[login] 로그인 시작: account=${request.account}, type=${request.principalType}, context=${context}" }
val (credentials, extraClaims) = getCredentials(request) val (credentials, extraClaims) = getCredentials(request)
val event = LoginHistoryEvent( val event = LoginHistoryEvent(
@ -56,10 +56,7 @@ class AuthService(
} catch (e: Exception) { } catch (e: Exception) {
eventPublisher.publishEvent(event.onFailure()) eventPublisher.publishEvent(event.onFailure())
when (e) { when (e) {
is AuthException -> { is AuthException -> { throw e }
log.info { "[login] 로그인 실패: account = ${request.account}" }
throw e
}
else -> { else -> {
log.warn { "[login] 로그인 실패: message=${e.message} account = ${request.account}" } log.warn { "[login] 로그인 실패: message=${e.message} account = ${request.account}" }
@ -74,7 +71,7 @@ class AuthService(
credentials: LoginCredentials credentials: LoginCredentials
) { ) {
if (credentials.password != request.password) { if (credentials.password != request.password) {
log.info { "[login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" } log.debug { "[login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" }
throw AuthException(AuthErrorCode.LOGIN_FAILED) throw AuthException(AuthErrorCode.LOGIN_FAILED)
} }
} }

View File

@ -11,8 +11,6 @@ import jakarta.annotation.PreDestroy
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.context.event.EventListener import org.springframework.context.event.EventListener
import org.springframework.scheduling.annotation.Async import org.springframework.scheduling.annotation.Async
import org.springframework.scheduling.annotation.EnableAsync
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.ConcurrentLinkedQueue
@ -21,8 +19,6 @@ import java.util.concurrent.TimeUnit
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@Component @Component
@EnableAsync
@EnableScheduling
class LoginHistoryEventListener( class LoginHistoryEventListener(
private val idGenerator: IDGenerator, private val idGenerator: IDGenerator,
private val loginHistoryRepository: LoginHistoryRepository, private val loginHistoryRepository: LoginHistoryRepository,
@ -35,7 +31,7 @@ class LoginHistoryEventListener(
@Async @Async
@EventListener(classes = [LoginHistoryEvent::class]) @EventListener(classes = [LoginHistoryEvent::class])
fun onLoginCompleted(event: LoginHistoryEvent) { fun onLoginCompleted(event: LoginHistoryEvent) {
log.info { "[onLoginCompleted] 로그인 이력 저장 이벤트 수신: id=${event.id}, type=${event.type}" } log.debug { "[onLoginCompleted] 로그인 이력 저장 이벤트 수신: id=${event.id}, type=${event.type}" }
queue.add(event.toEntity(idGenerator.create())).also { queue.add(event.toEntity(idGenerator.create())).also {
log.info { "[onLoginCompleted] 로그인 이력 저장 이벤트 큐 저장 완료: id=${event.id}, type=${event.type}" } log.info { "[onLoginCompleted] 로그인 이력 저장 이벤트 큐 저장 완료: id=${event.id}, type=${event.type}" }
@ -48,10 +44,10 @@ class LoginHistoryEventListener(
@Scheduled(fixedRate = 30, timeUnit = TimeUnit.SECONDS) @Scheduled(fixedRate = 30, timeUnit = TimeUnit.SECONDS)
fun flushScheduled() { fun flushScheduled() {
log.info { "[flushScheduled] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" } log.debug { "[flushScheduled] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" }
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.info { "[flushScheduled] 큐에 있는 로그인 이력이 없음." } log.debug { "[flushScheduled] 큐에 있는 로그인 이력이 없음." }
return return
} }
flush() flush()
@ -60,7 +56,7 @@ class LoginHistoryEventListener(
@PreDestroy @PreDestroy
fun flushAll() { fun flushAll() {
log.info { "[flushAll] 애플리케이션 종료. 큐에 있는 모든 이력 저장 시작: size=${queue.size}" } log.debug { "[flushAll] 애플리케이션 종료. 큐에 있는 모든 이력 저장 시작: size=${queue.size}" }
while (!queue.isEmpty()) { while (!queue.isEmpty()) {
flush() flush()
} }
@ -68,10 +64,10 @@ class LoginHistoryEventListener(
} }
private fun flush() { private fun flush() {
log.info { "[flush] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" } log.debug { "[flush] 큐에 저장된 로그인 이력 저장 시작: size=${queue.size}" }
if (queue.isEmpty()) { if (queue.isEmpty()) {
log.info { "[flush] 큐에 있는 로그인 이력이 없음." } log.debug { "[flush] 큐에 있는 로그인 이력이 없음." }
return; return;
} }

View File

@ -50,7 +50,7 @@ class JwtUtils(
val claims = extractAllClaims(token) val claims = extractAllClaims(token)
return claims.subject ?: run { return claims.subject ?: run {
log.info { "[JwtUtils.extractSubject] subject를 찾을 수 없음.: token = ${token}" } log.debug { "[JwtUtils.extractSubject] subject를 찾을 수 없음.: token = ${token}" }
throw AuthException(AuthErrorCode.INVALID_TOKEN) throw AuthException(AuthErrorCode.INVALID_TOKEN)
} }
} }

View File

@ -32,7 +32,7 @@ class OrderService(
fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) { fun confirm(reservationId: Long, paymentConfirmRequest: PaymentConfirmRequest) {
val paymentKey = paymentConfirmRequest.paymentKey val paymentKey = paymentConfirmRequest.paymentKey
log.info { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" } log.debug { "[confirm] 결제 및 예약 확정 시작: reservationId=${reservationId}, paymentKey=${paymentKey}" }
try { try {
transactionExecutionUtil.withNewTransaction(isReadOnly = false) { transactionExecutionUtil.withNewTransaction(isReadOnly = false) {
validateCanConfirm(reservationId) validateCanConfirm(reservationId)
@ -42,7 +42,7 @@ class OrderService(
paymentService.requestConfirm(reservationId, paymentConfirmRequest) paymentService.requestConfirm(reservationId, paymentConfirmRequest)
eventPublisher.publishEvent(ReservationConfirmEvent(reservationId)) eventPublisher.publishEvent(ReservationConfirmEvent(reservationId))
log.info { "[confirm] 결제 처리 및 예약 확정 이벤트 발행 완료" } log.info { "[confirm] 결제 처리 및 예약 확정 이벤트 발행 완료: reservationId=${reservationId}, paymentKey=${paymentKey}" }
} catch (e: Exception) { } catch (e: Exception) {
val errorCode: ErrorCode = if (e is RoomescapeException) { val errorCode: ErrorCode = if (e is RoomescapeException) {
e.errorCode e.errorCode
@ -55,7 +55,7 @@ class OrderService(
} }
private fun validateCanConfirm(reservationId: Long) { private fun validateCanConfirm(reservationId: Long) {
log.info { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" } log.debug { "[validateAndMarkInProgress] 예약 확정 가능 여부 검증 시작: reservationId=${reservationId}" }
val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId) val reservation: ReservationStateResponse = reservationService.findStatusWithLock(reservationId)
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId) val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(reservation.scheduleId)

View File

@ -27,15 +27,12 @@ class OrderValidator {
private fun validateReservationStatus(reservation: ReservationStateResponse) { private fun validateReservationStatus(reservation: ReservationStateResponse) {
when (reservation.status) { when (reservation.status) {
ReservationStatus.CONFIRMED -> { ReservationStatus.CONFIRMED -> {
log.info { "[validateCanConfirm] 이미 확정된 예약: id=${reservation.id}" }
throw OrderException(OrderErrorCode.ORDER_ALREADY_CONFIRMED) throw OrderException(OrderErrorCode.ORDER_ALREADY_CONFIRMED)
} }
ReservationStatus.EXPIRED -> { ReservationStatus.EXPIRED -> {
log.info { "[validateCanConfirm] 만료된 예약: id=${reservation.id}" }
throw OrderException(OrderErrorCode.EXPIRED_RESERVATION) throw OrderException(OrderErrorCode.EXPIRED_RESERVATION)
} }
ReservationStatus.CANCELED -> { ReservationStatus.CANCELED -> {
log.info { "[validateCanConfirm] 취소된 예약: id=${reservation.id}" }
throw OrderException(OrderErrorCode.CANCELED_RESERVATION) throw OrderException(OrderErrorCode.CANCELED_RESERVATION)
} }
else -> {} else -> {}
@ -44,14 +41,14 @@ class OrderValidator {
private fun validateScheduleStatus(schedule: ScheduleStateResponse) { private fun validateScheduleStatus(schedule: ScheduleStateResponse) {
if (schedule.status != ScheduleStatus.HOLD) { if (schedule.status != ScheduleStatus.HOLD) {
log.info { "[validateScheduleStatus] 일정 상태 오류: status=${schedule.status}" } log.debug { "[validateScheduleStatus] 일정 상태 오류: status=${schedule.status}" }
throw OrderException(OrderErrorCode.EXPIRED_RESERVATION) throw OrderException(OrderErrorCode.EXPIRED_RESERVATION)
} }
val scheduleDateTime = LocalDateTime.of(schedule.date, schedule.startFrom) val scheduleDateTime = LocalDateTime.of(schedule.date, schedule.startFrom)
val nowDateTime = KoreaDateTime.now() val nowDateTime = KoreaDateTime.now()
if (scheduleDateTime.isBefore(nowDateTime)) { if (scheduleDateTime.isBefore(nowDateTime)) {
log.info { "[validateScheduleStatus] 과거 시간인 일정으로 인한 실패: scheduleDateTime=${scheduleDateTime}(KST), now=${nowDateTime}(KST)" } log.debug { "[validateScheduleStatus] 과거 시간인 일정으로 인한 실패: scheduleDateTime=${scheduleDateTime}(KST), now=${nowDateTime}(KST)" }
throw OrderException(OrderErrorCode.PAST_SCHEDULE) throw OrderException(OrderErrorCode.PAST_SCHEDULE)
} }
} }

View File

@ -31,7 +31,7 @@ class PaymentService(
private val eventPublisher: ApplicationEventPublisher private val eventPublisher: ApplicationEventPublisher
) { ) {
fun requestConfirm(reservationId: Long, request: PaymentConfirmRequest): PaymentGatewayResponse { fun requestConfirm(reservationId: Long, request: PaymentConfirmRequest): PaymentGatewayResponse {
log.info { "[requestConfirm] 결제 요청 시작: paymentKey=${request.paymentKey}" } log.debug { "[requestConfirm] 결제 요청 시작: paymentKey=${request.paymentKey}" }
try { try {
return paymentClient.confirm(request.paymentKey, request.orderId, request.amount).also { return paymentClient.confirm(request.paymentKey, request.orderId, request.amount).also {
eventPublisher.publishEvent(it.toEvent(reservationId)) eventPublisher.publishEvent(it.toEvent(reservationId))
@ -90,7 +90,7 @@ class PaymentService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findDetailByReservationId(reservationId: Long): PaymentResponse? { fun findDetailByReservationId(reservationId: Long): PaymentResponse? {
log.info { "[findDetailByReservationId] 예약 결제 정보 조회 시작: reservationId=$reservationId" } log.debug { "[findDetailByReservationId] 예약 결제 정보 조회 시작: reservationId=$reservationId" }
val payment: PaymentEntity? = findByReservationIdOrNull(reservationId) val payment: PaymentEntity? = findByReservationIdOrNull(reservationId)
val paymentDetail: PaymentDetailEntity? = payment?.let { findDetailByPaymentIdOrNull(it.id) } val paymentDetail: PaymentDetailEntity? = payment?.let { findDetailByPaymentIdOrNull(it.id) }
@ -99,11 +99,13 @@ class PaymentService(
return payment?.toResponse( return payment?.toResponse(
detail = paymentDetail?.toResponse(), detail = paymentDetail?.toResponse(),
cancel = cancelDetail?.toResponse() cancel = cancelDetail?.toResponse()
) ).also {
log.info { "[findDetailByReservationId] 예약 결제 정보 조회 완료: reservationId=$reservationId" }
}
} }
private fun findByReservationIdOrThrow(reservationId: Long): PaymentEntity { private fun findByReservationIdOrThrow(reservationId: Long): PaymentEntity {
log.info { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } log.debug { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" }
return paymentRepository.findByReservationId(reservationId) return paymentRepository.findByReservationId(reservationId)
?.also { log.info { "[findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } } ?.also { log.info { "[findByReservationIdOrThrow] 결제 정보 조회 완료: reservationId=$reservationId, paymentId=${it.id}" } }
@ -114,7 +116,7 @@ class PaymentService(
} }
private fun findByReservationIdOrNull(reservationId: Long): PaymentEntity? { private fun findByReservationIdOrNull(reservationId: Long): PaymentEntity? {
log.info { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" } log.debug { "[findByReservationIdOrThrow] 결제 정보 조회 시작: reservationId=: $reservationId" }
return paymentRepository.findByReservationId(reservationId) return paymentRepository.findByReservationId(reservationId)
.also { .also {
@ -127,7 +129,7 @@ class PaymentService(
} }
private fun findDetailByPaymentIdOrNull(paymentId: Long): PaymentDetailEntity? { private fun findDetailByPaymentIdOrNull(paymentId: Long): PaymentDetailEntity? {
log.info { "[findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" } log.debug { "[findDetailByPaymentIdOrThrow] 결제 상세 정보 조회 시작: paymentId=$paymentId" }
return paymentDetailRepository.findByPaymentId(paymentId).also { return paymentDetailRepository.findByPaymentId(paymentId).also {
if (it != null) { if (it != null) {
@ -139,7 +141,7 @@ class PaymentService(
} }
private fun findCancelByPaymentIdOrNull(paymentId: Long): CanceledPaymentEntity? { private fun findCancelByPaymentIdOrNull(paymentId: Long): CanceledPaymentEntity? {
log.info { "[findDetailByReservationId] 취소 결제 정보 조회 시작: paymentId=${paymentId}" } log.debug { "[findDetailByReservationId] 취소 결제 정보 조회 시작: paymentId=${paymentId}" }
return canceledPaymentRepository.findByPaymentId(paymentId).also { return canceledPaymentRepository.findByPaymentId(paymentId).also {
if (it == null) { if (it == null) {

View File

@ -29,20 +29,16 @@ class PaymentEventListener(
fun handlePaymentEvent(event: PaymentEvent) { fun handlePaymentEvent(event: PaymentEvent) {
val reservationId = event.reservationId val reservationId = event.reservationId
log.info { "[handlePaymentEvent] 결제 정보 저장 이벤트 수신: reservationId=${reservationId}, paymentKey=${event.paymentKey}" } log.debug { "[handlePaymentEvent] 결제 정보 저장 이벤트 수신: reservationId=${reservationId}, paymentKey=${event.paymentKey}" }
val paymentId = idGenerator.create() val paymentId = idGenerator.create()
val paymentEntity: PaymentEntity = event.toEntity(paymentId) val paymentEntity: PaymentEntity = event.toEntity(paymentId)
paymentRepository.save(paymentEntity).also { paymentRepository.save(paymentEntity)
log.info { "[handlePaymentEvent] 결제 정보 저장 완료: paymentId=${paymentId}" }
}
val paymentDetailId = idGenerator.create() val paymentDetailId = idGenerator.create()
val paymentDetailEntity: PaymentDetailEntity = event.toDetailEntity(id = paymentDetailId, paymentId = paymentId) val paymentDetailEntity: PaymentDetailEntity = event.toDetailEntity(id = paymentDetailId, paymentId = paymentId)
paymentDetailRepository.save(paymentDetailEntity).also { paymentDetailRepository.save(paymentDetailEntity)
log.info { "[handlePaymentEvent] 결제 상세 저장 완료: paymentDetailId=${paymentDetailId}" }
}
log.info { "[handlePaymentEvent] 결제 정보 저장 이벤트 처리 완료" } log.info { "[handlePaymentEvent] 결제 정보 저장 이벤트 처리 완료: reservationId=${reservationId}, paymentId=${paymentId}, paymentDetailId=${paymentDetailId}" }
} }
} }

View File

@ -33,7 +33,7 @@ class TosspayClient(
amount: Int, amount: Int,
): PaymentGatewayResponse { ): PaymentGatewayResponse {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
log.info { "[TosspayClient.confirm] 결제 승인 요청: paymentKey=$paymentKey, orderId=$orderId, amount=$amount" } log.debug { "[TosspayClient.confirm] 결제 승인 요청: paymentKey=$paymentKey, orderId=$orderId, amount=$amount" }
return confirmClient.request(paymentKey, orderId, amount) return confirmClient.request(paymentKey, orderId, amount)
.also { .also {
@ -47,7 +47,7 @@ class TosspayClient(
cancelReason: String cancelReason: String
): PaymentGatewayCancelResponse { ): PaymentGatewayCancelResponse {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
log.info { "[TosspayClient.cancel] 결제 취소 요청: paymentKey=$paymentKey, amount=$amount, cancelReason=$cancelReason" } log.debug { "[TosspayClient.cancel] 결제 취소 요청: paymentKey=$paymentKey, amount=$amount, cancelReason=$cancelReason" }
return cancelClient.request(paymentKey, amount, cancelReason).also { return cancelClient.request(paymentKey, amount, cancelReason).also {
log.info { "[TosspayClient.cancel] 결제 취소 완료: duration_ms=${System.currentTimeMillis() - startTime}ms, paymentKey=$paymentKey" } log.info { "[TosspayClient.cancel] 결제 취소 완료: duration_ms=${System.currentTimeMillis() - startTime}ms, paymentKey=$paymentKey" }

View File

@ -17,7 +17,7 @@ class RegionService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun readAllSido(): SidoListResponse { fun readAllSido(): SidoListResponse {
log.info { "[readAllSido] 모든 시/도 조회 시작" } log.debug { "[readAllSido] 모든 시/도 조회 시작" }
val result: List<Pair<String, String>> = regionRepository.readAllSido() val result: List<Pair<String, String>> = regionRepository.readAllSido()
if (result.isEmpty()) { if (result.isEmpty()) {
@ -32,7 +32,7 @@ class RegionService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findSigunguBySido(sidoCode: String): SigunguListResponse { fun findSigunguBySido(sidoCode: String): SigunguListResponse {
log.info { "[findSigunguBySido] 시/군/구 조회 시작: sidoCode=${sidoCode}" } log.debug { "[findSigunguBySido] 시/군/구 조회 시작: sidoCode=${sidoCode}" }
val result: List<Pair<String, String>> = regionRepository.findAllSigunguBySido(sidoCode) val result: List<Pair<String, String>> = regionRepository.findAllSigunguBySido(sidoCode)
if (result.isEmpty()) { if (result.isEmpty()) {
@ -47,7 +47,7 @@ class RegionService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findRegionCode(sidoCode: String, sigunguCode: String): RegionCodeResponse { fun findRegionCode(sidoCode: String, sigunguCode: String): RegionCodeResponse {
log.info { "[findRegionCode] 지역 코드 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } log.debug { "[findRegionCode] 지역 코드 조회 시작: sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" }
return regionRepository.findRegionCode(sidoCode, sigunguCode)?.let { return regionRepository.findRegionCode(sidoCode, sigunguCode)?.let {
log.info { "[findRegionCode] 지역 코드 조회 완료: code=${it} sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" } log.info { "[findRegionCode] 지역 코드 조회 완료: code=${it} sidoCode=${sidoCode} / sigunguCode=${sigunguCode}" }
@ -60,7 +60,7 @@ class RegionService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findRegionInfo(regionCode: String): RegionInfoResponse { fun findRegionInfo(regionCode: String): RegionInfoResponse {
log.info { "[findRegionInfo] 지역 정보 조회 시작: regionCode=${regionCode}" } log.debug { "[findRegionInfo] 지역 정보 조회 시작: regionCode=${regionCode}" }
return regionRepository.findByCode(regionCode)?.let { return regionRepository.findByCode(regionCode)?.let {
log.info { "[findRegionInfo] 지역 정보 조회 완료: code=${it} regionCode=${regionCode}" } log.info { "[findRegionInfo] 지역 정보 조회 완료: code=${it} regionCode=${regionCode}" }

View File

@ -45,7 +45,7 @@ class ReservationService(
user: CurrentUserContext, user: CurrentUserContext,
request: PendingReservationCreateRequest request: PendingReservationCreateRequest
): PendingReservationCreateResponse { ): PendingReservationCreateResponse {
log.info { "[createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" } log.debug { "[createPendingReservation] Pending 예약 생성 시작: schedule=${request.scheduleId}" }
run { run {
val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(request.scheduleId) val schedule: ScheduleStateResponse = scheduleService.findStateWithLock(request.scheduleId)
@ -64,7 +64,7 @@ class ReservationService(
@Transactional @Transactional
fun confirmReservation(id: Long) { fun confirmReservation(id: Long) {
log.info { "[confirmReservation] Pending 예약 확정 시작: reservationId=${id}" } log.debug { "[confirmReservation] Pending 예약 확정 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id) val reservation: ReservationEntity = findOrThrow(id)
run { run {
@ -81,7 +81,7 @@ class ReservationService(
@Transactional @Transactional
fun cancelReservation(user: CurrentUserContext, reservationId: Long, request: ReservationCancelRequest) { fun cancelReservation(user: CurrentUserContext, reservationId: Long, request: ReservationCancelRequest) {
log.info { "[cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" } log.debug { "[cancelReservation] 예약 취소 시작: userId=${user.id}, reservationId=${reservationId}" }
val reservation: ReservationEntity = findOrThrow(reservationId) val reservation: ReservationEntity = findOrThrow(reservationId)
@ -100,7 +100,7 @@ class ReservationService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findAllUserReservationOverview(user: CurrentUserContext): ReservationOverviewListResponse { fun findAllUserReservationOverview(user: CurrentUserContext): ReservationOverviewListResponse {
log.info { "[findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" } log.debug { "[findSummaryByMemberId] 예약 조회 시작: userId=${user.id}" }
val reservations: List<ReservationEntity> = reservationRepository.findAllByUserIdAndStatusIsIn( val reservations: List<ReservationEntity> = reservationRepository.findAllByUserIdAndStatusIsIn(
userId = user.id, userId = user.id,
@ -125,7 +125,7 @@ class ReservationService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findDetailById(id: Long): ReservationAdditionalResponse { fun findDetailById(id: Long): ReservationAdditionalResponse {
log.info { "[findDetailById] 예약 상세 조회 시작: reservationId=${id}" } log.debug { "[findDetailById] 예약 상세 조회 시작: reservationId=${id}" }
val reservation: ReservationEntity = findOrThrow(id) val reservation: ReservationEntity = findOrThrow(id)
val user: UserContactResponse = userService.findContactById(reservation.userId) val user: UserContactResponse = userService.findContactById(reservation.userId)
@ -141,7 +141,7 @@ class ReservationService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findStatusWithLock(id: Long): ReservationStateResponse { fun findStatusWithLock(id: Long): ReservationStateResponse {
log.info { "[findStatusWithLock] 예약 LOCK + 상태 조회 시작: reservationId=${id}" } log.debug { "[findStatusWithLock] 예약 LOCK + 상태 조회 시작: reservationId=${id}" }
return reservationRepository.findByIdForUpdate(id)?.let { return reservationRepository.findByIdForUpdate(id)?.let {
log.info { "[findStatusWithLock] 예약 LOCK + 상태 조회 완료: reservationId=${id}" } log.info { "[findStatusWithLock] 예약 LOCK + 상태 조회 완료: reservationId=${id}" }
@ -154,7 +154,7 @@ class ReservationService(
@Transactional @Transactional
fun markInProgress(reservationId: Long) { fun markInProgress(reservationId: Long) {
log.info { "[markInProgress] 예약 상태 ${ReservationStatus.PAYMENT_IN_PROGRESS} 변경 시작." } log.debug { "[markInProgress] 예약 상태 ${ReservationStatus.PAYMENT_IN_PROGRESS} 변경 시작." }
findOrThrow(reservationId).apply { findOrThrow(reservationId).apply {
this.status = ReservationStatus.PAYMENT_IN_PROGRESS this.status = ReservationStatus.PAYMENT_IN_PROGRESS
@ -164,7 +164,7 @@ class ReservationService(
} }
private fun findOrThrow(id: Long): ReservationEntity { private fun findOrThrow(id: Long): ReservationEntity {
log.info { "[findOrThrow] 예약 조회 시작: reservationId=${id}" } log.debug { "[findOrThrow] 예약 조회 시작: reservationId=${id}" }
return reservationRepository.findByIdOrNull(id) return reservationRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 예약 조회 완료: reservationId=${id}" } } ?.also { log.info { "[findOrThrow] 예약 조회 완료: reservationId=${id}" } }

View File

@ -1,17 +1,15 @@
package com.sangdol.roomescape.reservation.business package com.sangdol.roomescape.reservation.business
import com.sangdol.common.utils.KoreaDateTime import com.sangdol.common.utils.KoreaDateTime
import com.sangdol.common.utils.toKoreaDateTime import com.sangdol.roomescape.reservation.dto.PendingReservationCreateRequest
import com.sangdol.roomescape.reservation.exception.ReservationErrorCode import com.sangdol.roomescape.reservation.exception.ReservationErrorCode
import com.sangdol.roomescape.reservation.exception.ReservationException import com.sangdol.roomescape.reservation.exception.ReservationException
import com.sangdol.roomescape.reservation.dto.PendingReservationCreateRequest
import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse import com.sangdol.roomescape.schedule.dto.ScheduleStateResponse
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.theme.dto.ThemeInfoResponse import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}

View File

@ -22,7 +22,7 @@ class ReservationEventListener(
fun handleReservationConfirmEvent(event: ReservationConfirmEvent) { fun handleReservationConfirmEvent(event: ReservationConfirmEvent) {
val reservationId = event.reservationId val reservationId = event.reservationId
log.info { "[handleReservationConfirmEvent] 예약 확정 이벤트 수신: reservationId=${reservationId}" } log.debug { "[handleReservationConfirmEvent] 예약 확정 이벤트 수신: reservationId=${reservationId}" }
val modifiedRows = reservationRepository.confirmReservation(Instant.now(), reservationId) val modifiedRows = reservationRepository.confirmReservation(Instant.now(), reservationId)
if (modifiedRows == 0) { if (modifiedRows == 0) {

View File

@ -5,7 +5,6 @@ import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleReposi
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -15,7 +14,6 @@ import java.util.concurrent.TimeUnit
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@Component @Component
@EnableScheduling
class IncompletedReservationScheduler( class IncompletedReservationScheduler(
private val scheduleRepository: ScheduleRepository, private val scheduleRepository: ScheduleRepository,
private val reservationRepository: ReservationRepository private val reservationRepository: ReservationRepository
@ -24,10 +22,10 @@ class IncompletedReservationScheduler(
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES) @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
@Transactional @Transactional
fun processExpiredHoldSchedule() { fun processExpiredHoldSchedule() {
log.info { "[processExpiredHoldSchedule] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" } log.debug { "[processExpiredHoldSchedule] 만료 시간이 지난 ${ScheduleStatus.HOLD} 상태의 일정 재활성화 시작" }
val targets: List<Long> = scheduleRepository.findAllExpiredHeldSchedules(Instant.now()).also { val targets: List<Long> = scheduleRepository.findAllExpiredHeldSchedules(Instant.now()).also {
log.info { "[processExpiredHoldSchedule] ${it.size} 개의 일정 조회 완료" } log.debug { "[processExpiredHoldSchedule] ${it.size} 개의 일정 조회 완료" }
} }
scheduleRepository.releaseHeldSchedules(targets).also { scheduleRepository.releaseHeldSchedules(targets).also {
@ -38,7 +36,7 @@ class IncompletedReservationScheduler(
@Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES) @Scheduled(initialDelay = 1, fixedRate = 1, timeUnit = TimeUnit.MINUTES)
@Transactional @Transactional
fun processExpiredReservation() { fun processExpiredReservation() {
log.info { "[processExpiredReservation] 결제되지 않은 예약 만료 처리 시작" } log.debug { "[processExpiredReservation] 결제되지 않은 예약 만료 처리 시작" }
val targets: List<Long> = reservationRepository.findAllExpiredReservation().also { val targets: List<Long> = reservationRepository.findAllExpiredReservation().also {
log.info { "[processExpiredReservation] ${it.size} 개의 예약 조회 완료" } log.info { "[processExpiredReservation] ${it.size} 개의 예약 조회 완료" }

View File

@ -34,7 +34,7 @@ class AdminScheduleService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse { fun searchSchedules(storeId: Long, date: LocalDate?, themeId: Long?): AdminScheduleSummaryListResponse {
log.info { "[searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" } log.debug { "[searchSchedules] 일정 검색 시작: storeId=$storeId, date=$date, themeId=$themeId" }
val searchDate = date ?: KoreaDate.today() val searchDate = date ?: KoreaDate.today()
@ -44,14 +44,12 @@ class AdminScheduleService(
.sortedBy { it.time } .sortedBy { it.time }
return schedules.toAdminSummaryResponse() return schedules.toAdminSummaryResponse()
.also { .also { log.info { "[searchSchedules] ${it.schedules.size} 개의 일정 조회 완료" } }
log.info { "[searchSchedules] ${it.schedules.size} 개의 일정 조회 완료" }
}
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findScheduleAudit(id: Long): AuditingInfo { fun findScheduleAudit(id: Long): AuditingInfo {
log.info { "[findDetail] 일정 감사 정보 조회 시작: id=$id" } log.debug { "[findDetail] 일정 감사 정보 조회 시작: id=$id" }
val schedule: ScheduleEntity = findOrThrow(id) val schedule: ScheduleEntity = findOrThrow(id)
@ -64,7 +62,7 @@ class AdminScheduleService(
@Transactional @Transactional
fun createSchedule(storeId: Long, request: ScheduleCreateRequest): ScheduleCreateResponse { fun createSchedule(storeId: Long, request: ScheduleCreateRequest): ScheduleCreateResponse {
log.info { "[createSchedule] 일정 생성 시작: storeId=${storeId}, date=${request.date}, time=${request.time}, themeId=${request.themeId}" } log.debug { "[createSchedule] 일정 생성 시작: storeId=${storeId}, date=${request.date}, time=${request.time}, themeId=${request.themeId}" }
scheduleValidator.validateCanCreate(storeId, request) scheduleValidator.validateCanCreate(storeId, request)
@ -79,14 +77,12 @@ class AdminScheduleService(
} }
return ScheduleCreateResponse(schedule.id) return ScheduleCreateResponse(schedule.id)
.also { .also { log.info { "[createSchedule] 일정 생성 완료: id=${it.id}" } }
log.info { "[createSchedule] 일정 생성 완료: id=${it.id}" }
}
} }
@Transactional @Transactional
fun updateSchedule(id: Long, request: ScheduleUpdateRequest) { fun updateSchedule(id: Long, request: ScheduleUpdateRequest) {
log.info { "[updateSchedule] 일정 수정 시작: id=$id, request=${request}" } log.debug { "[updateSchedule] 일정 수정 시작: id=$id, request=${request}" }
if (request.isAllParamsNull()) { if (request.isAllParamsNull()) {
log.info { "[updateSchedule] 일정 변경 사항 없음: id=$id" } log.info { "[updateSchedule] 일정 변경 사항 없음: id=$id" }
@ -104,7 +100,7 @@ class AdminScheduleService(
@Transactional @Transactional
fun deleteSchedule(id: Long) { fun deleteSchedule(id: Long) {
log.info { "[deleteSchedule] 일정 삭제 시작: id=$id" } log.debug { "[deleteSchedule] 일정 삭제 시작: id=$id" }
val schedule: ScheduleEntity = findOrThrow(id).also { val schedule: ScheduleEntity = findOrThrow(id).also {
scheduleValidator.validateCanDelete(it) scheduleValidator.validateCanDelete(it)
@ -116,7 +112,7 @@ class AdminScheduleService(
} }
private fun findOrThrow(id: Long): ScheduleEntity { private fun findOrThrow(id: Long): ScheduleEntity {
log.info { "[findOrThrow] 일정 조회 시작: id=$id" } log.debug { "[findOrThrow] 일정 조회 시작: id=$id" }
return scheduleRepository.findByIdOrNull(id) return scheduleRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 일정 조회 완료: id=$id" } } ?.also { log.info { "[findOrThrow] 일정 조회 완료: id=$id" } }

View File

@ -30,13 +30,12 @@ class ScheduleService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse { fun getStoreScheduleByDate(storeId: Long, date: LocalDate): ScheduleWithThemeListResponse {
log.info { "[getStoreScheduleByDate] 매장 일정 조회: storeId=${storeId}, date=$date" } log.debug { "[getStoreScheduleByDate] 매장 일정 조회 시작: storeId=${storeId}, date=$date" }
val currentDate: LocalDate = KoreaDate.today() val currentDate: LocalDate = KoreaDate.today()
val currentTime: LocalTime = KoreaTime.now() val currentTime: LocalTime = KoreaTime.now()
if (date.isBefore(currentDate)) { if (date.isBefore(currentDate)) {
log.warn { "[getStoreScheduleByDate] 이전 날짜 선택으로 인한 실패: date=${date}" }
throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME) throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME)
} }
@ -44,6 +43,7 @@ class ScheduleService(
scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date) scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date)
.filter { it.date.isAfter(currentDate) || it.time.isAfter(currentTime) } .filter { it.date.isAfter(currentDate) || it.time.isAfter(currentTime) }
return schedules.toResponseWithTheme() return schedules.toResponseWithTheme()
.also { .also {
log.info { "[getStoreScheduleByDate] storeId=${storeId}, date=$date${it.schedules.size}개 일정 조회 완료" } log.info { "[getStoreScheduleByDate] storeId=${storeId}, date=$date${it.schedules.size}개 일정 조회 완료" }
@ -52,7 +52,7 @@ class ScheduleService(
@Transactional @Transactional
fun holdSchedule(id: Long) { fun holdSchedule(id: Long) {
log.info { "[holdSchedule] 일정 Holding 시작: id=$id" } log.debug { "[holdSchedule] 일정 Holding 시작: id=$id" }
val schedule = findForUpdateOrThrow(id).also { val schedule = findForUpdateOrThrow(id).also {
scheduleValidator.validateCanHold(it) scheduleValidator.validateCanHold(it)
@ -69,7 +69,7 @@ class ScheduleService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findStateWithLock(id: Long): ScheduleStateResponse { fun findStateWithLock(id: Long): ScheduleStateResponse {
log.info { "[findDateTimeById] 일정 개요 조회 시작 : id=$id" } log.debug { "[findDateTimeById] 일정 개요 조회 시작 : id=$id" }
val schedule: ScheduleEntity = scheduleRepository.findByIdForUpdate(id) val schedule: ScheduleEntity = scheduleRepository.findByIdForUpdate(id)
?: run { ?: run {
@ -95,7 +95,7 @@ class ScheduleService(
@Transactional @Transactional
fun changeStatus(scheduleId: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus) { fun changeStatus(scheduleId: Long, currentStatus: ScheduleStatus, changeStatus: ScheduleStatus) {
log.info { "[reserveSchedule] 일정 상태 변경 시작: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } log.debug { "[reserveSchedule] 일정 상태 변경 시작: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" }
scheduleRepository.changeStatus(scheduleId, currentStatus, changeStatus).also { scheduleRepository.changeStatus(scheduleId, currentStatus, changeStatus).also {
log.info { "[reserveSchedule] 일정 상태 변경 완료: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" } log.info { "[reserveSchedule] 일정 상태 변경 완료: id=${scheduleId}, currentStatus=${currentStatus}, changeStatus=${changeStatus}" }
@ -103,7 +103,7 @@ class ScheduleService(
} }
private fun findForUpdateOrThrow(id: Long): ScheduleEntity { private fun findForUpdateOrThrow(id: Long): ScheduleEntity {
log.info { "[findForUpdateOrThrow] 일정 LOCK + 조회 시작: id=$id" } log.debug { "[findForUpdateOrThrow] 일정 LOCK + 조회 시작: id=$id" }
return scheduleRepository.findByIdForUpdate(id) return scheduleRepository.findByIdForUpdate(id)
?.also { log.info { "[findForUpdateOrThrow] 일정 조회 완료: id=$id" } } ?.also { log.info { "[findForUpdateOrThrow] 일정 조회 완료: id=$id" } }

View File

@ -59,7 +59,7 @@ class ScheduleValidator(
private fun validateAlreadyExists(storeId: Long, date: LocalDate, themeId: Long, time: LocalTime) { private fun validateAlreadyExists(storeId: Long, date: LocalDate, themeId: Long, time: LocalTime) {
if (scheduleRepository.existsDuplicate(storeId, date, themeId, time)) { if (scheduleRepository.existsDuplicate(storeId, date, themeId, time)) {
log.info { log.debug {
"[validateAlreadyExists] 동일한 날짜, 테마, 시간 존재로 인한 실패: date=${date} / themeId=${themeId} / time=${time}" "[validateAlreadyExists] 동일한 날짜, 테마, 시간 존재로 인한 실패: date=${date} / themeId=${themeId} / time=${time}"
} }
throw ScheduleException(ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS) throw ScheduleException(ScheduleErrorCode.SCHEDULE_ALREADY_EXISTS)
@ -71,7 +71,7 @@ class ScheduleValidator(
val inputDateTime = LocalDateTime.of(date, time).truncatedTo(ChronoUnit.MINUTES) val inputDateTime = LocalDateTime.of(date, time).truncatedTo(ChronoUnit.MINUTES)
if (inputDateTime.isBefore(now)) { if (inputDateTime.isBefore(now)) {
log.info { log.debug {
"[validateDateTime] 이전 시간 선택으로 인한 실패: date=${date} / time=${time}" "[validateDateTime] 이전 시간 선택으로 인한 실패: date=${date} / time=${time}"
} }
throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME) throw ScheduleException(ScheduleErrorCode.PAST_DATE_TIME)
@ -82,7 +82,7 @@ class ScheduleValidator(
scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date, themeId) scheduleRepository.findStoreSchedulesWithThemeByDate(storeId, date, themeId)
.firstOrNull { it.containsTime(time) } .firstOrNull { it.containsTime(time) }
?.let { ?.let {
log.info { "[validateTimeNotConflict] 시간이 겹치는 일정 존재: conflictSchedule(Id=${it.id}, time=${it.time}~${it.getEndAt()})" } log.debug { "[validateTimeNotConflict] 시간이 겹치는 일정 존재: conflictSchedule(Id=${it.id}, time=${it.time}~${it.getEndAt()})" }
throw ScheduleException(ScheduleErrorCode.SCHEDULE_TIME_CONFLICT) throw ScheduleException(ScheduleErrorCode.SCHEDULE_TIME_CONFLICT)
} }
} }

View File

@ -35,7 +35,7 @@ class StoreService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getDetail(id: Long): StoreDetailResponse { fun getDetail(id: Long): StoreDetailResponse {
log.info { "[getDetail] 매장 상세 조회 시작: id=${id}" } log.debug { "[getDetail] 매장 상세 조회 시작: id=${id}" }
val store: StoreEntity = findOrThrow(id) val store: StoreEntity = findOrThrow(id)
val region = regionService.findRegionInfo(store.regionCode) val region = regionService.findRegionInfo(store.regionCode)
@ -47,7 +47,7 @@ class StoreService(
@Transactional @Transactional
fun register(request: StoreRegisterRequest): StoreRegisterResponse { fun register(request: StoreRegisterRequest): StoreRegisterResponse {
log.info { "[register] 매장 등록 시작: name=${request.name}" } log.debug { "[register] 매장 등록 시작: name=${request.name}" }
storeValidator.validateCanRegister(request) storeValidator.validateCanRegister(request)
@ -70,7 +70,7 @@ class StoreService(
@Transactional @Transactional
fun update(id: Long, request: StoreUpdateRequest) { fun update(id: Long, request: StoreUpdateRequest) {
log.info { "[update] 매장 수정 시작: id=${id}, request=${request}" } log.debug { "[update] 매장 수정 시작: id=${id}, request=${request}" }
storeValidator.validateCanUpdate(request) storeValidator.validateCanUpdate(request)
@ -83,7 +83,7 @@ class StoreService(
@Transactional @Transactional
fun disableById(id: Long) { fun disableById(id: Long) {
log.info { "[inactive] 매장 비활성화 시작: id=${id}" } log.debug { "[inactive] 매장 비활성화 시작: id=${id}" }
findOrThrow(id).apply { findOrThrow(id).apply {
this.disable() this.disable()
@ -94,7 +94,7 @@ class StoreService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getAllActiveStores(sidoCode: String?, sigunguCode: String?): StoreNameListResponse { fun getAllActiveStores(sidoCode: String?, sigunguCode: String?): StoreNameListResponse {
log.info { "[getAllActiveStores] 전체 매장 조회 시작" } log.debug { "[getAllActiveStores] 전체 매장 조회 시작" }
val regionCode: String? = when { val regionCode: String? = when {
sidoCode == null && sigunguCode != null -> throw StoreException(StoreErrorCode.SIDO_CODE_REQUIRED) sidoCode == null && sigunguCode != null -> throw StoreException(StoreErrorCode.SIDO_CODE_REQUIRED)
@ -108,7 +108,7 @@ class StoreService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findStoreInfo(id: Long): StoreInfoResponse { fun findStoreInfo(id: Long): StoreInfoResponse {
log.info { "[findStoreInfo] 매장 정보 조회 시작: id=${id}" } log.debug { "[findStoreInfo] 매장 정보 조회 시작: id=${id}" }
val store: StoreEntity = findOrThrow(id) val store: StoreEntity = findOrThrow(id)
@ -117,7 +117,7 @@ class StoreService(
} }
private fun getAuditInfo(store: StoreEntity): AuditingInfo { private fun getAuditInfo(store: StoreEntity): AuditingInfo {
log.info { "[getAuditInfo] 감사 정보 조회 시작: storeId=${store.id}" } log.debug { "[getAuditInfo] 감사 정보 조회 시작: storeId=${store.id}" }
val createdBy = adminService.findOperatorOrUnknown(store.createdBy) val createdBy = adminService.findOperatorOrUnknown(store.createdBy)
val updatedBy = adminService.findOperatorOrUnknown(store.updatedBy) val updatedBy = adminService.findOperatorOrUnknown(store.updatedBy)
@ -132,7 +132,7 @@ class StoreService(
} }
private fun findOrThrow(id: Long): StoreEntity { private fun findOrThrow(id: Long): StoreEntity {
log.info { "[findOrThrow] 매장 조회 시작: id=${id}" } log.debug { "[findOrThrow] 매장 조회 시작: id=${id}" }
return storeRepository.findActiveStoreById(id) return storeRepository.findActiveStoreById(id)
?.also { ?.also {

View File

@ -31,21 +31,21 @@ class StoreValidator(
private fun validateDuplicateNameExist(name: String) { private fun validateDuplicateNameExist(name: String) {
if (storeRepository.existsByName(name)) { if (storeRepository.existsByName(name)) {
log.info { "[StoreValidator.validateDuplicateNameExist] 이름 중복: name=${name}" } log.debug { "[StoreValidator.validateDuplicateNameExist] 이름 중복: name=${name}" }
throw StoreException(StoreErrorCode.STORE_NAME_DUPLICATED) throw StoreException(StoreErrorCode.STORE_NAME_DUPLICATED)
} }
} }
private fun validateDuplicateContactExist(contact: String) { private fun validateDuplicateContactExist(contact: String) {
if (storeRepository.existsByContact(contact)) { if (storeRepository.existsByContact(contact)) {
log.info { "[StoreValidator.validateDuplicateContact] 연락처 중복: contact=${contact}" } log.debug { "[StoreValidator.validateDuplicateContact] 연락처 중복: contact=${contact}" }
throw StoreException(StoreErrorCode.STORE_CONTACT_DUPLICATED) throw StoreException(StoreErrorCode.STORE_CONTACT_DUPLICATED)
} }
} }
private fun validateDuplicateAddressExist(address: String) { private fun validateDuplicateAddressExist(address: String) {
if (storeRepository.existsByAddress(address)) { if (storeRepository.existsByAddress(address)) {
log.info { "[StoreValidator.validateDuplicateAddress] 주소 중복: address=${address}" } log.debug { "[StoreValidator.validateDuplicateAddress] 주소 중복: address=${address}" }
throw StoreException(StoreErrorCode.STORE_ADDRESS_DUPLICATED) throw StoreException(StoreErrorCode.STORE_ADDRESS_DUPLICATED)
} }
} }

View File

@ -3,22 +3,18 @@ package com.sangdol.roomescape.theme.business
import com.sangdol.common.persistence.IDGenerator import com.sangdol.common.persistence.IDGenerator
import com.sangdol.roomescape.admin.business.AdminService import com.sangdol.roomescape.admin.business.AdminService
import com.sangdol.roomescape.common.types.AuditingInfo import com.sangdol.roomescape.common.types.AuditingInfo
import com.sangdol.roomescape.theme.dto.ThemeDetailResponse import com.sangdol.roomescape.theme.dto.*
import com.sangdol.roomescape.theme.dto.ThemeSummaryListResponse
import com.sangdol.roomescape.theme.dto.ThemeNameListResponse
import com.sangdol.roomescape.theme.dto.ThemeCreateRequest
import com.sangdol.roomescape.theme.dto.ThemeCreateResponse
import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.exception.ThemeException import com.sangdol.roomescape.theme.exception.ThemeException
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.mapper.toDetailResponse import com.sangdol.roomescape.theme.mapper.toDetailResponse
import com.sangdol.roomescape.theme.mapper.toSummaryListResponse
import com.sangdol.roomescape.theme.mapper.toEntity import com.sangdol.roomescape.theme.mapper.toEntity
import com.sangdol.roomescape.theme.mapper.toNameListResponse import com.sangdol.roomescape.theme.mapper.toNameListResponse
import com.sangdol.roomescape.theme.mapper.toSummaryListResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.cache.annotation.CacheEvict
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -34,7 +30,7 @@ class AdminThemeService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemeSummaries(): ThemeSummaryListResponse { fun findThemeSummaries(): ThemeSummaryListResponse {
log.info { "[findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" } log.debug { "[findAdminThemes] 관리자 페이지에서의 테마 목록 조회 시작" }
return themeRepository.findAll() return themeRepository.findAll()
.toSummaryListResponse() .toSummaryListResponse()
@ -43,7 +39,7 @@ class AdminThemeService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findThemeDetail(id: Long): ThemeDetailResponse { fun findThemeDetail(id: Long): ThemeDetailResponse {
log.info { "[findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" } log.debug { "[findAdminThemeDetail] 관리자 페이지에서의 테마 상세 정보 조회 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id) val theme: ThemeEntity = findOrThrow(id)
@ -57,7 +53,7 @@ class AdminThemeService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findActiveThemes(): ThemeNameListResponse { fun findActiveThemes(): ThemeNameListResponse {
log.info { "[findActiveThemes] open 상태인 모든 테마 조회 시작" } log.debug { "[findActiveThemes] open 상태인 모든 테마 조회 시작" }
return themeRepository.findActiveThemes() return themeRepository.findActiveThemes()
.toNameListResponse() .toNameListResponse()
@ -69,7 +65,7 @@ class AdminThemeService(
@Transactional @Transactional
fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse { fun createTheme(request: ThemeCreateRequest): ThemeCreateResponse {
log.info { "[createTheme] 테마 생성 시작: name=${request.name}" } log.debug { "[createTheme] 테마 생성 시작: name=${request.name}" }
themeValidator.validateCanCreate(request) themeValidator.validateCanCreate(request)
@ -81,10 +77,10 @@ class AdminThemeService(
} }
} }
@CacheEvict(cacheNames = ["theme-details"], key = "#id")
@Transactional @Transactional
fun deleteTheme(id: Long) { fun deleteTheme(id: Long) {
log.info { "[deleteTheme] 테마 삭제 시작: id=${id}" } log.debug { "[deleteTheme] 테마 삭제 시작: id=${id}" }
val theme: ThemeEntity = findOrThrow(id) val theme: ThemeEntity = findOrThrow(id)
@ -93,9 +89,10 @@ class AdminThemeService(
} }
} }
@CacheEvict(cacheNames = ["theme-details"], key = "#id")
@Transactional @Transactional
fun updateTheme(id: Long, request: ThemeUpdateRequest) { fun updateTheme(id: Long, request: ThemeUpdateRequest) {
log.info { "[updateTheme] 테마 수정 시작: id=${id}, request=${request}" } log.debug { "[updateTheme] 테마 수정 시작: id=${id}, request=${request}" }
if (request.isAllParamsNull()) { if (request.isAllParamsNull()) {
log.info { "[updateTheme] 테마 변경 사항 없음: id=${id}" } log.info { "[updateTheme] 테마 변경 사항 없음: id=${id}" }
@ -124,7 +121,7 @@ class AdminThemeService(
} }
private fun findOrThrow(id: Long): ThemeEntity { private fun findOrThrow(id: Long): ThemeEntity {
log.info { "[findOrThrow] 테마 조회 시작: id=$id" } log.debug { "[findOrThrow] 테마 조회 시작: id=$id" }
return themeRepository.findByIdOrNull(id) return themeRepository.findByIdOrNull(id)
?.also { log.info { "[findOrThrow] 테마 조회 완료: id=$id" } } ?.also { log.info { "[findOrThrow] 테마 조회 완료: id=$id" } }

View File

@ -10,6 +10,8 @@ import com.sangdol.roomescape.theme.mapper.toInfoResponse
import com.sangdol.roomescape.theme.mapper.toListResponse import com.sangdol.roomescape.theme.mapper.toListResponse
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.micrometer.core.instrument.MeterRegistry
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@ -21,13 +23,19 @@ private val log: KLogger = KotlinLogging.logger {}
@Service @Service
class ThemeService( class ThemeService(
private val themeRepository: ThemeRepository private val themeRepository: ThemeRepository,
meterRegistry: MeterRegistry
) { ) {
private val themeDetailQueryRequestCount = meterRegistry.counter("theme.detail.query.requested")
@Cacheable(cacheNames = ["theme-details"], key="#id")
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findInfoById(id: Long): ThemeInfoResponse { fun findInfoById(id: Long): ThemeInfoResponse {
log.info { "[findInfoById] 테마 조회 시작: id=$id" } log.debug { "[findInfoById] 테마 조회 시작: id=$id" }
val theme = themeRepository.findByIdOrNull(id) ?: run { val theme = themeRepository.findByIdOrNull(id)?.also {
themeDetailQueryRequestCount.increment()
} ?: run {
log.warn { "[updateTheme] 테마 조회 실패: id=$id" } log.warn { "[updateTheme] 테마 조회 실패: id=$id" }
throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND) throw ThemeException(ThemeErrorCode.THEME_NOT_FOUND)
} }
@ -38,7 +46,7 @@ class ThemeService(
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findMostReservedThemeLastWeek(count: Int): ThemeInfoListResponse { fun findMostReservedThemeLastWeek(count: Int): ThemeInfoListResponse {
log.info { "[findMostReservedThemeLastWeek] 인기 테마 조회 시작: count=$count" } log.debug { "[findMostReservedThemeLastWeek] 인기 테마 조회 시작: count=$count" }
val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(KoreaDate.today()) val previousWeekSunday = DateUtils.getSundayOfPreviousWeek(KoreaDate.today())
val previousWeekSaturday = previousWeekSunday.plusDays(6) val previousWeekSaturday = previousWeekSunday.plusDays(6)

View File

@ -29,7 +29,7 @@ class UserService(
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findCredentialsByAccount(email: String): UserLoginCredentials { fun findCredentialsByAccount(email: String): UserLoginCredentials {
log.info { "[findCredentialsByAccount] 회원 조회 시작: email=${email}" } log.debug { "[findCredentialsByAccount] 회원 조회 시작: email=${email}" }
return userRepository.findByEmail(email) return userRepository.findByEmail(email)
?.let { ?.let {
@ -37,14 +37,13 @@ class UserService(
it.toCredentials() it.toCredentials()
} }
?: run { ?: run {
log.info { "[findCredentialsByAccount] 회원 조회 실패" }
throw UserException(UserErrorCode.USER_NOT_FOUND) throw UserException(UserErrorCode.USER_NOT_FOUND)
} }
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun findContactById(id: Long): UserContactResponse { fun findContactById(id: Long): UserContactResponse {
log.info { "[findContactById] 회원 연락 정보 조회 시작: id=${id}" } log.debug { "[findContactById] 회원 연락 정보 조회 시작: id=${id}" }
val user = findOrThrow(id) val user = findOrThrow(id)
@ -56,7 +55,7 @@ class UserService(
@Transactional @Transactional
fun signup(request: UserCreateRequest): UserCreateResponse { fun signup(request: UserCreateRequest): UserCreateResponse {
log.info { "[signup] 회원가입 시작: request:$request" } log.debug { "[signup] 회원가입 시작: request:$request" }
userValidator.validateCanSignup(request.email, request.phone) userValidator.validateCanSignup(request.email, request.phone)

View File

@ -15,14 +15,14 @@ class UserValidator(
) { ) {
fun validateCanSignup(email: String, phone: String) { fun validateCanSignup(email: String, phone: String) {
log.info { "[UserValidator.validateCanSignup] 회원가입 가능 여부 검증 시작: email:$email / phone:$phone" } log.debug { "[UserValidator.validateCanSignup] 회원가입 가능 여부 검증 시작: email:$email / phone:$phone" }
if (userRepository.existsByEmail(email)) { if (userRepository.existsByEmail(email)) {
log.info { "[UserValidator.validateCanSignup] 중복된 이메일 입력으로 인한 실패: email:$email" } log.debug { "[UserValidator.validateCanSignup] 중복된 이메일 입력으로 인한 실패: email:$email" }
throw UserException(UserErrorCode.EMAIL_ALREADY_EXISTS) throw UserException(UserErrorCode.EMAIL_ALREADY_EXISTS)
} }
if (userRepository.existsByPhone(phone)) { if (userRepository.existsByPhone(phone)) {
log.info { "[UserValidator.validateCanSignup] 중복된 휴대폰 번호 입력으로 인한 실패: phone:$phone" } log.debug { "[UserValidator.validateCanSignup] 중복된 휴대폰 번호 입력으로 인한 실패: phone:$phone" }
throw UserException(UserErrorCode.PHONE_ALREADY_EXISTS) throw UserException(UserErrorCode.PHONE_ALREADY_EXISTS)
} }
} }

View File

@ -16,6 +16,9 @@ spring:
jdbc: jdbc:
batch_size: ${JDBC_BATCH_SIZE:100} batch_size: ${JDBC_BATCH_SIZE:100}
order_inserts: true order_inserts: true
cache:
type: caffeine
cache-names: ${CACHE_NAMES:theme-details}
management: management:
endpoints: endpoints:

View File

@ -1,4 +1,4 @@
package com.sangdol.roomescape.auth.business package com.sangdol.roomescape.auth
import com.sangdol.roomescape.auth.business.domain.PrincipalType import com.sangdol.roomescape.auth.business.domain.PrincipalType
import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryEntity import com.sangdol.roomescape.auth.infrastructure.persistence.LoginHistoryEntity

View File

@ -1,12 +1,13 @@
package com.sangdol.roomescape.reservation.business.event package com.sangdol.roomescape.reservation
import com.sangdol.roomescape.reservation.business.event.ReservationConfirmEvent
import com.sangdol.roomescape.reservation.business.event.ReservationEventListener
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationRepository
import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus import com.sangdol.roomescape.reservation.infrastructure.persistence.ReservationStatus
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleRepository
import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus import com.sangdol.roomescape.schedule.infrastructure.persistence.ScheduleStatus
import com.sangdol.roomescape.supports.FunSpecSpringbootTest import com.sangdol.roomescape.supports.FunSpecSpringbootTest
import io.kotest.assertions.assertSoftly import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull

View File

@ -6,21 +6,28 @@ import com.sangdol.roomescape.admin.infrastructure.persistence.AdminType
import com.sangdol.roomescape.auth.exception.AuthErrorCode import com.sangdol.roomescape.auth.exception.AuthErrorCode
import com.sangdol.roomescape.supports.* import com.sangdol.roomescape.supports.*
import com.sangdol.roomescape.supports.ThemeFixture.createRequest import com.sangdol.roomescape.supports.ThemeFixture.createRequest
import com.sangdol.roomescape.theme.business.AdminThemeService
import com.sangdol.roomescape.theme.business.MIN_DURATION import com.sangdol.roomescape.theme.business.MIN_DURATION
import com.sangdol.roomescape.theme.business.MIN_PARTICIPANTS import com.sangdol.roomescape.theme.business.MIN_PARTICIPANTS
import com.sangdol.roomescape.theme.business.MIN_PRICE import com.sangdol.roomescape.theme.business.MIN_PRICE
import com.sangdol.roomescape.theme.business.ThemeService
import com.sangdol.roomescape.theme.dto.ThemeInfoResponse
import com.sangdol.roomescape.theme.exception.ThemeErrorCode import com.sangdol.roomescape.theme.exception.ThemeErrorCode
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest import com.sangdol.roomescape.theme.dto.ThemeUpdateRequest
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.springframework.cache.CacheManager
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
class AdminThemeApiTest( class AdminThemeApiTest(
private val themeRepository: ThemeRepository private val themeRepository: ThemeRepository,
private val themeService: ThemeService,
private val cacheManager: CacheManager
) : FunSpecSpringbootTest() { ) : FunSpecSpringbootTest() {
init { init {
@ -482,14 +489,19 @@ class AdminThemeApiTest(
} }
} }
test("정상 삭제") { test("정상 삭제 및 캐시 제거 확인") {
val token = testAuthUtil.defaultHqAdminLogin().second val token = testAuthUtil.defaultHqAdminLogin().second
val createdTheme = initialize("테스트를 위한 테마 생성") { val createdTheme = initialize("테스트를 위한 테마 생성") {
dummyInitializer.createTheme() dummyInitializer.createTheme()
} }
initialize("테마 캐시 추가") {
themeService.findInfoById(createdTheme.id)
cacheManager.getCache("theme-details")?.get(createdTheme.id, ThemeInfoResponse::class.java).shouldNotBeNull()
}
runTest( runTest(
token = testAuthUtil.defaultHqAdminLogin().second, token = token,
on = { on = {
delete("/admin/themes/${createdTheme.id}") delete("/admin/themes/${createdTheme.id}")
}, },
@ -498,6 +510,7 @@ class AdminThemeApiTest(
} }
).also { ).also {
themeRepository.findByIdOrNull(createdTheme.id) shouldBe null themeRepository.findByIdOrNull(createdTheme.id) shouldBe null
cacheManager.getCache("theme-details")?.get(createdTheme.id, ThemeInfoResponse::class.java) shouldBe null
} }
} }
@ -566,7 +579,7 @@ class AdminThemeApiTest(
val updateRequest = ThemeUpdateRequest(name = "modified") val updateRequest = ThemeUpdateRequest(name = "modified")
test("정상 수정 및 감사 정보 변경 확인") { test("정상 수정 및 감사 정보 & 캐시 변경 확인") {
val createdThemeId: Long = initialize("테스트를 위한 관리자1의 테마 생성") { val createdThemeId: Long = initialize("테스트를 위한 관리자1의 테마 생성") {
runTest( runTest(
token = testAuthUtil.defaultHqAdminLogin().second, token = testAuthUtil.defaultHqAdminLogin().second,
@ -582,6 +595,11 @@ class AdminThemeApiTest(
).extract().path("data.id") ).extract().path("data.id")
} }
initialize("테마 캐시 추가") {
themeService.findInfoById(createdThemeId)
cacheManager.getCache("theme-details")?.get(createdThemeId, ThemeInfoResponse::class.java).shouldNotBeNull()
}
val (otherAdmin, otherAdminToken) = initialize("감사 정보 변경 확인을 위한 관리자2 로그인") { val (otherAdmin, otherAdminToken) = initialize("감사 정보 변경 확인을 위한 관리자2 로그인") {
testAuthUtil.adminLogin( testAuthUtil.adminLogin(
AdminFixture.createHqAdmin(permissionLevel = AdminPermissionLevel.WRITABLE) AdminFixture.createHqAdmin(permissionLevel = AdminPermissionLevel.WRITABLE)
@ -604,6 +622,12 @@ class AdminThemeApiTest(
updatedTheme.name shouldBe updateRequest.name updatedTheme.name shouldBe updateRequest.name
updatedTheme.updatedBy shouldBe otherAdmin.id updatedTheme.updatedBy shouldBe otherAdmin.id
// 캐시 제거 확인
assertSoftly(cacheManager.getCache("theme-details")?.get(createdThemeId, ThemeInfoResponse::class.java)) {
this shouldBe null
}
} }
} }

View File

@ -10,23 +10,32 @@ import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import com.sangdol.roomescape.theme.mapper.toEntity import com.sangdol.roomescape.theme.mapper.toEntity
import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity import com.sangdol.roomescape.user.infrastructure.persistence.UserEntity
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.collections.shouldContainInOrder import io.kotest.matchers.collections.shouldContainInOrder
import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.comparables.shouldBeLessThan import io.kotest.matchers.comparables.shouldBeLessThan
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.equalTo
import org.springframework.cache.CacheManager
import org.springframework.http.HttpMethod import org.springframework.http.HttpMethod
import java.time.LocalDate import java.time.LocalDate
class ThemeApiTest( class ThemeApiTest(
private val themeRepository: ThemeRepository private val themeRepository: ThemeRepository,
private val cacheManager: CacheManager
) : FunSpecSpringbootTest() { ) : FunSpecSpringbootTest() {
init { init {
context("ID로 테마 정보를 조회한다.") { context("ID로 테마 정보를 조회한다.") {
test("정상 응답") { test("정상 응답 및 캐시 저장 확인") {
val createdTheme: ThemeEntity = initialize("조회를 위한 테마 생성") { val createdTheme: ThemeEntity = initialize("조회를 위한 테마 생성") {
dummyInitializer.createTheme() dummyInitializer.createTheme()
} }
cacheManager.getCache("theme-details")?.get(createdTheme.id, ThemeInfoResponse::class.java).also {
it shouldBe null
}
runTest( runTest(
on = { on = {
get("/themes/${createdTheme.id}") get("/themes/${createdTheme.id}")
@ -43,6 +52,15 @@ class ThemeApiTest(
) )
} }
) )
assertSoftly(cacheManager.getCache("theme-details")) {
this.shouldNotBeNull()
val themeFromCache = this.get(createdTheme.id, ThemeInfoResponse::class.java)
themeFromCache.shouldNotBeNull()
themeFromCache.id shouldBe createdTheme.id
}
} }
test("테마가 없으면 실패한다.") { test("테마가 없으면 실패한다.") {

View File

@ -0,0 +1,68 @@
package com.sangdol.roomescape.theme
import com.ninjasquad.springmockk.MockkBean
import com.sangdol.roomescape.supports.FunSpecSpringbootTest
import com.sangdol.roomescape.supports.IDGenerator
import com.sangdol.roomescape.supports.initialize
import com.sangdol.roomescape.theme.business.ThemeService
import com.sangdol.roomescape.theme.infrastructure.persistence.Difficulty
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeEntity
import com.sangdol.roomescape.theme.infrastructure.persistence.ThemeRepository
import io.mockk.every
import io.mockk.verify
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import org.springframework.data.repository.findByIdOrNull
import java.util.concurrent.CountDownLatch
class ThemeConcurrencyTest(
private val themeService: ThemeService,
@MockkBean private val themeRepository: ThemeRepository,
) : FunSpecSpringbootTest() {
init {
test("동일한 테마에 대한 반복 조회 요청시, DB 요청은 1회만 발생한다.") {
val entity = ThemeEntity(
id = IDGenerator.create(),
name = "테스트입니다.",
description = "테스트에요!",
thumbnailUrl = "http://localhost:8080/hello",
difficulty = Difficulty.VERY_EASY,
price = 10000,
minParticipants = 3,
maxParticipants = 5,
availableMinutes = 90,
expectedMinutesFrom = 70,
expectedMinutesTo = 80,
isActive = true
)
every {
themeRepository.findByIdOrNull(entity.id)
} returns entity
initialize("캐시 등록") {
themeService.findInfoById(entity.id)
}
val requestCount = 64
withContext(Dispatchers.IO) {
val latch = CountDownLatch(requestCount)
(1..requestCount).map {
async {
latch.countDown()
latch.await()
themeService.findInfoById(entity.id)
}
}
}
verify(exactly = 1) {
themeRepository.findByIdOrNull(entity.id)
}
}
}
}

View File

@ -18,6 +18,9 @@ spring:
init: init:
mode: always mode: always
schema-locations: classpath:schema/schema-mysql.sql schema-locations: classpath:schema/schema-mysql.sql
cache:
type: caffeine
cache-names: ${CACHE_NAMES:theme-details}
security: security:
jwt: jwt: