[Spring] Spring MVC

JH·2023년 5월 2일

Spring

목록 보기
4/9

1. TIL

Spring구조, ★ : 예외가 발생하는 곳

Exception 객체는 Framework layer에서만 사용할 수 있으므로 Exception 객체를 공유하려고 Exception Resolver를 사용함

동일한 try-cacth문을 계속 사용하므로 이를 해결하기 위해 Resolver를 사용

html에서 요청 방식은 GET, POST 방식 밖에 사용하지 못함

이를 해결하기 위해 일단 <input type="hidden" name="_method" value="PUT" /> 이렇게 서버에 전달해야함

그리고 서버는 delete, update를 사용하려면 무조건 컨버터를 사용해야함

Converter Reference
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-rest-method-conversion

예외 Resolver 리퍼런스
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-exceptionhandlers


DTO는 생략 (특이사항 : lombok Annotation 활용)


A. Mybatis 설정

1. DBConfiguration.java

SqlSession을 생성하려면 Mybatis에서는 팩토리 패턴으로 객체를 만듦

팩토리 패턴 : 특정 객체를 셍성하기 위한 팩토리를 먼저 만들고, 그 뒤에 해당 특정 객체를 생성하는 방법

@Configuration
public class DBConfiguration {
	static String resource = "/mybatis-config.xml";

	@Bean
	public static SqlSessionFactory getSqlSessionFactory() {
		SqlSessionFactoryBuilder sqlSessionFactoryBuilder;
		SqlSessionFactory sqlSEssionFactory = null;
		InputStream is = null;
        
		try {
			is = Resources.getResourceAsStream(resource);
			sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
			sqlSEssionFactory = sqlSessionFactoryBuilder.build(is);
		}
        catch (Exception e) {
			e.printStackTrace();
		}
		return sqlSEssionFactory;
	}
	
	@Bean
	public static SqlSession getSqlSession() {
		SqlSession sqlSession = null;
		sqlSession = getSqlSessionFactory().openSession();
		return sqlSession;
	}
}

2. mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>

	<!-- DTO 별칭 설정 -->
	<typeAliases>
		<typeAlias type="com.spring.dto.Dept" alias="Dept"/>
	</typeAliases>
	
    <!-- JDBC 설정 -->
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC"/>
				<dataSource type="POOLED">
					<property name="driver" value="oracle.jdbc.driver.OracleDriver"/>
					<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe"/>
					<property name="username" value="userid"/>
					<property name="password" value="userpw"/>
				</dataSource>
		</environment>
	</environments>
	
    <!-- 매퍼 설정 -->
	<mappers>
		<mapper resource="/com/spring/mapper/DeptMapper.xml"/>
		<mapper resource="/com/spring/mapper/EmpMapper.xml"/>
	</mappers>
	
</configuration>



B. Controller

1. HomeController

@Controller : 스프링이 컨트롤러로 인식함
@Autowired : 스프링 컨테이너가 의존성 주입

@Controller
public class HomeController {
	@Autowired 
	private DeptService service;
	
	@RequestMapping(value = "/main", method = RequestMethod.GET)
	public String main(Model model) {
		List<Dept> deptList = service.getAllDepts();
		
		model.addAttribute("deptList", deptList);
		
		return "main";
	}
}

2. DeptController

Dept CRUD 관련 컨트롤러

@ModelAttribute : 클라이언트에서 요청한 데이터를 Bean에 등록된 객체로 받을 수 있음
@PathVariable : url 파라미터로 전달받은 값을 1대1 매핑시켜줌

@Controller
public class DeptController {
	@Autowired
	DeptService service;
	
	// detail
	@RequestMapping(value = "/dept/{deptno}", method = RequestMethod.GET)
	public String getDeptByDeptno(Model model, @PathVariable int deptno) {
		Dept dept = service.getDeptByDeptno(deptno);		
		model.addAttribute("dept", dept);
		
		return "deptDetail";
	}
	
	// insertForm으로 이동
	@RequestMapping(value = "/dept", method = RequestMethod.GET)
	public String insertDeptForm() {
		return "registerDept";
	}
	
	// insert
	@RequestMapping(value = "/dept", method = RequestMethod.POST)
	public String insertDept(@ModelAttribute Dept newDept, Model model) {
		String view = "error";
		boolean result = false;
		
		try {
			result = service.insertDept(newDept);
			
			if (result) {
				// version 1
//				view = "main";
//				model.addAttribute("deptList", service.getAllDepts()); 
				// main으로 이동 시 새로 생성된 dept가 포함되지 않으므로 model에 추가해줘야 함 
				
				// version 2
				view = "redirect:/main";
				return view;
			} 
			
		} catch (Exception e) {
			return view;
		}
		
		return view;
	}
	
	// updateForm으로 이동
	@RequestMapping(value = "/modify/dept/{deptno}", method = RequestMethod.GET)
	public String updateDeptForm(@PathVariable int deptno, Model model) {
		Dept dept = service.getDeptByDeptno(deptno);
		
		model.addAttribute("dept", dept); // name, 객체
		
		return "updateDept";
	}
	
	// update
	@RequestMapping(value = "/dept/{deptno}", method = RequestMethod.PUT)
	public String updateDept(@PathVariable int deptno, Model model,
							@ModelAttribute("dname") String dname, // @ModelAttribute 를 사용하면 무조건 객체와 매핑된다.
							@ModelAttribute("loc") String loc) {
							
		// deptno로 기존 dept객체 확인 -> 위에서 확인한 dname, loc 해당 객체 setter
		// 제대로 update가 되었으면 -> dept/{deptno} detail페이지 이동
		Dept dept = service.getDeptByDeptno(deptno);	
		dept.setDname(dname);
		dept.setLoc(loc);
		
		boolean result = false;
		String view = "error";
		
		try {
			result = service.updateDnameAndLoc(dept);
			
			if (result) {
				view = "redirect:/dept/" + deptno;
			}
		} catch (Exception e) {
			return view;
		}
		
		return view;
	}
	
	// delete
	@RequestMapping(value = "/dept/{deptno}", method = RequestMethod.DELETE)
	public String deleteDeptByDeptno(@PathVariable int deptno) {
		boolean result = false;
		String view = "error";
		
		try {
			result = service.deleteDeptByDeptno(deptno);
			if (result) {
				return "redirect:/main";
			} 
		} catch (Exception e) {
			return view;
		}
		
		return "view;";
	}
    
	// 예외 step2 : DeptController 내부에서 발생하는 모든 NPE 처리
//	@ExceptionHandler(NullPointerException.class)
//	public String nullPointerExceptionHandler(Exception exception) {
//		return "error";
//	}
}

ExceptionHandlerExceptionResolver : 우선 순위가 가장 높음, FrontController 예외 처리


3. LoginController

@Controller
public class LoginController {
	
	@Autowired
	EmpService empService;
	
	// logingForm
	@RequestMapping(value = "/login", method = RequestMethod.GET)
	public String loginForm() {
		return "login";
	}
	
	// version1 : login, 예외처리 : try-catch
//	@RequestMapping(value = "/login", method = RequestMethod.POST)
//	public String login(@RequestParam("empno") int empno, // POST 방식도 @RequestParam 사용할 수있
//						@RequestParam("ename") String ename, // ModelAttribute는 객체를 받을 때 사용
//						HttpSession session, Model model) {
//
//		String view = "erorr";
//		Emp emp = null;
//		
//		try {
//			 emp = empService.getEmpByEmpnoAndEname(empno, ename);
//			 
//			 session.setAttribute("userId", emp.getEmpno());
//			 session.setAttribute("userName", emp.getEname());
//			 
//			 view = "redirect:/main";
//			 
//		} catch (Exception e) {
//			e.printStackTrace();
//			return view;
//		}
//		
//		return view;
//	}
	
	// version2 : login, @ExceptionHandler로 예외 처리
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	public String loginTest(@RequestParam("empno") int empno,
						@RequestParam("ename") String ename,
						HttpSession session,
						Model model) throws SQLException, Exception {
		
        String view = "error";
		
		Emp emp = empService.getEmpByEmpnoAndEname(empno, ename);
		
		session.setAttribute("userId", emp.getEmpno());
		session.setAttribute("userName", emp.getEname());
			 
		view = "redirect:/main";
		return view;
	}
	
	// logout
	@RequestMapping(value = "/logout", method = RequestMethod.GET)
	public String logout(HttpSession session) {
		
		if(session != null) {
			session.invalidate();
		}
		
		return "redirect:/main";
	}
	
	// 예외 version2 : LoginController 내부에서 발생하는 모든 예외 처리, xml을 점점 지양하는 방향
//	@ExceptionHandler(Exception.class)
//	public ModelAndView nullPointerExceptionHandler(Exception exception) {
//		ModelAndView mv = new ModelAndView();
//		mv.addObject("exception", exception);
//		mv.setViewName("error");
//		return mv;
//	}
}

4. GlobalExceptionHandler.java

// 예외 step3 : @ControllerAdvice, 공통적인 예외는 ControllerAdvice에 처리
@ControllerAdvice
public class GlobalExceptionHandler {

	@ExceptionHandler(Exception.class)
	public ModelAndView ExceptionHandler(Exception exception) {
		ModelAndView mv = new ModelAndView();
		mv.addObject("exception", exception);
		mv.setViewName("error");
		return mv;
	}
}


C. Service

1. DeptService.java

@Service
public class DeptService {
	@Autowired
	DeptMapper mapper;
	
	@Autowired
	SqlSession sqlSession;
	
	// List
	public List<Dept> getAllDepts() {
		return mapper.getAllDepts();
	}

	// 객체
	public Dept getDeptByDeptno(int deptno) { 
		return mapper.getDeptByDeptno(deptno);
	}
	
	// List - Map
	public List<HashMap<String, Object>> getAllDeptsHashMap() {
		return mapper.getAllDeptsHashMap();
	}

	// HashMap
	public HashMap<String, Object> getDeptHashMap(int deptno) {
		return mapper.getDeptHashMap(deptno);
	}
	
	public String getDnameByDeptnoAndLoc(int deptno, String loc) {
		// ver1 : HashMap
//		HashMap<String, Object> deptnoAndLoc = new HashMap<String, Object>();
//		deptnoAndLoc.put("deptno", deptno);
//		deptnoAndLoc.put("loc", loc);
		
//		return mapper.getDnameByDeptnoAndLoc(deptnoAndLoc);
		
		// ver2 : Param
		return mapper.getDnameByDeptnoAndLoc(deptno, loc);
	}

	// String
	public String getDnameByDeptno(int deptno) {
		String dname = "";
		
		try {
			dname = mapper.getDnameByDeptno(deptno);
			sqlSession.commit();
		} catch (SQLException e) {
			e.printStackTrace();
			sqlSession.rollback();
		}
		sqlSession.close();
		
		return dname;
	};
	
	// insert
	public boolean insertDept(Dept dept) throws SQLException, Exception {
		boolean result = false;
		
		int res = mapper.insertDept(dept);
		
		if(res != 0) {
			result = true;
		} else {
			throw new Exception("부서 생성 실패");
		}
		return result;
	}
	
	// update
	public boolean updateDnameAndLoc(Dept dept) throws SQLException, Exception {
		boolean result = false;
		
		int res = mapper.updateDnameAndLoc(dept);
		
		if(res != 0) {
			result = true;
		} else {
			throw new Exception("부서 수정 실패");
		}
		return result;
	}

	// delete
	public boolean deleteDeptByDeptno(int deptno) throws SQLException, Exception{
		boolean result = false;
		
		int res = mapper.deleteDeptByDeptno(deptno);
		
		if(res != 0) {
			result = true;
		} else {
			throw new Exception("부서 삭제 실패");
		}
		return result;
	}
}

2. EmpService.java

@Service
public class EmpService {
	@Autowired
	EmpMapper empMapper;
	
	public Emp getEmpByEmpnoAndEname(int empno, String ename) throws SQLException, Exception {
		Emp emp = empMapper.getEmpByEmpnoAndEname(empno, ename);
		
		if(emp == null) {
			throw new Exception("해당 사원 존재하지 않음");
		}	
		return emp;
	}
}


D. Mapper.Interface (프록시 서버)

1. DeptMapper.java

@Mapper
public interface DeptMapper {
	// List
	public List<Dept> getAllDepts();

	// 객체 - Dept
	public Dept getDeptByDeptno(int deptno);
	
	// HashMap
	public HashMap<String, Object> getDeptHashMap(int deptno);
	
	// List - Map
	public List<HashMap<String, Object>> getAllDeptsHashMap();
	
	// String - dname
	public String getDnameByDeptno(int deptno) throws SQLException;
	
	// ver1 : hashmap
//	public String getDnameByDeptnoAndLoc(HashMap<String, Object> deptnoAndLoc);
	
	// ver2 : Param
	public String getDnameByDeptnoAndLoc(@Param("deptno") int deptno, @Param("loc") String loc); 
	
	// insert - Dept, Mybatis는 insert의 기본 리턴값이 int임
	public int insertDept(Dept dept) throws SQLException;

	// update
	public int updateDnameAndLoc(Dept dept) throws SQLException;

	// delete
	public int deleteDeptByDeptno(int deptno) throws SQLException;
}

2. EmpMapper.java

2개 이상의 파라미터에서는 @Param을 사용함

@Mapper
public interface EmpMapper {
	public Emp getEmpByEmpnoAndEname(@Param("empno") int empno, @Param("ename") String ename) throws SQLException, Exception
}


E. Mapper.xml

1. DeptMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
  
 <mapper namespace="com.spring.mapper.DeptMapper">
 	 	
 	<select id="getAllDepts" resultType="com.spring.dto.Dept">
 		SELECT deptno, dname, loc FROM dept
 	</select>
 	
 	<select id="getDeptByDeptno" resultMap="selectDeptMap">
 		SELECT deptno, dname, loc FROM dept WHERE deptno = #{deptno}
 	</select>

 	<!-- 반환 객체 타입 - Map -->
 	<resultMap type="com.spring.dto.Dept" id="selectDeptMap">
 		<result column="deptno" property="deptno"/>
 		<result column="dname" property="dname"/>
 		<result column="loc" property="loc"/>
 	</resultMap>
 	
  	<select id="getAllDeptsHashMap" resultType="java.util.HashMap">
 		SELECT deptno, dname, loc FROM dept
 	</select>
 	
 	<select id="getDeptHashMap" resultMap="selectDeptHashMap">
 		SELECT dname, deptno, loc FROM dept WHERE deptno = #{deptno}
 	</select>
 	<resultMap type="java.util.HashMap" id="selectDeptHashMap">
 		<result column="deptno" property="deptno"/>
 		<result column="dname" property="dname"/>
 		<result column="loc" property="loc"/>
 	</resultMap>
 	
 	<select id="getDnameByDeptno" parameterType="_int" resultType="string">
		SELECT dname FROM dept WHERE deptno = #{deptno}
 	</select>
 	
 	<!-- ver1 -->
 	<!-- <select id="getDnameByDeptnoAndLoc" parameterType="hashmap" resultType="string"> -->
 	<!-- ver2 -->
 	<select id="getDnameByDeptnoAndLoc" resultType="string">
 		SELECT dname 
 		FROM dept 
 		WHERE deptno = #{deptno} AND loc = #{loc}
 	</select>

 	<insert id="insertDept" parameterType="com.spring.dto.Dept">
 		INSERT INTO dept (deptno, dname, loc) VALUES (#{deptno}, #{dname}, #{loc})
 	</insert>
 	
 	<update id="updateDnameAndLoc" parameterType="com.spring.dto.Dept">
 		UPDATE DEPT
 		SET dname = #{dname},
 		 	loc = #{loc}
 		 WHERE deptno = #{deptno}
 	</update>
 	
 	<delete id="deleteDeptByDeptno" parameterType="_int">
 		DELETE FROM DEPT
 		WHERE deptno = #{deptno}
 	</delete>
 </mapper>

2. EmpMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
 <mapper namespace="com.spring.mapper.EmpMapper">
 	<select id="getEmpByEmpnoAndEname" resultType="com.spring.dto.Emp">
 		SELECT 
			empno, 
			ename, 
			job, 
			mgr, 
			hiredate, 
			sal, 
			comm, 
			deptno
		FROM emp	 
 		WHERE empno = #{empno} AND ename = #{ename}
 	</select>
 </mapper>


F. xml (servlet, web)

1. servlet-context.xml

예외 처리 : SimpleMappingExceptionResolver
resources 매핑
prefix, suffix 설정

<?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="com.spring.mvc" />
	
	<!-- 예외 step01 : SimpleMappingExceptionResolver -->
	<!-- <beans:bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
		exceptionMappins
		<beans:property name="exceptionMappings">
			<beans:props>
				<beans:prop key="java.lang.Exception">error</beans:prop>
			</beans:props>
		</beans:property>
		<beans:property name="defaultErrorView" value="error" />
	</beans:bean> -->
	
</beans:beans>

2. web.xml

클라이언트에서 put, delete 를 사용하기위해 컨버터를 설정해야함

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>
	
	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- Processes application requests -->
	<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>

	<!-- put, delete를 연결 시켜주는 컨버터 -->
	<filter>
    	<filter-name>httpMethodFilter</filter-name>
    	<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
	</filter>

	<filter-mapping>
	    <filter-name>httpMethodFilter</filter-name>
	    <url-pattern>/*</url-pattern>
	</filter-mapping>
	
	<!-- 에러 / 예외 처리 -->	
	<error-page>
		<error-code>404</error-code>
		<location>/WEB-INF/views/error.jsp</location>
	</error-page>
</web-app>

2. 에러

2개 이상의 데이터를 mapper.xml에 전달하려면 @Param, HashMap을 사용해야함

대부분의 예외는 url mapping 예외나 제대로 전달하지 못한 경우가 많았음


3. 보완 해야 할 것

예외 처리에서 구조에 대한 이해가 너무 어려움

@ControllerAdvice를 사용해서 하나의 핸들러에서 공통적인 예외를 처리하는데 framework layer, Servlet layer 사이에서 처리되는 것인지 FrontController 에서 처리되는 것인지 잘 모르겠음


4. 느낀점

spring은 사용하기 편리한 것 같으면서 어려운 것 같다.

부족한 기능을 또 다른 전처리를 통해서 사용하는 방법이 많았다.

profile
잘해볼게요

0개의 댓글