Skip to content
Merged
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
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3'
services:
rabbitmq:
image: rabbitmq:3-management
container_name: rabbitmq
ports:
- "5672:5672"
- "15672:15672"
- "15692:15692"
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
2 changes: 2 additions & 0 deletions src/main/java/com/profect/tickle/TickleApplication.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.profect.tickle;

import org.springframework.amqp.rabbit.annotation.EnableRabbit;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableRabbit
@SpringBootApplication
@EnableScheduling
@ConfigurationPropertiesScan(basePackages = "com.profect.tickle")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.profect.tickle.domain.chat.listener;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.profect.tickle.domain.chat.dto.request.ChatMessageSendRequestDto;
import com.profect.tickle.domain.chat.entity.ChatMessageType;
import com.profect.tickle.domain.chat.entity.ChatRoom;
import com.profect.tickle.domain.chat.repository.ChatRoomRepository;
import com.profect.tickle.domain.member.entity.Member;
import com.profect.tickle.domain.member.repository.MemberRepository;
import com.profect.tickle.global.config.RabbitMQConfig;
import com.profect.tickle.global.config.RabbitMQChatConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
Expand All @@ -29,7 +27,7 @@ public class RabbitMQMessageListener {
/**
* 채팅 메시지 큐 리스너
*/
@RabbitListener(queues = RabbitMQConfig.CHAT_MESSAGE_QUEUE)
@RabbitListener(queues = RabbitMQChatConfig.CHAT_MESSAGE_QUEUE)
public void handleChatMessage(Map<String, Object> messageMap) {
try {
log.debug("RabbitMQ에서 채팅 메시지 수신: {}", messageMap);
Expand Down Expand Up @@ -63,7 +61,7 @@ public void handleChatMessage(Map<String, Object> messageMap) {
/**
* 채팅 알림 큐 리스너
*/
@RabbitListener(queues = RabbitMQConfig.CHAT_NOTIFICATION_QUEUE)
@RabbitListener(queues = RabbitMQChatConfig.CHAT_NOTIFICATION_QUEUE)
public void handleChatNotification(Map<String, Object> notificationMap) {
try {
log.debug("RabbitMQ에서 채팅 알림 수신: {}", notificationMap);
Expand All @@ -87,7 +85,7 @@ public void handleChatNotification(Map<String, Object> notificationMap) {
/**
* 채팅 파일 큐 리스너
*/
@RabbitListener(queues = RabbitMQConfig.CHAT_FILE_QUEUE)
@RabbitListener(queues = RabbitMQChatConfig.CHAT_FILE_QUEUE)
public void handleChatFile(Map<String, Object> fileMap) {
try {
log.debug("RabbitMQ에서 채팅 파일 수신: {}", fileMap);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.profect.tickle.domain.chat.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.profect.tickle.global.config.RabbitMQConfig;
import com.profect.tickle.global.config.RabbitMQChatConfig;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
Expand Down Expand Up @@ -29,8 +29,8 @@ public void sendMessage(String message) {

// 채팅 큐로 메시지 전송
rabbitTemplate.convertAndSend(
RabbitMQConfig.CHAT_EXCHANGE,
RabbitMQConfig.MESSAGE_ROUTING_KEY,
RabbitMQChatConfig.CHAT_EXCHANGE,
RabbitMQChatConfig.MESSAGE_ROUTING_KEY,
messageMap
);

Expand All @@ -53,8 +53,8 @@ public void sendNotification(String notification) {

// 알림 큐로 메시지 전송
rabbitTemplate.convertAndSend(
RabbitMQConfig.CHAT_EXCHANGE,
RabbitMQConfig.NOTIFICATION_ROUTING_KEY,
RabbitMQChatConfig.CHAT_EXCHANGE,
RabbitMQChatConfig.NOTIFICATION_ROUTING_KEY,
notificationMap
);

Expand All @@ -77,8 +77,8 @@ public void sendFileMessage(String fileMessage) {

// 파일 큐로 메시지 전송
rabbitTemplate.convertAndSend(
RabbitMQConfig.CHAT_EXCHANGE,
RabbitMQConfig.FILE_ROUTING_KEY,
RabbitMQChatConfig.CHAT_EXCHANGE,
RabbitMQChatConfig.FILE_ROUTING_KEY,
fileMap
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.profect.tickle.domain.event.controller;

import com.profect.tickle.domain.event.dto.response.*;
import com.profect.tickle.domain.event.dto.request.CouponCreateRequestDto;
import com.profect.tickle.domain.event.dto.request.TicketEventCreateRequestDto;
import com.profect.tickle.domain.event.dto.response.*;
import com.profect.tickle.domain.event.entity.EventType;
import com.profect.tickle.domain.event.service.event.CouponService;
import com.profect.tickle.domain.event.service.event.EventService;
import com.profect.tickle.domain.event.service.application.CouponService;
import com.profect.tickle.domain.event.service.application.EventService;
import com.profect.tickle.domain.event.service.rabbitmq.dto.ApplyResponseDto;
import com.profect.tickle.global.paging.PagingResponse;
import com.profect.tickle.global.response.ResultCode;
import com.profect.tickle.global.response.ResultResponse;
Expand Down Expand Up @@ -68,9 +69,10 @@ public ResultResponse<TicketEventResponseDto> createTicketEvent(@Valid @RequestB
content = @Content(schema = @Schema(implementation = TicketApplyResponseDto.class))),
@ApiResponse(responseCode = "400", description = "포인트 부족, 중복 응모 등 예외 발생")})
@PostMapping("/ticket/{eventId}")
public ResultResponse<TicketApplyResponseDto> applyTicketEvent(@PathVariable Long eventId) {
TicketApplyResponseDto dto = eventService.applyTicketEvent(eventId);
return ResultResponse.of(ResultCode.EVENT_APPLY_SUCCESS, dto);
public ResultResponse<ApplyResponseDto> applyTicketEvent(@PathVariable Long eventId) {
ApplyResponseDto response = eventService.applyTicketEvent(eventId);

return ResultResponse.of(ResultCode.EVENT_APPLY_SUCCESS, response);
}

@Operation(summary = "쿠폰 이벤트 응모", description = "유저가 쿠폰 이벤트에 응모하여 쿠폰을 발급받습니다.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@
public record TicketApplyResponseDto(
Long eventId,
Long memberId,
String message
boolean message
) {
public static TicketApplyResponseDto from(Long eventId, Long memberId) {
return new TicketApplyResponseDto(
eventId,
memberId,
"성공적으로 응모되었습니다. 마이페이지에서 결과를 확인하세요."
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.profect.tickle.domain.event.mapper;

import com.profect.tickle.domain.event.dto.response.*;
import com.profect.tickle.domain.event.entity.Event;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

Expand All @@ -22,4 +23,6 @@ public interface EventMapper {
// 스케쥴러 update mapper 추가
int markEventsAsOngoing(); // 변경된 행 수 리턴
int markEventsAsFinished();

List<Event> findEventsToMarkAsOngoing();
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.profect.tickle.domain.event.repository;

import com.profect.tickle.domain.event.entity.Event;
import com.profect.tickle.global.status.Status;
import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
Expand All @@ -15,12 +16,27 @@
@Repository
public interface EventRepository extends JpaRepository<Event, Long> {

@Query("""
SELECT e FROM Event e
JOIN e.seat s
JOIN s.performance p
WHERE e.status.id = :scheduledStatus
AND p.startDate <= CURRENT_TIMESTAMP
AND p.endDate > CURRENT_TIMESTAMP
""")
List<Event> findEventsToStart(@Param("scheduledStatus") Long scheduledStatus);

interface AccrueRow {
Integer getEventAccrued();
Long getStatusId();
}

@Modifying
@Query("UPDATE Event e SET e.accrued = :accrued, e.status = :status WHERE e.id = :eventId")
void updateAccruedAndStatus(@Param("eventId") Long eventId,
@Param("accrued") int accrued,
@Param("status") Status status);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
UPDATE event
Expand Down Expand Up @@ -69,16 +85,8 @@ int incrementAccrued(@Param("eventId") Long eventId,
Long findSeatIdByEventId(@Param("eventId") Long eventId);*/


// Postgres면 증가 후 값을 곧바로 받는 버전 권장 (더 깔끔)
@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
update event
set event_accrued = event_accrued + :delta
where event_id = :eventId and status_id = :inProgress
returning event_accrued
""", nativeQuery = true)
List<Integer> incrementAccruedReturning(@Param("eventId") Long eventId,
@Param("delta") int delta,
@Param("inProgress") Long inProgress);
@Modifying(clearAutomatically = true)
@Query("UPDATE Event e SET e.status = :status WHERE e.id = :eventId")
int updateEventStatus(@Param("eventId") Long eventId, @Param("status") Status status);

}
Original file line number Diff line number Diff line change
@@ -1,22 +1,38 @@
package com.profect.tickle.domain.event.scheduler;

import com.profect.tickle.domain.event.entity.Event;
import com.profect.tickle.domain.event.mapper.EventMapper;
import com.profect.tickle.domain.event.repository.EventRepository;
import com.profect.tickle.global.redis.service.RedisInitialize;
import com.profect.tickle.global.status.StatusIds;
import jakarta.annotation.PostConstruct;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service
@RequiredArgsConstructor
public class EventStatusScheduler {

private final EventMapper eventMapper;
private final EventRepository eventRepository;
private final RedisInitialize redisInitialize;

//@Scheduled(cron = "0 * * * * *") // 매 분 0초
@Scheduled(cron = "0 * * * * *")
@Transactional
public void syncEventStatuses() {
int toOngoing = eventMapper.markEventsAsOngoing();
int toFinished = eventMapper.markEventsAsFinished();
// 필요하면 로그
// log.info("event status updated: toOngoing={}, toFinished={}", toOngoing, toFinished);
List<Event> eventsToStart = eventRepository.findEventsToStart(StatusIds.Event.SCHEDULED);

log.info("toOngoing={}개 이벤트 Redis 초기화 시작", eventsToStart.size());

for (Event event : eventsToStart) {
if (event == null) continue;
log.info("Redis 초기화 대상 eventId={}, goalPrice={}", event.getId(), event.getGoalPrice());
redisInitialize.initializeRedisEvent(event);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.profect.tickle.domain.event.service.event;
package com.profect.tickle.domain.event.service.application;

import com.profect.tickle.domain.event.dto.response.CouponListResponseDto;
import com.profect.tickle.domain.event.dto.response.CouponResponseDto;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package com.profect.tickle.domain.event.service.event;
package com.profect.tickle.domain.event.service.application;

import com.profect.tickle.domain.event.dto.EventDecision;
import com.profect.tickle.domain.event.entity.Event;
import com.profect.tickle.domain.event.repository.EventRepository;
import com.profect.tickle.domain.member.repository.MemberRepository;
import com.profect.tickle.domain.reservation.repository.SeatRepository;
import com.profect.tickle.domain.reservation.service.ReservationService;
import com.profect.tickle.global.exception.BusinessException;
import com.profect.tickle.global.exception.ErrorCode;
Expand All @@ -20,49 +18,61 @@
public class EventCoreLockService {

private final EventRepository eventRepository;
private final MemberRepository memberRepository;
private final SeatRepository seatRepository;
private final StatusProvider statusProvider;
private final ReservationService reservationService;
private final StatusProvider statusProvider;

//TODO: 임계영역에 대해서 동시성을 보장하면 원하는 결과가 나올겁니다?
//TODO: 영한님의 고급 1편을 보세요. 자바 코드에 대한 동시성을 찾아보세요

@Transactional(propagation = Propagation.REQUIRES_NEW)
public EventDecision applyCore(Long eventId, Long memberId) {
Event event = eventRepository.findById(eventId)
public EventDecision applyCore(Long memberId, Long eventId) {

long startTotal = System.nanoTime();
Event event;
long t1, t2, t3;

// (1) Event 조회
long start1 = System.nanoTime();
event = eventRepository.findById(eventId)
.orElseThrow(() -> new BusinessException(ErrorCode.EVENT_NOT_FOUND));
Long statusId = event.getStatus().getId();
t1 = (System.nanoTime() - start1) / 1_000_000; // ms 단위

Long statusId = event.getStatus().getId();
if (!StatusIds.Event.IN_PROGRESS.equals(statusId)) {
throw new BusinessException(ErrorCode.EVENT_NOT_IN_PROGRESS);
}

// 2) 포인트 차감
// (2) 누적 및 상태 변경 쿼리
long start2 = System.nanoTime();
short delta = event.getPerPrice();
var deducted = memberRepository.tryDeductPointReturning(memberId, delta);
if (deducted.isEmpty()) throw new BusinessException(ErrorCode.INSUFFICIENT_POINT);

// 3) 누적 + 종료 전환(원자)
var rows = eventRepository.accrueAndMaybeComplete(
eventId, delta,
StatusIds.Event.IN_PROGRESS,
StatusIds.Event.COMPLETED);
t2 = (System.nanoTime() - start2) / 1_000_000;

if (rows.isEmpty()) throw new BusinessException(ErrorCode.EVENT_ALREADY_COMPLETED);

var r = rows.get(0);
boolean completed = r.getStatusId().equals(StatusIds.Event.COMPLETED);

// (3) 좌석 배정
Long seatId = null;
boolean winner = false;

long start3 = System.nanoTime();
if (completed) {
seatId = reservationService.assignSeatForWinner(eventId, memberId);
System.out.println("seatId = " + seatId);
winner = (seatId != null); // ★ 좌석 배정 성공 시에만 winner
winner = (seatId != null);
}
t3 = (System.nanoTime() - start3) / 1_000_000;

long total = (System.nanoTime() - startTotal) / 1_000_000;

return new EventDecision(eventId, memberId, delta, winner, r.getEventAccrued(), seatId);
}

@Transactional
public void completeEvent(Long eventId) {
eventRepository.updateEventStatus(eventId, statusProvider.provide(StatusIds.Event.COMPLETED));
}
}
Loading