Skip to content

Commit

Permalink
Merge pull request #35 from dnd-side-project/feat/#34
Browse files Browse the repository at this point in the history
[#34] ExceptionHandler 추가하고 Custom Exception으로 변환
  • Loading branch information
youngreal authored Sep 4, 2024
2 parents 8c24d83 + ed11b69 commit 192b22b
Show file tree
Hide file tree
Showing 17 changed files with 272 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.dndtravel.auth.exception;

public class AppleTokenDecodingException extends RuntimeException {
private static final String MESSAGE = "Apple 토큰 payload 해독실패";

public AppleTokenDecodingException(Exception e) {
super(MESSAGE, e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.dndtravel.auth.exception;

import io.jsonwebtoken.JwtException;

public class JwtTokenDecodingException extends RuntimeException {
private static final String MESSAGE = "Jwt 토큰 해독 실패";

public JwtTokenDecodingException(JwtException e) {
super(MESSAGE, e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.dndtravel.auth.exception;

import io.jsonwebtoken.ExpiredJwtException;

public class JwtTokenExpiredException extends RuntimeException {
private static final String MESSAGE = "Jwt 토큰이 만료되었음";

public JwtTokenExpiredException(ExpiredJwtException e) {
super(MESSAGE, e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.dndtravel.auth.exception;

public class RefreshTokenInvalidException extends RuntimeException {
private static final String MESSAGE = "유효하지 않은 RefreshToken 토큰 [refreshToken=%s]";

public RefreshTokenInvalidException(String refreshToken) {
super(String.format(MESSAGE, refreshToken));
}
}
7 changes: 5 additions & 2 deletions src/main/java/com/dnd/dndtravel/auth/service/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

import javax.crypto.SecretKey;

import com.dnd.dndtravel.auth.exception.JwtTokenExpiredException;
import com.dnd.dndtravel.auth.exception.JwtTokenDecodingException;

@Component
public class JwtProvider {
private static final String CLAIM_CONTENT = "memberId";
Expand Down Expand Up @@ -54,9 +57,9 @@ public Claims parseClaims(String splitHeader) {
.parseSignedClaims(splitHeader);

} catch (ExpiredJwtException e) {
throw new RuntimeException("message", e); // 유효하지 않은 토큰
throw new JwtTokenExpiredException(e);
} catch (JwtException e) {
throw new RuntimeException("message", e); // 토큰 해독 실패
throw new JwtTokenDecodingException(e);
}

return claims.getPayload();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.transaction.annotation.Transactional;

import com.dnd.dndtravel.auth.domain.RefreshToken;
import com.dnd.dndtravel.auth.exception.RefreshTokenInvalidException;
import com.dnd.dndtravel.auth.repository.RefreshTokenRepository;
import com.dnd.dndtravel.auth.service.dto.response.TokenResponse;
import com.dnd.dndtravel.auth.service.dto.response.ReissueTokenResponse;
Expand Down Expand Up @@ -34,7 +35,7 @@ public TokenResponse generateTokens(Long memberId) {
@Transactional
public ReissueTokenResponse reIssue(String token) {
//validation
RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token).orElseThrow(() -> new RuntimeException("유효하지 않은 토큰"));
RefreshToken refreshToken = refreshTokenRepository.findByRefreshToken(token).orElseThrow(() -> new RefreshTokenInvalidException(token));

//RTR
refreshTokenRepository.delete(refreshToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.util.Base64;

import com.dnd.dndtravel.auth.exception.AppleTokenDecodingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;

Expand All @@ -25,7 +26,7 @@ public static <T> T decodePayload(String token, Class<T> targetClass) {
try {
return objectMapper.readValue(payload, targetClass);
} catch (Exception e) {
throw new RuntimeException("Error decoding token payload", e);
throw new AppleTokenDecodingException(e);
}
}
}
125 changes: 125 additions & 0 deletions src/main/java/com/dnd/dndtravel/common/CommonExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.dnd.dndtravel.common;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.dnd.dndtravel.auth.exception.AppleTokenDecodingException;
import com.dnd.dndtravel.auth.exception.JwtTokenDecodingException;
import com.dnd.dndtravel.auth.exception.JwtTokenExpiredException;
import com.dnd.dndtravel.auth.exception.RefreshTokenInvalidException;
import com.dnd.dndtravel.map.exception.MemberAttractionNotFoundException;
import com.dnd.dndtravel.map.exception.MemberNotFoundException;
import com.dnd.dndtravel.map.exception.PhotoDeleteFailException;
import com.dnd.dndtravel.map.exception.PhotoEmptyException;
import com.dnd.dndtravel.map.exception.PhotoInvalidException;
import com.dnd.dndtravel.map.exception.PhotoUploadFailException;
import com.dnd.dndtravel.map.exception.RegionNotFoundException;
//todo 예외클래스가 많아지면 해당클래스가 길어질것으로 예상, 개선필요해보이고 보안 때문에 상태코드별로 애매하게 동일한 메시지를 전달해주고, 스웨거 문서로 상세 오류를 전달해주는데 이 구조가 적절한건지 고민해봐야한다.
@RestControllerAdvice
public class CommonExceptionHandler {

private static final String INTERNAL_SERVER_ERROR_MESSAGE = "Internal Server Error";
private static final String BAD_REQUEST_MESSAGE = "잘못된 요청입니다";

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<String> runtimeException() {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BAD_REQUEST_MESSAGE);
}

@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<String> runtimeException(MemberNotFoundException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BAD_REQUEST_MESSAGE);
}

@ExceptionHandler(MemberAttractionNotFoundException.class)
public ResponseEntity<String> runtimeException(MemberAttractionNotFoundException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BAD_REQUEST_MESSAGE);
}

@ExceptionHandler(RegionNotFoundException.class)
public ResponseEntity<String> runtimeException(RegionNotFoundException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BAD_REQUEST_MESSAGE);
}

@ExceptionHandler(AppleTokenDecodingException.class)
public ResponseEntity<String> runtimeException(AppleTokenDecodingException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("토큰 인증에 실패했습니다");
}

@ExceptionHandler(JwtTokenExpiredException.class)
public ResponseEntity<String> runtimeException(JwtTokenExpiredException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("토큰 인증에 실패했습니다");
}

@ExceptionHandler(JwtTokenDecodingException.class)
public ResponseEntity<String> runtimeException(JwtTokenDecodingException e) {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body("토큰 인증에 실패했습니다");
}

@ExceptionHandler(RefreshTokenInvalidException.class)
public ResponseEntity<String> runtimeException(RefreshTokenInvalidException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BAD_REQUEST_MESSAGE);
}

@ExceptionHandler(PhotoEmptyException.class)
public ResponseEntity<String> runtimeException(PhotoEmptyException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BAD_REQUEST_MESSAGE);
}

@ExceptionHandler(PhotoDeleteFailException.class)
public ResponseEntity<String> runtimeException(PhotoDeleteFailException e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(INTERNAL_SERVER_ERROR_MESSAGE);
}

@ExceptionHandler(PhotoUploadFailException.class)
public ResponseEntity<String> runtimeException(PhotoUploadFailException e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(INTERNAL_SERVER_ERROR_MESSAGE);
}

@ExceptionHandler(PhotoInvalidException.class)
public ResponseEntity<String> runtimeException(PhotoInvalidException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(BAD_REQUEST_MESSAGE);
}


@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> runtimeException(RuntimeException e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(INTERNAL_SERVER_ERROR_MESSAGE);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<String> runtimeException(Exception e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(INTERNAL_SERVER_ERROR_MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.dndtravel.map.exception;

public class MemberAttractionNotFoundException extends RuntimeException {
private static final String MESSAGE = "존재하지 않는 방문기록 [memberAttractionId=%s]";

public MemberAttractionNotFoundException(long memberAttractionId) {
super(String.format(MESSAGE, memberAttractionId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.dndtravel.map.exception;

public class MemberNotFoundException extends RuntimeException {
private static final String MESSAGE = "존재하지 않는 유저 [memberId=%s]";

public MemberNotFoundException(long memberId) {
super(String.format(MESSAGE, memberId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.dnd.dndtravel.map.exception;

import com.amazonaws.SdkClientException;

public class PhotoDeleteFailException extends RuntimeException{
private static final String MESSAGE = "s3 이미지 삭제 실패 [imagePath=%s]";

public PhotoDeleteFailException(String existingFileName, SdkClientException e) {
super(String.format(MESSAGE,existingFileName), e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.dndtravel.map.exception;

public class PhotoEmptyException extends RuntimeException{
private static final String MESSAGE = "이미지가 존재하지 않음";

public PhotoEmptyException() {
super(MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dnd.dndtravel.map.exception;

public class PhotoInvalidException extends RuntimeException {

public PhotoInvalidException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.dnd.dndtravel.map.exception;

import java.io.IOException;

import org.springframework.web.multipart.MultipartFile;

import com.amazonaws.SdkClientException;

public class PhotoUploadFailException extends RuntimeException {
private static final String MESSAGE = "s3 이미지 업로드 실패 [image=%s]";

public PhotoUploadFailException(MultipartFile image, IOException e) {
super(String.format(MESSAGE, image), e);
}

public PhotoUploadFailException(String image, SdkClientException e) {
super(String.format(MESSAGE, image), e);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.dnd.dndtravel.map.exception;

public class RegionNotFoundException extends RuntimeException {
private static final String MESSAGE = "존재하지 않는 지역 [region=%s]";

public RegionNotFoundException(String region) {
super(String.format(MESSAGE, region));
}
}
21 changes: 12 additions & 9 deletions src/main/java/com/dnd/dndtravel/map/service/MapService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import com.dnd.dndtravel.map.domain.MemberAttraction;
import com.dnd.dndtravel.map.domain.MemberRegion;
import com.dnd.dndtravel.map.domain.Region;
import com.dnd.dndtravel.map.exception.MemberAttractionNotFoundException;
import com.dnd.dndtravel.map.exception.MemberNotFoundException;
import com.dnd.dndtravel.map.exception.RegionNotFoundException;
import com.dnd.dndtravel.map.repository.dto.projection.AttractionPhotoProjection;
import com.dnd.dndtravel.map.repository.dto.projection.RecordProjection;
import com.dnd.dndtravel.map.service.dto.RegionDto;
Expand Down Expand Up @@ -45,8 +48,7 @@ public class MapService {

@Transactional(readOnly = true)
public RegionResponse allRegions(long memberId) {
//todo custom ex
Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저"));
Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId));

// 유저의 지역 방문기록 전부 가져온다
List<MemberRegion> memberRegions = memberRegionRepository.findByMemberId(memberId);
Expand All @@ -70,8 +72,8 @@ public RegionResponse allRegions(long memberId) {
@Transactional
public void recordAttraction(RecordDto recordDto, long memberId) {
// validate
Region region = regionRepository.findByName(recordDto.region()).orElseThrow(() -> new RuntimeException("존재하지 않는 지역"));
Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저"));
Region region = regionRepository.findByName(recordDto.region()).orElseThrow(() -> new RegionNotFoundException(recordDto.region()));
Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId));

Attraction attraction = attractionRepository.save(Attraction.of(region, recordDto.attractionName()));

Expand All @@ -90,7 +92,7 @@ public void recordAttraction(RecordDto recordDto, long memberId) {
@Transactional(readOnly = true)
public List<AttractionRecordResponse> allRecords(long memberId, long cursorNo, int displayPerPage) {
//validation
Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저"));
Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId));
List<MemberAttraction> memberAttractions = memberAttractionRepository.findByMemberId(memberId);
if (memberAttractions.isEmpty()) {
return List.of();
Expand All @@ -115,16 +117,17 @@ public List<AttractionRecordResponse> allRecords(long memberId, long cursorNo, i
//기록 단건 조회
@Transactional(readOnly = true)
public AttractionRecordDetailViewResponse findOneVisitRecord(long memberId, long memberAttractionId) {
Member member = memberRepository.findById(memberId).orElseThrow(() -> new RuntimeException("존재하지 않는 유저"));
MemberAttraction memberAttraction = memberAttractionRepository.findById(memberAttractionId).orElseThrow(() -> new RuntimeException("유효하지 않은 방문 상세 기록"));
//todo memberAttraction 쿼리한방으로 줄일수도 있을것같다.
Member member = memberRepository.findById(memberId).orElseThrow(() -> new MemberNotFoundException(memberId));
MemberAttraction memberAttraction = memberAttractionRepository.findById(memberAttractionId).orElseThrow(() -> new MemberAttractionNotFoundException(memberAttractionId));
return AttractionRecordDetailViewResponse.from(memberAttraction);
}

// 방문기록 수정
@Transactional
public void updateVisitRecord(RecordDto dto, long memberId, long memberAttractionId) {
//validation
MemberAttraction memberAttraction = memberAttractionRepository.findByIdAndMemberId(memberAttractionId, memberId).orElseThrow(() -> new RuntimeException("유효하지 않은 방문 상세 기록"));
MemberAttraction memberAttraction = memberAttractionRepository.findByIdAndMemberId(memberAttractionId, memberId).orElseThrow(() -> new MemberAttractionNotFoundException(memberAttractionId));

//update
memberAttraction.updateVisitRecord(dto.region(), dto.dateTime(), dto.memo());
Expand All @@ -146,7 +149,7 @@ public void updateVisitRecord(RecordDto dto, long memberId, long memberAttractio
public void deleteRecord(long memberId, long memberAttractionId) {
//validation
MemberAttraction memberAttraction = memberAttractionRepository.findByIdAndMemberId(memberAttractionId, memberId)
.orElseThrow(() -> new RuntimeException("유효하지 않은 방문 상세 기록"));
.orElseThrow(() -> new MemberAttractionNotFoundException(memberAttractionId));

List<Photo> photos = photoRepository.findByMemberAttractionId(memberAttraction.getId());
photoRepository.deleteAll(photos);
Expand Down
Loading

0 comments on commit 192b22b

Please sign in to comment.