최근 제출을 목적으로 풀스택으로 웹 시스템을 개발했다.
그렇게 제출 2시간 전, 마지막 테스트를 하고 있던 와중, 어마어마한(?) 버그를 발견하게 되었다......
바로 체크박스 check / uncheck가 ajax로 유지되지 않는다는 것...!
원래는 잘 동작했던 기능이었기에, 도대체 뭐가 문제지 하고 식은땀이 주르르르르르르륵 났던 것 같다...
결과적으론 버그를 찾아서 해결했지만,,정말 식겁한 순간이었던 것 같다...ㅠㅠㅠㅠㅠ
다시는 이런 문제를 일으키지 않기 위해 블로그에 가볍게 정리해보려고 한다.
1. 배경
구현하고자한 내용을 간단히 설명하자면, 위 사진처럼 체크 박스 이벤트에 의해 DB에 체크여부를 저장하는 로직을 구현해야 했었다.
체크를 클릭하면, DB에 is_checked 칼럼이 true, uncheck 하면 false로 등록되는 그런 간단한 로직이었다.
2. 문제 발생
이상하게도.. uncheck 상태에서 check상태로 변경하면(체크박스를 체크하면) DB에 잘 저장되었다.
스프링부트 로그에서 봤을 때도 DB의 is_checked 상태를 변경하기 위한 update 쿼리가 정상적으로 출력되었다.
그리고 새로고침해도 체크상태는 잘 유지되었다.
근데 check 상태에서 uncheck 상태로 변경하고자하면(체크박스를 해제하면)..동작하지 않았다.
uncheck 자체는 화면상 보였으나 실제로 uncheck 로직이 수행되지도 않았고, 새로고침 시 여전히 check 상태로 유지되었다.
더 자세히 살펴보니 update쿼리도 나가지 않았던 걸 확인할 수 있었다.
3. 분석
위 현상을 토대로 나름의 분석을 진행했다.
check는 정상 동작. 그러나 uncheck는 비정상. 실제로 JPA를 기반한 update 쿼리도 날라가지 않음.
백엔드에서 체크로직을 살펴보자.
스프링 @Transaction 어노테이션 기반 하에 JPA를 이용해 백엔드를 구성.
그러므로 update 쿼리가 나간다는 것은, JPA 영속성 컨텍스트 안에서 객체 값이 변경되었다는 의미.
반대로 말하면, update 쿼리가 안나간다는 것은 객체 값의 변경이 없었다는 의미!!!
결론적으로, 체크 박스의 체크 상태에 대한 변화가 없었다고 인식했기 때문이다!
4-1. 첫번째 가설
컨트롤러 단에서 받는 RequestBody에서 넘어오는 타입에 문제가 있을지?
당시 RequestBody로 전달하는 dto는 아래와 같았다.
{
"name": "aaaa",
"isChecked" : "true" // 문자열로 보내기 때문에 boolean으로 인식 못하는게 아닐까??
}
실제로 프론트 코드를 보면, isChecked 값을 dto에 담아 보낼 때 true,false 값 자체를 String으로 보냈기 때문에 타입에러에서 발생하는 문제라고 생각이 들었다!
fixExtensionItemButtons.forEach(checkbox => {
checkbox.addEventListener("click", event => {
const target = event.currentTarget;
target.classList.toggle("active");
const extensionName = target.name;
// 여기 아래 코드 보면 String으로 전달!
const isChecked = target.classList.contains("active").toString();
updateFixExtensionChecked(extensionName, isChecked);
});
});
그러나, requestBody에서 json 형태 안에 String 값으로 오는 true,false는 Jackson 라이브러리가 알아서 잘 딱 깔끔하게 boolean 값으로 치환해준다고 한다...이게 대문자로 오든 소문자로 오든 상관없다고,,,
4-2. 두번째 가설
requestBody로 true,false를 문자열로 보내는 타입 문제가 아니라면,더 앞단에서 체크할 필요가 있었다.
발생했던 현상을 요약해보면, check는 정상동작, uncheck는 비정상동작이었다.
그렇다는 말은..check 로직은 정상적으로 동작한다는 것이고, 로직의 흐름 상 중간에 데이터가 끊기거나 하는 일은 없다는 것!
데이터 흐름이 끊기지 않는데 uncheck 기능은 비정상이라는 뜻은, uncheck할 때도 isChecked 값이 항상 true로 담기는 것이 아닐까?
프론트 코드를 살펴 볼 필요가 있었다.
타임리프 문법을 사용해 fix.isChecked가 'true' 면 클래스를 button active, 아니면 button 타입으로 두도록 조건문을 걸어놨다.
fix는 객체로, DB에 현재 저장된 상태를 가져온다.
target.classList.toggle("active") 라는 코드를 잘봐야한다.
만약 class에 active가 포함되어 있으면 class명에서 active를 제거하고, active가 포함되어 있지 않으면, class명에 active를 추가하라는 뜻이다.
즉, 요약하자면 체크박스를 클릭하면
현재 체크박스의 class = button이다. (아직 클릭 반영 전, 이전 DB에 저장되어 있던 체크박스 상태 가져옴)
이후 active가 클래스명에 없으므로 class = button active 으로 확정 (target.classList.toggle("active") )
active가 달려있으므로 const ischecked = true.
RequestDto에 isChecked 값은 true로 날라간다.
문제는 여기서 부터다.
문자열로 되어있다..조건문에서...
그러나 백엔드 엔티티에서 isChecked 타입은 Boolean이다..
당연히 fix.isChecked = Boolean 타입의 true,false로 나갈테니, 뭐로 나가든 문자열의 "true"는 아니다!!!
그렇기에 항상 class가 button으로만 나갔던거고, check는 정상동작하고, uncheck는 비정상동작했던 원인이 여기있었다.
5. 해결
사실 앞서 말했다시피..제출 2시간전에 발견했기 때문에 이렇게 자세히 복기할 순 없었다.
앞서 이런 결론을 도출한 과정도 과제 제출 후 복기해보는 과정에서 결론지은 내용이다.
실제로 해결한 방법은, 백엔드에서 fix 엔티티의 isChecked 칼럼을 Boolean에서 String으로 변경한 것이다.
당시 디버깅을 할 때 앞서 확인한 내용이 확실하게 와닿진 않았지만, 느낌적인 느낌(?)이 있었다.
- 뭔가 isChecked에서 문제가 있는 것 같다..
- boolean 타입과 String 타입 간 호환 문제가 있지 않나?
위와 같은 생각을 했었고 그렇기에 가설도 실제 원인과 유사하게 유추해나갈 수 있었다.
결론적으로 엔티티 내 isChecked 칼럼의 타입을 String으로 바꿨고, 다행히 정상적으로 동작하는 것을 확인할 수 있었다.
6. 결론
사실 따지고 보면 그렇게 어려운 문제도 아니었다. 순전히 정말 단순한 타입 에러였고, 어찌보면 휴먼 에러에 더욱 가까웠던 버그였다.
그럼에도 불구하고 글을 쓰는 이유는, 한정된 시간 안에 정확하게 문제 원인을 파악하여 해결하진 못해도, 주어진 상황을 파악하고 분석한 후 문제가 발생한 것 같은 위치를 유추하여 해결해나가는 과정을 기억해두고 싶어서 작성하게 되었다.
실제로 현업에서도 정말 현실과 타협할 일도 많을테고, 항상 최선의 선택을 하지 못할 경우가 발생할텐데, 그런 상황을 대비해서라도 나의 경험과 생각의 흐름을 정리할 수 있는 기회였다.
결론적으로 타임리프로 Boolean 타입의 true를 보내면 String값으로 알아서 알아들을까요? (Boolean true == "true"가 가능할까요?)라는 질문의 답은 아니다! 이다.
'Development > TrobleShooting' 카테고리의 다른 글
[Gasip Project] JPA / could not initialize proxy - no Session. (2) | 2023.11.07 |
---|---|
[Gasip Project] 왜 Column Name이 중복이라고 뜨죠? _ DuplicateMappingException (2) | 2023.11.05 |
[Git] Push가 안되는 이유는? git remote : Permission to ---The requested URL returned error: 403 (6) | 2023.10.16 |
@Autowired에 빨간줄이 그어진 이유 (4) | 2023.10.10 |
인텔리제이 read-only가 안풀려서 도저히 코딩을 못하겠다. (0) | 2023.09.07 |