스프링(Spring)을 활용해 작고 간단한 가상의 일정 관리 앱을 개발했다.
이 프로젝트는 과제와 스프링 학습을 위해 시작했지만, 실제로 CRUD와 RESTful API를 설계하고 구현하는 과정을 통해 많은 것을 배울 수 있었다는 생각이 든다.
1.CRUD와 API 설계 명세
2.개발 과정
CRUD란 Create, Read, Update, Delete의 약자로, 데이터를 다루는 가장 기본적인 작업을 뜻한다. 스프링 기반 웹 애플리케이션에서 CRUD는 각 HTTP 메서드와 밀접한 연관이 있다.
기능 설명 HTTP Method Endpoint Create 새로운 일정을 추가 POST /schedulesRead 일정 조회 GET /schedules또는/schedules/{id}Update 기존 일정 수정 PUT 또는 PATCH /schedules/{id}Delete 일정 삭제 DELETE /schedules/{id}
✨Create(생성)
- 새로운 데이터를 추가하는 작업.
- 보통 HTTP POST 요청으로 처리.
- 예시
- Endpoint :
POST /schedules- Request Body
{ "title": "놀기", "description": "공부 안하고 엉덩이 벅벅 긁기", "userName": "semin" "password": "tpals1234" }
- Response : HTTP Status
201 Created- Body
{ "id": 1, "title": "놀기", "description": "공부 안하고 엉덩이 벅벅 긁기", "userName": "semin" "createdAt": "2024-12-06T11:23:58" "updatedAt": "2024-12-06T11:23:58" }
📖 Read(조회)
- 데이터를 읽거나 검색하는 작업.
- HTTP GET 요청으로 처리.
- 모든 데이터 조회 : 전체 일정 리스트를 가져오는 작업.
- 단일 데이터 조회 : 특정 ID의 데이터를 가져오는 작업.
- 예시 :
- Endpoint :
POST /schedules- Response : HTTP Status
200 OK- Body
{ "id": 1, "title": "놀기", "description": "공부 안하고 엉덩이 벅벅 긁기", "userName": "semin" "createdAt": "2024-12-06T11:23:58" "updatedAt": "2024-12-06T11:23:58" }
✏️ Update(수정)
- 기존 데이터를 수정하는 작업.
- HTTP PUT 또는 PATCH 요청으로 처리.
- PUT: 리소스를 전체적으로 대체.
- PATCH: 리소스의 일부를 수정.
- 예시:
- Endpoint :
PUT /schedules/{id}- Request Body :
{ "title": "놀기", "description": "공부 안하고 엉덩이 벅벅 긁기", }
- Response : HTTP Status
204 No Content- Body
{ "id": 1, "title": "회의 변경", "description": "주간 회의 내용 변경", "date": "2024-12-09" }
🗑️ Delete(삭제)
- 데이터를 삭제하는 작업.
- HTTP DELETE 요청으로 처리.
- 예시 :
- Endpoint :
DELETE /schedules/{id}- Request Body :
{ "password": "tpals1234", }
- Response:
204 No Content
일정 관리 앱은 간단하지만 명확한 요구사항이 있기에 그에 따르는 것이 중요했다.
주요 요구사항은 다음과 같다.
- 사용자가 일정을 추가할 수 있어야 한다.
- 이 때 사용자로부터 받아야 할 정보는 이름, 비밀번호, 제목, 내용이다.
- 사용자는 전체 일정을 확인하거나 특정 일정을 선택해 세부 정보를 볼 수 있어야 한다.
- ID를 통한 조회, 날짜 혹은 사용자명에 의한 조회를 할 수 있어야 한다.
- 이미 저장된 일정은 수정이 가능해야 하며, 이 때 사용자에게 비밀번호를 요구한다.
- 필요 없는 일정은 삭제할 수 있어야 하며, 마찬가지로 사용자에게 비밀번호를 요구한다.
1. 계층형 아키텍처
프로젝트를
Controller,Service,Repository로 나누고 각각의 책임을 명확히 하기로 했다.
Controller: HTTP 요청과 응답 처리.Service: 비즈니스 로직.Repository: 데이터베이스 접근.이후 DTO를 추가로 설계해 클라이언트와 데이터베이스간의 데이터를 분리하기로 했다.
2. 엔티티 설계
우선 JDBC를 사용해 테이터베이스 테이블과 매칭될 예정인
Schedule엔티티를 설계했다.@Getter @Setter public class Schedule { private Long id; private String title; private String description; private String userName; private String password; private LocalDateTime postedTime; private LocalDateTime updatedTime; }3 . DTO설계
일단 기본적으로 클라이언트의 요청과 응답을 처리하기 위해
SchedulerRequest와ScheduleResponseDTO를 설계했다.
ScheduleRequest: 클라이언트가 전달하는 요청 데이터.ScheduleResponse: 클라이언트에게 반환되는 데이터.@Getter public class ScheduleRequest { private String title; private String description; private String userName; private String password; } @Getter public class ScheduleResponse { private Long id; private String title; private String description; private String userName; private LocalDateTime createdAt; private LocalDateTime updatedAt; }여기에 추가적으로 삭제와 수정에 필요한 데이터만을 요청하는 DTO를 추가로 생성했다.
@Getter public class ScheduleDeleteRequest { private String password; } @Getter public class ScheduleUpdateRequest { private String title; private String description; private String password; }4. 각 기능 구현
4-1 일정 생성(Create)
Controller에서 클라이언트의 요청(JSON 형식)을 받아Service계층으로 전달.Service에서 DTO를 엔티티로 변환한 뒤Repository를 통해 데이터베이스에 저장.- 저장된 데이터를 다시 DTO로 변환해 클라이언트에게 반환.
@Service public class ScheduleServiceImpl implements ScheduleService { private final ScheduleRepository scheduleRepository; public ScheduleServiceImpl(ScheduleRepository scheduleRepository) { this.scheduleRepository = scheduleRepository; } @Override public void createSchedule(ScheduleRequest request) { Schedule schedule = new Schedule(); schedule.setTitle(request.getTitle()); schedule.setDescription(request.getDescription()); schedule.setUserName(request.getUserName()); schedule.setPassword(request.getPassword()); schedule.setPostedTime(LocalDateTime.now()); schedule.setUpdatedTime(LocalDateTime.now()); scheduleRepository.save(schedule); } }4-2. 일정 조회(Read)
- 전체 조회와 단일 조회를 구분.
- 전체 조회는 데이터베이스에서 모든 데이터를 가져와 리스트로 반환.
- 단일 조회는 ID를 기반으로 특정 데이터를 조회해 반환.
- 조건 조회는 이름과 날짜를 기반으로 조회한 모든 데이터를 리스트로 반환
@Override public List<ScheduleResponse> getAllSchedules() { return scheduleRepository.findAll().stream() .map(this::getScheduleResponse).collect(Collectors.toList()); } @Override public ScheduleResponse getScheduleById(Long id) { Schedule schedule = scheduleRepository.findById(id); return getScheduleResponse(schedule); } private ScheduleResponse getScheduleResponse(Schedule schedule) { ScheduleResponse scheduleResponse = new ScheduleResponse(); scheduleResponse.setId(schedule.getId()); scheduleResponse.setTitle(schedule.getTitle()); scheduleResponse.setDescription(schedule.getDescription()); scheduleResponse.setUserName(schedule.getUserName()); scheduleResponse.setCreatedAt(schedule.getPostedTime()); scheduleResponse.setUpdatedAt(schedule.getUpdatedTime()); return scheduleResponse; } @Override public List<ScheduleResponse> getScheduleByNameAndDate(Optional<String> name, Optional<LocalDate> date) { return scheduleRepository.findByNameAndDate(name,date).stream() .map(this::getScheduleResponse).collect(Collectors.toList()); }4-3 일정 수정(Update)
- 요청 데이터와 ID를 받아 해당 ID의 데이터를 수정.
- ID와 데이터(비밀번호)를 조건으로 조회함으로써 따로 비밀번호를 검증하는 로직을 넣지 않음.
- 데이터가 존재하지 않을 경우 예외를 던져 오류를 명확히 처리.
@Override public void updateSchedule(Long id, ScheduleUpdateRequest request) { Optional<Schedule> scheduleOp = scheduleRepository.findByIdAndPassword(id, request.getPassword()); Schedule schedule = scheduleOp.orElseThrow(() -> new IllegalArgumentException("can't find schedule or wrong password.")); schedule.setTitle(request.getTitle()); schedule.setDescription(request.getDescription()); schedule.setUpdatedTime(LocalDateTime.now()); scheduleRepository.update(schedule); }4-4 일정 삭제(Delete)
- 요청 데이터와 ID를 받아 해당 ID의 데이터를 삭제.
- 수정할 때의 경우와 같이 ID와 데이터를 통해 조회하고, 해당 데이터가 존재하지 않을 경우 예외를 던져 오류를 명확히 처리한다.
@Override public void deleteSchedule(Long id,ScheduleDeleteRequest request) { Optional<Schedule> scheduleOp = scheduleRepository.findByIdAndPassword(id, request.getPassword()); Schedule schedule = scheduleOp.orElseThrow(() -> new IllegalArgumentException("can't find schedule or wrong password.")); scheduleRepository.deleteById(schedule.getId()); }