본문 바로가기
[패스트캠퍼스] Spring/스프링의 정석 : 남궁성과 끝까지 간다

Ch.02 Spring MVC (09~12)

by 엑츄얼리 2022. 5. 11.

9. 관심사의 분리와 MVC 패턴 - 이론

1. 관심사의 분리 (Seperation Of Concerns)

 YoilTeller_remote.java

해당 Java파일은 3개의 관심사, 1. 입력 2. 작업(처리) 3. 출력으로 이루어져 있다.

 

 - OOP 5대 설계 원칙 : SOLID

   S(SRP) : 하나의 메서드는 하나의 Concern만 책임진다. 

    => 상위의 코드는 하나의 메서드가 3개의 Concern을 책임지므로 좋은 코드가 아님, SRP원칙에 따라 분리 필요

 

 - 분리 기준

  a. 관심사 (Concern)

  b. 변하는 것(common), 자주 변하지 않는 것(uncommon)

  c. 공통 코드

 

 

2. 공통 코드의 분리 - 입력의 분리

 - 입력(관심사)의 분리

    1. 의 코드에서 main 선언 부분을 위와 같이 int로 수정 시,

     request객체에 들어온 파라미터를 key와 value에 맞추어 데이터 형을 변환해준다. 

 

 

3. 출력(View)의 분리 - 변하는 것과 변하지 않는 것의 분리

 - 2. 작업(처리)과 3. 출력의 경우 같은 변수를 사용하기 때문에 이를 분리하기 위해 Model 객체를 생성

    관심사(Concern)를 처리하는데 필요한 변수를 저장하여 필요할 때 접근 및 수정

    => MVC 패턴 (Model, View, Control)

 

 

10. 관심사의 분리와 MVC 패턴 - 실습

 * 기존 코드를 새로운 메서드로 만드는 법

새로운 메서드로 만들 부분을 우클릭 + Refactor + ExtractMethod (getYoil 생성 방법)

[return String] 뷰 이름을 반환

package com.spring.fastcampus.controller.No10;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Calendar;

//10.관심사의 분리와 MVC패턴 - 실습
@Controller
public class YoilTellerMVC {
    @RequestMapping("/getYoilMVC") // http://localhost:8085/ch2/getYoilMVC?year=2021&month=10&day=1
    public String main(int year, int month, int day, Model model) throws IOException {

        //*1. 유효성 검사
        if(!isValie(year, month, day))
            return "No10/yoilError";

        // 2. 처리 - *2. MVC과정에서 우클릭 - Refactor - Extract Method
        char yoil = getYoil(year, month, day);

        //*2. 계산한 결과를 model에 저장 (model은 view로 자동 전달)
        model.addAttribute("year", year);
        model.addAttribute("month", month);
        model.addAttribute("day", day);
        model.addAttribute("yoil", yoil);

        //3. *3. 출력 to MVC
        return "No10/yoil";
    }

    private boolean isValie(int year, int month, int day) {

        return true;
    }

    private char getYoil(int year, int month, int day) {
        Calendar cal = Calendar.getInstance();
        cal.set(year, month - 1, day);

        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
        return " 일월화수목금토".charAt(dayOfWeek);
    }
}

 

[return void] 

public class YoilTellerMVCReturnVoid {
    @RequestMapping("/No10/getYoilMVC") // http://localhost:8085/ch2/No10/getYoilMVC?year=2021&month=10&day=1
    public void main(int year, int month, int day, Model model) throws IOException {

 - 선언부를 위와 같이 수정(String을 void로) 후 return 값을 제거하면,

    해당 코드는 return "/No10/getYoilMVC"; 이 있는 코드와 같음 (즉, 위의 코드와 같음)

 

[return ModelAndView]

@Controller
public class YoilTellerMVCModelAndView {
    @RequestMapping("/getYoilMVCModelAndView") // http://localhost:8085/ch2/getYoilMVCModelAndView?year=2021&month=10&day=1
    //    public static void main(String[] args) {
//    public String main(int year, int month, int day, Model model) throws IOException {
    public ModelAndView main(int year, int month, int day) throws IOException {
        //1. ModelAndView를 생성하고, 기본 뷰를 지정
        ModelAndView mv = new ModelAndView();

        //2. 유효성 검사
        if(!isValid(year, month, day)){
            return mv;
        }

        //3. 작업
        char yoil = getYoil(year, month, day);

        //4. ModelAndView에 작업한 결과를 저장
        mv.addObject("year", year);
        mv.addObject("month", month);
        mv.addObject("day", day);
        mv.addObject("yoil", yoil);

        //5. 작업 결과를 보여줄 뷰의 이름을 지정
        mv.setViewName("No10/yoil");

        //6. ModelAndView를 반환환
        return mv;
    }

    private boolean isValid(int year, int month, int day) {
        return true;
    }

    private char getYoil(int year, int month, int day) {
        Calendar cal = Calendar.getInstance();
        cal.set(year, month - 1, day);

        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
        return " 일월화수목금토".charAt(dayOfWeek);
    }
}

 - 잘 안 쓰인다고 하지만 신기해서 열심히 이해했다.

 

 

11. 관심사의 분리와 MVC 패턴 - 원리(1)

MethodInfo.java

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.StringJoiner;

public class MethodInfo {
    public static void main(String[] args) throws Exception {

        //1. YoilTeller클래스의 객체를 생성
        Class clazz = Class.forName("com.spring.fastcampus.controller.No11.YoilTeller");
        Object obj = clazz.newInstance();

        //2. 모든 메서드 정보를 가져와서 배열에 저장
        Method[] methodArr = clazz.getDeclaredMethods();

        for (Method m : methodArr) {
            String name = m.getName(); //메서드의 이름
            Parameter[] paramArr = m.getParameters(); //매개변수 목록
//       Class[] paramTypeArr = m.getParameterTypes();
            Class returnType = m.getReturnType(); //반환 타입

            StringJoiner paramList = new StringJoiner(", ", "(", ")");

            for (Parameter param : paramArr) {
                String paramName = param.getName();
                Class paramType = param.getType();

                paramList.add(paramType.getName() + " " + paramName);
            }

            System.out.printf("!%s %s%s%n", returnType.getName(), name, paramList);

//            매개 변수의 이름은 Compiler에게 중요하지 않기 때문에 arg0, arg1,,, 등으로 처리하여 관리
//            매개 변수 이름을 저장하려면 javac -parameters 라는 옵션을 주어야 함 (컴파일러 옵션에 추가)
//            !java.lang.String main(int arg0, int arg1, int arg2, org.springframework.ui.Model arg3)
//            !char getYoil(int arg0, int arg1, int arg2)
//            !boolean isValie(int arg0, int arg1, int arg2)
        }
    } // main
}

 

 1. STS를 통한 javac -parameters 설정

 - javac -parameters를 설정하지 않으면 위 코드를 실행 결과 아래와 같이 파라미터의 이름이 제대로 나오지 않는다.

!java.lang.String main(int arg0, int arg1, int arg2, org.springframework.ui.Model arg3)
!char getYoil(int arg0, int arg1, int arg2)
!boolean isValie(int arg0, int arg1, int arg2)

 

 - javac - parameters 설정 후

!java.lang.String main(int year, int month, int day, org.springframework.ui.Model model)
!boolean isValie(int year, int month, int day)
!char getYoil(int year, int month, int day)



  1) (STS) Windows -> Preferencs - Compiler - Store Information about method parameters (Check)

  2) pom.xml (Maven 설정 파일)

<java-version>을 11로 수정
<source>,<target>을 ${java-version}으로 수정

 

 2. Spring이 매개변수 이름을 얻는 법 (STS)

  1) Reflection API => javac -parameters 설정

  2) ClassFile

   - ClassFile 보는 법(STS)  (src폴더 => *.java, target폴더 => *.class)

    Windows -> show view -> other -> Navigator

  target폴더의 원하는 클래스 파일 열기 -> Local Variable Table에 변수 타입과 이름이 저장되어 있음

 

 3. Intellij Gradle을 통한 -parameters 설정

  - build.gradle에

compileJava.options.compilerArgs.add '-parameters'
compileTestJava.options.compilerArgs.add '-parameters'

4. Intellij자체 -parameters 설정

https://stackoverflow.com/questions/39217830/how-to-use-parameters-javac-option-in-intellij

 

12. 관심사의 분리와 MVC 패턴 - 원리(2)

MethodCall3.java

package com.spring.fastcampus.controller.No12;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;

import org.springframework.ui.Model;
import org.springframework.validation.support.BindingAwareModelMap;

//스프링이 Controller의 Method의 매개변수에 맞게 값들을 model에 저장하고 view에 표현하는 방식 확인
public class MethodCall3 {
    public static void main(String[] args) throws Exception{
        Map map = new HashMap();
        map.put("year", "2021");
        map.put("month", "10");
        map.put("day", "1");

        Model model = null;
        Class clazz = Class.forName("com.spring.fastcampus.controller.No10.YoilTellerMVC");
        Object obj  = clazz.newInstance();

        // YoilTellerMVC.main(int year, int month, int day, Model model)
        Method main = clazz.getDeclaredMethod("main", int.class, int.class, int.class, Model.class);

        Parameter[] paramArr = main.getParameters(); //main 메서드의 매개변수 목록을 가져옴
        Object[] argArr = new Object[main.getParameterCount()]; //매개변수 갯수와 같은 길이의 Object배열

        for(int i=0;i<paramArr.length;i++) {
            String paramName = paramArr[i].getName();
            Class  paramType = paramArr[i].getType();
            Object value = map.get(paramName); // map에서 못찾으면 value는 null

            // paramType중에 Model이 있으면, 생성 & 저장
            if(paramType==Model.class) {
                argArr[i] = model = new BindingAwareModelMap();
            } else if(value != null) {  // map에 paramName이 있으면,
                // value와 parameter의 타입을 비교해서, 다르면 변환해서 저장
                argArr[i] = convertTo(value, paramType);
            }
        }
        System.out.println("paramArr="+Arrays.toString(paramArr));
        System.out.println("argArr="+Arrays.toString(argArr));


        // Controller의 main()을 호출 - YoilTellerMVC.main(int year, int month, int day, Model model)
        String viewName = (String)main.invoke(obj, argArr);
        System.out.println("viewName="+viewName);

        // Model의 내용을 출력
        System.out.println("[after] model="+model);

        // 텍스트 파일을 이용한 rendering
        render(model, viewName);
    } // main

    private static Object convertTo(Object value, Class type) {
        //isInstance 객체의 Class를 반환
        if(type==null || value==null || type.isInstance(value)) // 타입이 같으면 그대로 반환
            return value;

        // 타입이 다르면, 변환해서 반환
        //Integer.parseInt({String}) : 기본 int 반환
        //Integer.valueOf({String}) : Integer 객체 반환
        if(String.class.isInstance(value) && type==int.class) { // String -> int
            return Integer.valueOf((String)value);
        } else if(String.class.isInstance(value) && type==double.class) { // String -> double
            return Double.valueOf((String)value);
        }

        return value;
    }

    private static void render(Model model, String viewName) throws IOException {
        String result = "";

        // 1. 뷰의 내용을 한줄씩 읽어서 하나의 문자열로 만든다.
        Scanner sc = new Scanner(new File("src/main/webapp/WEB-INF/views/"+viewName+".jsp"), "utf-8");

        while(sc.hasNextLine())
            result += sc.nextLine()+ System.lineSeparator();

        // 2. model을 map으로 변환
        Map map = model.asMap();

        // 3.key를 하나씩 읽어서 template의 ${key}를 value바꾼다.
        Iterator it = map.keySet().iterator();

        while(it.hasNext()) {
            String key = (String)it.next();

            // 4. replace()로 key를 value 치환한다.
            result = result.replace("${"+key+"}", ""+map.get(key));
        }

        // 5.렌더링 결과를 출력한다.
        System.out.println(result);
    }
}

/* [실행결과]
paramArr=[int year, int month, int day, org.springframework.ui.Model model]
argArr=[2021, 10, 1, {}]
viewName=yoil
[after] model={year=2021, month=10, day=1, yoil=금}
<%@ page contentType="text/html;charset=utf-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
   <title>YoilTellerMVC</title>
</head>
<body>
<h1>2021년 10월 1일은 금요일입니다.</h1>
</body>
</html>
*/

 - 스프링이 Method에 맞게 값들을 변환하고 입력하는 방식 확인 가능(model을 view에 전달)

 

MyDispatcherServlet.java

package com.spring.fastcampus.controller.No12;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;
import java.util.Scanner;

import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.ui.Model;
import org.springframework.validation.support.BindingAwareModelMap;

//@Controller + @RequestMapping
@WebServlet("/myDispatcherServlet")  // http://localhost/ch2/myDispatcherServlet?year=2021&month=10&day=1
public class MyDispatcherServlet extends HttpServlet {
    @Override
    public void service(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Map    map = request.getParameterMap();
        Model  model = null;
        String viewName = "";

        try {
            Class clazz = Class.forName("com.spring.fastcampus.controller.No10.YoilTellerMVC");
            Object obj = clazz.newInstance();

            // 1. main메서드의 정보를 얻는다.
            Method main = clazz.getDeclaredMethod("main", int.class, int.class, int.class, Model.class);

            // 2. main메서드의 매개변수 목록(paramArr)을 읽어서 메서드 호출에 사용할 인자 목록(argArr)을 만든다.
            Parameter[] paramArr = main.getParameters();
            Object[] argArr = new Object[main.getParameterCount()];

            for(int i=0;i<paramArr.length;i++) {
                String paramName = paramArr[i].getName();
                Class  paramType = paramArr[i].getType();
                Object value = map.get(paramName);

                // paramType중에 Model이 있으면, 생성 & 저장
                if(paramType==Model.class) {
                    argArr[i] = model = new BindingAwareModelMap();
                } else if(paramType==HttpServletRequest.class) {
                    argArr[i] = request;
                } else if(paramType==HttpServletResponse.class) {
                    argArr[i] = response;
                } else if(value != null) {  // map에 paramName이 있으면,
                    // value와 parameter의 타입을 비교해서, 다르면 변환해서 저장
                    String strValue = ((String[])value)[0];    // getParameterMap()에서 꺼낸 value는 String배열이므로 변환 필요
                    argArr[i] = convertTo(strValue, paramType);
                }
            }

            // 3. Controller의 main()을 호출 - YoilTellerMVC.main(int year, int month, int day, Model model)
            viewName = (String)main.invoke(obj, argArr);
        } catch(Exception e) {
            e.printStackTrace();
        }

        // 4. 텍스트 파일을 이용한 rendering
        render(model, viewName, response);
    } // main

    private Object convertTo(Object value, Class type) {
        if(type==null || value==null || type.isInstance(value)) // 타입이 같으면 그대로 반환
            return value;

        // 타입이 다르면, 변환해서 반환
        if(String.class.isInstance(value) && type==int.class) { // String -> int
            return Integer.valueOf((String)value);
        } else if(String.class.isInstance(value) && type==double.class) { // String -> double
            return Double.valueOf((String)value);
        }

        return value;
    }

    private String getResolvedViewName(String viewName) {
        return getServletContext().getRealPath("/WEB-INF/views") +"/"+viewName+".jsp";
    }

    private void render(Model model, String viewName, HttpServletResponse response) throws IOException {
        String result = "";

        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();

        // 1. 뷰의 내용을 한줄씩 읽어서 하나의 문자열로 만든다.
        Scanner sc = new Scanner(new File(getResolvedViewName(viewName)), "utf-8");

        while(sc.hasNextLine())
            result += sc.nextLine()+ System.lineSeparator();

        // 2. model을 map으로 변환
        Map map = model.asMap();

        // 3.key를 하나씩 읽어서 template의 ${key}를 value바꾼다.
        Iterator it = map.keySet().iterator();

        while(it.hasNext()) {
            String key = (String)it.next();

            // 4. replace()로 key를 value 치환한다.
            result = result.replace("${"+key+"}", map.get(key)+"");
        }

        // 5.렌더링 결과를 출력한다.
        out.println(result);
    }
}

@WebServlet = @Controller + @RequestMapping

 - @WebServlet : Class 단위 Mapping (Method 단위 X), HttpServlet을 상속받아야 함

 

 - @RequestMapping : Method 단위 Mapping

 

 

댓글