-
Notifications
You must be signed in to change notification settings - Fork 4
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[#14] 복잡한 쿼리 최적화 & 동적 스케줄링으로 변경 #17
Merged
Merged
Changes from 12 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
14cfe11
feat : 월별 통계 조회 기능
codesejin 9d4fb54
refactor : 불필요한 내용 제외 및 주석 수정
codesejin 17c7c1f
refactor : 동적 스케줄링으로 변경
codesejin aad35c5
refactor : 순서 변경
codesejin 34c556c
refactor : 주석정리
codesejin f480382
style : 코드 스타일 정리
codesejin 484fd0b
refactor : 인덱스 설정한 내용 추가
codesejin 7824730
refactor : 집계함수 분리 시 사용했던 VO제거
codesejin 299e6d5
refactor : 요청 일자 검증 예외 추가
codesejin bf802ff
refactor : 요청 일자 검증 Exception 추가
codesejin 5024c1d
refactor : 로직 변경 - 1달 기준으로 안해도 됨
codesejin c931928
refactor : 병렬스트림 사용 시 Thread safe 한 자료구조 사용 및 파라미터로 Collection 넘기지 말고…
codesejin 53b6aa4
refactor : MySQL 문법 SNAKE_CASE로 변경 및 원본객체를 정렬할때는 동시성이슈를 피하기 위해 stream적용
codesejin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
src/main/java/com/flab/offcoupon/component/scheduler/DynamicScheduler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.flab.offcoupon.component.scheduler; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.scheduling.Trigger; | ||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; | ||
|
||
/** | ||
* 동적 스케줄러(ThreadPoolTaskScheduler)를 생성하는 컴포넌트 클래스입니다.<br> | ||
* 스케줄러를 시작하거나 종료할 수 있습니다.<br> | ||
* <p> | ||
* 동적 스케줄링을 구현한 이유는 @Scheduled 로 작성된 스케줄러의 경우 여러 디렉토리가 분산되어 있다면 동료 개발자에게 혼동을 줄 수 있기 때문입니다. | ||
* </p> | ||
*/ | ||
@RequiredArgsConstructor | ||
public class DynamicScheduler { | ||
private ThreadPoolTaskScheduler scheduler; | ||
private final Runnable runnable; | ||
private final Trigger trigger; | ||
|
||
public void startScheduler() { | ||
scheduler = new ThreadPoolTaskScheduler(); | ||
scheduler.initialize(); | ||
// 스케줄러가 시작되는 부분 | ||
scheduler.schedule(runnable, trigger); | ||
} | ||
|
||
public void stopScheduler() { | ||
scheduler.shutdown(); | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
src/main/java/com/flab/offcoupon/component/scheduler/ScheduleRunner.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.flab.offcoupon.component.scheduler; | ||
|
||
import com.flab.offcoupon.component.scheduler.coupon_issue.ConsumeMqScheduler; | ||
import com.flab.offcoupon.component.scheduler.coupon_issue.UpdateTotalCouponIssueCntScheduler; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.stereotype.Component; | ||
|
||
@RequiredArgsConstructor | ||
@Component | ||
public class ScheduleRunner { | ||
|
||
private final ConsumeMqScheduler consumeMqScheduler; | ||
private final UpdateTotalCouponIssueCntScheduler updateTotalCouponIssueCntScheduler; | ||
public void run() { | ||
consumeMqScheduler.startScheduler(); | ||
updateTotalCouponIssueCntScheduler.startScheduler(); | ||
} | ||
} |
32 changes: 32 additions & 0 deletions
32
src/main/java/com/flab/offcoupon/component/scheduler/coupon_issue/ConsumeMqScheduler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package com.flab.offcoupon.component.scheduler.coupon_issue; | ||
|
||
import com.flab.offcoupon.component.scheduler.DynamicScheduler; | ||
import com.flab.offcoupon.service.coupon_issue.async.consumer.CouponIssueConsumer; | ||
import org.springframework.scheduling.Trigger; | ||
import org.springframework.scheduling.support.PeriodicTrigger; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
|
||
/** | ||
* RabbitMQ를 이용한 비동기 쿠폰 발행을 처리하는 컨슈머 클래스와 스케줄러를 연결하는 클래스입니다. | ||
*/ | ||
@Component | ||
public class ConsumeMqScheduler { | ||
private CouponIssueConsumer couponIssueConsumer; | ||
|
||
public ConsumeMqScheduler(CouponIssueConsumer couponIssueConsumer) { | ||
this.couponIssueConsumer = couponIssueConsumer; | ||
} | ||
|
||
// 실행 로직 | ||
private final Runnable runnable = () -> couponIssueConsumer.consumeCouponIssueMessage(); | ||
|
||
// 실행 주기 | ||
private final Trigger trigger = new PeriodicTrigger(3, TimeUnit.SECONDS); | ||
|
||
public void startScheduler() { | ||
DynamicScheduler scheduler = new DynamicScheduler(runnable, trigger); | ||
scheduler.startScheduler(); | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
...m/flab/offcoupon/component/scheduler/coupon_issue/UpdateTotalCouponIssueCntScheduler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.flab.offcoupon.component.scheduler.coupon_issue; | ||
|
||
import com.flab.offcoupon.component.scheduler.DynamicScheduler; | ||
import com.flab.offcoupon.service.coupon_issue.async.consumer.CouponIssueConsumer; | ||
import org.springframework.scheduling.Trigger; | ||
import org.springframework.scheduling.support.PeriodicTrigger; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.concurrent.TimeUnit; | ||
|
||
/** | ||
* 비동기 쿠폰 발행 쿠폰 발행 총 갯수를 업데이트하는 스케줄러를 연결하는 클래스입니다. | ||
*/ | ||
@Component | ||
public class UpdateTotalCouponIssueCntScheduler { | ||
private CouponIssueConsumer couponIssueConsumer; | ||
|
||
public UpdateTotalCouponIssueCntScheduler(CouponIssueConsumer couponIssueConsumer) { | ||
this.couponIssueConsumer = couponIssueConsumer; | ||
} | ||
|
||
private final Runnable runnable = () -> couponIssueConsumer.updateTotalCouponIssueCount(); | ||
|
||
private final Trigger trigger = new PeriodicTrigger(10, TimeUnit.SECONDS); | ||
|
||
public void startScheduler() { | ||
DynamicScheduler scheduler = new DynamicScheduler(runnable, trigger); | ||
scheduler.startScheduler(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
src/main/java/com/flab/offcoupon/controller/StatisticsController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package com.flab.offcoupon.controller; | ||
|
||
import com.flab.offcoupon.dto.request.StatisticsRequest; | ||
import com.flab.offcoupon.dto.response.MonthlyOrderStatistics; | ||
import com.flab.offcoupon.service.StatisticsService; | ||
import com.flab.offcoupon.util.ResponseDTO; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.GetMapping; | ||
import org.springframework.web.bind.annotation.RequestBody; | ||
import org.springframework.web.bind.annotation.RequestMapping; | ||
import org.springframework.web.bind.annotation.RestController; | ||
|
||
import java.util.List; | ||
|
||
@RequiredArgsConstructor | ||
@RequestMapping("/api/v1/statistics") | ||
@RestController | ||
public class StatisticsController { | ||
|
||
private final StatisticsService statisticsService; | ||
|
||
/** | ||
* 1000만의 데이터로 월별 주문 통계 조회에 대한 쿼리 최적화와 속도 개선을 목표로 했습니다. | ||
* 관련된 내용을 포스팅하여 블로그에 작성했으며, 링크는 아래에 있는 @See에서 확인할 수 있습니다. | ||
* <p> | ||
* 1. DB 쿼리 속도 개선 | ||
* <ul> | ||
* <li> 테이블 당 1000만건의 데이터가 있다고 가정하고, csv파일로 로컬 DB에 import했습니다.</li> | ||
* <li> EXPLAIN명령어를 사용하여 실행계획을 분석하고, 복합인덱스, 커버링인덱스를 사용하여 약 2배의 속도를 개선했습니다.</li> | ||
* </ul> | ||
* </p> | ||
* <p> | ||
* 2. 애플리케이션 속도 개선 | ||
* <ul> | ||
* <li> 쿼리를 최적화여 속도는 개선했지만, where절 기준으로 여전히 읽어야할 레코드의 수가 많아서 만족스러운 속도가 나오지 않았습니다.</li> | ||
* <li> 따라서 요청의 시작일과 종료일을 1달 기준으로 분리하여 쿼리를 날렸지만, 결국에 월별로 실행된 쿼리도 1s씩 합쳐지게 되어 결국 속도가 다를바 없었습니다. </li> | ||
* <li> 병렬 스트림을 사용하여 월별로 쿼리를 병렬적으로 수행하여 약 3배의 속도를 개선했습니다</li> | ||
* </ul> | ||
* </p> | ||
* @param request 월별 주문 통계 조회 요청 | ||
* @return 월별 주문 통계 조회 결과 | ||
* @See <a href="https://strong-park.tistory.com/entry/1000%EB%A7%8C%EA%B1%B4%EC%9D%98-%EB%8D%B0%EC%9D%B4%ED%84%B0%EB%A5%BC-%EB%8C%80%EC%83%81%EC%9C%BC%EB%A1%9C-%EC%BF%BC%EB%A6%AC%EC%B5%9C%EC%A0%81%ED%99%94%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EC%9E%90">1000만건의 데이터를 대상으로 쿼리최적화 with. 복합인덱스, 커버링인덱스</a> | ||
*/ | ||
@GetMapping("/monthly-order") | ||
public ResponseEntity<ResponseDTO<List<MonthlyOrderStatistics>>> getMonthlyOrderStatistics(@RequestBody final StatisticsRequest request) { | ||
return ResponseEntity.status(HttpStatus.OK).body(statisticsService.getMonthlyOrderStatistics(request)); | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
...in/java/com/flab/offcoupon/domain/vo/persistence/statistics/MonthlyOrderStatisticsVo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.flab.offcoupon.domain.vo.persistence.statistics; | ||
|
||
import java.math.BigDecimal; | ||
import java.time.YearMonth; | ||
|
||
/** | ||
* MyBatis에서 여러개의 반환 값을 전달 받기 위한 VO | ||
* | ||
* 월별 주문 통계를 조회하기 위해 사용 | ||
*/ | ||
public record MonthlyOrderStatisticsVo( | ||
YearMonth yearMonth, | ||
long totalOrderCnt, | ||
BigDecimal totalPaymentPrice, | ||
long totalCouponUseCnt, | ||
BigDecimal totalCouponPrice | ||
) { | ||
} |
9 changes: 9 additions & 0 deletions
9
...ava/com/flab/offcoupon/domain/vo/persistence/statistics/MonthlyStatisticsParameterVo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.flab.offcoupon.domain.vo.persistence.statistics; | ||
|
||
import java.time.LocalDate; | ||
|
||
public record MonthlyStatisticsParameterVo( | ||
LocalDate startedAt, | ||
LocalDate endedAt | ||
) { | ||
} |
16 changes: 16 additions & 0 deletions
16
src/main/java/com/flab/offcoupon/dto/request/StatisticsRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
package com.flab.offcoupon.dto.request; | ||
|
||
import lombok.*; | ||
|
||
import java.time.LocalDate; | ||
|
||
@Generated | ||
@Getter | ||
@EqualsAndHashCode | ||
@RequiredArgsConstructor | ||
public final class StatisticsRequest { | ||
@NonNull | ||
private final LocalDate startedAt; | ||
@NonNull | ||
private final LocalDate endedAt; | ||
} |
28 changes: 28 additions & 0 deletions
28
src/main/java/com/flab/offcoupon/dto/response/MonthlyOrderStatistics.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package com.flab.offcoupon.dto.response; | ||
|
||
import com.flab.offcoupon.domain.vo.persistence.statistics.MonthlyOrderStatisticsVo; | ||
import lombok.AllArgsConstructor; | ||
import lombok.Getter; | ||
|
||
import java.math.BigDecimal; | ||
import java.time.YearMonth; | ||
|
||
|
||
@Getter | ||
@AllArgsConstructor | ||
public final class MonthlyOrderStatistics { | ||
|
||
private final YearMonth yearMonth; | ||
private final long totalOrderCnt; | ||
private final BigDecimal totalPaymentPrice; | ||
private final long totalCouponUseCnt; | ||
private final BigDecimal totalCouponPrice; | ||
|
||
public MonthlyOrderStatistics(MonthlyOrderStatisticsVo vo) { | ||
this.yearMonth = (vo != null) ? vo.yearMonth() : null; | ||
this.totalOrderCnt = (vo != null) ? vo.totalOrderCnt() : 0; | ||
this.totalPaymentPrice = (vo != null) ? vo.totalPaymentPrice() : BigDecimal.ZERO; | ||
this.totalCouponUseCnt = (vo != null) ? vo.totalCouponUseCnt() : 0; | ||
this.totalCouponPrice = (vo != null) ? vo.totalCouponPrice() : BigDecimal.ZERO; | ||
} | ||
} |
18 changes: 18 additions & 0 deletions
18
src/main/java/com/flab/offcoupon/exception/statistics/LocalDateBadRequestException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.flab.offcoupon.exception.statistics; | ||
|
||
import com.flab.offcoupon.exception.CustomException; | ||
import lombok.AccessLevel; | ||
import lombok.NoArgsConstructor; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.web.bind.annotation.ResponseStatus; | ||
|
||
|
||
@ResponseStatus(value = HttpStatus.BAD_REQUEST) | ||
@NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
public class LocalDateBadRequestException extends CustomException { | ||
|
||
public LocalDateBadRequestException(String message) { | ||
super(message); | ||
} | ||
|
||
} |
9 changes: 9 additions & 0 deletions
9
src/main/java/com/flab/offcoupon/exception/statistics/StatisticsErrorMessage.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.flab.offcoupon.exception.statistics; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.NoArgsConstructor; | ||
|
||
@NoArgsConstructor(access = AccessLevel.PRIVATE) | ||
public class StatisticsErrorMessage { | ||
public static final String START_MUST_BE_BEFORE_THANT_END = "시작일은 종료일보다 이전이어야 합니다. startedAt : %s, endedAt : %s"; | ||
} |
22 changes: 22 additions & 0 deletions
22
src/main/java/com/flab/offcoupon/exception/statistics/StatisticsExceptionHandler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.flab.offcoupon.exception.statistics; | ||
|
||
import com.flab.offcoupon.util.ResponseDTO; | ||
import jakarta.servlet.http.HttpServletRequest; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.http.HttpStatus; | ||
import org.springframework.http.ResponseEntity; | ||
import org.springframework.web.bind.annotation.ExceptionHandler; | ||
import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
||
import static com.flab.offcoupon.exception.GlobalExceptionHandler.HTTP_REQUEST; | ||
|
||
@Slf4j | ||
@RestControllerAdvice | ||
public class StatisticsExceptionHandler { | ||
@ExceptionHandler(LocalDateBadRequestException.class) | ||
public ResponseEntity<ResponseDTO<String>> couponNotFountException(LocalDateBadRequestException ex, HttpServletRequest request) { | ||
log.info(HTTP_REQUEST, request.getMethod(), request.getRequestURI(), | ||
ex.getMessage(), HttpStatus.BAD_REQUEST); | ||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ResponseDTO.getFailResult(ex.getMessage())); | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
src/main/java/com/flab/offcoupon/repository/mysql/StatisticsRepository.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.flab.offcoupon.repository.mysql; | ||
|
||
import com.flab.offcoupon.domain.vo.persistence.statistics.MonthlyOrderStatisticsVo; | ||
import com.flab.offcoupon.domain.vo.persistence.statistics.MonthlyStatisticsParameterVo; | ||
import org.apache.ibatis.annotations.Mapper; | ||
|
||
import java.util.List; | ||
|
||
@Mapper | ||
public interface StatisticsRepository { | ||
|
||
/** | ||
* 월별 주문 통계를 조회합니다. | ||
* <p> | ||
* 다음과 같은 통계를 월별로 조회합니다: | ||
* <ol> | ||
* <li>조회하는 연도와 월</li> | ||
* <li>주문 수량 총합</li> | ||
* <li>주문 총 금액</li> | ||
* <li>주문에 사용된 쿠폰 수량 총합</li> | ||
* <li>주문에 사용된 쿠폰 총 금액</li> | ||
* </ol> | ||
* | ||
* @param monthlyStatisticsParameterVo 월별 통계 조회 조건 | ||
* @return 월별 주문 통계 | ||
*/ | ||
List<MonthlyOrderStatisticsVo> getMonthlyOrderStatistics(final MonthlyStatisticsParameterVo monthlyStatisticsParameterVo); | ||
|
||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
생성자 파라미터로 null이 넘어와야하는 경우가 있나요? 만약 null이 넘어온다면 오류인 상황일 것으로 보이는데, 이 경우에는 명시적으로 에러를 throw해주는게 좋습니다. 이런식으로 단언문을 사용하기도 하니 참고해주세요~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분은 제가 조회에서 데이터 가져올때 어차피 1개월 기준으로 받아오니까 List가 아닌 객체로 받으면 되지 않을까 싶었는데요!
List가 아닌 객체로 받아오니까
해당하는 기간 범위에 데이터가 없을때
에 객체가 Null이 되버리는 문제가 있어서 추가했었습니다!그런데, List로 받아오면 애초에 null인 객체가 List에 추가되지 않아서 List로 반환하는걸로 확정 했습니다.
그래서 말씀해주신 Null 체크는 없애도록 하겠습니다! 더 좋은 코드 추천해주셔서 감사합니다~!