/growith

๐Ÿ‘ฉ๐Ÿปโ€๐Ÿ’ป IT ์ปค๋ฎค๋‹ˆํ‹ฐ ์„œ๋น„์Šค

Primary LanguageJava

growith

๋ฐฐํฌ ์ฃผ์†Œ(์ œ์ž‘์ค‘)

์Šค์›จ๊ฑฐ ์ฃผ์†Œ


ํ”„๋กœ์ ํŠธ ์ค‘ ๊ฒช์—ˆ๋˜ ์ด์Šˆ ๋ฐ ๊ณ ๋ฏผ

WebClient ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์–ด๋–ป๊ฒŒ ์ž‘์„ฑํ•ด์•ผํ• ๊นŒ? MockWebServer vs WireMock

โ“ Mock ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ 

WebClient๋ฅผ ์‚ฌ์šฉํ•ด์„œ Github Api๋กœ ์œ ์ € ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜ค๋Š” WebClientService ํด๋ž˜์Šค๊ฐ€ ์žˆ์—ˆ๊ณ , ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์งœ๊ธฐ ์œ„ํ•ด ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ณด๋‹ˆ, Mock ์›น ์„œ๋ฒ„๋ฅผ ์ •์˜ํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


  1. ์‹ค์ œ API ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ๋ฅผ ํ•˜๋ฉด, ์„œ๋ฒ„ ์ƒํƒœ์— ๋”ฐ๋ผ ํ…Œ์ŠคํŠธ์˜ ๊ฒฐ๊ณผ๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  2. Mock ์„œ๋ฒ„๋กœ๋„ ์ถฉ๋ถ„ํžˆ ์™ธ๋ถ€ API๊ฐ€ ์ •์ƒ์ด๊ณ  ์ •์ƒ์ ์ธ ๊ฐ’(์˜ˆ์ƒํ•œ ๊ฐ’)์ด ๋ฐ˜ํ™˜๋œ๋‹ค๋ฉด, ์ •์ƒ์ ์œผ๋กœ ๋กœ์ง์ด ์ž‘๋™ํ•˜๋Š” ๊ฒƒ์„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  3. ๋กœ์ปฌ์— ์„œ๋ฒ„๋ฅผ ๋„์›Œ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์†๋„๋„ ๋น ๋ฅด๋‹ค๋Š” ์žฅ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.


์œ„์™€ ๊ฐ™์€ ์žฅ์ ์ด ์žˆ๋Š” Mock ์›น ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋Š” ๋Œ€ํ‘œ์ ์œผ๋กœ MockWebServer์™€ WireMock ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

stackOverflow์˜ ๋‹ต๋ณ€์— ์˜ํ•˜๋ฉด WireMock์ด ์•ˆ๋“œ๋กœ์ด๋“œ์—์„œ ์ด์Šˆ๊ฐ€ ์žˆ์–ด์„œ ๋‚˜์˜จ ๊ฒƒ์ด MockWebServer๋ผ์„œ

์•ˆ๋“œ๋กœ์ด๋“œ ํ™˜๊ฒฝ์ด ์•„๋‹ˆ๋ผ๋ฉด WireMock์ด ๋” ์ข‹๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ €๋Š” ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ• ๋ชจ๋‘ ์‚ฌ์šฉํ•ด์„œ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ํ•œ๋ฒˆ ์ž‘์„ฑํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋‘ ๋ฐฉ์‹์˜ ๊ณตํ†ต์ ์€ ๋กœ์ปฌ์— ๊ฐ€์งœ ์„œ๋ฒ„๋ฅผ ๋„์šด ๋’ค, ๊ทธ ์„œ๋ฒ„์— ํŠน์ • ์š”์ฒญ์„ ํ–ˆ์„ ๋•Œ์˜ ์‘๋‹ถ๊ฐ’์„ ๊ฐ€์ •ํ•œ ๋’ค, ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

1๏ธโƒฃ MockWebServer

testImplementation group: 'com.squareup.okhttp3', name: 'mockwebserver', version: '5.0.0-alpha.11'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '5.0.0-alpha.11'

mockwebserver ์™€ okhttp ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ๋‘ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ๋ฒ„์ „์€ ์ผ์น˜ํ•ด์•ผ ํ•œ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

@ExtendWith(MockitoExtension.class)
class WebClientServiceTest {


    private WebClientService webClientService;

    public static MockWebServer mockWebServer;

    @BeforeAll
    public static void setUp() throws IOException {
        mockWebServer = new MockWebServer();
        mockWebServer.start();
    }

    @BeforeEach
    void init() {
        String baseUrl = String.format("http://localhost:%s", mockWebServer.getPort());
        webClientService = new WebClientService(WebClient.create(baseUrl));
    }

    @AfterAll
    public static void shutdown() throws IOException {
        mockWebServer.shutdown();
    }

๋จผ์ €, ์ƒ์„ฑํ•œ MockWebServer์— ์š”์ฒญ์„ ๋ณด๋‚ด์•ผ ์ง์ ‘ ์ •์˜ํ•œ ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, WebClient์— ์ž…๋ ฅํ•˜๋Š” baseUrl์„ MockServer ํฌํŠธ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.

        @Test
        @DisplayName("AccessToken ๊ฐ€์ ธ์˜ค๊ธฐ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ")
        public void getAccessTokenSuccess() {
            String expectedToken = "token";
            String expectedResponse = String.format("access_token=%s&expires_in={๊ฐ’}&refresh_token={๊ฐ’}&refresh_token_expires_in={๊ฐ’}&scope=&token_type={๊ฐ’}", expectedToken);
            
            mockWebServer.enqueue(new MockResponse()
                    .setBody(expectedResponse));

            String code = "code";

            String accessToken = webClientService.getAccessToken(code, "/login/oauth/access_token");

            assertThat(accessToken).isEqualTo(expectedToken);

        }

enqueue() ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด์„œ mockWebServer๋กœ ์š”์ฒญ์ด ๋“ค์–ด์™”์„๋•Œ ์‘๋‹ต ๊ฐ’์„ ์ง€์ •ํ•ด์ค๋‹ˆ๋‹ค.

GitHub Api๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ํ†ตํ•ด ์ธ์ฆํ•˜๋ฉด ๋ฐ›๋Š” Code๋ฅผ https://github.com/login/oauth/access_token๋กœ Post ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด

access_token={๊ฐ’}&expires_in={๊ฐ’}&refresh_token={๊ฐ’}&refresh_token_expires_in={๊ฐ’}&scope=&token_type={๊ฐ’}

์œ„์™€ ๊ฐ™์€ ํ˜•์‹์œผ๋กœ ์‘๋‹ตํ•ฉ๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ, ์œ„์™€ ๊ฐ™์€ ๊ฐ’์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์‹ค์ œ๋กœ๋Š” https://github.com/login/oauth/access_token ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€๋งŒ, ์ง์ ‘ ์ƒ์„ฑํ•œ ๊ฐ€์งœ ์„œ๋ฒ„์— ์š”์ฒญ์„ ๋ณด๋‚ด์•ผ ํ•˜๋ฏ€๋กœ uri๋ฅผ /login/oauth/access_token๋กœ ์ž…๋ ฅํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ ์ƒ์œผ๋กœ๋Š”localhost:{mockWebServer์˜ port}/login/oauth/access_token ๋กœ ์š”์ฒญ์ด ๋ณด๋‚ด์งˆ ๊ฒƒ์ด๊ณ , ์„ค์ •ํ•œ ์‘๋‹ต๊ฐ’์„ ๊ธฐ๋Œ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ „์ฒด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ


2๏ธโƒฃ WireMock

implementation group: 'org.springframework.cloud', name: 'spring-cloud-starter-contract-stub-runner', version: '4.0.1'

WireMock์„ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์œ„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

@AutoConfigureWireMock
class WebClientServiceTest2 {

    private static WireMockServer wireMockServer;

    @Autowired
    private WebClientService webClientService;
    
    @BeforeAll
    public static void setUp() throws IOException {
        wireMockServer = new WireMockServer();
        wireMockServer.start();
    }

    @BeforeEach
    void init(){
        String baseUrl = String.format("http://localhost:%s", wireMockServer.port());
        webClientService = new WebClientService(WebClient.create(baseUrl));
    }

    @AfterAll
    public static void stop(){
        wireMockServer.stop();
    }

WireMock ๋ฐฉ์‹๋„ MockWebServer ๋ฐฉ์‹๊ณผ ์ดˆ๊ธฐ ์„ธํŒ…์€ ์œ ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

        @Test
        @DisplayName("AccessToken ๊ฐ€์ ธ์˜ค๊ธฐ ์„ฑ๊ณต ํ…Œ์ŠคํŠธ")
        public void Success(){
            String expectedToken = "token";
            String expectedResponse = String.format("access_token=%s&expires_in={๊ฐ’}&refresh_token={๊ฐ’}&refresh_token_expires_in={๊ฐ’}&scope=&token_type={๊ฐ’}", expectedToken);

            wireMockServer.stubFor(post(urlEqualTo("/login/oauth/access_token"))
                    .willReturn(
                    aResponse()
                            .withBody(expectedResponse)
            ));
            String code = "code";
            String accessToken = webClientService.getAccessToken(code, "/login/oauth/access_token");
            System.out.println(accessToken);

            assertThat(accessToken).isEqualTo(expectedToken);

        }

ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋„ ์—ญ์‹œ ๋น„์Šทํ•˜์ง€๋งŒ,

WireMock ๋ฐฉ์‹์˜ ๊ฒฝ์šฐ Http Method๋„ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด ํฐ ์ฐจ์ด์ ์ธ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ ์œ„ ์ฝ”๋“œ์—์„œ stubFor(post(urlEqualTo("/login/oauth/access_token"))์˜ post๋ฅผ stubFor(get(urlEqualTo("/login/oauth/access_token")) get์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ ์‹คํŒจ๋ฅผ ํ•˜๊ฒŒ๋ฉ๋‹ˆ๋‹ค.

์™œ๋ƒํ•˜๋ฉด getAccessToken() ๋ฉ”์„œ๋“œ์—์„œ๋Š”, ํŒŒ๋ผ๋ฏธํ„ฐ์— ์ž…๋ ฅ๋œ url์— post ์š”์ฒญ์„ ๋ณด๋‚ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

MockWebServer ๋ณด๋‹ค ๊ตฌ์ฒด์ ์ธ ์ƒํ™ฉ์œผ๋กœ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด ์ฝ”๋“œ๊ฐ€ ์–ด๋–ค ์—ญํ• ์ธ์ง€ ๋” ์ž˜ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋Š” ์žฅ์ ์ด ์žˆ๋‹ค๊ณ  ์ƒ๊ฐ์ด ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค.

์ „์ฒด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ


WebClient ๊ด€๋ จ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ๋ฐฉ๋ฒ•์„ ์ดํ•ดํ•˜๋Š”๋ฐ ์–ด๋ ค์›€์ด ์žˆ์—ˆ๊ณ  ๋งŽ์€ ์‹œ๊ฐ„์„ ํˆฌ์žํ–ˆ์Šต๋‹ˆ๋‹ค.

MockWebServer ์™€ WireMock์— ๋Œ€ํ•ด ์•Œ๊ฒŒ ๋˜์–ด ์™ธ๋ถ€ API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์งค ์ˆ˜ ์žˆ๊ฒŒ ๋˜์–ด ๊ฐ’์ง„ ์‹œ๊ฐ„์ด์—ˆ๋˜ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋ ค๋‹ค ๋ณด๋‹ˆ, ๋•๋ถ„์— WebClient ๊ฐ์ฒด๋ฅผ Bean์œผ๋กœ ๋“ฑ๋กํ•œ ๋’ค DI ๋ฐ›๋„๋ก ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ  ์š”์ฒญ์„ ๋ณด๋‚ด๋Š” uri๋„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ž…๋ ฅ๋ฐ›๋„๋ก ๋ฆฌํŒฉํ† ๋งํ•˜๊ฒŒ ๋˜๋Š” ๊ณ„๊ธฐ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

@RequestParam ๋ฌธ์ž์—ด์„ Converter๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜์—ฌ ๋‹ค์ด๋ ‰ํŠธ๋กœ Enum ํƒ€์ž… ๋งคํ•‘ํ•˜๊ธฐ

๐Ÿ’ก RequestParam์œผ๋กœ ์š”์ฒญ๋œ ๋ฌธ์ž์—ด์„ Enum ํƒ€์ž…์œผ๋กœ ๋งคํ•‘ํ•˜๊ธฐ

Postํ•„๋“œ์—๋Š” Enum์œผ๋กœ ๊ด€๋ฆฌ๋˜๋Š” Categoryํ•„๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์ €๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ Request Parameter๋กœ ์ž…๋ ฅ๋ฐ›์•„ ์นดํ…Œ๊ณ ๋ฆฌ์— ํ•ด๋‹นํ•˜๋Š” ๊ฒŒ์‹œ๊ธ€๋“ค์„ ๋ฐ˜ํ™˜ํ•˜๋Š” GET ์š”์ฒญ API๋ฅผ ๋งŒ๋“ค๋ ค๊ณ  ํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ์ƒํ•˜๋Š” uri๋Š” /api/v1/posts/categories?category=qna ์™€ ๊ฐ™์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

@RestController
@Slf4j
@RequestMapping("/api/v1/posts")
@RequiredArgsConstructor
public class PostApiController {

    @GetMapping("/categories")
    public ResponseEntity<Response<Page<PostGetListResponse>>> getAllByCategory(@RequestParam String requestCategory, Pageable pageable){
        Category category = Category.valueOf(requestCategory)
        Page<PostGetListResponse> response = postService.getAllPostsByCategory(category, pageable);
        return ResponseEntity.ok(Response.success(response));
    }
}

๋”ฐ๋ผ์„œ ์œ„ ์ฝ”๋“œ์™€ ๊ฐ™์ด RequestParam์€ String๊ฐ’์œผ๋กœ ์ž…๋ ฅ๋ฐ›๊ณ , ์ž…๋ ฅ๋ฐ›์€ ๊ฐ’์„ Enumํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜์‹œ์ผœ์•ผ ํ–ˆ์Šต๋‹ˆ๋‹ค.


์ด ๋ณ€ํ™˜ ๊ณผ์ • ๋•Œ๋ฌธ์— ์ฝ”๋“œ๊ฐ€ ๋งŽ์ด ์ถ”๊ฐ€๋˜์–ด ๊ธธ์–ด์ง€๋Š” ๊ฒƒ์€ ์•„๋‹ˆ์ง€๋งŒ,

์ž…๋ ฅ๋ฐ›์Œ๊ณผ ๋™์‹œ์— ์ฒ˜๋ฆฌ๋œ๋‹ค๋ฉด ์ฝ”๋“œ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ๊ณผ ํ•ต์‹ฌ ๋กœ์ง๋งŒ ๋ฉ”์„œ๋“œ ์•ˆ์— ๋‹ด์„ ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋ผ ์ƒ๊ฐ๋˜์–ด ๋ฐฉ๋ฒ•์„ ์ฐพ์•„๋ดค์Šต๋‹ˆ๋‹ค.


public enum Category {
    QNA, COMMUNITY, STUDY, NOTICE;

    public static Category create(String requestCategory) {
        for (Category value : Category.values()) {
            if (value.toString().equals(requestCategory)) {
                return value;
            }
        }
        throw new IllegalStateException("์ผ์น˜ํ•˜๋Š” ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
    }
}

๋จผ์ € Enumํƒ€์ž…์ธ Category์•ˆ์— Stringํƒ€์ž…์ธ requestCategory๋ฅผ Category๋กœ ๋งŒ๋“ค์–ด์ฃผ๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.


public class PostEnumConverter implements Converter<String, Category> {

    @Override
    public Category convert(String requestCategory) {
        return Category.create(requestCategory.toUpperCase());
    }
}

๊ทธ๋ฆฌ๊ณ  import org.springframework.core.convert.converter.Converter; ์— ์žˆ๋Š” Converter ์˜ convert() ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ฉ๋‹ˆ๋‹ค.

Request Parameter๋กœ ์ž…๋ ฅ๋˜๋Š” ๋ฌธ์ž์—ด์„ toUpperCase() ๋ฅผ ์ด์šฉํ•ด์„œ ๋ชจ๋“  ๋ฌธ์ž๋ฅผ ๋Œ€๋ฌธ์ž๋กœ ๋ฐ”๊ฟ”์ค€ ํ›„ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.


@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new PostEnumConverter());
    }
}

๊ทธ๋ฆฌ๊ณ  WebConfiguer์˜ addFormatters() ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜์—ฌ ๋งŒ๋“  Converter๋ฅผ ์ถ”๊ฐ€ํ•ด์ค๋‹ˆ๋‹ค.

์ด์ œ ์ž˜ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ์„ ํ•ด๋ด…๋‹ˆ๋‹ค.

/api/v1/posts/categories?category=QNA ๋‚˜ /api/v1/posts/categories?category=qna ๋กœ ์š”์ฒญํ–ˆ์„ ๋•Œ, ์ž˜ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์ด ํ™•์ธ์ด ๋˜์ง€๋งŒ,

/api/v1/posts/categories?category= ๋ผ๋˜์ง€ /api/v1/posts/categories?category=hi ์™€ ๊ฐ™์€ ์ž˜๋ชป๋œ ์š”์ฒญ ์‹œ, ์—๋Ÿฌ ํ•ธ๋“ค๋ง์ด ๋˜์ง€ ์•Š์Œ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.


๋กœ๊ทธ๋ฅผ ๋ณด๋‹ˆ /api/v1/posts/categories?category= ์š”์ฒญ์ฒ˜๋Ÿผ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ž์—ด๋กœ ์š”์ฒญ์„ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” MissingServletRequestParameterException ์—๋Ÿฌ๊ฐ€,

/api/v1/posts/categories?category=hi ์š”์ฒญ์ฒ˜๋Ÿผ Enum ์œผ๋กœ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ’์„ ์š”์ฒญํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” MethodArgumentTypeMismatchException ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒƒ์„ ํ™•์ธํ–ˆ์Šต๋‹ˆ๋‹ค.

@RestControllerAdvice
public class ExceptionManager {

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseEntity<?> converterExceptionHandler(MissingServletRequestParameterException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Response.error(ErrorCode.REQUEST_PARAM_NOT_MATCH.getMessage()));
    }
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResponseEntity<?> converterSecondExceptionHandler(MethodArgumentTypeMismatchException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Response.error(ErrorCode.REQUEST_PARAM_NOT_MATCH.getMessage()));
    }
}

๋”ฐ๋ผ์„œ ์œ„์™€ ๊ฐ™์ด ์—๋Ÿฌ๋ฅผ ํ•ธ๋“ค๋งํ•˜๋Š” ExceptionManager ๋ฅผ ํ†ตํ•ด ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ์ง„ํ–‰ํ–ˆ์Šต๋‹ˆ๋‹ค.

image-20230401173431514

์ •์ƒ์ ์œผ๋กœ ์—๋Ÿฌ ์‘๋‹ต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    @GetMapping("/categories")
    public ResponseEntity<Response<Page<PostGetListResponse>>> getAllByCategory(@RequestParam Category category, Pageable pageable) {
        Page<PostGetListResponse> response = postService.getAllPostsByCategory(category, pageable);
        return ResponseEntity.ok(Response.success(response));
    }

๋•๋ถ„์— Controller์˜ ๋ฉ”์„œ๋“œ๊ฐ€ ํ›จ์”ฌ ๊น”๋”ํ•ด์ ธ ๊ฐ€๋…์„ฑ์ด ํ–ฅ์ƒ๋˜์—ˆ๊ณ , ํ•ต์‹ฌ ๋กœ์ง์—๋งŒ ์ง‘์ค‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

Request Dto์— ์žˆ๋Š” Enum ํƒ€์ž… ํ•„๋“œ validation ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๊ฐœ์„ ํ•˜๊ธฐ

๐Ÿ’ก Request Dto์— ์žˆ๋Š” Enum ํƒ€์ž… ํ•„๋“œ validation ์˜ˆ์™ธ์ฒ˜๋ฆฌ ๊ฐœ์„ ํ•˜๊ธฐ


implementation 'org.springframework.boot:spring-boot-starter-validation'

์œ„ validation ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด

Controller์—์„œ @RequestBody ์–ด๋…ธํ…Œ์ด์…˜์œผ๋กœ ๋งคํ•‘ํ•˜๋Š” request dto์˜ ํ•„๋“œ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

public class PostCreateRequest {
    @NotBlank(message = "์ œ๋ชฉ์€ ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.")
    private String title;
    @NotBlank(message = "๋‚ด์šฉ์€ ํ•„์ˆ˜ ์ž…๋ ฅ ํ•ญ๋ชฉ์ž…๋‹ˆ๋‹ค.")
    private String content;
    @NotNull(message = "์œ ํšจํ•˜์ง€ ์•Š์€ ์นดํ…Œ๊ณ ๋ฆฌ๊ฐ€ ์ž…๋ ฅ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
    private Category category;
}

์œ„์™€ ๊ฐ™์ด ๋งคํ•‘๋˜๋Š” ํ•„๋“œ์— validation์—์„œ ์ œ๊ณตํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, ์œ„ Category ํ•„๋“œ์— ์‚ฌ์šฉํ•œ @NotNull ์–ด๋…ธํ…Œ์ด์…˜์˜ ๊ฒฝ์šฐ null์ด ์ž…๋ ฅ๋˜๋Š” ๊ฒƒ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.


Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `com.growith.domain.post.Category` from String "category": not one of the values accepted for Enum class: [NOTICE, STUDY, QNA, COMMUNITY]

๊ทธ๋ฆฌ๊ณ  BindingError๋ฅผ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ํ…Œ์ŠคํŠธ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ ๋’ค, ์‹คํ–‰์‹œ์ผœ๋ณด๋‹ˆ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.


public enum Category {
    QNA, COMMUNITY, STUDY, NOTICE;
    }
}

์นดํ…Œ๊ณ ๋ฆฌ์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ƒ์ˆ˜๋“ค์ด ์กด์žฌํ•˜๋Š”๋ฐ,


null ๊ฐ’์ด ์ž…๋ ฅ๋˜๋Š” ์ƒํ™ฉ์€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ, Enum์œผ๋กœ ์ •์˜๋˜์ง€ ์•Š์€ ๋ฌธ์ž์—ด(์œ„ ์˜ˆ์‹œ์—์„  category:"category")์ด ์ž…๋ ฅ๋์„ ๋•Œ๋Š” ์ฒ˜๋ฆฌ๊ฐ€ ๋˜์ง€ ์•Š์€ ์ƒํ™ฉ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ, "qna" ์™€ ๊ฐ™์ด ์†Œ๋ฌธ์ž๋กœ ์ž…๋ ฅ๋˜๋Š” ๊ฒฝ์šฐ์—๋„ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ๋Œ€์†Œ๋ฌธ์ž๋Š” ๊ตฌ๋ณ„ํ•˜์ง€ ์•Š๊ณ  ์ผ์น˜ํ•˜๋ฉด ์ •์ƒ์ ์œผ๋กœ ๋กœ์ง์ด ์‹คํ–‰๋˜๊ณ ,

null์ด๊ฑฐ๋‚˜ enum์œผ๋กœ ์ •์˜ํ•˜์ง€ ์•Š์€ ๊ฐ’์ด ๋“ค์–ด์˜ค๋Š” ๊ฒฝ์šฐ๋งŒ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


public enum Category {
    QNA, COMMUNITY, STUDY, NOTICE;
    }

    @JsonCreator
    public static Category parsing(String inputValue) {
        return Stream.of(Category.values())
                .filter(category -> category.toString().equals(inputValue.toUpperCase()))
                .findFirst()
                .orElse(null);
    }
}

๋ฐฉ๋ฒ•์€ enum ๊ฐ์ฒด ์•ˆ์— @JsonCreator ์–ด๋…ธํ…Œ์ด์…˜๊ณผ ์ƒ์„ฑ์ž๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ Json๋ฐ์ดํ„ฐ๋ฅผ ์—ญ์ง๋ ฌํ™” ํ•˜๋Š” ๊ณผ์ •์„ ์ˆ˜๋™ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

category ๋ฅผ ํ‚ค๊ฐ’์œผ๋กœ ํ•˜๋Š” value๊ฐ’์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ์„œ inputValue๊ฐ’์œผ๋กœ ๋ฐ›์•„์™€์„œ ๋กœ์ง์ด ์ˆ˜ํ–‰๋ฉ๋‹ˆ๋‹ค.

์ž…๋ ฅ๋˜๋Š” inputValue๊ฐ’์„ toUpperCase() ๋ฉ”์„œ๋“œ๋กœ ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜ ํ›„,

์ผ์น˜ํ•˜๋Š” Category๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ฐ˜ํ™˜ํ•˜๊ณ  ์—†๋‹ค๋ฉด null์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ํ–ˆ์Šต๋‹ˆ๋‹ค.

null์ด ๋ฐ˜ํ™˜๋˜๋ฉด validation์œผ๋กœ ์„ค์ •ํ•œ @NotNull์–ด๋…ธํ…Œ์ด์…˜์— ์˜ํ•ด BindingError๊ฐ€ ๋ฐœ์ƒํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ฒฐ๊ณผ

image-20230404192343858

๋ชฉํ‘œํ–ˆ๋˜ ๋Œ€๋กœ ์˜ˆ์™ธ์ฒ˜๋ฆฌ๊ฐ€ ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ , ์—๋Ÿฌ ์‘๋‹ต์„ ํ†ตํ•ด ์–ด๋–ค ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋Š”์ง€ ๋ช…์‹œํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.