이번 포스팅에서는 `StudyWithMe`의 스터디 룸 Update API를 구현하면서 생각한 고민들에 관해 포스팅 하려고 한다.
설명하기에 앞서,
아니 UPDATE API를 작성하는데 고민을 왜해? 그냥 하면 되는거 아니야?
라고 생각할 수 있다. 물론, 맞는 말이다.
하지만, 스터디 룸과 연관되어 있는 테이블들이 굉장히 많다보니 이런 고민이 생기게 되었다.
스터디 룸 연관 테이블
- 스터디 룸 이미지
- 스터디 룸 태그
- 스터디 룸 휴무일
- 스터디 룸 옵션
- 스터디 룸 타입
- 스터디 룸 예약 조건
과 같은데 이걸 하나의 API에서 다 처리하자니 코드도 길어지고, 해당 API가 너무 무겁다는 생각이 들었다.
하나의 Update API로 전부 처리
우선, 하나의 API로 처리하려고 했던 코드의 내용은 아래와 같다.(작성하다가 포기했다... 너무 길어서)
(코드를 읽어보실 분들에게 설명드리자면, 몇개만 구현되어있을 뿐 전체 다 구현하려면 코드가 너무나도 길어진다.)
하나의 Update API로 처리
@Transactional
public void update(StudyRoomUpdateRequest request, UUID userId){
StudyRoom studyRoom = validateStudyRoomWithUserId(request.getStudyRoomId(), userId);
studyRoom.modifyStudyRoom(request);
updateStudyRoomInfo(request, studyRoom);
}
private void updateStudyRoomInfo(StudyRoomUpdateRequest request, StudyRoom studyRoom) {
// 각 수정 작업을 담당하는 서비스로 위임
applyModificationLogic(request.getImageModification(), studyRoom, this::imageModifyLogic);
applyModificationLogic(request.getTagModification(), studyRoom, this::tagModifyLogic);
applyModificationLogic(request.getDayOffModification(), studyRoom, this::dayOffModifyLogic);
applyModificationLogic(request.getOptionModification(), studyRoom, this::optionModifyLogic);
applyModificationLogic(request.getTypeModification(), studyRoom, this::typeModifyLogic);
applyModificationLogic(request.getReservationTypeModification(), studyRoom, this::reserveTypeModifyLogic);
}
private <T> void applyModificationLogic(T modification, StudyRoom studyRoom, BiConsumer<T, StudyRoom> modifyLogic) {
if (modification != null) {
modifyLogic.accept(modification, studyRoom);
}
}
private void imageModifyLogic(StudyRoomImageModifyRequest imageModification, StudyRoom studyRoom) {
if(imageModification.getImagesToAdd() != null && !imageModification.getImagesToAdd().isEmpty()) {
addImages(imageModification.getImagesToAdd(), studyRoom);
}
if(imageModification.getImagesToUpdate() != null && !imageModification.getImagesToUpdate().isEmpty()) {
updateImages(imageModification.getImagesToUpdate(), studyRoom);
}
if(imageModification.getImageIdsToRemove() != null && !imageModification.getImageIdsToRemove().isEmpty()) {
removeImages(imageModification.getImageIdsToRemove(), studyRoom);
}
}
private void tagModifyLogic(StudyRoomTagModifyRequest tagModification, StudyRoom studyRoom) {
if(tagModification.getTagsToAdd() != null && !tagModification.getTagsToAdd().isEmpty()) {
addTags(tagModification.getTagsToAdd(), studyRoom);
}
if(tagModification.getTagsToUpdate() != null && !tagModification.getTagsToUpdate().isEmpty()) {
updateTags(tagModification.getTagsToUpdate(), studyRoom);
}
if(tagModification.getTagIdsToRemove() != null && !tagModification.getTagIdsToRemove().isEmpty()) {
removeTags(tagModification.getTagIdsToRemove(), studyRoom);
}
}
private void dayOffModifyLogic(StudyRoomDayOffModifyRequest dayOffModification, StudyRoom studyRoom) {
if(dayOffModification.getDayOffsToAdd() != null && !dayOffModification.getDayOffsToAdd().isEmpty()) {
addDayOffs(dayOffModification.getDayOffsToAdd(), studyRoom);
}
if(dayOffModification.getDayOffsToUpdate() != null && !dayOffModification.getDayOffsToUpdate().isEmpty()) {
updateDayOffs(dayOffModification.getDayOffsToUpdate(), studyRoom);
}
if(dayOffModification.getDayOffIdsToRemove() != null && !dayOffModification.getDayOffIdsToRemove().isEmpty()) {
removeDayOffs(dayOffModification.getDayOffIdsToRemove(), studyRoom);
}
}
private void optionModifyLogic(StudyRoomOptionModifyRequest optionModification, StudyRoom studyRoom) {
if(optionModification.getOptionsToAdd() != null && !optionModification.getOptionsToAdd().isEmpty()) {
addOptions(optionModification.getOptionsToAdd(), studyRoom);
}
if(optionModification.getOptionsToUpdate() != null && !optionModification.getOptionsToUpdate().isEmpty()) {
updateOptions(optionModification.getOptionsToUpdate(), studyRoom);
}
if(optionModification.getOptionIdsToRemove() != null && !optionModification.getOptionIdsToRemove().isEmpty()) {
removeOptions(optionModification.getOptionIdsToRemove(), studyRoom);
}
}
private void typeModifyLogic(StudyRoomTypeModifyRequest typeModification, StudyRoom studyRoom) {
if(typeModification.getTypesToAdd() != null && !typeModification.getTypesToAdd().isEmpty()) {
addTypes(typeModification.getTypesToAdd(), studyRoom);
}
if(typeModification.getTypesToUpdate() != null && !typeModification.getTypesToUpdate().isEmpty()) {
updateTypes(typeModification.getTypesToUpdate(), studyRoom);
}
if(typeModification.getTypeIdsToRemove() != null && !typeModification.getTypeIdsToRemove().isEmpty()) {
removeTypes(typeModification.getTypeIdsToRemove(), studyRoom);
}
}
private void reserveTypeModifyLogic(StudyRoomReservationTypeModifyRequest reservationTypeModification, StudyRoom studyRoom) {
if(reservationTypeModification.getReservationTypesToAdd() != null && !reservationTypeModification.getReservationTypesToAdd().isEmpty()) {
addReservationTypes(reservationTypeModification.getReservationTypesToAdd(), studyRoom);
}
if(reservationTypeModification.getReservationTypesToUpdate() != null && !reservationTypeModification.getReservationTypesToUpdate().isEmpty()) {
updateReservationTypes(reservationTypeModification.getReservationTypesToUpdate(), studyRoom);
}
if(reservationTypeModification.getReservationTypeIdsToRemove() != null && !reservationTypeModification.getReservationTypeIdsToRemove().isEmpty()) {
removeReservationTypes(reservationTypeModification.getReservationTypeIdsToRemove(), studyRoom);
}
}
private void addImages(List<String> imagesToAdd, StudyRoom studyRoom) {
studyRoomImageRepository.batchInsert(imagesToAdd, studyRoom);
}
private void updateImages(List<StudyRoomImageUpdateRequest> imagesToUpdate, StudyRoom studyRoom) {
Map<Long, String> imageMap = new HashMap<>();
List<Long> imageIds = new ArrayList<>();
for (StudyRoomImageUpdateRequest imageToUpdate : imagesToUpdate) {
imageMap.put(imageToUpdate.getImageId(), imageToUpdate.getImageUrl());
imageIds.add(imageToUpdate.getImageId());
}
List<StudyRoomImage> studyRoomImages = studyRoomImageRepository.findAllByIdInAndStudyRoom(imageIds, studyRoom);
if (studyRoomImages.size() != imagesToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
studyRoomImages.forEach(studyRoomImage ->
studyRoomImage.modifyImageUrl(imageMap.get(studyRoomImage.getId()))
);
}
private void removeImages(List<Long> imageIdsToRemove, StudyRoom studyRoom) {
int size = studyRoomImageRepository.countStudyRoomImageByIdInAndStudyRoom(imageIdsToRemove, studyRoom);
if(size == imageIdsToRemove.size()){
studyRoomImageRepository.deleteAllByIdInBatch(imageIdsToRemove);
} else{
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
}
다 작성하지 않았는데도 이렇게나 길다...
그래서 카카오톡 오픈채팅방에 계시는 백엔드 고수님들에게 자문을 구해보았다.
음.. 확실히 서로에게 큰 영향이 가는 데이터도 아니었고,
수정 API만을 위해 각각의 테이블에 대한 `Controller`와 `Service`를 만드는 것도 좀 과처리라는 생각에 도달했다.
그래서 아래와 같이 각 테이블 별로 Update API를 나누는 대신,
똑같이 `StudyRoomCommandService`로 처리했다.
각 테이블 별 Update API로 처리
@Slf4j
@Service
@RequiredArgsConstructor
public class StudyRoomCommandService {
private final StudyRoomRepository studyRoomRepository;
private final UserRepository userRepository;
private final StudyRoomDayOffRepository dayOffRepository;
private final StudyRoomImageRepository imageRepository;
private final StudyRoomOptionInfoRepository optionInfoRepository;
private final StudyRoomTypeInfoRepository typeInfoRepository;
private final StudyRoomReserveTypeRepository reserveTypeRepository;
private final StudyRoomTagRepository tagRepository;
@Transactional
public void create(StudyRoomCreateRequest request, UUID userId){
User user = validateRoomAdmin(userId);
StudyRoom studyRoom = StudyRoom.of(request);
studyRoom.modifyRoomAdmin(user);
studyRoom = studyRoomRepository.save(studyRoom);
createAllOfStudyRoomRelatedInfo(request, studyRoom);
}
private void createAllOfStudyRoomRelatedInfo(StudyRoomCreateRequest request, StudyRoom studyRoom) {
validateDayOffs(request.getDayOffs(), studyRoom);
validateTags(request.getTags(), studyRoom);
imageRepository.batchInsert(request.getImageUrls(), studyRoom);
optionInfoRepository.batchInsert(request.getOptions(), studyRoom);
typeInfoRepository.batchInsert(request.getTypes(), studyRoom);
reserveTypeRepository.batchInsert(request.getReservationTypes(), studyRoom);
}
@Transactional
public void update(StudyRoomUpdateRequest request, UUID userId){
StudyRoom studyRoom = validateStudyRoomWithUserId(request.getStudyRoomId(), userId);
studyRoom.modifyStudyRoom(request);
imageModifyLogic(request.getImageModification(), studyRoom);
}
@Transactional
public void updateTag(StudyRoomTagModifyRequest request, UUID userId) {
StudyRoom studyRoom = validateStudyRoomWithUserId(request.getStudyRoomId(), userId);
tagModifyLogic(request, studyRoom);
}
@Transactional
public void updateDayOff(StudyRoomDayOffModifyRequest request, UUID userId) {
StudyRoom studyRoom = validateStudyRoomWithUserId(request.getStudyRoomId(), userId);
dayOffModifyLogic(request, studyRoom);
}
@Transactional
public void updateOption(StudyRoomOptionInfoModifyRequest request, UUID userId) {
StudyRoom studyRoom = validateStudyRoomWithUserId(request.getStudyRoomId(), userId);
optionModifyLogic(request, studyRoom);
}
@Transactional
public void updateType(StudyRoomTypeInfoModifyRequest request, UUID userId) {
StudyRoom studyRoom = validateStudyRoomWithUserId(request.getStudyRoomId(), userId);
typeModifyLogic(request, studyRoom);
}
@Transactional
public void updateReserveType(StudyRoomReservationTypeModifyRequest request, UUID userId) {
StudyRoom studyRoom = validateStudyRoomWithUserId(request.getStudyRoomId(), userId);
reserveTypeModifyLogic(request, studyRoom);
}
private void imageModifyLogic(StudyRoomImageModifyRequest imageModification, StudyRoom studyRoom) {
if (imageModification != null) {
if (imageModification.getImagesToAdd() != null && !imageModification.getImagesToAdd().isEmpty())
imageRepository.batchInsert(imageModification.getImagesToAdd(), studyRoom);
if (imageModification.getImagesToUpdate() != null && !imageModification.getImagesToUpdate().isEmpty())
updateImages(imageModification.getImagesToUpdate(), studyRoom);
if (imageModification.getImageIdsToRemove() != null && !imageModification.getImageIdsToRemove().isEmpty())
removeImages(imageModification.getImageIdsToRemove(), studyRoom);
}
}
private void updateImages(List<StudyRoomImageUpdateRequest> imagesToUpdate, StudyRoom studyRoom) {
Map<Long, String> imageMap = new HashMap<>();
List<Long> imageIds = new ArrayList<>();
for (StudyRoomImageUpdateRequest imageToUpdate : imagesToUpdate) {
imageMap.put(imageToUpdate.getImageId(), imageToUpdate.getImageUrl());
imageIds.add(imageToUpdate.getImageId());
}
List<StudyRoomImage> studyRoomImages = imageRepository.findAllByIdInAndStudyRoom(imageIds, studyRoom);
if (studyRoomImages.size() != imagesToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
studyRoomImages.forEach(studyRoomImage ->
studyRoomImage.modifyImageUrl(imageMap.get(studyRoomImage.getId()))
);
}
private void removeImages(List<Long> imageIdsToRemove, StudyRoom studyRoom) {
int size = imageRepository.countStudyRoomImageByIdInAndStudyRoom(imageIdsToRemove, studyRoom);
if(size == imageIdsToRemove.size()){
imageRepository.deleteAllByIdInBatch(imageIdsToRemove);
} else{
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
}
private void tagModifyLogic(StudyRoomTagModifyRequest request, StudyRoom studyRoom) {
if (request.getTagsToAdd() != null && !request.getTagsToAdd().isEmpty())
tagRepository.batchInsert(request.getTagsToAdd(), studyRoom);
if (request.getTagsToUpdate() != null && !request.getTagsToUpdate().isEmpty())
updateTags(request.getTagsToUpdate(), studyRoom);
if (request.getTagIdsToRemove() != null && !request.getTagIdsToRemove().isEmpty())
removeTags(request.getTagIdsToRemove(), studyRoom);
}
private void updateTags(List<StudyRoomTagUpdateRequest> tagsToUpdate, StudyRoom studyRoom) {
Map<Long, String> tagMap = new HashMap<>();
List<Long> tagIds = new ArrayList<>();
for (StudyRoomTagUpdateRequest tagToUpdate : tagsToUpdate) {
tagMap.put(tagToUpdate.getTagId(), tagToUpdate.getTag());
tagIds.add(tagToUpdate.getTagId());
}
List<StudyRoomTag> studyRoomTags = tagRepository.findAllByIdInAndStudyRoom(tagIds, studyRoom);
if (studyRoomTags.size() != tagsToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
studyRoomTags.forEach(studyRoomTag ->
studyRoomTag.modifyTag(tagMap.get(studyRoomTag.getId()))
);
}
private void removeTags(List<Long> tagIdsToRemove, StudyRoom studyRoom) {
int size = tagRepository.countStudyRoomTagByIdInAndStudyRoom(tagIdsToRemove, studyRoom);
if(size == tagIdsToRemove.size()){
tagRepository.deleteAllByIdInBatch(tagIdsToRemove);
} else{
throw new GlobalException(ErrorCode.NOT_VALID, "Some tag not matching StudyRoom.");
}
}
private void dayOffModifyLogic(StudyRoomDayOffModifyRequest request, StudyRoom studyRoom) {
if (request.getDayOffsToAdd() != null && !request.getDayOffsToAdd().isEmpty())
dayOffRepository.batchInsert(request.getDayOffsToAdd(), studyRoom);
if (request.getDayOffsToUpdate() != null && !request.getDayOffsToUpdate().isEmpty())
updateDayOffs(request.getDayOffsToUpdate(), studyRoom);
if (request.getDayOffIdsToRemove() != null && !request.getDayOffIdsToRemove().isEmpty())
removeDayOffs(request.getDayOffIdsToRemove(), studyRoom);
}
private void updateDayOffs(List<StudyRoomDayOffUpdateRequest> dayOffsToUpdate, StudyRoom studyRoom) {
Map<Long, DayOfWeek> dayOffMap = new HashMap<>();
List<Long> dayOffIds = new ArrayList<>();
for (StudyRoomDayOffUpdateRequest dayOffToUpdate : dayOffsToUpdate) {
dayOffMap.put(dayOffToUpdate.getDayOffId(), dayOffToUpdate.getDayOff());
dayOffIds.add(dayOffToUpdate.getDayOffId());
}
List<StudyRoomDayOff> studyRoomDayOffs = dayOffRepository.findAllByIdInAndStudyRoom(dayOffIds, studyRoom);
if (studyRoomDayOffs.size() != dayOffsToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some day-off not matching StudyRoom.");
}
studyRoomDayOffs.forEach(studyRoomDayOff ->
studyRoomDayOff.modifyDayOff(dayOffMap.get(studyRoomDayOff.getId()))
);
}
private void removeDayOffs(List<Long> dayOffIdsToRemove, StudyRoom studyRoom) {
int size = dayOffRepository.countStudyRoomDayOffByIdInAndStudyRoom(dayOffIdsToRemove, studyRoom);
if (size == dayOffIdsToRemove.size()) {
dayOffRepository.deleteAllByIdInBatch(dayOffIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some day-off not matching StudyRoom.");
}
}
private void optionModifyLogic(StudyRoomOptionInfoModifyRequest request, StudyRoom studyRoom) {
if (request.getOptionsToAdd() != null && !request.getOptionsToAdd().isEmpty())
optionInfoRepository.batchInsert(request.getOptionsToAdd(), studyRoom);
if (request.getOptionsToUpdate() != null && !request.getOptionsToUpdate().isEmpty())
updateOptions(request.getOptionsToUpdate(), studyRoom);
if (request.getOptionsIdsToRemove() != null && !request.getOptionsIdsToRemove().isEmpty())
removeOptions(request.getOptionsIdsToRemove(), studyRoom);
}
private void updateOptions(List<StudyRoomOptionUpdateRequest> optionsToUpdate, StudyRoom studyRoom) {
Map<Long, StudyRoomOption> optionMap = new HashMap<>();
List<Long> optionIds = new ArrayList<>();
for (StudyRoomOptionUpdateRequest optionToUpdate : optionsToUpdate) {
optionMap.put(optionToUpdate.getOptionId(), optionToUpdate.getOption());
optionIds.add(optionToUpdate.getOptionId());
}
List<StudyRoomOptionInfo> studyRoomOptions = optionInfoRepository.findAllByIdInAndStudyRoom(optionIds, studyRoom);
if (studyRoomOptions.size() != optionsToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some option not matching StudyRoom.");
}
studyRoomOptions.forEach(studyRoomOption ->
studyRoomOption.modifyOption(optionMap.get(studyRoomOption.getId()))
);
}
private void removeOptions(List<Long> optionIdsToRemove, StudyRoom studyRoom) {
int size = optionInfoRepository.countStudyRoomOptionInfoByIdInAndStudyRoom(optionIdsToRemove, studyRoom);
if (size == optionIdsToRemove.size()) {
optionInfoRepository.deleteAllByIdInBatch(optionIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some option not matching StudyRoom.");
}
}
private void typeModifyLogic(StudyRoomTypeInfoModifyRequest request, StudyRoom studyRoom) {
if (request.getTypesToAdd() != null && !request.getTypesToAdd().isEmpty())
typeInfoRepository.batchInsert(request.getTypesToAdd(), studyRoom);
if (request.getTypesToUpdate() != null && !request.getTypesToUpdate().isEmpty())
updateTypes(request.getTypesToUpdate(), studyRoom);
if (request.getTypeIdsToRemove() != null && !request.getTypeIdsToRemove().isEmpty())
removeTypes(request.getTypeIdsToRemove(), studyRoom);
}
private void updateTypes(List<StudyRoomTypeUpdateRequest> typesToUpdate, StudyRoom studyRoom) {
Map<Long, StudyRoomType> typeMap = new HashMap<>();
List<Long> typeIds = new ArrayList<>();
for (StudyRoomTypeUpdateRequest typeToUpdate : typesToUpdate) {
typeMap.put(typeToUpdate.getTypeId(), typeToUpdate.getType());
typeIds.add(typeToUpdate.getTypeId());
}
List<StudyRoomTypeInfo> studyRoomTypes = typeInfoRepository.findAllByIdInAndStudyRoom(typeIds, studyRoom);
if (studyRoomTypes.size() != typesToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some type not matching StudyRoom.");
}
studyRoomTypes.forEach(studyRoomType ->
studyRoomType.modifyType(typeMap.get(studyRoomType.getId()))
);
}
private void removeTypes(List<Long> typeIdsToRemove, StudyRoom studyRoom) {
int size = typeInfoRepository.countStudyRoomTypeInfoByIdInAndStudyRoom(typeIdsToRemove, studyRoom);
if (size == typeIdsToRemove.size()) {
typeInfoRepository.deleteAllByIdInBatch(typeIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some type not matching StudyRoom.");
}
}
private void reserveTypeModifyLogic(StudyRoomReservationTypeModifyRequest request, StudyRoom studyRoom) {
if (request.getReservationTypesToAdd() != null)
reserveTypeRepository.batchInsert(request.getReservationTypesToAdd(), studyRoom);
if (request.getReservationTypesToUpdate() != null && !request.getReservationTypesToUpdate().isEmpty())
updateReserveTypes(request.getReservationTypesToUpdate(), studyRoom);
if (request.getReservationTypeIdsToRemove() != null && !request.getReservationTypeIdsToRemove().isEmpty())
removeReserveTypes(request.getReservationTypeIdsToRemove(), studyRoom);
}
private void updateReserveTypes(
List<StudyRoomReservationTypeUpdateRequest> reservationTypesToUpdate, StudyRoom studyRoom
) {
Map<Long, StudyRoomReservationTypeCreateRequest> reserveTypeMap = new HashMap<>();
List<Long> reserveTypeIds = new ArrayList<>();
for (StudyRoomReservationTypeUpdateRequest reserveTypeToUpdate : reservationTypesToUpdate) {
reserveTypeMap.put(reserveTypeToUpdate.getReservationTypeId(), reserveTypeToUpdate.getReservationType());
reserveTypeIds.add(reserveTypeToUpdate.getReservationTypeId());
}
List<StudyRoomReserveType> reserveTypes =
reserveTypeRepository.findAllByIdInAndStudyRoom(reserveTypeIds, studyRoom);
if (reserveTypes.size() != reservationTypesToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some reserve type not matching StudyRoom.");
}
reserveTypes.forEach(reserveType ->
reserveType.modifyReserveType(reserveTypeMap.get(reserveType.getId()))
);
}
private void removeReserveTypes(List<Long> reservationTypeIdsToRemove, StudyRoom studyRoom) {
int size = reserveTypeRepository.countStudyRoomReserveTypeByIdInAndStudyRoom(
reservationTypeIdsToRemove, studyRoom);
if (size == reservationTypeIdsToRemove.size()) {
reserveTypeRepository.deleteAllByIdInBatch(reservationTypeIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some reserve type not matching StudyRoom.");
}
}
}
확실히 작업을 나눔으로 이제 무거웠던 하나의 Update API가 여러 개로 분산되었다.
하지만.. 이번엔 각 작업에 대해 하나의 API를 만드니 그것과 관련된 중복 코드가 길어져 괜히 코드가 길어진다는 생각이 들었다.
또한, 이러면 프론트엔드 작업에 있어서 굉장히 불편할 것이며 나중에 로그를 확인할 때도 불편할 것이라는 생각이 들었다.
그래서, 고민을 하다가 아.. 화면 프레임 별 혹은 그 중에서도 비슷한 성격을 가지는 애들끼리 한 API에 묶는 건 어떨까?
라는 생각에 도달하게 되었다.
비슷한 속성을 가지는 테이블을 묶어 2개의 API로 처리
우선, 스터디 룸의 정보(제목, 소제목, 소개, 가이드라인 등)과 비슷한 성격을 가지는 정보들은
스터디 룸 휴무일, 스터디 룸 태그, 스터디 룸 이미지라고 생각이 들었다.
그리고 그 외의 스터디 룸 옵션, 스터디 룸 타입, 스터디 룸 예약 타입은 기본 정보 변경과 다른 성격을 가진다고 생각했고
상대적으로 변경 횟수가 적은 친구들이기에 `update`와 `updateSettings` 두 개로 API를 나눴다.
묶어서 처리한 Update 코드
@Slf4j
@Service
@RequiredArgsConstructor
public class StudyRoomCommandService {
...
@Transactional
public void update(
UpdateStudyRoomRequest request,
Long studyRoomId,
UUID userId
){
StudyRoom studyRoom = validateStudyRoomWithUserId(studyRoomId, userId);
studyRoom.modifyStudyRoom(request);
imageModifyLogic(request.getImageModification(), studyRoom);
tagModifyLogic(request.getTagModification(), studyRoom);
dayOffModifyLogic(request.getDayOffModification(), studyRoom);
}
@Transactional
public void updateSettings(
UpdateStudyRoomSettingRequest request,
Long studyRoomId,
UUID userId
) {
StudyRoom studyRoom = validateStudyRoomWithUserId(studyRoomId, userId);
optionModifyLogic(request.getOptionInfoModification(), studyRoom);
typeModifyLogic(request.getTypeInfoModification(), studyRoom);
reserveTypeModifyLogic(request.getReservationTypeModification(), studyRoom);
}
private void imageModifyLogic(ModifyStudyRoomImageRequest imageModification, StudyRoom studyRoom) {
if (imageModification != null) {
if (imageModification.getImagesToAdd() != null && !imageModification.getImagesToAdd().isEmpty())
imageRepository.batchInsert(imageModification.getImagesToAdd(), studyRoom);
if (imageModification.getImagesToUpdate() != null && !imageModification.getImagesToUpdate().isEmpty())
updateImages(imageModification.getImagesToUpdate(), studyRoom);
if (imageModification.getImageIdsToRemove() != null && !imageModification.getImageIdsToRemove().isEmpty())
removeImages(imageModification.getImageIdsToRemove(), studyRoom);
}
}
private void updateImages(List<UpdateStudyRoomImageRequest> imagesToUpdate, StudyRoom studyRoom) {
Map<Long, String> imageMap = new HashMap<>();
List<Long> imageIds = new ArrayList<>();
for (UpdateStudyRoomImageRequest imageToUpdate : imagesToUpdate) {
imageMap.put(imageToUpdate.getImageId(), imageToUpdate.getImageUrl());
imageIds.add(imageToUpdate.getImageId());
}
List<StudyRoomImage> studyRoomImages = imageRepository.findAllByIdInAndStudyRoom(imageIds, studyRoom);
if (studyRoomImages.size() != imagesToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
studyRoomImages.forEach(studyRoomImage ->
studyRoomImage.modifyImageUrl(imageMap.get(studyRoomImage.getId()))
);
}
private void removeImages(List<Long> imageIdsToRemove, StudyRoom studyRoom) {
int size = imageRepository.countStudyRoomImageByIdInAndStudyRoom(imageIdsToRemove, studyRoom);
if(size == imageIdsToRemove.size()){
imageRepository.deleteAllByIdInBatch(imageIdsToRemove);
} else{
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
}
private void tagModifyLogic(ModifyStudyRoomTagRequest request, StudyRoom studyRoom) {
if (request.getTagsToAdd() != null && !request.getTagsToAdd().isEmpty())
tagRepository.batchInsert(request.getTagsToAdd(), studyRoom);
if (request.getTagsToUpdate() != null && !request.getTagsToUpdate().isEmpty())
updateTags(request.getTagsToUpdate(), studyRoom);
if (request.getTagIdsToRemove() != null && !request.getTagIdsToRemove().isEmpty())
removeTags(request.getTagIdsToRemove(), studyRoom);
}
private void updateTags(List<UpdateStudyRoomTagRequest> tagsToUpdate, StudyRoom studyRoom) {
Map<Long, String> tagMap = new HashMap<>();
List<Long> tagIds = new ArrayList<>();
for (UpdateStudyRoomTagRequest tagToUpdate : tagsToUpdate) {
tagMap.put(tagToUpdate.getTagId(), tagToUpdate.getTag());
tagIds.add(tagToUpdate.getTagId());
}
List<StudyRoomTag> studyRoomTags = tagRepository.findAllByIdInAndStudyRoom(tagIds, studyRoom);
if (studyRoomTags.size() != tagsToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some image not matching StudyRoom.");
}
studyRoomTags.forEach(studyRoomTag ->
studyRoomTag.modifyTag(tagMap.get(studyRoomTag.getId()))
);
}
private void removeTags(List<Long> tagIdsToRemove, StudyRoom studyRoom) {
int size = tagRepository.countStudyRoomTagByIdInAndStudyRoom(tagIdsToRemove, studyRoom);
if(size == tagIdsToRemove.size()){
tagRepository.deleteAllByIdInBatch(tagIdsToRemove);
} else{
throw new GlobalException(ErrorCode.NOT_VALID, "Some tag not matching StudyRoom.");
}
}
private void dayOffModifyLogic(ModifyStudyRoomDayOffRequest request, StudyRoom studyRoom) {
if (request.getDayOffsToAdd() != null && !request.getDayOffsToAdd().isEmpty())
dayOffRepository.batchInsert(request.getDayOffsToAdd(), studyRoom);
if (request.getDayOffsToUpdate() != null && !request.getDayOffsToUpdate().isEmpty())
updateDayOffs(request.getDayOffsToUpdate(), studyRoom);
if (request.getDayOffIdsToRemove() != null && !request.getDayOffIdsToRemove().isEmpty())
removeDayOffs(request.getDayOffIdsToRemove(), studyRoom);
}
private void updateDayOffs(List<UpdateStudyRoomDayOffRequest> dayOffsToUpdate, StudyRoom studyRoom) {
Map<Long, DayOfWeek> dayOffMap = new HashMap<>();
List<Long> dayOffIds = new ArrayList<>();
for (UpdateStudyRoomDayOffRequest dayOffToUpdate : dayOffsToUpdate) {
dayOffMap.put(dayOffToUpdate.getDayOffId(), dayOffToUpdate.getDayOff());
dayOffIds.add(dayOffToUpdate.getDayOffId());
}
List<StudyRoomDayOff> studyRoomDayOffs = dayOffRepository.findAllByIdInAndStudyRoom(dayOffIds, studyRoom);
if (studyRoomDayOffs.size() != dayOffsToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some day-off not matching StudyRoom.");
}
studyRoomDayOffs.forEach(studyRoomDayOff ->
studyRoomDayOff.modifyDayOff(dayOffMap.get(studyRoomDayOff.getId()))
);
}
private void removeDayOffs(List<Long> dayOffIdsToRemove, StudyRoom studyRoom) {
int size = dayOffRepository.countStudyRoomDayOffByIdInAndStudyRoom(dayOffIdsToRemove, studyRoom);
if (size == dayOffIdsToRemove.size()) {
dayOffRepository.deleteAllByIdInBatch(dayOffIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some day-off not matching StudyRoom.");
}
}
private void optionModifyLogic(ModifyStudyRoomOptionInfoRequest request, StudyRoom studyRoom) {
if (request.getOptionsToAdd() != null && !request.getOptionsToAdd().isEmpty())
optionInfoRepository.batchInsert(request.getOptionsToAdd(), studyRoom);
if (request.getOptionsToUpdate() != null && !request.getOptionsToUpdate().isEmpty())
updateOptions(request.getOptionsToUpdate(), studyRoom);
if (request.getOptionsIdsToRemove() != null && !request.getOptionsIdsToRemove().isEmpty())
removeOptions(request.getOptionsIdsToRemove(), studyRoom);
}
private void updateOptions(List<UpdateStudyRoomOptionRequest> optionsToUpdate, StudyRoom studyRoom) {
Map<Long, StudyRoomOption> optionMap = new HashMap<>();
List<Long> optionIds = new ArrayList<>();
for (UpdateStudyRoomOptionRequest optionToUpdate : optionsToUpdate) {
optionMap.put(optionToUpdate.getOptionId(), optionToUpdate.getOption());
optionIds.add(optionToUpdate.getOptionId());
}
List<StudyRoomOptionInfo> studyRoomOptions = optionInfoRepository.findAllByIdInAndStudyRoom(optionIds, studyRoom);
if (studyRoomOptions.size() != optionsToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some option not matching StudyRoom.");
}
studyRoomOptions.forEach(studyRoomOption ->
studyRoomOption.modifyOption(optionMap.get(studyRoomOption.getId()))
);
}
private void removeOptions(List<Long> optionIdsToRemove, StudyRoom studyRoom) {
int size = optionInfoRepository.countStudyRoomOptionInfoByIdInAndStudyRoom(optionIdsToRemove, studyRoom);
if (size == optionIdsToRemove.size()) {
optionInfoRepository.deleteAllByIdInBatch(optionIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some option not matching StudyRoom.");
}
}
private void typeModifyLogic(ModifyStudyRoomTypeInfoRequest request, StudyRoom studyRoom) {
if (request.getTypesToAdd() != null && !request.getTypesToAdd().isEmpty())
typeInfoRepository.batchInsert(request.getTypesToAdd(), studyRoom);
if (request.getTypesToUpdate() != null && !request.getTypesToUpdate().isEmpty())
updateTypes(request.getTypesToUpdate(), studyRoom);
if (request.getTypeIdsToRemove() != null && !request.getTypeIdsToRemove().isEmpty())
removeTypes(request.getTypeIdsToRemove(), studyRoom);
}
private void updateTypes(List<UpdateStudyRoomTypeRequest> typesToUpdate, StudyRoom studyRoom) {
Map<Long, StudyRoomType> typeMap = new HashMap<>();
List<Long> typeIds = new ArrayList<>();
for (UpdateStudyRoomTypeRequest typeToUpdate : typesToUpdate) {
typeMap.put(typeToUpdate.getTypeId(), typeToUpdate.getType());
typeIds.add(typeToUpdate.getTypeId());
}
List<StudyRoomTypeInfo> studyRoomTypes = typeInfoRepository.findAllByIdInAndStudyRoom(typeIds, studyRoom);
if (studyRoomTypes.size() != typesToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some type not matching StudyRoom.");
}
studyRoomTypes.forEach(studyRoomType ->
studyRoomType.modifyType(typeMap.get(studyRoomType.getId()))
);
}
private void removeTypes(List<Long> typeIdsToRemove, StudyRoom studyRoom) {
int size = typeInfoRepository.countStudyRoomTypeInfoByIdInAndStudyRoom(typeIdsToRemove, studyRoom);
if (size == typeIdsToRemove.size()) {
typeInfoRepository.deleteAllByIdInBatch(typeIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some type not matching StudyRoom.");
}
}
private void reserveTypeModifyLogic(ModifyStudyRoomReservationTypeRequest request, StudyRoom studyRoom) {
if (request.getReservationTypesToAdd() != null)
reserveTypeRepository.batchInsert(request.getReservationTypesToAdd(), studyRoom);
if (request.getReservationTypesToUpdate() != null && !request.getReservationTypesToUpdate().isEmpty())
updateReserveTypes(request.getReservationTypesToUpdate(), studyRoom);
if (request.getReservationTypeIdsToRemove() != null && !request.getReservationTypeIdsToRemove().isEmpty())
removeReserveTypes(request.getReservationTypeIdsToRemove(), studyRoom);
}
private void updateReserveTypes(
List<UpdateStudyRoomReservationTypeRequest> reservationTypesToUpdate, StudyRoom studyRoom
) {
Map<Long, CreateStudyRoomReservationTypeRequest> reserveTypeMap = new HashMap<>();
List<Long> reserveTypeIds = new ArrayList<>();
for (UpdateStudyRoomReservationTypeRequest reserveTypeToUpdate : reservationTypesToUpdate) {
reserveTypeMap.put(reserveTypeToUpdate.getReservationTypeId(), reserveTypeToUpdate.getReservationType());
reserveTypeIds.add(reserveTypeToUpdate.getReservationTypeId());
}
List<StudyRoomReserveType> reserveTypes =
reserveTypeRepository.findAllByIdInAndStudyRoom(reserveTypeIds, studyRoom);
if (reserveTypes.size() != reservationTypesToUpdate.size()) {
throw new GlobalException(ErrorCode.NOT_VALID, "Some reserve type not matching StudyRoom.");
}
reserveTypes.forEach(reserveType ->
reserveType.modifyReserveType(reserveTypeMap.get(reserveType.getId()))
);
}
private void removeReserveTypes(List<Long> reservationTypeIdsToRemove, StudyRoom studyRoom) {
int size = reserveTypeRepository.countStudyRoomReserveTypeByIdInAndStudyRoom(
reservationTypeIdsToRemove, studyRoom);
if (size == reservationTypeIdsToRemove.size()) {
reserveTypeRepository.deleteAllByIdInBatch(reservationTypeIdsToRemove);
} else {
throw new GlobalException(ErrorCode.NOT_VALID, "Some reserve type not matching StudyRoom.");
}
}
}
이렇게 하니 확실히 코드가 줄었다!
(하지만, 아직 ADD/UPDATE/DELETE 작업에 있어 중복 코드가 남아있다.)
반복되는 작업을 `BiConsumer` 인터페이스를 사용해 함수형 프로그래밍 형식으로 처리할 수 있겠지만,
잘 모르기도 했고 그렇다고 막 눈에 띌 정도로 코드가 줄진 않았다.(거의 중복되지만 살짝 식 다른 부분이 있기 때문이다.)
암튼 순서를 정리하자면,
- 하나의 Update API로 전부 다 처리
- 각 테이블 별 Update API로 처리
- 비슷한 성격을 가진 친구들을 묶어 2개의 Update API로 처리
가 된다.
추후 더 좋은 방법이 있다면 코드를 바꿀 것이지만, 초기 Update API 설계에 비해 훨씬 나아졌다고 생각한다.
Update API에서 사전 데이터를 넣어주냐 마냐
위의 Update API 설계 고민 외에도 한 가지 고민이 더 있었다.
그것은 "기존 데이터를 프론트엔드로부터 받을 것이냐 말 것이냐"였다.
프론트엔드가 데이터를 넣어주는 경우
- 백엔드는 별 다른 조건문 처리를 해주지 않아도 된다.
- 네트워크 비용이 비싸진다.
프론트엔드가 데이터를 넣지 않는 경우
- 백엔드는 조건문으로 해당 값이 존재하는지 확인해야 한다.(코드가 조건문으로 도배 됨)
- 이 경우 Null 혹은 Blank 값을 확인하는 메서드를 따로 생성할 수 있을 것이다.
- 네트워크 비용이 비교적 덜 든다.
Postman으로 기존 데이터를 보냈을 경우와 보내지 않았을 때의`Request Size`를 비교해 보았다.
기존 데이터를 담아주지 않는 경우
기존 데이터를 담아주는 경우
절대적인 크기 차이로는 `570B(바이트)`이고,
상대적인 크기 차이로 보면 `1.37KB -> 1.94KB`는 약 `41.6%`의 증가이다.
이때, 대규모 트래픽의 환경에서 570B의 차이는 100만 건의 요청 시 570MB의 추가 트래픽이 될 수 있다.
그리고 저속 네트워크에서 41%의 차이는 좀 크다고 할 수 있다..
그래서... 어떤 방식을 선택할까 굉장히 고민을 많이 했지만!!
그냥 프론트엔드에서 데이터를 보내주는 방식을 선택했다.
만약, 대규모 트래픽 환경이었다면 당연히 백엔드에서 처리하도록 했겠지만,
현재 우리 서비스는 소규모 트래픽이라고 할 수 있는 하루 요청량인 수백 ~ 수천 건도 생긴다면 감지덕지이며,
모바일 네트워크나 IoT 디바이스의 저속 네트워크가 아닌 고속 네트워크 환경이다.
그리하여, 프론트엔드에서 담아서 보내주는 것으로 결정했다.
만약, 대규모 트래픽이 나올 수 있는 서비스가 된다면 당연히 리팩토링 해야하는 부분이라고 생각한다.
'프로젝트 > StudyWithMe' 카테고리의 다른 글
[StudyWithMe] 멀티 스레드에서 트랜잭션 작업 간 정보 불일치 문제 (0) | 2024.12.08 |
---|---|
[StudyWithMe] 페이징 시 Offset 방식 대신 Cursor 방식 적용(약 111배 성능 향상) (0) | 2024.12.07 |
[StudyWithMe] Flyway의 다양한 활용과 조심 (0) | 2024.12.06 |
[StudyWithMe] PostgreSQL을 처음 접해보며 (0) | 2024.12.06 |
[StudyWithMe] StudyWithMe 프로젝트 시작 (0) | 2024.12.06 |