From 49cb5e21274531109b2dfe11b374251aa4de83ec Mon Sep 17 00:00:00 2001 From: hyejungg Date: Sat, 16 Oct 2021 18:26:00 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[feat:#18]=20MySQL=20user-level=20=EB=B6=84?= =?UTF-8?q?=EC=82=B0=EB=9D=BD=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/ticket/service/LockService.java | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 ticket/src/main/java/comento/backend/ticket/service/LockService.java diff --git a/ticket/src/main/java/comento/backend/ticket/service/LockService.java b/ticket/src/main/java/comento/backend/ticket/service/LockService.java new file mode 100644 index 0000000..9519512 --- /dev/null +++ b/ticket/src/main/java/comento/backend/ticket/service/LockService.java @@ -0,0 +1,84 @@ +package comento.backend.ticket.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.function.Supplier; + +@Slf4j +@Service +public class LockService { + private static final String GET_LOCK = "SELECT GET_LOCK(?, ?)"; + private static final String RELEASE_LOCK = "SELECT RELEASE_LOCK(?)"; + + private final DataSource dataSource; + + public LockService(DataSource dataSource) { + this.dataSource = dataSource; + } + + public T execute(String lockName, int timeoutSeconds, Supplier supplier) { + //@brief : Lock을 획득한 connection으로 Lock 획득, 반납 작업 필요 + try (Connection connection = dataSource.getConnection()) { + try { + log.info("lock 획득"); + getLock(connection, lockName, timeoutSeconds); + return supplier.get(); + } finally { + log.info("lock 반납"); + releaseLock(connection, lockName); + } + } catch (SQLException e) { + log.info("lock 획득 실패"); + throw new IllegalStateException(e); + } + } + + //@see : 스레드 당 1개의 lock을 획득하도록 + public void execute(String lockName, int timeoutSeconds, Runnable runnable) { + this.execute(lockName, timeoutSeconds, () -> { + runnable.run(); + return null; + }); + } + + private void getLock(Connection connection, String lockName, int timeoutSeconds) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(GET_LOCK)) { + preparedStatement.setString(1, lockName); + preparedStatement.setInt(2, timeoutSeconds); + + try (ResultSet resultSet = preparedStatement.executeQuery()) { + if (!resultSet.next()) { + throw new IllegalStateException("[LockFail] RESULT EMPTY"); + } + + int result = resultSet.getInt(1); + if (result != 1) { + throw new IllegalStateException("[LockFail] result : " + result + ", lockName : " + lockName + ", timeoutSeconds : " + timeoutSeconds); + } + } + } + } + + private void releaseLock(Connection connection, String lockName) throws SQLException { + try (PreparedStatement preparedStatement = connection.prepareStatement(RELEASE_LOCK)) { + preparedStatement.setString(1, lockName); + + try (ResultSet resultSet = preparedStatement.executeQuery()) { + if (!resultSet.next()) { + throw new IllegalStateException("[UnlockFail] RESULT EMPTY"); + } + + int result = resultSet.getInt(1); + if (result != 1) { + throw new IllegalStateException("[UnlockFail] result : " + result + ", lockName : " + lockName); + } + } + } + } +} From 133761fe1ca18f096041fe6dade583e21190c861 Mon Sep 17 00:00:00 2001 From: hyejungg Date: Sat, 16 Oct 2021 18:29:44 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[refator:]=20=EC=BD=94=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. booking 여부에 따른 log 값 insert의 경우 booking 결과에 따라 controller 에서 진행하는게 올바른 방법 2. read하는 경우, 무조건 read만 하도록 트랜잭션 어노테이션 추가 3. MySQL user-level의 분산락에 트랜잭션 코드 추가 --- .../backend/ticket/config/ErrorCode.java | 3 +- .../ticket/config/GlobalExceptionHandler.java | 12 +++- .../customException/NoDataException.java | 10 +++ .../ticket/controller/BookingController.java | 35 ++++++++-- .../ticket/service/BookingService.java | 70 ++++++++++--------- .../ticket/service/PerformanceService.java | 3 + .../backend/ticket/service/SeatService.java | 20 ++---- .../backend/ticket/service/UserService.java | 2 + 8 files changed, 101 insertions(+), 54 deletions(-) create mode 100644 ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java diff --git a/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java b/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java index 8769d25..6bad193 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java +++ b/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java @@ -13,7 +13,8 @@ public enum ErrorCode { INVALID_VALUE(400, HttpStatus.BAD_REQUEST, "BAD REQUEST", "잘못된 요청입니다. 다시 요청해주세요."), NO_USER(401, HttpStatus.UNAUTHORIZED,"UNAUTHORIZED", "등록되지 않은 사용자입니다"), - NO_DATA(404, HttpStatus.NOT_FOUND, "NOT FOUND ERROR", "요청할 수 없는 리소스입니다."), + NOT_FOUND(404, HttpStatus.NOT_FOUND, "NOT FOUND ERROR", "요청할 수 없는 리소스입니다."), + NO_DATA(404, HttpStatus.NOT_FOUND, "NO DATA", "데이터가 존재하지 않습니다"), INVALID_USER(409, HttpStatus.CONFLICT, "CONFLICT", "이미 존재하는 사용자입니다."), INVALID_SEAT(409, HttpStatus.CONFLICT, "CONFLICT", "이미 예약된 좌석입니다."), SERVER_ERROR(500, HttpStatus.INTERNAL_SERVER_ERROR, "INTERVAL_SERVER ERROR", "서버 에러입니다."); diff --git a/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java b/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java index b02f14d..7fcb3c8 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java +++ b/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java @@ -3,10 +3,10 @@ import com.fasterxml.jackson.core.JsonParseException; import comento.backend.ticket.config.customException.DuplicatedException; import comento.backend.ticket.config.customException.NoAuthException; +import comento.backend.ticket.config.customException.NoDataException; import comento.backend.ticket.config.customException.NotFoundDataException; import javassist.NotFoundException; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -47,9 +47,17 @@ public static ResponseEntity noAuthExceptionHandler(NoAuthException e){ errorCode.getHttpStatus()); } + @ExceptionHandler(NoDataException.class) + public static ResponseEntity noDataExceptionHandler(Throwable t){ + errorCode = ErrorCode.NO_DATA; + log.error(errorCode.getStatus() + " " + errorCode.getMessage(), t); + return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), + errorCode.getHttpStatus()); + } + @ExceptionHandler({NoSuchElementException.class, NotFoundException.class, NotFoundDataException.class}) public static ResponseEntity notFoundExceptionHandler(Throwable t){ - errorCode = ErrorCode.NO_DATA; + errorCode = ErrorCode.NOT_FOUND; log.error(errorCode.getStatus() + " " + errorCode.getMessage(), t); return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), errorCode.getHttpStatus()); diff --git a/ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java b/ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java new file mode 100644 index 0000000..96a252c --- /dev/null +++ b/ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java @@ -0,0 +1,10 @@ +package comento.backend.ticket.config.customException; + +public class NoDataException extends RuntimeException{ + public NoDataException() { + } + + public NoDataException(String message) { + super(message); + } +} diff --git a/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java b/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java index 6ad1e89..591c8de 100644 --- a/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java +++ b/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java @@ -2,10 +2,14 @@ import comento.backend.ticket.config.SuccessCode; import comento.backend.ticket.config.SuccessResponse; +import comento.backend.ticket.config.customException.DuplicatedException; +import comento.backend.ticket.domain.Performance; +import comento.backend.ticket.domain.Seat; +import comento.backend.ticket.domain.User; import comento.backend.ticket.dto.BookingDto; import comento.backend.ticket.dto.BookingResponse; import comento.backend.ticket.dto.BookingResponseCreated; -import comento.backend.ticket.service.BookingService; +import comento.backend.ticket.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -18,18 +22,39 @@ @RequestMapping("/api/performance/booking") public class BookingController { private final BookingService bookingService; + private final BookingHistoryService bookingHistoryService; + private final UserService userService; + private final PerformanceService performanceService; + private final SeatService seatService; + private SuccessCode successCode; + private BookingResponseCreated result; @Autowired - public BookingController(BookingService bookingService) { + public BookingController(BookingService bookingService, BookingHistoryService bookingHistoryService, UserService userService, PerformanceService performanceService, SeatService seatService) { this.bookingService = bookingService; + this.bookingHistoryService = bookingHistoryService; + this.userService = userService; + this.performanceService = performanceService; + this.seatService = seatService; } @PostMapping("") public ResponseEntity addBooking(@Valid @RequestBody BookingDto reqBooking){ - bookingService.saveBookging(reqBooking); - BookingResponseCreated result = new BookingResponseCreated(reqBooking.getSeatType(), reqBooking.getSeatNumber()); - successCode = SuccessCode.CREATED; + final User user = userService.getUser(reqBooking.getEmail()); + final Performance performance = performanceService.getPerformance(reqBooking.getId(), reqBooking.getTitle()); + final Seat seat = seatService.getIsBooking(user, performance, reqBooking.getSeatType(), reqBooking.getSeatNumber()); //false라면 예약 가능 + + if(seat.isBooking()){ //true면 이미 예약된 상태 + bookingHistoryService.saveBookingFailLog(user, performance, seat); + throw new DuplicatedException("SeatService"); + }else{ + bookingService.saveBooking(user, performance, seat, reqBooking); + bookingHistoryService.saveBookingSucessLog(user, performance, seat); + result = new BookingResponseCreated(reqBooking.getSeatType(), reqBooking.getSeatNumber()); + successCode = SuccessCode.CREATED; + } + return new ResponseEntity(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), result), HttpStatus.CREATED); } diff --git a/ticket/src/main/java/comento/backend/ticket/service/BookingService.java b/ticket/src/main/java/comento/backend/ticket/service/BookingService.java index 43e290b..5846ae9 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/BookingService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/BookingService.java @@ -1,6 +1,6 @@ package comento.backend.ticket.service; -import comento.backend.ticket.config.customException.NotFoundDataException; +import comento.backend.ticket.config.customException.NoDataException; import comento.backend.ticket.domain.*; import comento.backend.ticket.dto.*; import comento.backend.ticket.repository.BookingRepository; @@ -21,52 +21,58 @@ public class BookingService { private static final String SEAT_LOCK = "seat_lock"; private final BookingRepository bookingRepository; - private final PerformanceService performanceService; - private final BookingHistoryService bookingHistoryService; private final UserService userService; private final SeatService seatService; private final RedissonClient redissonClient; + private final LockService lockService; - public BookingService(BookingRepository bookingRepository, PerformanceService performanceService, - BookingHistoryService bookingHistoryService, UserService userService, - SeatService seatService, RedissonClient redissonClient) { + public BookingService(BookingRepository bookingRepository, UserService userService, + SeatService seatService, RedissonClient redissonClient, LockService lockService) { this.bookingRepository = bookingRepository; - this.performanceService = performanceService; - this.bookingHistoryService = bookingHistoryService; this.userService = userService; this.seatService = seatService; this.redissonClient = redissonClient; + this.lockService = lockService; } - public Booking saveBookging(final BookingDto reqBooking) { - RLock lock = redissonClient.getLock(SEAT_LOCK); - Booking booking = null; - try { - if (!(lock.tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS))) { - log.info("lock 획득 실패"); - throw new RuntimeException("Rock fail!"); - } - log.info("lock 획득"); - final User user = userService.getUser(reqBooking.getEmail()); - final Performance performance = performanceService.getPerformance(reqBooking.getId(), reqBooking.getTitle()); - final Seat seat = seatService.getIsBooking(user, performance, reqBooking.getSeatType(), reqBooking.getSeatNumber()); //false라면 예약 가능 + //@brief : Redisson을 이용하여 동시성 제어할 경우 (선언적 트랜잭션 사용 불가, transcationManager 이용해야 함) +// public Booking saveBooking(final User user, final Performance performance, final Seat seat, final BookingDto reqBooking) { +// RLock lock = redissonClient.getLock(SEAT_LOCK); +// Booking booking = null; +// try { +// if (!(lock.tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS))) { +// log.info("lock 획득 실패"); +// throw new RuntimeException("Rock fail!"); +// } +// log.info("lock 획득"); +// +// //seat 테이블의 is_booking 칼럼을 true로 update +// updateSeat(seat, performance, reqBooking); +// +// //booking 여부 insert +// booking = reqBooking.toEntity(user, performance, seat); +// } catch (InterruptedException e) { +// throw new RuntimeException(e.getMessage()); +// } finally { +// lock.unlock(); +// log.info("lock 반납"); +// } +// return bookingRepository.save(booking); +// } + //@brief : MySQL User-level lock을 이용하여 동시성 제어할 경우(선언적 트랜잭션 사용 가능) + @Transactional + public Booking saveBooking(final User user, final Performance performance, final Seat seat, final BookingDto reqBooking){ + Booking[] booking = {null}; + lockService.execute(SEAT_LOCK, 10, () -> { //seat 테이블의 is_booking 칼럼을 true로 update updateSeat(seat, performance, reqBooking); - //seat의 값이 있다면, booking 가능 - bookingHistoryService.saveBookingSucessLog(user, performance, seat); - //booking 여부 insert - booking = reqBooking.toEntity(user, performance, seat); - } catch (InterruptedException e) { - throw new RuntimeException(e.getMessage()); - } finally { - lock.unlock(); - log.info("lock 반납"); - } + booking[0] = reqBooking.toEntity(user, performance, seat); + }); - return bookingRepository.save(booking); + return bookingRepository.save(booking[0]); } private void updateSeat(final Seat seat, final Performance performance, final BookingDto reqBooking) { @@ -85,7 +91,7 @@ public List getMyBooking(String email) { User user = userService.getUser(email); List result = bookingRepository.findMyBooking(true, user.getEmail()); if (result.isEmpty()) { - throw new NotFoundDataException(); + throw new NoDataException(); } return result; } diff --git a/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java b/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java index 5282968..b7cd9c6 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java @@ -6,6 +6,7 @@ import comento.backend.ticket.dto.PerformanceResponse; import comento.backend.ticket.repository.PerformanceRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Date; @@ -20,6 +21,7 @@ public PerformanceService(PerformanceRepository performanceRepository) { this.performanceRepository = performanceRepository; } + @Transactional(readOnly = true) public List getListPerformance(final PerformanceDto performanceDto){ Date startDate = performanceDto.getStartDate(); String title = performanceDto.getTitle(); @@ -32,6 +34,7 @@ public List getListPerformance(final PerformanceDto perform return result.stream().map(PerformanceResponse::of).collect(Collectors.toList()); } + @Transactional(readOnly = true) public Performance getPerformance(final Long id, final String title){ Optional performance = performanceRepository.findByIdAndTitle(id, title); return performance.orElseThrow(NotFoundDataException::new); diff --git a/ticket/src/main/java/comento/backend/ticket/service/SeatService.java b/ticket/src/main/java/comento/backend/ticket/service/SeatService.java index a7a483a..be8a664 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/SeatService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/SeatService.java @@ -1,7 +1,6 @@ package comento.backend.ticket.service; -import comento.backend.ticket.config.customException.DuplicatedException; -import comento.backend.ticket.config.customException.NotFoundDataException; +import comento.backend.ticket.config.customException.NoDataException; import comento.backend.ticket.domain.Performance; import comento.backend.ticket.domain.Seat; import comento.backend.ticket.domain.User; @@ -9,6 +8,7 @@ import comento.backend.ticket.emum.SeatType; import comento.backend.ticket.repository.SeatRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; @@ -17,19 +17,18 @@ @Service public class SeatService { private final SeatRepository seatRepository; - private final BookingHistoryService bookingHistoryService; - public SeatService(SeatRepository seatRepository, BookingHistoryService bookingHistoryService) { + public SeatService(SeatRepository seatRepository) { this.seatRepository = seatRepository; - this.bookingHistoryService = bookingHistoryService; } + @Transactional(readOnly = true) public List getListPerformanceSeat(final PerformanceResponse performanceData) { Performance performance = new Performance(); performance.setId(performanceData.getId()); List seatResult = seatRepository.findByPerformance(performance); if(seatResult.isEmpty()){ - throw new NotFoundDataException(); + throw new NoDataException(); } return seatResult.stream().map(SeatResponse::of).collect(Collectors.toList()); } @@ -39,17 +38,10 @@ public Seat updateSeat(final SeatDto seatDto) { return seatRepository.save(seat); } + @Transactional(readOnly = true) public Seat getIsBooking(final User user, final Performance performance, final SeatType seatType, final Integer seatNumber){ Seat seatIsBooking = seatRepository.findByPerformanceAndSeatTypeAndSeatNumber(performance, seatType, seatNumber) .orElseGet(()-> null); - if(seatIsBooking.isBooking()){ //true라면 예약 불가 - bookingHistoryService.saveBookingFailLog(user, performance, seatIsBooking); - throw new DuplicatedException("SeatService"); - } - - if(Objects.isNull(seatIsBooking)){ //좌석 정보 없음 - throw new NotFoundDataException(); - } return seatIsBooking; } } diff --git a/ticket/src/main/java/comento/backend/ticket/service/UserService.java b/ticket/src/main/java/comento/backend/ticket/service/UserService.java index 2890a98..4d01593 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/UserService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/UserService.java @@ -6,6 +6,7 @@ import comento.backend.ticket.dto.UserDto; import comento.backend.ticket.repository.UserRepository; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.Objects; import java.util.Optional; @@ -19,6 +20,7 @@ public UserService(UserRepository userRepository) { this.userRepository = userRepository; } + @Transactional public User saveUser(final UserDto userDto){ User user = userDto.toEntity(); From c0c766529cf2eaac722007b3d99cc01f4137a2ff Mon Sep 17 00:00:00 2001 From: hyejungg Date: Mon, 18 Oct 2021 23:31:09 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[refator:]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20-=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/ticket/config/ErrorCode.java | 1 - .../backend/ticket/config/ErrorResponse.java | 1 - .../ticket/config/GlobalExceptionHandler.java | 65 +++++-------------- .../backend/ticket/config/SuccessCode.java | 4 +- .../ticket/config/SuccessResponse.java | 8 ++- .../customException/DuplicatedException.java | 10 +-- .../customException/NoAuthException.java | 10 +-- .../customException/NoDataException.java | 10 --- .../NotFoundDataException.java | 10 +-- .../ticket/controller/BookingController.java | 15 +++-- .../controller/PerformanceController.java | 8 +-- .../ticket/controller/UserController.java | 5 +- .../ticket/service/BookingService.java | 7 +- .../ticket/service/PerformanceService.java | 5 +- .../backend/ticket/service/SeatService.java | 9 ++- .../backend/ticket/service/UserService.java | 5 +- 16 files changed, 66 insertions(+), 107 deletions(-) delete mode 100644 ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java diff --git a/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java b/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java index 6bad193..4116911 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java +++ b/ticket/src/main/java/comento/backend/ticket/config/ErrorCode.java @@ -14,7 +14,6 @@ public enum ErrorCode { INVALID_VALUE(400, HttpStatus.BAD_REQUEST, "BAD REQUEST", "잘못된 요청입니다. 다시 요청해주세요."), NO_USER(401, HttpStatus.UNAUTHORIZED,"UNAUTHORIZED", "등록되지 않은 사용자입니다"), NOT_FOUND(404, HttpStatus.NOT_FOUND, "NOT FOUND ERROR", "요청할 수 없는 리소스입니다."), - NO_DATA(404, HttpStatus.NOT_FOUND, "NO DATA", "데이터가 존재하지 않습니다"), INVALID_USER(409, HttpStatus.CONFLICT, "CONFLICT", "이미 존재하는 사용자입니다."), INVALID_SEAT(409, HttpStatus.CONFLICT, "CONFLICT", "이미 예약된 좌석입니다."), SERVER_ERROR(500, HttpStatus.INTERNAL_SERVER_ERROR, "INTERVAL_SERVER ERROR", "서버 에러입니다."); diff --git a/ticket/src/main/java/comento/backend/ticket/config/ErrorResponse.java b/ticket/src/main/java/comento/backend/ticket/config/ErrorResponse.java index 93d3254..e7def2c 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/ErrorResponse.java +++ b/ticket/src/main/java/comento/backend/ticket/config/ErrorResponse.java @@ -12,7 +12,6 @@ public class ErrorResponse{ private String message; private String reason; - //공통된 Error Message를 전송할 때 사용 public static ErrorResponse res(final Integer status, final String message, final String reason){ return ErrorResponse.builder() .status(status) diff --git a/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java b/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java index 7fcb3c8..4595c6c 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java +++ b/ticket/src/main/java/comento/backend/ticket/config/GlobalExceptionHandler.java @@ -1,10 +1,6 @@ package comento.backend.ticket.config; import com.fasterxml.jackson.core.JsonParseException; -import comento.backend.ticket.config.customException.DuplicatedException; -import comento.backend.ticket.config.customException.NoAuthException; -import comento.backend.ticket.config.customException.NoDataException; -import comento.backend.ticket.config.customException.NotFoundDataException; import javassist.NotFoundException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -22,65 +18,36 @@ public class GlobalExceptionHandler { private static ErrorCode errorCode; - /** - * 특정 Exception을 지정하여 별도로 처리 - */ - //ConstraintViolationException.class 는 유효성 검사 실패시 (@Validated) @ExceptionHandler({JsonParseException.class, MethodArgumentNotValidException.class, ConstraintViolationException.class, MethodArgumentTypeMismatchException.class, MissingServletRequestParameterException.class}) - public static ResponseEntity missMatchExceptionHandler(Throwable t){ + public static ResponseEntity missMatchExceptionHandler(){ errorCode = ErrorCode.INVALID_VALUE; - log.error(errorCode.getStatus() + " " + errorCode.getMessage(), t); - return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), - errorCode.getHttpStatus()); - } - - @ExceptionHandler(NoAuthException.class) - public static ResponseEntity noAuthExceptionHandler(NoAuthException e){ - errorCode = ErrorCode.NO_USER; - log.error(errorCode.getStatus() + errorCode.getMessage(), e); - return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), - errorCode.getHttpStatus()); + log.error(String.valueOf(errorCode.getHttpStatus()), errorCode.getMessage()); + return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason())); } - @ExceptionHandler(NoDataException.class) - public static ResponseEntity noDataExceptionHandler(Throwable t){ - errorCode = ErrorCode.NO_DATA; - log.error(errorCode.getStatus() + " " + errorCode.getMessage(), t); - return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), - errorCode.getHttpStatus()); + @ExceptionHandler(CustomException.class) + public static ResponseEntity customExceptionHandler(CustomException e){ + errorCode = e.getErrorCode(); + log.error(String.valueOf(errorCode.getHttpStatus()), errorCode.getMessage()); + return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason())); } - @ExceptionHandler({NoSuchElementException.class, NotFoundException.class, NotFoundDataException.class}) - public static ResponseEntity notFoundExceptionHandler(Throwable t){ + @ExceptionHandler({NoSuchElementException.class, NotFoundException.class}) + public static ResponseEntity notFoundExceptionHandler(){ errorCode = ErrorCode.NOT_FOUND; - log.error(errorCode.getStatus() + " " + errorCode.getMessage(), t); - return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), - errorCode.getHttpStatus()); - } - - @ExceptionHandler(DuplicatedException.class) - public static ResponseEntity duplicateExceptionHandler(DuplicatedException e){ - if (e.getMessage() == "UserService") { //호출된 곳이 UserService - errorCode = ErrorCode.INVALID_USER; - log.error(errorCode.getStatus() + " " + errorCode.getMessage(), e); - }else{ //좌석 예약 시 - errorCode = ErrorCode.INVALID_SEAT; - log.error(errorCode.getStatus() + " " + errorCode.getMessage(), e); - } - return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), - errorCode.getHttpStatus()); + log.error(String.valueOf(errorCode.getHttpStatus()), errorCode.getMessage()); + return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason())); } - @ExceptionHandler({IllegalStateException.class, RuntimeException.class}) - public static ResponseEntity IllExceptionHandler(Throwable t){ + @ExceptionHandler(Exception.class) + public static ResponseEntity IllExceptionHandler(Exception e){ errorCode = ErrorCode.SERVER_ERROR; - log.error(errorCode.getStatus() + " " + errorCode.getMessage(), t); - return new ResponseEntity<>(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason()), - errorCode.getHttpStatus()); + log.error(String.valueOf(errorCode.getHttpStatus()), errorCode.getMessage(), e); + return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.res(errorCode.getStatus(), errorCode.getMessage(), errorCode.getReason())); } } diff --git a/ticket/src/main/java/comento/backend/ticket/config/SuccessCode.java b/ticket/src/main/java/comento/backend/ticket/config/SuccessCode.java index 61701c7..a2f0065 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/SuccessCode.java +++ b/ticket/src/main/java/comento/backend/ticket/config/SuccessCode.java @@ -7,9 +7,9 @@ @Getter @AllArgsConstructor public enum SuccessCode { - OK(200, HttpStatus.OK, "OK"), - CREATED(201, HttpStatus.CREATED, "CREATED"); + CREATED(201, HttpStatus.CREATED, "CREATED"), + NO_DATA(204, HttpStatus.NO_CONTENT, "데이터가 존재하지 않습니다"); private final Integer status; private final HttpStatus httpStatus; diff --git a/ticket/src/main/java/comento/backend/ticket/config/SuccessResponse.java b/ticket/src/main/java/comento/backend/ticket/config/SuccessResponse.java index e491cea..b34ae9d 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/SuccessResponse.java +++ b/ticket/src/main/java/comento/backend/ticket/config/SuccessResponse.java @@ -12,7 +12,6 @@ public class SuccessResponse { private String message; private Object data; - //공통된 Success Message를 전송할 때 사용 public static SuccessResponse res(final Integer status, final String message, final Object data){ return SuccessResponse.builder() .status(status) @@ -21,4 +20,11 @@ public static SuccessResponse res(final Integer status, final String message, fi .build(); } + public static SuccessResponse res(final Integer status, final String message){ + return SuccessResponse.builder() + .status(status) + .message(message) + .build(); + } + } diff --git a/ticket/src/main/java/comento/backend/ticket/config/customException/DuplicatedException.java b/ticket/src/main/java/comento/backend/ticket/config/customException/DuplicatedException.java index bcf6588..dbf3db6 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/customException/DuplicatedException.java +++ b/ticket/src/main/java/comento/backend/ticket/config/customException/DuplicatedException.java @@ -1,10 +1,10 @@ package comento.backend.ticket.config.customException; -public class DuplicatedException extends RuntimeException{ - public DuplicatedException() { - } +import comento.backend.ticket.config.CustomException; +import comento.backend.ticket.config.ErrorCode; - public DuplicatedException(String message) { - super(message); +public class DuplicatedException extends CustomException { + public DuplicatedException(ErrorCode errorCode) { + super(errorCode); } } diff --git a/ticket/src/main/java/comento/backend/ticket/config/customException/NoAuthException.java b/ticket/src/main/java/comento/backend/ticket/config/customException/NoAuthException.java index f264136..b7d1ae9 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/customException/NoAuthException.java +++ b/ticket/src/main/java/comento/backend/ticket/config/customException/NoAuthException.java @@ -1,10 +1,10 @@ package comento.backend.ticket.config.customException; -public class NoAuthException extends RuntimeException{ - public NoAuthException() { - } +import comento.backend.ticket.config.CustomException; +import comento.backend.ticket.config.ErrorCode; - public NoAuthException(String message) { - super(message); +public class NoAuthException extends CustomException { + public NoAuthException(ErrorCode errorCode) { + super(errorCode); } } diff --git a/ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java b/ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java deleted file mode 100644 index 96a252c..0000000 --- a/ticket/src/main/java/comento/backend/ticket/config/customException/NoDataException.java +++ /dev/null @@ -1,10 +0,0 @@ -package comento.backend.ticket.config.customException; - -public class NoDataException extends RuntimeException{ - public NoDataException() { - } - - public NoDataException(String message) { - super(message); - } -} diff --git a/ticket/src/main/java/comento/backend/ticket/config/customException/NotFoundDataException.java b/ticket/src/main/java/comento/backend/ticket/config/customException/NotFoundDataException.java index c53f237..6ca7dac 100644 --- a/ticket/src/main/java/comento/backend/ticket/config/customException/NotFoundDataException.java +++ b/ticket/src/main/java/comento/backend/ticket/config/customException/NotFoundDataException.java @@ -1,10 +1,10 @@ package comento.backend.ticket.config.customException; -public class NotFoundDataException extends RuntimeException{ - public NotFoundDataException() { - } +import comento.backend.ticket.config.CustomException; +import comento.backend.ticket.config.ErrorCode; - public NotFoundDataException(String message) { - super(message); +public class NotFoundDataException extends CustomException { + public NotFoundDataException(ErrorCode errorCode) { + super(errorCode); } } diff --git a/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java b/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java index 591c8de..cab4aab 100644 --- a/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java +++ b/ticket/src/main/java/comento/backend/ticket/controller/BookingController.java @@ -1,5 +1,7 @@ package comento.backend.ticket.controller; +import comento.backend.ticket.config.ErrorCode; +import comento.backend.ticket.config.ErrorResponse; import comento.backend.ticket.config.SuccessCode; import comento.backend.ticket.config.SuccessResponse; import comento.backend.ticket.config.customException.DuplicatedException; @@ -43,27 +45,26 @@ public BookingController(BookingService bookingService, BookingHistoryService bo public ResponseEntity addBooking(@Valid @RequestBody BookingDto reqBooking){ final User user = userService.getUser(reqBooking.getEmail()); final Performance performance = performanceService.getPerformance(reqBooking.getId(), reqBooking.getTitle()); - final Seat seat = seatService.getIsBooking(user, performance, reqBooking.getSeatType(), reqBooking.getSeatNumber()); //false라면 예약 가능 + final Seat seat = seatService.getIsBooking(performance, reqBooking.getSeatType(), reqBooking.getSeatNumber()); //false라면 예약 가능 if(seat.isBooking()){ //true면 이미 예약된 상태 bookingHistoryService.saveBookingFailLog(user, performance, seat); - throw new DuplicatedException("SeatService"); + throw new DuplicatedException(ErrorCode.INVALID_SEAT); }else{ bookingService.saveBooking(user, performance, seat, reqBooking); bookingHistoryService.saveBookingSucessLog(user, performance, seat); result = new BookingResponseCreated(reqBooking.getSeatType(), reqBooking.getSeatNumber()); successCode = SuccessCode.CREATED; } - - return new ResponseEntity(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), result), - HttpStatus.CREATED); + return ResponseEntity.status(successCode.getHttpStatus()) + .body(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), result)); } @GetMapping("/email/{email}") public ResponseEntity showMyBooking(@Valid @PathVariable String email){ List result = bookingService.getMyBooking(email); successCode = SuccessCode.OK; - return new ResponseEntity(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), result), - HttpStatus.OK); + return ResponseEntity.status(successCode.getHttpStatus()) + .body(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), result)); } } diff --git a/ticket/src/main/java/comento/backend/ticket/controller/PerformanceController.java b/ticket/src/main/java/comento/backend/ticket/controller/PerformanceController.java index a1c59be..c80ccad 100644 --- a/ticket/src/main/java/comento/backend/ticket/controller/PerformanceController.java +++ b/ticket/src/main/java/comento/backend/ticket/controller/PerformanceController.java @@ -41,9 +41,7 @@ public ResponseEntity showPerformanceInfo(@Valid @RequestParam(value = "date", r @Valid @RequestParam(value = "title", required = false) String title) { PerformanceDto performanceDto = new PerformanceDto(title, date); List result = performanceService.getListPerformance(performanceDto); - - return new ResponseEntity(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), result), - HttpStatus.OK); + return ResponseEntity.ok().body(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), result)); } @GetMapping("/info/seat") @@ -54,8 +52,6 @@ public ResponseEntity showPerformanceSeatInfo(@Valid @RequestParam(value = "date List performanceData = performanceService.getListPerformance(performanceDto); List seatResult = seatService.getListPerformanceSeat(performanceData.get(0)); - - return new ResponseEntity(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), seatResult), - HttpStatus.OK); + return ResponseEntity.ok().body(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), seatResult)); } } diff --git a/ticket/src/main/java/comento/backend/ticket/controller/UserController.java b/ticket/src/main/java/comento/backend/ticket/controller/UserController.java index 9848d07..44afb59 100644 --- a/ticket/src/main/java/comento/backend/ticket/controller/UserController.java +++ b/ticket/src/main/java/comento/backend/ticket/controller/UserController.java @@ -7,6 +7,7 @@ import comento.backend.ticket.dto.UserDto; import comento.backend.ticket.service.UserService; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -26,7 +27,7 @@ public UserController(UserService userService) { @PostMapping("/signup") public ResponseEntity addEmail(@Validated @RequestBody UserDto userDto){ userService.saveUser(userDto); - return new ResponseEntity<>(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), CREATED_MSG), - successCode.getHttpStatus()); + return ResponseEntity.status(successCode.getHttpStatus()) + .body(SuccessResponse.res(successCode.getStatus(), successCode.getMessage(), CREATED_MSG)); } } diff --git a/ticket/src/main/java/comento/backend/ticket/service/BookingService.java b/ticket/src/main/java/comento/backend/ticket/service/BookingService.java index 5846ae9..4e92f1a 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/BookingService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/BookingService.java @@ -1,17 +1,16 @@ package comento.backend.ticket.service; -import comento.backend.ticket.config.customException.NoDataException; +import comento.backend.ticket.config.ErrorCode; +import comento.backend.ticket.config.customException.NotFoundDataException; import comento.backend.ticket.domain.*; import comento.backend.ticket.dto.*; import comento.backend.ticket.repository.BookingRepository; import lombok.extern.slf4j.Slf4j; -import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; -import java.util.concurrent.TimeUnit; @Slf4j @Service @@ -91,7 +90,7 @@ public List getMyBooking(String email) { User user = userService.getUser(email); List result = bookingRepository.findMyBooking(true, user.getEmail()); if (result.isEmpty()) { - throw new NoDataException(); + throw new NotFoundDataException(ErrorCode.NOT_FOUND); } return result; } diff --git a/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java b/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java index b7cd9c6..a345290 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/PerformanceService.java @@ -1,5 +1,6 @@ package comento.backend.ticket.service; +import comento.backend.ticket.config.ErrorCode; import comento.backend.ticket.config.customException.NotFoundDataException; import comento.backend.ticket.domain.Performance; import comento.backend.ticket.dto.PerformanceDto; @@ -29,7 +30,7 @@ public List getListPerformance(final PerformanceDto perform performanceRepository.findByTitleAndStartDateGreaterThanEqualOrderByStartDate(title, startDate) : performanceRepository.findByStartDateGreaterThanEqualOrderByStartDateAsc(startDate); if(result.isEmpty()){ - throw new NotFoundDataException(); + throw new NotFoundDataException(ErrorCode.NOT_FOUND); } return result.stream().map(PerformanceResponse::of).collect(Collectors.toList()); } @@ -37,6 +38,6 @@ public List getListPerformance(final PerformanceDto perform @Transactional(readOnly = true) public Performance getPerformance(final Long id, final String title){ Optional performance = performanceRepository.findByIdAndTitle(id, title); - return performance.orElseThrow(NotFoundDataException::new); + return performance.orElseThrow(() -> new NotFoundDataException(ErrorCode.NOT_FOUND)); } } diff --git a/ticket/src/main/java/comento/backend/ticket/service/SeatService.java b/ticket/src/main/java/comento/backend/ticket/service/SeatService.java index be8a664..df48186 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/SeatService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/SeatService.java @@ -1,9 +1,9 @@ package comento.backend.ticket.service; -import comento.backend.ticket.config.customException.NoDataException; +import comento.backend.ticket.config.ErrorCode; +import comento.backend.ticket.config.customException.NotFoundDataException; import comento.backend.ticket.domain.Performance; import comento.backend.ticket.domain.Seat; -import comento.backend.ticket.domain.User; import comento.backend.ticket.dto.*; import comento.backend.ticket.emum.SeatType; import comento.backend.ticket.repository.SeatRepository; @@ -11,7 +11,6 @@ import org.springframework.transaction.annotation.Transactional; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; @Service @@ -28,7 +27,7 @@ public List getListPerformanceSeat(final PerformanceResponse perfo performance.setId(performanceData.getId()); List seatResult = seatRepository.findByPerformance(performance); if(seatResult.isEmpty()){ - throw new NoDataException(); + throw new NotFoundDataException(ErrorCode.NOT_FOUND); } return seatResult.stream().map(SeatResponse::of).collect(Collectors.toList()); } @@ -39,7 +38,7 @@ public Seat updateSeat(final SeatDto seatDto) { } @Transactional(readOnly = true) - public Seat getIsBooking(final User user, final Performance performance, final SeatType seatType, final Integer seatNumber){ + public Seat getIsBooking(final Performance performance, final SeatType seatType, final Integer seatNumber){ Seat seatIsBooking = seatRepository.findByPerformanceAndSeatTypeAndSeatNumber(performance, seatType, seatNumber) .orElseGet(()-> null); return seatIsBooking; diff --git a/ticket/src/main/java/comento/backend/ticket/service/UserService.java b/ticket/src/main/java/comento/backend/ticket/service/UserService.java index 4d01593..0c9fb54 100644 --- a/ticket/src/main/java/comento/backend/ticket/service/UserService.java +++ b/ticket/src/main/java/comento/backend/ticket/service/UserService.java @@ -1,5 +1,6 @@ package comento.backend.ticket.service; +import comento.backend.ticket.config.ErrorCode; import comento.backend.ticket.config.customException.DuplicatedException; import comento.backend.ticket.config.customException.NoAuthException; import comento.backend.ticket.domain.User; @@ -25,7 +26,7 @@ public User saveUser(final UserDto userDto){ User user = userDto.toEntity(); if(!checkEmailDuplicate(user.getEmail())) { - throw new DuplicatedException("UserService"); + throw new DuplicatedException(ErrorCode.INVALID_USER); } return userRepository.save(user); @@ -38,6 +39,6 @@ private boolean checkEmailDuplicate(final String email){ public User getUser(final String email){ Optional user = userRepository.findByEmail(email); - return user.orElseThrow(NoAuthException::new); + return user.orElseThrow(() -> new NoAuthException(ErrorCode.NO_USER)); } }