Skip to content

Commit

Permalink
[BE-48] Answer Read/Write CQRS 적용 (axon) #149 (#154)
Browse files Browse the repository at this point in the history
* [feat] : CQRS Mongo Async and Command

* [feat]: Query CQRS Refactoring ( JPA -> MoongoDB )

* [refactor]: remove unused entity and repository

* [feat]: Axon update query logging level disabled
  • Loading branch information
BlackBean99 authored Feb 12, 2024
1 parent 853357b commit 553ae09
Show file tree
Hide file tree
Showing 54 changed files with 713 additions and 602 deletions.
Binary file modified server/.gradle/7.6.1/checksums/checksums.lock
Binary file not shown.
Binary file modified server/.gradle/7.6.1/executionHistory/executionHistory.bin
Binary file not shown.
Binary file modified server/.gradle/7.6.1/executionHistory/executionHistory.lock
Binary file not shown.
Binary file modified server/.gradle/7.6.1/fileHashes/fileHashes.bin
Binary file not shown.
Binary file modified server/.gradle/7.6.1/fileHashes/fileHashes.lock
Binary file not shown.
Binary file modified server/.gradle/7.6.1/fileHashes/resourceHashesCache.bin
Binary file not shown.
Binary file modified server/.gradle/buildOutputCleanup/buildOutputCleanup.lock
Binary file not shown.
Binary file modified server/.gradle/file-system.probe
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.econovation.recruit.api.applicant.aggregate;

import static org.axonframework.modelling.command.AggregateLifecycle.apply;

import com.econovation.recruit.api.applicant.command.CreateAnswerCommand;
import com.econovation.recruitdomain.domains.applicant.domain.MongoAnswer;
import com.econovation.recruitdomain.domains.applicant.event.aggregateevent.AnswerCreatedEvent;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.axonframework.commandhandling.CommandHandler;
import org.axonframework.eventsourcing.EventSourcingHandler;
import org.axonframework.modelling.command.AggregateIdentifier;
import org.axonframework.spring.stereotype.Aggregate;

@AllArgsConstructor
@Getter
@Aggregate
@Slf4j
@NoArgsConstructor
public class AnswerAggregate {

@AggregateIdentifier private String id;
private Integer year;
private Map<String, Object> qna;
// Constructor for creating an AnswerAggregate
@CommandHandler
public AnswerAggregate(CreateAnswerCommand command) {

apply(new AnswerCreatedEvent(command.getId(), command.getYear(), command.getQna()));
}

// Event handler for AnswerCreatedEvent
@EventSourcingHandler
public void on(AnswerCreatedEvent event) {
this.id = event.getId();
this.year = event.getYear();
this.qna = event.getQna();
}

public static AnswerAggregate from(MongoAnswer answer) {
return new AnswerAggregate(answer.getId(), answer.getYear(), answer.getQna());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.econovation.recruit.api.applicant.aggregate;

import com.econovation.recruitdomain.domains.applicant.domain.MongoAnswer;
import com.econovation.recruitdomain.domains.applicant.domain.MongoAnswerAdaptor;
import com.econovation.recruitdomain.domains.applicant.event.aggregateevent.AnswerCreatedEvent;
import lombok.RequiredArgsConstructor;
import org.axonframework.eventhandling.EventHandler;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class AnswerCreatedEventListener {
private final MongoAnswerAdaptor answerAdaptor;

@EventHandler
public void handle(AnswerCreatedEvent event) {
MongoAnswer answer =
MongoAnswer.builder()
.id(event.getId())
.year(event.getYear())
.qna(event.getQna())
.build();
answerAdaptor.save(answer);

// email 전송 event처리
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.econovation.recruit.api.applicant.command;

import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.axonframework.modelling.command.TargetAggregateIdentifier;

@AllArgsConstructor
@ToString
@Data
@NoArgsConstructor
@Getter
public class CreateAnswerCommand {
@TargetAggregateIdentifier private String id;
private Integer year;
private Map<String, Object> qna;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
package com.econovation.recruit.api.applicant.controller;

import static com.econovation.recruitcommon.consts.RecruitStatic.APPLICANT_SUCCESS_REGISTER_MESSAGE;
import static com.econovation.recruitcommon.consts.RecruitStatic.QUESTION_SUCCESS_REGISTER_MESSAGE;

import com.econovation.recruit.api.applicant.command.CreateAnswerCommand;
import com.econovation.recruit.api.applicant.docs.CreateApplicantExceptionDocs;
import com.econovation.recruit.api.applicant.usecase.AnswerLoadUseCase;
import com.econovation.recruit.api.applicant.usecase.ApplicantRegisterUseCase;
import com.econovation.recruit.api.applicant.usecase.QuestionRegisterUseCase;
import com.econovation.recruit.api.applicant.dto.AnswersResponseDto;
import com.econovation.recruit.api.applicant.usecase.ApplicantQueryUseCase;
import com.econovation.recruit.api.applicant.usecase.TimeTableLoadUseCase;
import com.econovation.recruit.api.applicant.usecase.TimeTableRegisterUseCase;
import com.econovation.recruitcommon.annotation.ApiErrorExceptionsExample;
import com.econovation.recruitcommon.annotation.TimeTrace;
import com.econovation.recruitcommon.annotation.XssProtected;
import com.econovation.recruitdomain.domains.applicant.dto.BlockRequestDto;
import com.econovation.recruitdomain.domains.applicant.dto.TimeTableVo;
import com.econovation.recruitdomain.domains.applicant.exception.ApplicantOutOfDateException;
import com.econovation.recruitdomain.domains.dto.ApplicantPaginationResponseDto;
import com.econovation.recruitdomain.domains.dto.EmailSendDto;
import com.econovation.recruitdomain.domains.dto.QuestionRequestDto;
import com.econovation.recruitdomain.domains.timetable.domain.TimeTable;
import com.econovation.recruitinfrastructure.apache.CommonsEmailSender;
import io.swagger.v3.oas.annotations.Operation;
Expand All @@ -27,9 +24,9 @@
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.axonframework.commandhandling.gateway.CommandGateway;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
Expand All @@ -45,23 +42,48 @@
@Slf4j
@Tag(name = "[1.0]. 지원서 API", description = "지원서 관련 API")
public class ApplicantController {
private final ApplicantRegisterUseCase applicantRegisterUseCase;
private final TimeTableRegisterUseCase timeTableRegisterUseCase;
private final TimeTableLoadUseCase timeTableLoadUseCase;
private final QuestionRegisterUseCase questionRegisterUseCase;
private final AnswerLoadUseCase answerLoadUseCase;
private final ApplicantQueryUseCase applicantQueryUseCase;
private final CommonsEmailSender commonsEmailSender;
private final CommandGateway commandGateway;

@Operation(summary = "지원자가 지원서를 작성합니다.", description = "반환 값은 생성된 지원자의 ID입니다.")
@ApiErrorExceptionsExample(CreateApplicantExceptionDocs.class)
@XssProtected
@PostMapping("/applicants")
public ResponseEntity registerApplicant(
@RequestBody @Valid List<BlockRequestDto> blockElements) {
// validateOutdated();
UUID applicantId = applicantRegisterUseCase.execute(blockElements);
return new ResponseEntity<>(applicantId, HttpStatus.OK);
@TimeTrace
public ResponseEntity registerMongoApplicant(@RequestBody Map<String, Object> qna) {
// validateOutdated();
commandGateway.send(new CreateAnswerCommand(UUID.randomUUID().toString(), 21, qna));
return new ResponseEntity<>(APPLICANT_SUCCESS_REGISTER_MESSAGE, HttpStatus.OK);
}

@Operation(summary = "지원자 id로 지원서를 조회합니다.")
@TimeTrace
@ApiErrorExceptionsExample(CreateApplicantExceptionDocs.class)
@GetMapping("/applicants/{applicant-id}")
public ResponseEntity<Map<String, Object>> getApplicantById(
@PathVariable(value = "applicant-id") String applicantId) {
return new ResponseEntity<>(applicantQueryUseCase.execute(applicantId), HttpStatus.OK);
}

@Operation(summary = "지원자 기수로 지원서를 조회합니다. / page는 1부터 시작합니다.")
@TimeTrace
@GetMapping("/page/{page}/year/{year}/applicants")
public ResponseEntity<AnswersResponseDto> getApplicantsByYear(
@PathVariable(value = "year") Integer year,
@PathVariable(value = "page") Integer page) {
return new ResponseEntity<>(applicantQueryUseCase.execute(year, page), HttpStatus.OK);
}

@Operation(summary = "모든 지원자의 지원서를 조회합니다.")
@TimeTrace
@GetMapping("/applicants")
public ResponseEntity<List<Map<String, Object>>> getApplicants() {
return new ResponseEntity<>(applicantQueryUseCase.execute(), HttpStatus.OK);
}
// ------------------------------

private void validateOutdated() {
// 현재 한국 시간 가져오기
Expand All @@ -80,67 +102,43 @@ private void validateOutdated() {
}
}

@Operation(summary = "지원자 id로 지원서를 조회합니다.")
@GetMapping("/applicants/{applicant-id}")
public ResponseEntity<Map<String, String>> getApplicantById(
@PathVariable(value = "applicant-id") String applicantId) {
return new ResponseEntity<>(answerLoadUseCase.execute(applicantId), HttpStatus.OK);
}

@Operation(summary = "모든 지원자의 지원서를 조회합니다.")
@GetMapping("/applicants")
public ResponseEntity<List<Map<String, String>>> getApplicants() {
return new ResponseEntity<>(answerLoadUseCase.execute(), HttpStatus.OK);
}

@Operation(summary = "모든 지원자의 지원서를 페이지 단위로(1페이지당 8개) 조회합니다.")
@GetMapping("/page/{page}/applicants")
public ResponseEntity<ApplicantPaginationResponseDto> getApplicantsByPage(
// TODO 정렬 기준 추가
@PathVariable(value = "page") Integer page) {
return new ResponseEntity<>(answerLoadUseCase.execute(page), HttpStatus.OK);
}

@Operation(summary = "지원자가 면접 가능 시간을 작성합니다.")
@ApiErrorExceptionsExample(CreateApplicantExceptionDocs.class)
@TimeTrace
@PostMapping("/applicants/{applicant-id}/timetables")
public ResponseEntity registerApplicantTimeTable(
@PathVariable(value = "applicant-id") UUID applicantId,
@PathVariable(value = "applicant-id") String applicantId,
@RequestBody List<Integer> startTimes) {
timeTableRegisterUseCase.execute(applicantId.toString(), startTimes);
timeTableRegisterUseCase.execute(applicantId, startTimes);
return new ResponseEntity<>(APPLICANT_SUCCESS_REGISTER_MESSAGE, HttpStatus.OK);
}

@Operation(summary = "면접관이 면접 질문을 추가합니다.")
@PostMapping("/questions")
public ResponseEntity registerInterviewQuestion(
@RequestBody List<QuestionRequestDto> questions) {
questionRegisterUseCase.execute(questions);
return new ResponseEntity<>(QUESTION_SUCCESS_REGISTER_MESSAGE, HttpStatus.OK);
}

@Operation(summary = "모든 면접 가능 시간을 조회합니다.")
@TimeTrace
@GetMapping("/timetables")
public ResponseEntity<List<Map<String, List<TimeTableVo>>>> getTimeTables() {
return new ResponseEntity(timeTableLoadUseCase.findAll(), HttpStatus.OK);
}

@Operation(summary = "지원자의 면접 가능 시간을 조회합니다.")
@GetMapping("/applicants/{applicant-id}/timetables")
@TimeTrace
public ResponseEntity<List<TimeTable>> getTimeTables(
@PathVariable(name = "applicant-id") String applicantId) {
List<Integer> timeTableDto = timeTableLoadUseCase.getTimeTableByApplicantId(applicantId);
return new ResponseEntity(timeTableDto, HttpStatus.OK);
}

@Operation(summary = "면접 가능 시간마다 일치하는 지원자의 정보(희망분야, 이름)를 조회합니다.")
@TimeTrace
@GetMapping("/timetables/applicants")
public ResponseEntity<Map<Integer, List<String>>> getApplicantsByTimeTable() {
return new ResponseEntity(
timeTableLoadUseCase.findAllSimpleApplicantWithTimeTable(), HttpStatus.OK);
}

@Operation(summary = "지원서 제출한 html을 email 로 전송합니다.")
@TimeTrace
@PostMapping("/applicants/mail")
public ResponseEntity sendEmail(@RequestBody EmailSendDto emailSendDto) {
commonsEmailSender.send(emailSendDto.getEmail(), emailSendDto.getApplicantId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@
import com.econovation.recruitcommon.exception.RecruitCodeException;
import com.econovation.recruitcommon.interfaces.SwaggerExampleExceptions;
import com.econovation.recruitdomain.domains.applicant.exception.ApplicantDuplicateSubmitException;
import com.econovation.recruitdomain.domains.applicant.exception.ApplicantWrongPositionException;

@ExceptionDoc
public class CreateApplicantExceptionDocs implements SwaggerExampleExceptions {
@ExplainError("지원서를 중복으로 제출했을 경우")
public RecruitCodeException 지원서_중복_제출 = ApplicantDuplicateSubmitException.EXCEPTION;

@ExplainError("지원서를 잘못된 포지션으로 제출했을 경우")
public RecruitCodeException 지원서_지원_포지션_오류 = ApplicantWrongPositionException.EXCEPTION;

@ExplainError("지원서를 중복으로 제출했을 경우")
public RecruitCodeException 지원서_중복_제출_예외 = ApplicantDuplicateSubmitException.EXCEPTION;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.econovation.recruit.api.applicant.docs;

import com.econovation.recruitcommon.annotation.ExceptionDoc;
import com.econovation.recruitcommon.annotation.ExplainError;
import com.econovation.recruitcommon.exception.RecruitCodeException;
import com.econovation.recruitcommon.interfaces.SwaggerExampleExceptions;
import com.econovation.recruitdomain.domains.applicant.exception.ApplicantDuplicateSubmitException;

@ExceptionDoc
public class ReadApplicantExceptionDocs implements SwaggerExampleExceptions {
@ExplainError("지원서를 중복으로 제출했을 경우")
public RecruitCodeException 지원서_중복_제출_예외 = ApplicantDuplicateSubmitException.EXCEPTION;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.econovation.recruit.api.applicant.dto;

import com.econovation.recruit.utils.vo.PageInfo;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AnswersResponseDto {
private PageInfo pageInfo;
private List<Map<String, Object>> answers;

public static AnswersResponseDto of(List<Map<String, Object>> list, PageInfo pageInfo) {
return new AnswersResponseDto(pageInfo, list);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.econovation.recruit.api.applicant.handler;

import com.econovation.recruit.api.user.helper.NcpMailHelper;
import com.econovation.recruitdomain.domains.applicant.event.ApplicantRegisterEvent;
import com.econovation.recruitdomain.domains.applicant.event.domainevent.ApplicantRegisterEvent;
import com.econovation.recruitinfrastructure.apache.CommonsEmailSender;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.econovation.recruit.api.applicant.query;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AnswerQuery {
private String answerId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.econovation.recruit.api.applicant.query;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class AnswersByYearQuery {
private Integer year;
private Integer page;
}
Loading

0 comments on commit 553ae09

Please sign in to comment.