TIL(20240810) [이미지업로드 관련 코드 리팩토링]
챌린지 등록시 그리고 인증요청시 이미지 업로드 기능이 있는데 각각 하나의 함수 안에 모든 로직이 처리되어있어서 코드의 반복과 가독성이 떨어진다는 느낌을 받았다.
그래서 주말동안 다른부분도 마찬가지겠지만 코드 리팩토링을 하기로 계획했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// AdminService
public AdminChallengeCreateResponseDto createChallenge(MultipartFile image, AdminChallengeCreateRequestDto reqDto, User loginUser) {
if (!loginUser.getUserRole().equals(UserRole.ADMIN)) {
throw new GlobalException(ErrorCode.USER_ACCESS_DENIED);
}
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(image.getContentType());
metadata.setContentLength(image.getSize());
amazonS3Client.putObject(BUCKET, "challenge/" + image.getOriginalFilename(), image.getInputStream(), metadata);
String imageUrl = amazonS3Client.getResourceUrl(BUCKET, "challenge/" + image.getOriginalFilename());
Challenge challenge = Challenge.builder()
.title(reqDto.getTitle())
.content(reqDto.getContent())
.image(image.getOriginalFilename())
.imageUrl(imageUrl)
.category(Category.valueOf(reqDto.getCategory()))
.conditionStatus(ConditionStatus.valueOf(reqDto.getConditionStatus()))
.startTime(reqDto.getStartTime())
.endTime(reqDto.getEndTime())
.limitedUsers(reqDto.getLimitedUsers())
.build();
Challenge savedChallenge = challengeRepository.save(challenge);
return new AdminChallengeCreateResponseDto(savedChallenge);
} catch (IOException e) {
throw new FileUploadFailureException("파일 업로드 실패");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// VerificationService
@Transactional
public VerificationResponseDto requestVerification(Long challengeId, MultipartFile image, VerificationRequestDto requestDto, User user) {
try {
if(image.isEmpty()) {
throw new GlobalException(ErrorCode.EMPTY_FILE);
}
ObjectMetadata metadata= new ObjectMetadata();
metadata.setContentType(image.getContentType());
metadata.setContentLength(image.getSize());
amazonS3Client.putObject(BUCKET, "verification/" + image.getOriginalFilename(), image.getInputStream(), metadata);
Challenge challenge = challengeService.findById(challengeId);
LocalDateTime currentDateTime = LocalDateTime.now();
LocalDate currentDate = currentDateTime.toLocalDate();
LocalDateTime startOfDay = currentDate.atStartOfDay();
LocalDateTime endOfDay = currentDate.atTime(LocalTime.MAX);
boolean checkVerification = verificationRepository.existsByChallengeIdAndUserAndCreatedAtBetween(
challengeId, user, startOfDay, endOfDay
);
if(checkVerification) {
throw new GlobalException(ErrorCode.ALREADY_EXISTS_VERIFICATION);
}
String imageUrl= amazonS3Client.getResourceUrl(BUCKET, "verification/" + image.getOriginalFilename());
Verification verification = new Verification(requestDto.getTitle(), requestDto.getContent(), image.getOriginalFilename(), imageUrl, challenge, user);
verificationRepository.save(verification);
return new VerificationResponseDto(verification.getId(), verification.getTitle(), verification.getContent(), imageUrl, verification.getStatus());
} catch(IOException e) {
throw new FileUploadFailureException("파일 업로드 실패");
}
}
이전에 작성했던 코드다. 챌린지를 생성하는 builder 부분은 내가 작성하진 않았고 이미지 업로드 기능만 내가 작성했다. 일단 봐도 무지 길다.. 가독성도 떨어지고 심지어 인증요청 부분에도 이미지 업로드 관련 코드가 중복됨을 확인할 수 있다.
그리고 예외처리가 잘 되어있지 않는 것을 확인할 수 있는데, 1) image 파일이 비어있는 경우 2) s3 버킷 정책에 jpg,png,jpeg,gif 설정해놓았는데, 다른 파일 형식이 들어올 경우 3) 파일크기에 대한 예외처리
그래서 이부분에 대한 추가 예외처리와 공통기능에 대한 분리로 코드 중복을 제거하고 코드 재사용성을 높이고 싶었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// S3ServiceImpl
public class S3ServiceImpl implements S3Service {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String BUCKET;
@Value("${spring.servlet.multipart.max-file-size}")
private String maxFileSize;
@Override
public String imageUpload(MultipartFile image, String key) {
if (image.isEmpty()) {
throw new GlobalException(ErrorCode.EMPTY_FILE);
}
fileSizeExceed(image);
allowedImageTypes(image);
try {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(image.getContentType());
metadata.setContentLength(image.getSize());
amazonS3Client.putObject(BUCKET, key + image.getOriginalFilename(), image.getInputStream(), metadata);
String imageUrl = amazonS3Client.getResourceUrl(BUCKET, key + image.getOriginalFilename());
return imageUrl;
} catch (IOException e) {
throw new FileUploadFailureException("파일 업로드 실패");
}
}
@Override
public void deleteChallengeImage(Challenge challenge, String key) {
DeleteObjectRequest request = new DeleteObjectRequest(BUCKET, key + challenge.getImage());
amazonS3Client.deleteObject(request);
}
@Override
public void deleteVerificationImage(Verification verification, String key) {
DeleteObjectRequest request = new DeleteObjectRequest(BUCKET, key + verification.getImageName());
amazonS3Client.deleteObject(request);
}
private long parseSize(String size) {
if (size.endsWith("MB")) {
return Long.parseLong(size.replace("MB", "")) * 1024 * 1024;
} else if (size.endsWith("KB")) {
return Long.parseLong(size.replace("KB", "")) * 1024;
} else if (size.endsWith("GB")) {
return Long.parseLong(size.replace("GB", "")) * 1024 * 1024 * 1024;
} else {
return Long.parseLong(size);
}
}
private void fileSizeExceed(MultipartFile image) {
if (image.getSize() > parseSize(maxFileSize)) {
throw new GlobalException(ErrorCode.FILE_UPLOAD_ERROR);
}
}
private void allowedImageTypes(MultipartFile image) {
String fileName = image.getOriginalFilename();
if (fileName == null || !fileName.matches(".*\\.(jpg|jpeg|png|gif)$")) {
throw new InvalidFileTypeException("지원되지 않는 파일 형식입니다: " + fileName);
}
}
일단 delete 같은경우는 해당 객체의 이미지를 불러와야 하기 때문에 매개변수에 대한 고민을 했다. 너무 피곤한 탓일까.. 왜 도저히 어떻게 해야할지 떠오르지 않았다. 일단은 나중에 또 수정해보기로 하고 delete challenge/verification 함수를 각각 만들어줬다. (일단 무지 찝찝하다. 이 부분은 방법을 찾아서 추후에 수정할 것이다.)
파일 크기와 파일형식은 내부메서드로 정의해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 리팩토링 후 adminSevice
@Transactional
public AdminChallengeCreateResponseDto createChallenge(MultipartFile image, AdminChallengeCreateRequestDto reqDto, User loginUser) {
if (!loginUser.getUserRole().equals(UserRole.ADMIN)) {
throw new GlobalException(ErrorCode.USER_ACCESS_DENIED);
}
String key = "challenge/";
String imageUrl = s3Service.imageUpload(image, key);
Challenge challenge = Challenge.builder()
.title(reqDto.getTitle())
.content(reqDto.getContent())
.image(image.getOriginalFilename())
.imageUrl(imageUrl)
.category(Category.valueOf(reqDto.getCategory()))
.conditionStatus(ConditionStatus.valueOf(reqDto.getConditionStatus()))
.startTime(reqDto.getStartTime())
.endTime(reqDto.getEndTime())
.limitedUsers(reqDto.getLimitedUsers())
.build();
Challenge savedChallenge = challengeRepository.save(challenge);
return new AdminChallengeCreateResponseDto(savedChallenge);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 리팩토링 후 VerificationService
@Transactional
public VerificationResponseDto requestVerification(Long challengeId, MultipartFile image, VerificationRequestDto requestDto, User user) {
Challenge challenge = challengeService.findById(challengeId);
duplicateVerification(challengeId, user);
String key = "verification/";
String imageUrl = s3Service.imageUpload(image, key);
Verification verification = new Verification(requestDto.getTitle(), requestDto.getContent(), image.getOriginalFilename(), imageUrl, challenge, user);
verificationRepository.save(verification);
return new VerificationResponseDto(verification.getId(), verification.getTitle(), verification.getContent(), imageUrl, verification.getStatus());
}
@Transactional
public void cancelVerification(Long verificationId, User user) {
Verification verification = findVerificationById(verificationId);
if(verification.getStatus().equals(Status.APPROVE)) {
throw new GlobalException(ErrorCode.DO_NOT_CANCEL_VERIFICATION);
}
verification.checkUser(user);
String key = "verification/";
s3Service.deleteVerificationImage(verification, key);
verificationRepository.delete(verification);
}
private void duplicateVerification(Long challengeId, User user) {
LocalDateTime currentDateTime = LocalDateTime.now();
LocalDate currentDate = currentDateTime.toLocalDate();
LocalDateTime startOfDay = currentDate.atStartOfDay();
LocalDateTime endOfDay = currentDate.atTime(LocalTime.MAX);
boolean checkVerification = verificationRepository.existsByChallengeIdAndUserAndCreatedAtBetween(
challengeId, user, startOfDay, endOfDay
);
if(checkVerification) {
throw new GlobalException(ErrorCode.ALREADY_EXISTS_VERIFICATION);
}
}
이 부분 말고도 리팩토링해야 할 다른 코드들이 많다..하하하..! 처음 코드를 작성할 때 조금 더 신중해야지 하면서도 마감기한 때문에 신경을 많이 못썼던 것 같다. 이번기회로 리팩토링 하면서 나의 코드가 참 가독성이 많이 떨어지구나.. 라는 생각이 많이 든다.
더 고민하고 더 공부하자..🤦♀️