본문 바로가기
TIL

Spring 일정 관리 앱 Develop 트러블 슈팅

by suyeoneee 2024. 12. 19.

이번 과제는 저번 주에 JDBC로 구현했던 일정관리 앱을 JSP로 구현하고, 로그인 필터, 암호화 등의 요구사항이 추가되었다.

튜터님께서 기술이 불편하면 새로운 기술이 나타난다고 하신 말씀이 생각난다.

실제로 JDBC을 이용하다가 JPA를 이용해보니 코드가 훨씬 간결해지고 편하게 짤 수 있었다.

하지만 지금도 계속 새로운 기술이 개발되고 있겠지..

 

과제하면서 제일 구현하기 어려웠던 요구사항은 4. 로그인(인증) 이다.  지금은 어느정도 이해가 되긴하지만 chain 이용에 대해서는 아직.. 더 공부해야할 것 같다.

API 명세

기능 Method URL request response 상태코드 설명
일정 생성 POST /todos [JSON]
∙title
∙contents
TodoResponseDto 200 CREATED 작성일, 수정일
👉🏻JPA Auditing
Session에 있는 유저 정보로 일정 작성
전체 일정 조회 GET /todos [Param]
∙modifiedAt
∙userId
∙page ∙ size
<List> TodoPageResponseDto 200 OK 일정 페이징 조회
: 일정의 수정일을 기준으로 DESC
특정 일정 조회 GET /todos/{todoId}   TodoPageResponseDto 200 OK 일정 ID로 조회
특정 일정 수정 PATCH /todos/{todoId} [JSON]
∙title
∙contents
TodoResponseDto 200 OK 일정 ID로 수정
특정 일정 삭제 DELETE /todos/{todoId}     204 No Content 일정 ID로 삭제
유저 생성 (회원가입) POST /users/signup [JSON]
∙email
∙userName
∙password
SingUpResponseDto 200 CREATED 작성일, 수정일
👉🏻JPA Auditing
전체 유저 조회 GET /users   <List> UserResponseDto 200 OK 회원 가입한 모든 유저 조회
Bcrypt 해시암호
특정 유저 조회 GET /users/{userId}   <List> UserResponseDto 200 OK 유저 ID로 조회
특정 유저 수정 PATCH /users/{userId} [JSON]
- email
- userName
UserResponseDto 200 OK 유저 Id로 수정
특정 유저 삭제 DELETE /users/{userId}     204 No Content 유저 Id로 삭제
로그인 POST /login [JSON]
- email
- userName
LoginResponseDto 200 OK 이메일과 비밀번호로 로그인
→ 성공시 Session에 저장
로그아웃 POST /logout     204 No Content  
댓글 작성 POST /todos/{todoId}/comment [Text]
∙Content
CommentresponseDto 200 CREATED 작성일, 수정일
👉🏻JPA Auditing
Session에 저장된 UserId
전체 댓글 조회 GET /todos/{todoId}/comment   <List> CommentresponseDto 200 OK 일정 Id에 등록된 댓글 전체 조회
내가 작성한 댓글 조회 GET /todos/mycomments   <List>
MyCommentresponseDto
200 OK 내가 작성한 모든 댓글 조회
내가 작성한 댓글 수정 PATCH /todos/mycomments/{commentId} [Text]
∙Content
<List> MyCommentresponseDto 200 OK 내가 작성한 댓글을 댓글Id로 조회하여 수정
내가 작성한 댓글 삭제 DELETE /todos/mycomments/{commentId}     204 No Content 내가 작성한 댓글을 댓글Id로 조회하여 삭제

 

 

ERD

일정 N : 유저 1 , 댓글 N : 일정1

 

 


로그인한 유저 정보를 세션에 저장 , Filter 인증

LoginFilter.java

@Slf4j
public class LoginFilter implements Filter {

    // 회원가입, 로그인 요청은 인증 처리에서 제외
    private static final String[] WHITE_LIST = {"/users/signup", "/login", "/logout"};

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // request를 쉽게 사용하기 위해 down casting
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();

        HttpServletResponse httpResponse = (HttpServletResponse) response;

        log.info("로그인 필터 로직 실행");

        // 로그인을 체크해야하는지 검사
        // WHITE_LIST에 포함된 경우 true -> !true = false

        if(!isWhiteList((requestURI))) { // WHITE_LIST에 포함되어 있지 않다면

            // 로그인 확인 (로그인 하면 session에 값이 저장되어 있다는 가정)
            // 세션이 존재하면 가져온다. 세션이 없으면 session = null
            HttpSession session = httpRequest.getSession(false);

            // 로그인하지 않은 사용자인 경우
            if (session == null || session.getAttribute("USER_ID") == null) {
                throw new RuntimeException("로그인 해주세요. ");
            }

        }
        chain.doFilter(request, response);

    }

    public boolean isWhiteList(String requestURI) {
        //WHITE_LIST로 만들어놨던 URL에 포함되어 있지 않다면 false 반환
        return PatternMatchUtils.simpleMatch(WHITE_LIST, requestURI);
    }
}

 

WebConfig.java

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public FilterRegistrationBean loginFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<>();
        // Filter 등록 (로그인 필터)
        filterRegistrationBean.setFilter(new LoginFilter());
        // Filter 순서 결정
        filterRegistrationBean.setOrder(1);
        // 전체 URL에 Filter 적용
        filterRegistrationBean.addUrlPatterns("/*");

        return filterRegistrationBean;
    }
}

 

여기서 Filter는 로그인 필터 하나만을 사용해서 우선순위 의미가 없지만.. 만약 필터를 추가한다면 우선 순위를 결정해 주면된다.

 

 

💥  trouble 1. chain.doFilter 사용

if (!isWhiteList(requestURI)) { // WHITE_LIST에 포함되어 있지 않다면

    // 로그인 확인 (로그인 하면 session에 값이 저장되어 있다는 가정)
    // 세션이 존재하면 가져온다. 세션이 없으면 session = null
    HttpSession session = httpRequest.getSession(false);

    // 로그인하지 않은 사용자인 경우
    if (session == null || session.getAttribute("USER_ID") == null) {
        throw new RuntimeException("로그인 해주세요.");
    }
}

log.info("로그인 성공1"); // doFilter 전 
chain.doFilter(request, response);
log.info("로그인 성공2"); // doFilter 후 루프를 돌고옴

* 로그아웃은 WHITE_LIST에 포함된 상황

 

WHITE_LIST에 속하는 /logout URL에 GET 요청을 했더니 로그인 성공 log가 찍혔다.

https://lh4.googleusercontent.com/proxy/NoVCXXumasb9oU6xLUl9BdYwl4Yyow__B0-cgbuCUNRjjwXprDXuk_9S7ezdjCVy5kzl6bWRWij3zG0OTZPKADoNFpEi-PnFeV1c

 

Filter Chain은 위와 같이 동작한다. 루프를 돈다고 생각하면 된다.

chain.doFilter(request, response) 전의 코드를 필터 우선 순위대로 실행한 후, 루프를 돌았다면 chain.doFilter(request, response) 후의 코드를 다시 루프 도는 것이다.

 

따라서, Filter chain을 사용할 때에는 구조와 순위를 잘 생각해서 사용할 것!

 

💥 trouble 2. 세션에 저장된 로그인 유저 인증

일정 CRUD, 댓글 CRUD, 유저 수정, 삭제 등을 하려면 Login Filter를 거쳐 로그인을 한 상태여야지 url 요청이 가능하게 설계하였다.

그리고 세션에 저장된 유저 정보로 일정이나 댓글을 생성할 수 있게 API를 구현하려고 했다. (param이나 body로 유저 id를 안 넣어도 됨)

 

동작 순서를 정리해보면,

회원가입 → 로그인 → 일정 생성 URL post 요청 → userId = 세션에 저장된 유저 식별자 → DB에 일정 저장

와 같이 동작한다.

 

그런데, url 요청이 들어올 때 로그인한 유저 정보를 어떻게 저장해야하는지 몰랐다.

찾아보니 세션 값을 가져올 수 있는 @SessionAttribute("Key") annotaion이 있다는 것을 알았다.

id 변수에 annotation을 적용하고, requestDto와 함께 Service에 파라미터로 전달해주었다.

@RestController
@RequestMapping("/todos")
@RequiredArgsConstructor
public class TodoController {

    private final TodoService todoService;
    private static final String USER_ID = "USER_ID";

    // 일정 생성
    @PostMapping
    public ResponseEntity<TodoResponseDto> save(@SessionAttribute(USER_ID) Long userId, @RequestBody TodoRequestDto requestDto) {

        TodoResponseDto todoResponseDto = todoService.save( //service 요청
                requestDto.getTitle(),
                requestDto.getContents(),
                userId
        );
        return new ResponseEntity<>(todoResponseDto, HttpStatus.CREATED);
    }
}
@Service
@RequiredArgsConstructor
public class TodoService {

    private final TodoRepository todoRepository;
    private final UserRepository userRepository; // Todo N : User 1 연관 관계
    private final CommentRepository commentRepository;

    // 일정 생성
    public TodoResponseDto save(String title, String contents, Long userId) {

        User findUser = userRepository.findByUserIdOrElseThrow(userId); //세션에 저장된 로그인 유저ID

        Todo todo = new Todo(title, contents);
        todo.setUser(findUser);

        Todo savedTodo = todoRepository.save(todo); // Repository에 저장

        return new TodoResponseDto(savedTodo);
    }
}

 

결과 )

일정 등록할 때 유저 정보를 body에 넣지 않아도 세션에 저장된 유저 ID로 저장되는 걸 알 수 있다.

일정 등록뿐만 아니라 유저 수정, 작성 댓글 조회 등도 이와 같이 @SessionAttribute("Key") 을 활용하면 된다.

댓글