BackEnd/Spring

SpringMVC_Parameter_국비_Day83

Leo.K 2022. 6. 29. 16:45

* 일반적인 다이나믹 웹 프로젝트는 contextPath가 프로젝트 이름이 되지만, spring legacy프로젝트는 초기에 프로젝트를 생성하는 경우 설정한 최상위 패키지가 com.ict.myapp이라고 한다면, myapp이 contextPath가 된다. 

[ Spring MVC Request LifrCycle ]

사용자의 요청 -> Filter -> DispatcherServlet(Main Controller) -> 전처리(파일에 대한 전처리) -> HandlerMapping (요청에 담긴 URL을 보고 작업을 분류한다.) -> HandlerInterceptor(실제 작업 처리를 진행하기 전에 보안 처리, 로그인 여부 확인 등) -> Handler(action controller), buisnesslogic 처리 -> main controller에게 view이름만 리턴 -> 포워드의 경우viewResolver(포워드할 뷰에 대한 접두어(절대경로), 접미어(확장자) 붙이기), 리다이렉트의 경우 maincontroller가 바로 응답 -> view에게 리턴 -> view가 maincontroller에게 리턴 -> 사용자 응답  

 

[ MVC 동작구조 ]

root-content.xml -> 프로젝트가 실행하기 전에 필요한 Bean을 미리 생성한다. 

servlet-context.xml -> 실제로 작업을 하는 작업 컨트롤러, viewresolver, resource접근 등 생성

사용자는 작업 컨트롤러만 직접 코딩하고 나머지는 환경설정으로 생성된 서블릿이 알아서 진행한다.

src/test/~ : 하나의 모듈에 대한 단위테스트를 위한 공간

[ MVC 초기화1 ]

보안상의 문제로 뷰(jsp파일)은 외부에 노출하지 않는다고 했던 것을 기억하는가? 이로 인해 간단한 로그인 폼을 호출할 때도 포워딩 서블릿을 거쳐서 호출했었다. 이러한 이유가 스프링에서도 적용이 되는데, 외부에서는 접근할 수 없도록 WEB-INF파일에 뷰에 관련된 resource파일을 저장하고, 내부에서는 포워딩을 통해 WEB-INF에 접근할 수 있기 때문에 뷰 파일을 WEB-INF에 저장하는 것이다.

DispatcherServlet이 만들어 질때 servlet-context.xml참고하면서 세가지를 초기화 한다.

  1. resource파일의 저장위치 
  2. 작업 컨트롤러가 반환한 뷰의 이름에다가 접두어로 경로, 접미어로 확장자를 붙인다. 
  3. 작업 컨트롤러를 생성한다.

 

Annotation또한 인터페이스이며, 상속이 가능하다. 

각 클래스의 역할에 따라서 적절할 Annotation을 사용해야 초기 환경설정에서 클래스가 생성이 될 수 있다.

@Component는 초기에 컨트롤러가 자동생성이 되지만, Controller의 기능을 수행하지 못한다. 따라서 작업 컨트롤러로 지정하는 경우에는 @Controller를 사용해서 자동생성이 되고 Controller의 기능을 사용할 수 있다. 상속구조는 아래와 같다. 즉, Component를 상속받는 인터페이스는 자동생성 조건에 적용된다.

@Component
  ㄴ@Controller
  ㄴ@Service : Buisness Logic
  ㄴ@Repository : DAO

[ 생성 순서 ]

의존성을 주입하는 DI는 프로젝트가 실행 되기 전에 사전 작업으로 이루어지기 때문에 생성순서가 중요하다. DI의 순서대로 컨트롤러를 생성해야 한다.

DI 순서(이 순서로 생성해야 한다.)

MyBatis -> DAO -> Service -> Action Controller

필자는 밑의 파라미터를 수신하는 과정과 MVC의 요청 처리 방식이 헷갈려서 한 번 정리하겠다. 

요청 처리 방식에서 사용하는 예제는 서버로 전달된 파라미터는 없고, 단순한 비즈니스 로직을 처리한 후에 그 결과를 DispatcherServlet이 사용하는 임시객체 Model에 담아 반환한 것이다. 

파라미터를 수신하는 예제는 서버로 전달된 파라미터가 있을 때, 작업 컨트롤러는 특정 작업을 수행하기 위해 필요한 파라미터를 메서드의 인자로 요청한다. 그러면 DispatcherServlet초기에 생성될 때, 메서드의 이름과 인자 정보를 모두 읽어오게 되는데, 이 메서드를 호출하게 되는 일이 생기면, 해당 메서드가 요구하는 데이터를 담을 임시객체를 생성해서 이 객체에 서버로 전달된 파라미터를 담아서 메서드를 호출할 때 같이 전달하는 것이다. 결론적으로 파라미터 수신까지 DispatcherServlet이 해주니 개발자는 이 값을 사용해서 비즈니스 로직만 처리하면 된다,,,

전자에서 언급된 임시 객체는 작업 컨트롤러의 처리결과를 담는 임시객체, 후자에서 언급된 임시객체는 특정 메서드를 호출할 때, 해당 메서드가 요구하는 인자값을 충족하기 위해 DispatcherServlet 서버로 전달된 파라미터를 수신하여 저장한 임시객체 이다.  

[ MVC 요청 처리 방식1 ] - view와 data를 따로 전송

  1. /test.do라는 사용자의 요청이 들어오면, DispatcherServlet이 이를 수신한다.
  2. DispatcherServlet이 URL을 확인해서 Handler Mapping에게 작업분류를 시키고 Handler Mapping이 URL에 있는 패턴을 어노테이션으로 가지는 작업 컨트롤러를 호출(invoke)한다. 
  3. 작업 컨트롤러는 Model에 출력내용을, 리턴값으로 뷰 정보(jsp파일 이름)를 DispatcherServlet에게 준다. 
    1. Model은 DispatcherServlet이 만들어서 사용하는 임시저장객체를 호출된 작업 컨트롤러가 사용할 수 있도록 DispatcherServlet이 제공하는 인터페이스(사용설명서)이다. Model을 통해서 전달된 데이터를 DispatcherServlet이 처리한다. 
  4. DispatcherServlet이 뷰 정보를 수신하면 ViewResolver에게 경로와 확장자를 설정하도록 한다.
    1. 포워드 하는 경우 DispatcherServlet이 request binding시키고 포워드 시킨다.
    2. 리다이렉트 하는 경우 Model을 통해 전달받은 데이터를 파라미터로 사용한다. 
      1. return "redirect:list.do" => list.do?msg=안녕하세요;

[ MVC 요청 처리 방식2 ] - view와 data를 하나로 묶어서 전송

1과 전반적인 과정은 동일하다. 작업 컨트롤러의 반환값을 2개로 나누어서 하느냐, 하나의 객체로 묶어서 하느냐의 차이일 뿐이다. 객체를 넘기더라도 객체를 분해하여 필요한 데이터를 추출하는 작업 또한 DispatcherServlet이 해준다...(신인가,,)

클라이언트에서 전달해주는 파라미터를 받는 방법을 학습해보자. 총 3가지 방법을 소개하겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<form>
    <table border="1">
        <tr>
            <th>이름</th>
            <td><input name="name" value="일길동"></td>
        </tr>
        <tr>
            <th>나이</th>
            <td><input name="age"  value="20"></td>
        </tr>
        <tr>
            <th>전화번호</th>
            <td><input name="tel"  value="010-111-1234"></td>
        </tr>
        <tr>
            <td colspan="2" align="center">
                <input type="button" value="낱개로 받기"  onclick="send1(this.form);"> 
                <input type="button" value="객체로 받기"  onclick="send2(this.form);"> 
                <input type="button" value="Map으로 받기" onclick="send3(this.form);"> 
            </td>
        </tr>
    </table>
</form>
cs

[ 파라미터 받기1 ]

파라미터를 낱개로 따로따로 받는 방법

위에서부터 거듭 강조하면서 말하지만, 작업을 분류하고 작업을 지시하며, 작업 결과를 반환받는 객체는 모두 DispatcherServlet이라고 했다. 따라서 호출하는 메소드의 파라미터는 DispatcherServlet이 제공하는 것이고, 작업을 담당하는 컨트롤러에게 내가 준 객체를 사용하고 처리 한 결과만 나에게 줘~라고 하는 것이다. 여기서 하나의 어노테이션을 더 알아보자.

@RequestParam : 클라이언트단에서 전달한 파라미터 값에 대해 꼬리표를 달아주는 것이다. 속성으로는 value, required가 있는데, required속성은 기본값이 true이다. true(필수 입력 o), false(필수 입력x)

만일 클라이언트단에서 전달하는 파라미터이름과 메서드의 인자명이 다르다면 아래와 같이 파라미터를 사용한다. 이를 의역하면 DispatcherServlet이 파라미터명이 name인 값을 URL에서 읽어서 irum이라는 인자에 담아서 제공해줄테니, 비즈니스 로직을 처리한 결과를 반환해달라는 뜻이다. 

마지막으로 한 번 더 언급하고 가자면, 작업 컨트롤러의 작업 결과를 반환하면 DispatcherServlet이 3가지 방법으로 응답을 하는데, forward, redirect, @ResponseBody라는 어노테이션이 붙은 경우이다. 앞의 2개는 익숙하니 마지막 하나에 대해서 설명해보겠다.

@ResponseBody를 사용하면 작업 결과를 반환받는 DispatcherServlet에게 이건 포워드나 리다이렉트 하지 말고 바로 클라이언트단에 응답해줘~라고 작업 컨트롤러가 남기는 꼬리표이다. DispatcherServlet은 이를 인식해서 바로 출력을 하는데, 이 때 이 내용이 한글이라면, 인코딩의 문제로 깨질 수가 있다. 이를 해결하기 위해 아래와 같이 produces속성을 사용하는 것이다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    //낱개로 받기
    // /person/insert1.do?name=일길동&age=20&tel=010-111-1234
    //  parameter를 받을 메서드의 인자값은 DispatcherServlet에 대한 요구사항이다. 
    //  ㄴ-> 파라미터명이 name이라는 값을 가져와서 인자에 있는 변수에 넣어서 DispatcherServlet이 메서드에게 전달한다.
    // 클라이언트단에서 넘긴 파라미터의 이름과 다르다면, @RequestParam을 파라미터에 선언하면 인식할 수 있다
    // parameter명 == 받을 변수명 => 어노테이션 생략가능, 다르면 생략 불가능. 
    // @RequestParam의 속성 required : 필수 입력(true) <=> 선택 입력(false)
    
    // @responsebody를 통해 인코딩을 하지 않는 경우 한글이 깨질 수 있다.
    @RequestMapping(value="/person/insert1.do", produces="text/html; charset=utf-8")
    @ResponseBody
    public String insert1(
                            @RequestParam(value="name", required=trueString irum, //필수 입력값. 단, required의 기본값은 true이다.
                            int age, 
                            String tel,
                            @RequestParam(value="addr", required = false, defaultValue = "서울시"String addr
                            ){
        
        System.out.printf("name : %s\n", name);
        System.out.printf("age : %d\n", age);
        System.out.printf("tel : %s\n", tel);
        System.out.printf("addr : %s\n", addr);
        
        return "입력완료!!";
    }
cs

 

[ 파라미터 받기2 ] - 객체로 받기 

DispatcherServlet이 호출한 메서드가 메서드의 파라미터로 적은 내용은 DispatcherServlet에게 요청하는 요구사항이다. 파라미터로 PersonVo를 요청하고, 개발자가 직접 PersonVo를 생성만 해주면, DispatcherServlet이 직접 Setter메서드를 사용해서 파라미터를 받고, 형변환까지 자동으로 해준다.(map으로 받는 것은 예외, 객체의 속성이 정의되어 있을 때만 가능하다.) 호출한 메서드에게 전달해준다. 

이때문에 반드시 객체(Vo)의 속성명과 URL로 전달되어 들어오는 파라미터 명을 일치시키고, get/set메서드를 만들라고 한 것이다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    //객체로 받기
    // /person/insert1.do?name=일길동&age=20&tel=010-111-1234
    // 내(DispatcherServlet)가 널(메서드) 호출할 때 너가 메서드의 인자로 요청한 변수 or 객체에 들어온 파라미터를 Vo로 포장해서 내가 줄게
    // 단, 파라미터 이름 == Vo의 속성명이 반드시 일치해야 해
    // ㄴ> 들어온 파라미터를 PersonVo로 포장해서 전달해주렴.
    // ㄴ> 포장을 한다는 말은 너가 PersonVo를 만들어줘야 해!!!!!
    // ㄴ> 개발자가 PersonVo만 만들어주면 DispatcherServlet이 SetterInjection으로 값을 주입해준다!
    
    //메서드의 인자로 기록하는 것이 DispatcherServlet에게 요구하는 요구사항이다.
    @RequestMapping(value="/person/insert2.do", produces="text/html; charset=utf-8;")
    @ResponseBody
    public String insert2(PersonVo vo, HttpServletRequest request) {
        
        String ip = request.getRemoteAddr();
        
        System.out.printf("name : %s\n", vo.getName());
        System.out.printf("age : %d\n", vo.getAge());
        System.out.printf("tel : %s\n", vo.getTel());
        
        System.out.println(ip);
        
        return "객체로 받기 완료!!";
    }
cs

 

[ 파라미터 받기3 ]

주의 사항 

  • map으로 받는 경우는 보통 클라이언트 단에서 전달되는 파라미터를 예측할 수 없을 때 사용한다. 
  • 이때문에 반드시 @RequestParam어노테이션을 메서드의 파라미터에 붙여야 한다. 
  • 추가로 map으로 읽어오는 값은 모두 String이라는 것을 주의하고 형변환을 진행하도록 하자.
1
2
3
4
5
6
7
8
9
10
11
12
13
    //Map으로 받기          key  valeu key value ...
    // /person/insert1.do?name=일길동&age=20&tel=010-111-1234
    @RequestMapping(value="/person/insert3.do", produces="text/html; charset=utf-8;")
    @ResponseBody
    public String insert3(@RequestParam Map map) {
                            //map으로 파라미터를 수신할 때는 반드시 어노테이션을 사용해야 한다!!!, 
        
        System.out.printf("name : %s\n", map.get("name"));
        System.out.printf("age  : %s\n", map.get("age"));
        System.out.printf("tel  : %s\n", map.get("tel"));
        
        return "맵으로 받기 완료!!";
    }
cs

 

 

[ Spring MVC 구동원리 ]

  1. 프로젝트가 실행 되면 가장 먼저 web.xml파일을 읽는다. 
  2. root-context.xml 파일을 읽어서 ContextLoaderListener객체를 만들고, 이 객체가 root-context.xml에 정의된 Spring 공유자원(AOP), bean객체를 미리 생성한다.
  3. servlet-context.xml 파일을 읽어서 메인 컨트롤러인 DispatcherServlet을 생성한다. 메인 컨트롤러가 생성될 때 다음의 세가지 환경설정이 초기화된다.
    1.  뷰에 대한 정보인 resource파일의 저장위치를 지정한다. (기본 경로 외 다른 경로를 지정할 수 있다.)
    2. 작업 컨트롤러를 생성한다. 
    3. 작업 컨트롤러가 반환한 뷰의 이름에 경로(접두어)와 확장자(접미어)를 붙인다.
  4. URL로 들어온 요청의 URL-Pattern을 Annotation으로 가지고 있는 메서드를 호출한다. DispatcherServlet은 해당 메서드를 초기 환경설정에서 읽어올 때, 메서드의 인자까지 함께 읽어온다. 이를 요구사항으로 인지한다.
  5. 따라서 요구사항을 만족하기 위해서 해당 메서드를 호출할 때, 파라미터를 받아서 메서드의 인자로 채운 후에 메서드를 호출한다. 이때 사용되는 메서드의 인자는 DispatcherServlet에서 임시객체로 생성을 해서 전달하기 때문에, bean객체를 생성해줄 필요가 없다.
  6. 따라서 호출되는 메서드를 가지는 작업 컨트롤러는 클라이언트 단에서 건너오는 파라미터도 DispatcherServlet이 수신해주기 때문에, 이를 사용해서 비즈니스 로직만을 처리하면 된다. 
  7. 비즈니스 로직을 처리한 결과를 리턴하면 DispatcherServlet이 반환값에 맞도록 포워딩 or 리다이렉트 or responsebody하여 사용자에게 응답한다.

'BackEnd > Spring' 카테고리의 다른 글

SpringFileUpload_국비_Day86  (0) 2022.07.04
SpringMVC_DB_국비Day84  (0) 2022.06.30
SpringCollection_국비Day82  (0) 2022.06.28
Spring학습_Day4_AOP  (0) 2022.06.28
Spring_국비Day81  (0) 2022.06.27