김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
Jar
Jar를 사용하면 항상 내장 서버( 톰캣 등 )를 사용하고, webapp 경로도 사용하지 않고 내장 서버 사용에 최적화 되어 있는 기능
JSP를 사용하지 않는 경우, Jar를 사용하는 것이 좋음
스프링부트에 Jar 를 사용하면 /resources/static/
위치에 index.html
파일을 두면 Welcome 페이지로 처리해준다
War
톰캣 같은 WAS를 별도로 설치하고 빌드한 파일을 넣을 때 사용
JSP를 사용하는 경우 War를 사용
War를 사용하면 내장 서버도 사용 가능하지만 주로 외부 서버에 배포하는 목적으로 사용
spring-boot-starter 라이브러리를사용하면 spring-boot-starter-logging 라이브러리가 함께 포함
SLF4J : 원래는 많은 로그 라이브러리가 있는데 그것을 통합해서 인터페이스로 제공
SLF4J 인터페이스와 이를 구현한 Logback을 주로 사용
@Slf4j
@RestController
public class LogTestController {
// 로그 선언
// private final Logger log = LoggerFactory.getLogger(getClass());
@RequestMapping("/log-test")
public String logTest() {
String name = "Spring";
log.trace("trace log={}", name);
log.debug("debug log={}", name);
log.info("info log={}", name);
log.warn("warn log={}", name);
log.error("error log={}", name);
return "ok";
}
}
@Slf4j
를 붙여주면 로그 선언 없이 로그 사용 가능@RestController
@Controller
문자를 반환하면 view 이름으로 인식 ( ➜ view를 찾고 렌더링됨 )
@Controller
를 사용하면서 메소드 위에 @ResponseBody
를 사용하면 String을 반환해도 @RestController
처럼 동작하게 됨
@ResponseBody
trace > debug > info > warn > error
application.properties
에서 로그 레벨을 설정할 수 있음
logging.level.프로젝트이름.패키지=레벨
패키지와 하위 로그 레벨 설정
레벨이 trace이면 trace, debg 등 모든 하위 레벨 로그가 출력됨
debug로 설정하면 trace 제외한 모든 로그가 출력됨
logging.level.root=레벨
전체 로그 레벨 설정
레벨의 디폴트는 info
@RequestMapping
배열을 이용해서 2개 이상의 url을 매핑할 수 있다
method 속성으로 HTTP 메서드를 지정하지 않으면 HTTP 메서드와 무관하게 무조건 호출된다
method 지정 시, url은 맞지만 method에 맞지 않는 요청이 들어오면 스프링 MVC는 405 상태코드를 반환한다
method 속성을 부여하는 대신 @GetMapping
처럼 축약해서 사용 가능
@GetMapping
내부를 살펴보면 @RequestMapping
에 method 속성이 RequestMethod.GET
으로 설정되어 있음
즉, @RequestMapping(value = "url", method = RequestMethod.GET)
= @GetMapping("url")
@PathVariable
URL 자체에 값이 들어가 있는 경우, 파라미터에서 @PathVariable
을 사용해 꺼낼 수 있음
@GetMapping("/mapping/{userId}")
이면 @PathVariable("userId") String userId
경로 변수의 이름 ( URL을 통해 들어오는 값의 이름 )과 메서드에서 사용되는 파라미터 이름이 같으면 생략할 수 있다
@PathVariable("userId") String userId
➜ @PathVariable String userId
@GetMapping(value = "/mapping-param", params = "mode=debug")
mode=debug
라는 파라미터가 있어야 메소드가 실행됨
mapping-param?mode=debug
@GetMapping(value = "/mapping-header", headers = "mode=debug")
// Content-Type 지정
// 맞지 않으면 415 상태 코드 반환
@PostMapping(value = "/mapping-consume", consumes = "application/json")
// Accept 지정
// 안되면 406 상태 코드 반환
@PostMapping(value = "/mapping-produce", produces = "text/html")
어노테이션 기반의 컨트롤러는 헤더를 조회하기 위해 다양한 파라미터를 받아들일 수 있음
HttpServletRequest
HttpServletResponse
HttpMethod
: HTTP 메서드를 조회
Locale
: Locale 정보 ( 언어 정보 )를 조회
@RequestHeader MultiValueMap<String, String> headerMap
: 모든 HTTP 헤더를 MultiValueMap
형식으로 조회
@RequestHeader("헤더이름") String host
: 특정 HTTP 헤더를 조회
@CookieValue(value = "쿠키이름", required = false) String cookie
: 특정 쿠키 조회
MultiValueMap
하나의 키에 여러 값을 받을 수 있음
HTTP header, HTTP 쿼리 파라미터와 같이 하나의 키에 여러 값을 받을 때 사용
get("keyA")
으로 키의 값을 꺼내면 배열로 반환된다
List<>
@Controller
에서 사용 가능한 파라미터 목록 : https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments
HttpServletRequest
사용쿼리 파라미터, HTML Form은 형식이 같기 때문에 동일한 메서드로 조회 가능
HttpServletRequest
의 request.getParameter()
를 사용해서 조회
HttpServletResponse
의 response.getWriter().write()
를 사용해서 응답
@RequestParam
사용@RequestParam("파라미터이름") 반환형 변수명
요청 파라미터에서 데이터를 읽을 때 사용
파라미터 이름과 변수명이 동일하다면 파라미터 이름을 생략 가능
@RequestParam 반환형 변수명
파라미터 이름과 변수명이 동일하고 String, int, Integer 등의 단순타입이면 @RequestParam
까지 생략 가능
속성
defaultValue
파라미터 값이 넘어오지 않은 경우의 기본값을 설정
파라미터 값으로 빈 문자열이 들어와도 기본값으로 설정된다
required
파라미터 필수 여부 ( default는 true )
들어오지 않으면 400 상태 코드 반환
false이고 값이 들어오지 않으면 null이 들어오는데 이 때 int 형이면 오류 ( 500 상태 코드 반환 )
위의 경우라면 Integer를 사용해야함 ( 객체에는 null이 들어갈 수 있기 때문에 )
모든 파라미터 Map으로 조회
@RequestParam Map<String, Object> paramMap
: 요청 파라미터의 value가 1개인 경우에 사용
@RequestParam MultiValueMap<String, Object> paramMap
: 하나의 파라미터에 값이 2개 이상인 경우
get("파라미터 이름")
을 통해 조회
@ResponseBody
클래스에 @Controller
가 붙어 있는 경우, 특정 메서드에서 문자를 그대로를 반환하고 싶다면 메서드 레벨에 @ResponseBody
어노테이션을 붙인다
반환되는 문자를 view 이름으로 인식하지 않고, HTTP message body에 직접 내용을 입력한다
@ModelAttribute
사용요청 파라미터로 받은 값을 이용해 객체를 만드는 경우에 사용
@RequestParam
으로 받아서 직접 객체를 생성할 수 있지만 @ModelAttribute
를 사용하면 이 과정을 스프링이 자동으로 처리
단> 객체 클래스에 @Data
어노테이션이 붙어 있어야 가능하다
@Data
는 @Getter
, @Setter
, @ToString
, @EqualsAndHashCode
, @RequiredArgsConstructor
를 자동으로 적용해준다@ModelAttribute
명시된 객체를 만든다 ➜ 요청 파라미터의 이름으로 객체의 프로퍼티를 찾는다 ➜ setter()를 호출하여 요청 파라미터의 값을 객체에 주입한다
@ModelAttribute 클래스이름 객체이름
@ModelAttribute
를 생략할 수 있음
@RequestParam
으로 인식하고 나머지는 @ModelAttribute
로 인식한다스프링 MVC 1편 2번 게시글의 5-4 ~ 5-6 번 참고
요청 파라미터로 데이터가 넘어오는 방식과는 다르게 message body를 통해 데이터가 넘어오는 경우 @Requestparam
, @ModelAttribute
를 사용할 수 없음
요청 파라미터를 조회 : @RequestParam
, @ModelAttribute
HTTP 메시지 바디를 직접 조회 : @RequestBody
@PostMapping("/request-body-string-v1")
public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
...
}
message body의 데이터는 InputStream
을 사용해서 직접 읽을 수 있음
getInputStream()
: message body의 내용을 바이트 코드로 반환
StreamUtils.copyToString()
: 위의 메서드로 얻은 stream( inputStream )을 String 으로 변환
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException {
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
...
}
InputStream : HTTP 요청 메시지 바디의 내용을 직접 조회
Writer : HTTP 응답 메시지의 바디에 직접 결과 출력
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException {
String messageBody = httpEntity.getBody();
log.info("message body={}", messageBody);
return new HttpEntity<>("ok");
}
HttpEntity<>
는 HTTP header, body 정보를 편리하게 조회할 수 있는 객체
getHeaders()
, getBody()
HttpEntity<>
로 응답도 가능
메세지 바디에 정보를 직접 넣는다
헤더 정보를 포함할 수 있다
HttpEntity를 상속받은 객체들
RequestEntity<>
ResponseEntity<>
@RequestBody
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException {
log.info("message body={}", messageBody);
return "ok";
}
@RequestBody
: HTTP 요청 메세지의 body를 조회
@RequestHeader
: 요청 메세지의 헤더를 조회할 때 사용
@ResponseBody
: 응답 결과(반환되는 값)를 HTTP 메시지 바디에 직접 담아서 전달
HttpEntity
, @RequestBody
를 사용하면 스프링 MVC 내부에서 HTTP message body를 읽어서 문자나 객체로 변환해서 전달해준다
이 때, HTTP 메세지 컨버터 기능을 사용
위에서 String으로 지정했으니 String으로 변환된다
단> HttpEntity는 getBody()
를 통해 내용을 꺼내와야 함
@PostMapping("/request-body-json-v1")
public void requestBodyJsonV1(HttpServletRequest request, HttpServletResponse response) throws IOException {
ServletInputStream inputStream = request.getInputStream();
String messageBody = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
...
}
HttpServletRequest를 사용해 직접 HTTP message body에서 데이터를 읽어와 문자로 변환
문자로 된 JSON 데이터를 Jackson 라이브러리인 objectMapper
를 사용해서 자바 객체로 변환
@PostMapping("/request-body-json-v2")
public String requestBodyJsonV2(@RequestBody String messageBody) throws IOException {
HelloData helloData = objectMapper.readValue(messageBody, HelloData.class);
...
}
@RequestBody
로 데이터를 문자로 읽어온다 ➜ 객체로 변환@PostMapping("/request-body-json-v3")
public String requestBodyJsonV3(@RequestBody HelloData helloData) throws IOException {
log.info("messageBodey = {}", helloData);
log.info("username = {}, age = {}", helloData.getUsername(), helloData.getAge());
...
}
@RequestBody
에 객체를 지정하면 HTTP 메세지 컨버터가 message body의 내용을 객체로 변환시켜준다
@RequestBody
는 생략 불가능
@RequestParam
, @ModelAttribute
가 적용됨@PostMapping("/request-body-json-v4")
public String requestBodyJsonV4(HttpEntity<HelloData> httpEntity) throws IOException {
HelloData helloData = httpEntity.getBody();
...
}
HttpEntity
도 원하는 형식으로 받아올 수 있지만 getBody()를 통해 꺼내와야 한다@ResponseBody
@PostMapping("/request-body-json-v5")
public HelloData requestBodyJsonV5(@RequestBody HelloData data) {
log.info("username={}, age={}", data.getUsername(), data.getAge());
return data;
}
@ResponseBody
는 반환되는 값을 직접 message body에 넣어주는데, 문자 뿐만 아니라 객체를 message body에 넣어줄 수도 있다
@RequestBody
요청
@ResponseBody
응답
스프링(서버)에서 응답 데이터를 만드는 방법
정적 리소스
view template
message body에 직접 입력
웹 브라우저에 정적인 HTML, css, js를 제공할 때 사용
해당 파일을 변경 없이 그대로 제공
resources/static
하위에 있는 파일들이 정적 리소스 파일
웹 브라우저에 동적인 HTML을 제공할 때 view template을 사용
뷰 템플릿을 거쳐서 HTML이 생성되고, 뷰가 응답을 만들어서 전달
/resources/templates
하위에 있는 파일들이 뷰 템플릿 파일
@RequestMapping("/response-view-v1")
public ModelAndView responseViewV1() {
ModelAndView mav = new ModelAndView("response/hello")
.addObject("data", "hello!");
return mav;
}
response/hello
뷰 템플릿 이름 ( view의 논리이름으로 ModelAndView 객체 생성 )
위의 경로로 view resolver가 실행되어서 view를 찾고 랜더링
addObject("data", "hello!")
data = model 이름 ( 타임리프에서 data 라는 이름의 모델에서 값을 꺼내서 사용 )
"hello!" = model에 들어가는 값
@RequestMapping("/response-view-v2")
public String responseViewV2(Model model) {
model.addAttribute("data", "hello!!");
return "response/hello";
}
클래스 레벨에 @Controller
가 붙어있는 경우, String을 반환하면 view의 논리 이름으로 인식
위 메서드에 @ResponseBody
를 붙이면 message body에 "response/hello" 라는 문자열이 그대로 입력된다
@RequestMapping("/response/hello")
public void responseViewV3(Model model) {
model.addAttribute("data", "hello!!");
}
@Controller
를 사용하고 응답 메세지 바디를 처리하는 파라미터 ( HttpServletResponse, Writer )가 없으면 요청 URL을 참고해 view의 논리 이름으로 사용
요청 경로와 view의 논리 이름이 같은 경우, void 반환형 가능
요청경로가 /response/hello
이면 view의 논리 이름을 response/hello
로 인식
권장하지 않는 방식
// 문자 처리
@GetMapping("/response-body-string-v1")
public void responseBodyV1(HttpServletResponse response) throws IOException {
response.getWriter().write("ok");
}
ResponseEntity<>
@GetMapping("/response-body-string-v2")
public ResponseEntity<String> responseBodyV2() throws IOException {
return new ResponseEntity<>("ok", HttpStatus.OK);
}
HttpEntity는 HTTP 메시지의 헤더, 바디 정보를 가지고 있다
HttpEntity를 상속받은 ResponseEntity 는 HTTP 응답 코드를 설정할 수 있다
@ResopnseBody
@ResponseBody
@GetMapping("/response-body-string-v3")
public String responseBodyV3() {
return "ok";
}
@ResponseBody
를 사용하면 view를 사용하지 않고, HTTP 메시지 컨버터를 통해 HTTP 메시지를 직접 입력ResponseEntity<>
// JSON 처리
@GetMapping("/response-body-json-v1")
public ResponseEntity<HelloData> responseBodyJsonV1() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return new ResponseEntity<>(helloData, HttpStatus.OK);
}
어노테이션 이용
@ResponseStatus(HttpStatus.OK)
@ResponseBody
@GetMapping("/response-body-json-v2")
public HelloData responseBodyJsonV2() {
HelloData helloData = new HelloData();
helloData.setUsername("userA");
helloData.setAge(20);
return helloData;
}
@ResponseBody
를 사용하면 응답 코드 설정이 어렵기 때문에 @ResponseStatus(HttpStatus.OK)
어노테이션을 사용해 응답 코드 설정HTTP message body에서 JSON 데이터를 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다
HttpMessageConverter는 인터페이스이고 이를 구현한 클래스가 많이 존재한다
스프링 MVC는 아래의 경우에 HTTP message Converter를 자동으로 적용한다
@RequestBody
, HttpEntity<RequestEntity>
@ResponseBody
, HttpEntity<ResponseEntity>
응답의 경우 클라이언트의 HTTP Accept 해더와 서버의 컨트롤러 반환 타입 정보를 조합해서 HttpMessageConverter
가 선택
@ResponseBody
를 사용
message body에 문자 내용을 직접 반환
viewResolver 대신에 HttpMessageConverter
가 동작
기본 문자처리: StringHttpMessageConverter
기본 객체처리: MappingJackson2HttpMessageConverter
byte 처리 등등 기타 여러 HttpMessageConverter
가 기본으로 등록되어 있음
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
HTTP Message Converter는 요청과 응답 둘 다 사용된다
canRead()
, canWrite()
: 메시지 컨버터가 해당 클래스, 미디어타입을 지원하는지 체크
read()
, write()
: 메시지 컨버터를 통해서 메시지를 읽고 쓰는 기능
스프링부트는 대상 클래스 타입과 미디어 타입을 체크해서 사용 여부를 결정
Content-Type
만족하지 않으면 다음 메시지 컨버터로 우선순위가 넘어간다
0순위 : ByteArrayHttpMessageConverter
byte[] 데이터를 처리
클래스 타입 : byte[] , 미디어타입 : */*
( 모든 미디어 타입을 받아 들일 수 있음 )
응답 시 쓰기 미디어 타입 : application/octet-stream
1순위 : StringHttpMessageConverter
String 문자로 데이터를 처리
클래스 타입 : String , 미디어타입 : */*
응답 시 쓰기 미디어 타입 : text/plain
2순위 : MappingJackson2HttpMessageConverter
application/json
클래스 타입 : 객체 또는 HashMap , 미디어타입 : application/json
관련
응답 시 쓰기 미디어 타입 : application/json
HTTP 요청이 온 후 컨트롤러에서 @RequestBody
, HttpEntity
파라미터를 사용한다고 가정하면
메시지 컨버터가 메시지를 읽을 수 있는지 확인하기 위해 canRead()
를 호출
대상 클래스 타입을 지원하는가 ( @RequestBody
의 대상 클래스 )
HTTP 요청의 Content-Type을 지원하는가
canRead()
조건을 만족하면 read()
를 호출해서 객체 생성하고, 컨트롤러의 파라미터로 반환
컨트롤러에서 @ResponseBody
, HttpEntity
로 값이 반환된다고 가정하면
메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite()
를 호출
대상 클래스 타입을 지원하는가 ( return의 대상 클래스 )
HTTP 요청의 Accept 미디어 타입을 지원하는가 ( 더 정확히는 @RequestMapping
의 produces )
canWrite()
조건을 만족하면 write()
를 호출해서 HTTP 응답 메시지 바디에 데이터를 생성
@Controller
public class RequestBodyStringController {
@PostMapping("/request-body-string-v1")
public void requestBodyStringV1(HttpServletRequest request, HttpServletResponse response) throws IOException { ... }
@PostMapping("/request-body-string-v2")
public void requestBodyStringV2(InputStream inputStream, Writer responseWriter) throws IOException { ... }
@PostMapping("/request-body-string-v3")
public HttpEntity<String> requestBodyStringV3(HttpEntity<String> httpEntity) throws IOException { ... }
@ResponseBody
@PostMapping("/request-body-string-v4")
public String requestBodyStringV4(@RequestBody String messageBody) throws IOException { ... }
}
어노테이션 기반 컨트롤러에서 파라미터로 HttpServletRequest, Model, @RequestParam, @RequestBody, HttpEntity 등을 사용했는데 이것을 사용할 수 있으려면 누군가가 데이터를 해당 파라미터에 맞게 전달해주어야 한다
이런 것을 처리해주는 것이 바로 ArgumentResolver
이다
어노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapter
는 바로 이 ArgumentResolver
를 호출해서 컨트롤러가 필요로 하는 다양한 파라미터의 값(객체)을 생성하고 모든 파라미터의 값이 준비되면 컨트롤러를 호출하면서 값을 넘겨준다
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
ArgumentResolver의 정확한 이름은 HandlerMethodArgumentResolver
핸들러( Controller )가 필요로 하는 파라미터의 값( 객체 )을 생성하는 역할을 수행
RequestMappingHandlerAdapter가 호출함으로써 실행되고, 생성한 객체를 RequestMappingHandlerAdapter에게 넘겨준다
supportsParameter()
: 핸들러( Controller )가 받아야 하는 파라미터 정보를 지원하는지 판단
지원하는 경우, resolveArgument()
를 통해 객체를 만들어서 반환한다
스프링은 30개가 넘는 ArgumentResolver 를 기본으로 제공
RequestMappingHandlerAdapter는 @RequestMapping
을 사용하는 어노테이션 기반의 컨트롤러에서 사용되는 핸들러 어댑터
RequestMappingHandlerAdapter 가 ArgumentResolver 를 호출
supportsParameter()
를 호출해서 해당 파라미터를 지원하는지 체크지원한다면 resolveArgument()
를 호출해서 핸들러( Controller )가 필요로 하는 객체를 생성
이렇게 생성된 객체들이 컨트롤러 호출 시 넘어가게 된다
public interface HandlerMethodReturnValueHandler {
boolean supportsReturnType(MethodParameter returnType);
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
ReturnValueHandler : 핸들러가 값을 반환할 때 응답 값을 변환하고 처리해준다
핸들러에서 String으로 view 이름을 반환해도 동작하는 이유가 ReturnValueHandler 덕분
스프링은 ModelAndView
, @ResponseBody
, HttpEntity
, String
등을 처리하는 10여 개의 ReturnValueHandler가 있다
@RequestBody
와 @ResponseBody
를 컨트롤러에서 사용하는데 이들은 모두 HttpMessageConverter 를 사용한다
즉, @RequestBody
, @ResponseBody
, HttpEntity
를 사용하는 경우 ArgumentResolver와 ReturnValueHandler 가 메세지 컨버터를 사용한다
요청 시
@RequestBody
, HttpEntity
등을 처리하는 서로 다른 ArgumentResolver 가 있다 ( 여러 개 존재 )
ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한 객체를 생성한다
응답 시
@ResponseBody
, HttpEntity
등을 처리하는 서로 다른 ReturnValueHandler 가 있다 ( 여러 개 존재 )
ReturnValueHandler 들이 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다
참고
HttpEntityMethodProcessor
: HttpEntity 가 있을 때 사용하는 ArgumentResolver
RequestResponseBodyMethodProcessor
: @RequestBody, @ResponseBody 가 있을 때 사용하는 ArgumentResolver
스프링은 ArgumentResolver, ReturnValueHandler, HttpMessageConverter 를 모두 인터페이스로 제공하기 때문에 필요한 경우 기능을 확장할 수 있다
실제로 기능 확장할 일은 많지 않지만 필요하다면 WebMvcConfigurer
를 상속받아서 스프링 빈으로 등록하면 된다