일단
controller Layer를 등록하기 전 제일 먼저 돌아가는 DispatcherServlet을 등록해보자. 우리 어플리케이션에서 유일한 서블릿이다.
(정리가 잘 되어진 글을 찾았다. https://im-recording-of-sw-studies.tistory.com/75 참고하기)
1. DispatcherServlet등록
▶web.xml
<!-- The front controller of this Spring Web application, responsible for handling all application requests -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
이 웹 어플리케이션에서 동작하는 앞단의 컨트롤러이다. 어플리케이션의 모든 요청 해들링에 응답할 수 있다.
서블릿 이름: springDispatcherServlet
초기화 파라미터로 ContextConfigLocation을 받는다. 바로 하위 컨텍스트에 해당하는 컨텍스트이다.
1번째로 시작하도록 설정한다.
<!-- Map all requests to the DispatcherServlet for handling -->
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
핸들링하기 위해 모든 요청을 springDispatcherServlet 에 맵핑한다.
/ : 정적자원을 다루는 자원을 표시하는데, 톰캣(서버)가 가지고 있던 것을 D.S이 가지고 와버렸다. 정적자원에 대한 요청까지도 C.L로 처리해줘야하게 되었다. 그래서 이것을 스프링에게 넘긴다.
두번째로 할 일은 상위 컨텍스트와 하위 컨텍스트를 구분지어 등록해야한다. 하위 컨텍스트는 위 그림에서 파란선 앞단과 대화하기 때문에 디스패쳐 서블릿에 등록한다. (컨텍스트는 그 대상이 시작되는 제일 처음에 걸어놓아야한다. 그래서 앞단에서 제일 먼저 동작하는 디스패쳐 서블릿에 맵핑 걸어 둔 것이다.)
등록한 하위 컨텍스트에 이제 어노테이션을 어떤 것을 받을지 어떤 빈을 어떻게 등록할지를 설정해보자.
2. 하위 컨텍스트 설정하기
▶sevlet-context.xml (우리 어플리케이션의 하위 컨텍스트)
: 여기에 beans: 빈객체 등록, c: 생성자 인젝션 , p: 셋터인젝션, mvc : 웹 이렇게 4개의 네임스페이스를 등록한다.
<mvc:annotation-driven></mvc:annotation-driven>
어노테이션을 통한 기본 전략개체들이 이 태그를 통해 등록되어버린다. (32개정도...) // 기본으로 설정되는 ViewResolver(V.R)에는 prefix와 suffix가 설정되어잇지 않아서
<bean id="IRVR" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/views/"
p:suffix=".jsp"
/>
이렇게 빈으로 I.R.V.R를 수동으로 등록하면서 prefix와 suffix 설정을 넣어준다.
그런데 나중에 우리가 jsp만 view로 사용하지 않는다. 그냥 class(? 클래스라고 하는 것이 맞나?)를 view로 등록할수도 있기 때문에 추가로 bean객체를 더 등록해보겠다.
<bean id="BNVR" class="org.springframework.web.servlet.view.BeanNameViewResolver"
p:order="1"
/>
B.N.V.R은 bean의 이름으로 bean을 찾아서 그 이름을 coc에 따라서 만들어서 viewResolver에게 넘긴다.
이것을 생성자 인젝션으로 주입하고 그 순서를 첫번째로 한다.
위에 I.R.V.R은 프리픽스 서픽스를 붙일 대상을 찾아 붙이고 없으면 그냥 404에러를 내보내기때문에 제일 마지막으로 순서를 설정해주는 것이 좋다. 그래서 integer로 가장 큰 값을 p:order로 기본으로 등록되어있다. 그래서 따로 값을 등록하지 않으면 알아서 마지막으로 설정되기에 그냥 두는 것이 좋다. (Integer의 가장 큰 값은 무엇일까? 2의 16승-1이라고 한다.)
<context:component-scan base-package="kr.or.ddit" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>
context:component는 아래와 같은 관계를 가지고 있다. 우리가 지금 필요한 것은 노란색으로 칠해놓은 4가지 인데 그 의존관계에 의해서 context를 가져오면 나머지 세개도 가져올 수 있기 때문에 컨텍스트로 한번에 등록한다.
context:component-scan 태그를 열고 base-package를 설정한다.
use-default-filters="false" 를 false로 자동 등록을 막아두고 필요한 자원만 include했다.
아래는 컨트롤러를 include하면 딸려오는 아이들이다. 요건 이따 뒤에서 다뤄볼 것이다. 기억하기!!
controller / restcontroller / controlleradvice / restcontrolleradvice
그리고 controllerAdvice도 등록한다. 잘은 모르겠지만 도큐먼트 보면 이렇게 써있다.
{@link #basePackages} 속성의 별칭입니다.
* <p>더 간결한 주석 선언 허용 — 예를 들어,
* {@code @ControllerAdvice("org.my.pkg")}는 다음과 같습니다.
* {@code @ControllerAdvice(basePackages = "org.my.pkg")}.
아무래도 베이스 패키지를 간단하게 등록하는 어노테이션인가보다.
아까 dispatcher Servlet에서 정적자원을 맵핑걸어뒀었다. 그럼 이제 하위 컨텍스트에 관련된 Controller Layer(C.L)에서는 정적자원을 처리해야한다. 그런데 그에 대한 설정이 뿔뿔이 흩어져있으면 관리가 어렵다. 그래서 하위 컨텍스트에 정책을 몰아둬서 관리를 용이하게 해보려고 한다.
<mvc:resources location="/resources/" mapping="/resources/** cache-period="0""></mvc:resources>
정적자원을 하위 컨텍스트 등록하여 한곳에서 관리하기 때문에 정적자원의 정책이 흩어지지 않고 몰아서 사용할 수 있다.
이제 상위 컨텍스트 차례!
3. 상위 컨텍스트도 등록하자
▶web.xml
<!-- needed for ContextLoaderListener -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/*-context.xml</param-value>
</context-param>
<!-- Bootstraps the root web application context before servlet initialization -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
상위 컨텍스트를 일단 web.xml에 등록한다. 하위 컨텍스트는 서블릿의 맵핑부분에 등록했지만 상위컨텍스는 <context-param> 태그로 등록한다.
이름을 contextConfigLocation 값을 로케이션을 정해준다. -context.xml로 끝나는 저 위치에 있는 모든 파일들을 가리킨다.
서블릿을 초기화하기 전에 루트 웹 어플리케이션 컨텍스트를 부트스트랩한다.......부트스트랩한다??
리고 이벤트 리스너를 등록한다.
▶root-context.xml
요건이제 맨 위에 그림에서 파란선 오른쪽부분을 담당한다. 모델과 그 뒷단....
여기서 네임스페이스는 beans, c, p, util(: property파일 쓰려구,.,,)을 쓴다.
<context:component-scan base-package="kr.or.ddit" >
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
<context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice"/>
</context:component-scan>
상위와 마찬가지로 context:component-scan을 등록하고 베이스 패키지를 등록한다.
여기서는 컨트롤러랑 view만 빼면 되니까 use-default-filter는 true 기본값으로 그냥 두기위해 그냥 설정하지 않을 것이고, exclude로 controller annotation 은 빼도록 한다.
그런데... controllerAdvice는 왜 빼는건지...? 일단 진도좀 빼고 찾아봐야겠다....
<util:properties id="appInfo" location="classpath:kr/or/ddit/appInfo.properties" />
요건 프로퍼티스 등록할때 요렇게 사용한다. 앞에 클래스패스 붙여서 클래스패스 리소스임을 명시한다.
<bean id="filterMultipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
p:maxInMemorySize="#{appInfo.maxInMemorySize}"
p:maxUploadSize="#{appInfo.maxUploadSize}"
p:maxUploadSizePerFile="#{appInfo.maxUploadSizePerFile}"
/>
위에 등록한 프로퍼티파일에는 리소스들의 주소와 그 아이디를 값과 키로 등록해뒀다. 그래서 값들을 멀티파트 리졸버에서 사용하기위해 생성자 인젝션으로 주입했다. #{}이 표현은 뭐게~? 스프링에서의 EL이다! 헷갈리면안된다! #이다!
복습끝
자바에서는 이벤트처리구조를 리스너를 만들어 처리한다.
DS이 먼저 작동하기 위해서 리스너가 필요하다.
서버가 구동이 되면 이벤트핸들러가 작동해야한다.
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
ContextLoaderListener : Bootstrap listener to start up and shut down Spring's root WebApplicationContext.
웹 어플리케이션 컨텍스트 (: 웹 컨테이너) 시작하고 종료되는 일을 한다.=>서버의 생명주기와 동일하다.
ServletContextListener : Interface for receiving notification events about ServletContext lifecycle changes.
서블릿 컨텍스트의 생명주기에 대해 이벤트 노티피케이션을 받는 인터페이스.
위에 리스너를 선택해서 추가해보자. 컨텍스트의 생명주기와 연결되니, 컨텍스트에 달아줘야한다.
<!-- needed for ContextLoaderListener -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/*-context.xml</param-value>
</context-param>
<!-- Bootstraps the root web application context before servlet initialization -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
1.아까 만들어두었던 컨텍스트를 찾았다.
2. 리스너를 추가한 것이다., 즉 이 컨텍스트는 이 컨텍스트가 시작되면 작동하는 리스너이다.
어노테이션을 통해 스프링은 강제로 private을 풀어버린다. 그래서 강제로 주입해버린다. 편하긴하지만 내가 정한 룰에 따라 받을 수 없다. 그래서 내가 정한 룰대로 하려면 셋터와 생성자를 설정해주는 것이 좋다.
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {
HttpStatus 상수로 이미 관리되고 있다. value가 코드어시스트에 나온 것을 보니 싱글벨류로 쓸수 있다. 그래서 "code"지우고 그냥 HttpStatus.BAD_REQUEST로 추가함. resp에 있던 것들과 같은 것들이 다 있다.
미션! << injection구조를 만들어라 ==> new라는 keyword를 싹 지워라. >>
@Controller
public class MemberListController{
@Inject
private MemberService service;
@RequestMapping("/member/memberList.do")
public String memberList(
@RequestParam(value="page", required = false, defaultValue = "1") int currentPage
, @ModelAttribute("simpleCondition") SimpleCondition simpleCondition
, Model model
){
Pagination<MemberVO> pagination = new Pagination(3, 2);
pagination.setCurrentPage(currentPage);
pagination.setSimpleCondition(simpleCondition);
List<MemberVO> memList = service.retrieveMemberList(pagination);
pagination.setDataList(memList);
model.addAttribute("pagination", pagination);
// pagination.setRenderer(new DefaultPaginationRenderer());
return "member/memberList";
}
}
스프링이 다 가공해서 넣어주는데 우리가 req를 받는다면 안된다.
return값은 한번에 하나만 보낼 수 있는 한계점을 해결하기 위해 콜바이레퍼런스를 사용. model 객체에 넣는다. 어댑터가 다시 꺼내서 사용할 수 있다. 양손에 모델과 리턴을 가지고 있는 어댑터.
req한테 넣어준 model은 2개이다. @ModelAttribute이랑 model.addAttribute("pagination", pagination);
@Controller
public class MemberViewController{
@Inject
private MemberService service;
@RequestMapping("/member/memberView.do")
public ModelAndView memView(@RequestParam("who") String memId) {
// 1. 중복
MemberVO member = service.retrieveMember(memId);
ModelAndView mav = new ModelAndView();
mav.addObject("member", member);
mav.setViewName("member/memberView");
return mav;
// req.setAttribute("member", member);
// return "member/memberView";
}
콜바이레퍼런스를 쓰지 않고도 model과 view를 어댑터에게 줄 수 있다.
@Controller
@RequiredArgsConstructor
public class MemberInsertController{
private final MemberService service;
어차피 MemberService는 싱글턴으로 관리되기 때문에 final로 injection 주입하면 객체가 먼저 생성되어야한다. 그래서 생성자가 필수로 필요하다.
@no 기본
@all 모두
그래서 @RequiredArgsConstructor (: final로 되어있는 전역변수만 생성자로 받는 생성자.)를 사용한다.
이것을 생성하면 컨트롤러 생성시에 하나밖에 없는 서비스를 꼭 생성해야한다. 그렇기 때문에 인젝트 없이도 그냥 바로 주입되어 생성된다.
@GetMapping("/member/memberInsert.do")
public String insertForm() {
return "member/memberForm"; // logical view name 전체 뷰의 path가 아니다.
}
@PostMapping("/member/memberInsert.do")
public String insert(
명확하게 getMapping도 가능하다.
@RequestMapping(method = RequestMethod.GET)
public @interface GetMapping {
meta 어노테이션으로 들어있기 때문
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
requestMapping의 타겟 주의 ==> TYPE에도 사용가능 : 클래스에도 사용가능
@RequestMapping("/member")
public class MemberInsertController{
클래스에 사용한 예//
@GetMapping("memberInsert.do")
public String insertForm() {
return "member/memberForm"; // logical view name 전체 뷰의 path가 아니다.
}
@PostMapping("memberInsert.do")
public String insert(
반복되는 /member를 클래스 클래스쪽으로 올려줄 수 있다.
잠깐! CommandObject제대로 사용하기위해 만들어보기 !
public void setMemImage(MultipartFile memImage) throws IOException {
if(memImage==null || memImage.isEmpty()) return;
this.memImage = memImage;
this.memImg = memImage.getBytes();
}
memberVO는 커맨드 어브젝트로도 사용된다.
memImg는 db와 소통용. private MultipartFile memImage; 추가하면 좀더 완벽한 C.O로 사용 가능
핸들러 어뎁터가 memImg로 만들어둔 memImage의 셋터 호출하여 사용할 수 있도록 할 것이다.
이미지가 있다면 바이트 배열로 바꿔서, img에 미리 넣어둔다. 그러면 컨트롤러에서 이 이미지를 넣는 과정이 삭제된다.
// 원래있던 memImg : c <- db
private byte[] memImg;
// 클라이언트한테 받을 때 사용할 memImage : c -> db
private MultipartFile memImage;
// memImage가 들어오면 셋터를 작동할때 바이트배열로 memImg가 자동으로 생성되도록 할 수 있다.
public void setMemImage(MultipartFile memImage) throws IOException {
if(memImage==null || memImage.isEmpty()) return;
this.memImage = memImage;
this.memImg = memImage.getBytes();
}
// memImg가 있을때 이 getter를 호출하면
// 이진데이터 텍스트로 인코딩하여 바로 c쪽에서 쓸 수 있도록 해준다.
public String getBase64MemImg() {
if(memImg==null) {
return null;
}else {
return Base64.getEncoder().encodeToString(memImg);
}
}
@RequestMapping("/member/memberInsert.do")
public class MemberInsertController{
private final MemberService service;
@GetMapping
public String insertForm() {
return "member/memberForm"; // logical view name 전체 뷰의 path가 아니다.
}
@PostMapping
public String insert(
이렇게 넘겨버릴 수도 있다.
mypage, MemberUpdateController 시큐리티 스프링으로 나중에 바꿔야한다.
, @RequestParam String password
@ModelAttribute("input") MemberVO input 이거 이름 의미없다 우리 리다이렉션이동할거라 >> MemberVO input
@PostMapping("loginProcess")
public String loginProcess(
HttpSession session
, MemberVO input
, RedirectAttributes redirectAttributes
) {//login그룹추가하거나 여기의 vo파람을 따로따로 받거나
if(session.isNew()) {
throw new BadRequestException("세션이 처음 만들어진거면 넌 누구냐!!!");
}
boolean valid = ValidateUtils.validate(input, new LinkedHashMap<String, List<String>>(), LoginGroup.class);
String viewName = null;
if(valid) {
try{
MemberVO saved = service.authenticate(input);
session.setAttribute("authMember", saved);
viewName = "/";
}catch (AuthenticateException e) {
String message = "로그인 실패";
//꺼내는순간 바로 삭제.
redirectAttributes.addFlashAttribute("message", message);
viewName = "/login/loginForm";
}
}else {
//꺼내는순간 바로 삭제.
redirectAttributes.addFlashAttribute("message", "필수 파라미터 누락");
viewName = "/login/loginForm";
}
return "redirect:" + viewName;
}
RedirectAttributes redirectAttributes.addFlashAttribute("message", message); //꺼내는순간 바로 삭제.
▶loginForm.jsp 에서도 <c:remove var="message" scope="session"/> 제거가능!!
MultipartFilter :
Servlet Filter that resolves multipart requests via a MultipartResolver. in the root web application context.
Looks up the MultipartResolver in Spring's root web application context. Supports a "multipartResolverBeanName" filter init-param in web.xml; the default bean name is "filterMultipartResolver".
버전에 따라 서블릿 버전을 건드리는 것이 아니라 MultipartResolver를 갈아끼우는 것
<bean id="filterMultipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
p:maxInMemorySize="10240"
p:maxUploadSize="#{1024*1024*100}"
p:maxUploadSizePerFile="#{1024*1024*10}"
/>
서블릿스펙 2점대에서 사용하겠다.
web.xml에서 multipartconfig지우고, 이제는 multipartresolver에서 관리한다.
어플리케이션은 어떤 멀티파트 리졸버가 쓰이는지 전혀 신경쓸 필요가 없다.
2.5나 3점대나 파일 다루는 방식이 같아진다. 그러니 따로 있을 필요 없어서 파일업로드 하나 지움...!
<bean id="filterMultipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
p:maxInMemorySize="#{appInfo.maxInMemorySize}"
p:maxUploadSize="#{appInfo.maxUploadSize}"
p:maxUploadSizePerFile="#{appInfo.maxUploadSizePerFile}"
/>
# file upload setting
maxInMemorySize=10240
maxUploadSize=102400000
maxUploadSizePerFile=10240
appinfo에 등록하고 하드코딩 지우기
model만 보내버리면?
RequestToViewNameTranslator 전략을 이용해서 url로부터 정보를 찾아낸다.
@RequestMapping("/buyer/buyerView.do")
return "buyer/buyerView"; --> /와 .do를 지우면 view가 찾아진다.
public void setBuyerImage(MultipartFile buyerImage) throws IOException { // c->db
if(buyerImage==null || buyerImage.isEmpty()) return;
this.buyerImage = buyerImage;
this.buyerImg = buyerImage.getBytes();
}
public String getBase64BuyerImg() { // c<-db
if(buyerImg==null) {
return null;
}else {
return Base64.getEncoder().encodeToString(buyerImg);
}
}
@PostMapping
public String insert(
@ModelAttribute("buyer") BuyerVO buyer
// , @RequestPart(value="buyerImage", required=false) MultipartFile buyerImage
// , @RequestPart(value="contractFile", required=false) MultipartFile contractFile
, Model model
) throws IOException {
// buyer.setBuyerImage(buyerImage);
// buyer.setContractFile(contractFile);
download처리를 뷰로 넘기자.
BeanNameViewResolver
한가지 역할을 하는 전략체가 여러개라면 반드시 순서를 정해야한다.
InternalResourceViewResolver 자기가 찾고나서 못찾으면 404로 넘김. 그래서 순서를 생략하면 2의 16승 -1로 등록해버림. 인티저의 가장 큰 값.
AttatchFileVO atchFile = (AttatchFileVO)model.get("atchContract");
이름 하드코딩
public static final String DOWNTARGETNAME = "downloadTarget"; 상수설정하고
▶ContractDownloadController
model.addAttribute(AttatchDownloadView.DOWNTARGETNAME,atchContract); 상수로 이름 저장
@Value("#{appInfo.contracts}")
private File saveFolder;
경로 하드코딩
▶AttatchFileVO 에 private File atchFile; 추가
▶ContractDownloadController에 atchContract.setAtchFile(new File(saveFolder, atchContract.getAtchSaveName()));
▶AttatchDownloadView에 File saveFile = atchFile.getAtchFile();로 변경
// json content -> json response
@RequestMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) //produces : accept
@ResponseBody
public CalculateVO case2(@RequestBody CalculateVO vo) {
return vo;
}
순수하게 데이터를 요청하고 응답할때 json, xml을 사용한다. -> REST, RESTfurl uri
/member(GET), /member/a001(GET), /member/a001(PUT)
rest요청에 대한 응답ㅇ느 대부분 json.xml로 나간다.
@Controller
@ResponseBody
public @interface RestController {
컨트롤러이고, 마샬링을 무조건 할거야.
@Controller
@RequestMapping("/calculator/case3")
public class CalculatorController_Case3 {
// parameter content -> json response
@RequestMapping
public String case1(@ModelAttribute("calculator") CalculateVO vo, Model model) {
model.addAttribute("prop1", "샘플");
model.addAttribute("prop2", 232);
return "jsonView";
}
// json content -> json response
// public case2() {
//
// }
}
마샬링의 대상이 vo가 아니라 전체 다가 된다.
다음주!!!!!
spring과 mybatis
spring과 h/v
연결하는 것 할것이다.
미션!!!!!!!!!!!!!!
HR_MNG : 스프링 프로젝트 셋팅부터 다시해봐라.
'내가 보려고 정리하는 > Spring' 카테고리의 다른 글
웹 : 전략객체에게 검증 넘기기, Tiles사용해보기 : 0411 (0) | 2023.04.11 |
---|---|
웹 : datasource-context : 0410 (1) | 2023.04.10 |
웹 : Spring , java, web : 0406(3) (0) | 2023.04.06 |
웹 : Spring : Annotation기반 : 0406(2) (0) | 2023.04.06 |
웹 : Spring : 컨테이너의 Bean 관리 특성 : 0406 (0) | 2023.04.06 |