본문 바로가기

체크박스·라디오·셀렉트

게시판 공개 여부와 분류·태그 선택을 통해 체크박스 hidden 표시, 단일·복수 선택, 열거형 변환 실패와 선택지 소유권을 검증합니다.

선택 제어는 화면에서는 비슷해 보여도 전송 규칙이 다릅니다.

선택하지 않은 체크박스는 파라미터 자체를 보내지 않고, 라디오 버튼은 같은 이름 중 하나만 보내며, 다중 선택 목록은 한 이름을 여러 번 보냅니다.

Spring과 Thymeleaf의 편의 기능을 쓰더라도 실제 HTTP 파라미터가 무엇인지 알아야 수정 화면에서 이전 값이 남는 오류를 막을 수 있습니다.

checkbox는 marker만 전송되면 이전 true를 false로 바꾸고, tagCodes marker는 기존 선택을 빈 목록으로 바꿉니다. category QUESTION은 enum, categoryCode NOTICE는 String이며 알 수 없는 enum EXTREME은 단위 바인더의 typeMismatch입니다. 반복 tagCodes는 순서와 중복을 보존합니다. 실제 MVC POST는 바인딩 뒤 화면만 다시 그리며 현재 활성 옵션·소유권 조회와 정책/command 실행은 추가 제품 설계입니다.

PROCESS · WIRE SHAPE · CURRENT POLICY

선택 입력의 전송 모양과 실제 바인딩 결과를 비교한다

checkbox marker·단일 값·반복 값은 서로 다른 상태를 표현합니다. 이 교재가 실행한 바인딩·재표시와 추가로 설계해야 할 현재 옵션·소유권 정책을 나누어 읽습니다.

원 POST의 바인딩·재표시·오류 경계문자열·marker·반복값을 새 폼에 바인딩한 원 POST의 구현 범위이다. 변환 오류가 없을 때만 update가 같은 화면200을 반환한다. category의알수없는enum처럼바인딩오류가있으면인접BindingResult가없는현재핸들러에진입하지않고기본MVC400이다. tagCodesList의중복과categoryCodeString의허용옵션정책검증은이POST에연결되지않았다. marker는권한증거가아니며저장/현재활성조회는추가설계이다.오류 없음바인딩 오류POST 전송값marker · 반복 값WebDataBinder폼 속성 · 타입 변환update 진입정책 · 저장 호출 없음같은 화면 200checked · selected기본 MVC 400handler 진입 전같은 폼 반환 아님
원 POST의 바인딩·재표시·오류 경계문자열·marker·반복값을 새 폼에 바인딩한 원 POST의 구현 범위이다. 변환 오류가 없을 때만 update가 같은 화면200을 반환한다. category의알수없는enum처럼바인딩오류가있으면인접BindingResult가없는현재핸들러에진입하지않고기본MVC400이다. tagCodesList의중복과categoryCodeString의허용옵션정책검증은이POST에연결되지않았다. marker는권한증거가아니며저장/현재활성조회는추가설계이다.오류 없음바인딩 오류POST 전송값marker · 반복 값WebDataBinder폼 속성 · 타입 변환update 진입정책 · 저장 호출 없음같은 화면 200checked · selected기본 MVC 400handler 진입 전같은 폼 반환 아님

이 흐름은 현재 POST의 구현 경계입니다. 아래 전송 모양 비교와 단위 바인더의 기존 객체 조건을 함께 읽으세요. List의 중복·선택지 활성 상태·소유권과 저장 정책은 이 POST에 연결되어 있지 않습니다.

CHECKBOX · MARKER ONLY

이전 true라도 해제하면 false로

체크 상태는 publicVisible=true와 _publicVisible=on을 보냅니다. 해제하면 marker만 보냅니다. 단위 바인더는 기존 true를 false로 바꾸고, MVC marker-only POST는 unchecked HTML을 만듭니다. marker는 권한 증명이 아닙니다.

RADIO / SINGLE SELECT

같은 코드라도 대상 타입을 구분한다

category=QUESTION은 PostCategory enum, categoryCode=NOTICE는 String 속성입니다. GET fixture는 QUESTION radio checked와 NOTICE option selected를 확인합니다. category=EXTREME은 단위 바인더의 typeMismatch입니다.

REPEATED PARAMETERS

원본 List의 순서와 중복을 남긴다

tagCodes=java&tagCodes=spring처럼 같은 이름을 반복합니다. 단위 바인더가 java·spring·java 세 값을 같은 순서로 보존합니다. 처음부터 Set으로 받으면 중복 관찰을 잃습니다. 쉼표 연결은 브라우저 폼의 기본 전송 규칙이 아닙니다.

COLLECTION MARKER

모두 해제하면 빈 목록으로

_tagCodes=on만 보내면 단위 바인더는 기존 java 선택을 비웁니다. 실제 MVC marker-only POST의 폼과 HTML도 빈 tagCodes와 checked 없음, marker 존재를 확인합니다.

IMPLEMENTED FIXTURE

실제 POST는 바인딩 후 화면 재표시

컨트롤러는 고정 categories·tags를 모델에 넣고 바인딩된 form으로 같은 템플릿을 렌더링합니다. 서비스·repository 조회·정책 검증·command 실행은 이 POST에 연결되어 있지 않습니다. 5개 단위 바인더와 2개 MVC 렌더링 테스트의 범위를 구분합니다.

PRODUCT POLICY / EXERCISE

현재 활성·접근 범위 검사는 추가 설계

제품에서는 POST 시점의 존재·활성·사용자 접근 범위를 다시 조회하고 원본 List의 개수·중복을 검사해야 합니다. 연습의 TagSelectionPolicy는 최대3·중복·blank를 거부하지만 현재 POST에 연결되지 않았습니다. marker나 GET 옵션은 이 검사를 대신하지 않습니다.

선택 상태를 옮기는 바인더와 현재 권한·업무 정책을 검사하는 애플리케이션의 책임은 다릅니다. 이 그림의 wire 비교는 실행된 fixture를 설명하며 아래 제품 정책은 구현 범위를 확대하는 설계 과제입니다.

그림의 wire와 바인딩 결과는 아래 단위 바인더·MVC 렌더링 fixture가 검증합니다. 예제 POST는 바인딩된 폼으로 화면을 다시 그릴 뿐 저장이나 현재 활성 옵션·소유권 조회를 실행하지 않습니다. 그 정책과 command 연결은 제품 설계와 연습 문제의 추가 범위입니다.


체크박스 미선택 값

수정 폼에서 publicVisible=true인 객체를 열었다가 체크박스를 해제하면 브라우저는 publicVisible 파라미터를 보내지 않습니다.

바인더가 “값 없음”을 기존 값 유지로 해석하면 true가 계속 남습니다.

Thymeleaf의 th:field는 _publicVisible 숨은 표시를 함께 만들어 “제어는 화면에 있었지만 선택하지 않았다”를 알립니다.

src/main/resources/templates/controls/post-settings.html
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8" />
  <title>게시글 표시 설정</title>
</head>
<body>
  <main>
    <h1>게시글 표시 설정</h1>
    <form th:action="@{/b88/controls/posts/{id}/settings(id=${postId})}"
          th:object="${form}" method="post">
      <label>
        <input type="checkbox" th:field="*{publicVisible}" />
        다른 회원에게 공개
      </label>
      <fieldset>
        <legend>게시글 분류</legend>
        <label th:each="category : ${categories}">
          <input type="radio" th:field="*{category}"
                 th:value="${category}" />
          <span th:text="${category.label}">질문</span>
        </label>
      </fieldset>
      <div th:replace="~{controls/_post-settings-options :: options}"></div>
      <button type="submit">설정 저장</button>
    </form>
  </main>
</body>
</html>

렌더링된 체크박스에는 name="publicVisible" value="true"와 name="_publicVisible" value="on"이 존재합니다.

체크박스가 체크되면 둘 다 전송되고, 해제되면 표시만 전송됩니다.

공격자가 표시를 임의로 만들 수 있으므로 이것은 보안 장치가 아니라 바인딩 상태 표현입니다.

권한은 별도로 검사해야 합니다.


라디오·단일 셀렉트

선택지를 템플릿에 문자열로 중복하면 Java 열거형이 바뀌었을 때 화면과 바인더가 어긋납니다.

컨트롤러가 허용 집합을 모델에 넣고 폼은 열거형을 받습니다.

src/main/java/board/controls/PostSettingsForm.java
package board.controls;

import java.util.ArrayList;
import java.util.List;

public final class PostSettingsForm {
    private boolean publicVisible;
    private PostCategory category;
    private String categoryCode = "";
    private List<String> tagCodes = new ArrayList<>();

    public boolean isPublicVisible() {
        return publicVisible;
    }

    public void setPublicVisible(boolean publicVisible) {
        this.publicVisible = publicVisible;
    }

    public PostCategory getCategory() {
        return category;
    }

    public void setCategory(PostCategory category) {
        this.category = category;
    }

    public String getCategoryCode() {
        return categoryCode;
    }

    public void setCategoryCode(String categoryCode) {
        this.categoryCode = categoryCode;
    }

    public List<String> getTagCodes() {
        return List.copyOf(tagCodes);
    }

    public void setTagCodes(List<String> tagCodes) {
        this.tagCodes = tagCodes == null
                ? new ArrayList<>()
                : new ArrayList<>(tagCodes);
    }

    public enum PostCategory {
        GENERAL("일반"), QUESTION("질문"), NOTICE("공지");

        private final String label;

        PostCategory(String label) {
            this.label = label;
        }

        public String getLabel() {
            return label;
        }
    }
}

열거형 이름은 저장·전송 계약이 되므로 무심코 이름을 변경하지 않습니다.

외부 계약을 안정시키려면 NOTICE 대신 별도의 코드를 두고 Converter<String, PostCategory>에서 허용 코드를 해석합니다.

레이블은 로케일별 메시지로 분리할 수 있습니다.


복수 선택 바인딩

태그 java, spring을 선택하면 본문은 tagCodes=java&tagCodes=spring 모양입니다.

쉼표 하나로 합쳐 보내는 것은 별도 컨버터 계약이며 HTML 폼 기본 동작이 아닙니다.

컬렉션 속성은 null보다 빈 컬렉션으로 초기화하여 미선택 상태를 안정적으로 다룹니다.

src/main/resources/templates/controls/_post-settings-options.html
<div xmlns:th="http://www.thymeleaf.org" th:fragment="options">
  <label for="categoryCode">분류</label>
  <select id="categoryCode" th:field="*{categoryCode}">
    <option value="">분류를 선택하세요</option>
    <option th:each="category : ${categories}"
            th:value="${category.name()}"
            th:text="${category.label}">일반</option>
  </select>

  <fieldset>
    <legend>태그</legend>
    <label th:each="tag : ${tags}">
      <input type="checkbox" th:field="*{tagCodes}"
             th:value="${tag.code}" />
      <span th:text="${tag.name}">Backend</span>
    </label>
  </fieldset>
</div>

완성 템플릿은 이 fragment를 폼 안에서 교체하므로 부모의 th:object가 셀렉트와 태그 체크박스에도 이어집니다.

src/main/java/board/controls/PostSettingsController.java
package board.controls;

import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/b88/controls/posts/{postId}/settings")
public final class PostSettingsController {
    @ModelAttribute("categories")
    List<PostSettingsForm.PostCategory> categories() {
        return List.of(PostSettingsForm.PostCategory.values());
    }

    @ModelAttribute("tags")
    List<TagOption> tags() {
        return List.of(
                new TagOption("java", "Java"),
                new TagOption("spring", "Spring"));
    }

    @GetMapping
    String edit(@PathVariable long postId, Model model) {
        var form = new PostSettingsForm();
        form.setPublicVisible(true);
        form.setCategory(PostSettingsForm.PostCategory.QUESTION);
        form.setCategoryCode("NOTICE");
        form.setTagCodes(List.of("spring"));
        model.addAttribute("postId", postId);
        model.addAttribute("form", form);
        return "controls/post-settings";
    }

    @PostMapping
    String update(
            @PathVariable long postId,
            @ModelAttribute("form") PostSettingsForm form,
            Model model
    ) {
        model.addAttribute("postId", postId);
        return "controls/post-settings";
    }

    public record TagOption(String code, String name) {}
}

예시 GET의 edit()가 만든 공개 true·태그 spring의 폼은 그 응답의 모델입니다. 원 POST에는 그 폼을 세션이나 조회로 다시 가져오는 코드가 없으므로 새 PostSettingsForm에 전송값을 바인딩합니다. 앞의 “이전 true 유지” 사례는 이미 true인 객체를 대상으로 삼은 단위 바인더 실험입니다. 이 두 전제를 섞으면 marker 없는 POST에서도 GET의 선택이 자동으로 이어진다고 오해할 수 있습니다.

원 POST의 categoryCode는 문자열이라 EXTREME도 바인딩할 수 있지만 category는 열거형이라 같은 값이 typeMismatch가 됩니다. 이 핸들러에는 인접 BindingResult가 없어 그 변환 오류는 메서드 진입 전 예외 경로로 빠집니다. 기본 MVC 처리는 400이며 같은 폼을 반환하는 update()의 성공 재표시와 다릅니다. 오류 값과 선택지를 같은 화면에 돌려주려면 앞 절처럼 오류 객체를 받고 표시 모델을 완성하는 별도 구현이 필요합니다. Spring의 모델 속성 오류 처리와 기본 예외 상태를 함께 확인할 수 있습니다.

등록 실패 후 다시 렌더링할 때 th:field는 결합된 컬렉션과 옵션 값을 비교해 checked와 selected를 복원합니다.

문자열 대소문자나 앞뒤 공백을 암묵적으로 보정하지 않습니다.

코드 정규화가 필요하면 입력 경계에서 한 번만 수행하고 DB의 정규 코드와 비교합니다.

선택지 목록은 화면 표시와 입력 검증이라는 두 역할을 갖습니다.

컨트롤러가 보여 준 선택지가 요청 사이에 비활성화될 수 있으므로 POST 시점에 리포지토리에서 다시 조회해 활성 상태와 현재 사용자의 접근 범위를 확인합니다.

클라이언트가 보낸 코드가 옵션에 있었을 것이라는 가정은 권한 검사가 아닙니다.


알 수 없는 선택 값

src/test/java/board/controls/PostSettingsBindingTest.java
package board.controls;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.web.bind.WebDataBinder;

class PostSettingsBindingTest {
    @Test
    void checkbox_marker는_기존_true를_false로_바꾼다() {
        var form = new PostSettingsForm();
        form.setPublicVisible(true);
        var binder = new WebDataBinder(form, "form");

        binder.bind(new MutablePropertyValues(
                Map.of("_publicVisible", "on")));

        assertThat(form.isPublicVisible()).isFalse();
        assertThat(binder.getBindingResult().hasErrors()).isFalse();
    }

    @Test
    void category_속성들은_같은_이름의_accessor로_바인딩된다() {
        var form = new PostSettingsForm();
        var binder = new WebDataBinder(form, "form");

        binder.bind(new MutablePropertyValues(
                Map.of(
                        "category", "QUESTION",
                        "categoryCode", "NOTICE")));

        assertThat(form.getCategory())
                .isEqualTo(PostSettingsForm.PostCategory.QUESTION);
        assertThat(form.getCategoryCode()).isEqualTo("NOTICE");
        assertThat(binder.getBindingResult().hasErrors()).isFalse();
    }

    @Test
    void 존재하지_않는_enum은_typeMismatch로_남는다() {
        var form = new PostSettingsForm();
        var binder = new WebDataBinder(form, "form");

        binder.bind(new MutablePropertyValues(
                Map.of("category", "EXTREME")));

        assertThat(binder.getBindingResult()
                .getFieldError("category")
                .getCode()).isEqualTo("typeMismatch");
    }

    @Test
    void 반복된_tagCodes는_원본_순서와_중복을_보존한다() {
        var form = new PostSettingsForm();
        var binder = new WebDataBinder(form, "form");
        var values = new MutablePropertyValues();
        values.add("tagCodes", new String[]{"java", "spring", "java"});

        binder.bind(values);

        assertThat(form.getTagCodes())
                .containsExactly("java", "spring", "java");
    }

    @Test
    void tagCodes_marker는_기존_선택을_빈_목록으로_바꾼다() {
        var form = new PostSettingsForm();
        form.setTagCodes(List.of("java"));
        var binder = new WebDataBinder(form, "form");

        binder.bind(new MutablePropertyValues(
                Map.of("_tagCodes", "on")));

        assertThat(form.getTagCodes()).isEmpty();
        assertThat(binder.getBindingResult().hasErrors()).isFalse();
    }
}
src/test/java/board/controls/ControlsTestApplication.java
package board.controls;

import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Import;

@SpringBootConfiguration(proxyBeanMethods = false)
@EnableAutoConfiguration
@Import(PostSettingsController.class)
public class ControlsTestApplication {}
src/test/java/board/controls/PostSettingsMvcRenderTest.java
package board.controls;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import org.jsoup.Jsoup;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.web.servlet.MockMvc;

@SpringBootTest(classes = ControlsTestApplication.class)
@AutoConfigureMockMvc
class PostSettingsMvcRenderTest {
    @Autowired
    MockMvc mvc;

    @Test
    void 수정_화면은_marker와_저장된_선택을_fragment까지_렌더링한다()
            throws Exception {
        var response = mvc.perform(get(
                        "/b88/controls/posts/42/settings"))
                .andExpect(status().isOk())
                .andExpect(view().name("controls/post-settings"))
                .andReturn()
                .getResponse();
        var document = Jsoup.parse(response.getContentAsString(UTF_8));

        assertThat(document.select(
                "form[action='/b88/controls/posts/42/settings']"))
                .hasSize(1);
        assertThat(document.select(
                "input[name=_publicVisible][value=on]")).hasSize(1);
        assertThat(document.select(
                "input[name=publicVisible][checked]")).hasSize(1);
        assertThat(document.select(
                "input[name=category][value=QUESTION][checked]")).hasSize(1);
        assertThat(document.select(
                "select[name=categoryCode] option[value=NOTICE][selected]"))
                .hasSize(1);
        assertThat(document.select(
                "input[name=_tagCodes][value=on]")).isNotEmpty();
        assertThat(document.select(
                "input[name=tagCodes][value=spring][checked]")).hasSize(1);
    }

    @Test
    void marker만_POST하면_체크박스_선택을_비운_상태로_다시_그린다()
            throws Exception {
        var result = mvc.perform(post(
                        "/b88/controls/posts/42/settings")
                        .param("_publicVisible", "on")
                        .param("_tagCodes", "on"))
                .andExpect(status().isOk())
                .andExpect(view().name("controls/post-settings"))
                .andReturn();
        var form = (PostSettingsForm) result.getModelAndView()
                .getModel().get("form");
        var document = Jsoup.parse(
                result.getResponse().getContentAsString(UTF_8));

        assertThat(form.isPublicVisible()).isFalse();
        assertThat(form.getTagCodes()).isEmpty();
        assertThat(document.select(
                "input[name=publicVisible][checked]")).isEmpty();
        assertThat(document.select(
                "input[name=tagCodes][checked]")).isEmpty();
        assertThat(document.select(
                "input[name=_tagCodes][value=on]")).isNotEmpty();
    }
}

단위 바인더 테스트는 HTML 렌더링을 대신하지 않습니다.

MVC 테스트에서는 실제 폼과 marker-only POST를 렌더링하고, 단위 WebDataBinder 테스트에서는 반복 파라미터와 알 수 없는 열거형 값을 보냅니다.

두 단계는 전송값·바인딩·재표시 템플릿의 연결을 검증합니다. 실제 브라우저의 사용자 조작과 전송, 현재 옵션의 활성 상태·권한, 저장 결과까지 실행한 검증은 별도로 필요합니다.


입력 컨트롤 선택 기준

질문체크박스라디오 버튼선택 목록
선택 개수불리언 또는 복수최대 하나하나 또는 multiple
미선택 전송파라미터 없음이름 없음빈 옵션 또는 이름 없음
재표시 기준값 포함 여부값 동일성옵션 값 동일성
대표 위험표시 오해허용 밖 열거형비활성 옵션 변조

“옵션이 세 개니까 라디오 버튼”처럼 모양으로 먼저 고르지 않습니다.

사용자가 반드시 하나를 선택해야 하는지, 선택하지 않음이 유효한 상태인지, 선택지가 얼마나 자주 바뀌는지 결정한 뒤 제어를 고릅니다.

선택지가 수십 개라면 검색 가능한 콤보 상자를 고려하되 키보드와 화면 낭독기 동작을 함께 구현합니다.

민감한 불리언은 체크박스 하나로 즉시 변경하지 않습니다.

공개 전환처럼 영향이 큰 기능은 현재 상태, 변경 결과, CSRF 보호, 소유권을 모두 확인합니다.

Spring의 데이터 바인딩은 값을 옮길 뿐 업무 승인 절차를 제공하지 않습니다.


연습 문제

게시글에 최대 세 개의 태그만 선택할 수 있게 만드세요.

화면에서는 네 번째 선택을 안내하되, 서버는 tagCodes가 네 번 전송되거나 같은 코드가 반복되거나 비활성 태그가 포함된 경우를 각각 거부해야 합니다.

실패 후 사용자가 보낸 선택을 안전하게 다시 보여 주세요.

해설 보기

DTO 검증에서 크기와 중복을 확인하고, 애플리케이션 서비스에서 활성 태그를 조회합니다.

컬렉션을 Set으로 바로 받으면 중복 공격을 조용히 지우므로 원본 List에서 중복 여부를 먼저 검사합니다.

src/main/java/board/controls/TagSelectionPolicy.java
package board.controls;

import java.util.HashSet;
import java.util.List;

public final class TagSelectionPolicy {
    public void validate(List<String> codes) {
        if (codes.size() > 3) {
            throw new IllegalArgumentException("at most three tags");
        }
        if (new HashSet<>(codes).size() != codes.size()) {
            throw new IllegalArgumentException("duplicate tag code");
        }
        if (codes.stream().anyMatch(String::isBlank)) {
            throw new IllegalArgumentException("blank tag code");
        }
    }
}

MVC 테스트는 param("tagCodes", "java", "spring", "java")처럼 반복 값을 그대로 보냅니다.

서비스 테스트에서는 DB에서 하나가 비활성화된 시점을 재현해 오래된 화면도 통과하지 못하게 합니다.

독립 확인

단위 바인더 세 개는 각각 공개 true·태그 [java]인 새 객체로 시작합니다. A는 두 필드와 marker를 모두 생략하고, B는 _publicVisible=on·_tagCodes=on만 보냅니다. C는 두 marker와 publicVisible=true, 반복 tagCodes=java&tagCodes=spring&tagCodes=java를 함께 보냅니다. 공개 여부와 태그 List를 정하세요.

별도 MVC 요청은 원 POST에 categoryCode=EXTREME와 위 반복 태그를 보낸다고 가정합니다. 원 컨트롤러와 연습의 TagSelectionPolicy를 각각 읽고, 바인딩·화면 반환·중복 거부·저장이 실제로 어디까지 일어나는지 설명하세요. 이때 이름을 category로 바꿔 EXTREME을 보내면 같은 결과인가요?

기존 객체·marker·연결된 정책을 구분하는 풀이

A는 값과 marker가 없으므로 기존 true와 [java]를 유지합니다. B는 제어가 있었지만 값이 없다는 marker를 바인더가 처리해 false와 빈 List가 됩니다. C는 실제 값이 있으므로 marker가 그것을 덮어 비우지 않습니다. true와 [java, spring, java]이며 순서·중복을 보존합니다. WebDataBinder의 marker 규칙은 대응 값이 없을 때 초기화한다고 정합니다.

MVC 요청의 categoryCode는 EXTREME 문자열이 되고 반복 태그도 그대로 바인딩됩니다. 원 핸들러는 같은 폼을 200으로 반환하지만 TagSelectionPolicy 호출도 저장도 하지 않습니다. 그 정책을 별도로 호출하면 List 크기는 3이라 첫 검사를 통과하고 중복 검사에서 거부합니다. 코드가 문서에 존재한다는 사실과 요청 경로에 연결됐다는 사실은 다릅니다.

category=EXTREME으로 바꾸면 열거형 변환 오류입니다. 현재 MVC 경로에서는 인접 오류 객체가 없으므로 update()의 재표시까지 도달하지 않고 기본 400으로 처리됩니다. 정확한 오류 화면이나 본문은 추가 어드바이스·설정이 정하는 계약입니다. 위 결과들은 원 코드·기본 MVC 규칙에서 도출한 별도 입력의 예상이며 새 브라우저·MVC 실행 기록은 아닙니다.

다음 문서에서는 화면 문구를 코드와 로케일로 분리하고, 번들 탐색과 대체가 예측 가능하게 작동하도록 만듭니다.