[Spring Boots] 일정 관리 앱 만들기

김현정·2025년 4월 3일
0

진행 날짜 : 2025.03.31 ~ 04.02

프로젝트 설명 (일정 API)

이 프로젝트는 사용자별 일정 관리를 위한 RESTful API입니다. 사용자는 회원가입 후 로그인하여 자신의 일정을 생성하고, 수정하고, 삭제할 수 있으며, 댓글을 달 수 있습니다.


주요기능

  • 회원 관리

    • 회원가입, 로그인, 로그아웃 기능 제공

    • 세션을 활용한 인증 처리

  • 일정 관리

    • 일정 생성: 사용자가 일정 제목과 내용을 입력하여 일정 추가

    • 일정 조회: 전체 일정 및 특정 일정 조회

    • 일정 수정: 일정 작성자가 제목/내용을 수정 가능

    • 일정 삭제: 일정 작성자만 해당 일정 삭제 가능

  • 댓글 기능

    • 일정에 대한 댓글 작성 및 조회, 삭제

    • 일정 작성자만 댓글을 작성 가능

  • 예외 처리 및 보안

    • 로그인하지 않은 사용자는 일정 관리 기능 접근 불가 (401 Unauthorized 응답)

    • 잘못된 요청 시 예외 처리 (IllegalArgumentException, UserNotFoundException 등)

    • @Transactional을 활용한 데이터 일관성 유지


기술 스택

  • Java 17, Spring Boot3
  • Spring Data JPA, MySQL
  • Lombok, Jakarta Validation
  • GitHub, Postman
  • IntelliJ IDEA

API 명세서


ERD 작성


SQL 작성

CREATE TABLE `member` (
    `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '사용자 고유 ID',
    `email` VARCHAR(50) NOT NULL UNIQUE COMMENT '이메일 (로그인 ID)',
    `password` VARCHAR(255) NOT NULL COMMENT '비밀번호',
    `username` VARCHAR(50) NOT NULL COMMENT '사용자 이름',
    `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '가입일',
    `modifiedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일'
);

CREATE TABLE `scheduler` (
    `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '일정 고유 ID',
    `title` VARCHAR(50) NOT NULL COMMENT '일정 제목',
    `contents` TEXT NOT NULL COMMENT '일정 내용',
    `memberId` BIGINT NOT NULL COMMENT '일정 생성자 ID',
    `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
    `modifiedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일',
    CONSTRAINT `FK_SCHEDULER_MEMBER` FOREIGN KEY (`memberId`) REFERENCES `member`(`id`) ON DELETE CASCADE
);

CREATE TABLE `comment` (
    `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '댓글 고유 ID',
    `content` VARCHAR(255) NOT NULL COMMENT '댓글 내용',
    `memberId` BIGINT NOT NULL COMMENT '댓글 작성자 ID',
    `schedulerId` BIGINT NOT NULL COMMENT '댓글이 달린 일정 ID',
    `createdAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '생성일',
    `modifiedAt` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정일',
    CONSTRAINT `FK_COMMENT_MEMBER` FOREIGN KEY (`memberId`) REFERENCES `member`(`id`) ON DELETE CASCADE,
    CONSTRAINT `FK_COMMENT_SCHEDULER` FOREIGN KEY (`schedulerId`) REFERENCES `scheduler`(`id`) ON DELETE CASCADE
);

역할들 정리



진행 중 어려웠던 점과 해결방법

1. 예외처리 문제

1) 문제 상황

  • 프로젝트에서 다양한 예외가 발생하는데, (사용자가 id가 일치하지 않는 상황, 이메일을 못찾는 상황 등등) 이를 일일이 컨트롤러에서 처리하면 코드가 길어지고 관리가 어려웠음.
  • 예외 발생 시 일관 된 에러 응답을 반환해야 하지만 컨트롤러마다 예외처리가 다르게 이루어지기에
    핸들러를 작성하기로 함.

2) 전역 예외 처리 핸들러 (GlobalExceptionHandler) 추가

  • @RestControllerAdvice를 사용하여 애플리케이션 전반에서 발생하는 예외를 한 곳에서 관리하도록 변경함.

  • GlobalExceptionHandler 구현

package com.example.scheduler.handler;

import com.example.scheduler.exception.EmailNotFoundException;
import com.example.scheduler.exception.InvalidPasswordException;
import com.example.scheduler.exception.UserNotFoundException;
import lombok.AllArgsConstructor;
import lombok.Getter;
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 java.util.List;
import java.util.stream.Collectors;


@RestControllerAdvice
public class GlobalExceptionHandler {

    // 아이디를 찾을 수 없는 예외 처리
    @ExceptionHandler(UserNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException e) {
        ErrorResponse errorResponse = new ErrorResponse(
                e.getCode(),
                e.getMessage(),
                e.getStatus(),
                List.of(new FieldErrorResponse("username", e.getMessage()))
        );

        return new ResponseEntity<>(errorResponse, e.getStatus());
    }

    // 비밀번호가 틀린 예외 처리
    @ExceptionHandler(InvalidPasswordException.class)
    public ResponseEntity<ErrorResponse> handleInvalidPasswordException(InvalidPasswordException e) {
        ErrorResponse errorResponse = new ErrorResponse(
                e.getCode(),
                e.getMessage(),
                e.getStatus(),
                List.of(new FieldErrorResponse("password", e.getMessage()))
        );
        return new ResponseEntity<>(errorResponse, e.getStatus());
    }

    // 이메일이 틀린 예외 처리
    @ExceptionHandler(EmailNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleEmailNotFoundException(EmailNotFoundException e) {
        ErrorResponse errorResponse = new ErrorResponse(
                e.getCode(),
                e.getMessage(),
                e.getStatus(),
                List.of(new FieldErrorResponse("email", e.getMessage()))
        );
        return new ResponseEntity<>(errorResponse, e.getStatus());
    }


    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResponse> methodArgumentNotValidException (MethodArgumentNotValidException e){

        List<FieldErrorResponse> fieldErrors = e.getBindingResult().getFieldErrors().stream()
                .map(fieldError -> new FieldErrorResponse(fieldError.getField(), fieldError.getDefaultMessage()))
                .collect(Collectors.toList());

        return ResponseEntity.badRequest().body(new ErrorResponse("E001", "잘못된 입력값입니다.",HttpStatus.BAD_REQUEST, fieldErrors));
    }

    @Getter
    @AllArgsConstructor
    public static class ErrorResponse {
        private String code;
        private String message;
        private HttpStatus status;
        private List<FieldErrorResponse> errors;
    }
    @Getter
    @AllArgsConstructor
    public static class FieldErrorResponse{
        private String field;
        private String message;
    }
}

3) 에러 코드 작성이 어려웠던 점

  • ResponseEntity<>를 활용하여 에러코드를 구현하기 전에 Map을 사용했던 문제
  • 작성을 거의 해 본적이 없어서 구글링을 통해서 작성을 하였음.
  • Map<String, String>을 활용하여 JSON 형태로 에러 메시지를 반환했지만 튜터님께서 ResponseEntity<>를 활용을 하는 것이 좋을 것 같다고 피드백을 해주셔서 다시 작성
  • ErrorResponse를 적는 과정에서 어디에 적을지 몰라 헤매기도 했지만 양이 적어 관리가 쉬우니 핸들러 안에 입력함.

4) 개선할 점

  • 공통 로직이 있는 예외는 CustomException을 활용하여 사용하고, 특수한 예외는 새로 만들어서 관리하기.
  • Error라는 패키지를 만들어서 핸들러, 예외, Error코드를 한번에 관리하기

2. 댓글 생성 시 멤버 ID와 일정 ID 가져오는 문제

[멤버 ID 가져오는 문제]

1) 문제 상황

  • 멤버 ID를 세션에서 가져올 때 sessionKey로 받아서 멤버 ID로 받아올 수 없었음.
  • 댓글을 생성 할 시에는 멤버를 생성하고 로그인을 하여 일정을 만들고 그 일정에 댓글을 달기에 로그인을 할 시에 멤버ID를 받아와야하는데 못받아오는 문제가 생김

2) 해결 방법

  • 기존 코드에서 세션을 만들 때는 sessionKey라는 이름을 사용했는데, 직관적으로 memberId로 변경하여 혼란을 줄임.

  • 기존 코드

session.setAttribute("sessionKey", member.getId());
  • 수정 코드
session.setAttribute("memberId", member.getId());

[일정 ID 가져오는 문제]

1) 문제 상황

  • id를 가져오는 상황에서 댓글id, 멤버id, 일정id 구분이 안가서 무엇을 받아오는지에 대한 혼란.
  • @PathVariable Long Id 라고만 적어서 변수명을 전체적으로 다 바꿀지 고민

2) 해결 방법

  • 기존 코드에서는 id라는 변수명을 사용했기에 이게 댓글 id인지 일정의 id인지 햇갈려서 명확한 변수명(schedulerId)를 사용하여 가독성 높임
// 댓글 생성
    @PostMapping
    public ResponseEntity<CommentResponseDto> saveComment (@PathVariable Long schedulerId, HttpServletRequest request, @Valid @RequestBody CommentRequestDto requestDto){
        // 로그인 id 받아오기
        HttpSession session = request.getSession();
        Long memberId = (Long) session.getAttribute("memberId");

        // 로그인 확인 여부
        if(memberId == null ) {
            return new ResponseEntity<>(HttpStatus.UNAUTHORIZED);
        }

        CommentResponseDto commentResponseDto = commentService.save(schedulerId, memberId, requestDto.getContent());

        return new ResponseEntity<>(commentResponseDto, HttpStatus.CREATED);
    }

3) 변수명 수정이 어려웠던 부분

  • 모든 코드들의 id 변수명들을 전부 바꿔야하나 고민했음. PK키인 id의 변수가 바뀌면 구현한 코드들의 id 변수명을 바꿔야하기에 이게 맞나 고민을 함.
  • 하지만 댓글 생성하는 CRUD를 구현할 때만 변수명을 지정할 수 있어서 편했음.

4) 개선할 점

  • 변수명을 만들때 미리 만들 코드들을 생각하고 만들기!

느낌점

일정 관리 앱을 JPA 방식으로 만들면서 jdbc방식보다 편리함을 느꼈다. 하지만 로직이 많아질수록 변수명을 가독성 좋게하고, ERD를 잘 작성해야겠다는 필요성을 느꼈다. 단계별로 나누어서 개발하면서 구현해야할 부분이 늘어나다보니 위에서 놓쳐서 새로운 단계를 구현할 때 다시 수정해야하는 번거러움이 있기에 처음에 잘 생각하고 구현하는 것이 좋을 것 같다.

또, 어노테이션을 잘 활용해야하며, import하는 것도 어떤 것을 import해야하는지 구별을 잘 해야할 것 같다. Jakarta Filter인지 다른 Filter인지 구별해야지 맞는 메소드를 활용할 수 있다.

처음 비밀번호 암호화하는 것도 배우며 자바에는 많은 메소드들이 존재한다는 것과 끊임없이 배우고 공부해야하는 것을 깨달으며 마음을 다 잡기도 했지만 하나씩 이뤄지는 모습을 보며 조금 뿌듯하기도 했다.

0개의 댓글