Skip to content
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

[feat] 추첨 로직 구현(#40) #43

Merged
merged 16 commits into from
Aug 9, 2024
Merged

Conversation

blaxsior
Copy link
Collaborator

@blaxsior blaxsior commented Aug 9, 2024

#️⃣ 연관 이슈

ex) #40

📝 작업 내용

  • 추첨 이벤트 당첨자 선정 로직 구현
  • 추첨 점수 계산기 구현
  • 당첨자 등록 기능 구현

참고 이미지 및 자료

spring MVC에서는 뷰를 생성할 때 message를 자동으로 주입해줬지만, api 서버로 사용할 때는 예외에 자동으로 주입해주지 않음. 따라서 messagesource를 주입받아 직접 변환. 좀 더 일관된 방식이 있다면 변경 예정
JPA로 추첨 이벤트 당첨 정보를 저장하기 위해서는 연관 객체인 EventUser를 객체로 가져와 연관 관계를 지정해야 하고, insertAll을 수행해도 단일 insert 여러번으로 처리된다. 이런 불필요한 작업을 제거하기 위해 JDBC Template 기반으로 bulk insert 기능을 구현한다.
차후 2개의 과정으로 분리할 수도 있음
@blaxsior blaxsior added the feat 기능 구현 label Aug 9, 2024
@blaxsior blaxsior self-assigned this Aug 9, 2024
@win-luck win-luck self-requested a review August 9, 2024 07:44
@win-luck win-luck linked an issue Aug 9, 2024 that may be closed by this pull request
3 tasks
Copy link
Collaborator

@win-luck win-luck left a comment

Choose a reason for hiding this comment

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

코로나로 컨디션이 엉망이라 온전하게 모든 과정을 이해하지는 못했지만, 적어도 사전 조사를 기반으로 세련된 설계를 하셨다는 점은 알 수 있었습니다!
선착순 이벤트와 함께 저희 프로젝트의 핵심 기능인 만큼, 저희의 고민과 결과, 다이어그램 등이 담긴 구현 전략 및 설계 과정을 Github Wiki에 구체적으로 담아보는 것도 좋을 것 같아요!

금요일에 정말 고생 많으셨습니다! 주말 푹 쉬세요:)

Comment on lines +16 to +21
if (count - maxPickCount <= 10 || // 두 값 차이가 작을 때
((double) count / maxPickCount) <= 1.1 ) {
return pickMany(items, maxPickCount);
} else {
return pickManyUsingSet(items, maxPickCount);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

사전조사에서 설계하셨던 내용을 여기에 담으신거군요~! 좋습니다

Comment on lines +23 to +33
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
EventUserScoreDto userScore = userScores.get(i);
ps.setLong(1, userScore.score());
ps.setLong(2, userScore.userId());
}

@Override
public int getBatchSize() {
return userScores.size();
}
Copy link
Collaborator

@win-luck win-luck Aug 9, 2024

Choose a reason for hiding this comment

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

Inner Method를 선택하신 이유가 있을까요??

Comment on lines +32 to +37
@Query(value = "SELECT c.event_user_id as eventUserId, COUNT(c.event_user_id) as count " +
"FROM comment c " +
"JOIN event_frame ef ON c.event_frame_id = ef.id " +
"JOIN event_metadata e ON ef.id = e.event_frame_id " +
"WHERE e.id = :eventRawId " +
"GROUP BY c.event_user_id " , nativeQuery = true)
Copy link
Collaborator

Choose a reason for hiding this comment

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

유저와 기대평 개수를 카운트해서 Dto로 바로 가져오는 로직이군요!

String code = e.getErrorCode();
String fieldName = e.getName();
Locale locale = LocaleContextHolder.getLocale(); // 현재 스레드의 로케일 정보를 가져온다.
String errorMessage = messageSource.getMessage(code, null, locale); // 국제화 된 메시지를 가져온다.
Copy link
Collaborator

Choose a reason for hiding this comment

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

국제화까지 신경써주셔서 감사합니다!

Comment on lines +21 to +28
public Map<Long, Long> calculate(long eventId, List<DrawEventScorePolicy> policies) {
Map<Long, Long> scoreMap = new HashMap<>();
for (var policy : policies) {
ActionHandler handler = handlerMap.get(policy.getAction());
handler.handle(scoreMap, eventId, policy.getScore());
}
return scoreMap;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

점수 계산을 별도로 분리해버리는 것은 좋은 전략인 것 같습니다.

@@ -14,7 +14,7 @@
@NoArgsConstructor
public class Admin {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@GeneratedValue(strategy = GenerationType.IDENTITY)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

초기 의도는 mysql AUTO_INCREMENT Primary Key를 이용하는 방식이었지만, 실제로는 다른 전략을 사용하고 있어 수정했습니다.

Map<String, String> errors = new HashMap<>();
errors.put(e.getName(), e.getLocalizedMessage());
return ResponseEntity.badRequest().body(errors);
public Map<String,String> handleInValidRequestException(MethodArgumentTypeMismatchException e) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Spring MVC 중 View를 사용하는 경우 메세지 소스로부터 메시지 코드를 치환해주지만, API 서버로 사용할 때는 예외 메시지에 대한 메시지를 치환해주지 않습니다. 따라서, 예외에 대한 메시징 기능은 MessageSource를 받아 직접 수정해야 합니다.

MethodArgumentTypeMismatchException은 query parameter / path variable에 대한 타입이 일치하지 않을 때 발생하는 예외입니다. 한국 이외의 국가에서도 현재 시스템을 이용할 수 있는 상황을 염두에 두고, Spring Message 으로 예외 메시지를 관리합니다.


// actionHandler을 Action 타입으로 가져올 수 있도록 설정
@Bean(name = "actionHandlerMap")
public Map<DrawEventAction, ActionHandler> actionHandlerMap(Map<String, ActionHandler> handlers) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

현재 추첨 이벤트의 점수 계산 로직은 하드코딩 된 로직 대신 확장 가능한 액션 ( 댓글 작성 등 ) 및 점수를 포함한 정책을 기반으로 유연하게 변경할 수 있는 구조로 설계되고 있습니다. 액션은 enum 타입으로 관리되고 있는데, 각 Action Handler을 문자열 대신 enum 타입으로 접근할 수 있도록 설정을 추가했습니다.


@Primary
@Component
public class AccSumBasedWinnerPicker implements WinnerPicker {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

추첨 로직은 다양할 수 있습니다. 현재는 누적 합 기반의 로직을 작성했지만, 추후 더 좋은 방법이 발견되는 경우 쉽게 전환할 수 있도록 WinnerPicker 명세와 누적합 방식의 구현 형태로 분리했습니다.

public List<PickTarget> pick(List<PickTarget> items, long count) {
long maxPickCount = Math.min(items.size(), count);
// TODO: 둘 중 하나를 선택하는 최적 조건 분석하기.
if (count - maxPickCount <= 10 || // 두 값 차이가 작을 때
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

조건에 따라 누적합 테이블을 매번 생성하거나, 한번 테이블을 만들고 추첨하는 방식 중에서 선택할 수 있게 구성했습니다. 구체적인 수치는 테스트를 통해 변경할 예정입니다.

* <p>component 이름은 action 이름 + ActionHandler 형식을 띄어야 한다.</p>
* <p>액션 = {@link hyundai.softeer.orange.event.draw.enums.DrawEventAction}</p>
*/
public interface ActionHandler {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

각 액션 / 정책에 대한 점수 채점을 수행하는 객체에 대한 명세입니다.

public void insertMany(List<DrawEventWinningInfoBulkInsertDto> targets) {
String sql = "insert into draw_event_winning_info (event_user_id, ranking, draw_event_id) VALUES (?, ?, ?)";

jdbcTemplate.batchUpdate(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Spring Data JPA을 직접 이용하면 mysql에서 Auto Increment Primary Key에 대해 각각 insert 가 발생하므로 DB 성능 측면에서 손해가 발생합니다. 대안으로 JDBC Template 또는 QueryDSL SQL이 있다는 것을 조사했고, 별도의 라이브러리 설치 및 깊은 학습이 필요하지 않은 JDBC 방식을 이용하기로 했습니다.

*/
@Transactional
@Async
public void draw(Long drawEventId) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

현재는 채점 + 추첨 로직을 동시에 진행하고 있지만, 대안에 따라 채점 + 추첨 로직을 별개로 분리하는 것도 염두에 두고 있습니다.

// 몇 등이 몇명이나 있는지 적혀 있는 정보. 등급끼리 정렬해서 1 ~ n 등 순서로 정렬
// 확률 높은 사람이 손해보면 안됨
List<DrawEventMetadata> metadataList = drawEvent.getMetadataList();
metadataList.sort(Comparator.comparing(DrawEventMetadata::getGrade));
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

현재 당첨자의 등수를 고려하지 않고 모두 뽑은 후 내부적으로 등수를 책정하는 방식을 이용하고 있습니다. 이때, 등수 정보가 정렬되지 않으면 가산점이 높은 사람이 오히려 낮은 상품을 받아 손해를 볼 여지가 있습니다. 데이터베이스에서 항상 정렬된 상태로 전달된다는 보장이 없으므로 코드 수준에서 정렬을 수행합니다.

import static org.assertj.core.api.Assertions.assertThat;

// 매 테스트마다 초기화하는 코드 찾아봐야 할듯?
@Sql(value = "classpath:sql/CommentRepositoryTest.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_CLASS)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

테스팅에 필요한 db 데이터를 별도의 파일로 분리하고 어노테이션 기반으로 가져오도록 구성하여, 테스트 코드 수준에서 Jdbc 등을 활용하여 입력하는 보일러 플레이트 코드를 없애고 동일 db 데이터를 여러 테스트 코드에서 공유할 수 있게 했습니다.

@blaxsior blaxsior merged commit 29df496 into dev Aug 9, 2024
1 check passed
@win-luck win-luck deleted the feature/40-draw-event-logic branch August 13, 2024 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat 기능 구현
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[feat] 추첨 로직 구현(#40)
2 participants