Hearus 프로젝트의 마일스톤을 위와 같이 설정하였다.
공개SW 개발자대회 1차 출품을 위해서 총 10주의 시간이 주어지는데, SPRINT 5까지의 시간동안 MVP 모델의 1차적인 개발을 마치고 배포를 통한 중간점검을 수행할 예정이다.
또한 비교적 무거운 SpringBoot를 사용하고 있기 때문에 FE와의 협업을 강화하기 위하여 Postman Workspace를 활용하였다.
Workspace 팀을 생성하고, BE 팀원을 초대한 이후 기존의 API들에 대한 exmaple
들을 생성해주어 Mock Server를 생성하고 이를 FE 팀원들에게 전달하였다.
테스트, 즉 Problem의 경우 위와 같으 화면에서 총 4개의 유형으로 생성할 수 있도록 디자인되었다.
또한 위는 실제로 문제를 사용자에게 제공하는 화면으로
해당 디자인에 기반하여 BE의 데이터를 구조화하고 제공할 예정이다.
/lecture
라우트의 경우 위와 같이 총 9개의 API로 구성되며
강의에 관한 데이터를 저장하는 Lecture
Collection은 MongoDb에 위와 같이 구성된다.
또한 Problem들의 경우 각 Lecture 하위에 고유한 ID를 가지며 Array 형태로 저장되어
추후 Lang Chain을 통해 문제를 생성하고 저장할 때의 유연성을 향상시켰다.
또한 MongoDB의 경우에는 기존 Express.js
로 개발할 때의 DB를 그대로 사용한다.
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
Spring Boot에 위와 같이 spring-boot-starter-data-mongodb
라이브러리를 import하고
# application-private.properties
# MongoDB
# `mongodb+srv://<유저이름>:<비밀번호>@<클러스터이름>.ergif.mongodb.net/<데이터베이스이름>?retryWrites=true&w=majority`
spring.data.mongodb.uri=mongodb+srv://<>:<>@<>...mongodb.net/<>?retryWrites=true&w=majority&appName=allkul
MongoDB 연결을 위한 URI를 설정하면 정상적으로 클러스터에 연결되는 모습을 볼 수 있다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
// Document 어노테이션은 해당 클래스가 MongoDB에 저장될 문서임을 나타냄
@Document(collection = "lecture")
public class LectureModel {
@Id
@Column(unique = true)
private String id;
@Column(unique = true)
private String name;
private List<String> processedScript;
private String scheduleElementId;
private Date lectureDate;
private Date createdAt;
private List<Problem> problems;
public void addProblem(Problem problem) {
if (problems == null) {
problems = new ArrayList<>();
}
problem.setId(UUID.randomUUID().toString());
problems.add(problem);
}
public void updateProblem(String problemId, Problem newProblem) {
if (problems != null) {
for (int i = 0; i < problems.size(); i++) {
Problem problem = problems.get(i);
if (problem.getId().equals(problemId)) {
newProblem.setId(problemId);
problems.set(i, newProblem);
break;
}
}
}
}
public void deleteProblem(String problemId) {
if (problems != null) {
problems.removeIf(problem -> problem.getId().equals(problemId));
}
}
}
LectureModel
은 위와 같이 구현되었으며 Problem들과 관련된 별도의 메소드들을 두어 Problem List를 효율적으로 관리할 수 있게 하였다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Problem {
@Id
@Column(unique = true)
private String id;
private String type;
private String direction;
private List<String> options;
private String answer;
}
LectureModel
과 Problem
Class는 위와 같이 구현되었으며 Problem들과 관련된 별도의 메소드들을 두어 Problem List를 효율적으로 관리할 수 있게 하였다.
public interface LectureRepository extends MongoRepository<LectureModel, String> {
// READ
List<LectureModel> findAll();
LectureModel findFirstById(String id);
LectureModel findFirstByName(String id);
boolean existsById(String id);
boolean existsByName(String name);
// DELETE
void deleteById(String id);
}
@Slf4j
@Service
public class LectureDAOImpl implements LectureDAO {
UserRepository userRepository;
ScheduleElementRepository scheduleElementRepository;
LectureRepository lectureRepository;
@Autowired
public LectureDAOImpl(ScheduleElementRepository scheduleElementRepository, UserRepository userRepository, LectureRepository lectureRepository) {
this.scheduleElementRepository = scheduleElementRepository;
this.userRepository = userRepository;
this.lectureRepository = lectureRepository;
}
@Override
@Transactional
public CommonResponse addLecture(String userId, LectureModel lecture) {
try {
if(lectureRepository.existsByName(lecture.getName()))
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Lecture name already exists");
lecture.setCreatedAt(new Date());
LectureModel savedLecture = lectureRepository.save(lecture);
// User의 savedLectures에 저장된 강의 ID 추가
UserEntity user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));
// UserRepository에 새로운 SavedLectures 추가
List<String> newSavedLectures = user.getSavedLectures();
if (newSavedLectures == null) {
newSavedLectures = new ArrayList<>();
}
newSavedLectures.add(savedLecture.getId());
user.setSavedLectures(newSavedLectures);
UserEntity updatedUser = userRepository.save(user);
log.info("[LectureDAOImpl]-[addLecture] User {} SavedLecturesSize {}", userId, updatedUser.getSavedLectures().size());
return CommonResponse.builder()
.status(HttpStatus.OK)
.isSuccess(true)
.msg("Lecture added successfully")
.object(savedLecture)
.build();
} catch (Exception e) {
log.error("Failed to add lecture", e);
return CommonResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.isSuccess(false)
.msg("Failed to add lecture")
.build();
}
}
@Override
public CommonResponse putScript(String lectureId, String script) {
try{
LectureModel lecture = lectureRepository.findFirstById(lectureId);
if(lecture == null)
return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
lecture.getProcessedScript().add(script);
LectureModel savedLecture = lectureRepository.save(lecture);
return new CommonResponse(true, HttpStatus.OK, "Lecture ProcessedScript Added", savedLecture);
}catch (Exception e){
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to put Script");
}
}
@Override
public CommonResponse updateLecture(LectureModel lecture) {
try{
if(lecture == null)
return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
LectureModel updatedLecture = lectureRepository.save(lecture);
return new CommonResponse(true, HttpStatus.OK, "Lecture Updated", updatedLecture);
}catch (Exception e){
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to put Script");
}
}
@Transactional
@Override
public CommonResponse deleteLecture(String userId, String lectureId) {
try {
lectureRepository.deleteById(lectureId);
UserEntity user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));
List<String> newSavedLectures = user.getSavedLectures();
newSavedLectures.remove(lectureId);
user.setSavedLectures(newSavedLectures);
UserEntity updatedUser = userRepository.save(user);
log.info("[LectureDAOImpl]-[deleteLecture] User {} SavedLecturesSize {}", userId, updatedUser.getSavedLectures().size());
return new CommonResponse(true, HttpStatus.OK, "Lecture Deleted successfully");
} catch (Exception e) {
log.error("Failed to delete lecture", e);
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete lecture");
}
}
@Override
public CommonResponse addProblem(String lectureId, Problem problem) {
try{
LectureModel lecture = lectureRepository.findFirstById(lectureId);
if(lecture == null)
return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
lecture.addProblem(problem);
LectureModel updatedLecture = lectureRepository.save(lecture);
return new CommonResponse(true, HttpStatus.OK, "Problem Added", updatedLecture.getProblems());
}catch (Exception e){
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to add Problem");
}
}
@Override
public CommonResponse updateProblem(String lectureId, String problemId, Problem newProblem) {
try{
LectureModel lecture = lectureRepository.findFirstById(lectureId);
if(lecture == null)
return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
lecture.updateProblem(problemId, newProblem);
LectureModel updatedLecture = lectureRepository.save(lecture);
return new CommonResponse(true, HttpStatus.OK, "Lecture Updated", updatedLecture.getProblems());
}catch (Exception e){
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to update Problem");
}
}
@Override
public CommonResponse deleteProblem(String lectureId, String problemId) {
try{
LectureModel lecture = lectureRepository.findFirstById(lectureId);
if(lecture == null)
return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
lecture.deleteProblem(problemId);
LectureModel updatedLecture = lectureRepository.save(lecture);
return new CommonResponse(true, HttpStatus.OK, "Lecture Updated", updatedLecture.getProblems());
}catch (Exception e){
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to delete Problem");
}
}
@Override
public CommonResponse getLecture(String lectureId) {
try{
LectureModel lecture = lectureRepository.findFirstById(lectureId);
if(lecture == null)
return new CommonResponse(false, HttpStatus.NOT_FOUND, "Lecture doesn't exists");
return new CommonResponse(true, HttpStatus.OK, "LectureModel", lecture);
}catch (Exception e){
return new CommonResponse(false, HttpStatus.INTERNAL_SERVER_ERROR, "Failed to get Lecture");
}
}
}
LectureDAO
에서는 MongoDB의 데이터들과 MariaDB의 데이터들 사이의 관계를 정의하기 위한 여러 메소드들과 Lecture Model내의 Problem과 관련된 여러 메소드들을 위와 같이 정의하였다.
@Service
public class LectureServiceImpl implements LectureService {
@Autowired
private LectureDAOImpl lectureDAO;
@Override
public CommonResponse addLecture(String userId, LectureModel lecture) {
return lectureDAO.addLecture(userId, lecture);
}
@Override
public CommonResponse putScript(String lectureId, String script) {
return lectureDAO.putScript(lectureId, script);
}
@Override
public CommonResponse updateLecture(LectureModel lecture) {
return lectureDAO.updateLecture(lecture);
}
@Override
public CommonResponse deleteLecture(String userId, String lectureId) {
return lectureDAO.deleteLecture(userId, lectureId);
}
@Override
public CommonResponse addProblem(String lectureId, Problem problem) {
return lectureDAO.addProblem(lectureId, problem);
}
@Override
public CommonResponse updateProblem(String lectureId, String problemId, Problem newProblem) {
return lectureDAO.updateProblem(lectureId, problemId, newProblem);
}
@Override
public CommonResponse deleteProblem(String lectureId, String problemId) {
return lectureDAO.deleteProblem(lectureId, problemId);
}
@Override
public CommonResponse getLecture(String lectureId) {
return lectureDAO.getLecture(lectureId);
}
}
@Slf4j
@RestController
@RequestMapping("/api/v1/lecture")
public class LectureController {
@Autowired
private LectureService lectureService;
private CommonResponse response;
private String getUserIdFromContext(){
// SecurityContext에서 Authentication으로 UserID를 받아온다
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return (String) authentication.getPrincipal();
}
@PostMapping(value="/addLecture")
public ResponseEntity<CommonResponse> addSchedule(@Valid @RequestBody LectureModel lectureModel){
log.info("[LectureController]-[addLecture] API Call");
if(lectureModel.getScheduleElementId().isEmpty() || lectureModel.getName().isEmpty()){
log.warn("[LectureController]-[addLecture] Failed : Empty Variables");
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Name");
return ResponseEntity.status(response.getStatus()).body(response);
}
String userId = getUserIdFromContext();
response = lectureService.addLecture(userId, lectureModel);
return ResponseEntity.status(response.getStatus()).body(response);
}
@PutMapping(value="/putScript")
public ResponseEntity<CommonResponse> putScript(@Valid @RequestBody Map<String, Object> requestBody){
ObjectMapper objectMapper = new ObjectMapper();
String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
String script = objectMapper.convertValue(requestBody.get("script"), String.class);
log.info("[LectureController]-[putScript] API Call - LectureId : {}", lectureId);
if(lectureId.isEmpty() || script.isEmpty()){
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Variables");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.putScript(lectureId, script);
return ResponseEntity.status(response.getStatus()).body(response);
}
@PutMapping(value="/updateLecture")
public ResponseEntity<CommonResponse> updateLecture(@Valid @RequestBody LectureModel lectureModel){
log.info("[LectureController]-[updateLecture] API Call");
if(lectureModel.getId().isEmpty()){
log.warn("[LectureController]-[updateLecture] Failed : Empty Variables");
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Id");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.updateLecture(lectureModel);
return ResponseEntity.status(response.getStatus()).body(response);
}
@DeleteMapping(value="/deleteLecture")
public ResponseEntity<CommonResponse> deleteLecture(@Valid @RequestBody Map<String, String> requestBody){
log.info("[LectureController]-[deleteLecture] API Call");
ObjectMapper objectMapper = new ObjectMapper();
String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
if(lectureId.isEmpty()){
log.warn("[LectureController]-[deleteLecture] Failed : Empty LectureId");
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.deleteLecture(getUserIdFromContext(), lectureId);
return ResponseEntity.status(response.getStatus()).body(response);
}
@PostMapping(value="/addProblem")
public ResponseEntity<CommonResponse> addProblem(@Valid @RequestBody Map<String, Object> requestBody){
log.info("[LectureController]-[addProblem] API Call");
ObjectMapper objectMapper = new ObjectMapper();
String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
Problem problem = objectMapper.convertValue(requestBody.get("problem"), Problem.class);
if(lectureId.isEmpty()){
log.warn("[LectureController]-[addLecture] Failed : Empty LectureId");
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.addProblem(lectureId, problem);
return ResponseEntity.status(response.getStatus()).body(response);
}
@PutMapping(value="/updateProblem")
public ResponseEntity<CommonResponse> updateProblem(@Valid @RequestBody Map<String, Object> requestBody){
log.info("[LectureController]-[addProblem] API Call");
ObjectMapper objectMapper = new ObjectMapper();
String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
Problem newProblem = objectMapper.convertValue(requestBody.get("problem"), Problem.class);
if(lectureId.isEmpty()){
log.warn("[LectureController]-[addLecture] Failed : Empty LectureId");
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.updateProblem(lectureId, newProblem.getId(), newProblem);
return ResponseEntity.status(response.getStatus()).body(response);
}
@DeleteMapping(value="/deleteProblem")
public ResponseEntity<CommonResponse> deleteProblem(@Valid @RequestBody Map<String, String> requestBody){
log.info("[LectureController]-[addProblem] API Call");
ObjectMapper objectMapper = new ObjectMapper();
String lectureId = objectMapper.convertValue(requestBody.get("lectureId"), String.class);
String problemId = objectMapper.convertValue(requestBody.get("problemId"), String.class);
if(lectureId.isEmpty() || problemId.isEmpty()){
log.warn("[LectureController]-[addLecture] Failed : Empty Variables");
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty Variables");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.deleteProblem(lectureId, problemId);
return ResponseEntity.status(response.getStatus()).body(response);
}
@GetMapping("/getLecture")
public ResponseEntity<CommonResponse> getLecture(@RequestParam("lectureId") String lectureId) {
log.info("[LectureController]-[getLecture] API Call");
if(lectureId.isEmpty()){
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.getLecture(lectureId);
return ResponseEntity.status(response.getStatus()).body(response);
}
@GetMapping("/getProblem")
public ResponseEntity<CommonResponse> getProblem(@RequestParam("lectureId") String lectureId) {
log.info("[LectureController]-[getLecture] API Call");
if(lectureId.isEmpty()){
response = new CommonResponse(false, HttpStatus.BAD_REQUEST,"Empty LectureId");
return ResponseEntity.status(response.getStatus()).body(response);
}
response = lectureService.getLecture(lectureId);
LectureModel lecture = (LectureModel) response.getObject();
response.setObject(lecture.getProblems());
return ResponseEntity.status(response.getStatus()).body(response);
}
}
LectureController
에서는 기존 API 명세서에서 정의한 여러 API들을 구현하였다.
다양한 타입, 조건의 Request Body로 구성되므로 ObjectMapper
를 통해 JSON 형태의 Req Body에서 여러 필드 값들을 추출하여 사용할 수 있도록 정의하였다.
새로운 Lecture를 정상적으로 생성하는 것을 볼 수 있으며
같은 이름의 Lecture를 생성했을 때 위와 같이 실패하는 것을 볼 수 있다.
STT 처리가 완료된 Script를 기존의 Lecture에 정상적으로 추가하는 것을 볼 수 있고
MongoDB에도 해당 결과가 정상적으로 반영되는 것을 볼 수 있다.
Lecutere의 스크립트를 업데이트하거나, Date를 업데이트할 때 사용하는 API로 정상적으로 UPDATE 요청한 결과가 MongoDB에도 정상적으로 반영되는 것을 볼 수 있다.
먼저 /deleteLecture
를 테스트하기 위하여 Dummy Lecture Data를 위와 같이 추가하면 위와 같이 서버의 로그에 현재 몇개의 LectureID가 User
테이블에 존재하는지 확인할 수 있다. 또한 MongoDB에 새로운 Dummy Lecture가 추가된 것을 볼 수 있으며 위와 같이 LectureId에 대한 DELETE 요청을 보내면
Lecture가 삭제되고 Server의 로그에도 정상적으로 User
테이블에서 삭제된 것을 볼 수 있다.
위와 같이 문제를 추가하는 요청을 보내면
MongoDB에서 문제가 정상적으로 추가된 것을 확인할 수 있다.
LectureModel의 메소드로 생성된 Problem의 ID를 담아 문제를 수정하는 요청을 전송하게되면
위와 같이 기존의 Index
위치로 문제가 수정된 것을 볼 수 있다.
위와 같이 새로운 Dummy 문제를 생성한 이후
해당 문제의 ID와 LectureId를 통해 DELETE를 요청하면
문제가 정상적으로 삭제되는 것을 확인할 수 있다.
Lecture의 아이디를 파라미터로 하여 GET 요청을 보내면 강의 정보를 JSON 형태로 전달받을 수 있다.
Lecture의 아이디를 파라미터로 하여 GET 요청을 보내면 위처럼 문제만 제공받을 수 있도록 API를 구현하였다.