본문 바로가기
Spring

[Spring] AOP 트랜잭션 처리

by moca7 2024. 10. 23.

 

 

 

하나의 서비스에 하나의 쿼리만 실행시, 쿼리가 성공적으로 수행되면 자동으로 스프링에서 커밋처리가 된다.

- 그런데 하나의 서비스에 여러 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되었다. 

 

 

'Spring' 카테고리의 다른 글

[Spring] 파일 업로드(2) 한 개의 첨부파일 업로드  (1) 2024.10.23
[Spring] 파일 업로드(1) 세팅  (1) 2024.10.23
[Spring] @Around  (0) 2024.10.23
[Spring] AOP(2) @Before Advice, @After Advice  (0) 2024.10.23
[Spring] AOP(1) 세팅  (0) 2024.10.22