[CRUD 연습] 405 Error in Spring
이번에는 '405 Method Not Allowed' 에러 코드를 만나버렸다.. 🙄🙄
어떻게 된 영문인지.. 알아보도록 하자..!
🍫 405 Method Not Allowed 원인
CRUD 중, UPDATE 부분을 구현하고 있을 때였다. GET과 POST의 동작 방식을 공부했고, 그것을 바탕으로 테스트도 완료했다.
오 그러면? 데이터를 저장하고, id를 찾은 다음, postForEntity로 수정된 객체를 올리면 되지 않나? 라고 생각해서 코드를 짜봤다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MusicAPIControllerTest {
@LocalServerPort private int port;
@Autowired private MusicRepository musicRepository;
@Autowired private TestRestTemplate restTemplate;
@Test
void 수정() {
// given
Music music = new Music("I'm not the only one", "Sam Smith", "In the Lonely Hour", "You and me we made a vow");
musicRepository.save(music);
String url = "http://localhost:" + port + "/api/v1/music/" + music.getId();
MusicUpdateRequestDto requestDto = new MusicUpdateRequestDto("I don't think that I like her", "Charlie Puth", "Charlie", "Get her name and get her number");
// when
ResponseEntity<Music> responseEntity = restTemplate.postForEntity(url, requestDto, Music.class);
// then
System.out.println("responseEntity = " + responseEntity);
assertThat(responseEntity.getStatusCode()).isEqualTo(OK);
}
}
responseEntity가 받아온 값을 직접 확인해보면, 아래처럼 로그가 찍힌다.
responseEntity = <405,com.limjustin.crudprac.domain.music.Music@1a3a6216,[Allow:"PUT, GET", Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Thu, 12 Jan 2023 03:08:21 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
일단 시작부터 Status Code가 405라서 별로 마음에 들지 않는데, 뒤로 가보면 평소와 다른 약간 킹(?)받는 부분이 있다.
// Status Code 200 ResponseEntity
<200,com.limjustin.crudprac.domain.music.Music@58cd6088,[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Thu, 12 Jan 2023 03:11:00 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
// Status Code 405 ResponseEntity
<405,com.limjustin.crudprac.domain.music.Music@1a3a6216,[Allow:"PUT, GET", Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Thu, 12 Jan 2023 03:08:21 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
Status Code가 '200 OK'가 나온 ResponseEntity 값과 비교해보면,
Content-Type 앞에 Allow:"PUT, GET" 부분이 생겼다는 것을 확인해 볼 수 있다!
'405 Method Not Allowed' 의 내용은 '서버가 요청 메서드를 알고 있지만 대상 리소스가 이 메서드를 지원하지 않음' 이라고 한다.
왜 이런 에러 내용이 발생했는지 알아보기 위해서, 테스트 코드를 차근차근 읽어보도록 하겠다.
일단 테스트 코드의 given 부분에서 Music 객체(엔티티)를 하나 만들고, 이를 save( ) 함수를 통해 저장하였다.
그렇다면, 저장한 데이터가 매핑되어 있는 url이 있을 것이고, 그 주소는 '/api/v1/music/' + music.getId( ) 로 가져올 수 있다.
하지만 해당 주소는 이미 GET으로 한 번 요청한 url인데, 여기에다가 postForEntity를 해버리니 발생한 문제였다.
즉, 서버는 대상 리소스에 POST를 지원하지 않는데, 이미 GET으로 요청된 url에 POST까지 붙어버린 것이다.
그래서 Allow:"PUT, GET" 부분이 생겼다는 것을 짐작할 수 있다!
🍩 UPDATE 두둥등장
흠.. 그러면 어떻게 UPDATE 방식을 구현하지? 바로 exchage 라는 메서드를 사용하면 된다!
스프링 공식 docs를 확인해보면, 위와 같이 exchage 메서드가 있는 것을 확인할 수 있다.
올바르게 파라미터를 설정하고, 테스트 코드를 이전과 다르게 수정하면 다음과 같다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MusicAPIControllerTest {
@LocalServerPort private int port;
@Autowired private MusicRepository musicRepository;
@Autowired private TestRestTemplate restTemplate;
@Test
void 수정() {
// given
Music music = new Music("I'm not the only one", "Sam Smith", "In the Lonely Hour", "You and me we made a vow");
musicRepository.save(music);
String url = "http://localhost:" + port + "/api/v1/music/" + music.getId();
MusicUpdateRequestDto requestDto = new MusicUpdateRequestDto("I don't think that I like her", "Charlie Puth", "Charlie", "Get her name and get her number");
// when
// ResponseEntity<Music> responseEntity = restTemplate.postForEntity(url, requestDto, Music.class);
ResponseEntity<Long> responseEntity = restTemplate.exchange(url, HttpMethod.PUT, new HttpEntity<>(requestDto), Long.class);
// then
System.out.println("responseEntity = " + responseEntity);
List<Music> musicList = musicRepository.findAll();
assertThat(responseEntity.getStatusCode()).isEqualTo(OK);
assertThat(musicList.get(0).getTitle().isEqualTo(requestDto.getTitle());
}
}
테스트를 성공했고, responseEntity 값도 로그를 찍어 다시 확인해보면..
responseEntity = <200,1,[Content-Type:"application/json", Transfer-Encoding:"chunked", Date:"Thu, 12 Jan 2023 03:32:00 GMT", Keep-Alive:"timeout=60", Connection:"keep-alive"]>
'200 OK' 상태가 나온 것을 확인할 수 있고,
Hibernate: update music set album=?, artist=?, lyrics=?, title=? where id=?
POST & GET 방식(INSERT 쿼리 사용)과 다르게, Hibernate가 UPDATE 쿼리를 통해 동작을 수행한 것도 확인할 수 있다!
이어서 Controller, Service 코드도 확인해보자.
@RestController
public class MusicAPIController {
private final MusicService musicService;
@PutMapping("/api/v1/music/{id}") // 수정 (PUT-UPDATE)
public Long update(@PathVariable Long id, @RequestBody MusicUpdateRequestDto requestDto) {
return musicService.update(id, requestDto);
}
}
@Service
public class MusicService {
private final MusicRepository musicRepository;
@Transactional
public Long update(Long id, MusicUpdateRequestDto requestDto) {
Optional<Music> result = musicRepository.findById(id);
if (result.isPresent()) {
Music music = result.get();
music.update(requestDto.getTitle(), requestDto.getArtist(), requestDto.getAlbum(), requestDto.getLyrics());
} else {
System.out.println("적절한 예외 처리 필요!");
}
return id;
}
}
그리고 엔티티 그 잡채인! Music 객체의 수정을 위해서 Music 클래스 안에 update 메서드도 포함시켜주어야 한다.
update 메서드를 포함하지 않는다면, 데이터베이스에 값이 영속적으로 저장되지 않기 때문이다!
@Entity
public class Music {
...
public void update(String title, String artist, String album, String lyrics) {
this.title = title;
this.artist = artist;
this.album = album;
this.lyrics = lyrics;
}
}
🧁 JPA에서 save 함수 로직
UPDATE를 공부하면서, JPA에서 사용하는 save 함수 로직을 봤는데, INSERT와 UPDATE를 구분하는 조건문이 존재한다는 사실!
우리가 처음에 JpaRepository를 구현한 Repository 인터페이스를 사용할 때, 아래와 같이 파라미터를 넘겼다.
그리고 JPA에서 데이터가 save 될 때, Entity를 파라미터로 넘겨주는 것을 볼 수 있다.
이 때, 해당하는 Primary Key 값에 Entity가 할당되어 있는지 아닌지를 통해 INSERT와 UPDATE를 구분하는 것이다.
전혀 새로운 값이라면? INSERT를 통해 값을 저장하고, 기존에 있던 값이라면? merge 를 통해서 UPDATE를 하는 것이다.
그래서 INSERT와 UPDATE는 친구라고 볼 수 있다! 같은 save 함수를 사용하나 들어가는 로직이 다를 뿐! 😀😀