ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 학원 day92. 트랜잭션, 태그, 첨부파일
    기록 2023. 1. 12. 12:55

    스프링의 트랜잭션 처리

    - spring framework는 선언적 트랜잭션 처리, 프로그래밍적 트랜잭션 처리를 지원한다. (프로그래밍적 트랜잭션 처리는 사용하지 않음)

    - spring-tx.jar 라이브러리는 트랜잭션 처리를 지원하는 라이브러리다.

     

    트랜잭션

     - 여러 번의 데이터베이스 엑세스 작업을 하나의 논리적인 작업그룹으로 묶는 것이다.

    트랜잭션의 사용 목적

     - 여러번의 데이터베이스 엑세스 작업이 실행될 때마다 데이터베이스에 직접 반영시키지 않고, 

        commit/rollback 명령을 실행할 때 한꺼번에 반영/취소 시키는 것이다.

     - 데이터베이스 엑세스 작업에 대한 부분적인 성공을 허용하지 않게하기 위함이다.

     - 부분적인 성공을 허용하지 않기 때문에 데이터의 일관성을 유지할 수 있다.

     

     - 트랜잭션의 시작

      * 첫번째 DML(INSERT/UPDATE/DELETE/SELECT) 명령어가 실행될 때 시작된다. 

      * commit/rollback 명령어를 실행하면 기존 트랜잭션이 종료되고, 새 트랜잭션이 시작된다. 

     - 트랜잭션의 종료

      * commit/rollback 명령어를 실행하면 기존 트랜잭션이 종료된다.

     

    자바프로그램에서는 자동 커밋이 일어나고 있다. 

    선언적 트랜잭션 처리

    - 별도의 트랜잭션 코드없이 간단한 설정과 어노테이션을 이용해서 트랜잭션 처리를 수행하는 것이다.

    - 스프링의 선언적 트랜잭션 처리

     

    트랜잭션은 언제필요하고 어디에 사용하는지?

    여러번의 db엑세스 작업을 하나의 논리적인 구조로 묶고 논리적인 구조 안에서 실행되는 db엑세스 작업이 전부 성공하면 commit해서 반영하고, 한개라도 실패하면 rollback으로 전부 취소하게 만든다. 데이터의 일관성을 유지하기 위해서 사용한다. all or nothing 전부 반영하던지 전부 취소하기. 

     

    select가 여러번있으면 트랜잭션처리 대상이 아니다. db 변경작업이 없으니까.

    저장(insert), 변경(update), 삭제(delete) 작업이 두번 이상 있으면 트랜잭션처리 대상이 된다.

    (온라인쇼핑할 때, 주문하다 오류나면 처음부터 다시 오는 것과 같음)

    부분적인 성공을 허용하지 않는다. 

     

    서비스의 메소드가 트랜잭션처리의 적용대상이 된다. db엑세스 작업을 하는 곳이 서비스이기 때문에..

    서비스에서는 여러번의 db엑세스 작업이 있기 때문에 controller가 아닌 service에서 트랜잭션처리를 한다.

    업무로직을 컨트롤러가 아닌 서비스에 만드는 이유가 트랜잭션과도 연관이 있다.

    매퍼에서는 db엑세스 작업이 하나로 구성되어 있기 때문에 트랜잭션 처리를 하지 않는다.

     

    스프링은 트랜잭션 매니저를 제공해준다.

    @Transactional이라는 스프링에서 제공해주는 어노테이션을 붙여준다.

    트랜잭션 처리가 필요한 메소드마다 붙이거나 클래스에 붙인다.(클래스에 붙이면 트랜잭션처리가 모든 메소드에 적용된다.) 

     

    @Transactional

    - 트랜잭션처리가 필요한 메소드를 가지고 있는 인터페이스/클래스 혹은 해당 메소드에 사용되는 어노테이션이다.

    - 이 어노테이션이 인터페이스에서 사용하면, 인터페이스를 구현한 클래스의 모든 메소드가 실행될 때마다 트랜잭션처리가 지원된다.

    - 이 어노테이션을 메소드에서 사용하면, 해당 메소드가 실행될 때마다 트랜잭션처리가 지원된다.

     

    트랜잭션을 사용하기 위해서는 자동커밋기능을 끄는 코드를 작성해줘야 한다.

    메소드가 시작되기 직전에 트랜잭션을 실행하고, 메소드가 끝날 때 트랜잭션을 종료한다.

    예외가 발생하면 rollback을 실행한다.

     

    * @Transactional을 이용한 트랜잭션처리

            1. TransactionManager 트랜잭션 매니저 객체를 스프링컨테이너의 빈으로 등록시킨다.

               - 스프링은 PlatformTransactionManager 인터페이스를 구현한 다양한 트랜잭션매니저를 제공한다.

               - PlatformTransactionManager의 주요 API

                         TransactionStatus getTransaction(TransactionDefinition definition)

                              * 현재 진행중인 트랜잭션을 조회한다.

                          void commit(TransactionStatus status)

                              * 트랜잭션내의 모든 데이터베이스 엑세스 작업을 영구적으로 데이터베이스에 반영시킨다.

                          void rollback(TransactionStatus status)

                              * 트랜잭션내의 모든 데이터베이스 엑세스 작업의 데이터베이스 반영을 전부 취소시킨다. 

             - PlatformTransactionManager 인터페이스의 주요 구현 클래스

                         DataSourceTransactionManager

                                   - spring jdbc, ibatis, mybatis를 사용해서 데이터베이스 엑세스 작업을 수행했을 때 사용되는 트랜잭션 매니저

                         HibernateTransactionManager

                                   - hibernate를 사용해서 데이터베이스 엑세스 작업을 수행했을 때 사용된다. 

                         JpaTransactionManager (자바 영속화 api 트랜잭션 매니저, HibernateTransactionManager를 만든사람이 만듦, jpa가 표준(인터페이스)이고 구현체중에 Hibernate가 있다.)

                                  - Jpa를 사용해서 데이터베이스 엑세스 작업을 수행했을 때 사용된다.

                         JtaTransactionManager (글로벌/분산 트랜잭션)

                                  - Jta(글로벌/분산 트랜잭션)에 대한 트랜잭션처리를 지원한다. (두개 이상의 데이터베이스에 대해서 트랜잭션이 필요할 때 사용)

     

    다양한 트랜잭션 매니저가 있는데, 각각의 db엑세스 기술이 있는데 거기에 맞는 트랜잭션매니저가 있다.

    jdbc의 경우, commit, rollback 메소드를 이용해서 트랜잭션 처리를 한다. 마이바티스의 경우 beginTransaction, endTransaction을 사용해서 트랜잭션처리를 한다. 내가 사용하는 db엑세스 기술에 따라서, 트랜재션 처리하는 메소드가 다르다. 혼란스럽기 떄문에 트랜잭션 매니저라는 인터페이스를 만든 것이다. 각 기술에 맞게 commit, rollback메소드를 구현할 수 있도록 함. db엑세스 기술에 상관없이 commit, rollback으로 처리되도록 PlatformTransactionManager 인터페이스를 만들었다.

     

            - 선언적 트랜잭션처리가 적용된 메소드가 실행될 때 트랜잭션 매니저가 같이 실행된다. 

                    1. 트랜잭션 매니저가 실행된다.

                    2. 새로운 트랜잭션이 시작된다.

                    3. 메소드 내에서 데이터베이스 엑세스 작업을 수행할 때마다 해당 작업을 트랜잭션에 포함시킨다.

                    4. 메소드 실행이 완료되면 commit을 호출해서 트랜잭션에 포함된 모든 데이터베이스 엑세스 작업을 데이터베이스에 영구적으로 반영시킨다. 메소드 실행 중 예외(RuntimeException)가 발생하면 rollback을 호출해서 트랜잭션에 포함된 모든 데이터베이스 엑세스 작업을 취소시킨다. 

                    5. 트랜잭션 매니저가 종료된다. 

           -  트랜잭션 매니저를 스프링 컨테이너의 빈으로 등록시키기

                  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

                          <property name="dataSource" ref="dataSource"/>   // Connection Pool 객체를 의존성주입시킨다.

                  </bean>

         2. @Transactional 어노테이션을 활성화시키는 객체를 스프링 컨테이너의 빈을 등록시킨다. 

                     <tx:annotation-driven transaction-manager="transactionManager"/>

         3. 트랜잭션처리가 필요한 메소드를 포함하고 있는 인터페이스/클래스 혹은 해당 메소드에 @Transactional을 추가한다.

     

    * @Transactional의 주요 속성

        - isolation

            격리 레벨

               DEFAULT : 데이터베이스, 커넥션풀의 격리레벨이 적용된다. 대부분 READ_COMMITED다. 

               READ_UNCOMMITED  : 커밋되지 않은 행을 읽을 수 있다. 즉, 다른 트랜잭션이 변경하고 있는 행을 읽을 수 있다. (격리레벨이 가장 낮음)
               READ_COMMITED        : 커밋되지 않은 행은 읽을 수 없다. 즉, 다른 트랜잭션에서 변경하고 있는 행은 읽을 수 없다. (대부분의 격리 레벨에 해당)
               REPEATABLE_READ     : 트랜잭션이 종료되기 전까지는 반복해서 읽을 수 있다. 즉, 다른 트랜잭션에서 값을 변경할 수 없다.

               SERIALIZABLE : 모든 트랜잭션이 순서대로 처리된다.

        - propagation

            전파 규칙

                ★ REQUIRED : 기본값, 서비스에서 다른 서비스를 호출할 때 쓴다, 트랜잭션을 시작하고 다른 트랜잭션 대상이 있으면 하나의 같은 트랜잭션으로 묶는다. 

                ★ REQUIRES_NEW : 무조건 새로운 트랜잭션을 만든다, 이미 시작중인 트랜잭션과 별도의 트랜잭션을 실행시킨다, 서로 영향을 받지 않게 하기 위함이다. 

                 SUPPORTED : m2 메소드에서 수행중인 데이터베이스 엑세스 작업은 진행중인 트랜잭션이 있으면 그 트랜잭션에 포함시키고, 진행중인 트랜잭션이 없으면 새로운 트랜잭션을 시작하지 않는다.

                 NOT_SUPPORTED : m2 메소드에서 수행중인 데이터베이스 엑세스 작업은 진행중인 트랜잭션에 포함되지 않는다. 
                 NEVER : 트랜잭션처리 절대로 안할 것, 트랜잭션이 진행 중인상태에서 호출하면 오류남.
                 MANDATORY : 반드시 실행중인 트랜잭션이 있어야 한다.  컨트롤러에서 호출할 수 없음. 
                 NESTED : m2 메소드는 내포된 트랜잭션을 시작한다.

                                   m2 메소드의 데이터베이스 엑세스 작업은 m1 메소드에서 진행중인 트랜잭션에 영향을 주지 않는다.                                   m1 메소드의 데이터베이스 엑세스 작업은 m2 메소드에서 진행중인 내포된 트랜잭션에 영향을 준다.

                                   m2 메소드의 작업은 m1 메소드의 부수적인 작업이다.

                                   m2 메소드의 작업은 m1 메소드에 영향을 주지 말아야 한다.

        - readOnly

             읽기 전용 트랜잭션을 사용한다. (select 조회만 하는 경우에는 굳이 트랜잭션에 포함시키지 않아도 되므로 readonly=true로 설정한다.)

             해당 트랜잭션이 적용되면 INSERT/UPDATE/DELETE 작업을 수행했을 때 오류가 발생한다.

             @Transactional(readonly = true)   

        - rollbackFor

            : 롤백 대상이 되는 예외 클래스 (스프링의 선언적 트랜잭션처리에서는 무조건 RuntimeException의 자손만 롤백 대상 예외클래스이다.)

            : RuntimeException의 자손이 아닌 예외클래스를 롤백 대상인 예외클래스로 지정한다.

        - noRollbackFor

            : 롤백 대상이 되지 않은 예외 클래스 지정 

            : RuntimeException의 자손인 예외클래스를 롤백 대상이 아닌 예외클래스로 지정한다.           


    태그 구현하기

    태그

    실제로 서버로 제출되는 코드는 hidden으로 되어있는 아래코드라서 위의 코드에서 name을 적지 않았음

    event.which는 입력된 키의 아스키코드값을 반환한다.
    Enter키의 아스키코드값은 13이다.

    if (event.which == 13) { // Enter키를 입력했을 때
    // 입력필드의 값을 읽어온다.
    let value = $tagInput.val();
    // 버튼 모양의 태그를 생성한다.
    let tagBtn = `<small>#\${value} <a href=""><i class="bi bi-x"></i></a></small>`;
    // 히든 필드 태그를 생성한다.
    let tag = `<input type="hidden" name="tags" value="\${value}">`;

    // 각각의 div에 추가한다.
    $tagBtnBox.append(tagBtn);
    $tagBox.append(tag);

    // 입력필드의 값을 삭제한다.
    $tagInput.val("");

    // Enter 키 입력에 대한 기본 동작은 폼을 서버로 제출하는 것인데, 그 기본동작이 일어나지 않게 하기 위해서 false값을 반환한다.
    return false;
    }
    // Enter 키를 제외한 다른 키에 대해서는 기본동작이 일어나게 하기 위해서 true를 반환한다.
    return true;

     

    키 입력에 대한 기본 동작은 화면에 출력되는 것이다.
    엔터 키에 대한 기본 동작은 서버로 전송되는 것이다.


    스프링에서 첨부파일 업로드 구현하기 (multipart 요청 처리하기)

     

    1. apache commons fileupload 라이브러리 의존성을 추가한다.
    <dependency>
         <groupId>commons-fileupload</groupId>
         <artifactId>commons-fileupload</artifactId>
         <version>1.4</version>
    </dependency>

    2. MultipartResolver 객체를 스프링 컨테이너의 빈으로 등록시킨다.
    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <property name="defaultEncoding" value="UTF-8" />
    <property name="maxUploadSize" value="10485760" />
    <property name="maxUploadSizePerFile" value="10485760" />
    </bean>

    3. 폼 작성하기  
    <form method="post" action="insert" enctype="multipart/form-data">
    제목
    <input type="text" name="title" />
    내용
    <textarea name="content"></textarea>
    첨부파일
    <input type="file" name="upfile1" />
    <input type="file" name="upfile2" disabled />
    </form>


    4. 폼 클래스 작성하기
    public class PostRegisterForm {

    private String title;
    private String content;
    // 업로드된 첨부파일 정보를 표현하는  MultipartFile 객체가 대입되는 멤버변수
    private MultipartFile upfile1; // MultipartFile 객체가 들어있다.
    private MultipartFile upfile2; // null이 들어있다.
    // 업로드된 첨부파일의 이름이 대입되는 멤버변수
    private String filename1;
    private String filename2;

    // Getter, Setter 메소드 작성
    }

    MultipartFile
    * 업로드된 첨부파일 정보를 표현하는 객체다.
    * 파일이 업로드되지 않아도  MultipartFile 객체는 생성된다.
    * 주요 API
    boolean isEmpty()
    - 업로드된 파일이 없으면 true를 반환한다.
    String getOriginalFilename()
    - 업로드된 파일명을 반환한다.
    String getContentType()
    -  업로드된 파일의 컨텐츠 타입을 반환한다. 예) text/plain, image/png
    long getSize()
    - 업로드된 파일의 크기를 바이트 단위로 반환한다.
    byte[] getBytes()
    - 업로드된 파일 데이터를 반환한다.
    InputStream getInputStream()
    - 업로드된 파일을 읽어오는 스트림을 반환한다.
    void transferTo(File dest)
    - 지정된 목적지에 업로드된 파일을 전송한다.

    5. 요청핸들러 메소드에서 업로드된 첨부파일 처리하기
    - 업로드된 첨부파일의 이름을 조회해서 테이블에 저장되게 한다.
    - 업로드된 첨부파일을 지정된 위치에 저장시킨다.

    public class PostController {

    //@Value("${file.save.directory}")
    //private String directory;

    private String directory = "c:/files";

    @PostMapping("/insert")
    public String insert(@LoginUser LoginUserInfo loginUserInfo, PostRegisterForm form) {

    // 첨부파일 처리하기
    MultipartFile upfile1 = form.getUpfile1();
    MultipartFile upfile2 = form.getUpfile2();
    if (!upfile1.isEmpty()) {
    // 파일이름을 조회해서 Form객체에 저장한다.
    String filename1 = upfile1.getOriginalFilename();
    form.setFilename1(filename1);
    // 첨부파일을 지정된 위치에 복사한다(저장시킨다.)
    FileCopyUtils.copy(upfile1.getInputStream(), new FileOutputStream(new File(directory, filename1)));
    }

    if (!upfile2.isEmpty()) {
    String filename2 = upfile2.getOriginalFilename();
    form.setFilename2(filename2);
    FileCopyUtils.copy(upfile2.getInputStream(), new FileOutputStream(new File(directory, filename2)));
    }

    postService.insertPost(loginUserInfo.getUserId(), form);

    }
    }

     

    같은 시퀀스번호 부여하기

    방법1.

    시퀀스 번호를 얻는 쿼리를 따로 만들어두고 사용

     

    방법2.

    시퀀스번호를 얻는 쿼리를 별도로 만들지 않고, selectKey라는 태그를 메인쿼리 전에 작성하여 사용한다.

    댓글

Designed by Tistory.