이번에는 피드백 받았던 부분들과 스프링의 강점인 객체 지향을 제대로 활용할 수 있도록 리팩터링을 진행할 예정이다.
@RequestMapping
API가 여러 개이면 공통된 주소를 적어주는 것이 좋다. 마치 학급의 급훈처럼 걸어두는 느낌이라 생각하자
지금은 Controller 내에 메서드가 하나이지만 더 증가하면 지저분해진다.
@RestController
// 공통 주소가 없음
public class MemberController {
@GetMapping("/api/members/{memberId}") // 😫 반복!
public MemberResponse getMember(...) { ... }
@GetMapping("/api/members/{memberId}/histories") // 😫 반복!
public List<HistoryResponse> getHistories(...) { ... }
@PostMapping("/api/members") // 😫 반복!
public void createMember(...) { ... }
}
@RestController
@RequestMapping("/api/members") // 👍 공통 주소 선언!
public class MemberController {
// 실제 주소: /api/members/{memberId}
@GetMapping("/{memberId}")
public MemberResponse getMember(...) { ... }
// 실제 주소: /api/members/{memberId}/histories
@GetMapping("/{memberId}/histories")
public List<HistoryResponse> getHistories(...) { ... }
// 실제 주소: /api/members (POST)
@PostMapping
public void createMember(...) { ... }
}
- 가독성: 이 클래스는 /api/members 와 관련된 일만 하는구나 라고 한 눈에 알 수 있다.
- 수정 용이: URL이 변경되어도 공통 부분을 고치면 해결할 수 있다.
IDOR(Insecure Direct Object Reference)
부적절한 인가: 악의적인 사용자 조작 문제가 발생할 수 있다.
'도둑'이라는 유저가 걸음 수 요청을 가로채서 JSON을 조작하여 'user1'으로 바꿔서 요청을 보내면
서버는 조작된 줄 모르고 'user1'의 걸음 수 데이터를 바꿔준다.
"남의 일기장에 내가 몰래 글을 쓰는 것"
- 해결: 토큰에서 꺼내기
- 로그인할 때 발급해준 토근(JWT)안에 들어있는 ID를 믿고 작업을 처리한다.
- DTO에서 아이디를 보내지 않고 Spring Security를 사용
- 컨트롤러가 넘겨준 검증된 ID로 서비스 로직을 처리한다.
우선, 전반적인 코드 구현을 끝마치고 JWT를 도입해보자.
에러 코드 모음
보통 ErrorCode는 Enum으로 관리한다.
서비스 A, 서비스 B 에서 각각 다르게 오류를 내보내면 프로젝트가 커졌을 때 처리할 일이 정말 많다.
enum으로 관리하면 통일성 있는 오류 메시지를 출력할 수 있다.
명확한 오류를 설정할 수 있다. 404, 403 등,,
package NetZero.global.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public enum ErrorCode {
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 에러입니다. 관리자에게 문의하세요."),
INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "잘못된 입력값입니다."),
// 상황: DB에 없는 ID로 조회/저장 하려고 할 때
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 회원입니다."),
// 상황: 걸음 수가 음수로 들어오거나, 비정상적인 수치일 때
INVALID_STEP_COUNT(HttpStatus.BAD_REQUEST, "걸음 수는 0 이상이어야 합니다."),
// 상황: 미래 날짜의 걸음 수를 저장하려고 할 때 (유효성 검사)
INVALID_STEP_DATE(HttpStatus.BAD_REQUEST, "미래의 걸음 수는 저장할 수 없습니다."),
// 상황: 존재하지 않는 쿠폰을 조회하려고 할 때
COUPON_NOT_FOUND(HttpStatus.NOT_FOUND, "존재하지 않는 쿠폰입니다."),
// 상황: 이미 사용한 쿠폰을 또 쓰려고 할 때
ALREADY_USED_COUPON(HttpStatus.BAD_REQUEST, "이미 사용된 쿠폰입니다.");
private final HttpStatus httpStatus;
private final String message;
}
이런 식으로 오류를 전체적으로 관리하고
package NetZero.global.exception;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final ErrorCode errorCode;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
커스텀 Exception을 사용한다.
.orElseThrow(() -> new IllegalArgumentException("존재하지 않는 ID입니다."));
//After
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
이런 식으로 작성할 수 있다!
@Transactional
public void saveStep(StepRequest stepRequest) {
Member member = memberRepository.findById(stepRequest.getUserId()).orElseThrow(
() -> new BusinessException(ErrorCode.USER_NOT_FOUND)
);
StepInfo stepInfo = new StepInfo(member, stepRequest.getStepCount(), stepRequest.getRecordDate());
stepInfoRepository.save(stepInfo);
}
--------------
public MemberResponse getMemberInfo(String memberId){
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new BusinessException(ErrorCode.USER_NOT_FOUND));
return new MemberResponse(member);
}
내 코드에 직접 작성해보았다.

존재하지 않는 회원에 해당하는 걸음 수를 저장하려고 하면 이런 오류가 뜨는 것을 확인했다!
만보기 로직 점검
클라이언트는 걸음 수를 폰 내부(로컬)에 가지고 있다가 꼭 필요할 때만 서버에 던져줘야 한다.
- 앱이 백그라운드로 내려갈 때
- 이때까지의 기록을 서버에 안전하게 저장
- 앱이 켜질 때
- 서버에 있는 데이터와 로컬에 있는 데이터를 비교하여 보여준다.
지금 내 코드상으로 보면 걸음 수 저장이 무분별하게 쌓이게 된다.
따라서 같은 날짜의 기록이 있으면 Update하고 없으면 Insert하는 로직으로 변경해야 했다.
@Transactional
public void saveStep(StepRequest stepRequest) {
Member member = memberRepository.findById(stepRequest.getUserId()).orElseThrow(
() -> new BusinessException(ErrorCode.USER_NOT_FOUND)
);
StepInfo existingStep = stepInfoRepository.findByMemberAndRecordDate(member, stepRequest.getRecordDate()).orElse(null);
if(existingStep != null){
existingStep.updateSteps(stepRequest.getStepCount());
}else{
StepInfo newStep = new StepInfo(member, stepRequest.getStepCount(), stepRequest.getRecordDate());
stepInfoRepository.save(newStep);
}
}
걸음 수 데이터가 하루에 한 줄 생기도록 한다.
하루에 앱을 여러 번 동기화해도 DB에는 해당하는 날짜 한 줄만 생긴다.



로직이 잘 작동함을 확인할 수 있다.
간단한 보완을 진행했다. 다음엔 스웨거 문서화를 해보고 테스트 코드 작성 후에 프론트와 연동하기 위해
리액트 네이티브 코드를 다룰 예정이다1