독학을 시작한지 벌써 7주가 지났다.
처음에 시작했을때 많은 시행착오를 거쳐, 이것저것 많이 시도해봤다가 폭풍오열과 함께 다시 기초를 다지고.. 이런식으로 이랬다 저랬다 했던것 같은데
이제 거의 완벽하게 나만의 공부방법이 확립된것같다.
그리고 드디어! 나도 학원을 다닌다. 처음에 고민을 굉장히 많이했고, 이걸로 인해서 나한테 어떤 득이 있고 실이 있을지 생각을 많이 했던것 같다.
결국에 정말 모든 상황이 잘 따라준다면, 나에게 좋은 동료와 좋은 강의자료들, 그리고 프로젝트들로 얻어갈 포트폴리오와 훈련지원금이라는 득이 있고, 실은 내가 과연 몸관리를 잘 해가며 컨트롤 할 수 있을지, 잘 따라갈지 정도였다.
최악의 상황이라면 득은 수업 스케줄로 인한 생활리듬이 올바르게 잡힌다는 것과, 돈을 받아가면서 자습 혹은 독학을 할 수 있다는것과, 실이라면 독학으로 혼자서 자유롭게 할때보다 생각치 못했던 상황들로 인해 진도가 더 느려진다거나 할 수 있다는..?
하지만 중간으로 생각해봐도 내 기준 득이 더 많을거같다는 생각에 시작을 하게 되었다.
그래서 그냥 시작전까지 쉬어버릴까.. 했는데 내 기준 하루라도 뭘 하지 않아서 조금이라도 배운걸 잊어버리는게 겁이났다. 그렇게 하루정도만 날려버렸고.. 회고록또한 하루가 밀려버렸다 ㅋ
저번 한주동안 진행했던 것들은
여기서 가장 보람찼던 건 댓글 수정 기능을 다른 코드를 단순 복붙으로 구현한게 아니라 내 나름대로 고민하고 검색해서 최소한의 변경점을 가지고 기능을 구현했다는것이다.
이중에서 나름 내 기준 몰랐던것을 어떻게든 찾아서 새로 추가한 것들은 이런것들이다.
저번주 회고록에 썼듯이, 비밀번호를 BCrypt 방식으로 인코딩을 한 후에 가입시에 정보를 입력해주면, 자꾸만 스프링 시큐리티에서 암호화된 비밀번호를 입력받지 않았다고 뜨더라.
왜 그럴까..? 하고 생각을 해보았다.
결론은
왜 너는 회원가입시에 입력하는 비밀번호만 암호화를 시키고 너가 입력한 비밀번호를 암호화 시켜서 전달하는 구조를 만들지 않았니 ?
였다 ........
방법은 간단했다 스프링 시큐리티 설정 클래스에
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userSecurityService).passwordEncoder(passwordEncoder());
}
해당 메소드를 추가해주면 된다. UserDetailsService 같은 경우는 로그인을 진행할때 Spring Security에서 사용자의 정보를 담는 인터페이스인데, 이때 해당 인터페이스에는 비밀번호를 암호화 해서 검증하는 부분이 없기 때문에 내가 직접 추가를 해줘야된다는 것이다.
Spring Security) UserDetailsService에서 비밀번호는 어디에서 검사하는 걸까?
좋은 글이 있어서 첨부해보았다.
강의를 보고 생각을 했던게 , 왜 강사님은 기존 Dto 하나만을 가지고 전달 하는게 아니라 왜 Request, Response dto 를 따로 만들어서 사용하실까 ?
였는데 얼마 지나지 않아 구조의 문제를 알게 되었다.
첫번째 - 나는 단순 게시글을 올리거나 댓글을 등록할때 해당 게시글이나 댓글의 필수 내용만 전달하면 되는데 쓸데없이 같이 넘어가야되는 정보가 너무 많다.
두번째 - 단순히 뭘 등록하거나 수정 할때는 해당 내용만 넘어가면 되지만, 큰 스케일의 dto를 통으로 넘겨버리면 공개되면 안되는 정보가 유출될 위험이 있다.
그래서 게시글 같은 경우에는 내가 직접 내용을 작성해야 하는 제목, 내용, 해시태그 만 따로 전달하는 Request Dto 를 만들고, 댓글은 내용만 전달하는 Request Dto 를 만들어 필수 내용만 전달하고 전달받게 구조를 변경했다.
해당 부분이 가장 보람찬 부분이다.
내가 아무리 검색해봐도 댓글 수정을 타임리프로 동기식으로 구현하신 분들이 거의 없었다. (아예 없었던것 같았다.)
지금 내 상황은 처음부터 crud 를 동기식으로 동작하게 처리했기 때문에 저 상황에서 Ajax 와 Jquery 를 활용해 댓글 수정만 비동기식으로 동작하게 변경하기엔 내가 다시 바꿔야할 구조들이 너무 많았고, 다시 이거 하나만을 위해 공부해야 될 부분이 너무 많았다.
그래서 부트스트랩의 콜랩스를 활용해서 댓글수정 버튼을 누르면 입력창을 펼치거나 닫는 방식으로 수정 댓글을 POST 하는 방법을 생각했는데,
[스프링] ajax 활용 안하고 댓글 수정 기능 구현하기
내가 수정이 가능한 모든 댓글에 입력칸이 등장했다...........
문제가 뭘까 하고 온갖 방법을 시도해보았었다. 하지만 결국엔 해결을 못하고 있다가
th:id 라는 문법이 있다는걸 발견했다.
그리고 가장 큰 문제는 내가 타임리프 문법속 each 문에 다른것들과 똑같이 콜랩스를 적용시켜도 콜랩스는 그 각각 칸으로 적용되는게 아닌 딱 그 태그 하나만 실행이 되는것 같았다. 결론적으론 프론트 문제였다.
온갖 검색을 동원한 결과..<button th:attr="data-bs-target=|#c${articleComment.id}"> ... <div class="collapse" th:id="c + ${articleComment.id}">
이런 식으로 각 댓글 div 마다 댓글id 를 고유값으로 입력하게 하여서 one to many 에서 one to one 으로 변경을 해주었다.
이런 방법으로 수정을 했다. 이래서 구글링이 중요하다. 스텍오버플로우에 나같은 문제를 겪었던 사람들이 꽤 있었다 ㅎ..
검색기능 같은 경우엔
@Transactional(readOnly = true)
public Page<ArticleDto> searchArticles(SearchType searchType, String searchKeyword, Pageable pageable) {
if (searchKeyword == null || searchKeyword.isBlank()) {
return articleRepository.findAll(pageable).map(ArticleDto::from);
}
return switch (searchType) {
case TITLE -> articleRepository.findByTitleContaining(searchKeyword, pageable).map(ArticleDto::from);
case CONTENT -> articleRepository.findByContentContaining(searchKeyword, pageable).map(ArticleDto::from);
case ID -> articleRepository.findByUserAccount_UserIdContaining(searchKeyword, pageable).map(ArticleDto::from);
case NICKNAME -> articleRepository.findByUserAccount_NicknameContaining(searchKeyword, pageable).map(ArticleDto::from);
case HASHTAG -> articleRepository.findByHashtag(searchKeyword, pageable).map(ArticleDto::from);
};
}
이렇게 기존에 게시글 리스트를 반환할때는 검색어와 필터가 없을때 모든 결과를 리턴해주는 방식으로 로직을 설계했고, 검색을 할때는 Enum 으로 만들어둔 검색타입별로 검색어를 포함한 결과물을 반환하게 했다.
Page<Article> findByTitleContaining(String title, Pageable pageable);
Page<Article> findByContentContaining(String content, Pageable pageable);
Page<Article> findByUserAccount_UserIdContaining(String userId, Pageable pageable);
Page<Article> findByUserAccount_NicknameContaining(String nickname, Pageable pageable);
Page<Article> findByHashtag(String hashtag, Pageable pageable);
해당 메소드들은 JpaRepository 에 선언해두었다.
그래서 단순히 게시판 템플릿에 검색과 페이징 기능을 추가만 하면 됐다.
<select name="searchType" class="form-select" id="exampleSelect1">
<option value="TITLE" >제목</option>
<option value="CONTENT" >본문</option>
<option value="ID">ID</option>
<option value="NICKNAME">닉네임</option>
<option value="HASHTAG">해시태그</option>
</select>
...
<input type="text" placeholder="Search..." class="form-control" id="searchValue" name="searchValue">
이렇게 검색 폼에 셀렉트 태그에 각 옵션마다 밸류를 타입 이름으로 설정하고, 각각 태그별로 필드를 SearchType 과 SearchKeyword 로 타임리프 문법으로 설정해주면 PathVariable 에 해당 타입과 검색어가 변수로 들어가게 된다.
페이징 기능은
<attr sel="#pagination">
<attr sel="li[0]/a"
th:text="'<<'"
th:href="@{/articles(page=${articles.number - 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
th:class="'page-link' + (${articles.number} <= 0 ? ' disabled' : '')"
/>
<attr sel="li[1]" th:class="page-item" th:each="pageNumber : ${paginationBarNumbers}">
<attr sel="a"
th:text="${pageNumber + 1}"
th:href="@{/articles(page=${pageNumber}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
th:class="'page-link' + (${pageNumber} == ${articles.number} ? ' disabled' : '')"
/>
</attr>
<attr sel="li[2]/a"
th:text="'>>'"
th:href="@{/articles(page=${articles.number + 1}, searchType=${param.searchType}, searchValue=${param.searchValue})}"
th:class="'page-link' + (${articles.number} >= ${articles.totalPages - 1} ? ' disabled' : '')"
/>
</attr>
이렇게 검색어가 있을땐 검색어와 검색 타입을 반영하고, 없을땐 전체 게시글을 페이지별로 출력하도록 Pageable 인터페이스를 구현해 기능을 구현했다.
개인적으로 댓글수정기능을 구현했을때는 생각보다 유익한 정보를 얻기가 힘들었는데 어떻게든간에 해외 사이트까지 요리조리 파고 들어가 해결 방법을 찾아내서 구현했던게 너무 좋았고, 국내 검색 결과 뿐만 아니라 해외 에서도 정보를 찾으면 어쩌면 더 유용한 정보를 얻을 수 있겠다는 생각을 갖게 되었다.
새로운 기능을 활용하고 추가하는데 필요한 논리적사고(?)능력이 늘었다. 어디에 뭘 어떻게 넣어야할지 전보다 더 분별력이 좋아진 느낌이라고 해야할까 ?
개발이 자연스럽게 내 일상에 들어왔다. 오락 대신 코딩. 오래 공부할 수 있을것같다 ㅎㅎ.
이제 드디어 국비과정이 시작됐고, 정말 운이 좋게 내가 평소에 자습하던 스케줄과 비슷하게 진행이 된다.
7달동안 급하게 배울 생각은 전혀 없기때문에 , 내가 가져갈수있는 부분은 최대한 많이 가져가려고 맘을 먹었다.
소소한 목표라면 수료를 할때쯤에는 내 블로그에 글이 몇백개가 있고 깃허브 잔디는 빼곡히 채워져있는 모습을 보고싶다.
습관이라는게 굉장히 중요하기때문에 ㅎㅎ 열심히 살아야지 하고 매주 다짐 또 다짐