본문 바로가기

카테고리 없음

Spring 미니 프로젝트 [RN & Spring] - 리팩터링 (1)

이번에는 피드백 받았던 부분들과 스프링의 강점인 객체 지향을 제대로 활용할 수 있도록 리팩터링을 진행할 예정이다.

 

@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