<aside>
일정 관리 앱 디밸롭 과제 링크 (repository)
https://github.com/kolyn092/ScheduleManagementDevelop.git
구현 리스트
- [x] 일정 CRUD
- [x] 유저 CRUD
- [x] 회원가입 (비밀번호 필드 추가)
- [x] 로그인
- [x] 예외처리 (Validation)
- [x] 비밀번호 암호화
- [x] 댓글 CRUD
- [x] 일정 페이징 조회
</aside>
<aside>
작업 내역
작업 의도
-
중복되는 필드끼리 묶어서 관리하고, 명시적으로 기능 별 dto를 생성하여 관리
중복되는 필드를 묶어서 별도의 dto로 만들고, 명시적으로 어떤 dto인지 알기 위해 추가적인 필드가 없지만 상속을 받아서 사용했다. (**현재는 로직 변경으로 인해 사용하지 않는다)
-
패키지 분리 의도 (기능 별 vs 레이어 계층 별)
- 기능 별로 패키지를 분리하는 경우에는 기능과 관련된 코드들을 응집해서 볼 수 있다는 것이고, 레이어 별로 패키지를 분리하는 경우에는 각 레이어 별로 묶어서 볼 수 있다는 것이다.
- 현재 프로젝트의 변경 영향 범위와 응집도를 기준으로 선택하는 것이 좋다. 즉, 큰 규모의 프로젝트에서는 기능 별로 패키지를 분리하는 것이 좋고, 작은 규모의 프로젝트에서는 레이어 별로 패키지를 분리하는 것이 좋다.
- 현재는 기능 별로도 패키지를 분리해보고 싶어서 시도해보았다.
-
User 데이터를 어떻게 Service 단에서 가져올 것인가
- 이전 과제에서는 스케쥴 Service에서 타 도메인(댓글)의 Repository를 직접 주입 받아서 사용했었다. 즉, 단일 책임 원칙(SRP)을 위배했었다.
- 해당 부분을 보완하기 위해 Repository를 주입 받는 것이 아닌, Service를 통해 데이터를 받도록 구현했다.
-
유저 수정, 삭제 기능을 사용자의 기능으로 구현할 것인가, 관리자 기능으로 구현할 것인가
- 사용자의 기능 관점으로 개발을 진행해보았다.
- 회원 정보 수정, 탈퇴(삭제)
-
signup을 어떤 도메인에서 구현해야 하는가?
- 처음에는 User 도메인으로 구현했다가 인증을 구현하면서 Auth 도메인으로 변경했다.
-
페이지네이션을 쿼리로 작성해서 dto로 바로 받아서 사용 vs 엔티티 객체로 받은 것을 dto로 변환해서 사용
- 쿼리를 작성하는 것이 한 번에 보기 편해서 쿼리로 dto를 받아서 사용하도록 구현했다.
-
유저 데이터를 저장하는 엔티티가 하나이고, 유저를 soft delete하고 이후에 동일 이메일로 재가입하는 경우를 어떻게 처리할 것인가
- 회원 가입할 때 이메일이 존재하는지 검사하는 함수에 delete가 false인 조건을 추가했다.
트러블 슈팅
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
[Request processing failed:
org.springframework.dao.DataIntegrityViolationException:
not-null property references a null or transient value for entity
com.schedule.entity.Schedule.createdAt] with root cause
- 원인 : nullable 옵션이 false인 컬럼에 null 값이 들어가서 오류가 발생했다.
- 해결 : request body 값은 정상적으로 파라미터로 보내지고 있었다. 그래서 createAt과 modifiedAt의 값이 생성되지 않는다는 것을 깨달았고, JPA Auditing 설정에 문제가 있다는 것을 알 수 있었고 @EnableJpaAuditing 어노테이션을 통해 Auditing 인프라를 활성화했다.
Resolved [org.springframework.web.bind.ServletRequestBindingException:
Missing session attribute 'loginUser' of type UserSession]
- 오류 : SessionAttribute의 required 옵션이 true 상태에서 session이 null 인 경우 내부에 null 체크 로직이 진행되지 않고 바로 예외를 throw했다.
- 해결 : required 옵션을 true로 유지한 채 에러를 핸들링하기 위해 GlobalExceptionHandler에 ServletRequestBindingException를 핸들링하는 코드를 만들었다.
The dependencies of some of the beans in the application context form a cycle:
commentController defined in file [CommentController.class]
┌─────┐
| commentService defined in file [CommentService.class]
↑ ↓
| scheduleService defined in file [ScheduleService.class]
└─────┘
- 오류 : 일정 단 건 조회 시 댓글 정보를 포함하는 로직을 추가하니 서로 참조하게 되어 순환 참조가 발생하여 실행이 불가능했다.
- 과정 : CommentService에서 ScheduleService 의존을 제거하고, EntityManager의 getReference()를 활용하여 연관 엔티티를 참조하도록 변경했다. 하지만 단일 책임 원칙(SRP)을 위배하게 되었다.
- 해결 : ApplicationService 계층을 도입하여 여러 서비스가 필요한 복합 기능들을 해당 계층에서 관리하도록 리팩토링하여 순환 참조 문제를 해결하게 되었다.
java.sql.SQLIntegrityConstraintViolationException:
Cannot delete or update a parent row: a foreign key constraint fails
(`developdb`.`schedules`, CONSTRAINT `fk_schedule_user` FOREIGN KEY (`user_id`)
REFERENCES `users` (`id`))
- 원인 : 유저, 일정, 댓글이 만들어져 있을 때, 유저를 삭제하거나 일정을 삭제하는 경우 FK키 오류가 발생했다. (부모를 먼저 삭제하는 문제)
- 해결
- 일정 삭제 - 댓글을 먼저 삭제한 후 일정을 삭제하도록 구현하여 해결했다.
- 유저 삭제 - 유저에 삭제 여부를 저장하는 컬럼을 생성하고 실제 유저 데이터는 삭제하지 않지만 삭제된 유저로 나타내도록 구현했다. (soft delete)
회고
- 이전 과제에서는 Spring 사용법울 익히는 데에 집중했다면, 이번 과제에서는 설계(패키지 구조, 계층 책임, 예외 처리 등)와 다양한 문제를 해결하는 경험을 할 수 있었다.
- 특히 단순 기능 구현을 넘어, 유지보수성과 확장성을 고려한 구조로 설계하고 리팩토링하는 과정이 중요하다는 것을 깨달았다.
</aside>