코드네임 JY

[CRUD 연습] 406 Error in Spring 본문

백엔드 공부

[CRUD 연습] 406 Error in Spring

영재임재영 2023. 1. 12. 11:32

 

스프링 공부 도중, '406 Not Acceptable' 이라는 에러 코드를 만나버렸다.. 😱😱

하지만 구글이 있다면? 절 대 로 무섭지 않지. 공부한 내용을 블로그에 정리해보도록 하겠다!

🍤 406 Not Acceptable 원인

ResponseEntity<MusicResponseDto> responseEntity
        = restTemplate.getForEntity(url, MusicResponseDto.class);
        
assertThat(responseEntity.getStatusCode()).isEqualTo(OK);  // 406 NOT_ACCEPTABLE

에러 코드의 시발점(욕 아님 🥲🥲)은 바로 여기서부터 시작되었다.

responseEntity 변수에 getStatusCode( ) 메서드를 사용하면 Status Code를 확인할 수 있다고 이전 포스팅에서 공부했다!

그렇게 해서 값을 찍어봤는데, '406 Not Acceptable' 이 발생했다고 나온 것이다!

 

이제 근본적인 문제는 responseEntity 변수를 해결하는 것에 있다.

왜냐면? responseEntity가 제대로 된 Status Code(200 OK)와 body(url에 매핑된 데이터)를 가져오지 못 했기 때문이다.

그렇다면 실제로 ResponseEntity가 어떻게 동작하는지 살펴볼 필요가 있다. (이거 진짜 로그 다 찍어보며 엄청나게 공부했다 ㅠ)

 

🍣 통합 테스트 진행 과정 (로그 찍어서 연구)

구글링을 해보면서 얻은 답은, 현재 MusicAPIController에서 DTO로 사용하는 객체에 Getter가 없는 것이 원인이라고 한다.

아래 두 코드는 각각 Controller, Service 코드이다.

@RestController
public class MusicAPIController {
    private final MusicService musicService;

    @GetMapping("/api/v1/music/{id}")
    public MusicResponseDto findById(@PathVariable Long id) {
        MusicResponseDto responseDto = musicService.findById(id);
        return responseDto;
    }
}
@Service
public class MusicService {
    private final MusicRepository musicRepository;
    
    public MusicResponseDto findById(Long id) {
        Optional<Music> result = musicRepository.findById(id);
        Music entity = new Music();

        if (result.isPresent()) {  // Optional 처리
            entity = result.get();
        } else {
            System.out.println("적절한 예외 처리 필요!");
        }
        return new MusicResponseDto(entity);
    }
}

그리고 문제의 원인이 있는 DTO 코드. '406 Not Acceptable' 에러는 Getter가 없어서 발생한 것이다!

public class MusicResponseDto {
    private Long id;
    private String title;
    private String artist;
    private String album;
    private String lyrics;

    public MusicResponseDto() {}

    public MusicResponseDto(Music entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.artist = entity.getArtist();
        this.album = entity.getAlbum();
        this.lyrics = entity.getLyrics();
    }
    
    // Getter 없어서 발생한 문제!
}

그리고 위 3개의 코드가 유기적으로 연결되어 잘 동작하는지 알아볼 수 있는 테스트 코드이다.

@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("Loco", "ITZY", "CRAZY IN LOVE", "I'm gettin loco");
        musicRepository.save(music);

        String url = "http://localhost:" + port + "/api/v1/music/" + music.getId();

        // when
        ResponseEntity<Music> responseEntity = restTemplate.getForEntity(url, Music.class);
        MusicResponseDto responseDto = new MusicResponseDto(responseEntity.getBody());
        
        // then
        assertThat(responseDto.getTitle()).isEqualTo(music.getTitle());
        assertThat(responseEntity.getStatusCode()).isEqualTo(OK);
    }
}

이제부터 테스트 코드를 중심으로, Controller, Service, DTO가 어떻게 연결되어 있는지 설명하도록 하겠다.

그러면서 DTO에 왜 Getter가 필요했는지 이유를 찾아보도록 하겠다!

 

위 과정(Step 1~6)을 문제 없이 진행했다면, 컨트롤러에서 사용되는 DTO에 Entity 객체가 매핑되어야 한다.

 

DTO 객체의 생성자 부분에 로그를 찍어서 확인해보면, DTO에 Entity 객체의 데이터가 잘 매핑된 것을 확인할 수 있다.

음? 그러면 406 에러가 뜬 이유는 뭐지? DTO에 데이터를 잘 받아왔으니까 200 OK가 떠야하는거 아닌가?

 

문제는 바로 Controller 에서 DTO를 리턴하는 부분에 있다.

 return responseDto;  라는 코드는 매우 단순해서 그냥 넘어갈 만 하지만, 그렇지 않았다.. 😭😭

public class MusicResponseDto {
    private Long id;
    private String title;
    private String artist;
    private String album;
    private String lyrics;

    public MusicResponseDto() {}

    public MusicResponseDto(Music entity) {
        System.out.println("MusicResponseDto.MusicResponseDto Constructor Call");
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.artist = entity.getArtist();
        this.album = entity.getAlbum();
        this.lyrics = entity.getLyrics();
        System.out.println("MusicResponseDto.MusicResponseDto Constructor End");
    }

    // Getter 추가
    public Long getId() {
        System.out.println("MusicResponseDto.getId");
        return id;
    }

    public String getTitle() {
        System.out.println("MusicResponseDto.getTitle");
        return title;
    }

    public String getArtist() {
        System.out.println("MusicResponseDto.getArtist");
        return artist;
    }

    public String getAlbum() {
        System.out.println("MusicResponseDto.getAlbum");
        return album;
    }

    public String getLyrics() {
        System.out.println("MusicResponseDto.getLyrics");
        return lyrics;
    }
}

DTO에 Getter가 없어서 발생한 문제이므로, Getter를 만들고 각각 로그를 적어두어서 Getter가 호출되는지 확인하였다.

@RestController
public class MusicAPIController {
    private final MusicService musicService;

    @GetMapping("/api/v1/music/{id}")
    public MusicResponseDto findById(@PathVariable Long id) {
        MusicResponseDto responseDto = musicService.findById(id);
        System.out.println("Come back to Controller");
        return responseDto;
    }
}

그리고 컨트롤러에서 DTO를 리턴하기 전, 로그 하나를 찍어두고 테스트 코드를 실행해보았더니..

 

이게 뭐람? DTO 객체의 생성자를 통해 Entity 데이터가 매핑되는 것은 확인했는데,

컨트롤러에 다시 돌아온 후 responseDto를 리턴할 때 DTO 객체의 Getter가 실행되는 것을 확인할 수 있다..!! 🔥🔥

결국 DTO에 Getter가 필요한 이유는, 컨트롤러에서 DTO 객체의 값을 리턴할 때 필요했던 것이었다!

 

그렇다면 Status Code가 406(서버가 목록과 일치하는 응답을 생성할 수 없음)으로 발생했던 이유는 무엇일까?

컨트롤러에서 DTO 객체를 리턴해줄 때, DTO 객체에 Getter가 없었기 때문에 값을 가져올 수 없었고,

그래서 제대로 된 응답을 생성할 수 없었던 것이다!

 

결론 : Controller에서 DTO 객체를 리턴해줄 때, DTO 객체의 값을 가져오기 위해서는 Getter가 반드시 필요하다!

 

사실 이 문제는 롬복을 써서 처음부터 @Getter 어노테이션을 사용했다면, 간단하게 해결할 수 있는 문제였을 것이다.

하지만 롬복은 편의 기능이라서, 공부하는데 큰 도움이 될 것이라 생각하지 않았고, 일단 롬복 없이 전체 프로젝트를 구현한다.

그리고 전체적으로 기능을 완성하면, 추후에 코드를 리팩토링하는 과정에서 롬복을 추가할 예정이다!

Comments