진행 날짜 : 2025.03.31 ~ 04.02
이 프로젝트는 사용자별 일정 관리를 위한 RESTful API입니다. 사용자는 회원가입 후 로그인하여 자신의 일정을 생성하고, 수정하고, 삭제할 수 있으며, 댓글을 달 수 있습니다.
회원 관리
회원가입, 로그인, 로그아웃 기능 제공
세션을 활용한 인증 처리
일정 관리
일정 생성: 사용자가 일정 제목과 내용을 입력하여 일정 추가
일정 조회: 전체 일정 및 특정 일정 조회
일정 수정: 일정 작성자가 제목/내용을 수정 가능
일정 삭제: 일정 작성자만 해당 일정 삭제 가능
댓글 기능
일정에 대한 댓글 작성 및 조회, 삭제
일정 작성자만 댓글을 작성 가능
예외 처리 및 보안
로그인하지 않은 사용자는 일정 관리 기능 접근 불가 (401 Unauthorized 응답)
잘못된 요청 시 예외 처리 (IllegalArgumentException, UserNotFoundException 등)
@Transactional을 활용한 데이터 일관성 유지
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) 문제 상황
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) 에러 코드 작성이 어려웠던 점
4) 개선할 점
[멤버 ID 가져오는 문제]
1) 문제 상황
2) 해결 방법
기존 코드에서 세션을 만들 때는 sessionKey라는 이름을 사용했는데, 직관적으로 memberId로 변경하여 혼란을 줄임.
기존 코드
session.setAttribute("sessionKey", member.getId());
session.setAttribute("memberId", member.getId());
[일정 ID 가져오는 문제]
1) 문제 상황
2) 해결 방법
// 댓글 생성
@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) 변수명 수정이 어려웠던 부분
4) 개선할 점
일정 관리 앱을 JPA 방식으로 만들면서 jdbc방식보다 편리함을 느꼈다. 하지만 로직이 많아질수록 변수명을 가독성 좋게하고, ERD를 잘 작성해야겠다는 필요성을 느꼈다. 단계별로 나누어서 개발하면서 구현해야할 부분이 늘어나다보니 위에서 놓쳐서 새로운 단계를 구현할 때 다시 수정해야하는 번거러움이 있기에 처음에 잘 생각하고 구현하는 것이 좋을 것 같다.
또, 어노테이션을 잘 활용해야하며, import하는 것도 어떤 것을 import해야하는지 구별을 잘 해야할 것 같다. Jakarta Filter인지 다른 Filter인지 구별해야지 맞는 메소드를 활용할 수 있다.
처음 비밀번호 암호화하는 것도 배우며 자바에는 많은 메소드들이 존재한다는 것과 끊임없이 배우고 공부해야하는 것을 깨달으며 마음을 다 잡기도 했지만 하나씩 이뤄지는 모습을 보며 조금 뿌듯하기도 했다.