코딩 개발/Spring

Spring MVC 이해하기 - JUnit, 웹 프로그램 실행 (Spring legacy)

호소세 2023. 7. 14. 21:33
728x90
반응형

저희가 이전에 배운 Model2 Architecture, Front Controller Pattern의 진화형입니다.

 

https://pabeba.tistory.com/158

 

Model 2 Architecture (MVC)

Model2와 Model 1 비교해보면 재밌습니다. https://pabeba.tistory.com/157 Model 1 Architecture Model1 Architecture 이란? Model 1 은 View와 Model을 모두 JSP 페이지 하나에서 처리하는 구조를 말합니다. Java Bean은 class를 만

pabeba.tistory.com

https://pabeba.tistory.com/164

 

MVC - FrontController Pattern

https://pabeba.tistory.com/157 Model 1 Architecture Model1 Architecture 이란? Model 1 은 View와 Model을 모두 JSP 페이지 하나에서 처리하는 구조를 말합니다. Java Bean은 class를 만들어서 로직을 작성해 놓은 것입니다.

pabeba.tistory.com

 

Spring MVC Model 이란?

Spring Framework 기반 Java Web Application 구현을 위한 기술입니다.

 

MVC Design Pattern

Model : 비즈니스 로직과 데이터 액세스 로직

View : 클라이언트로의 response를 전담

Controller : request 분석, Model 연동, View 선택해 응답

 

Front Controller Design Pattern

모든 클라이언트의 요청을 한 곳을 집중시켜 공통 정책을 수행합니다.

인코딩, 보안(인증과 인가) , 예외처리와 같은 공통 정책을 효과적으로 수행할 수 있습니다.

 

Spring MVC 동작 순서

사진을 보면서 확인하면 조금 더 알기 쉽습니다.

1. 클라이언트로부터 HTTP 요청이 도착합니다. DispatcherServlet이 해당 요청을 받습니다. 이는 웹 응용 프로그램의 전반적인 흐름을 제어하는 중앙 컨트롤러 역할을 합니다.

2. DispatcherServlet은 HandlerMapping에게 요청을 전달하고, 적절한 컨트롤러를 선택합니다. HandlerMapping은 요청 URL을 기반으로 컨트롤러를 매핑하는 역할을 수행합니다.

3. 선택된 컨트롤러는 Handler Adapter를 통해 요청을 처리하고

4. 필요한 비즈니스 로직을 수행합니다.

5. 컨트롤러는 요청 처리 결과를 담은 Model 객체와 보여줄 View 이름을 반환합니다.

6. DispatcherServlet은 ViewResolver에게 View 이름을 전달하고, 실제로 사용될 View를 검색합니다.

7. ViewResolver는 View 이름을 기반으로 적절한 View 객체를 찾아 반환합니다.

8. DispatcherServlet은 선택된 View에게 Model 데이터를 전달하여 실제 응답을 생성합니다. 생성된 응답은 클라이언트로 전송됩니다.

출처: https://terasolunaorg.github.io

저는 HandlerMapping과 HandlerAdapter의 차이가 궁금했기 때문에 이와 관련된 내용을 작성해 보겠습니다. (적확한 지는 모르겠습니다.)

 

HandlerMapping

HandlerMapping은 클라이언트의 요청 URL을 기반으로 어떤 컨트롤러(Handler)가 해당 요청을 처리할지 결정합니다. 요청 URL을 분석하고 매핑된 컨트롤러 객체를 찾아 반환하는 역할을 합니다. Spring MVC에서는 다양한 HandlerMapping 구현체가 제공되며, 가장 일반적인 구현체는 RequestMappingHandlerMapping입니다. 이 HandlerMapping은 어노테이션 기반의 컨트롤러 메서드와 URL 패턴을 매핑하여 적절한 핸들러를 찾아줍니다.

 

HandlerAdapter

HandlerAdapter는 HandlerMapping을 통해 결정된 컨트롤러(Handler)를 실행하고 처리하는 역할을 합니다. HandlerAdapter는 핸들러의 실행을 담당하며, 핸들러의 특성에 따라 적절한 방식으로 실행됩니다. Spring MVC에서는 다양한 HandlerAdapter 구현체를 제공합니다. 예를 들어, RequestMappingHandlerAdapter는 @RequestMapping 어노테이션이 적용된 메서드를 처리하는 데 사용됩니다. 이 HandlerAdapter는 핸들러 메서드의 인자를 추출하고, 적절한 변환을 수행하여 메서드를 호출하고 결과를 반환합니다.

 

간단히 말씀드리자면 mapper는 컨트롤러를 찾아주는 역할을 하고 adapter는 컨트롤러의 메서드를 실행해 주는 역할을 합니다.

 

 

 

이번에는 xmlconfig와 어노테이션을 이용해서 junit과 mybatis를 사용해 보는 연습을 해보겠습니다.

설정 파일부터 머리가 너무 아프긴 하지만... 잘해봐요.

 

xmlconfig로 DynamicWeb 제작하기

Eclipse로 Dynamic web project를 생성합니다. (마우스 오른쪽 버튼 - new - dynamic web proejct)

 

다음 maven project로 convert 합니다. (프로젝트 위에서 마우스 오른쪽 버튼 - configure - convert to maven project)

 

이번 편은 Spring Boot 가 아니기 때문에 필요한 라이브러리가 많습니다.

 

pom.xml 파일부터 빠르게 채워 넣어볼까요?

 

<pom.xml>

<dependencies>
 		<!-- springmvc , slf4j,logback 사용을 위해 commons logging은 제외  -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-webmvc</artifactId>
		<version>5.3.20</version>
			<exclusions>
			<exclusion>
				<groupId>commons-logging</groupId>
				<artifactId>commons-logging</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
	<!-- db 연동을 위한 mybatis 설정  -->
		<dependency>
		<groupId>com.oracle.database.jdbc</groupId>
		<artifactId>ojdbc8</artifactId>
		<version>21.5.0.0</version>
	</dependency>
	<dependency>
		<groupId>org.apache.commons</groupId>
		<artifactId>commons-dbcp2</artifactId>
		<version>2.8.0</version>
	</dependency>
	<dependency>
		<groupId>org.mybatis</groupId>
		<artifactId>mybatis</artifactId>
		<version>3.5.6</version>
	</dependency>
	<dependency>
		<groupId>org.mybatis</groupId>
		<artifactId>mybatis-spring</artifactId>
		<version>1.3.0</version>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-jdbc</artifactId>
		<version>5.3.20</version>
	</dependency>
	<!--  view jstl  -->
	<dependency>
		<groupId>javax.servlet</groupId>
		<artifactId>jstl</artifactId>
		<version>1.2</version>
	</dependency>
	<!--  AOP -->
	<dependency>
		<groupId>org.aspectj</groupId>
		<artifactId>aspectjweaver</artifactId>
		<version>1.8.1</version>
	</dependency>
	<!--  Logging -->
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>1.7.25</version>
	</dependency>
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.2.3</version>
	</dependency>
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>jcl-over-slf4j</artifactId>
		<version>1.7.25</version>
	</dependency>
	<!-- ajax/json -->
	<dependency>
		<groupId>com.fasterxml.jackson.core</groupId>
		<artifactId>jackson-databind</artifactId>
		<version>2.10.0</version>
	</dependency>
   <!-- spring junit test -->
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.13</version>
		<scope>test</scope>
	</dependency>		
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-test</artifactId>
		<version>5.3.20</version>
	</dependency>
</dependencies>

1. unit test를 위한 junit 라이브러리

2. ajax/json

3. view를 위한 jstl

4. spring-test

정도가 처음 보는 라이브러리네요.

 

web.xml

웹 애플리케이션의 설정과 구성을 담당하는 배포 서술자입니다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
	id="WebApp_ID" version="4.0">
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>

	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<servlet>
		<servlet-name>appServlet</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>
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	
	<filter>
		<filter-name>EncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>utf-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>EncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
</web-app>

1. <context-param>: 애플리케이션의 콘텍스트 파라미터를 정의합니다. 

contextConfigLocation은 Spring 콘텍스트 파일의 위치를 지정하는 매개 변수입니다. /WEB-INF/spring/root-context.xml에 있는 파일을 로드하여 루트(Spring) 컨텍스트를 생성합니다. 이 컨텍스트는 모든 서블릿과 필터에서 공유됩니다. (DB Mapper 로 이용)

2. <listener>: 컨텍스트 로딩 이벤트를 처리하기 위한 리스너를 등록합니다. 

ContextLoaderListener는 애플리케이션 컨텍스트를 초기화하고 설정된 컨텍스트 파일을 로드합니다. 이 리스너는 웹 애플리케이션 시작 시점에 컨텍스트를 로드합니다.

3. <servlet>: Spring MVC의 디스패처 서블릿을 등록합니다. 

appServlet이라는 이름으로 DispatcherServlet 클래스를 사용하여 서블릿을 정의합니다. contextConfigLocation은 해당 서블릿의 컨텍스트 파일 위치를 지정합니다. /WEB-INF/spring/appServlet/servlet-context.xml에 있는 파일을 로드하여 서블릿 컨텍스트를 생성합니다.

4. <servlet-mapping>: 서블릿과 URL 패턴을 매핑합니다.

appServlet 서블릿을 루트 URL(/)에 매핑하도록 설정합니다. 즉, 모든 요청은 이 서블릿으로 전달됩니다.

5. <filter> 및 <filter-mapping>: 요청과 응답을 필터링하기 위한 필터를 정의하고 매핑합니다. 

위의 예시에서는 EncodingFilter라는 이름의 필터를 정의하고 CharacterEncodingFilter 클래스를 사용하여 UTF-8 인코딩을 적용합니다. /* 패턴으로 모든 URL에 이 필터를 적용하도록 설정합니다.

 

이렇게 설정된 web.xml은 Spring MVC 웹 애플리케이션의 구성 요소를 정의하고 설정하는 역할을 합니다. 이 파일을 사용하여 컨텍스트 로딩, 디스패처 서블릿 등을 설정하고, 필터를 등록하여 요청과 응답을 처리할 수 있습니다.

 

위의 설정에서 나온 root-context.xml 설정에 대하여 알아보겠습니다.

 

root-context.xml

DB연동, MyBatis Proxy 설정을 담당하는 설정 파일입니다. 이전에 알아본 파일과 유사합니다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:mybatis-spring="http://mybatis.org/schema/mybatis-spring"
	xsi:schemaLocation="http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring-1.2.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

	<context:component-scan
		base-package="myproject.model" />
	<bean id="dbcp" class="org.apache.commons.dbcp2.BasicDataSource">
		<property name="driverClassName"	value="oracle.jdbc.OracleDriver" />
		<property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:xe" />
		<property name="username" value="mango" />
		<property name="password" value="apple" />
	</bean>
	<bean id="sqlSessionFactory" 	class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dbcp" />
		<property name="configuration">
			<bean class="org.apache.ibatis.session.Configuration">
				<property name="mapUnderscoreToCamelCase" value="true" />
			</bean>
		</property>
	</bean>
	<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg ref="sqlSessionFactory" />
	</bean>
	<mybatis-spring:scan base-package="myproject.model.mapper" />
</beans>

1. dbcp 생성

2. spring과 mybatis framework 연동 설정

3. 반복 전인 db연동 작업을 최소화하는 sqlSessionTemplate 객체 생성

4. MyBatis Proxy 설정 (myproject.model.mapper 스캔해서 사용)

 

 

Dispatcher Servlet을 위한 servlet-context.xml 설정파일을 알아봅시다.

servlet-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

	<annotation-driven />

	<resources mapping="/resources/**" location="/resources/" />

	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
	<context:component-scan base-package="myproject.controller" />
</beans:beans>

1. <annotation-driven>: Spring MVC의 @Controller 어노테이션을 사용한 컨트롤러를 활성화합니다.

이를 통해 Spring MVC는 어노테이션 기반 컨트롤러를 자동으로 검색하고 요청 처리에 활용합니다. 

2. <resources>: /resources/** 경로로 들어오는 HTTP GET 요청에 대해 정적 리소스를 처리하기 위한 설정입니다.

/resources/ 디렉터리의 정적 리소스를 효율적으로 제공합니다. (이미지나, html, css 등등... 을 저장해 놓습니다.)

3. <beans:bean>: InternalResourceViewResolver를 정의합니다.

이 빈은 @Controller에서 선택한 뷰를 /WEB-INF/views/ 디렉터리의 JSP 리소스로 변환하여 렌더링 합니다. prefix 속성은 뷰 이름의 접두어를 설정하고, suffix 속성은 뷰 이름의 접미사를 설정합니다.

만약에 home이 반환되면 /WEB-INF/views/home.jsp 파일이 실행되는 겁니다.

4. <context:component-scan>: 컨트롤러 클래스를 검색하고 Spring 콘텍스트에 등록하기 위해 컴포넌트 스캐닝을 설정합니다. base-package 속성은 컨트롤러 클래스가 위치한 패키지를 지정합니다.

myproject.controller를 스캔합니다.

 

JUnit을 이용하여 Test 하기

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("file:src/main/webapp/WEB-INF/spring/root-context.xml")
public class JUnitTestCustomer {
	@Autowired
	private CustomerMapper customerMapper;
	@Test
	public void testCustomerMapperDI() {
		Assert.assertNotNull(customerMapper);
	}
	@Test
	public void getTotalCustomerCount() {
		int count=customerMapper.getTotalCustomerCount();
		Assert.assertEquals(1, count);//첫번째 매개변수 기대값 1 , 두번째 실제값 
	}
	@Test
	public void findCustomerById() {
		CustomerVO customerVO=customerMapper.findCustomerById("java");
		Assert.assertNotNull(customerVO);
	}
	@Test
	public void findCustomerList() {
		List<CustomerVO> list=customerMapper.findCustomerList();
		Assert.assertEquals(3, list.size());
	}
}

@RunWith는 작성한 class로 테스트 클래스의 실행기를 변경해 줍니다.

@ContextConfiguration을 이용하여 root-context.xml을 이용하여 단위테스트를 실행합니다.

 

Assert.asserNotNull(customerMapper);

-> assert의 뜻이 '주장하다', '확실히 하다'라는 의미가 있네요.

JUnit의 클래스인 Assert를 이용하여 매개변수의 값이 null이 아니라는 것을 확인하는 것입니다.

 

이와 유사하게

Assert.assertEquals(3, list.size());

이것은 list.size()가 3과 같은지 확인하는 문장입니다.

 

MyBatis는 1~4탄을 보고 작성해 보는 연습을 해봐요.

 

그다음은 web 서버를 이용하여 브라우저에 고객이 원하는 화면을 나타내 보겠습니다.

 

웹 브라우저의 요청 보내기

위에서 MVC 모델의 동작 순서를 확인했지 않습니까?

처음에 request를 보내어 home 화면이 나타나게 하는 동작을 해보겠습니다.

 

그럼 먼저 Controller를 만들어야 DispatcherServlet에서 찾아내겠죠?

 

<HomeController.java>

@Controller
public class HomeController {
	private CustomerMapper customerMapper;
	@Autowired
	public HomeController(CustomerMapper customerMapper) {
		super();
		this.customerMapper = customerMapper;
	}
	@RequestMapping("/")
	public String home(Model model) {
		model.addAttribute("totalCustomerCount", customerMapper.getTotalCustomerCount());
		return "home"; //view name : ViewResolver 설정에 의해 WEB-INF/views/home.jsp 로 응답
	}
}

@Controller라고 명시하고

CustomerMapper를 주입합니다.

@RequestMapping("/")을 통해서 프로그램의 요청에 "/" 오면 home 화면이 나오게 합니다.

Model 객체를 매개변수로 이용해서 request에 담아서 값을 보내줍니다.

 

그러면 이제 "WEB-INF/views/home.jsp"이라는 view와 고객의 숫자를 담은 model이 함께 고객에게 응답 보내집니다.

 

실제로 WEB-INF/views/home.jsp 이 파일이 열리게 되면서 고객의 숫자를 담은 값이 나오게 됩니다.

<home.jsp>

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>spring mvc</title>
</head>
<body>
<h4>Spring Legacy Project Test (IOC/DI MVC MyBatis JUnit)</h4>
<br><br>
전체 고객수 ${totalCustomerCount} 명 <br><br>
</body>
</html>

이렇게 나오게 됩니다.

 

소감

Spring Boot의 동작 원리를 알기 위한 발버둥이라고 해야 할까요... Boot를 이용하면 설정할 파일이 점점 없어지긴 합니다.

그 동작원리를 알기 위해 우리가 legacy를 하고 있는 것이지요.

조금은 지루하고 어려워도 피가 되고 살이 되는 작업이라고 생각합니다. 교육만 받았다면 알지 못했을 것들을 수업 이후에 복습하고 작성해 보고 다시 한번 실행해 보면서 조금씩 더 알게 되는 것 같습니다.

처음 교육을 들었을 때는 시간이 왜 이리 안 가지..라는 생각을 했지만 시간이 지날수록 너무 빠르다는 생각이 들었습니다. 그리고 지금은 이제 면접준비나 프로젝트 준비를 하면서 시간이 부족하다는 생각이 들게 되었네요.... ㅠ

시간을 효율적으로 잘 활용하는 사람이 성공하는 것 같습니다.

쉼과 병행하면서 열심히 나아가는 사람이 되어보겠습니다.

반응형