CH.3 일정 관리 앱 과제

정예진·2026년 4월 13일

Spring

목록 보기
6/20

2026.04.13

과제를 진행한 순서대로 정리하는 글~
이번 과제도 역시나 필수 기능과 도전 기능으로 나눠져있음...!

필수 기능

프로젝트 및 구조 생성

우선 3 Layer Architecture를 적용해서

패키지를 만들어서 이 과제의 틀을 먼저 잡아주고 시작했음.

Entity 생성

그리고는 바로 Schedule Entity부터 만들었음.

기본적으로 필요한 속성들을 넣고 생성자까지 만들어줌.

골뱅이들을 간단하게 보면,
@Getter : 각 필드의 getter 메서드를 자동으로 만들어줌.
@Entity : 이 클래스가 JPA가 관리하는 엔티티라는 뜻. (이 클래스가 DB 테이블과 연결됨.)
@Table(name = "schedules") : 이 엔티티가 연결될 테이블 이름을 지정해줌.
@NoArgsConstructor(access = AccessLevel.PROTECTED) : 기본 생성자를 자동으로 만들어줌. 근데 접근 제한을 Protected로 둔 것. 이걸 쓰는 이유는 JPA는 기본 생성자가 필요하기 때문임. 그래서 만들어주긴 해야하는데, 아무데서나 막 new Schedule() 하게 두진 않으려고 protected로 둔 것.
@Id : 이 필드가 기본키(PK) 라는 뜻.
@GeneratedValue(strategy = GenerationType.IDENTITY) : 기본키 값을 DB가 자동으로 생성하게 해주는 설정. 즉 직접 id를 넣지 않아도 DBㅏ 1,2,3... 식으로 넣어줌.
@Column : 이 테이블이 어떤 정보를 저장할 건지 정해놓은 항목이라고 생각하면 됨. 괄호 안에 null 허용 여부나 길이 제한 등 이런걸 정할 때 사용함.

그 다음은 일정 생성일자와 수정일자를 JPA Auditing을 사용해서 나타내야하기 때문에

BaseEntity를 만들어서 Scheudle 엔티티, 그리고 이후 도전과제에서 나올 Comment 엔티티에서 extends를 해줌. (이게 상속이였나?)

여기 있는 골뱅이들도 보면,
@MappedSuperclass : 이 클래스의 필드들을 상속받는 엔티티가 같이 물려받게 해주는 설정.
@EntityListeners(AuditingEntityListener.class) : 엔티티의 생성/수정 시점을 감지해서 Auditing 기능이 동작하게 해주는 설정.
public abstract class BaseEntity : 처음엔 abstract가 뭐지? 했는데 찾아보니 이 클래스는 직접 객체를 만들려고 쓰는 클래스가 아니라, 상속용 클래스라는 뜻이였음!!
@CreatedDate : 엔티티가 처음 저장될 때 생성 시간을 자동으로 넣어줌.
@LastModifiedDate : 엔티티가 수정될 때 마지막 수정 시간을 자동으로 넣어줌.
그리고 @Column 에 들어가 있는 건 이 컬럼은 한 번 저장된 뒤 수정되지 않게 한다라는 뜻!

Repository 생성

이후로는 3 Layer Architecture 형식에 맞게 순서대로 만들어줌.
제일 먼저 만들기 쉬운게 Repository 라서 먼저 만들어줌.

이게 끝임...ㅎ
얘는 extends로 JpaRepository를 꼮꼭ㄱ꼮 해줘야함. <> 안에는 해당 id를 갖고 있는 엔티티를 넣어주고 id의 자료형을 순서대로 넣어줌.

Service 생성

Service가 속성으로 가지는게 Repository 이기 때문에 넣어줌.

@Service : 이 클래스가 서비스 계층의 클래스라고 표시해주는 것.
@RequiredArgsConstructor : final이 붙은 필드들을 가지고 생성자를 자동으로 만들어주는 어노테이션.

Controller 생성

다음은 Controller를 만들어서 서비스를 속성으로 넣어줌.

@RestController : 이 클래스가 HTTP 요청을 받고 응답을 반환하는 컨트롤러 라는 뜻. 얘가 클라이언트가 보낸 요청을 받고 서비스를 호출해서 결과를 돌려줌. JSON 형태로 바로 반환해줌!!

여기까지 만들었으면 이제 CRUD를 하나씩 구현해볼 거임.

POST

우선, POST 먼저 만듦. 다른 사람들은 어디서부터 만드는진 모르겠지만 난 요청이 들어오는 흐름 순서대로 만듦. 그래서 Controller에서 시작할거임~!

생성이기 때문에 @PostMapping("/schedules")을 넣고 만들어줌. POST 요청이 /schedules로 들어 왔을 때 이 메서드를 실행 하는것임.
코드도 하나씩 뜯어보면 그렇게 어렵진 않음!

public ResponseEntity<ScheduleCreateResponseDto> scheduleCreate(...)

이 매서드가 최종적으로 응답을 돌려주는 메서드라는 뜻임.

  • ScheduleCreateResponseDto : 응답으로 보낼 데이터 형태.
  • ResponseEntity : 응답 데이터 + 상태코드까지 같이 보내기 위한 객체.
@RequestBody ScheduleCreateRequestDto requestDto

클라이언트가 보낸 JSON 요청값을 requestDto에 담아서 받는것임.
즉, 사용자가 보낸

  • title
  • content
  • authorName
  • password

이런 값들이 여기로 들어오게 되는 것!

ScheduleCreateResponseDto result = scheduleService.save(requestDto);

컨트롤러가 직접 저장하지 않고, 서비스한테 넘겨서 "이 요청값으로 일정 저장해줘~" 하고 맡기는거임.

  • requestDto를 서비스로 넘김.
  • 서비스가 저장 처리함.
  • 저장 결과를 result로 받음.

자세한 내용물은 컨트롤러가 모르게 움직일려고 하는 것임.

return ResponseEntity.status(HttpStatus.CREATED).body(result);

클라이언트에게 최종 응답을 보내는 부분으로

  • 상태코드 : 201 Created
  • 응답 바디 : result

즉, "일정이 정상적으로 생성되었다~ 그리고 생성된 일정 정보는 이거예요~" 하고 돌려주는 거임.
컨트롤러에 있는 애들은 다 이 형태로 만들어져있음. 세부적인 로직은 서비스에서 하고 있기 때문에 컨트롤러는 비교적 간단한 형태만 띄고 있는 듯?
이렇게 만들고 나면 사실 있지도 않은 것들을 마구 만들어대서 빨간줄이 여러게 생김...ㅎ 그럼 대충 형태만 잡아놓고 빨간줄을 해결하러 갔다옴.
다음으로 만드는게 DTO 어려운 친구가 아니라서 금방 만들고 서비스 친구에게 달려갈 거임.

POST는 일단 생성을 하는 거기 때문에 어떤 식으로 만들건지에 대한 요청이 있어야겠지? 그럼 먼저 만들건 ScheduleCreateRequestDto임.

일정을 만들기 위해서 필요한 것들을 넣어줌. 일정 제목, 일정 내용, 작성자명, 해당 일정에 대한 비밀번호 4가지만 받음!

그리고 ScheduleCreateResponseDto를 만듦. 받은 요청들로 어떤걸 보여줄건지를 넣으면 됨.

이렇게 만들고 나면 서비스로 가서 저장을 할 로직 save()를 만들면 됨.

서비스에서 나오는 어노테이션은 @Transactional 인데 이 메서드 안에서 일어나는 작업을 하나의 트랜잭션으로 묶어준다는 뜻!

이 로직의 흐름은 요청 DTO에서 값을 꺼내서, 그 값으로 Schedule 객체를 생성하고, scheduleRepository.save()로 DB에 저장 후 저장된 결과를 응답 DTO로 만들어서 반환을 하는 것임!

Schedule schedule = new Schedule(
    requestDto.getTitle(),
    requestDto.getContent(),
    requestDto.getAuthorName(),
    requestDto.getPassword()
);

여기서 요청값으로 엔티티를 만듦. 사용자가 보낸 데이터를 Schedule 객체로 바꾸는 단계.

Schedule savedSchedule = scheduleRepository.save(schedule);

DB에 저장함. 이 객체를 실제 DB에 저장하는 단계로, 저장되면 id, createdAt, updatedAt 같은 값도 포함된 상태로 돌아옴.

return new ScheduleCreateResponseDto(
    savedSchedule.getId(),
    savedSchedule.getTitle(),
    savedSchedule.getContent(),
    savedSchedule.getAuthorName(),
    savedSchedule.getCreatedAt(),
    savedSchedule.getUpdatedAt()
);

응답 DTO로 바꿈. 저장된 결과를 그대로 반환하지 않고, 클라이언트에게 보여줄 값만 담아서 응답 DTO로 만드는 것임.
이렇게 하면 생성하는 기능을 구현한 것! 다른 기능들도 생긴 구조는 다 비슷함.

GET

다음은 생성한걸 조회할 수 있도록 GET을 만들어 볼거임.

이건 전체 일정을 조회하는 기능인데, DTO는 단건 조회랑 같이 씀. 나중에 댓글 기능 넣으면서 분리 시키긴 할거지만,,,

생성이랑 다르게 보이는 부분이

List<ScheduleGetResponseDto>

여기임. 왜냐? 단건조회들을 리스트로 만들어서 보여주면 전체 조회이기 때문 ㅋ. 나중에 분리시키는데 머리 복잡해서 후회하긴 했음...ㅋㅋㅋ

@RequestParam(required = false) String authorName

URL 뒤에 붙는 쿼리 파라미터 값을 받는건데 예시로 보면

/schedules?authorName=예진

대충 이런식임.
그리고 괄호에 들어있는 required = false 이거는 이 값이 있어도 되고 없어도 된다는 뜻!

scheduleService.getAll(authorName)

컨트롤러가 받은 authorName 값을 서비스한테 넘겨줘서 전체 조회 또는 조건 조회를 처리하게 할려고 넣었음.

DTO는 이거 하나로 지금은 일단 퉁 침.
조회에서는 따로 받을 데이터가 없기 때문에 응답만 있으면 됌.

서비스 로직은 DB에서 전체 일정을 가져오고, authorName 조건이 있으면 그 작성자만 걸러내고, 수정일 기준으로 내림차순 정렬을 해서 응답 DTO로 변환해서 반환하는 것임.

if (authorName == null || authorName.isBlank()) {
    filteredScheduleList.addAll(scheduleList);
} else {
    for (Schedule schedule : scheduleList) {
        if (schedule.getAuthorName().equals(authorName)) {
            filteredScheduleList.add(schedule);
        }
    }
}

작성자면 조건으로 필터링을 하는 건데, authorName이 없으면 전체 일정 조회를 하고, authorName이 있으면 같은 작성자명 일정만 조회되도록 한것임.
이 부분이 전체 조회 / 조건 조회를 하나의 API로 처리한 것.

filteredScheduleList.sort((Schedule s1, Schedule s2) -> s2.getUpdatedAt().compareTo(s1.getUpdatedAt()));

이건 수정일을 기준으로 내림차순 정렬을 해준거고

for (Schedule schedule : filteredScheduleList) {
    ScheduleGetResponseDto dto = new ScheduleGetResponseDto(
            schedule.getId(),
            schedule.getTitle(),
            schedule.getContent(),
            schedule.getAuthorName(),
            schedule.getCreatedAt(),
            schedule.getUpdatedAt()
    );
    dtos.add(dto);
}

마지막으로 DTO로 변환해서 엔티티를 그대로 반환하지 않고, 응답에 필요한 값만 담아서 보내는 것!

단건 조회도 똑같음.

컨트롤러 먼저 만들어주고 DTO는 전체 조회에서 썻던 거랑 같은걸 사용하기 때문에 바로 서비스를 만들어줌.

선택한 일정이 있는지 확인 하고 없으면 예외처리까지 해줌. 그리고는 앞에 봣던 애들이랑 흐름은 같음.

PUT

@PathVariable 은 URL 경로에 있는 scheduleId 값을 받아오는 것임.

@RequestBody ScheduleUpdateRequestDto requestDto

클라이언트가 보낸 수정 요청 데이터를 DTO로 받는 것.
즉, 수정할 제목, 수정할 작성자명, 수정하기 위해 필요한 비밀번호(생성할 때 입력한거)

scheduleService.update(scheduleId, requestDto)

컨트롤러가 직접 수정하기 않고, 서비스한테 "이 일정 ID와 요청값으로 수정해줘~"하고 맡김.
즉, scheduleId로 수정할 대상을 찾고, requestDto로 수정할 값을 전달함.

요청 DTO는 항상 간단함.ㅎ

그리고 응답으로 줄 DTO도 만들어주고,

수정을 해야하기 때문에 setter도 만들어줘야함...!

엔티티로 와서 수정이 가능한 항목만 setter로 만들어 주고

서비스 로직도 만들어줌.
scheduleId로 수정할 일정을 조회하고
일정이 없으면 예외 발생, 있으면 title, authorName 수정.
수정된 결과를 응답 DTO로 반환되는 흐름임.

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

수정하고자 하는 일정을 조회하고

schedule.update(requestDto.getTitle(), requestDto.getAuthorName());

조회한 일정의 값을 실제로 바꾸는 단계.

return new ScheduleUpdateResponseDto(
        schedule.getId(),
        schedule.getTitle(),
        schedule.getAuthorName(),
        schedule.getCreatedAt(),
        schedule.getUpdatedAt()
);

그리고 수정하고 난 뒤 보여줄 응답들을 DTO에 담아서 반환.

DELETE

마지막으로 삭제기능만 만들면 필수기능은 다 구현된 것임.

삭제가 제일 쉬움.

삭제는 반환되는 데이터가 없기 때문에 <> 안에 Void를 넣어줌.

그리고 바로 서비스 로직만 만들어주면 됨.

흐름은 scheduleId로 삭제할 일정을 조회하고
일정이 없으면 예외 발생, 있으면 입력한 비밀번호가 저장된 비밀번호와 일치하는지 비교함.
그리고 일치하면 삭제.

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

삭제할 대상을 찾아본 다음에 없으면 예외를 발생 시킴.

if (requestDto.getPassword() == null || requestDto.getPassword().isBlank()) {
    throw new IllegalArgumentException("비밀번호를 입력해주세요.");
}

삭제할 대상을 찾았으면 저장된 비밀번호와 입력한 비밀번호를 비교할 건데 그 전에 비밀번호를 입력하지 않을 수도 있기 때문에 그에 맞는 예외처리도 처리해줌.

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

이제 진짜 비밀번호를 비교함. 일치 하지 않으면 또 다시 예외 발생.

scheduleRepository.delete(schedule);

비밀번호를 확인하면 삭제시킴.

근데 지금 보니까 오타가 있네...ㅋㅋㅋ

이렇게 하면 필수 기능 과제 끝!

0개의 댓글