IT 강의 정리/[스프링의 정석] 남궁성의 끝까지 간다

ch.02 Spring MVC 09~12 관심사의 분리와 MVC패턴

Nellie Kim 2022. 10. 2. 15:09
728x90

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

OOP 5대설계원칙 - SOLID

1. SRP - 단일책임의 원칙 : 하나의 메서드는 하나의 책임(관심사)만 진다.

2~5번째는 나중에 배울 것이다.

 

관심사가 분리되지 않은 YoilTeller코드

앞에서 만들었던 YoilTeller코드를 보자.

크게  입력코드, 요일계산하는코드, 출력코드 이렇게 3부분으로 해야할 작업(관심사)이 나뉘어 있다!

그렇게되면 바로 위의 설계원칙인 SRP-단일책임의 원칙에 위배된다.

하나의 main메서드에서 3가지 작업을 하고 있다..!!! 

어떻게 해결할 것인가?

 

객체지향적코딩을 하려면 코드의 분리를 잘 해야한다.

1) 관심사 분리

2) 변하는것, (자주)변하지않는것의 분리

3) 중복코드의 분리

 

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

입력부분을 보면,  비슷한 패턴이 반복, 중복되고 있다. year, month, day 마다 request.getParameter() 를 쓰고 있다.

입력 부분을 좀 단순화 하고싶은 욕구가 생긴다. 공통코드로 따로 뽑아내자.

request를 매개변수로 받지않고, year, month, day를 개별적으로 직접 매개변수로 받자.

int로 매개변수를 받으면 parseInt도 쓸 필요가 없어져서 코드가 단 2줄로 줄일 수 있다.

공통 코드의 분리 - 출력의 분리

입력코드는 축약을 잘 시켰는데, 이제 처리와 출력 부분이 남았다.

여기서 문제가 생기는데, 같은 main메서드 안에 있을 때는 매개변수로 year, month, day로 받은 것을 가져다가 쓸 수 있었는데, 메서드가 분리되면서 scope에 의해 가져다 쓸 수 없게 되었다! 어떻게 할까 ?

바로 중간 객체인 model 이라는 것을 사용하는 것! 

필요한 정보를 가진 매개변수들을 model객체에 담았다가 출력부분에서 사용하면 된다. model은 Map의 형태로 저장한다.

이 모델을 DispatcherServlet이 View에게 전달한다. 컨트롤러가 어떤 View를 통해서 반환할지 지정할수 있다.

return "yoil"; 이라고하면 yoil이라는 뷰를 보여달라는 뜻.

처리부분을 Controller, 출력부분을 보여지는 부분이라고 해서 View, 중간 객체를 Model 이라고해서 MVC라고 한다!

 

예를들어, 은행해서 원하는 자료를 출력하고 싶을때 PDF, CSV, Excel로 출력할 수 있도록 버튼이 있는데 이런것도 MVC패턴을 활용한 것이라고 볼 수 있다!

 

스프링은 아래 그림처럼 동작한다.

 

관심사가 모두 분리된 MVC패턴

MVC패턴 동작 원리

 

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

관심사 분리 전의 yoilTeller코드와 분리 후의 yoilTeller코드를 비교해보자.

분리 전의 yoilTeller코드

@Controller 
public class YoilTeller {
	@RequestMapping("/getYoil")  
    public void main(HttpServletRequest request, HttpServletResponse response) 
    		throws IOException{ 
        //1. 입력
        String year = request.getParameter("year"); //args[0]지우고 request.getParameter()
        String month = request.getParameter("month");
        String day = request.getParameter("day");
 
        int yyyy = Integer.parseInt(year);
        int mm = Integer.parseInt(month);
        int dd = Integer.parseInt(day);
 
        //2.작업
        Calendar cal = Calendar.getInstance();
        cal.set(yyyy, mm - 1, dd);
 
        int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
        char yoil = " 일월화수목금토".charAt(dayOfWeek);
        
        //3.출력
	      response.setContentType("text/html"); 
	      response.setCharacterEncoding("utf-8");
	      PrintWriter out = response.getWriter(); 
	      out.println("<html>");
	      out.println("<html>");
	      out.println("<head>");
	      out.println("</head>");
	      out.println("<body>");
	      out.println(year + "년 " + month + "월 " + day + "일은 "); 
	      out.println(yoil + "요일입니다.");
	      out.println("</body>");
	      out.println("</html>");
    }
}

 

MVC패턴으로 분리 후의 yoilTeller코드

yoilTeller2.java / yoil.jsp / yoilError.jsp 이렇게 3부분으로 나누어진다.

 

1) YoilTeller2.java

@Controller
public class YoilTeller2 {
	@RequestMapping("/getYoilMVC")   //http://localhost/ch2/getYoilMVC?year=2022&month=10&day=1
	public String main(int year, int month, int day, Model model) throws IOException 
    //여기서 반환타입을 String안쓰고 void로 쓰면 맵핑된 url로 해석이 된다. 이렇게는 잘 안씀.
    //반환타입을 ModelAndView 로 쓸수도 있다. 두가지의 객체를 담는다. 역시 잘 안씀

		//1. 유효성 검사
		if(!isValid(year,month,day))
		   return "yoilError";
		
		//2. 요일계산
		char yoil = getYoil(year, month, day);
		
		//3. 계산한 결과를 model에 저장
		model.addAttribute("year",year);
		model.addAttribute("month",month);
		model.addAttribute("day",day);
		model.addAttribute("yoil",yoil);
		
       return "yoil";
    }
	
	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);
	}
	
	private boolean isValid(int year, int month, int day) {
		return true;
	}
}

2) yoil.jsp

<%@ page contentType="text/html; charset=utf-8" %>>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
	<title>Home</title>
</head>
<body>
<P>${year }년 ${month }월 ${day }일 ${yoil }입니다. </P>  

</body>
</html>

3. yoilError.jsp

<%@ page contentType="text/html; charset=utf-8" %>>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
	<title>Home</title>
</head>
<body>
<h1>
	잘못된 요청입니다. 년, 월, 일을 모두 올바르게 입력하세요.
</h1>
</body>
</html>

 

컨트롤러 메서드의 반환타입 3가지

 

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

 

스프링이 매개변수 이름을 얻는 방법 2가지 - MethodInfo예제

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

		// 1. YoilTeller클래스의 객체를 생성
		Class clazz = Class.forName("com.fastcampus.ch2.YoilTeller2");
		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);
		}
	} // main
}

출력 결과

이때 콘솔에 매개변수 이름이 제대로 안나오고 args[0] 이런식으로 나올때는 window - preference 에가서 자바설정을 java11로 해준뒤 , Maven의 설정파일인 pom.xml파일에서 직접 11로 수정을 해줘야 한다. 그리고 나서 업데이트를 해주면 매개변수 이름이 정상출력 된다. 이렇게 매개변수 이름을 얻는 방법이 Reflection API를 이용한 것!!

현재 우리가 다루는 MVC프로젝트는 Maven을 이용해서 관리를 한다. 

 

스프링이 매개변수 이름을 얻는 방법은 두가지가 있다.

1. Reflection API 이용

parameters옵션을 넣고 컴파일을 해야한다. 그런데 이 옵션은 jdk1.8 (java8)부터 가능하다. jdk1.8전에는 클래스파일을 읽어서 얻어왔다.

2. Class file을 직접 읽기 

window - show view - other - Navigator검색하면, target따라 들어가면 클래스파일을 볼 수 있다. src가 java파일, target이 클래스파일. 우리가 하기엔 어렵다..

원하는 클래스파일에 들어가서 쭉 내려가보면, 저렇게 매개변수 이름을 찾을 수 있다.

 

스프링은 먼저 Reflection API를 이용해서 매개변수를 얻어오려고 한다. 만약에 실패하면 두번째방법인 Class file에 접근해서 직접 읽어서 얻어온다.

자바개발자가 parameters옵션을 주지않고 컴파일을 했을수도 있으니까 스프링은 2가지 방법을 시도하는 것이다.

 

컨트롤러 직접 생성 후 메서드 호출 - ModelController예제

학생들에게 질문이 많은 코드라고 한다.

class ModelController { //진짜 컨트롤러는 아닌데, 컨트롤러처럼 작동
	public String main(HashMap map) {
		// 작업 결과물을 map에 저장
		map.put("id", "asdf");
		map.put("pwd", "1111");
	
		return "txtView2"; //뷰이름을 반환. 위의 두줄은 사실상 필요가 없다!!!
	}
}

public class MethodCall {
	public static void main(String[] args) throws Exception{
		HashMap map = new HashMap();
		System.out.println("before:"+map);

		ModelController mc = new ModelController(); //컨트롤러를 직접생성
		String viewName = mc.main(map); //컨트롤러의 메서드 호출
		
		System.out.println("after :"+map);
		
		render(map, viewName);
	}
	
	static void render(HashMap map, String viewName) throws IOException {
		String result = "";
		
		// 1. 뷰의 내용을 한줄씩 읽어서 하나의 문자열로 만든다.
		Scanner sc = new Scanner(new File(viewName+".txt"));
		
		while(sc.hasNextLine())
			result += sc.nextLine()+ System.lineSeparator();
		
		// 2. map에 담긴 key를 하나씩 읽어서 template의 ${key}를 value바꾼다.
		Iterator it = map.keySet().iterator();
		
		while(it.hasNext()) {
			String key = (String)it.next();

			// 3. replace()로 key를 value 치환한다.
			result = result.replace("${"+key+"}", (String)map.get(key));
		}
		
		// 4.렌더링 결과를 출력한다.
		System.out.println(result);
	}
}


[txtView1.txt]
id:${id}
pwd:${pwd}

[txtView2.txt]
id=${id}, pwd=${pwd}

jsp도 이런식으로 구성 되어있다. 뷰를 여러개 만들어놓고 뷰이름을 반환하면 해당 뷰에 맞게 값이 채워진다.

이 예제에서는 컨트롤러를 직접 생성하고 메서드를 호출했다.

 

 

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

11강에서는 컨트롤러를 직접생성하고 메서드를 호출했다.

이번에는 리플렉션API를 이용해서 컨트롤러를 생성하고 메서드를 호출하는 방법을 배워보자.

Reflection API이용하여 컨트롤러 생성 - 하드코딩

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

		// 1. YoilTellerMVC의 객체를 생성
		Class clazz = Class.forName("com.fastcampus.ch2.YoilTellerMVC");
		Object obj = clazz.newInstance();
		
		// 2. main메서드의 정보를 가져온다.
		Method main = clazz.getDeclaredMethod("main", int.class, int.class, int.class, Model.class);
		
		// 3. Model을 생성
		Model model = new BindingAwareModelMap(); //Model은 인터페이스기때문에 직접 생성이 안된다. 
		System.out.println("[before] model="+model);
		
		// 4. main메서드를 호출
		// String viewName = obj.main(2021, 10, 1, model); //아래줄과 동일한 코드
		String viewName = (String)main.invoke(obj, new Object[] { 2021, 10, 1, model });//리플렉션API로 호출을 하려면 invoke메서드를 써야함. 하드코딩 	
		System.out.println("viewName="+viewName);	
		
		// Model의 내용을 출력 
		System.out.println("[after] model="+model);
				
		// 텍스트 파일을 이용한 rendering
		render(model, viewName);			
	} // main
	
	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);
	}
}

/* [실행결과] 
[before] model={}
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>

*/

 

Reflection API이용하여 컨트롤러 생성 -  동적인 생성 (하드코딩x)

package com.fastcampus.ch2;

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;

public class MethodCall3 {
	public static void main(String[] args) throws Exception{
		//1. 요청할 때 제공된 값 - request.getParameterMap();
		Map map = new HashMap();
		map.put("year", "2021");
		map.put("month", "10");
		map.put("day", "1");

		Model model = null;
		Class clazz = Class.forName("com.fastcampus.ch2.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의 타입을 비교해서, 다르면 변환해서 저장. String -> int로
				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) {
		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 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>
*/

 

 

 

 

출처 : [패스트캠퍼스] 스프링의 정석 : 남궁성과 끝까지 간다