로그인 필터 로직에서, 로그인 해주세요 응답 처리하기
// 기존 코드
if (!isWhiteList(requestURI)) {
// 로그인 확인 -> 로그인하면 session에 값이 저장되어 있다는 가정.
// 세션이 존재하면 가져온다. 세션이 없으면 session = null
HttpSession session = httpRequest.getSession(false);
// 로그인하지 않은 사용자인 경우
if (session == null || session.getAttribute("sessionKey값") == null) {
throw new RuntimeException("로그인 해주세요.");
}
// 로그인 성공 로직
}

로그인 안 했을 때 접근하면 이렇게 에러만 뜬다.
아래 블로그를 참고하여 커스텀 에러를 만들어보려 했지만 모종의 이유에서 그만두고..

if(session == null || session.getId()==null) {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setContentType("application/json");
httpResponse.getWriter().write("{\"error\":\"로그인 해주세요\"}");
return;
}
그냥 filter 내부에서 오류 메세지를 json으로 내보내주기로 했다.

근데 이렇게 물음표만????
-> 이건 백퍼 UTF-8 설정을 안해줘서 그럼
if(session == null || session.getId()==null) {
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpResponse.setContentType("application/json; charset=utf-8");
httpResponse.getWriter().write("{\"error\":\"로그인 해주세요\"}");
return;
}
이제 아래와 같이 잘 나오는 것을 볼 수 있다!

추가적으로, 다른 응답 처리들에 대해서도 GlobalExceptionHandler 클래스를 만들어서 컨트롤러에서 설정해둔 에러 메세지들이 응답에 잘 출력되도록 구현하였다!👏🏻
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex){
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
@ExceptionHandler(ResponseStatusException.class)
public ResponseEntity<Map<String, Object>> handleResponseStatusException(ResponseStatusException ex, HttpServletRequest request){
Map<String, Object> errors = new HashMap<>();
errors.put("status", ex.getStatusCode().value());
errors.put("message", ex.getMessage());
errors.put("path", request.getRequestURI());
return ResponseEntity.status(ex.getStatusCode()).body(errors);
}
}

Long countCommentByTaskId(Long taskId);
public Long countComments(Long taskId) {
Comment findTask = commentRepository.findByIdOrElseThrow(taskId);
return commentRepository.countCommentByTaskId(taskId);
}
countRepository에서 이런 식으로 댓글 세기 구현했는데 오류가 발생하였다.
Caused by: org.springframework.data.mapping.PropertyReferenceException: No property 'id' found for type 'Task'; Traversed path: Comment.task
오류 원인은 CommentRepository의 countCommentByTaskId(Long taskId) 메서드에서 Task 엔티티에 id 속성이 존재하지 않는다는 문제이다.
음.. JPA가 내가 작성한 메서드를 이해하지 못하는 것 같다.
나는 헷갈리는 걸 방지하기 위해서 여러 entity들의 id들을 모두 ___Id 구성으로 만들었는데, 내가 만든 countCommentByTaskId 메서드는 id 필드로 찾고 있는 것임!
public interface CommentRepository extends JpaRepository<Comment, Long> {
Long countCommentByTask_Id(Long taskId); // 기존 메서드 대신 수정
}

그래서 필드명을 id로 바꾸었고, 메서드도 countCommentByTask_Id(Long taskId)로 바꾸었다.
하지만!!!!!! 오류를 해결했어도 뭔가 계속 복잡해져서..
public class CommentService {
private final CommentRepository commentRepository;
private final TaskRepository taskRepository;
private final UserRepository userRepository;
@Transactional
public CommentResponseDto createComment(HttpServletRequest request, Long taskId, CommentRequestDto requestDto) {
HttpSession session = request.getSession();
Long userId = (Long) session.getAttribute(Const.LOGIN_USER_ID);
User user = userRepository.findByIdOrElseThrow(userId);
Task findTask = taskRepository.findByIdOrElseThrow(taskId);
Comment comment = new Comment(requestDto.getContent(), findTask, user);
Comment saved = commentRepository.save(comment);
// 이 부분!!!!
findTask.updateNumOfComments(findTask.getNumOfComments() + 1);
return new CommentResponseDto(saved.getId(), saved.getContent());
}
Comment saved = commentRepository.save(comment);
게시물을 조회할 때마다 댓글을 count해주는 게 아니라 애초에 comment를 달 때 새로 업데이트를 해줘야하는 것!
추가로 update요소가 들어갔기 때문에 @Transactional 어노테이션이 꼭 필요하다!
왜 댓글 달 때 카운트를 추가해줄 수 있는 걸 생각을 못하고 헛짓을 했을까........
➡️ 중간 피드백 중 튜터님께서 이 부분에 대한 이야기를 해주셨다!!
나도 처음에는 무조건 db에 저장된 댓글 수를 count 하려 했는데, 비정규화/역정규화 와 관련되어 있다고 한다.
인스타그램 저스틴비버 문제 라고 검색하면 나옴 결론은 내가 한 방식대로 +1 해주는게 역정규화 라고 하고, 잘 구현했다는 뜻!!
Entity 설정할 때에 필드 이름이나 메서드 이름을 어떻게 작성해야할지 고민하게 되었다는 것
코딩할 때에는 나 편하자고 그냥 무슨무슨id로 적어놨었는데 흠;...
모르겠다!!
➡️ 이것도 피드백에서 여쭤봤는데 맘대로 해도 괜찮다고 하심.
아마 인식 못했던 거는 내가 메서드 구현을 잘못했을 가능성이 크다는 것.. ㅎㅎ
(실제로 다 알게되고 다시 생각해보니 내가 잘못 작성한 게 맞았다.)

By 뒷부분은 SQL에서 WHERE절과 동일하다.중간 피드백을 받는데 튜터님께서 고민해보라고 하셨다.
이미 로그인이 되어있는데, 서버 문제로 인하여 다시 로그인 호출을 하게 된다면?!
//로그인 컨트롤러
@PostMapping("/login")
public ResponseEntity<String> login(@Valid @RequestBody LoginRequestDto requestDto, HttpServletRequest request) {
HttpSession session = request.getSession(false);
if(session != null && session.getAttribute("userId") != null){
log.info("******** 이미 로그인 되어있을때 getSession"+request.getSession().getId()+"************");
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이미 로그인되어 있습니다. 로그아웃 후 다시 시도해주세요.");
}
LoginResponseDto user = loginService.login(requestDto);
if(user == null){
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "로그인 실패: 아이디 또는 비밀번호를 확인하세요.");
}
Long userId = user.getUserId();
HttpSession newSession = request.getSession();
UserResponseDto loginUser = userService.findUserById(userId);
newSession.setAttribute(Const.LOGIN_USER, loginUser);
newSession.setAttribute(Const.LOGIN_USER_ID, userId);
return ResponseEntity.ok(loginUser.getUsernamme()+"님 로그인 성공!");
}
if(session != null && session.getAttribute("userId") != null){ log.info("******** 이미 로그인 되어있을때 getSession"+request.getSession().getId()+"************"); throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이미 로그인되어 있습니다. 로그아웃 후 다시 시도해주세요."); }어떻게하면 첫 로그인인데 로그인된 사용자라고 뜨고 🥲
아님 아예 이 검증이 안되고 난리가 났다. 저if문자체에 안 걸리는 것..
그래서 아래처럼 고쳐봤다! 로그를 찍어서 어느 부분이 Null 이길래 검증이 안되는 것인지 확인해보자.
<검증>
if(session != null){
log.info("*** 이미 로그인 되어있는 사용자라면?");
log.info("*** session.getAttribute (userId) : "+session.getAttribute("userId"));
log.info("*** session.getAttribute(loginUser) : "+session.getAttribute("loginUser"));
log.info("*** getSession().getId() : "+request.getSession().getId()+"************");
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이미 로그인되어 있습니다. 로그아웃 후 다시 시도해주세요.");
}
<결과>
로그인 필터 로직 실행
2025-02-12T20:25:35.109+09:00 INFO 9931 --- [ScheduleProject_develop] [nio-8080-exec-5] c.e.s.auth.AuthController : *** 이미 로그인 되어있는 사용자라면?
2025-02-12T20:25:35.109+09:00 INFO 9931 --- [ScheduleProject_develop] [nio-8080-exec-5] c.e.s.auth.AuthController : *** session.getAttribute (userId) : null
2025-02-12T20:25:35.109+09:00 INFO 9931 --- [ScheduleProject_develop] [nio-8080-exec-5] c.e.s.auth.AuthController : *** session.getAttribute(loginUser) : com.example.scheduleproject_develop.user.dto.UserResponseDto@1fe6836c
2025-02-12T20:25:35.109+09:00 INFO 9931 --- [ScheduleProject_develop] [nio-8080-exec-5] c.e.s.auth.AuthController : *** getSession().getId() : 7FFD4BF4EFE31C83209BD386E63C832A************
로그 보니까 userId는 왜 안나오는지..>??흠냐
if(session != null){
log.info("*** 이미 로그인 되어있는 사용자라면?");
log.info("*** session.getAttribute(userId) : "+session.getAttribute(Const.LOGIN_USER_ID));
log.info("*** session.getAttribute(loginUser) : "+session.getAttribute(Const.LOGIN_USER));
log.info("*** getSession().getId() : "+request.getSession().getId()+"************");
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이미 로그인되어 있습니다. 로그아웃 후 다시 시도해주세요.");
}
------------------------------------------------------------------------------
로그인 필터 로직 실행
2025-02-12T20:31:22.535+09:00 INFO 10039 --- [ScheduleProject_develop] [nio-8080-exec-4] c.e.s.auth.AuthController : *** 이미 로그인 되어있는 사용자라면?
2025-02-12T20:31:22.535+09:00 INFO 10039 --- [ScheduleProject_develop] [nio-8080-exec-4] c.e.s.auth.AuthController : *** session.getAttribute(userId) : 1
2025-02-12T20:31:22.535+09:00 INFO 10039 --- [ScheduleProject_develop] [nio-8080-exec-4] c.e.s.auth.AuthController : *** session.getAttribute(loginUser) : com.example.scheduleproject_develop.user.dto.UserResponseDto@42c6b81
2025-02-12T20:31:22.536+09:00 INFO 10039 --- [ScheduleProject_develop] [nio-8080-exec-4] c.e.s.auth.AuthController : *** getSession().getId() : 6EB9E0FF3CA241D24FE61AF4A26E1FAC************
이렇게 바꾸니까 정상적으로 로그가 찍힌다. 그래서 보니까

// Const.java
package com.example.scheduleproject_develop.common;
public interface Const {
String LOGIN_USER = "loginUser";
String LOGIN_USER_ID = "loginUserId";
}
하하핳 내가 이렇게 정해두었었네..
(아마 강의자료 복붙했어서 그런가보다... 강의자료라도 제대로 이해하고 기억하고 사용하자!!!)
완전 삽질했다 ㅡㅡ,,
if(session != null){
log.info("*** 이미 로그인 되어있는 사용자라면?");
log.info("*** session.getAttribute(userId) : "+session.getAttribute("loginUserId"));
log.info("*** session.getAttribute(loginUser) : "+session.getAttribute("loginUser"));
log.info("*** getSession().getId() : "+request.getSession().getId()+"************");
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "이미 로그인되어 있습니다. 로그아웃 후 다시 시도해주세요.");
}
최종 수정 이렇게 하니까 잘 출력도 되고, 로그인된 사용자라고 session 검출도 잘 되었다!
2025-02-13T00:14:43.690+09:00 WARN 11529 --- [ScheduleProject_develop] [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1451, SQLState: 23000
2025-02-13T00:14:43.690+09:00 ERROR 11529 --- [ScheduleProject_develop] [nio-8080-exec-2] o.h.engine.jdbc.spi.SqlExceptionHelper : Cannot delete or update a parent row: a foreign key constraint fails (`task`.`task`, CONSTRAINT `FK2hsytmxysatfvt0p1992cw449` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`))
2025-02-13T00:14:43.695+09:00 ERROR 11529 --- [ScheduleProject_develop] [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [Cannot delete or update a parent row: a foreign key constraint fails (`task`.`task`, CONSTRAINT `FK2hsytmxysatfvt0p1992cw449` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`))] [/* delete for com.example.scheduleproject_develop.user.User */delete from user where user_id=?]; SQL [/* delete for com.example.scheduleproject_develop.user.User */delete from user where user_id=?]; constraint [null]] with root cause
java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`task`.`task`, CONSTRAINT `FK2hsytmxysatfvt0p1992cw449` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`))
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:109) ~[mysql-connector-j-9.1.0.jar:9.1.0]
- 참조 무결성
- 외래키 제약 조건
user 테이블에서 userId를 삭제할 때, 해당 userId를 참조하는 다른 테이블의 데이터(예: task, comment)가 존재하면 외래키 제약 조건으로 인해 삭제가 불가능하다.
userRepository.delete(findUser) 메서드로 사용자를 삭제하려고 하지만, task 테이블에서 해당 userId를 참조하는 레코드가 있다면 MySQL에서 외래키 제약 오류가 발생한다.
-> 따라서, userId를 참조하는 데이터들을 삭제하거나 업데이트해야 한다.
예를 들어, task 테이블에서 userId를 참조하는 모든 레코드를 먼저 삭제하거나 무결성을 유지할 수 있는 방법을 선택해야 한다.
그래서 사용자를 삭제하기 전에 taskService에서 관련 Task들을 먼저 삭제해주려고 했다.
//UserController
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUserById(@PathVariable Long userId){
taskService.deleteTasksByUserId(userId);
userService.deleteUserById(userId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT); //삭제 성공
}
//TaskService
public void deleteTasksByUserId(Long userId) {
List<Task> taskByUserId = taskRepository.findTaskByUser_Id(userId);
taskRepository.deleteAll(taskByUserId);
}
//TaskRepository
List<Task> findTaskByUser_Id(Long userId);
또 오류 대환장파티~
아까 위에서 말했던 것과 비슷한? 같은?오류같다.
List<Task> findTaskByUser_Id(Long userId);
이 부분이 문제 같은데.. 저 By뒤의 userID를 아무리 바꿔도 못찾는단다ㅠ

@ManyToOne
@JoinColumn(name = "user_id")
private User user;
위에서 말했듯이, 이렇게 엔티티에서 @Id 필드명을 user_id로 이름을 지정했을 경우 메서드를 아래처럼 작성해야함!!!
public List<Task> findTaskByUser_UserId(Long userId);
테스트 해보니 사용자 삭제를 전송하니 기존 일정 게시물들도 다 삭제가 되었다!
(근데 댓글은 상관 없나?....) -> 댓글 추가해보고 하려니 똑같은 오류 발생ㅎ
와중에 댓글도 똑같은 방식으로 구현하니 또 오류 발생 (또뭐가문젠데!!!!!!!)
2025-02-13T00:43:00.223+09:00 WARN 12600 --- [ScheduleProject_develop] [nio-8080-exec-8] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1451, SQLState: 23000
2025-02-13T00:43:00.223+09:00 ERROR 12600 --- [ScheduleProject_develop] [nio-8080-exec-8] o.h.engine.jdbc.spi.SqlExceptionHelper : Cannot delete or update a parent row: a foreign key constraint fails (`task`.`comment`, CONSTRAINT `FKfknte4fhjhet3l1802m1yqa50` FOREIGN KEY (`task_id`) REFERENCES `task` (`id`))
2025-02-13T00:43:00.229+09:00 ERROR 12600 --- [ScheduleProject_develop] [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [Cannot delete or update a parent row: a foreign key constraint fails (`task`.`comment`, CONSTRAINT `FKfknte4fhjhet3l1802m1yqa50` FOREIGN KEY (`task_id`) REFERENCES `task` (`id`))] [/* delete for com.example.scheduleproject_develop.task.Task */delete from task where id=?]; SQL [/* delete for com.example.scheduleproject_develop.task.Task */delete from task where id=?]; constraint [null]] with root cause
java.sql.SQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`task`.`comment`, CONSTRAINT `FKfknte4fhjhet3l1802m1yqa50` FOREIGN KEY (`task_id`) REFERENCES `task` (`id`))
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:109) ~[mysql-connector-j-9.1.0.jar:9.1.0]
이번에는요.. 이미 댓글이 있는 task를 지우려하니 안된답니다!ㅎㅎㅎㅎㅎㅎㅎ
아니그데 너무 어려워지는데?ㅠ 아니 userID로 Task들 찾아서 다 지우는데...
역시나 .. 나는 또 삽질을 한 것이다... 분명 배웠는데 까먹은 것!!!!!!!!!!
연관관계 매핑할 때,
cascade = CascadeType.ALL
이 속성을 적어주면 자동으로 연결되어있는 것들을 다 지워준다.
-> 근데 실무에서는 cascade나 ondelete를 안 쓴다고 한다.. (hard delete)
대신 soft-delete 사용해보자!
: 실제로 db에서는 지우지 않는데 지우는 척을 하는 거임!
@SQLDelete(sql = "UPDATE user SET deleted = true WHERE user_id = ?")
@SQLRestriction("deleted = false")
이렇게 엔티티마다 어노테이션을 붙여주고,
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
@Column(nullable = false)
private Boolean deleted = Boolean.FALSE;
}
일단 모든 엔티티에 적용할 거니까 BaseEntity에 deleted 컬럼을 넣어주면??

회원 탈퇴를 진행하면, deleted 플래그를 true로 바꾸어주고, 사용자에게는 탈퇴된 것처럼/ 게시물이 삭제된 것처럼 보여지게 한다.
😲 약간 갤러리나 SNS를 이용할 때에도 일단 휴지통에 넣어놓지만, 일정 기간(ex.30일) 내에는 복구가 가능하도록 해둔 게 이런 기능 덕분일까??
물론 지금은 작은 규모의 과제이고, 회원탈퇴와 동시에 영구삭제가 되어도 상관없지만 ㅎ
일단 이런 게 있구나라는 걸 알아두는 정도로만 생각해보겠다.
[참고]
https://0soo.tistory.com/133
https://resilient-923.tistory.com/417
https://yusang.tistory.com/103
이걸 하면서 드는 생각인데,,
회원 삭제를 하면 바로 로그아웃이 되도록 해야겠군!!!!!
또 이걸 하면서 드는 생각인데
회원 삭제=탈퇴니까 Auth로 가야하는 것 같기도!!!
=> 이 부분들은 다 반영하였다 ㅎㅎ
확실히 도메인이 늘어날 수록 고려해야할 것들도 많아지고.. 어려운 것 같다.
대신 이런 과정을 겪으며 어떤 기능을 개발할 때엔 어떤 것들을 고려해야하는지 더욱 알아가고 있는 것 같다!!!!
그리고 문법 공부하다가 하니까 넘나 재밌음🥹
하지만...
처음 코드를 짤 때부터 모든 고려사항들을 떠올리고, 활용할 줄 아는 사람이 되고싶다.
스프링은 처음 배우는 거니까 그런 거겠지????ㅜㅜ