ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [리팩토링] @Transactional 적용
    프로젝트/웹 ERD 프로젝트 2023. 11. 27. 22:12

    개요

    저번에 친구가

    dao.saveMail(mailDto); 이후에 예외가 발생하면 어떻게 될까?
    - 첨부파일은 이미 저장되어 있는데 삭제되어야 할까?
    - DB에 보낸 메일정보를 저장은 삭제되어야 할까?

    라는 문제를 줘서 @Transactional 에 대해서 찾아보면 정리를 좀 했었다.

    https://hsch19.tistory.com/13

     

    @Transactional이란 무엇인가

    개요 친구가 문제를 줬다. dao.saveMail(mailDto); 이후에 예외가 발생하면 어떻게 될까? - 첨부파일은 이미 저장되어 있는데 삭제되어야 할까? - DB에 보낸 메일정보를 저장은 삭제되어야 할까? 검색해

    hsch19.tistory.com

    그래서 이번엔 @Transactional을 사용해서 프로젝트를 리팩토링 해봤다.

     

    기존의 코드

    기존의 코드는 DB와 관련된 처리중 예외가 생길 경우를 생각하지 못했었다.

    때문에 에러가 발생하기 직전에 저장된 데이터에 대한 처리를 하지 않아서 에러가 발생하더라도 롤백이 되지 않았다.

    //메일 보내기
    public String sendMail(MailDTO mailDto, String addressListStr, MultipartFile attachmentFile) {
            //첨부파일 저장
            if(!attachmentFile.isEmpty()) {
                saveAttachmentFile(mailDto, attachmentFile);
            }
            dao.saveMail(mailDto);
    
            //보낸 메일 고유번호
            int recentMailNum = mailDto.getMail_num();
            //DB에 넘겨줄 MailRecDto
            MailRecDTO mailRecDto = new MailRecDTO();
            mailRecDto.setMail_num(recentMailNum);
    
            //Mail_Rec테이블 insert (MappingTable -- 받은 사람들 저장하기)
            String msg = "메일전송이 완료되었습니다!";
            int res = 0;
            int resCheck = 0;
    
            if(addressListStr != null) {
                List<String>addressList = Arrays.asList(addressListStr.split(" "));
                List<Integer> rec_numList = dao.findMemberNumByMailAddress(addressList);
                resCheck = rec_numList.size();
    
                for(int i=0; i<rec_numList.size(); i++) {
                    mailRecDto.setRec_num(rec_numList.get(i));
                    mailRecDto.setMail_receiver(addressList.get(i));
                    res += dao.saveReceiveTable(mailRecDto); //insert 작업
                }
                return msg;
            }
            
    	// (생략) ...
        
            return msg;
    	}

    순서로는

    1.첨부파일이 있다면 첨부파일이 저장되고,

    2. 메일의 내용을 DB에 insert해준다.

    3. 방금 저장된 메일의 고유 번호를 가지고 메핑테이블인 mail_rec 테이블에 정보를 넘겨줄 DTO(mailRecDTO)를 만든다.

    4. 받는사람(addressListStr)을 List<String>로 파싱 후, 받는 사람 수 만큼 반복문을 돌면서 DTO의 값을 바꿔주며 메핑테이블(mail_rec)에 데이터를 insert해준다. 

     

    하지만 트랜잭션을 써주지 않았기 때문에 4번 과정에서 에러가 발생하게 되면, 2번 과정은 DB에 저장된 채로 끝이 나버린다.

    mail 테이블
    매핑테이블 mail_rec테이블

    148번 메일이 저장됐지만 메핑테이블 저장과정에서 에러가 발생했기 때문에 148번 메일의 받는 사람이 저장되지 않은 모습을 볼 수 있다.

     

    수정된 코드

    //메일 보내기
    @Transactional
    public String sendMail(MailDTO mailDto, String addressListStr, MultipartFile attachmentFile) {
    
        dao.saveMail(mailDto);
    
        //보낸 메일 고유번호
        int recentMailNum = mailDto.getMail_num();
        //DB에 넘겨줄 MailRecDto
        MailRecDTO mailRecDto = new MailRecDTO();
        mailRecDto.setMail_num(recentMailNum);
    
        //Mail_Rec테이블 insert (MappingTable -- 받은 사람들 저장하기)
        String msg = "메일전송이 완료되었습니다!";
        int res = 0;
        int resCheck = 0;
    
        if(addressListStr != null) {
            List<String>addressList = Arrays.asList(addressListStr.split(" "));
            List<Integer> rec_numList = dao.findMemberNumByMailAddress(addressList);
            resCheck = rec_numList.size();
    
            for(int i=0; i<rec_numList.size(); i++) {
                mailRecDto.setRec_num(rec_numList.get(i));
                mailRecDto.setMail_receiver(addressList.get(i));
                res += dao.saveReceiveTable(mailRecDto); //insert 작업
            }
            return msg;
        }
    
    	// (생략) ...
    
        //첨부파일 저장
        if(!attachmentFile.isEmpty()) {
            saveAttachmentFile(mailDto, attachmentFile);
        }
        return msg;
    }

    @Transactional 어노테이션을 붙여줘서 한 트랜잭션 안에서 에러가 발생할 경우, 이전의 작업들이 다 롤백되는 것을 볼 수 있었다.

    그리고 이전에는 첨부파일을 먼저 저장한 뒤에 DB저장 작업을 진행했기 때문에 에러가 발생해도 첨부파일이 서버에 저장되었다. 그래서 첨부파일 저장 로직을 DB관련 작업이 끝난 후에 처리 되도록해서 DB저장이 성공하면 첨부파일이 저장되도록 바꿨다.

     

    그리고 이외에도 단순 조회를 통한 작업에 @Transactional(readOnly = true)를 달아줬다.

    @Transactional 작업을 처리하기 위해서는 많은 리소스가 사용된다.

    특히 Read를 제외한 CUD는 sql문은 이전 상태로 되돌릴 수 있어야하기에 이전 상태를 가지고 있어야한다. 하지만 Select문은 쿼리를 실행해도 이전 상태와 변화가 없으므로 해당 Transaction에 lock을 적용할 필요가 없다.

    그래서 @Transactional(readOnly = true) 옵션을 사용해서 해당 메서드가 읽기 전용임을 명시하고 영속성 컨텍스트에 관리를 받지 않게 할 수있다.

     

    그런데 사실, 영속성 컨텍스트는 JPA와 관련되는 내용으로 myBatis를 사용하는 현재 프로젝트와는 상관이 없다.
    오히려 프록시를 사용해서 더 느려질 수도 있으나 명시적으로 읽기전용 메서드임을 나타내서 가독성을 높이고...그냥 한 번 써보고 싶어서 달아봤다.. 

     

    영속성 컨텍스트 : 영속성 컨텍스트는 엔티티를 영구 저장하는 환경이라는 뜻이다. 영속성 컨텍스트는 애플리케이션과 DB 사이에서 객체를 보관하는 가상의 DB 역할을 한다. 엔티티 매니저(EntityManager)를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리하게 된다.

Designed by Tistory.