< 개발 환경>
Language : Java
Version : JDK 17
Build systeem : Gradle - Groovy
Spring Boot version : 3.3.2
IDE : IntelliJ
DBMS : MySQL
프로젝트 목표
- 구현하고자 하는 서비스의 전체적인 흐름을 파악하고 필요한 기능을 설계해보자.
- API 명세서, ERD, SQL을 작성해 보자
- Spring Boot를 기반으로 CRUD(Create, Read, Update, Delete) 기능이 포함된 REST API를 만들어 보자.
- 3 Layer Architecture에 따라 각 Layer의 목적에 맞게 프로젝트를 개발해 보자.
- JDBC를 사용하며 기본적인 SQL 쿼리 작성과 데이터 관리 연습을 하며 개발해 보자
요구사항 & 프로젝트 설계 & API 명세서
[Spring] 나만의 일정 관리 앱 서버 만들기 (프로젝트 설계 & API 명세서)
[Spring] 나만의 일정 관리 앱 서버 만들기 (프로젝트 설계 & API 명세서)
요구사항1단계기능: 일정 작성조건할일, 담당자명, 비밀번호, 작성/수정일을 저장할 수 있습니다.기간 정보는 날짜와 시간을 모두 포함한 형태 입니다.각 일정의 고유 식별자(ID)를 자동으로
susuhancodingworld.tistory.com
전체 코드
GitHub - kang-sumin/Schedule-Management-Project: 일정 관리 앱 서버 만들기
일정 관리 앱 서버 만들기. Contribute to kang-sumin/Schedule-Management-Project development by creating an account on GitHub.
github.com
패키지 구조
`3 Layer Architecture`의 구조에 따라서 `Controller`, `Service`, `Repository`를 Layer의 목적에 따라 개발할 수 있도록 패키지를 나누어 보았습니다.
DTO(Data Transfer Object) 클래스들을 통해 데이터를 전송 및 이동을 위한 객체를 생성하여 계층 간의 데이터 처리를 객체로 처리할 수 있도록 하였습니다.
Controller
package com.sparta.schedule.controller;
import com.sparta.schedule.dto.ScheduleRequestDto;
import com.sparta.schedule.dto.ScheduleResponseDto;
import com.sparta.schedule.service.ScheduleService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api")
public class ScheduleController {
private final JdbcTemplate jdbcTemplate;
public ScheduleController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 일정 생성
@PostMapping("/schedules")
public ScheduleResponseDto createSchedule(@RequestBody ScheduleRequestDto requestDto) {
// 객체간 이동 위해 ScheduleService 객체 생성
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.createSchedule(requestDto);
}
// 일정 조회 (단건)
@GetMapping("/schedules/{id}")
public List<ScheduleResponseDto> getSchedule(@PathVariable Long id) {
// 객체간 이동 위해 ScheduleService 객체 생성
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.getSchedule(id);
}
// 일정 조회 (다건)
@GetMapping("/schedules")
public List<ScheduleResponseDto> getSchedules(@RequestParam(required = false) String updateDate, @RequestParam(required = false) String charge){
//객체간 이동 위한 ScheduleService 객체 생성
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.getSchedules(updateDate,charge);
}
// 일정 수정
@PutMapping("/schedules")
public Long updateSchedule(@RequestParam Long id,@RequestBody ScheduleRequestDto scheduleRequestDto){
//객체간 이동 위한 ScheduleService 객체 생성
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.updateSchedule(id, scheduleRequestDto);
}
// 일정 삭제
@DeleteMapping("/schedules")
public Long deleteSchedule(@RequestParam Long id, @RequestBody ScheduleRequestDto scheduleRequestDto){
//객체간 이동 위한 ScheduleService 객체 생성
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.deleteSchedule(id, scheduleRequestDto);
}
}
통일된 API를 `@RequestMapping`으로 묶어주고 각각의 역할에 맞는 API로 정리하여 구현해 주었습니다.
DTO
package com.sparta.schedule.dto;
import lombok.Getter;
@Getter
public class ScheduleRequestDto {
private String todo;
private String charge;
private String password;
}
package com.sparta.schedule.dto;
import com.sparta.schedule.entity.Schedule;
import lombok.Getter;
import java.time.LocalDateTime;
@Getter
public class ScheduleResponseDto {
private Long id;
private String todo;
private String charge;
private String createDate;
private String updateDate;
public ScheduleResponseDto(Schedule schedule) {
this.id = schedule.getId();
this.todo = schedule.getTodo();
this.charge = schedule.getCharge();
this.createDate = schedule.getCreateDate();
this.updateDate = schedule.getUpdateDate();
}
public ScheduleResponseDto(Long id, String todo, String charge, String createDate, String updateDate) {
this.id = id;
this.todo = todo;
this.charge = charge;
this.createDate = createDate;
this.updateDate = updateDate;
}
}
Entity
package com.sparta.schedule.entity;
import com.sparta.schedule.dto.ScheduleRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
public class Schedule {
private Long id;
private String todo;
private String charge;
private String password;
private String createDate;
private String updateDate;
public Schedule(ScheduleRequestDto requestDto) {
this.todo = requestDto.getTodo();
this.charge = requestDto.getCharge();
this.password = requestDto.getPassword();
}
}
Repository
package com.sparta.schedule.repository;
import com.sparta.schedule.dto.ScheduleRequestDto;
import com.sparta.schedule.dto.ScheduleResponseDto;
import com.sparta.schedule.entity.Schedule;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
public class ScheduleRepository {
private final JdbcTemplate jdbcTemplate;
public ScheduleRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
//DB 저장
public Schedule save(Schedule schedule) {
//DB 저장
KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체
String sql = "INSERT INTO schedule (todo, charge, password, createDate, updateDate) VALUES (?, ?, ?, ?, ?)";
jdbcTemplate.update(con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, schedule.getTodo());
preparedStatement.setString(2, schedule.getCharge());
preparedStatement.setString(3, schedule.getPassword());
preparedStatement.setString(4, schedule.getCreateDate());
preparedStatement.setString(5, schedule.getUpdateDate());
return preparedStatement;
},
keyHolder);
// DB Insert 후 받아온 기본키 확인
Long id = keyHolder.getKey().longValue();
schedule.setId(id);
return schedule;
}
// id를 통한 단건 조회
public List<ScheduleResponseDto> findId(Long id) {
// DB 에서 ID로 찾아내는 쿼리
String sql = "SELECT * FROM schedule WHERE id = ?";
return jdbcTemplate.query(sql, new RowMapper<ScheduleResponseDto>() {
@Override
public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String todo = rs.getString("todo");
String charge = rs.getString("charge");
String createDate = rs.getString("createDate");
String updateDate = rs.getString("updateDate");
return new ScheduleResponseDto(id, todo, charge, createDate, updateDate);
}
}, id);
}
// 수정일과 담당자를 통한 다건 조회
// 수정일을 기준으로 내림차순 정렬
public List<ScheduleResponseDto> findAll(String updateDate, String charge) {
String sql = "";
if (updateDate != null && charge != null) {
sql = "SELECT * FROM schedule WHERE DATE_FORMAT(updateDate, '%Y-%m-%d') = ? AND charge = ? ORDER BY updateDate DESC";
return jdbcTemplate.query(sql, new RowMapper<ScheduleResponseDto>() {
@Override
public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String todo = rs.getString("todo");
String charge = rs.getString("charge");
String createDate = rs.getString("createDate");
String updateDate = rs.getString("updateDate");
return new ScheduleResponseDto(id, todo, charge, createDate, updateDate);
}
}, updateDate, charge);
} else if (updateDate == null && charge != null) {
sql = "SELECT * FROM schedule WHERE charge = ? ORDER BY updateDate DESC";
return jdbcTemplate.query(sql, new RowMapper<ScheduleResponseDto>() {
@Override
public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String todo = rs.getString("todo");
String charge = rs.getString("charge");
String createDate = rs.getString("createDate");
String updateDate = rs.getString("updateDate");
return new ScheduleResponseDto(id, todo, charge, createDate, updateDate);
}
}, charge);
} else if (updateDate != null && charge == null) {
sql = "SELECT * FROM schedule WHERE DATE_FORMAT(updateDate, '%Y-%m-%d') = ? ORDER BY updateDate DESC";
return jdbcTemplate.query(sql, new RowMapper<ScheduleResponseDto>() {
@Override
public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String todo = rs.getString("todo");
String charge = rs.getString("charge");
String createDate = rs.getString("createDate");
String updateDate = rs.getString("updateDate");
return new ScheduleResponseDto(id, todo, charge, createDate, updateDate);
}
}, updateDate);
} else {
sql = "SELECT * FROM schedule ORDER BY updateDate DESC";
return jdbcTemplate.query(sql, new RowMapper<ScheduleResponseDto>() {
@Override
public ScheduleResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String todo = rs.getString("todo");
String charge = rs.getString("charge");
String createDate = rs.getString("createDate");
String updateDate = rs.getString("updateDate");
return new ScheduleResponseDto(id, todo, charge, createDate, updateDate);
}
});
}
}
public void update(Long id, ScheduleRequestDto scheduleRequestDto) {
String sql = "UPDATE schedule SET todo = ?, charge = ?, updateDate = ? WHERE id = ?";
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String nowstring = format.format(new Date());
jdbcTemplate.update(sql, scheduleRequestDto.getTodo(), scheduleRequestDto.getCharge(), nowstring, id);
}
// 일정 삭제
public void delete(Long id) {
String sql = "DELETE FROM schedule WHERE id = ?";
jdbcTemplate.update(sql,id);
}
public Schedule findById(Long id, String password) {
String sql = "SELECT * FROM schedule WHERE id = ?";
return jdbcTemplate.query(sql, resultSet -> {
if (resultSet.next()) {
Schedule schedule = new Schedule();
if(password.equals(resultSet.getString("password"))){
schedule.setTodo(resultSet.getString("todo"));
schedule.setCharge(resultSet.getString("charge"));
schedule.setCreateDate(resultSet.getString("createDate"));
schedule.setUpdateDate(resultSet.getString("updateDate"));
}else{
throw new IllegalArgumentException("비밀번호가 일치하기 않습니다.");
}
return schedule;
} else {
return null;
}
}, id);
}
}
Service
package com.sparta.schedule.service;
import com.sparta.schedule.dto.ScheduleRequestDto;
import com.sparta.schedule.dto.ScheduleResponseDto;
import com.sparta.schedule.entity.Schedule;
import com.sparta.schedule.repository.ScheduleRepository;
import org.springframework.jdbc.core.JdbcTemplate;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
public class ScheduleService {
private final JdbcTemplate jdbcTemplate;
public ScheduleService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public ScheduleResponseDto createSchedule(ScheduleRequestDto requestDto) {
// RequestDto -> Entity
Schedule schedule = new Schedule(requestDto);
//지금 현재 시간 데이터 저장
//LocalDateTime now = LocalDateTime.now();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String nowstring = format.format(new Date());
schedule.setCreateDate(nowstring);
schedule.setUpdateDate(nowstring);
// DB 저장
ScheduleRepository scheduleRepository = new ScheduleRepository(jdbcTemplate);
Schedule saveSchedule = scheduleRepository.save(schedule);
//Entity -> ResponseDto
ScheduleResponseDto scheduleResponseDto = new ScheduleResponseDto(saveSchedule);
return scheduleResponseDto;
}
// id를 통한 단건조회
public List<ScheduleResponseDto> getSchedule(Long id) {
// DB 조회
ScheduleRepository scheduleRepository = new ScheduleRepository(jdbcTemplate);
return scheduleRepository.findId(id);
}
// 수정일과 담당자를 통한 다건조회
public List<ScheduleResponseDto> getSchedules(String updateDate, String charge) {
// DB 조회
ScheduleRepository scheduleRepository = new ScheduleRepository(jdbcTemplate);
return scheduleRepository.findAll(updateDate, charge);
}
// 일정 수정
public Long updateSchedule(Long id,ScheduleRequestDto scheduleRequestDto) {
// DB 수정
ScheduleRepository scheduleRepository = new ScheduleRepository(jdbcTemplate);
String password = scheduleRequestDto.getPassword();
Schedule schedule = scheduleRepository.findById(id, password);
if(schedule != null){
scheduleRepository.update(id, scheduleRequestDto);
return id;
}else{
throw new IllegalArgumentException("선택하신 일정은 존재하지 않습니다.");
}
}
public Long deleteSchedule(Long id, ScheduleRequestDto scheduleRequestDto) {
// DB 삭제
ScheduleRepository scheduleRepository = new ScheduleRepository(jdbcTemplate);
String password = scheduleRequestDto.getPassword();
Schedule schedule = scheduleRepository.findById(id, password);
if(schedule != null){
scheduleRepository.delete(id);
return id;
}else{
throw new IllegalArgumentException("선택하신 일정은 존재하지 않습니다.");
}
}
}
코드 회고
코드의 전체 구조화를 세분화하는 작업에서 어떻게 코드를 구현해야 할지 막막하였지만, 학습하고 자료를 찾으면 필수 요구사항들을 구현해 낼 수 있었습니다.
하지만 클래스 내에서 CRUD 기능별로 메서드들을 세분화하면서 중복되는 기능이 많아 이러한 부분을 메서드화 하여 중복적인 코드를 최소화할 수 있도록 리팩토링 해야겠다고 생각했습니다.
method 주석을 좀 더 스프링 개발을 협업으로 진행했을 때 이해하기 편하게 API request 방식과 return 타입을 지정해 주며 가독성을 높이는 주석을 추가해 주어야겠다고 생각했습니다.
코드를 다시 리팩토링 하면서 사용되지 않는 변수 또는 메서드와 클래스 객체등을 점검하고 논리적으로 코드에 이상이 없는지 확인해 봐야겠다고 생각했습니다.
DB에 작성일과 수정일을 저장할 때 `SimpleDateFormat`으로 시간을 저장하였는데 이 부분은 java 8 버전 이후 사용되는 `java.time.LocalDateTime`으로 리팩토링 하여 업그레이드된 버전에 맞도록 수정해야겠습니다.
Rest API에 대한 학습을 더 진행하여 프로젝트를 진행하면서 미숙했던 API 설계를 보완하여 리팩토링 해보고 싶습니다.