CRUD 실습

Spring 좋아요 기능을 구현하며 느낀 점 [탐구/기록]

rexondex 2024. 9. 30. 18:40
[오류] 현재 좋아요 구현 결과

'좋아요' 기능이란 게시물의 '하트 버튼'을 클릭하면 숫자가 카운트로 +1 되며 게시물 속성인 'LIKE_COUNT' 숫자를 누적시키는 기능입니다.

유저당 게시물 1개에 1개의 좋아요를 줄 수 있으며 이미 누른 게시물은 채워진 하트, 누르지 않은 게시물은 채워지지 않은 하트로 보이게 하는게 조건입니다.​​

현재 데이터베이스의 게시물 행

 

행( 레코드 )이 2개 저장되어 있기 때문에 웹에서 보이는 게시물도 2개입니다.

'POST_ID = 82' 를 가진 게시물의 경우 하트를 누르면 LIKE_COUNT가 1 증가하며 테이블에 반영됩니다.

한번 더 누르면 LIKE_COUN

T가 1 감소하며 테이블에 반영됩니다.​​

Caused by: org.hibernate.NonUniqueResultException:
 Query did not return a unique result: 2 results were returned
 

'POST_ID = 81' 를 가진

게시물의 경우 하트 버튼을 누르면 하나의 행을 반환받아야 하는데

2개 이상의 행을 반환받기에 위와 같이 'NonUniqueResultException' 에러를 뱉었습니다.

먼저 생성된 게시물은 원래 의도대로 잘 동작했습니다​

 

처음에는 하나의 html 문서에서 한 게시물 요소들의 뭉치를 복사하듯 찍어내서 여러개의 게시물을 표현합니다.

그리고 타임리프 반복문을 통해 복사하듯 만들어진 여러개의 게시물에서 각 게시글의 식별값(ID)을 유니크하게 부여한 다음, js 모듈을 통해 클릭 이벤트를 실행하도록 작성했지만

게시물을 명확히 구분할 수 없기 때문인지 이 방법으로 구현하지 못했습니다.

테이블에 행 삽입을 통해 몇개의 게시물을 더 생성해보고 같은 문제가 여러번 반복된다면 복수값을 반환하는 현재 오류에 대한 힌트를 얻을 수 있을 것입니다

게시물 post-list.html 를 조작하는 post-list.js 는 이렇게 작성했습니다.

document.addEventListener('DOMContentLoaded', function() {
    const favoriteIcons = document.querySelectorAll('.favorite-icon');
    // 모든 좋아요 아이콘 선택

    favoriteIcons.forEach(favoriteIcon => {
        const postFooter = favoriteIcon.closest('.fan-post-card__footer');
        const postId = postFooter.getAttribute('data-post-id');
        const memberId = postFooter.getAttribute('data-member-id');
        const csrfTokenElement = postFooter.querySelector('.csrfToken');
        // 각 카드의 CSRF 토큰 요소 가져오기

        if (csrfTokenElement) {
            // 요소가 존재하는지 확인
            const csrfToken = csrfTokenElement.value;

            favoriteIcon.addEventListener('click', function(event) {
                event.preventDefault(); // 기본 폼 제출 동작 방지
                console.log('Favorite icon clicked'); // 디버깅용 로그

                fetch(`/likes/${postId}`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': csrfToken // CSRF 토큰 추가
                    },
                    body: JSON.stringify({ memberId: memberId })
                })
                .then(response => {
                    if (!response.ok) {
                        throw new Error('Network response was not ok ' + response.statusText);
                    }
                    return response.json();
                })
                .then(data => {
                    // 서버에서 반환된 데이터 형식을 확인하고, likeCount를 올바르게 업데이트
                    if (data && data.likeCount !== undefined) {
                        const likeCountElement = postFooter.querySelector('.like-count');
                        likeCountElement.textContent = data.likeCount;
                        console.log('Like count updated:', data.likeCount); // 디버깅용 로그
                    } else {
                        console.error('Invalid data format:', data);
                    }
                })
                .catch(error => console.error('Error:', error));
            });
        } else {
            console.error('CSRF token element not found');
        }
    });
});

 

그리고 post-list.html 의 게시물을 표현하는 요소들을 감싸고, 이것을 반복 생성하는 코드뭉치는 이렇게 작성했습니다.

<div th:each="postdto : ${postdtos}" class="fan-post-card-wrapper">
  <div class="fan-post-card">
    <div class="fan-post-card__header">
      <div th:text="${postdto.postTitle}" style="font-weight: bold;">제목입니다</div>
      <div style="font-size:13px;">
        <span>p-id,m-name,m-id:</span>
        <span th:text="${postdto.postId}">p-id</span>
        <span th:text="${postdto.memberName}">m-name</span>
        <span th:text="${postdto.memberId}">m-id</span>
      </div>
    </div>
    <div th:text="${postdto.postContent}" class="fan-post-card__link">
      <p>타임리프내용</p>
    </div>
    <div class="fan-post-card__topic">
      <span class="topic__tag">#<span th:text="${postdto.tagNames}">타임리프태그들</span></span>
    </div>
    <div class="fan-post-card__highlight"></div>
    <div class="fan-post-card__footer" th:data-post-id="${postdto.postId}" th:data-member-id="${postdto.memberId}">
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" class="csrfToken" />
      <i class="material-icons favorite-icon" style="color: red; font-size: 24px;">favorite</i>
      <span class="like-count" th:text="${postdto.likeCount}" style="padding-left: 5px;">0</span>
    </div>
  </div>
</div>

 

좋아요를 눌렀을 때 트랜잭션을 수행하긴 하나, 여러 게시물이 존재할 경우 오라클이 복수값을 반환하는 오류를 뱉는 것이 관건이었습니다.

이 작업을 수행하면서 진전이 없는데 너무 오래 잡고 있는 것 같다는 생각이 들었습니다.​

하트를 클릭해서 +1 했을 시 채워진 하트, 하트를 재클릭해서 -1 했을 시 빈 하트로 표현하는 기능까지 구현하기 위해

컨트롤러에서 'like() / unlike()' 메서드를 구성한 다음 js 모듈에서 아래와 같이 표현하여 시간을 적지않게 쏟았음에도 오류 해결에 진전이 없었습니다.

// 좋아요, 좋아요 취소 URL
const isLiked = favoriteIcon.getAttribute('data-liked') === 'true';
const url = isLiked ? `/likes/unlike/${postId}` : `/likes/like/${postId}`;
 

'좋아요' 기능은 게시물에서 중요한 기능이지만 아직 완성하지 못한 다른 기능들도 많았기 때문에 여기에 과도하게 많은 시간을 쏟고 있는 것 같아서

js 이벤트가 작동하는 것을 막아두고 '회원정보/수정' 기능과 '글쓰기/수정/삭제' 기능을 먼저 완성시켜야겠다는 생각이 들었습니다.

'좋아요' 기능을 수행하는 LikeControlller와 LikeService 는 게시물 관련 클래스들과는 분리하여 생성해두었기 때문에

진행 도중 해결책이 떠오르면 다시 돌아와 Like 에 관련된 클래스들을 완성하는 방법이 좋을 것 같습니다.


:: 반복되는 에러를 해결하려 시간을 쏟은 결과 느낀점 ::

javascript 모듈에서 분기된 함수가 있고 controller 레이어와 service 레이어까지 메서드와 함수들이 분기된 데이터를 처리하도록 로직을 설정하는 경우

어느 한쪽에 오류가 생기면 정확히 어느 부분에서 오류가 생겼는지 캐치하기 난해해지며

한번 분기되어 로직을 타기 시작한 데이터를 새로운 구조가 필요하거나, 로직을 수정해야 하는 경우에 매우 복잡해졌습니다.

하나의 독립적인 기능에 대해서는 트랜잭션을 수행하고 클라이언트가 반환/응답을 받을 때 까지 제 3자가 보았을 때도 명확하고 직관적으로 구조가 설계되어야 한다는 것을 다시 한번 느꼈습니다.

처음에는 작았던 설계 결함을 대수롭지 않게 여기고 그냥 진행한 결과 데이터 흐름의 분기점과 클래스의 책임을 명확히 구분하는게 힘들어졌고

몇개의 결함만 수정했다면 쉽게 수행할 수 있었을 작업을 매우 복잡하게 풀어나가야 했습니다.

데이터의 흐름을 1차적으로 먼저 생각해보고 이러한 흐름을 벗어나지 않게 클래스와 로직을 구성하는 것이 훨씬 유리하다는 점을 깨달았습니다.