[본캠프] 일정 관리 과제 과정

윤영범·2026년 4월 13일
post-thumbnail

과제정리

이번 과제는 Spring Boot + JPA + MySQL로 일정(Schedule) CRUD API를 만드는 것이 목표였다 먼저 과제를 시작하기전 요구사항에맞춰 ERD , API 명세서부터 작성했다

ERD

하나의 일정(Schedule)에는 여러 개의 댓글(Comment)이 달릴 수 있다
즉, 1:N 관계 (일정 1개 : 댓글 여러 개) 구조이다
댓글은 특정 일정에 종속되며, scheduleID를 통해 어떤 일정에 속하는지 식별한다

API

https://documenter.getpostman.com/view/53912059/2sBXitBn5S
1.포스트맨을 이용해 작성한 일정관리 API

2.댓글생성 API

1.일정 생성

클라이언트 요청 → Controller → Service → Repository → DB 저장 → 응답 반환

ScheduleController.java 에서 요청 받기

Controller에서는 POST /schedules 요청을 받아서 CreateScheduleRequest DTO로 요청 데이터를 전달받는다.

    @PostMapping("/schedules")
    public ResponseEntity<CreateScheduleResponse> createSchedule(@RequestBody CreateScheduleRequest request)
    {
        CreateScheduleResponse result = scheduleService.save(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(result);
    }

ScheduleService.java 에서 비즈니스 로직처리

@Transactional
public CreateScheduleResponse save(CreateScheduleRequest request) {
    Schedule schedule = new Schedule(request.getTitle(),
            request.getContent(),
            request.getName(),
            request.getPassword());
    Schedule saveSchedule = scheduleRepository.save(schedule);
    return new CreateScheduleResponse(saveSchedule.getId(),
            saveSchedule.getTitle(),
            saveSchedule.getContent(),
            saveSchedule.getName(),
            saveSchedule.getCreatedAt(),
            saveSchedule.getModifiedAt());
}

1.요청 DTO에서 제목, 내용, 작성자명, 비밀번호를 꺼낸다
2.new Schedule(...) 로 새로운 일정 엔티티를 만든다
3.scheduleRepository.save(schedule) 로 DB에 저장한다
4.저장된 엔티티 정보를 기반으로 응답 DTO를 생성해서 반환한다

3계층의 역활을 지키기위해 DTO를 사용하여 응답 엔티티를 바로 응답요청으로 보내지않음

2.일정 조회

클라이언트 요청 → Controller → Service → Repository → DB 조회 → DTO 변환 → 응답 반환

ScheduleController.java 에서 요청 처리
(1) 단건 조회 API

@GetMapping("/schedules/{scheduleId}")
public ResponseEntity<GetOneScheduleResponse> getOneSchedule(@PathVariable Long scheduleId)
{
    GetOneScheduleResponse result = scheduleService.getOne(scheduleId);
    return ResponseEntity.status(HttpStatus.OK).body(result);
}
  • @PathVariable을 사용하여 scheduleId를 URL에서 받아온다
  • Service 계층에 전달하여 해당 일정 데이터를 조회한다

ScheduleService.java 에서 비즈니스 로직 처리

@Transactional(readOnly = true)
public GetOneScheduleResponse getOne(Long scheduleId)
{
    Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow(
            () -> new IllegalStateException("없는 스케쥴 입니다")
    );

    List<Comment> comments = commentRepository.findByScheduleId(scheduleId);
    List<GetCommentResponse> dtos = new ArrayList<>();

    for(Comment comment : comments)
    {
        GetCommentResponse dto = new GetCommentResponse(
                comment.getId(),
                comment.getContent(),
                comment.getName(),
                comment.getCreatedAt(),
                comment.getModifiedAt(),
                comment.getScheduleId()
        );
        dtos.add(dto);
    }

    return new GetOneScheduleResponse(
            schedule.getId(),
            schedule.getTitle(),
            schedule.getContent(),
            schedule.getName(),
            schedule.getCreatedAt(),
            schedule.getModifiedAt(),
            dtos
    );
}

1.findById()를 통해 일정 존재 여부를 확인한다
→ 없으면 예외 발생
댓글을 별도로 조회한다

commentRepository.findByScheduleId(scheduleId);

댓글을 DTO로 변환한다
일정 정보 + 댓글 리스트를 하나의 Response DTO로 묶어서 반환한다

(2) 전체 조회 API (조건 조회 포함)
ScheduleController.java 에서 요청 처리

@GetMapping("/schedules")
public ResponseEntity<List<GetScheduleResponse>> getAllSchedule(@RequestParam(required=false) String name)
{
    List<GetScheduleResponse> result = scheduleService.getAll(name);
    return ResponseEntity.status(HttpStatus.OK).body(result);
}
  • @RequestParam(required = false)를 사용하여 작성자 이름을 선택적으로 받는다
  • name이 있으면 조건 조회, 없으면 전체 조회를 수행한다

ScheduleService.java 에서 비즈니스 로직 처리

@Transactional(readOnly = true)
public List<GetScheduleResponse> getAll(String name)
{
    List<Schedule> schedules;

    if (name == null) {
        schedules = scheduleRepository.findAllByOrderByModifiedAtDesc();
    } else {
        schedules = scheduleRepository.findByNameOrderByModifiedAtDesc(name);
    }

    List<GetScheduleResponse> dtos = new ArrayList<>();
    for(Schedule schedule : schedules)
    {
        GetScheduleResponse dto = new GetScheduleResponse(
                schedule.getId(),
                schedule.getTitle(),
                schedule.getContent(),
                schedule.getName(),
                schedule.getCreatedAt(),
                schedule.getModifiedAt()
        );
        dtos.add(dto);
    }
    return dtos;
}
  • name이 없으면 전체 조회
  • name이 있으면 작성자 기준 조회
  • 조회 결과는 수정일 기준 내림차순으로 정렬

3.일정 수정

클라이언트 요청 → Controller → Service → 일정 존재 여부 확인 → 비밀번호 검증 → 엔티티 수정 → 응답 반환

ScheduelController.java 에서 요청 처리

@PutMapping("/schedules/{scheduleId}")
public ResponseEntity<UpdateScheduleResponse> updateSchedule(
        @PathVariable Long scheduleId,
        @RequestBody UpdateScheduleRequest request
) {
    UpdateScheduleResponse result = scheduleService.update(scheduleId, request);
    return ResponseEntity.status(HttpStatus.OK).body(result);
}
  • @PutMapping("/schedules/{scheduleId}")
    → 특정 일정 수정 요청을 처리한다
  • @PathVariable Long scheduleId
    → URL에서 수정할 일정의 ID를 받는다
  • @RequestBody UpdateScheduleRequest request
    → 요청 본문에서 수정할 request dto 와 비밀번호를 받는다
    scheduleService.update(...)
    → 실제 수정 로직은 Service에 위임한다

ScheduleService.java 에서 수정 로직 처리

@Transactional
public UpdateScheduleResponse update(Long scheduleId, UpdateScheduleRequest request)
{
    Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow(
            () -> new IllegalStateException("없는 스케쥴 입니다")
    );

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

    schedule.updateSchedule(request.getTitle(), request.getName());

    return new UpdateScheduleResponse(
            schedule.getId(),
            schedule.getTitle(),
            schedule.getContent(),
            schedule.getName(),
            schedule.getModifiedAt()
    );
}

1.findById()로 수정할 일정이 존재하는지 조회한다
2.일정이 없으면 예외를 발생시킨다
3.request dto 으로 받은 비밀번호와 기존 일정의 비밀번호를 비교한다
4.비밀번호가 일치하지 않으면 수정하지 않고 예외를 발생시킨다
5.검증이 끝나면 엔티티의 수정 메서드를 호출한다
6.수정된 엔티티의 값을 바탕으로 response dto를 양식에맞춰 반환한다

4.일정 삭제

클라이언트 요청 → Controller → Service → 일정 존재 확인 → 비밀번호 검증 → 삭제 → 응답 반환

ScheduleController.java 에서 요청 처리

@DeleteMapping("/schedules/{scheduleId}")
public ResponseEntity<Void> deleteSchedule(
        @PathVariable Long scheduleId,
        @RequestBody DeleteScheduleRequest request
) {
    scheduleService.delete(scheduleId, request);
    return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
}
  • @DeleteMapping("/schedules/{scheduleId}")
    → 특정 일정 삭제 요청 처리
  • @PathVariable
    → 삭제할 일정 ID를 URL에서 받음
  • @RequestBody
    → 비밀번호를 요청 본문에서 받음

ScheduleService.java 에서 로직 처리

@Transactional
public void delete(Long scheduleId, DeleteScheduleRequest request) {

    Schedule schedule = scheduleRepository.findById(scheduleId)
            .orElseThrow(() -> new IllegalStateException("없는 스케줄 입니다"));

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

    scheduleRepository.deleteById(scheduleId);
}

1.findById()로 일정 존재 여부 확인
2.존재하지 않으면 예외 발생
3.요청 비밀번호와 저장된 비밀번호 비교
4.비밀번호가 다르면 삭제하지 않고 예외 발생
5.검증 완료 후 삭제 수행

트러블슈팅

1.Repository에서 query문을 findBy로 커스텀하는과정
-> 내림차순이 order by desc 명령어인건 알고있었는데 findAllByOrderByModifiedAtDesc,findByNameOrderByModifiedAtDesc 카멜케이스를 제대로 적용하지않아서 계속 오류나는 부분을 제대로 찾지못함
2.비밀번호 검증 오류

public void delete(Long scheduleId, DeleteScheduleRequest request) 
{ 
String password = request.getPassword(); 

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

boolean existance = scheduleRepository.existsById(scheduleId); 
if(!existance) 
{ throw new IllegalStateException("없는 유저 입니다"); }

scheduleRepository.deleteById(scheduleId); 
}

처음비밀번호 설계로직을 진행했을때,
항상 true (같음) 이라서 검증이 의미가 없었음
-> 해결방안

        Schedule schedule = scheduleRepository.findById(scheduleId)
                .orElseThrow(() -> new IllegalStateException("없는 스케줄 입니다"));

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

기존에 existance 로직을 schedule을 확인하는 로직으로 변경시켜서,
스케쥴 DB 내 저장된 비밀번호랑 검증

3.댓글 조회 방식 고민
도전기능에 일정의 대한 단건조회를 할때, 댓글목록도 함께 보여주는 조건이있었는데
이를 위해 ScheduleService 안에서 CommentRepository를 함께 주입받아 사용하는 구조를 생각하게 되었는데, 이때 CommentRepository.java 가 ScheduelService.java 안에서 다뤄지면 객체지향설계에 어긋나는 고민이있었다
해결방안
->이번 프로젝트에서는 일정 단건 조회 시 댓글을 함께 보여주는 것이 요구사항이었기 때문에,
ScheduleService에서 CommentRepository를 참조하여 조회 데이터를 조립하는 방식은
객체지향 설계에 크게 어긋나지 않는다고 판단했다

느낀점

이번 일정 관리 프로젝트를 통해 단순히 CRUD 기능을 구현하는 것을 넘어
Spring 기반 백엔드 구조와 설계에 대해 한 단계 더 깊이 이해할 수 있었다

3 Layer Architecture를 적용하면서 각 계층의 역할을 분리하는 것이 얼마나 중요한지 느끼게 되었다.
특히 Service 계층에서 비즈니스 로직을 담당하도록 구조를 나누니
코드의 흐름이 훨씬 명확해지고 유지보수하기 쉬워졌다

또한 DTO를 사용하여 Entity와 API 응답을 분리하는 과정에서
데이터를 어떻게 저장할 것인가와 어떻게 보여줄 것인가는 다르다는 점을 이해하게 되었다.
이 부분은 단순한 기능 구현보다 설계적인 관점에서 큰 도움이 되었다고 느낀다

조회 기능을 구현하면서는
연관관계를 사용하지 않고 Service에서 데이터를 조합하는 방식에 대해 고민하게 되었고
객체지향 설계에서 중요한 것은 단순한 참조 관계가 아니라
책임과 역할의 분리라는 점을 다시 한 번 느끼게 되었다.

또한 수정과 삭제 기능에서는 비밀번호 검증 로직을 추가하면서
단순 CRUD가 아닌 비즈니스 규칙을 적용하는 과정을 경험할 수 있었다
특히 잘못된 검증 로직으로 인해 오류가 발생했던 경험을 통해
조건문 하나라도 정확하게 이해하고 작성하는 것이 중요하다는 것을 체감했다

JPA를 사용하면서는 메서드 네이밍 규칙(CamelCase)과
findBy, countBy, existsBy와 같은 방식이 내부적으로 어떻게 동작하는지 이해하게 되었고
@Transactional 개념도 함께 익힐 수 있었다

전체적으로 이번 프로젝트는

API 설계 방식
계층 분리 구조
DTO 활용
비즈니스 로직 처리
예외 및 상태 코드 설계

를 경험해볼 수 있었던 의미 있는 과정이었다.

0개의 댓글