스프링 MVC 동작 원리
M 모델: 평범한 자바 객체 POJO V 뷰: HTML. JSP, 타임리프, … C 컨트롤러: 스프링 @MVC
MVC 패턴의 장점
- 동시 다발적(Simultaneous) 개발 - 백엔드 개발자와 프론트엔드 개발자가 독립적으로 개발을 진행할 수 있다.
- 높은 결합도 - 논리적으로 관련있는 기능을 하나의 컨트롤러로 묶거나, 특정 모델과 관련있는 뷰를 그룹화 할 수 있다.
- 낮은 의존도 - 뷰, 모델, 컨트롤러는 각각 독립적이다.
- 개발 용이성 - 책임이 구분되어 있어 코드 수정하는 것이 편하다.
- 한 모델에 대한 여러 형태의 뷰를 가질 수 있다.
MVC 패턴의 단점
- 코드 네비게이션 복잡함
- 코드 일관성 유지에 노력이 필요함
- 높은 학습 곡선
서블릿
서블릿 (Servlet)
- 자바 엔터프라이즈 에디션은 웹 어플리케이션 개발용 스팩과 API제공
- 요청 당 쓰레드 (만들거나, 풀에서 가져다가) 사용
- 그 중에서 가장 중요한 클래스 중 하나가 HttpServlet
장점 (CGI에 비해)
- 빠르다.
- 플랫폼 독립적
- 보안
- 이식성
서블릿 엔진 또는 서블릿 컨테이너가 하는 일
- 세션 관리
- 네트워크 서비스
- MIME기반 메시지 인코딩, 디코딩
- 서블릿 생명 주기 관리
서블릿 생명 주기
- 서블릿 컨테이너가 서블릿 인스턴스의 init() 메소드를 호출하여 초기화한다.
- 최초 요청 시 한번 초기화하고 이 과정 생략
- 서블릿이 초기화가 된 다음부터 클라이언트의 요청을 처리할 수 있다. 각 요청은 별도의 쓰레드로 처리하고 이 때 서블릿 인스턴스의 service() 메소드를 호출한다.
- 이 안에서 HTTP요청을 받고 클라이언트로 보낼 HTTP응답을 만든다.
- service()는 보통 HTTP Method에 따라 doGet(), doPost()등으로 처리를 위임한다.
- 따라서 보통 doGet() 또는 doPost()를 구현한다.
- 서블릿 컨테이너의 판단에 따라 해당 서블릿을 메모리에서 내려야 할 시점에 destory()를 호출한다.
서블릿 리스너와 필터
서블릿 리스너
- 웹 애플리케이션에서 발생하는 주요 이벤트를 감지하고 각 이벤트에 특별한 작업이 필요한 경우에 사용할 수 있다
서블릿 필터
- 들어온 요청을 서블릿으로 보내고, 또 서블릿이 작성한 응답을 클라이언트로 보내기 전에 특별한 처리가 필요한 경우에 사용할 수 있다.
실행 순서
서블릿 리스너 -> 서블릿 필터 -> 서블릿 … -> 서블릿 종료 -> 서블릿 필터 종료 -> 서블릿 리스너 종료
스프링 IoC컨테이너 연동
스프링을 사용한다는 이야기는 크게 2가지 의미
- 스프링이 제공하는 IoC Container를 사용하겠다.
- 스프링 MVC를 사용하겠다.
ContextLoaderListener
- 서블릿 리스너 구현체
- ApplicationContext를 만들어 준다.
- ApplicationContext를 서블릿 컨텍스트 라이프사이클에 따라 등록하고 소멸시켜준다.
- 서블릿에서 IoC 컨테이너를 ServletContext를 통해 꺼내 사용할 수 있다.
서블릿에서 IoC컨테이너 ContextLoaderListener를 사용해서 웹어플리케이션 컨텍스트를 만들고
그것을 서블릿에서 사용하는 방법
web.xml에 정의 -> 설정방법이 좀 어려워서 이렇게 정리하고 끝내자 (필요하면 강의 찾아보자)
스프링 MVC와 연동
DispatcherServlet
- 스프링 MVC의 핵심.
- Front Controller 역할을 한다.
DispatcherServlet 동작 원리
스프링 MVC1 강의 정리 - 김영한강의 정리할 때 잘 정리해놔서 이거 참고
DispatcherServlet 초기화
- 다음의 특별한 타입의 빈들을 찾거나, 기본 전략에 해당하는 빈들을 등록한다. ● HandlerMapping: 핸들러를 찾아주는 인터페이스
- HandlerAdapter: 핸들러를 실행하는 인터페이스
- HandlerExceptionResolver
- ViewResolver
- …
DispatcherServlet 동작 순서
- 요청을 분석한다. (로케일, 테마, 멀티파트 등)
- (핸들러 맵핑에게 위임하여) 요청을 처리할 핸들러를 찾는다.
- (등록되어 있는 핸들러 어댑터 중에) 해당 핸들러를 실행할 수 있는 “핸들러 어댑터”를 찾는다.
- 찾아낸 “핸들러 어댑터”를 사용해서 핸들러의 응답을 처리한다.
- 핸들러의 리턴값을 보고 어떻게 처리할지 판단한다.
- 뷰 이름에 해당하는 뷰를 찾아서 모델 데이터를 랜더링한다.
- @ResponseBody가 있다면 Converter를 사용해서 응답 본문을 만들고.
- (부가적으로) 예외가 발생했다면, 예외 처리 핸들러에 요청 처리를 위임한다.
- 최종적으로 응답을 보낸다.
스프링 MVC구성 요소
DispatcherSerlvet의 기본 전략
- DispatcherServlet.properties
MultipartResolver
- 파일 업로드 요청 처리에 필요한 인터페이스
- HttpServletRequest를 MultipartHttpServletRequest로 변환해주어 요청이 담고 있는 File을 꺼낼 수 있는 API 제공.
LocaleResolver
- 클라이언트의 위치(Locale) 정보를 파악하는 인터페이스
- 기본 전략은 요청의 accept-language를 보고 판단.
ThemeResolver
- 애플리케이션에 설정된 테마를 파악하고 변경할 수 있는 인터페이스
- 참고: https://memorynotfound.com/spring-mvc-theme-switcher-example/
HandlerMapping
- 요청을 처리할 핸들러를 찾는 인터페이스
HandlerAdapter
- HandlerMapping이 찾아낸 “핸들러”를 처리하는 인터페이스
- 스프링 MVC 확장력의 핵심 HandlerExceptionResolver
- 요청 처리 중에 발생한 에러 처리하는 인터페이스
RequestToViewNameTranslator
- 핸들러에서 뷰 이름을 명시적으로 리턴하지 않은 경우, 요청을 기반으로 뷰 이름을 판단하는 인터페이스
ViewResolver
- 뷰 이름(string)에 해당하는 뷰를 찾아내는 인터페이스
FlashMapManager
- FlashMap 인스턴스를 가져오고 저장하는 인터페이스
- FlashMap은 주로 리다이렉션을 사용할 때 요청 매개변수를 사용하지 않고 데이터를 전달하고 정리할 때 사용한다.
- redirect:/events
결국엔 (굉장히 복잡한) 서블릿
=> DispatcherServlet
DispatcherServlet 초기화
- 특정 타입에 해당하는 빈을 찾는다.
- 없으면 기본 전략을 사용한다. (DispatcherServlet.properties)
스프링 부트 사용하지 않는 스프링 MVC
- 서블릿 컨네이너(ex, 톰캣)에 등록한 웹 애플리케이션(WAR)에 DispatcherServlet을 등록한다.
- web.xml에 서블릿 등록
- 또는 WebApplicationInitializer에 자바 코드로 서블릿 등록 (스프링 3.1+, 서블릿 3.0+)
- 세부 구성 요소는 빈 설정하기 나름.
스프링 부트를 사용하는 스프링 MVC
- 자바 애플리케이션에 내장 톰캣을 만들고 그 안에 DispatcherServlet을 등록한다.
- 스프링 부트 자동 설정이 자동으로 해줌.
- 스프링 부트의 주관에 따라 여러 인터페이스 구현체를 빈으로 등록한다.
스프링 MVC 설정
@EnableWebMvc
애노테이션 기반 스프링 MVC를 사용할 때 편리한 웹 MVC 기본 설정
@Configuration
@EnableWebMvc
public class WebConfig {
}
WebMvcConfigurer 인터페이스
@EnableWebMvc가 제공하는 빈을 커스터마이징할 수 있는 기능을 제공하는 인터페이스
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/", ".jsp");
}
}
Spring MVC
WebApplicationInitialazer설정 or web.xml 설정 후 Dispatcher servlet설정하고
위의 설정이 기본 Spring MVC설정 방법
Spring Boot
- application.properties
- @Configuration + Implements WebMvcConfigurer: 스프링 부트의 스프링 MVC 자동설정 + 추가 설정
- @Configuration + @EnableWebMvc + Imlements WebMvcConfigurer: 스프링 부트의 스프링 MVC 자동설정 사용하지 않음.
Formatter
- Parser: 어떤 문자열을 (Locale 정보를 참고하여) 객체로 어떻게 변환할 것인가
- Printer: 해당 객체를 (Locale 정보를 참고하여) 문자열로 어떻게 출력할 것인가
포매터 추가하는 방법 1
- WebMvcConfigurer의 addFormatters(FormatterRegistry) 메소드 정의
포매터 추가하는 방법 2 (스프링 부트 사용시에만 가능 함)
- 해당 포매터를 빈으로 등록 (@Component)
Converter는 객체 <-> 객체
Tip!!
@WebMvcTest -> web과 관련된 빈만 등록 @SpringBootTest -> 모든 빈 등록
핸들러 인터셉터
HandlerInterceptor
- 핸들러 맵핑에 설정할 수 있는 인터셉터
- 핸들러를 실행하기 전, 후(아직 랜더링 전) 그리고 완료(랜더링까지 끝난 이후) 시점에 부가 작업을 하고 싶은 경우에 사용할 수 있다.
- 여러 핸들러에서 반복적으로 사용하는 코드를 줄이고 싶을 때 사용할 수 있다.
- 로깅, 인증 체크, Locale 변경 등…
boolean preHandle(request, response, handler)
- 핸들러 실행하기 전에 호출 됨
- “핸들러”에 대한 정보를 사용할 수 있기 때문에 서블릿 필터에 비해 보다 세밀한 로직을 구현할 수 있다.
- 리턴값으로 계속 다음 인터셉터 또는 핸들러로 요청,응답을 전달할지(true) 응답 처리가 이곳에서 끝났는지(false) 알린다.
void postHandle(request, response, modelAndView)
- 핸들러 실행이 끝나고 아직 뷰를 랜더링 하기 이전에 호출 됨
- “뷰”에 전달할 추가적이거나 여러 핸들러에 공통적인 모델 정보를 담는데 사용할 수도 있다. ● 이 메소드는 인터셉터 역순으로 호출된다.
- 비동기적인 요청 처리 시에는 호출되지 않는다.
void afterCompletion(request, response, handler, ex)
- 요청 처리가 완전히 끝난 뒤(뷰 랜더링 끝난 뒤)에 호출 됨
- preHandler에서 true를 리턴한 경우에만 호출 됨
- 이 메소드는 인터셉터 역순으로 호출된다.
- 비동기적인 요청 처리 시에는 호출되지 않는다.
리소스 핸들러
이미지, 자바스크립트, CSS 그리고 HTML 파일과 같은 정적인 리소스를 처리하는 핸들러 등록하는 방법
HTTP메시지 컨버터
HTTP 메시지 컨버터
- 요청 본문에서 메시지를 읽어들이거나(@RequestBody), 응답 본문에 메시지를 작성할 때(@ResponseBody) 사용한다.
괄호는 의존성 설정 필요
- 바이트 배열 컨버터
- 문자열 컨버터
- Resource 컨버터
- Form 컨버터 (폼 데이터 to/from MultiValueMap<String, String>)
- (JAXB2 컨버터)
- (Jackson2 컨버터)
- (Jackson 컨버터)
- (Gson 컨버터)
- (Atom 컨버터)
- (RSS 컨버터)
스프링 부트를 사용하지 않는 경우
- 사용하고 싶은 JSON 라이브러리를 의존성으로 추가
- GSON
- JacksonJSON
- JacksonJSON 2
스프링 부트를 사용하는 경우
- 기본적으로 JacksonJSON 2가 의존성에 들어있다.
- 즉, JSON용 HTTP 메시지 컨버터가 기본으로 등록되어 있다.
그밖에 WebMvcConfigurer 설정
CORS 설정
- Cross Origin 요청 처리 설정
- 같은 도메인에서 온 요청이 아니더라도 처리를 허용하고 싶다면 설정한다.
리턴 값 핸들러 설정
- 스프링 MVC가 제공하는 기본 리턴 값 핸들러 이외에 리턴 핸들러를 추가하고 싶을 때 설정한다.
아큐먼트 리졸버 설정
- 스프링 MVC가 제공하는 기본 아규먼트 리졸버 이외에 커스텀한 아규먼트 리졸버를 추가하고 싶을 때 설정한다.
뷰 컨트롤러
- 단순하게 요청 URL을 특정 뷰로 연결하고 싶을 때 사용할 수 있다.
비동기 설정
- 비동기 요청 처리에 사용할 타임아웃이나 TaskExecutor를 설정할 수 있다.
뷰 리졸버 설정
- 핸들러에서 리턴하는 뷰 이름에 해당하는 문자열을 View 인스턴스로 바꿔줄 뷰 리졸버를 설정한다.
Content Negotiation 설정
- 요청 본문 또는 응답 본문을 어떤 (MIME) 타입으로 보내야 하는지 결정하는 전략을 설정한다.
스프링 MVC 설정은 즉 DispatcherServlet이 사용할 여러 빈 설정.
- HandlerMapper
- HandlerAdapter
- ViewResolver
- ExceptionResolver
- LocaleResolver
일일히 등록하려니 너무 많고, 해당 빈들이 참조하는 또 다른 객체들까지 설정하려면 설정할게 너무 많다.
@EnableWebMvc
- 애노테이션 기반의 스프링 MVC 설정 간편화
- WebMvcConfigurer가 제공하는 메소드를 구현하여 커스터마이징할 수 있다. 스프링 부트
- 스프링 부트 자동 설정을 통해 다양한 스프링 MVC 기능을 아무런 설정 파일을 만들지 않아도 제공한다.
- WebMvcConfigurer가 제공하는 메소드를 구현하여 커스터마이징할 수 있다. - @EnableWebMvc를 사용하면 스프링 부트 자동 설정을 사용하지 못한다. 스프링 MVC 설정 방법
- 스프링 부트를 사용하는 경우에는 application.properties 부터 시작.
- WebMvcConfigurer로 시작
- @Bean으로 MVC 구성 요소 직접 등록
스프링 MVC 활용
HTTP요청 매핑
- @RequestMapping(method=RequestMethod.GET)
- @RequestMapping(method={RequestMethod.GET, RequestMethod.POST})
- @GetMapping, @PostMapping
RequestMapping은 따로 지정하지 않으면 모든 메서드 허용
잘못된 요청을 해 매핑이 되지 않는 경우 405에러
GET
- 캐싱 가능
- 브라우저 기록이 남는다.
- Bookmark사용이 가능
POST
- 캐시 x, 브라우저 기록 x, 북마크 x
- 데이터 길이 제한이 없다.
PUT
- URI에 해당하는 데이터를 새로 만들거나 수정할 때 사용한다.
- Post와 다른점은 URL에 대한 의미가 다르다
- POST는 URI는 보내는 데이터를 처리할 리소스를 지칭하며
- PUT의 URI는 보내는 데이터에 해당하는 리소스를 지칭한다
PATCH
- PUT과 비슷하지만, 기존 엔티티와 새 데이터의 차이점만 보낸다는 차이가 있다.
DELETE
- URI에 해당하는 리소스를 삭제할 때 사용한다.
HEAD
- GET 요청과 동일하지만 응답 본문을 받아오지 않고 응답 헤더만 받아온다.
OPTIONS
- 사용할 수 있는 HTTP Method 제공
- 서버 또는 특정 리소스가 제공하는 기능을 확인할 수 있다.
- 서버는 Allow 응답 헤더에 사용할 수 있는 HTTP Method 목록을 제공해야 한다.
미디어 타입 맵핑
특정한 타입의 데이터를 담고 있는 요청만 처리하는 핸들러
- @RequestMapping(consumes=MediaType.APPLICATION_JSON_UTF8_VALUE)
- Content-Type 헤더로 필터링
- 매치가 되지 않는 경우 415 Unsupported Media Type 응답
특정한 타입의 응답을 만드는 핸들러
- @RequestMapping(produces=”application/json”)
- 매치 되지 않는 경우에 406 Not Acceptable 응답
헤더와 매개 변수
특정한 헤더가 있는 요청을 처리하고 싶은 경우
- @RequestMapping(headers = “key”)
특정한 헤더가 없는 요청을 처리하고 싶은 경우
- @RequestMapping(headers = “!key”)
특정한 헤더 키/값이 있는 요청을 처리하고 싶은 경우
- @RequestMapping(headers = “key=value”)
특정한 요청 매개변수 키를 가지고 있는 요청을 처리하고 싶은 경우
- @RequestMapping(params = “a”)
특정한 요청 매개변수가 없는 요청을 처리하고 싶은 경우
- @RequestMapping(params = “!a”)
특정한 요청 매개변수 키/값을 가지고 있는 요청을 처리하고 싶은 경우
- @RequestMapping(params = “a=b”)
핸들러 메소드
핸들러 안에서 사용할 수 있는 메소드 아규먼트 타입과 리턴타입이 굉장히 다양하다
@PathVariable
- 요청 URI 패턴의 일부를 핸들러 메소드 아규먼트로 받는 방법.
@RequestParam
- 요청 매개변수에 들어있는 단순 타입 데이터를 메소드 아규먼트로 받아올 수 있다.
- 값이 반드시 있어야 한다.
- Map<String, String> 또는 MultiValueMap<String, String>에 사용해서 모든 요청 매개변수를 받아 올 수도 있다.
- form data도 @RequestParam으로 받을 수 있다.
- RequestParam은 생략이 가능하다.
@ModelAttribute
- URI 패스, 요청 매개변수, 세션 등 있는 단순 타입 데이터를 복합 타입 객체로 받아오거나 해당 객체를 새로 만들 때 사용할 수 있다.
- 생략이 가능하다
- @ModelAttribute는 세션에 있는 데이터도 바인딩한다.
예시
@PostMapping("/event")
@ResponseBody
// public Event getEvent(@ModelAttribute Event event) {
public Event getEvent(Event event) {
return event;
}
값을 바인딩 할 수 없는 경우
- BindException 발생 400 에러
바인딩 에러를 직접 다루고 싶은 경우
- BindingResult 타입의 아규먼트를 바로 오른쪽에 추가한다.
바인딩 이후에 검증 작업을 추가로 하고 싶은 경우
- @Valid 또는 @Validated 애노테이션을 사용한다.
@SessionAttribute
Tip!
- Web기능 테스트 할 때 MockMvc사용
예시 코드
@Autowired
MockMvc mockMvc;
@Test
public void createEvent() throws Exception {
mockMvc.perform(
post("/events")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaType.APPLICATION_JSON)
).andExpect(status().isOk());
}
@SessionAttribute
세션에 있는 값을 바로 읽어올 수 있다.
예제에선 Interceptor를 활용해 session에 localdatetime을 넣어주고 실행
@PostMapping("/event")
public Event getEvent(Model model, @SessionAttribute LocalDateTime visitTime) {
return event;
}
RedirectAttributes
SpringMVC에서는 Redirect될 때 자동으로 설정된 값에 의해서 model값이 담겨서 쿼리 파라미터로 자동으로 담긴다
SpringBoot는 그 설정을 꺼져있다
이 경우 RedirectAttributes에 설정해 넘겨준다. (Query Parameter에 담긴다)
@PostMapping("/event/form/limit")
public Event eventsFormLimitSubmit(@Validated @ModelAttribute Event evnt,
BindingResult bindingResult,
SessionStatus sessionStatus,
RedirectAttributes attributes) {
attributes.addAttribute("name", event.getName());
attributes.addAttribute("limit", event.getLimit());
return "redirect:/events/list;
}
Flash Attributes
주로 리다이렉트시에 복합적인 데이터를 전달할 때 사용한다.
RedirectAttributes에 보면은 addFlashAttribute()
메소드가 존재한다.
인자값으로 객체를 넣는데 바로 HTTP 세션에 들어간다.
넘어가고 나서는 세션에서 제거가 된다 -> 1회성 데이터
URL Path에 붙기 때문에 전부 String으로 변환이 가능해야 한다.
Model안에 담겨 꺼낼 수 있다.
@PostMapping("/event/form/limit")
public Event eventsFormLimitSubmit(@Validated @ModelAttribute Event evnt,
BindingResult bindingResult,
SessionStatus sessionStatus,
RedirectAttributes attributes) {
attributes.addFlashAttribute("newEvent", event);
return "redirect:/events/list;
}
@PostMapping("/event/list")
public Event eventsFormLimitSubmit(@Validated @ModelAttribute Event evnt,
Model model) {
return "/events/list";
}
- 데이터가 URI에 노출되지 않는다.
- 임의의 객체를 저장할 수 있다.
- 보통 HTTP 세션을 사용한다.
리다이렉트 하기 전에 데이터를 HTTP 세션에 저장하고 리다이렉트 요청을 처리 한 다음 그 즉시 제거한다. RedirectAttributes를 통해 사용할 수 있다.
MultipartFile
MultipartFile
- 파일 업로드시 사용하는 메소드 아규먼트
- MultipartResolver 빈이 설정 되어 있어야 사용할 수 있다. (스프링 부트 자동 설정이 해 줌)
- POST multipart/form-data 요청에 들어있는 파일을 참조할 수 있다.
- List
아큐먼트로 여러 파일을 참조할 수도 있다.
<form method="POST" enctype="multipart/form-data" action="#" th:action="@{/file}"> File: <input type="file" name="file"/>
<input type="submit" value="Upload"/>
</form>
@PostMapping("/file")
public String uploadFile(@RequestParam MultipartFile file, RedirectAttributes attributes) {
String message = file.getOriginalFilename() + " is uploaded.";
System.out.println(message);
attributes.addFlashAttribute("message", message);
return "redirect:/events/list";
}
ResponseEntity
파일 리소스를 읽어오는 방법
- 스프링 ResourceLoader 사용하기
파일 다운로드 응답 헤더에 설정할 내용
- Content-Disposition: 사용자가 해당 파일을 받을 때 사용할 파일 이름
- Content-Type: 어떤 파일인가
- Content-Length: 얼마나 큰 파일인가
파일의 종류(미디어 타입)을 알아내는 방법
- Tika라이브러리 사용
ResponseEntity
- 응답 상태 코드
- 응답 헤더
- 응답 본문
@PostMapping("/file")
public String uploadFile(@RequestParam MultipartFile file, RedirectAttributes attributes) {
String message = file.getOriginalFilename() + " is uploaded.";
System.out.println(message);
attributes.addFlashAttribute("message", message);
return "redirect:/events/list";
}
@RequestBody
@RequestBody
- 요청 본문(body)에 들어있는 데이터를 HttpMessageConveter를 통해 변환한 객체로 받아올 수 있다.
- @Valid 또는 @Validated를 사용해서 값을 검증 할 수 있다.
- BindingResult 아규먼트를 사용해 코드로 바인딩 또는 검증 에러를 확인할 수 있다.
@ResponseBody & ResponseEntity
@ResponseBody
- 데이터를 HttpMessageConverter를 사용해 응답 본문 메시지로 보낼 때 사용한다.
- @RestController 사용시 자동으로 모든 핸들러 메소드에 적용 된다.
- @ResponseBody를 명시하지 않고 객체만 써도 적용된다는 의미
ResponseEntity
- 응답 헤더 상태 코드 본문을 직접 다루고 싶은 경우에 사용한다.
@InitBinder
특정 컨트롤러에서 바인딩 또는 검증 설정을 변경하고 싶을 때 사용
@InitBinder
public void initEventBinder(WebDataBinder webDataBinder) {
webDataBinder.setDisallowedFields("id");
}
위 코드에서는 id를 form에서 보내주더라도 걸러준다.
바인딩 설정
- webDataBinder.setDisallowedFields();
포매터 설정
- webDataBinder.addCustomFormatter();
Validator 설정
- webDataBinder.addValidators()
@ExceptionHandler
- MVC에서 요청을 처리하다가 에러를 직접 발생하거나
- JAVA에서 발생하는 예외가 발생했을 떄 정의한 Handler로 처리해서 응답을 어떻게 보내줄지 정의할 수 있다.