ㅁ 하나의 서비스에 하나의 쿼리만 실행시, 쿼리가 성공적으로 수행되면 자동으로 스프링에서 커밋처리가 된다.
- 그런데 하나의 서비스에 여러 dml문을 수행할 때가 있다.
여러 dml문이 정상적으로 다 완료되었을 때만 커밋이 진행되어야 한다.
중간에 하나라도 예외가 발생하면 rollback해서 모든 dml문들이 취소되어야 한다.
- 첫번째 dml문이 성공적으로 수행되어도 두번째 dml문에서 예외가 발생하면 이 기능은 실패한 것이다.
ㅁ NoticeService
package com.br.sbatis.service;
import java.util.List;
import com.br.sbatis.dto.NoticeDto;
public interface NoticeService {
// 전체목록조회
List<NoticeDto> selectNoticeList();
// 번호로 공지사항 한 개 조회
NoticeDto selectNoticeByNo(int noticeNo);
// 공지사항 등록
int insertNotice(NoticeDto n);
// 공지사항 수정
int updateNotice(NoticeDto n);
// 다수의 번호들 가지고 공지사항 일괄삭제
int deleteNotice(String[] deleteNo);
// 트랜잭션 테스트용 메소드
int transactionTest();
}
ㅁ notice-mapper.xml
- 두개의 insert문을 하나의 기능으로 묶어서 실행할 예정이다.
- notice-mapper.xml에 작성한 insertNotice 쿼리를 사용한다.
content 컬럼은 not null이 아닌데 title 컬럼은 not null 제약조건이 걸려있다.
일부러 title 값을 주지 않아서 예외를 발생시켜 본다.
ㅁ NoticeServiceImpl
package com.br.sbatis.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.br.sbatis.dao.NoticeDao;
import com.br.sbatis.dto.NoticeDto;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class NoticeServiceImpl implements NoticeService {
private final NoticeDao noticeDao;
@Override
public List<NoticeDto> selectNoticeList() {
return noticeDao.selectNoticeList();
}
@Override
public NoticeDto selectNoticeByNo(int noticeNo) {
return noticeDao.selectNoticeByNo(noticeNo);
}
@Override
public int insertNotice(NoticeDto n) {
return noticeDao.insertNotice(n);
}
@Override
public int updateNotice(NoticeDto n) {
return noticeDao.updateNotice(n);
}
@Override
public int deleteNotice(String[] deleteNo) {
return noticeDao.deleteNotice(deleteNo);
}
@Override
public int transactionTest() {
int result = noticeDao.insertNotice(NoticeDto.builder().title("트랜잭션테스트제목1").content("트랜잭션테스트내용1").build()); // title, content 필드에 값을 담은 Notice 객체 생성
if(result > 0) {
result = noticeDao.insertNotice(NoticeDto.builder().content("실패예정").build()); // content 필드에만 값을 담은 Notice 객체 생성
}
return result;
}
}
- 첫번째 insertNotice 메소드 호출시에는 매개변수 2개짜리 NoticeDto 생성자가,
두번째 insertNotice 메소드 호출시에는 매개변수 1개짜리 NoticeDto 생성자가 필요하다.
- 그런데 NoticeDto에 생성자를 매번 추가하기 번거롭다.
또 다른사람과 클래스를 같이 쓸 때 생성자의 매개변수 개수와 타입이 같으면 겹쳐서 안되기도 했다.
- builder 패턴을 쓸예정이다.
- 필드명이 곧 메소드명이다. 필드를 선택해서 값을 담을 수가 있다.
메소드 체이닝으로 연이어 작성한다.
- 다 담고 마지막으로 build() 메소드를 호출한다.
- builder 패턴은 회사에서도 많이 쓴다.
- Dto 객체에 필드가 수십개 있는데 그 중 일부 필드에만 값을 담은 채로 생성하고 싶을 때 그때마다 매개변수 생성자를 만들 필요가 없다.
ㅁ NoticeDto
package com.br.sbatis.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class NoticeDto {
private int no;
private String title;
private String content;
}
- 매개변수 생성자를 사용할 예정이니 @AllArgsConstructor 어노테이션을 클래스 위에 붙인다.
매개변수 생성자를 자동으로 만들어준다.
- @Builder 어노테이션을 클래스 위에 붙인다. 롬복에서 제공한다.
ㅁ NoticeController
package com.br.sbatis.controller;
import java.util.List;
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.RequestMapping;
import com.br.sbatis.dto.NoticeDto;
import com.br.sbatis.service.NoticeService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequestMapping("/notice")
@RequiredArgsConstructor
@Controller
public class NoticeController {
private final NoticeService noticeService;
@GetMapping("/list.do") // /notice/list.do
public void noticeList(Model model) {
List<NoticeDto> list = noticeService.selectNoticeList();
log.debug("list: {}", list);
model.addAttribute("list", list);
// return "notice/list"; void여도 포워딩하려 한다. url mapping("/notice/list")를 가지고 포워딩한다.
}
@GetMapping("/detail.do") // /notice/detail.do
public void noticeDetail(int no, Model model) {
NoticeDto n = noticeService.selectNoticeByNo(no);
model.addAttribute("n",n);
}
@GetMapping("/enroll.do") // /notice/enroll.do
public void noticeEnroll() {}
// 이렇게만 작성해도 페이지 이동이 된다.
@PostMapping("/insert.do")
public String noticeInsert(NoticeDto n) {
int result = noticeService.insertNotice(n);
if(result > 0) { // 성공시 다시 목록페이지
return "redirect:/notice/list.do";
}else { // 실패시 메인페이지
return "redirect:/";
}
}
@GetMapping("/modify.do") // /notice/modify.do
public void noticeModify(int no, Model model) {
model.addAttribute("n", noticeService.selectNoticeByNo(no));
}
@PostMapping("/update.do")
public String noticeUpdate(NoticeDto n) {
int result = noticeService.updateNotice(n);
if(result > 0){ // 성공시 상세페이지
return "redirect:/notice/detail.do?no=" + n.getNo();
}else { // 실패시 목록페이지
return "redirect:/notice/list.do";
}
}
@PostMapping("/delete.do")
public String noticeDelete(String[] deleteNo) {
// String[] arr = request.getParameterValues("deleteNo") <- request 객체가 있었다면
// 체크박스 선택 안 한 경우 예외 처리
if (deleteNo == null || deleteNo.length == 0) {
// 선택 항목이 없을 경우 에러 메시지와 함께 목록 페이지로 이동
return "redirect:/notice/list.do?error=noSelection";
}
// 실행할 sql문 : delete from notice where no in (xx, xx, xx) <- 동적 쿼리
int result = noticeService.deleteNotice(deleteNo);
if(result == deleteNo.length){ // 성공시 목록페이지
return "redirect:/notice/list.do";
}else { // 실패시 메인페이지
return "redirect:/";
}
}
@GetMapping("/txtest.do")
public String transactionTest() {
noticeService.transactionTest();
return "redirect:/";
}
}
- transactionTest() 메소드를 생성한다.
ㅁ http://localhost:8888/sbatis/notice/txtest.do를 주소창에 입력해본다.
- 500에러가 뜨는건 정상이다.
첫번째 dml문은 실행되고 두번째 dml문 실행시 실패했다.
- 그런데 db에는 첫번째 dml문이 rollback되지 않고 데이터가 insert된 상태다.
- 하나의 기능에 하나의 dml문만 수행될 때는 상관없지만, 하나의 기능에 여러 dml문이 수행될 때는 내가 트랜잭션 처리를 따로 해줘야 한다.
- 그런데 트랜잭션 구문을 하나의 기능에 여러 dml문이 수행되는 모든 메소드에 작성하기는 번거롭다. (길기도 하고)
- AOP를 적용하여 한번만 따로 트랜잭션 구문을 작성할 수 있다.
============================================================================
ㅁ
- 트랜잭션 처리 코드를 자바로 하지 않고 xml 파일에 ~를 등록만 해두면 ~
우리가 직접 핵심 로직에 손을 댈 필요가 없다.
- 트랜잭션 처리를 AOP를 적용은 보통 root-context.xml에 적는다.
ㅁ root-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<bean class="org.apache.commons.dbcp2.BasicDataSource" id="dataSource" destroy-method="close">
<!-- <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" /> -->
<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy" />
<property name="url" value="jdbc:log4jdbc:oracle:thin:@localhost:1521:xe" />
<property name="username" value="sbatis" />
<property name="password" value="sbatis" />
</bean>
<bean class="org.mybatis.spring.SqlSessionFactoryBean" id="sqlSessionFactory">
<property name="configLocation" value="classpath:config/mybatis-config.xml" />
<property name="dataSource" ref="dataSource" />
</bean>
<bean class="org.mybatis.spring.SqlSessionTemplate" id="sqlSession">
<constructor-arg ref="sqlSessionFactory" />
</bean>
<!-- AOP를 이용한 트랜잭션 처리 -->
<!-- 1) 트랜잭션 매니저 빈으로 등록 (dataSource 객체 필요함) -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="txManager">
<property name="dataSource" ref=dataSource />
</bean>
<!-- 2) 트랜잭션 Advice 등록 -->
<tx:advice transaction-manager="txManager" id="txAdvice">
<tx:attributes>
<tx:method name="*" />
<tx:method name="select*" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 3) AOP 등록 -->
<aop:config>
<aop:pointcut expression="execution(* com.br.sbatis.service.*Impl.*(..))" id="txPointcut" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut" />
</aop:config>
</beans>
- 트랜잭션 매니저는 Spirng-jdbc 라이브러리에서 제공하는 클래스다.
DataSourceTransactionManager만 입력하고 ctrl + 스페이스바를 누르면 풀클래스명이 자동완성된다.
(1) 트랜잭션 매니저 빈으로 등록 (dataSource 객체 필요함)
(2) 트랜잭션 Advice 등록
(3) AOP 등록
- 트랜잭션 관련 태그(tx 태그)와 aop 관련 태그를 쓰려면 Namespaces 탭에서 선택을 해줘야 한다.
체크하면 무슨 창이 뜨는데 ok 한다.
- 첫번째 프로젝트 때도 Namespaces 탭에서 p를 체크한 적이 있었다.
- 이렇게 작성해도 된다. insert, update, delete로 시작하는 메소드명을 가진 메소드들.
ㅁ 서버키고 실행
- NoticeServiceImpl의 transactionTest() 메소드에서 넘기는 제목과 내용을 각각 "트랜잭션테스트제목2", "트랜잭션테스트내용2"로 변경했다.
- 다시 http://localhost:8888/sbatis/notice/txtest.do를 주소창에 입력해본다.
500 에러가 뜨는건 정상이다.
- db를 확인해봐야 한다. 아까와 달리 1번째 insert문의 데이터가 들어오지 않았다.
2번째 insert문에서 예외가 발생하자 1번째 insert문도 rollback되었다.