[회고] 일정 API 과제 회고

wannabeing·2025년 3월 26일
4

SPARTA-회고

목록 보기
3/5

✅ 개요

  • 개발기간: 2025.03.19(수) ~ 2025.03.26(수)
  • 자바버전: OpenJDK 17
  • 사용기술: Spring Boot, JDBC Template, MySQL
  • RESTful한 일정 관리 백엔드 API 서버
  • 프로젝트 링크: GitHub Repository

✅ 요구사항 정의

💪🏻 필수 기능

  • [CREATE] Schedule 생성
    • Schedule: 할일, 작성자명,비밀번호, 작성일, 수정일을 저장
    • Schedule은 고유 식별자(ID)를 자동으로 생성하여 관리
    • 최초 입력 시, 작성일수정일은 동일
  • [READ] 전체 Schedule 조회
    • 수정일 내림차순으로 정렬하여 조회
  • [READ] 단건 Schedule 조회
    • 선택한 Schedule 정보를 조회
    • 고유 식별자(ID)를 사용하여 조회
  • [UPDATE] 단건 Schedule 수정
    • Schedule 내용 중에 할일만 수정 가능
    • 서버에 요청할 때, 비밀번호를 함께 전달
    • 수정일을 수정완료 시점으로 변경
    • 비밀번호가 맞아야 수정 가능

🚀 도전 기능

  • [User 테이블] Schedule과 연관관계 있는 User 테이블 생성
    • User: 이름, 이메일, 등록일, 수정일
    • User의 고유 식별자(ID)를 통해 Schedule 검색 될 수 있도록 함
    • User의 고유 식별자(ID)가 Schedule 테이블의 FK가 될 수 있도록 함
  • [페이지네이션] 전체 Schedule 조회 기능에 페이지네이션 추가
    • 사용자에게 페이지 번호, 페이지 크기를 쿼리 파라미터로 받음
    • 전달받은 파라미터 기준으로 필요한 데이터를 조회하고 반환
    • 조회한 일정 목록에 작성자 이름 포함
    • 범위를 넘을 경우에 빈 배열을 반환
  • [Exception] 예외처리

    • 필요에 따라 Custom 에외 클래스를 생성하여 예외 처리
    • @RestControllerAdvice을 활용하여 공통 예외 처리
    • 예외가 발생할 경우, HTTP 상태코드와 함께 사용자에게 메시지 전달
    • 수정,삭제시 비밀번호가 일치하지 않을 때 예외 발생
    • 선택한 정보를 조회할 수 없을 때 예외 발생
  • [Validated] 유효성 검사 및 기타 사항

    • Spring에서 제공하는 @Validated 어노테이션 사용
    • 할일은 최대 200자 이내로 제한, 필수값 처리
    • 비밀번호는 필수값 처리

✅ Keep

현재 프로젝트에서 만족했고, 앞으로의 훈련기간에서 지속하고 싶은 부분을 작성했다.

  • 스프링의 동작방식에 대해 이해해보고자 노력했다
  • 스트림을 적절하게 적용해보았다고 생각한다.
  • 요구사항을 깊이 고민하고 구현하는 과정을 반복하고 있다.
  • 매일 코드카타를 통해 논리적인 접근을 계속해서 하고자 한다.
  • 앞으로 남은 훈련 과정에서도 주어진 과제에 적극적으로 임하며, 더 완성도 높은 결과물을 만들고 싶다.

✅ Problem

현재 프로젝트에서 어려웠던 점과 아쉬웠던 점을 작성했다.

- 1️⃣ ResponseEntity<T> 너 T야???

// [Controller] Schedule 생성 Controller
@PostMapping
public ResponseEntity<T> createSchedule(
        @Valid @RequestBody ScheduleRequestDto requestDto,
        BindingResult bindingResult) {

    // ✅ 1-1. 유효성 검사 실패 시
    if (bindingResult.hasErrors()) {
    	// 에러상태, 에러메시지를 저장하는 변수 선언
        Map<String, String> errors = new HashMap<>();
        
        // error 메시지를 저장
        for (FieldError error : bindingResult.getFieldErrors()) {
            errors.put(error.getField(), error.getDefaultMessage());
        }
        
        // ✅ 1-2. Map<String, String> 타입을 반환
        return ResponseEntity
               .status(HttpStatus.BAD_REQUEST).body(errors);
    }

    // ✅ 2-1. 유효성 검사 성공시, scheduleService 실행
    ScheduleResponseDto responseDto = scheduleService
                                      .createSchedule(requestDto);
    
    // ✅ 2-2. ResponseDto 타입을 반환
    return ResponseEntity
           .status(HttpStatus.CREATED).body(responseDto);
}
  • createSchedule 메서드의 반환타입은 T(제네릭) 인데
    검사 실패시에는 Map<String, String>이 반환되고,
    검사 성공시에는 ResponseDto 타입이 반환된다..
  • 유효성 검사는 해야겠고.. 응답객체로 반환해야겠고..
    아직 @RestContollerAdvice 어노테이션을 알기 전이라 고민이 되었다.

- 2️⃣ 해결방안: 제네릭 와일드 카드 사용 및 @RestControllerAdvice 사용

@PostMapping
// ✅ ? 제네릭 와일드 사용
public ResponseEntity<?> createSchedule(
        @Valid @RequestBody ScheduleRequestDto requestDto,
        BindingResult bindingResult) {
        ....
}
  • 반환 타입을 ResponseEntity<?>로 변경했다.
  • <?>는 모든 타입을 허용하는 제네릭 와일드카드라고 하며, 자유도가 높은 키워드이다.
  • 따라서 아무때나 사용하면 안되고, 보통 <? extends 상위타입>으로
    제한해서 사용한다고 한다.
  • 공변성/반공병성 등, 아직 공부해야 될게 많은 부분이 있었다.

@PostMapping
 public ResponseEntity<ScheduleResponseDto> createSchedule(
 			@Validated(ScheduleRequestDto.OnCreate.class)
            @RequestBody ScheduleRequestDto dto,
            @ModelAttribute("validUserId") Long validUserId
    ){
        // 생성된 일정 응답 객체 반환
        return new ResponseEntity<>(scheduleService
              .createSchedule(validUserId, dto), HttpStatus.CREATED);
    }
  • 이후에는 @RestControllerAdvice 어노테이션으로 전역 예외처리 핸들링을 하였고, BindingResult 에러 핸들링 부분을 제거하였다.
  • @Valid 대신에 @Validated 어노테이션을 사용하였다!

- 1️⃣ 비밀번호 암호화는 해야되지 않을까?

일정 관리를 하면서 생성/수정/삭제할 때, 비밀번호를 받아서 관리하고 있다.
요구사항에 따로 암호화 부분은 없었지만, 그래도 해야 된다 생각하고 구현하게 되었다.

- 2️⃣ 해결방안: Spring Security를 사용해보자!

  • BCrypt hashing 알고리즘을 제공하는 SpringSecurity를 사용하였다.
  • 설치 후에 모든(GET/POST) 요청에 403 Forbidden 응답이 반환되었다..
    스프링 시큐리티에서는 CSRF 토큰을 이용하고 있는데, 매 요청마다 CSRF 토큰은 서버에서 임의의 난수를 클라이언트에게 넘겨주는 보안설정이 되어 있기 때문이었다.
  • 따라서 CRSF 필터 해제를 통해 사용할 수 있게 되었다.

// 0. 선언
@Bean
public PasswordEncoder passwordEncoder() {
	return new BCryptPasswordEncoder();
}

// 1. 비밀번호 필드 암호화 로직
String encryptedPassword = passwordEncoder.encode(dto.getPassword());

// 2. 비밀번호 비교 로직
if (!passwordEncoder.matches(
	dto.getPassword(), existSchedule.getPassword())) 
{
	throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "비밀번호가 일치하지 않습니다.");
}
  • SecurityConfig 클래스로 선언을 하였다.
  • Service Layer에서 생성자로 선언하고, 위와 같은 로직으로 사용하였다.

- 1️⃣ RequestDto로 수정시에는 2개의 필드, 삭제시에는 1개의 필드를 받고 싶어용

@Getter
public class ScheduleRequestDto {
	@NotBlank(message = "할 일을 입력해주세요.")
    private String todo;
 
    @NotBlank(message = "비밀번호를 입력해주세요.")
    private String password;
}
  • RequestDto 하나로 생성/수정/삭제 모든 RequestBody 받고자 한다.
  • 위와 같이 설정하면 삭제 요청 시에도 todo 필드를 받아야 한다..
  • ScheduleDeleteRequestDto를 만들어야 되나 고민했지만,
    다른 방법이 있을거 같아서 구글링 및 지피티를 이용해보았다.

- 2️⃣ 해결방안: "유효성 그룹 인터페이스" 사용

@Getter
public class ScheduleRequestDto {
    // ✅ 유효성 그룹 인터페이스 정의
    // ✅ 3개로 구분 (Create, Update, Delete)
    public interface OnCreate {}
    public interface OnUpdate {}
    public interface OnDelete {}

	// ✅ todo 필드는 Create, Update 할 때, 필수 필드로 설정
    @NotBlank(message = "할 일을 입력해주세요.", groups = {OnCreate.class, OnUpdate.class})
    @Size(max = 200, message = "할 일은 200자 이내로 입력해주세요.", groups = {OnCreate.class, OnUpdate.class})
    private String todo;

	// ✅ pasword 필드는 Create, Update, Delete 할 때, 필수 필드로 설정
    @NotBlank(message = "비밀번호를 입력해주세요.", groups = {OnCreate.class, OnUpdate.class, OnDelete.class})
    @Size(max = 20, message = "비밀번호는 20자 이내로 입력해주세요.", groups = {OnCreate.class, OnUpdate.class})
    private String password;
}
// Controller
@DeleteMapping("/{scheduleId}")
public ResponseEntity<ApiResponseDto> deleteSchedule(
			@Validated(ScheduleRequestDto.OnDelete.class) @RequestBody ScheduleRequestDto dto,
            @ModelAttribute("validUserId") Long validUserId,
            @PathVariable Long scheduleId
    ){
        // 프론트에게 응답메시지 보내기 위해 204(NO_CONTENT) 대신 200(OK) 반환
        return new ResponseEntity<>(scheduleService.deleteSchedule(validUserId, scheduleId, dto.getPassword()), HttpStatus.OK);
    }
  • 상황별로 유효성 검증을 수행하고자 할 때 사용한다.
  • 그룹인터페이스를 선언하고, 필드에 groups 속성을 통해 지정한다.
  • Controller에서 @Valid는 기본 그룹만 검증하기 때문에, @Validated를 사용하여 그룹검증을 한다.

🤔 @Validated, @Valid 둘 중 어떤 것을 사용하지?

  • 그룹 기능이 필요하면 @Validated를 사용하고, 그렇지 않다면 더 편한 것을 사용한다고 한다. 팀에 사황에 따라 다르다고 한다!
  • @Valid는 자바에서 제공하고, @Validated는 스프링에서 제공하는 어노테이션
  • 둘을 사용했을 때 발생하는 예외도 다르다.
    @Valid에 의한 예외 : MethodArgumentNotValidException
    @Validated에 의한 예외 : ConstraintViolationException

✅ Try

다음 프로젝트에서 시도해볼 점들을 작성했다.

  • 주어진 강의를 좀 더 집중하여, 빨리 봐야될 것 같다..!
  • 요청/응답에 대한 처리 시퀀스에 대해 공부해야겠다.
  • 공부할게 산더미라서 천천히, 차근차근 해보자.
  • 내 코드의 구조와 동작원리를 명확히 이해하고, 설명하기

인프런 회고 문화

profile
wannabe---ing

0개의 댓글