[WIL] Spring - 일정 관리 앱 만들기

2026. 2. 2. 14:44·IL/WIL

🚩 설정

spring.application.name=spring-schedule

spring.datasource.url=jdbc:mysql://localhost:3306/schedule
spring.datasource.username=root
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

일단 properties에 추가
+ 이거 yml로 바꿈

spring:
  application:
    name: spring-schedule

  datasource:
    url: jdbc:mysql://localhost:3306/schedule
    username: root
    password: 19961222
    driver-class-name: com.mysql.cj.jdbc.Driver

  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQLDialect
        format_sql: true
    show-sql: true
create database schedule;

mysql 만들고 추가

package com.springschedule.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {

}
  • `@Configuration`: 이 클래스는 설정 모음집이다
  • `@EnableJpaAuditing`: JPA Auditing 기능 켜라

 

package com.springschedule.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime modifiedAt;
}

BaseEntity를 만들어 준다. 작성일이랑 수정일을 찍어 줘야 되기 때문에. 일정이든 댓글이든 언제 만들어졌고, 언제 수정됐는지가 필요하기 때문에 JPA에서 제공해 주는 것이다.

 


🚩API 명세 및 ERD 작성

ERD를 작성해 봤다. 저기에 왜 빨간색 글씨로 바뀌는지 모르겠다 ㅡㅡ

https://www.notion.so/API-2fead743b3ce806da919ff9358f2d788?source=copy_link

 

일정 만들기 API 기본 명세서 | Notion

Hosted by Notion Sites — The easiest way to get a website up and running.

brief-pint-841.notion.site

 

노션 템플릿을 사용해서 대충 틀을 잡고

깃허브 README에 더 작성 완료. https://github.com/Intheji/spring-schedule


🚩 일정 생성

1️⃣  entity 만들기

package com.springschedule.entity;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "schedules")
public class Schedule extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 30)
    private String title;

    @Column(nullable = false, length = 200)
    private String content;

    @Column(nullable = false)
    private String authorName;

    @Column(nullable = false)
    private String password;

    public Schedule(String title, String content, String authorName, String password) {
        this.title = title;
        this.content = content;
        this.authorName = authorName;
        this.password = password;
    }
}

1) `public class Schedule extends BaseEntity`

- `extends` 해서 Schedule이 BaseEntity의 `createdAt`, `modifiedAt`을 그대로 물려받는다.
- Schedule 테이블에서 시간 컬럼이 같이 생긴다.

2) `@Entity`

- 이 클래스가 DB 테이블이라고 관리하게 하는 것

3) `@Table(name = "schedules")`

- 테이블 이름을 정해 주는 것이다. 안 정하면 보통 클래스명으로 만들어지고 명확히 하기 위해 그냥 지정.

4) `@Id` + `@GemeratedValue`

- @Id로 정해 주는 건 이게 기본키
- `@GeneratedValue` = 고유번호를 DB가 자동으로 증가시키면서 매겨 주는 것
- `IDENTITY`는 자동증가 방식

5) `@Column(nullable=false, length=)`

- `nullable=false` = 이것이 빈값이면 안 된다
- length는 최대길이를 제한한다

6) `@NoArgsConstructor(access = PROTECTED)`

- JPA는 엔티티를 만들 때 기본 생성자가 필요한데 아무나 막 못 쓰게 하려고 PROTECTED로 막는다

 

2️⃣ 레파지토리 만들기

package com.springschedule.repository;

import com.springschedule.entity.Schedule;
import org.springframework.data.repository.CrudRepository;

public interface ScheduleRepository extends CrudRepository<Schedule, Long> {
}

`JpaRepository`는 스프링이 이미 만들어 둔 DB 직원이라고 보면 된다
`<Schedule, Long>` 어떤 엔티티를 다룰 건지, 그 엔티티의 ID 타입이 뭔지? 이렇게 하면 `save()` `findById()` `findAll()` `deleteById()` 기능들을 기본 제공해 준다. 이걸 service에 호출해서 API를 만든다.

 

3️⃣ 일정 생성 요청 dto 만들기

package com.springschedule.schedule.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class CreateScheduleRequest {

    private String title;
    private String content;
    private String authorName;
    private String password;
}

 

4️⃣ 일정 생성 응답 dto 만들기

package com.springschedule.schedule.dto;

import lombok.Getter;

import java.time.LocalDateTime;

@Getter
public class CreateScheduleResponse {

    private final Long id;
    private final String title;
    private final String content;
    private final String authorName;
    private final LocalDateTime createdAt;
    private final LocalDateTime modifiedAt;

    public CreateScheduleResponse(Long id, String title, String content, String authorName, LocalDateTime createdAt, LocalDateTime modifiedAt) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.authorName = authorName;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
    }
}

password 필드가 없다. 응답에 비밀번호를 제외해야 한다.

 

5️⃣ 서비스 만들기

package com.springschedule.service;

import com.springschedule.dto.CreateScheduleResponse;
import com.springschedule.dto.ScheduleCreateRequest;
import com.springschedule.entity.Schedule;
import com.springschedule.repository.ScheduleRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ScheduleService {

    private final ScheduleRepository scheduleRepository;

    @Transactional
    public CreateScheduleResponse save(ScheduleCreateRequest request) {

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

        Schedule saved = scheduleRepository.save(schedule);

        return new CreateScheduleResponse(
                saved.getId(),
                saved.getTitle(),
                saved.getContent(),
                saved.getAuthorName(),
                saved.getCreatedAt(),
                saved.getModifiedAt()
        );
    }
}

`@Service` : 얘는 서비스다!
`@Transactional` : 이 메서드 안의 DB작업은 한 묶음으로 처리해라!

 

6️⃣ 컨트롤러 만들기

@RestController
@RequiredArgsConstructor
public class ScheduleController {

    private final ScheduleService scheduleService;

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

`@RestController`: 이 클래스가 API 컨트롤러다!
`@PostMapping`: POST 요청을 받음
`scheduleService.save(request)`: 여기서는 접수만 하고 서비스한테 보낸다
`@RequestBody` : 요청의 json을 CreateScheduleRequest에 담아서 준다


🚩 일정 조회

 


🚩 일정 수정

이번 일정 관리 API 과제를 진행하면서 수정 API를 PUT으로 할지 PATCH로 할지가 고민이었다 둘 다 ‘수정’이지만 HTTP 메서드는 각각 의도를 어떻게 적용하느냐에 따라 API 설계가 달라진다.

`PUT`은 원래 “리소스를 통째로 교체한다"는 의미에 가깝다
즉 서버에 “이 리소스는 이제 이 모습이야”라고 전체 상태를 전달하는 방식이다. 같은 요청을 여러 번 보내도 결과가 같아야 하는 멱등성(idempotent)을 가지는 것이 일반적이다

`PATCH`는 “리소스의 일부만 수정한다”는 의미에 가깝다.
즉 “여기 일부만 바꿔줘”라는 의도로 변경이 필요한 필드 중심으로 업데이트할 때 사용한다.

이 과제는 수정 기능에서 “수정 가능한 필드가 제한되어 있다”는 점이 핵심이었다 일정 제목, 작성자명만 수정 가능하다고 했으니까

그래서 일정 전체를 새로 교체하는 느낌이라기보다, 허용된 일부 필드만 바꾸는 부분 수정이 더 자연스러웠고, 그래서 수정 엔드포인트의 HTTP 메서드는 PATCH로 선택했다. PUT이든 PATCH든 “값을 지정해서 수정하는 방식”으로 구현하면 멱등성을 가질 수 있다. 예를 들어 같은 PATCH 요청을 여러 번 보내도 최종 결과가 같게 만들 수 있다. 정답이 있는 건지 모르겠지만 일단 PATCH를 선택한 이유...... 

비밀번호 틀렸을 때


🚩 일정 삭제


🚩 댓글 생성




🚩 일정 단건 조회 업그레이드

댓글을 가져오려면 scheduleId로 댓글들을 찾아야 된다. AI한테 메서드 이름 추천해 달라고 하니까 findByScheduleIdOrderByCreateAtAsc이러길래 너무 길다 생각했는데 더 생각하기 힘들어서 일단 직관적이기는 한 것 같아서 이걸로 했다. 댓글 응답 dto를 만들고 일정이랑 댓글들을 포함한 응답 dto를  만들었다. 그리고 service에 있는 findOne을 댓글 포함 버전으로 변경. 그리고 컨트롤러도 새로운 응답을 주도록 변경


🚩 유저의 입력에 대한 검증 수행

이걸 어떻게 해야 될지 고민이 됐다. service에서 if로 바로 검사해서 조건문으로 막을까 하다 같은 팀원 분이 검사 로직을 만들어서 따로 빼는 것을 보고 나도 그냥 클래스로 빼서 메서드로 만들기를 도전해 봤다...... 커머스 과제에서도 유틸로 뺐었던 기억도 있고 찾아서 해 봤다

package com.springschedule.common.validation;

public class InputValidator {

    public static void requireText(String value, String fieldName) {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("비었음. 작성해 주세요.");
        }
    }

    public static void requireMaxLength(String value, String fieldName, int maxLength) {
        if (value.length() > maxLength) {
            throw new IllegalArgumentException(fieldName + " 이건 최대 " + maxLength + "자까지 가능합니다......");
        }
    }
}

값이 비면 안 되니까 검사를 해서 비었으면 던져서 요청을 중단 시킨다. 
글자 수가 넘으면 안 되니까 검사를 해서 요청이 잘못됐다고 알려주고 진행을 멈춘다

그런데 팀원 분이 예외를 이런 식으로 처리를 하면 500으로 서버 오류를 내기 때문에 400으로 바꿔 주는 게 좋다고 해서 이건 어떻게 해야 되나 고민했다. 구글링을 하니 https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-controller/ann-exceptionhandler.html 여기에 나오는 대로 이런 식으로 처리를 해야 된다고 해서 또 공통적으로 예외를 처리하게 만들었다. 

package com.springschedule.common.exception;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class CommonExceptionHandler {

    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException e) {
        return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
    }
}

`@RestControllerAdvice`는 모든 컨트롤러에서 터진 예외를 여기서 한 번에 잡겠다는 의미.....
`@ExceptionHandler`는 이 타입의 예외가 오면 이 메서드가 처리한다는 뜻이다 이 메서드는 맞춤 강의를 들을 때 잠시 흘려 들었었는데 쓸 일이 왔다.

+ 추가

 // 일정이 없을 때 404 상태코드로 처리
    @ExceptionHandler(IllegalStateException.class)
    public ResponseEntity<ErrorResponse> handleIllegalState(IllegalStateException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(new ErrorResponse(e.getMessage()));
    }

 

일정 없음을 던졌을 때 명세에는 404 상태 코드로 작성을 했으면서 테스트 진행하다가 IllegalStateException으로 던졌을 때 500으로 터지는 걸 알게 됐다 이건 생각 못했는데...... 명세를 작성했으면 그대로 해야지

// save에 검증 추가
requireText(request.getTitle(), "schedule 제목");
        requireMaxLength(request.getTitle(), "schedule 제목", 30);
        requireText(request.getContent(),"schedule 내용");
        requireMaxLength(request.getContent(), "schedule 내용", 200);
        requireText(request.getAuthorName(), "작성자이름");
        requireText(request.getPassword(), "비밀번호");

// update에 검증 추가
requireText(request.getPassword(), "비밀번호");
        requireText(request.getTitle(), "schedule 제목");
        requireMaxLength(request.getTitle(), "schedule 제목", 30);
        requireText(request.getAuthorName(), "작성자이름");
 
// delete에 검증 추가
requireText(request.getPassword(), "비밀번호");

서비스 메서드 위에 검증을 먼저 추가했다.

package com.springschedule.common.exception;

import lombok.Getter;

@Getter
public class ErrorResponse {
    private final String message;

    public ErrorResponse(String message) {
        this.message = message;
    }
}

응답을 

{
  "message": "비었음. 작성해 주세요."
}

이런 식으로 보낼 수 있다.

https://velog.io/@kiiiyeon/%EC%8A%A4%ED%94%84%EB%A7%81-ExceptionHandler%EB%A5%BC-%ED%86%B5%ED%95%9C-%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC

 

[스프링부트] @ExceptionHandler를 통한 예외처리

@ExceptionHandler는 Controller계층에서 발생하는 에러를 잡아서 메서드로 처리해주는 기능이다.Service, Repository에서 발생하는 에러는 제외한다.간단한 예시부터 살펴보자.이렇게 @Controller로 선언된 클

velog.io

참고......

제목이 비었을 때
제목이 30자를 넘었을 때

 


🚩 트러블슈팅

❗단건 조회를 할 때 댓글이 빈 배열이고 값이 안 보임... 디비에는 있는데

db에는 댓글이 들어가 있고 로그를 봐도 댓글 조회 쿼리가 실행되는데 dto의 값을 꺼내지 못하는 경우였다. 이거 왜 이러나 했는데 응답 dto에 @getter를 안 넣는 이상한 실수를 했다 이런......

❗Expected 7 arguments but found 6 (dto 생성자 인자 개수 오류)

컴파일 단계에서 저런 에러가 발생했는데 dto에서 updateAt으로 인텔리제이에서 자동생성해 주는 이름을 아무 생각 없이 쓰다가 발생했다. 그래서 service에서 인자 수가 불일치했다...... 바꿔서 해결 뭔 자꾸 이런 실수를 하는지

❗검증 예외처리에서 ErrorResponse 때문에 컴파일 동작이 꼬였다

예외 처리기를 만들었는데 에러가 나고 타입이 맞지 않는 문제가 생겼다. 이건 import를 잘못해서 생긴 일이었다. 나는 클래명을 ErrorResponse로 지었는데 같은 이름이라 IDE가 자동 import를 하는 과정에서 스프링 클래스를 잡는 경우...... 클래스명도 혼동 없는 이름으로 CommonExceptionHandler로 바꿨다. 스프링에는 비슷한 이름의 클래스가 많네...... import가 버그가 될 수 있는 듯

 


🚩느낀 점

더보기

강의에서 나오는 실습 코드를 보면서 하긴 했는데 컨트롤러 -> 서비스 -> 레포지토리 -> DB -> 다시 응답 이런 흐름이 처음에 보이지가 않아서 어디서 문제가 생겼는지 찾는 게 어려웠다. 또 예외 처리 같은 애들은 정답이 하나가 아니니까 이런 것들을 선택하는 게 어려웠다. 이번 과제를 하면서 제일 어려웠던 건 명세를 먼저 쓰는 과정이었다. 코드로는 일정 만들고 조회하고 수정하고 삭제가 뭔지 알겠는데 그걸 문서로 정리하려고 하니까 갑자기 머리가 하얘졌다. 요청이랑 응답이 어떤 모양으로 되는지 왜 그렇게 해야 되는지 적는 게 생각보다 더 어렵고 오래 걸렸다. 튜터님들을 찾아가서 대충 틀을 어떻게 잡아야 될지 듣고 과정 시작할 때 처음 들은 API 명세 템플릿을 정해 준 것을 참고해서 표를 채워나가는 형식으로 할 수 있었다. 

 

그리고 CRUD 구현은 하나씩 따라가면서 할만했는데 댓글 기능은 난이도가 올라갔다. 일정이랑 댓글이 1:N 관계인가? 이건 알겠는데 실제로 단건 조회에서 댓글 목록까지 포함해서 내려주려고 하니까 dto도 늘어나고 흐름이 복잡해졌다. 그리고 마지막에 검증할 때는 머리가 빠지는 줄 알았다. 검증은 어디서 해야 되는지 어떤 예외로 던져야 되는지도 어려웠고 지금도 완전히 이해했다고 자신 있게 말하기는 어려운 것 같다. 

그래도 이번 과제를 통해서 막연했던 스프링 구조가 조금은 보이기 시작했다. 요청이 들어오면 컨트롤러가 받고, 서비스에서 로직과 검증을 처리하고, 레포지토리로 DB를 다루고, 다시 응답을 만드는 흐름을...... 아직 부족하지만 다음에는 명세를 더 꼼꼼하게 적기 시작해서 예외에 검증도 포함해서 처음부터 설계해 보고 싶다.

 


 

`3 Layer Architecture`

이번 과제에서는 Controller-Service-Repository의 3계층 구조로 API를 구현했다.

Controller는 요청을 받아서 응답을 돌려주는 역할만 한다 `POST` `/schedules`로 들어온 json을 @requestBody로 받으면 Controller는 그 값을 검증 저장 로직을 직접 처리하지 않고 Service에 넘긴다. 비즈니스 로직이 섞이지 않는다. URL매핑, 요청 값을 받는다거나, 상태코드를 결정하고 dto로 응답을 반환하는 역할을 한다

Service는 진짜 일을 처리하는 곳이다. 일정 수정할 때는 비밀번호가 맞는지 확인하고, 수정가능한 필드만 업데이트하고 검증 실패면 예외 발생 같은 것들이 Service에 들어간다. 이런 규칙은 HTTP와 무관하게 동일하게 적용되어야 하기 때문에 service에 두는 게 좋다. 비즈니스 규칙(댓글은 최대 10개), 입력 검증, 트랙잭션 관리, 예외 발생들은 Service가 한다.

Repository는 DB랑 대화하는 역할을 한다. Service가 일정 목록을 수정일 내림차순으로 가져와 달라고 요청하면 레파지토리는 JPA 쿼리 메서드로 데이터를 가져온다. 이렇게 분리를 하게 되면 DB 구조가 바뀌거나 조회 방식이 바뀌어도 Service와 Controller의 수정 범위가 줄어들게 된다.

 

`@PathVariable`
URL 자체에 포함된 경로의 일부를 변수로 받는다. 

`@RequestParam`
필터, 정렬, 검색 같은 선택적으로 붙는 조건에 사용한다 

`@RequestBody`
클라이언트가 보내는 json을 dto로 변환해서 받는다. 보통 생성이나 수정처럼 전달할 데이터가 많은 경우에 사용한다. 구조가 명확해진다.

 

저작자표시 비영리 (새창열림)

'IL > WIL' 카테고리의 다른 글

[WIL] Spring - 일정 관리 앱 업글  (0) 2026.02.09
[WIL] - Java 커머스 과제  (0) 2026.01.16
20260107 [WIL] 왜 Git을 배우는데 머리가 아플까  (0) 2026.01.07
'IL/WIL' 카테고리의 다른 글
  • [WIL] Spring - 일정 관리 앱 업글
  • [WIL] - Java 커머스 과제
  • 20260107 [WIL] 왜 Git을 배우는데 머리가 아플까
견지
견지
개발로 개발하는지 새발로 개발하는지 내가 개인 건지 새인 건지 사람인 건지
  • 견지
    개발새발
    견지
  • 전체
    오늘
    어제
    • 분류 전체보기 (20)
      • ... (0)
      • IL (20)
        • TIL (16)
        • WIL (4)
        • MIL (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    CSS
    DB
    git
    HTML
    oracle
    java
    JSP
    JavaScript
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
견지
[WIL] Spring - 일정 관리 앱 만들기
상단으로

티스토리툴바