해당 프로젝트 코드는 다음 Github 링크에서 확인 가능합니다.
이번 프로젝트를 진행하면서 백엔드보다 오히려 데이터를 응답받는 프론트엔드 단에서 예외가 많이 발생하여 생각보다 개발 시간이 오래 걸렸습니다. 이를 통해 백엔드 개발자라고 백엔드만 알고 있으면 안된다는 생각을 하게 되었습니다.
다음은 개발 과정에서 발생한 예외들과 해결 방법 입니다.
Ambiguous handler methods mapped for HTTP path
날짜 또는 id를 기준으로 검색을 할 때 데이터가 제대로 조회되지 못하는 예외가 발생했습니다.
로그를 확인해보면 다음과 같은 예외가 발생함을 알 수 있었습니다.
java.lang.IllegalStateException: Ambiguous handler methods mapped for '/api/schedule/2024-10-10'
java.lang.IllegalStateException: Ambiguous handler methods mapped for '/api/schedule/7'
해당 예외는 Controller에서 메서드가 동일한 URL 패턴을 가질 때 발생합니다. 하기의 코드는 Controller에서 날짜와 ID 별로 데이터를 조회할 때 실행되는 메서드 입니다.
@Controller
public class ScheduleController {
@GetMapping("/api/schedule/{date}")
@ResponseBody
public List<ScheduleResponseDto> getDateSchedule(@PathVariable String date) {
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.getDateSchedule(date);
}
@GetMapping("/api/schedule/{id}")
@ResponseBody
public ScheduleResponseDto getIdSchedule(@PathVariable long id) {
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.getIdSchedule(id);
}
}
두 메서드 모두 /api/schedule/{...} 패턴을 사용하고 있습니다. getDateSchedule 메서드는 {date}라는 문자열 경로 변수를 사용하고, getIdSchedule 메서드는 {id}라는 long 타입의 경로 변수를 사용합니다. 하지만 Spring은 {...} 부분이 숫자일 수도 있고 문자열일 수도 있기 때문에 어떤 메서드를 호출해야 할지 구분할 수 없습니다.
이를 해결하기 위해 다음의 2가지 방법을 생각해보았습니다.
1. id 값을 숫자 데이터 타입으로 변경
프론트엔드에서 백엔드로 /api/schedule/{...} 를 요청할 때 모두 String 타입의 데이터를 PathVariable 방식으로 전달하고 있습니다.
그렇다면 Front side에서 id 값을 백엔드로 전달할 때 숫자 데이터 타입으로 보내면 Spring에서 이를 구분지어서 메서드를 호출할 수 있지 않을까 생각했습니다.
# 변경 전
// 날짜를 기준으로 조회합니다.
function getDateSchedule() {
// 1. 기존 메모 내용을 지웁니다.
$('#cards-box').empty();
// 2. 선택한 날짜 정보를 가져옵니다.
let date = userDate; // String 타입
// 3. 메모 목록을 불러와서 HTML로 붙입니다.
$.ajax({
type: 'GET',
url: `/api/schedule/${date}`,
success: function (response) {
for (let i = 0; i < response.length; i++) {
let message = response[i];
let id = message['id'];
let user = message['user'];
let content = message['content'];
let date = message['date'];
let updateDate = message['updateDate'];
let createDate = message['createDate'];
addHTML(id, user, content, date, updateDate, createDate);
}
}, error: function (xhr, status, error) {
console.error('AJAX 요청 실패:', status, error);
}
})
}
// ID를 기준으로 조회합니다.
function getIdSchedule() {
// 1. 기존 메모 내용을 지웁니다.
$('#cards-box').empty();
// 2. id 정보를 가져옵니다.
let id = $('#id-check').val(); // String 타입
// 3. 메모 목록을 불러와서 HTML로 붙입니다.
$.ajax({
type: 'GET',
url: `/api/schedule/${id}`,
success: function (response) {
let id = response.id;
let user = response.user;
let content = response.content;
let date = response.date;
let updateDate = response.updateDate;
let createDate = response.createDate;
addHTML(id, user, content, date, updateDate, createDate);
}, error: function (xhr, status, error) {
console.error('AJAX 요청 실패:', status, error);
}
})
}
# 변경 후
// ID를 기준으로 조회합니다.
function getIdSchedule() {
// 1. 기존 메모 내용을 지웁니다.
$('#cards-box').empty();
// 2. 선택한 날짜 정보를 가져옵니다.
let id = parseInt($('#id-check').val(), 10); // number 타입
// 3. 메모 목록을 불러와서 HTML로 붙입니다.
$.ajax({
type: 'GET',
url: `/api/schedule/${id}`,
success: function (response) {
let id = response.id;
let user = response.user;
let content = response.content;
let date = response.date;
let updateDate = response.updateDate;
let createDate = response.createDate;
addHTML(id, user, content, date, updateDate, createDate);
}, error: function (xhr, status, error) {
console.error('AJAX 요청 실패:', status, error);
}
})
}
상기와 같이 코드를 수정하고 API를 호출함에도 불구하고 여전히 동일한 에러가 발생하였습니다. 이를 통해 Spring은 PathVariable로 들어오는 값들은 모두 문자열로 인식하는 것을 알게 되었습니다.
2. API 변경
결국 요청하는 API를 구분짓기 위해서 다음과 같이 Controller에서 경로를 변경하였습니다.
@Controller
public class ScheduleController {
@GetMapping("/api/schedule/date/{date}")
@ResponseBody
public List<ScheduleResponseDto> getDateSchedule(@PathVariable String date) {
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.getDateSchedule(date);
}
@GetMapping("/api/schedule/id/{id}")
@ResponseBody
public ScheduleResponseDto getIdSchedule(@PathVariable long id) {
ScheduleService scheduleService = new ScheduleService(jdbcTemplate);
return scheduleService.getIdSchedule(id);
}
}
해당 경로를 변경하고 나니 정상적으로 애플리케이션이 응답하는 것을 확인했습니다.
type="button"
처음 설계 과정에서 헤더에 있는 '일정 조회'를 선택하면 전체 데이터가 조회되고, 날짜 또는 id 값을 검색란에 기입하면 해당하는 데이터만 조회되도록 설계했습니다.
문제는 날짜 또는 id 값을 넣어도 전체 데이터만 조회된다는 것이었습니다. 다음은 해당 코드 입니다.
<h2>검색</h2>
<form action="" class="mb-3">
<label>
<input id="calendar" placeholder="날짜를 입력해주세요"/>
<span class="validity"></span>
</label>
<button onclick="getDateSchedule()">검색</button>
</form>
<form action="" class="mb-3">
<label>
<input id="id-check" placeholder="id를 입력해주세요"/>
</label>
<button onclick="getIdSchedule()">검색</button>
</form>
</div>
$(document).ready(function () {
// HTML 문서를 로드할 때마다 실행합니다.
getSchedule();
})
// 전체 스케줄을 조회합니다.
function getSchedule() {
// 1. 기존 스케줄 내용을 지웁니다.
$('#cards-box').empty();
// 2. 스케줄 목록을 불러와서 HTML로 붙입니다.
$.ajax({
type: 'GET',
url: '/api/schedule',
success: function (response) {
for (let i = 0; i < response.length; i++) {
let message = response[i];
let id = message['id'];
let user = message['user'];
let content = message['content'];
let date = message['date'];
let updateDate = message['updateDate'];
let createDate = message['createDate'];
addHTML(id, user, content, date, updateDate, createDate);
}
}
})
}
// 날짜를 기준으로 조회합니다.
function getDateSchedule() {
// 1. 기존 스케줄 내용을 지웁니다.
$('#cards-box').empty();
// 2. 선택한 날짜 정보를 가져옵니다.
let date = userDate;
// 3. 스케줄 목록을 불러와서 HTML로 붙입니다.
$.ajax({
type: 'GET',
url: `/api/schedule/date/${date}`,
success: function (response) {
for (let i = 0; i < response.length; i++) {
let message = response[i];
let id = message['id'];
let user = message['user'];
let content = message['content'];
let date = message['date'];
let updateDate = message['updateDate'];
let createDate = message['createDate'];
addHTML(id, user, content, date, updateDate, createDate);
}
}, error: function (xhr, status, error) {
alert('해당 날짜에 일정이 없습니다.');
console.error('AJAX 요청 실패:', status, error);
}
})
}
// ID를 기준으로 조회합니다.
function getIdSchedule() {
// 1. 기존 스케줄 내용을 지웁니다.
$('#cards-box').empty();
// 2. 선택한 날짜 정보를 가져옵니다.
let id = $('#id-check').val();
// 3. 스케줄 목록을 불러와서 HTML로 붙입니다.
$.ajax({
type: 'GET',
url: `/api/schedule/id/${id}`,
success: function (response) {
let id = response.id;
let user = response.user;
let content = response.content;
let date = response.date;
let updateDate = response.updateDate;
let createDate = response.createDate;
addHTML(id, user, content, date, updateDate, createDate);
}, error: function (xhr, status, error) {
alert('id에 해당하는 일정이 없습니다.');
console.error('AJAX 요청 실패:', status, error);
}
})
}
원인을 찾기 위해 디버깅을 해보니 '검색'을 누르는 순간 $(document).ready(function) 만 동작하고 getDateSchedule() 이나 getIdSchedule() 메서드는 동작하지 않는다는 것을 알게 되었습니다.
결국 '검색'을 누르는 순간 지정한 메서드가 실행되는 것이 아니라 웹 페이지가 reload 되는 원인이 있을 것이라 판단하여 찾아보니 button에 문제가 있었습니다.
<button onclick="getIdSchedule()">검색</button>
button의 타입에는 3가지 값을 지정해줄 수 있는데 각각 submit, reset, button 입니다. 만약 아무런 값도 지정하지 않는다면 default 값은 submit이 됩니다.
따라서 form 태그 내에서 button을 사용할 때 타입 명시가 없다면 기본적으로 'submit' 처리가 일어나게 됩니다. 그렇게 되면 form 요소가 서버에 제출되고 폼 제출 후 페이지가 기본적으로 reload 됩니다. 이를 해결하기 위해 button에 type을 'button'으로 주었더니 정상적으로 지정한 메서드가 실행되었습니다.
<h2>검색</h2>
<form action="" class="mb-3">
<label>
<input id="calendar" placeholder="날짜를 입력해주세요"/>
<span class="validity"></span>
</label>
<button type="button" onclick="getDateSchedule()">검색</button>
</form>
<form action="" class="mb-3">
<label>
<input id="id-check" placeholder="id를 입력해주세요"/>
</label>
<button type="button" onclick="getIdSchedule()">검색</button>
</form>
</div>
참고 링크: https://nykim.work/96
버튼에 타입을 쓰는 이유 (button type="button")
프롤로그 가끔 이렇게 type을 명시한 버튼을 마주칠 때가 있는데 전 항상 궁금하더라구요. "아니 버튼이면 버튼이지 버튼 타입 버튼은 대체 뭐람" 그러고보면 비슷하게 타입을 명시하는 이라는
nykim.work
'Spring' 카테고리의 다른 글
[Spring] 일정관리 앱 코드 Fix (1) | 2024.10.30 |
---|---|
[Spring] a foreign key constraint fails 에러 발생 (0) | 2024.10.17 |
[Spring] 3 Layer Architecture (0) | 2024.10.02 |
[Spring] JDBC란 무엇이고 어떻게 사용할까? (1) | 2024.09.30 |
[Spring] DTO(Data Transfer Object) (0) | 2024.09.30 |