본문 바로가기
BackEnd

6/5 - Mapper 클래스, 트랜잭션 제어

by Jiwon_Loopy 2025. 6. 6.
반응형

목차


Mapper 클래스를 활용하여 자동 매핑


  • 리소스 폴더 아래에 java 폴더아래와같은 경로, 이름으로 Mapper 클래스가 있을 시 SqlSessionTemplate 으로 경로를 명시해주지 않아도 매핑하여 사용 가능
  • Mapper가 자동으로 해당 XML을 찾아 매핑 시켜줌
  • Service는 Mapper인터페이스를 주입 받아 사용
  • 주의 할 점은 파일명의 맨 앞이 대문자여야함!
  • chap09 > StudentMapper 동일
  • Config에는 @MapperScan 어노테이션을, 해당 매퍼 클래스에는 @Mapper를 사용하여 밝혀주어야 한다.

 

package chap09;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface StudentMapper {

	List<StudentVO> index(StudentVO vo);
	int count(StudentVO vo);
	int insertStudent(StudentVO vo);
	int insertHobby(HobbyVO vo);
	StudentVO login(StudentVO vo);
}
package chap09;

import javax.sql.DataSource;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "chap09") // 컴포넌트 스캔
@MapperScan(basePackages = "chap09", annotationClass = Mapper.class) // @Mapper 어노테이션이 있는 인터페이스만 Proxy개체로 생성
@EnableTransactionManagement // 트랜잭션 활성화
public class MvcConfig implements WebMvcConfigurer {

	// 뷰리졸버 - 컨트롤러에서 포워딩할 경로(앞/뒤) 설정
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.jsp("/WEB-INF/views/", ".jsp");
	}
	
	// 비즈니스로직이 필요없는 페이지
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/home.do").setViewName("home");
	}
	
	// 정적리소스 처리(스프링이 아니라 톰캣이 처리하도록) 활성화
	// img, css, js..
	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer config) {
		config.enable();
	}
	
	// HikariCP
	@Bean
	public DataSource dataSource() {
		HikariDataSource dataSource = new HikariDataSource();
//		dataSource.setDriverClassName("oracle.jdbc.OracleDriver");
//		dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
		dataSource.setDriverClassName("net.sf.log4jdbc.sql.jdbcapi.DriverSpy");
		dataSource.setJdbcUrl("jdbc:log4jdbc:mariadb://127.0.0.1:3308/mysql");
		dataSource.setUsername("root");
		dataSource.setPassword("test1234");
		return dataSource;
	}
	
	// MyBatis
	@Bean
	public SqlSessionFactory sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean ssf = new SqlSessionFactoryBean();
		ssf.setDataSource(dataSource()); // 의존성주입 (DI)
		// mapper파일 위치 설정
//		PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
//		ssf.addMapperLocations(resolver.getResources("classpath:/mapper/**/*.xml"));
		return ssf.getObject();
	}
	
	// DAO에 주입될 객체 (Mapper 에서는 필요 없음)
	// sql실행하려고
//	@Bean
//	public SqlSessionTemplate sst() throws Exception {
//		return new SqlSessionTemplate(sqlSessionFactory()); // 의존성주입(생성자방식)
//	}
	
	// 트랜잭션매니저 빈 등록
	@Bean
	public TransactionManager tm() {
		TransactionManager tm = 
				new DataSourceTransactionManager(dataSource());
		return tm;
	}
	
	
}
// MyBatis
	@Bean
	public SqlSessionFactory sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean ssf = new SqlSessionFactoryBean();
		ssf.setDataSource(dataSource()); // 의존성주입 (DI)
		// mapper파일 위치 설정
//		PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
//		ssf.addMapperLocations(resolver.getResources("classpath:/mapper/**/*.xml"));
		return ssf.getObject();
	}
	
	// DAO에 주입될 객체 (Mapper 에서는 필요 없음)
	// sql실행하려고
//	@Bean
//	public SqlSessionTemplate sst() throws Exception {
//		return new SqlSessionTemplate(sqlSessionFactory()); // 의존성주입(생성자방식)
//	}
	

 

트랜잭션


  • 논리적인 하나의 작업 단위
  • 전부 실행되거나 전부 안되거나 (All or Nothing)
  • Config파일에 datasource를 넣어 사용을 명시
	// 트랜잭션매니저 빈 등록
	@Bean
	public TransactionManager tm() {
		TransactionManager tm = 
				new DataSourceTransactionManager(dataSource());
		return tm;
	}
  • 어노테이션으로 활성화
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "chap09") // 컴포넌트 스캔
@MapperScan(basePackages = "chap09", annotationClass = Mapper.class) // @Mapper 어노테이션이 있는 인터페이스만 Proxy개체로 생성
**@EnableTransactionManagement // 트랜잭션 활성화 << 여기**
public class MvcConfig implements WebMvcConfigurer {

실습


  • 실습 시나리오
    • 학생 등록
    • 두 개의 테이블 Insert
    • 1 : 학생 테이블
    • 2 : 취미 테이블
  • 학생 테이블 insert → insert 후 pk 조회 → 취미 테이블 insert → 부모(학생)테이블에서 가져온 pk로 등록(반복)

로그인 + 인터셉터


  • 인터셉터란?
    • 스프링 MVC에서 요청(Request)이 컨트롤러에 도달하기 전/후에 동작하는 가로채기 기능입니다.
    • 필터(Filter)와 유사하지만, 스프링 MVC에 더 밀접하게 통합되어 있음.
  • 사용 예시
    • 로그인 체크
    • 권한 검사
    • 로깅
    • 요청 시간 측정
    • 공통 처리 (예: 모든 요청에 공통적으로 적용되는 로직)
  • 흐름
    • 등록 (write.do)
      • 주소창으로 write.do로 접속 시 get방식으로 controller에 요청 전달
      • controller는 student/write를 반환하여 write.js페이지로 전달
      • write.js 에서 form으로 등록한 post 형식으로 controller로 값 전달
      • post 방식으로 controller로 요청 들어옴
      • jsp의 폼에서 name과 같은 데이터를 VO에서 찾아 매개 변수로 넣어줌
      • 서비스의 insert 함수 실행
      • insert 함수는 mapper를 주입받고, mapper는 해당 mapper.xml에서 id값을 찾아 해당 쿼리문을 수행하고 값을 파싱해줌
      • 이때, 취미 설정을 진행하는데 필요한 방금 들어온 studno의 키 가 필요, autoIncrement로 설정된 pk id값을 가져와 사용하기 위해 SELECT LAST_INSERT_ID() 로 가져온 키를 변수에 저장하여 세팅 후 db에 넣기
      • redirect로 등록 처리가 되었으면 index.jsp로 보냄
    • 로그인 (login.do)
      • 주소창, 혹은 a태그의 href로 들어온 url로 이동 시 controller의 get매핑
      • login.jsp로 이동됨
      • post의 login.do로 form입력 시 데이터 전송
      • login.do의 post로 controller에 요청이 들어옴
      • HttpSession 은 세션 사용을 위해, vo은 데이터, RedirectAttributes 는 메세지 알림을 위한 1회성 리다이렉트 메세지로 전송
      • service에서 login mapper를 통해 주입 받고, 해당 id를 mapper xml에서 찾아 매핑
      • pwd=MD5(#{pwd}) 는 MD5 형식으로 암호화 하겠다는 것
      • resultType으로 vo를 리턴하고, 컨트롤러에서 해당 vo를 login으로 저장
      • 성공 시 세션에 vo 저장 후 index.jsp로 리다이렉션
      • 실패 시 현재 페이지로 리다이렉트 후 1회성 alret를 띄움, addFlashAttribute 는 리다이렉션 시에도 데이터를 유지하고 싶을 때 사용, 세션에 저장, 1회성이며, 요청으로 사용 시 자동 삭제됨

코드


package chap09;

import lombok.Data;

@Data
public class HobbyVO {
	private int h_num;
	private String h_name;
	private int studno;
}
package chap09;

import javax.sql.DataSource;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.zaxxer.hikari.HikariDataSource;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "chap09") // 컴포넌트 스캔
@MapperScan(basePackages = "chap09", annotationClass = Mapper.class) // @Mapper 어노테이션이 있는 인터페이스만 Proxy개체로 생성
@EnableTransactionManagement // 트랜잭션 활성화
public class MvcConfig implements WebMvcConfigurer {

	// 뷰리졸버 - 컨트롤러에서 포워딩할 경로(앞/뒤) 설정
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.jsp("/WEB-INF/views/", ".jsp");
	}
	
	// 비즈니스로직이 필요없는 페이지
	@Override
	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/home.do").setViewName("home");
	}
	
	// 정적리소스 처리(스프링이 아니라 톰캣이 처리하도록) 활성화
	// img, css, js..
	@Override
	public void configureDefaultServletHandling(DefaultServletHandlerConfigurer config) {
		config.enable();
	}
	
	// HikariCP
	@Bean
	public DataSource dataSource() {
		HikariDataSource dataSource = new HikariDataSource();
//		dataSource.setDriverClassName("oracle.jdbc.OracleDriver");
//		dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
		dataSource.setDriverClassName("net.sf.log4jdbc.sql.jdbcapi.DriverSpy");
		dataSource.setJdbcUrl("jdbc:log4jdbc:mariadb://127.0.0.1:3308/mysql");
		dataSource.setUsername("root");
		dataSource.setPassword("test1234");
		return dataSource;
	}
	
	// MyBatis
	@Bean
	public SqlSessionFactory sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean ssf = new SqlSessionFactoryBean();
		ssf.setDataSource(dataSource()); // 의존성주입 (DI)
		// mapper파일 위치 설정
//		PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
//		ssf.addMapperLocations(resolver.getResources("classpath:/mapper/**/*.xml"));
		return ssf.getObject();
	}
	
	// DAO에 주입될 객체 (Mapper 에서는 필요 없음)
	// sql실행하려고
//	@Bean
//	public SqlSessionTemplate sst() throws Exception {
//		return new SqlSessionTemplate(sqlSessionFactory()); // 의존성주입(생성자방식)
//	}
	
	// 트랜잭션매니저 빈 등록
	@Bean
	public TransactionManager tm() {
		TransactionManager tm = 
				new DataSourceTransactionManager(dataSource());
		return tm;
	}
	
	
}
package chap09;

import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
public class StudentController {
	@Autowired
	private StudentService service;
	
	// url 매핑 메서드, 파라미터받아, 서비스호출, 저장소에 저장, 포워딩
	@GetMapping("/student/index.do")
	public String index(Model model, StudentVO vo) {
		log.info("vo:"+vo);
//		log.info("s_major1:"+Arrays.toString(vo.getS_major1()));
		Map<String ,Object> map = service.index(vo);
		model.addAttribute("list", map); // request저장소에 list라는 이름으로 map객체를 저장 
		return "student/index";
	}
	
	// 등록폼
	@GetMapping("/student/write.do")
	public String write() {
		
		return "student/write";
	}
	
	// 등록처리
	@PostMapping("/student/write.do")
	public String write(Model model, 
			StudentVO vo, 
			@RequestParam String[] h_name) {
		boolean result = service.insert(vo, h_name);
//		if (result) {
//			return "";
//		}
		return "redirect:index.do";
	}
	
	@GetMapping("/student/login.do")
	public void login() {
		
	}
	@PostMapping("/student/login.do")
	public String login(HttpSession sess, StudentVO vo,
			RedirectAttributes ra) { // 리다이렉트할때 전송할값
		StudentVO login = service.login(vo);
		if (login != null) { // 로그인 성공
			sess.setAttribute("loginSession", login);
			return "redirect:index.do";
		} else {
			// 값 저장(일회성)
			ra.addFlashAttribute("msg", "로그인정보가 올바르지 않습니다.");
			return "redirect:login.do";
		}
	}
}
package chap09;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface StudentMapper {

	List<StudentVO> index(StudentVO vo);
	int count(StudentVO vo);
	int insertStudent(StudentVO vo);
	int insertHobby(HobbyVO vo);
	StudentVO login(StudentVO vo);
}
package chap09;

import java.util.Map;

public interface StudentService {
	Map<String, Object> index(StudentVO vo);
	boolean insert(StudentVO svo, String[] names);
	StudentVO login(StudentVO vo);
}
package chap09;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class StudentServiceImpl implements StudentService {

	@Autowired
	private StudentMapper mapper;
	
	@Override
	public Map<String, Object> index(StudentVO vo) {
		System.out.println(mapper.getClass().getName());
		List<StudentVO> list = mapper.index(vo);
		
		Map<String, Object> map = new HashMap<>();
		map.put("list", list);
		map.put("count", mapper.count(vo));
		return map;
	}

	@Transactional
	@Override
	public boolean insert(StudentVO svo, String[] names) {
		// studno가 0
		System.out.println("insert 전 studno:"+svo.getStudno());
		int r = mapper.insertStudent(svo);
		// studno가 PK값
		System.out.println("insert 후 studno:"+svo.getStudno());
		
		if (r > 0) {
			// TODO 취미파라미터개수만큼 반복, student테이블에 방금 insert된 데이터의 PK도 가져와야돼, 취미명
            for (String name : names) {
                HobbyVO hvo = new HobbyVO();
                hvo.setH_name(name);
                // TODO student테이블의 PK
                hvo.setStudno(svo.getStudno());
                mapper.insertHobby(hvo);
            }
		}
		
		return r > 0;
	}

	@Override
	public StudentVO login(StudentVO vo) {
		
		return mapper.login(vo);
	}
	
}
package chap09;

import java.sql.Timestamp;

import lombok.Data;

@Data
public class StudentVO {
	private int studno;
	private String name;
	private String id;
	private String pwd;
	private int grade;
	private String jumin;
	private Timestamp birthday;
	private String tel;
	private int height;
	private int weight;
	private int major1;
	private int major2;
	private int profno;
	
	private String majorname;
	
	// 검색용도
	private int s_grade;
	private String s_type;
	private String s_word;
//	private int[] s_major1;
	private String s_major1;
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="chap09.StudentMapper">

	<sql id="searchSql">
		<where>
			<!-- 학년 -->
			<if test="s_grade > 0">
			 AND grade = #{s_grade}
			</if>
			<!-- 검색어 -->
			<if test="s_word != null and s_word != ''"> <!-- 검색한 경우 -->
				<!-- 전체 -->
				<if test="s_type == ''">
					AND (studno LIKE '%${s_word}%'
					OR student.name LIKE '%${s_word}%'
					OR id LIKE '%${s_word}%')
				</if>
				<!-- 번호/이름/아이디 -->
				<if test="s_type != ''">
					AND ${s_type} LIKE '%${s_word}%'
				</if>
			</if>
			<!-- 전공 -->
			<if test="s_major1 != null">
				AND major1 IN 
				<!-- 
				<foreach collection="s_major1" item="m" open="(" close=")" separator=",">
					#{m}
				</foreach>
				 -->
				 (${s_major1})
			</if>
		</where>
	</sql>

	<select id="index" parameterType="chap09.StudentVO" resultType="chap09.StudentVO">
		SELECT 
			student.name, 
			student.studno,
			student.id,
			student.grade,
			major.name majorname
		FROM student LEFT JOIN major ON student.major1 = major.code 
		<include refid="searchSql"/>
		ORDER BY studno DESC
	</select>
	
	<select id="count" parameterType="chap09.StudentVO" resultType="int">
		SELECT COUNT(*) FROM student
		<include refid="searchSql"/>
	</select>
	
	<!-- 학생테이블 insert -->
	<insert id="insertStudent" parameterType="chap09.StudentVO">
		INSERT INTO student (
			name, id, pwd, grade, jumin
		) VALUES (
			#{name}, #{id}, md5(#{pwd}), #{grade}, #{jumin}
		)
		<selectKey keyProperty="studno" resultType="int" order="AFTER">
			SELECT LAST_INSERT_ID()
		</selectKey>
	</insert>
	
	<!-- 취미테이블 insert -->
	<insert id="insertHobby" parameterType="chap09.HobbyVO">
		INSERT INTO hobby (
			h_name, studno
		) VALUES (
			#{h_name}, #{studno}
		)
	</insert>
	
	<!-- 로그인 -->
	<select id="login" parameterType="chap09.StudentVO" resultType="chap09.StudentVO">
		SELECT * FROM student WHERE id=#{id} AND pwd=MD5(#{pwd})
	</select>
	
	
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="student">

	<sql id="searchSql">
		<where>
			<!-- 학년 -->
			<if test="s_grade > 0">
			 AND grade = #{s_grade}
			</if>
			<!-- 검색어 -->
			<if test="s_word != null and s_word != ''"> <!-- 검색한 경우 -->
				<!-- 전체 -->
				<if test="s_type == ''">
					AND (studno LIKE '%${s_word}%'
					OR name LIKE '%${s_word}%'
					OR id LIKE '%${s_word}%')
				</if>
				<!-- 번호/이름/아이디 -->
				<if test="s_type != ''">
					AND ${s_type} LIKE '%${s_word}%'
				</if>
			</if>
			<!-- 전공 -->
			<if test="s_major1 != null">
				AND major1 IN 
				<!-- 
				<foreach collection="s_major1" item="m" open="(" close=")" separator=",">
					#{m}
				</foreach>
				 -->
				 (${s_major1})
			</if>
		</where>
	</sql>

	<select id="index" parameterType="chap06.student.StudentVO" resultType="chap06.student.StudentVO">
		SELECT 
			student.name, 
			student.studno,
			student.id,
			student.grade,
			major.name majorname
		FROM student JOIN major ON student.major1 = major.code 
		<include refid="searchSql"/>
		ORDER BY studno DESC
	</select>
	
	<select id="count" parameterType="chap06.student.StudentVO" resultType="int">
		SELECT COUNT(*) FROM student
		<include refid="searchSql"/>
	</select>
</mapper>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script>
let msg = '${msg}';
if (msg) {
	alert(msg);
}
</script>
</head>
<body>
<h1>로그인</h1>
<form action="login.do" method="post">
아이디 : <input type="text" name="id"><br>
비밀번호 : <input type="password" name="pwd"><br>
<input type="submit" value="로그인">
</form>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>학생 등록</h1>
<form action="write.do" method="post">
아이디 : <input type="text" name="id"><br>
비밀번호 : <input type="text" name="pwd"><br>
이름 : <input type="text" name="name"><br>
학년 : <input type="text" name="grade"><br>
주민번호 : <input type="text" name="jumin"><br>
취미 : 
<input type="checkbox" name="h_name" value="독서"> 독서 
<input type="checkbox" name="h_name" value="여행"> 여행 
<input type="checkbox" name="h_name" value="게임"> 게임 
<input type="checkbox" name="h_name" value="영화"> 영화<br>
<input type="submit" value="등록"> 

</form>
</body>
</html>

메모


  • Last_Insert_ID()
    • MySQL, MariaDB에서 마지막에 넣은 키를 가져오는 함수
  • jsp 전송폼 이름을 vo의 이름하고 통일시키기 (name)
728x90
반응형