개요
CH 3 일정 관리 앱 Develop 필수/도전 기능을 구현하면서 고민했던 부분과 트러블 슈팅 과정에 대해 작성해보겠습니다.
📌 필수/도전 기능 가이드
필수 기능
Lv 1. 일정 CRUD
JPA를 이용해 CRUD를 구현했습니다.
이전에 Spring JDBC로 일정 관리 앱을 구현했던 것과 비교했을 때, JPA를 활용하니 더 편리했습니다.
| 차이점 | Spring JDBC | Spring Data JPA |
| SQL 작성 여부 | 직접 SQL 작성 | SQL 작성 불필요 |
| CRUD 메서드 구현 | 직접 작성 | sava, findById 등 기본 제공 |
| 조회 결과 | RowMapper로 결과 매핑 | 엔티티 매핑 자동 처리 |
JPA Auditing을 활용한 작성일, 수정일 자동 관리
`@EnableJpaAuditing`을 Application 클래스에 추가해서 JPA Auditing을 사용하도록 했습니다.
@EnableJpaAuditing
@SpringBootApplication
public class SpringJpaScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringJpaScheduleApplication.class, args);
}
}
공통 필드를 관리할 클래스로 BaseEntity를 생성했습니다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
BaseEntity를 상속받도록 하여 Schedule 엔티티에서 생성일, 수정일을 관리할 수 있도록 했습니다.
@Getter
@Entity
@Table(name = "schedule")
public class Schedule extends BaseEntity { ... }
Lv 2. 유저 CRUD
Schedule 엔티티와 Member 엔티티가 연관관계를 가지도록 구현했습니다.
@Getter
@Entity
@Table(name = "schedule")
public class Schedule extends BaseEntity {
/* 일정 필드 생략 */
@Setter
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
}

Lv 3. 회원가입
Member 엔티티 필드에 비밀번호를 추가하고, 멤버 등록 API의 매핑을 sign-up으로 변경했습니다.
회원가입에 필요한 RequestDto와 ResponseDto를 생성해서 사용했습니다.
Lv 4. 로그인(인증)
세션 기반 로그인(인증)을 구현했습니다.
📌 전체 흐름
- 사용자가 로그인 요청 → AuthController → AuthService
- 이메일, 비밀번호 검증 → 로그인 성공 시 세션(Session)에 회원 정보 저장
- 로그인 이후 API 접근 시 로그인 여부 확인 → LoginFilter
- 세션이 없거나 로그인 정보 없으면 401 응답
LoginFilter – API 요청 시 로그인 여부 확인
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/* 생략 */
}
}
- /api/* 경로에 접근할 때 실행
- 화이트리스트(로그인, 회원가입 등) 외에는 세션 존재 여부 확인
- 세션 없거나 로그인 정보 없으면 401 Unauthorized 응답 반환
WebConfig – 필터 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public FilterRegistrationBean<LoginFilter> loginFilter() {
/* 생략 */
}
}
- LoginFilter를 Bean 등록
- /api/* 경로에 적용
도전 기능
Lv 5. 다양한 예외처리 적용하기
Spring JDBC로 예외처리를 적용했던 것과 마찬가지로 예외 처리를 적용했습니다.
Member 엔티티의 email은 로그인시 사용자를 체크하는 필드이기때문에 Unique 제약조건을 추가했습니다.
@Column(nullable = false, unique = true, length = 50)
private String email;

Lv 6. 비밀번호 암호화
BCrypt hashing을 적용해서 비밀번호를 암호화했습니다.
Lv 7. 댓글 CRUD
Member 엔티티를 참조해서 댓글 작성자를 저장하도록하고, Schedule 엔티티를 참조해서 댓글을 단 일정을 알 수 있도록 연관관계를 설정했습니다.
@Getter
@Entity
@Table(name = "comment")
public class Comment extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 200)
private String contents;
@Setter
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
@Setter
@ManyToOne
@JoinColumn(name = "schedule_id")
private Schedule schedule;
}
Lv 8. 일정 페이징 조회
Pageable과 Page 인터페이스를 활용하여 페이지네이션을 구현했습니다.
PaginationResponse
Page<T> 객체의 불필요한 내부 필드를 추려서 반환하기 위해서, PaginationResponse 생성하여 공통 페이징 응답 형식을 적용시켰습니다.
@Getter
public class PaginationResponse<T> {
private final List<T> content;
private final int pageNumber;
private final int pageSize;
private final long totalElements;
private final int totalPages;
private final boolean isLast;
private final boolean isFirst;
private final boolean isSorted;
public PaginationResponse(Page<T> page) {
this.content = page.getContent();
this.pageNumber = page.getNumber();
this.pageSize = page.getSize();
this.totalElements = page.getTotalElements();
this.totalPages = page.getTotalPages();
this.isLast = page.isLast();
this.isFirst = page.isFirst();
this.isSorted = page.getSort().isSorted();
}
}
ScheduleDetailResponseDto
일정을 조회할 때,
할일 제목, 할일 내용, 댓글 개수, 일정 작성일, 일정 수정일, 일정 작성 유저명 필드를 조회하도록 반환 Dto를 수정했습니다.
@Getter
@AllArgsConstructor
public class ScheduleDetailResponseDto {
private final Long id;
private final String title;
private final String contents;
private final String memberName;
private final long commentCount;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
}
JPQL(@Query) 활용하여 Join으로 댓글 개수(count) 포함 조회
@Query와 JPQL을 활용하여 JOIN으로 일정(Schedule)과 댓글(Comment) 개수를 한 번에 조회하도록 했습니다.
@Query("SELECT new com.example.schedule.dto.schedule.ScheduleDetailResponseDto(" +
"s.id, s.title, s.contents, m.name, COUNT(c), s.createdAt, s.updatedAt) " +
"FROM Schedule s " +
"JOIN s.member m " +
"LEFT JOIN Comment c ON c.schedule = s " +
"GROUP BY s.id, s.title, s.contents, m.name, s.createdAt, s.updatedAt")
Page<ScheduleDetailResponseDto> findAllWithCommentCount(Pageable pageable);
- new 패키지명.Dto명() 사용 ➡️ DTO로 직접 결과 매핑
- JOIN 사용
- Schedule과 Member JOIN
- Comment는 LEFT JOIN (Comment가 없는 Schedule도 있기 때문에)
- COUNT()로 댓글 개수 집계
- COUNT를 사용하기 위해서 GROUP BY 필요 (모든 필드에 대해서)
추가 기능
목록 조회 시 필터링 추가
일정과 댓글 목록을 조회할 때 RequestParam으로 필터 조건을 전달해주도록 했습니다.
Controller
@GetMapping
public ResponseEntity<PaginationResponse<ScheduleDetailResponseDto>> getSchedules(
@RequestParam(required = false) String title,
@RequestParam(required = false) String name,
@RequestParam(required = false) String updatedAt,
@PageableDefault(size = 10, page = 0, sort = "updatedAt", direction = Sort.Direction.DESC) Pageable pageable
) {
Page<ScheduleDetailResponseDto> schedulePage = scheduleService.getSchedules(
title,
name,
updatedAt,
pageable
);
PaginationResponse<ScheduleDetailResponseDto> schedulePageResponse = new PaginationResponse<>(schedulePage);
return new ResponseEntity<>(schedulePageResponse, HttpStatus.OK);
}
Repository
@Query("SELECT new com.example.schedule.dto.schedule.ScheduleDetailResponseDto(" +
"s.id, s.title, s.contents, m.name, COUNT(c), s.createdAt, s.updatedAt) " +
"FROM Schedule s " +
"JOIN s.member m " +
"LEFT JOIN Comment c ON c.schedule = s " +
"WHERE (:title IS NULL OR s.title LIKE %:title%) " +
"AND (:name IS NULL OR m.name LIKE %:name%) " +
"AND (:updatedAt IS NULL OR FUNCTION('DATE_FORMAT', s.updatedAt, '%Y-%m-%d') = :updatedAt) " +
"GROUP BY s.id, s.title, s.contents, m.name, s.createdAt, s.updatedAt")
Page<ScheduleDetailResponseDto> findAllWithCommentCount(
@Param("title") String title,
@Param("name") String name,
@Param("updatedAt") String updatedAt,
Pageable pageable
);
- 일정 제목과 작성자 이름 같은 경우는 LIKE로 조회
결과


본인 확인 로직 추가: 세션의 유저 정보 활용
기존에는 회원, 일정, 댓글을 수정 및 삭제 시 PathVariable로 ID를 직접 받아서 처리했지만,
이 방식은 사용자가 URL에 임의로 ID를 넣을 수 있기 때문에, 로그인한 상태라면 세션에서 사용자 정보를 가져와 활용하도록 수정했습니다.
0. AuthService에 사용자 정보 조회 메서드 추가
public Long getLoggedInMemberId(HttpSession session) {
Long memberId = (Long) session.getAttribute(Const.LOGIN_MEMBER);
if (memberId == null) {
throw new UnauthenticatedException();
}
return memberId;
}
- 세션에서 LOGIN_MEMBER 키로 사용자 ID를 가져옴
- 없으면 예외처리! (401 Unauthorized)
1. 회원/일정/댓글 수정 또는 삭제 시 본인 확인
예: 일정 삭제 API 서비스단
@Transactional
public void deleteSchedule(Long id, HttpSession session) {
Long loggedInMemberId = authService.getLoggedInMemberId(session);
Schedule findSchedule = scheduleRepository.findByIdOrElseThrow(id);
if (!findSchedule.getMember().getId().equals(loggedInMemberId)) {
throw new UnauthorizedException("본인이 작성한 일정만 삭제할 수 있습니다.");
}
scheduleRepository.delete(findSchedule);
}
- 세션에서 로그인한 유저 ID 조회
- 일정을 작성한 유저 ID와 비교하여 다를경우 예외처리! (403 Forbidden)
2. 일정/댓글 작성 시 로그인한 사용자 ID 사용
예: 일정 생성 API 서비스단
@Transactional
public ScheduleResponseDto createSchedule(String title, String contents, HttpSession session) {
Long loggedInMemberId = authService.getLoggedInMemberId(session);
Member findMember = memberRepository.findByIdOrElseThrow(loggedInMemberId);
Schedule schedule = new Schedule(title, contents);
schedule.setMember(findMember);
Schedule savedSchedule = scheduleRepository.save(schedule);
return ScheduleResponseDto.toDto(savedSchedule);
}
- 세션에서 로그인한 사용자 ID를 가져와서 작성한 유저 ID로 설정
3. 유저 수정/삭제 시 PathVariable 제거
예: 유저 삭제 API 컨트롤러단

- PathVariable로 id를 전달하던것을 제거하고, session만 전달해서 현재 로그인한 사용자를 삭제하도록 수정
@Transactional 어노테이션 적용
Transactional 어노테이션을 서비스의 모든 메서드에 적용하는게 맞는지 헷갈려서 내용을 정리하고 적용해봤습니다.
2025.02.12 - [Java Study/Frameworks] - [Spring] @Transactional을 어디에 붙여야 할까🤔❔
[Spring] @Transactional을 어디에 붙여야 할까🤔❔
Spring JPA로 일정 관리 앱을 만들어보면서, @Transactionla을 붙이는 기준을 공부해봤다. @Transactional`@Transactional`은 데이터베이스의 일관성을 보장하기 위해 사용된다.쓰기(INSERT, UPDATE, DELETE) 연
mannakingdom.tistory.com
해결 못한 부분
1. 수정 후 updatedAt이 반영이 안되는 문제
Schedule 엔티티에 updateSchedule 메서드를 호출해서, 제목과 내용을 변경하고, 변경된 엔티티를 다시 조회하여 응답하도록 했습니다.
그런데 실행해보니 일정 수정 API의 response로 updatedAt이 반영이 안된걸 확인했습니다.
@Transactional
public ScheduleResponseDto updateSchedule(Long id, ScheduleUpdateRequestDto requestDto, HttpSession session) {
Long loggedInMemberId = authService.getLoggedInMemberId(session);
Schedule findSchedule = scheduleRepository.findByIdOrElseThrow(id);
if (!findSchedule.getMember().getId().equals(loggedInMemberId)) {
throw new UnauthorizedException("본인이 작성한 일정만 수정할 수 있습니다.");
}
findSchedule.updateSchedule(requestDto.getTitle(), requestDto.getContents());
Schedule updatedSchedule = scheduleRepository.findByIdOrElseThrow(id);
return ScheduleResponseDto.toDto(updatedSchedule);
}



JPA의 영속성 컨텍스트 때문에 변경된 엔티티를 조회하는게 의미가 없나 싶어서 (그냥 추측입니다,,)
수정후 엔티티를 따로 조회하지 않고 그냥 반환해도 똑같이 updatedAt이 반영이 안됩니다.
@Transactional
public ScheduleResponseDto updateSchedule(Long id, ScheduleUpdateRequestDto requestDto, HttpSession session) {
Long loggedInMemberId = authService.getLoggedInMemberId(session);
Schedule findSchedule = scheduleRepository.findByIdOrElseThrow(id);
if (!findSchedule.getMember().getId().equals(loggedInMemberId)) {
throw new UnauthorizedException("본인이 작성한 일정만 수정할 수 있습니다.");
}
findSchedule.updateSchedule(requestDto.getTitle(), requestDto.getContents());
return ScheduleResponseDto.toDto(findSchedule);
}



아직 왜 반영이 안되는지 확인을 못했습니다🥲
확인 후 수정할 예정입니다 ✅,,,
2. 연관관계 (양방향? 단방향?)
멤버(Member), 일정(Schedule), 댓글(Comment) 엔티티가 서로 연관관계가 있는데,
멤버에 작성된 일정이나 댓글이 있으면 멤버 삭제가 되지 않아서 ON DELETE CASCADE를 적용하려고 cascade = REMOVE와 orphanRemoval = true를 사용하여 양방향 연관관계를 설정했습니다.
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Schedule> schedules = new ArrayList<>();
@OneToMany(mappedBy = "member", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Comment> comments = new ArrayList<>();
그런데 과제 설명을 다시 보니 "JPA 연관관계는 단방향입니다. 정말 필요한 경우에만 양방향 적용!" 이라는 안내 문구를 확인하게 되었습니다.
단방향 관계를 유지하면서도, 멤버 삭제 시 관련 일정이나 댓글을 삭제하는 일반적인 처리 방식을 알아봐야겠습니다 🤓
마무리
헷갈리고 모르겠는 내용이 많아서 나름 정리하면서 했는데도 뇌에 차곡차곡 안쌓여서 괴롭지만,,,

과제 완성하고 나니 확실히 처음보다는 뭔가 알 것 같은 느낌이 들기도 합니다. 야호〰️
시간 부족으로 해결하지 못한 부분들을 공부/질문 해보고 과제 피드백도 반영하여 개선을 꼭 해보겠습니다❗❗
https://github.com/mannaKim/spring-jpa-schedule
GitHub - mannaKim/spring-jpa-schedule: 일정 관리 앱 Develop - Spring JPA
일정 관리 앱 Develop - Spring JPA. Contribute to mannaKim/spring-jpa-schedule development by creating an account on GitHub.
github.com
'Dev Projects' 카테고리의 다른 글
| [table-now] Refresh Token 저장소를 Redis로 교체하며 인증 구조 고도화하기 (0) | 2025.04.21 |
|---|---|
| [table-now] 소셜 로그인(Kakao, Naver) 통합 구현기 (1) | 2025.04.21 |
| [Spring JDBC_일정 관리 앱 만들기] 회고 및 트러블 슈팅 (1) | 2025.02.04 |
| [Java Project_키오스크 과제] 도전 기능 구현 및 트러블 슈팅 (0) | 2025.01.20 |
| [Java Project_키오스크 과제] 필수 기능 구현 및 단계별 설계 (0) | 2025.01.15 |




