Skip to content

Commit de414cc

Browse files
chanrhanchanrhan
andauthored
feature: 전역 에외 처리 핸들러 추가 (#34)
* feat: GlobalExceptionHandler 추가 * chore: BaseException 삭제 * feat: 사진 데이터 없을 때 예외 추가 * feat: 예외 케이스 CustomException으로 대체 * style: 코드 포맷팅 및 함수명 수정 * refactor: 예외 처리를 repository가 아닌 service단에서 처리하도록 변경 --------- Co-authored-by: chanrhan <km1104rs@naver.com>
1 parent bed244a commit de414cc

9 files changed

Lines changed: 214 additions & 9 deletions

File tree

src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import java.util.Date;
1414
import java.util.Optional;
1515
import kr.kro.photoliner.domain.photo.dto.ExifData;
16+
import kr.kro.photoliner.global.code.ApiResponseCode;
17+
import kr.kro.photoliner.global.exception.CustomException;
1618
import lombok.RequiredArgsConstructor;
1719
import org.locationtech.jts.geom.Coordinate;
1820
import org.locationtech.jts.geom.GeometryFactory;
@@ -33,7 +35,8 @@ public ExifData extract(MultipartFile file) {
3335
Point location = extractGpsLocation(metadata);
3436
return new ExifData(capturedDt, location);
3537
} catch (ImageProcessingException | IOException e) {
36-
throw new RuntimeException(e);
38+
throw CustomException.of(ApiResponseCode.FILE_PROCESSING_ERROR, "file name: " + file.getOriginalFilename(),
39+
e);
3740
}
3841
}
3942

src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import java.nio.file.StandardCopyOption;
99
import java.util.Objects;
1010
import java.util.UUID;
11+
import kr.kro.photoliner.global.code.ApiResponseCode;
12+
import kr.kro.photoliner.global.exception.CustomException;
1113
import org.springframework.beans.factory.annotation.Value;
1214
import org.springframework.stereotype.Component;
1315
import org.springframework.web.multipart.MultipartFile;
@@ -37,12 +39,12 @@ public String store(MultipartFile file) {
3739

3840
private void validateFile(MultipartFile file) {
3941
if (file.isEmpty()) {
40-
throw new IllegalArgumentException("Failed to store empty file");
42+
throw CustomException.of(ApiResponseCode.INVALID_FILE);
4143
}
4244

4345
String originalFilename = file.getOriginalFilename();
4446
if (originalFilename == null || originalFilename.contains("..")) {
45-
throw new IllegalArgumentException("Invalid file path: " + originalFilename);
47+
throw CustomException.of(ApiResponseCode.INVALID_FILE_NAME, "file name: " + originalFilename);
4648
}
4749
}
4850

@@ -58,7 +60,7 @@ private void saveFile(MultipartFile file, String fileName) {
5860
Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING);
5961
}
6062
} catch (IOException e) {
61-
throw new IllegalArgumentException("Failed to store file: " + file.getOriginalFilename(), e);
63+
throw CustomException.of(ApiResponseCode.FILE_STORE_ERROR, "file name: " + file.getOriginalFilename(), e);
6264
}
6365
}
6466

src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package kr.kro.photoliner.domain.photo.repository;
22

33
import java.util.List;
4+
import java.util.Optional;
45
import kr.kro.photoliner.domain.photo.model.Photo;
56
import kr.kro.photoliner.domain.photo.model.Photos;
67
import org.locationtech.jts.geom.Point;
@@ -30,7 +31,11 @@ List<Photo> findByUserIdInBox(
3031
Point ne
3132
);
3233

33-
default Photos findPhotosByUserIdInBox(Long userId, Point sw, Point ne) {
34+
default Photos getPhotosByUserIdInBox(Long userId, Point sw, Point ne) {
3435
return new Photos(findByUserIdInBox(userId, sw, ne));
3536
}
37+
38+
Photo save(Photo photo);
39+
40+
Optional<Photo> findById(Long photoId);
3641
}

src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import kr.kro.photoliner.domain.photo.model.Photo;
1010
import kr.kro.photoliner.domain.photo.model.Photos;
1111
import kr.kro.photoliner.domain.photo.repository.PhotoRepository;
12+
import kr.kro.photoliner.global.code.ApiResponseCode;
13+
import kr.kro.photoliner.global.exception.CustomException;
1214
import lombok.RequiredArgsConstructor;
1315
import org.locationtech.jts.geom.Coordinate;
1416
import org.locationtech.jts.geom.GeometryFactory;
@@ -34,7 +36,7 @@ public MapMarkersResponse getMarkersInViewport(MapMarkersRequest request) {
3436
Point sw = geometryFactory.createPoint(request.getSouthWestCoordinate());
3537
Point ne = geometryFactory.createPoint(request.getNorthEastCoordinate());
3638

37-
Photos photos = photoRepository.findPhotosByUserIdInBox(request.userId(), sw, ne);
39+
Photos photos = photoRepository.getPhotosByUserIdInBox(request.userId(), sw, ne);
3840

3941
return MapMarkersResponse.of(
4042
photos.filterInDate(request.from(), request.to()),
@@ -45,14 +47,14 @@ public MapMarkersResponse getMarkersInViewport(MapMarkersRequest request) {
4547
@Transactional
4648
public void updatePhotoCapturedDate(Long photoId, PhotoCapturedDateUpdateRequest request) {
4749
Photo photo = photoRepository.findById(photoId)
48-
.orElseThrow(() -> new IllegalArgumentException("사진을 찾을 수 없습니다. ID: " + photoId));
50+
.orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_PHOTO, "photo id: " + photoId));
4951
photo.updateCapturedDate(request.capturedDt());
5052
}
5153

5254
@Transactional
5355
public void updatePhotoLocation(Long photoId, PhotoLocationUpdateRequest request) {
5456
Photo photo = photoRepository.findById(photoId)
55-
.orElseThrow(() -> new IllegalArgumentException("사진을 찾을 수 없습니다. ID: " + photoId));
57+
.orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_PHOTO, "photo id: " + photoId));
5658
Point location = geometryFactory.createPoint(
5759
new Coordinate(request.longitude(), request.latitude())
5860
);

src/main/java/kr/kro/photoliner/domain/photo/service/PhotoUploadService.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
import kr.kro.photoliner.domain.photo.repository.PhotoRepository;
1111
import kr.kro.photoliner.domain.user.model.User;
1212
import kr.kro.photoliner.domain.user.repository.UserRepository;
13+
import kr.kro.photoliner.global.code.ApiResponseCode;
14+
import kr.kro.photoliner.global.exception.CustomException;
1315
import lombok.RequiredArgsConstructor;
1416
import org.springframework.stereotype.Service;
1517
import org.springframework.transaction.annotation.Transactional;
@@ -32,8 +34,10 @@ public PhotoUploadResponse uploadPhotos(Long userId, List<MultipartFile> files)
3234
String filePath = fileStorage.store(file);
3335
String fileName = file.getOriginalFilename();
3436
User user = userRepository.findUserById(userId)
37+
.orElseThrow(
38+
() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + userId));
39+
Photo photo = createPhoto(user, exifData, filePath, fileName);
3540
.orElseThrow(RuntimeException::new);
36-
Photo photo = createPhoto(user, exifData, fileName, filePath);
3741
Photo savedPhoto = photoRepository.save(photo);
3842
return InnerUploadedPhotoInfo.from(savedPhoto);
3943
})
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package kr.kro.photoliner.global.code;
2+
3+
import lombok.Getter;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.http.HttpStatus;
6+
7+
@Getter
8+
@RequiredArgsConstructor
9+
public enum ApiResponseCode {
10+
11+
/**
12+
* 2xx Success (성공)
13+
*/
14+
OK(HttpStatus.OK, "요청이 성공적으로 처리되었습니다."),
15+
CREATED(HttpStatus.CREATED, "요청이 성공적으로 처리되어 리소스가 생성되었습니다."),
16+
NO_CONTENT(HttpStatus.NO_CONTENT, "요청이 성공적으로 처리되었으나 반환할 내용이 없습니다."),
17+
18+
/**
19+
* 400 Bad Request (잘못된 요청)
20+
*/
21+
ILLEGAL_ARGUMENT(HttpStatus.BAD_REQUEST, "잘못된 인자가 전달되었습니다."),
22+
ILLEGAL_STATE(HttpStatus.BAD_REQUEST, "잘못된 상태로 요청이 들어왔습니다."),
23+
INVALID_REQUEST_BODY(HttpStatus.BAD_REQUEST, "잘못된 입력값이 포함되어 있습니다."),
24+
INVALID_DATE_TIME(HttpStatus.BAD_REQUEST, "잘못된 날짜 형식입니다."),
25+
INVALID_FILE(HttpStatus.BAD_REQUEST, "유효하지 않은 파일입니다."),
26+
INVALID_FILE_NAME(HttpStatus.BAD_REQUEST, "유효하지 않은 파일명입니다."),
27+
28+
/**
29+
* 401 Unauthorized (인증 필요)
30+
*/
31+
WITHDRAWN_USER(HttpStatus.UNAUTHORIZED, "탈퇴한 계정입니다."),
32+
33+
/**
34+
* 403 Forbidden (인가 필요)
35+
*/
36+
FORBIDDEN_USER_TYPE(HttpStatus.FORBIDDEN, "인가되지 않은 유저 타입입니다."),
37+
38+
/**
39+
* 404 Not Found (리소스를 찾을 수 없음)
40+
*/
41+
NOT_FOUND_USER(HttpStatus.NOT_FOUND, "사용자가 존재하지 않습니다."),
42+
NOT_FOUND_PHOTO(HttpStatus.NOT_FOUND, "사진이 존재하지 않습니다."),
43+
44+
/**
45+
* 409 CONFLICT (중복 혹은 충돌)
46+
*/
47+
DUPLICATE_LOGIN_ID(HttpStatus.CONFLICT, "이미 존재하는 로그인 아이디입니다."),
48+
DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."),
49+
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 존재하는 이메일입니다."),
50+
DUPLICATE_PHONE_NUMBER(HttpStatus.CONFLICT, "이미 존재하는 전화번호입니다."),
51+
52+
/**
53+
* 429 Too Many Requests (요청량 초과)
54+
*/
55+
TOO_MANY_REQUESTS_VERIFICATION(HttpStatus.TOO_MANY_REQUESTS, "하루 인증 횟수를 초과했습니다."),
56+
57+
/**
58+
* 500 Internal Server Error (서버 오류)
59+
*/
60+
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에서 오류가 발생했습니다."),
61+
FILE_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 처리 중 오류가 발생했습니다."),
62+
FILE_STORE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장 중 오류가 발생했습니다.");
63+
64+
private final HttpStatus httpStatus;
65+
private final String message;
66+
67+
public String getCode() {
68+
return this.name();
69+
}
70+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package kr.kro.photoliner.global.exception;
2+
3+
import kr.kro.photoliner.global.code.ApiResponseCode;
4+
import lombok.Getter;
5+
import org.springframework.util.StringUtils;
6+
7+
@Getter
8+
public class CustomException extends RuntimeException {
9+
private final ApiResponseCode errorCode;
10+
private final String detail;
11+
12+
private CustomException(ApiResponseCode errorCode, String detail) {
13+
super(errorCode.getMessage());
14+
this.errorCode = errorCode;
15+
this.detail = detail;
16+
}
17+
18+
private CustomException(ApiResponseCode errorCode, String detail, Throwable cause) {
19+
super(errorCode.getMessage(), cause);
20+
this.errorCode = errorCode;
21+
this.detail = detail;
22+
}
23+
24+
public static CustomException of(ApiResponseCode errorCode) {
25+
return new CustomException(errorCode, "");
26+
}
27+
28+
public static CustomException of(ApiResponseCode errorCode, String detail) {
29+
return new CustomException(errorCode, detail);
30+
}
31+
32+
public static CustomException of(ApiResponseCode errorCode, String detail, Throwable cause) {
33+
return new CustomException(errorCode, detail, cause);
34+
}
35+
36+
public String getFullMessage() {
37+
if (!StringUtils.hasText(detail)) {
38+
return super.getMessage();
39+
}
40+
return String.format("%s: %s", getMessage(), detail);
41+
}
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package kr.kro.photoliner.global.exception;
2+
3+
public record ErrorResponse(
4+
int status,
5+
String code,
6+
String message,
7+
String errorTraceId
8+
) {
9+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package kr.kro.photoliner.global.exception;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import java.time.DateTimeException;
5+
import java.util.UUID;
6+
import kr.kro.photoliner.global.code.ApiResponseCode;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.http.ResponseEntity;
9+
import org.springframework.web.bind.annotation.ExceptionHandler;
10+
import org.springframework.web.bind.annotation.RestControllerAdvice;
11+
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
12+
13+
@RestControllerAdvice
14+
@Slf4j
15+
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
16+
17+
@ExceptionHandler(CustomException.class)
18+
public ResponseEntity<Object> handlePhotoUploadException(
19+
HttpServletRequest request,
20+
CustomException e
21+
) {
22+
return buildErrorResponse(request, e.getErrorCode(), e.getFullMessage());
23+
}
24+
25+
@ExceptionHandler(IllegalArgumentException.class)
26+
public ResponseEntity<Object> handleIllegalArgumentException(
27+
HttpServletRequest request,
28+
IllegalArgumentException e
29+
) {
30+
return buildErrorResponse(request, ApiResponseCode.ILLEGAL_ARGUMENT, e.getMessage());
31+
}
32+
33+
@ExceptionHandler(IllegalStateException.class)
34+
public ResponseEntity<Object> handleIllegalStateException(
35+
HttpServletRequest request,
36+
IllegalStateException e
37+
) {
38+
return buildErrorResponse(request, ApiResponseCode.ILLEGAL_STATE, e.getMessage());
39+
}
40+
41+
@ExceptionHandler(DateTimeException.class)
42+
public ResponseEntity<Object> handleDateTimeException(
43+
HttpServletRequest request,
44+
DateTimeException e
45+
) {
46+
return buildErrorResponse(request, ApiResponseCode.INVALID_DATE_TIME, e.getMessage());
47+
}
48+
49+
private ResponseEntity<Object> buildErrorResponse(
50+
HttpServletRequest request,
51+
ApiResponseCode errorCode,
52+
String errorMessage
53+
) {
54+
String errorTraceId = UUID.randomUUID().toString();
55+
56+
log.error(errorMessage);
57+
58+
ErrorResponse response = new ErrorResponse(
59+
errorCode.getHttpStatus().value(),
60+
errorCode.getCode(),
61+
errorCode.getMessage(),
62+
errorTraceId
63+
);
64+
return ResponseEntity
65+
.status(response.status())
66+
.body(request);
67+
}
68+
}

0 commit comments

Comments
 (0)