일정 관리 앱 with Spring

유건우·2024년 9월 26일

프로젝트

목록 보기
3/9
post-thumbnail




ERD 설계

Schedule

  • schedule_id

    • schedule 테이블의 기본키 입니다.
  • user_id

    • user 엔티티를 조회를 위한 외래키입니다.
  • todo_list

    • 오늘 할일을의미하는 필드입니다.
    • varchar 형태로 데이터가 담깁니다. 데이터 크기가 클 경우는 Lob을 사용해야합니다.
  • created_at

    • 생성시간을 의미하는 필드입니다.
    • TimeStamp 형식으로 기록됩니다.
  • updated_at

    • 데이터가 업데이트될때 갱신되는 필드입니다.
    • TimeStamp 형식으로 기록됩니다.



User

  • schedule_id

    • 스케쥴 테이블의 외래키입니다.
    • 양방향 연관관계를 위한 것입니다.
  • user_id

    • User 테이블의 기본 키입니다.
  • username

    • 유저 이름을 의미합니다.
  • password

    • 유저 비밀번호를 의미합니다.
  • email

    • 유저 이메일을 의미하는 필드입니다.
  • create_at

    • 생성 시간을 의미하는 필드입니다.
    • TimeStamp 형식으로 기록됩니다.
  • updated_at

    • 업데이트 시간을 의미하는 필드입니다.
    • TimeStamp 형식으로 기록됩니다.





POST : /api/schedule


HTTP Request Body - Json

{
    "username" : "user1",
    "password" : "1234", 
    "todoList" : "today study spring", 
    "email" : "asdf@naver.com"
}



Controller

 @PostMapping("/schedule")
public UUID addSchedule(@RequestBody RequestScheduleWithUserDto requestScheduleWithUserDto) {
		return scheduleService.save(requestScheduleWithUserDto);
}
  • HttpBody에 담겨 있는 Json을 서비스 계층으로 전달해줍니다.



Service

@Transactional
public UUID save(RequestScheduleWithUserDto requestScheduleWithUserDto) {
		UUID scheduleId = UUID.randomUUID();
		UUID userId = UUID.randomUUID();
		LocalDateTime now = LocalDateTime.now();
		userRepository.add(scheduleId, userId, now, requestScheduleWithUserDto);
		scheduleRepository.add(scheduleId, userId, now, requestScheduleWithUserDto);
		return scheduleId;
}
  • UUID 를 공통적으로 생성합니다.
  • userRepository, scheduleRepository 로 데이터를 담아서 전달해줍니다.
  • @Transactional원자성을 만족하기위한 코드입니다. 항상 User가 먼저 저장되어야 합니다.



Repository

public void add(UUID scheduleId, UUID userId, LocalDateTime now, RequestScheduleWithUserDto requestScheduleWithUserDto) {
		Schedule schedule = new Schedule(scheduleId, userId, requestScheduleWithUserDto.getTodoList(), now, now);
		String sql = "insert into schedule(schedule_id, user_id, todo_list, created_at, updated_at) values (?, ?, ?, ?, ?)";
		jdbcTemplate.update(sql, schedule.getScheduleId().toString(), schedule.getUserId().toString(), schedule.getTodoList(), schedule.getCreatedAt(), schedule.getUpdatedAt());
}
public void add(UUID scheduleId, UUID userId, LocalDateTime now, RequestScheduleWithUserDto dto) {
		User user = new User(userId, scheduleId, dto.getUsername(), dto.getPassword(), dto.getEmail(), now, now);
		String sql = "insert into user(user_id, schedule_id, username, password, email, created_at, updated_at) values(?, ?, ?, ?, ?, ?, ?)";
		jdbcTemplate.update(sql, user.getUserId().toString(), user.getScheduleId().toString(), user.getUsername(), user.getPassword(), user.getEmail(), user.getCreatedAt(), user.getUpdatedAt());
}
  • jdbcTemplate 을 이용하여 데이터를 저장해줍니다.




GET : /api/{scheduleId}



Http Response Body - Json

{
    "scheduleId": "17953acc-5943-449c-9a2d-7a0c087cf1fe",
    "userId": "93fae0ef-1ae9-48af-9e6b-f5f2c769bbfc",
    "todoList": "today study spring",
    "username": "user1",
    "email": "asdf@naver.com",
    "createdAt": "2024-09-26T21:46:45",
    "updatedAt": "2024-09-26T21:46:45"
}



Controller

@GetMapping("/schedule/{scheduleId}")
public ResponseEntity<ResponseDetailsScheduleDto> getSchedule(@PathVariable String scheduleId) {
		return ResponseEntity.ok(scheduleService.getTodoList(UUID.fromString(scheduleId)));
}
  • scheduleId를 쿼리 파라미터로 할당받아 서비스 계층으로 전달해줍니다.



Service

 public ResponseDetailsScheduleDto getTodoList(UUID scheduleId) {
		return scheduleRepository.findScheduleById(scheduleId);
}



Repository

public ResponseDetailsScheduleDto findScheduleById(UUID scheduleId) {
		String sql = "select s.schedule_id, s.user_id, s.todo_list, u.username, u.email, s.created_at, s.updated_at from schedule s join user u on s.user_id = u.user_id where s.schedule_id = ?";
		return jdbcTemplate.queryForObject(sql, new ScheduleDetailsRowMapper(), scheduleId.toString());
}
  • Join 을 통해 데이터를 가져온후 DTO맵핑합니다.




PUT : /api/{scheduleId}



HTTP Request Body - Json

{
    "todoList" : "TIL 작성하기", 
    "password" : "1234"
}



변경 결과 확인

  • 원래 데이터인 today study spring 에서 TIL 작성하기 로 변경되었습니다.



Controller

@PutMapping("/schedule/{scheduleId}")
public UUID updateSchedule(@PathVariable String scheduleId, @RequestBody UpdateTodoList updateTodoList) throws BadRequestException {
		return scheduleService.updateSchedule(UUID.fromString(scheduleId), updateTodoList);
}
  • scheduleId 는 쿼리파라미터로 전달받습니다.
  • 업데이트할 todoList비밀번호Http Body 를 통해서 전달받습니다.


Service

public UUID updateSchedule(UUID scheduleId, UpdateTodoList updateTodoList) throws BadRequestException {
		String password = findUserById(scheduleId);

		if (!password.equals(updateTodoList.getPassword())) {
				throw new BadRequestException("비밀번호가 일치하지 않습니다.");
		}

		return scheduleRepository.update(scheduleId, updateTodoList);
}
  • 스케쥴아이디를 통해 유저 정보를 가져옵니다.
  • 비밀번호를 확인하고 비밀번호가 맞지 않다면 BadRequestException 예외를 던져줍니다.



Repository

public UUID update(UUID scheduleId, UpdateTodoList updateData) {
		String sql = "update schedule set todo_list = ?, updated_at = ? where schedule_id = ?";
		jdbcTemplate.update(sql, updateData.getTodoList(), LocalDateTime.now(), String.valueOf(scheduleId));
		return scheduleId;
}




DELETE : /api/{scheduleId}


HTTP Request Body - Json

{
    "password" : "1234"
}



Controller

@DeleteMapping("/schedule/{scheduleId}")
public void deleteSchedule(@PathVariable String scheduleId, @RequestBody Map<String, String> request) throws BadRequestException {
		scheduleService.deleteSchedule(UUID.fromString(scheduleId), request.get("password"));
}
  • 쿼리파라미터를 통해 scheduleId를 전달받습니다.
  • 바디에 있는 데이터는 하나이기때문에 Map을 사용하여 값을 가져옵니다.
  • 바디에 있는 데이터는 HttpServletRequest.getParameter() 를 통해 가져와도 됩니다.



Service

@Transactional
public void deleteSchedule(UUID scheduleId, String requestPassword) throws BadRequestException {
		String password = findUserById(scheduleId);
		if (!password.equals(requestPassword)) {
				throw new BadRequestException("비밀번호가 일치하지 않습니다.");
		}
		scheduleRepository.deleteScheduleById(scheduleId);
		userRepository.deleteUser(scheduleId);
}
  • 비밀번호를 확인하고 비밀번호가 맞지 않다면 예외를 발생시킵니다.
  • 예외가 발생되지 않았다면 scheduleRepositoryuserRepository를 호출하여 데이터 삭제를 진행시켜줍니다.
  • 데이터 삭제시 스케쥴 테이블부터 삭제시켜주어야합니다.


Repository

public void deleteScheduleById(UUID scheduleId) {
		String sql = "delete from schedule where schedule_id = ?";
		jdbcTemplate.update(sql, scheduleId.toString());
}
  • 비밀번호를 서비스 계층에서 확인하였다면 데이터를 삭제시켜줍니다.




GET : /api/schedules?limit={limit}&offset={offset}



HTTP Response Body - Json

[
    {
        "scheduleId": "c5994268-f0d2-4ac0-a82c-1742617cb99f",
        "userId": "4445ec97-7582-4ebb-a207-5967529d8458",
        "todoList": "today study spring cloud",
        "username": "user5",
        "createdAt": "2024-09-26T22:16:36",
        "updatedAt": "2024-09-26T22:16:36"
    },
    {
        "scheduleId": "75128456-10f7-499a-a3b5-6de77f043c09",
        "userId": "052d5d9b-6607-456e-8a59-2b2c6390cf04",
        "todoList": "today study spring AI",
        "username": "user4",
        "createdAt": "2024-09-26T22:16:28",
        "updatedAt": "2024-09-26T22:16:28"
    },
    {
        "scheduleId": "12fdc960-9824-4826-bdfb-bbba9c6941b3",
        "userId": "3849e325-ef79-4608-babb-aa73393ad9d7",
        "todoList": "today study spring batch",
        "username": "user3",
        "createdAt": "2024-09-26T22:16:20",
        "updatedAt": "2024-09-26T22:16:20"
    },
    {
        "scheduleId": "db606d7b-5856-4264-b0c9-905093f39bcc",
        "userId": "c147acdb-b213-4f61-a523-12bfb4b5f7c8",
        "todoList": "today study spring security",
        "username": "user2",
        "createdAt": "2024-09-26T22:16:14",
        "updatedAt": "2024-09-26T22:16:14"
    },
    {
        "scheduleId": "918e2248-29ca-412e-98e5-34afa83e36f4",
        "userId": "659cadf6-b916-4563-a309-42c65a7ba0f5",
        "todoList": "today study spring",
        "username": "user1",
        "createdAt": "2024-09-26T22:16:07",
        "updatedAt": "2024-09-26T22:16:07"
    }
]
  • 첫번째 페이지의 5개의 게시글을 가져옵니다.



Controller

@GetMapping("/schedules")
public ResponseEntity<List<ResponseScheduleDto>> getAllPost(@RequestParam(defaultValue = "10") int limit, @RequestParam(defaultValue = "1") int offset) {
		return ResponseEntity.ok(scheduleService.getAllTodoList(limit, offset));
}
  • @RequestParam 옵션에 required = true 을 적용하여 무조건 limit, offset 을 받아줘도 되지만 defaultValue를 이용하여 입력받지 않더라도 기본적으로 첫번째 페이지부터 10개의 게시글을 가져옵니다.



Service

public List<ResponseScheduleDto> getAllTodoList(int limit, int offset) {
		return scheduleRepository.findAllSchedule(limit, (offset - 1) * limit);
}
  • 페이징 쿼리는 0부터 시작하기 때문에 offset = (offset - 1) * limit 를 적용해줍니다.



Repository

public List<ResponseScheduleDto> findAllSchedule(int limit, int offset) {
		String sql = "select s.schedule_id, u.user_id, s.todo_list, u.username, s.created_at, s.updated_at " +
									"from schedule s join user u on s.user_id = u.user_id order by s.updated_at desc " +
									"limit ? offset ?";
		return jdbcTemplate.query(sql, new Object[]{limit, offset}, new ScheduleRowMapper());
}
  • 조인을 적용한 단순한 페이징 쿼리입니다.
  • 수정일자 순으로 내림차순이 적용됩니다.





🚫 비밀번호가 일치하지 않는 경우

전역 예외 처리

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BadRequestException.class)
    public ResponseEntity<String> missMatch(BadRequestException e) {
				return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
    }
}
  • 컨트롤러 어드바이스를 사용하면 개별 컨트롤러뿐만 아니라 전체 애플리케이션에 적용할 수 있습니다.
  • BadRequestException 이 발생하면 HttpBody 에 에러메시지를 출력해줍니다.








📖 톺아보기

  • JDBC 를 개념만 간단하게만 알고있어서 이번 과제는 어느정도 노력이 들어간 것 같습니다.
  • JDBC Template 을 적용해봤지만 여전히 코드가 반복적이었습니다.
  • JDBC는 데이터베이스의 의존적이기때문에 유지보수 비용이 발생합니다.
  • SQL 쿼리는 문자열이기때문에 띄어쓰기 하나라도 잘못입력하면 SQL 예외가 발생합니다.
  • 컴파일 시점에 SQL 쿼리문의 오류를 잡지못합니다. (런타임 시점에서야 오류가 발생…. )
  • 해당 과제는 정적쿼리만 작성하였지만 만약 동적쿼리가 적용된다면……OTL
profile
✅ 적당한 추상화를 찾아가는 개발자입니다.

0개의 댓글