{"id":27531300,"url":"https://github.com/simhani1/spring_transaction_db2","last_synced_at":"2025-10-04T20:42:50.389Z","repository":{"id":199127544,"uuid":"699929898","full_name":"simhani1/spring_transaction_db2","owner":"simhani1","description":"스프링 DB 2편 - 스프링 트랜잭션 이해","archived":false,"fork":false,"pushed_at":"2023-10-10T14:18:04.000Z","size":1039,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2023-10-10T22:36:46.217Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/simhani1.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-10-03T16:02:48.000Z","updated_at":"2023-10-10T22:36:46.217Z","dependencies_parsed_at":null,"dependency_job_id":"b9bc6f81-e530-45ec-ab96-25a74d55aa0e","html_url":"https://github.com/simhani1/spring_transaction_db2","commit_stats":null,"previous_names":["simhani1/spring_transaction_db2"],"tags_count":0,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simhani1%2Fspring_transaction_db2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simhani1%2Fspring_transaction_db2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simhani1%2Fspring_transaction_db2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/simhani1%2Fspring_transaction_db2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/simhani1","download_url":"https://codeload.github.com/simhani1/spring_transaction_db2/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249527135,"owners_count":21286376,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-18T17:50:08.966Z","updated_at":"2025-10-04T20:42:45.353Z","avatar_url":"https://github.com/simhani1.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"## 선언적 트랜잭션\n\n- 트랜잭션 어노테이션을 붙이면 프록시 방식의 AOP가 적용된다.\n\u003e 프록시 도입 전\n![img_1.png](img/img_1.png)\n\n\u003e 프록시 도입 후\n![img.png](img/img.png)\n\n\u003e 전체 과정\n![img_2.png](img/img_2.png)\n\n## 프록시 방식의 AOP\n\n- 트랜잭션 어노테이션이 붙어 있으면 트랜잭션 AOP는 대상 클래스의 프록시를 만들어 `스프링 컨테이너`에 등록한다.\n- 그래서 빈을 주입받는 경우 프록시 객체가 주입되는 것이다.\n![img_3.png](img/img_3.png)\n\n#### 프록시 객체 테스트 코드\n```java\n@Test\n    void proxyCheck() {\n        log.info(\"aop class={}\", basicService.getClass());\n        assertThat(AopUtils.isAopProxy(basicService)).isTrue();\n    }\n```\n- 결과(실제 BasicService 클래스가 아닌 CGLIB에 의해 만들어진 프록시 객체가 주입되는 것을 확인할 수 있다.)\n```text\n2023-10-04 01:41:31.166  INFO 3734 --- [    Test worker] hello.springtx.apply.TxBasicTest         : aop class=class hello.springtx.apply.TxBasicTest$BasicService$$EnhancerBySpringCGLIB$$562e918\n```\n\n#### 트랜잭션 시작, 종료 로그 찍기\n```properties\nlogging.level.org.springframework.transaction.interceptor=TRACE\n```\n\n#### 트랜잭션 시작, 종료 테스트 코드\n```java\n    @Slf4j\n    static class BasicService {\n        \n        @Transactional\n        public void tx() {\n            log.info(\"call tx\");\n            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();\n            log.info(\"tx active={}\", txActive);\n        }\n\n        public void nonTx() {\n            log.info(\"call nonTx\");\n            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();\n            log.info(\"tx active={}\", txActive);\n        }\n    }\n```\n\n- 결과\n```text\n2023-10-04 01:44:05.416 TRACE 3775 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.TxBasicTest$BasicService.tx]\n2023-10-04 01:44:05.419  INFO 3775 --- [    Test worker] h.s.apply.TxBasicTest$BasicService       : call tx\n2023-10-04 01:44:05.419  INFO 3775 --- [    Test worker] h.s.apply.TxBasicTest$BasicService       : tx active=true\n2023-10-04 01:44:05.420 TRACE 3775 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.TxBasicTest$BasicService.tx]\n2023-10-04 01:44:05.421  INFO 3775 --- [    Test worker] h.s.apply.TxBasicTest$BasicService       : call nonTx\n2023-10-04 01:44:05.421  INFO 3775 --- [    Test worker] h.s.apply.TxBasicTest$BasicService       : tx active=false\n```\n\n## 트랜잭션 적용 위치\n\n- 항상 구체적이고 자세한 것이 높은 우선순위를 가진다.\n- 메서드와 클래스가 있다면, 둘 중 메서드가 더 구체적인 것이다.\n\n## 트랜잭션 읽기, 쓰기 옵션\n\n#### 트랜잭션의 기본 옵션 값\n- 트랜잭션의 기본 옵션은 `readOnly=false`이다.\n\n#### 트랜잭션에 적용된 readOnly 옵션의 값을 반환\n`TransactionSynchronizationManager.isCurrentTransactionReadOnly`\n\n## 트랜잭션 AOP 주의사항 - 프록시 내부 호출1\n\n- 트랜잭션이 적용되려면 프록시 객체가 요청을 받아 트랜잭션을 처리하고 실제 객체를 호출해야 한다.\n- 그런데 대상 객체의 내부에서 트랜잭션이 붙어있는 다른 메서드를 호출한다면 트랜잭션이 적용되지 않는다.\n\n#### 내부 메서드 호출 시 트랜잭션 적용 여부 테스트 코드\n\n```java\n    static class CallService {\n\n        public void external() {\n            log.info(\"call external\");\n            printTxInfo();\n            internal();\n        }\n\n        @Transactional\n        public void internal() {\n            log.info(\"call internal\");\n            printTxInfo();\n        }\n\n        private void printTxInfo() {\n            boolean txActive = TransactionSynchronizationManager.isActualTransactionActive();\n            log.info(\"tx active={}\", txActive);\n        }\n    }\n```\n\n- 결과(`external()` 메서드를 호출하는 경우)\n```text\n2023-10-04 02:18:50.556  INFO 4355 --- [    Test worker] h.s.a.InternalCallV1Test$CallService     : call external\n2023-10-04 02:18:50.556  INFO 4355 --- [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx active=false\n2023-10-04 02:18:50.556  INFO 4355 --- [    Test worker] h.s.a.InternalCallV1Test$CallService     : call internal\n2023-10-04 02:18:50.556  INFO 4355 --- [    Test worker] h.s.a.InternalCallV1Test$CallService     : tx active=false\n```\n\n- `internal()` 메서드에는 트랜잭션이 적용되지 않고 있다.\n- 이는 프록시 객체에서 `internal()` 메서드를 호출하지 않고 실제 객체에서 메서드를 호출하고 있기 때문이다.\n\n#### 프록시 방식의 AOP 한계\n- `@Transaction`를 사용하는 트랜잭션 AOP는 프록시를 사용한다. 그리고 프록시를 사용하면 메서드 내부 호출에 프록시를 적용할 수 없다.\n- 이 문제를 해결하기 위해서는 `internal()` 메서드를 별도의 클래스로 분리시키는 것이 제일 간단한 방법 중 하나다.\n![img.png](img/img_4.png)\n\n- 결과(외부 클래스의 `internal()`메서드를 호출하는 경우)\n```text\n2023-10-04 02:25:47.099  INFO 4591 --- [    Test worker] h.s.a.InternalCallV2Test$CallService     : call external\n2023-10-04 02:25:47.100  INFO 4591 --- [    Test worker] h.s.a.InternalCallV2Test$CallService     : tx active=false\n\n2023-10-04 02:25:47.147 TRACE 4591 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.InternalCallV2Test$InternalService.internal]\n2023-10-04 02:25:47.153  INFO 4591 --- [    Test worker] h.s.a.InternalCallV2Test$InternalService : call internal\n2023-10-04 02:25:47.154  INFO 4591 --- [    Test worker] h.s.a.InternalCallV2Test$InternalService : tx active=true\n2023-10-04 02:25:47.154 TRACE 4591 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.InternalCallV2Test$InternalService.internal]\n```\n\n## 트랜잭션 AOP 주의 사항 - 초기화 시점\n\n#### `@PostConstruct`와 `@Transactional`을 함께 사용\n- `@PostConstruct`와 `@Transactional`을 함께 사용하면 트랜잭션이 적용되지 않는다.\n- 초기화 코드가 먼저 호출되고, 그 다음에 트랜잭션 AOP가 적용된다. 따라서 초기화 시점에 해당 메서드에 대한 트랜잭션은 얻을 수 없다.\n```java\n        @PostConstruct\n        @Transactional\n        public void initV1() {\n            boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();\n            log.info(\"Hello init @PostConstruct tx active={}\", isActive);\n        }\n```\n\n#### ApplicationReadyEvent 사용\n```java\n        @EventListener(value = ApplicationReadyEvent.class)\n        @Transactional\n        public void initV2() {\n            boolean isActive = TransactionSynchronizationManager.isActualTransactionActive();\n            log.info(\"Hello init ApplicationReadyEvent tx active={}\", isActive);\n        }\n```\n\n- 결과\n```text\n2023-10-04 03:14:45.018  INFO 5330 --- [    Test worker] hello.springtx.apply.InitTxTest$Hello    : Hello init @PostConstruct tx active=false\n\n2023-10-04 03:14:45.119 TRACE 5330 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.apply.InitTxTest$Hello.initV2]\n2023-10-04 03:14:45.123  INFO 5330 --- [    Test worker] hello.springtx.apply.InitTxTest$Hello    : Hello init ApplicationReadyEvent tx active=true\n2023-10-04 03:14:45.123 TRACE 5330 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.apply.InitTxTest$Hello.initV2]\n```\n\n## 예외와 트랜잭션 커밋, 롤백 - 기본\n\n#### 예외 발생 시 트랜잭션 AOP는 예외의 종류에 따라 트랜잭션을 커밋하거나 롤백한다.\n\n- 언체크 예외(`RuntimeException`, `Error`)\n  - 트랜잭션 롤백\n- 체크 예외(`Exception`)\n  - 트랜잭션 커밋\n\n#### rollbackFor 옵션을 사용하여 체크 예외가 발생할 때 트랜잭션을 롤백하도록 설정\n```java\n    @Slf4j\n    static class RollbackService {\n\n        // 런타임 예외 발생: 롤백\n        @Transactional\n        public void runtimeException() {\n            log.info(\"call runtimeException\");\n            throw new RuntimeException();\n        }\n\n        // 체크 예외 발생: 커밋\n        @Transactional\n        public void checkedException() throws MyException {\n            log.info(\"call checkedException\");\n            throw new MyException();\n        }\n\n        // 체크 예외 rollbackFor 지정: 롤백\n        @Transactional(rollbackFor = MyException.class)\n        public void rollbackFor() throws MyException {\n            log.info(\"call checkedException\");\n            throw new MyException();\n        }\n    }\n```\n\n- 결과 \n- 런타임 예외\n```text\n2023-10-07 02:03:13.059 DEBUG 30444 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.exception.RollbackTest$RollbackService.runtimeException]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-07 02:03:13.090 DEBUG 30444 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(427006214\u003copen\u003e)] for JPA transaction\n2023-10-07 02:03:13.092 DEBUG 30444 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@3815a7d1]\n2023-10-07 02:03:13.092 TRACE 30444 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.exception.RollbackTest$RollbackService.runtimeException]\n2023-10-07 02:03:13.096  INFO 30444 --- [    Test worker] h.s.e.RollbackTest$RollbackService       : call runtimeException\n2023-10-07 02:03:13.096 TRACE 30444 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.exception.RollbackTest$RollbackService.runtimeException] after exception: java.lang.RuntimeException\n2023-10-07 02:03:13.096 DEBUG 30444 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback\n2023-10-07 02:03:13.096 DEBUG 30444 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(427006214\u003copen\u003e)]\n2023-10-07 02:03:13.097 DEBUG 30444 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(427006214\u003copen\u003e)] after transaction\n```\n\n- 체크 예외\n```text\n2023-10-07 02:04:07.933 DEBUG 30459 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.exception.RollbackTest$RollbackService.checkedException]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-07 02:04:07.961 DEBUG 30459 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(427006214\u003copen\u003e)] for JPA transaction\n2023-10-07 02:04:07.962 DEBUG 30459 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@3815a7d1]\n2023-10-07 02:04:07.963 TRACE 30459 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.exception.RollbackTest$RollbackService.checkedException]\n2023-10-07 02:04:07.966  INFO 30459 --- [    Test worker] h.s.e.RollbackTest$RollbackService       : call checkedException\n2023-10-07 02:04:07.966 TRACE 30459 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.exception.RollbackTest$RollbackService.checkedException] after exception: hello.springtx.exception.RollbackTest$MyException\n2023-10-07 02:04:07.966 DEBUG 30459 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-07 02:04:07.966 DEBUG 30459 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(427006214\u003copen\u003e)]\n2023-10-07 02:04:07.967 DEBUG 30459 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(427006214\u003copen\u003e)] after transaction\n```\n\n- `@Transactional(rollbackFor = MyException.class)`\n```text\n2023-10-07 02:04:55.479 DEBUG 30471 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.exception.RollbackTest$RollbackService.rollbackFor]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,-hello.springtx.exception.RollbackTest$MyException\n2023-10-07 02:04:55.507 DEBUG 30471 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(329929969\u003copen\u003e)] for JPA transaction\n2023-10-07 02:04:55.509 DEBUG 30471 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@1d2d4d7a]\n2023-10-07 02:04:55.509 TRACE 30471 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.exception.RollbackTest$RollbackService.rollbackFor]\n2023-10-07 02:04:55.513  INFO 30471 --- [    Test worker] h.s.e.RollbackTest$RollbackService       : call checkedException\n2023-10-07 02:04:55.514 TRACE 30471 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.exception.RollbackTest$RollbackService.rollbackFor] after exception: hello.springtx.exception.RollbackTest$MyException\n2023-10-07 02:04:55.514 DEBUG 30471 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback\n2023-10-07 02:04:55.514 DEBUG 30471 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(329929969\u003copen\u003e)]\n2023-10-07 02:04:55.515 DEBUG 30471 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(329929969\u003copen\u003e)] after transaction\n```\n\n## 예외와 트랜잭션 커밋, 롤백 - 활용\n\n#### 왜 체크 예외는 커밋하고 언체크 예외는 롤백하는가?\n- 스프링은 기본적으로 체크 예외는 비즈니스 의미가 있을 때 사용하고, 런타임 예외는 복구 불가능한 예외로 가정한다.\n- 체크 예외\n  - 비즈니스 의미가 있을 때 사용(시스템에는 문제가 없고 비즈니스 상황에 문제가 있는 예외이다.)\n- 언체크 예외\n  - 복구 불가능한 예외\n\n#### 요구사항\n\u003e 주문을 하는데 상황에 따라 다음과 같이 조치한다.\n\u003e 1. 정상: 주문시 결제를 성공하면 주문 데이터를 저장하고 결제 상태를 완료 로 처리한다.\n\u003e 2. 시스템 예외: 주문시 내부에 복구 불가능한 예외가 발생하면 전체 데이터를 롤백한다.\n\u003e 3. 비즈니스 예외: 주문시 결제 잔고가 부족하면 주문 데이터를 저장하고, 결제 상태를 대기 로 처리한다. 이 경우 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내한다.\n\n- 정상 주문\n```text\n2023-10-09 01:17:39.883 TRACE 39452 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.order.OrderService.order]\n2023-10-09 01:17:39.889  INFO 39452 --- [    Test worker] hello.springtx.order.OrderService        : order 호출\n2023-10-09 01:17:39.890 DEBUG 39452 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1456947885\u003copen\u003e)] for JPA transaction\n2023-10-09 01:17:39.891 DEBUG 39452 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction\n2023-10-09 01:17:39.891 TRACE 39452 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]\n2023-10-09 01:17:39.895 DEBUG 39452 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-09 01:17:39.919 TRACE 39452 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]\n2023-10-09 01:17:39.919  INFO 39452 --- [    Test worker] hello.springtx.order.OrderService        : 결제 프로세스 진입\n2023-10-09 01:17:39.919  INFO 39452 --- [    Test worker] hello.springtx.order.OrderService        : 정상 승인\n2023-10-09 01:17:39.919  INFO 39452 --- [    Test worker] hello.springtx.order.OrderService        : 결제 프로세스 완료\n2023-10-09 01:17:39.919 TRACE 39452 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.order.OrderService.order]\n2023-10-09 01:17:39.919 DEBUG 39452 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-09 01:17:39.919 DEBUG 39452 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1456947885\u003copen\u003e)]\n2023-10-09 01:17:39.924 DEBUG 39452 --- [    Test worker] org.hibernate.SQL                        : insert into orders (pay_status, username, id) values (?, ?, ?)\n2023-10-09 01:17:39.926 DEBUG 39452 --- [    Test worker] org.hibernate.SQL                        : update orders set pay_status=?, username=? where id=?\n```\n\n 런타임 예외 발생(롤백)\n```text\n2023-10-09 01:18:32.953 TRACE 39467 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]\n2023-10-09 01:18:32.958 DEBUG 39467 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-09 01:18:32.986 TRACE 39467 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]\n2023-10-09 01:18:32.986  INFO 39467 --- [    Test worker] hello.springtx.order.OrderService        : 결제 프로세스 진입\n2023-10-09 01:18:32.986  INFO 39467 --- [    Test worker] hello.springtx.order.OrderService        : 시스템 예외 발생\n2023-10-09 01:18:32.986 TRACE 39467 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.order.OrderService.order] after exception: java.lang.RuntimeException: 시스템 예외\n2023-10-09 01:18:32.986 DEBUG 39467 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback\n2023-10-09 01:18:32.986 DEBUG 39467 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(448512468\u003copen\u003e)]\n2023-10-09 01:18:32.987 DEBUG 39467 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(448512468\u003copen\u003e)] after transaction\n2023-10-09 01:18:32.999 DEBUG 39467 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly\n2023-10-09 01:18:33.000 DEBUG 39467 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(1200612373\u003copen\u003e)] for JPA transaction\n2023-10-09 01:18:33.000 DEBUG 39467 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@197180a5]\n2023-10-09 01:18:33.000 TRACE 39467 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]\n2023-10-09 01:18:33.005 DEBUG 39467 --- [    Test worker] org.hibernate.SQL                        : select order0_.id as id1_0_0_, order0_.pay_status as pay_stat2_0_0_, order0_.username as username3_0_0_ from orders order0_ where order0_.id=?\n2023-10-09 01:18:33.008 TRACE 39467 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]\n```\n\n- 체크 예외(비즈니스 예외, 커밋) \n```text\n2023-10-09 01:19:52.679 TRACE 39484 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.order.OrderService.order]\n2023-10-09 01:19:52.684  INFO 39484 --- [    Test worker] hello.springtx.order.OrderService        : order 호출\n2023-10-09 01:19:52.686 DEBUG 39484 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(1456947885\u003copen\u003e)] for JPA transaction\n2023-10-09 01:19:52.686 DEBUG 39484 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction\n2023-10-09 01:19:52.686 TRACE 39484 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]\n2023-10-09 01:19:52.691 DEBUG 39484 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-09 01:19:52.712 TRACE 39484 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]\n2023-10-09 01:19:52.712  INFO 39484 --- [    Test worker] hello.springtx.order.OrderService        : 결제 프로세스 진입\n2023-10-09 01:19:52.712  INFO 39484 --- [    Test worker] hello.springtx.order.OrderService        : 잔고 부족 비즈니스 예외 발생\n2023-10-09 01:19:52.712 TRACE 39484 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.order.OrderService.order] after exception: hello.springtx.order.NotEnoughMoneyException: 잔고가 부족합니다.\n2023-10-09 01:19:52.713 DEBUG 39484 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-09 01:19:52.713 DEBUG 39484 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1456947885\u003copen\u003e)]\n2023-10-09 01:19:52.717 DEBUG 39484 --- [    Test worker] org.hibernate.SQL                        : insert into orders (pay_status, username, id) values (?, ?, ?)\n2023-10-09 01:19:52.719 DEBUG 39484 --- [    Test worker] org.hibernate.SQL                        : update orders set pay_status=?, username=? where id=?\n2023-10-09 01:19:52.721 DEBUG 39484 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(1456947885\u003copen\u003e)] after transaction\n2023-10-09 01:19:52.721  INFO 39484 --- [    Test worker] hello.springtx.order.OrderServiceTest    : 고객에게 잔고 부족을 알리고 별도의 계좌로 입금하도록 안내\n```\n\n## 스프링 트랜잭션 전파1 - 커밋, 롤백\n\n- 트랜잭션 획득 후 커밋\n```java\n    @Test\n    void commit() {\n        log.info(\"트랜잭션 시작\");\n        TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());\n\n        log.info(\"트랜잭션 커밋 시작\");\n        txManager.commit(status);\n        log.info(\"트랜잭션 커밋 완료\");\n    }\n```\n\n- 결과\n```text\n2023-10-09 01:35:42.842  INFO 39820 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 트랜잭션 시작\n2023-10-09 01:35:42.843 DEBUG 39820 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-09 01:35:42.843 DEBUG 39820 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@210158726 wrapping conn0: url=jdbc:h2:mem:898ca3c7-2d7b-479e-a521-b9385e70ac39 user=SA] for JDBC transaction\n2023-10-09 01:35:42.844 DEBUG 39820 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@210158726 wrapping conn0: url=jdbc:h2:mem:898ca3c7-2d7b-479e-a521-b9385e70ac39 user=SA] to manual commit\n2023-10-09 01:35:42.844  INFO 39820 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 트랜잭션 커밋 시작\n2023-10-09 01:35:42.844 DEBUG 39820 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit\n2023-10-09 01:35:42.845 DEBUG 39820 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@210158726 wrapping conn0: url=jdbc:h2:mem:898ca3c7-2d7b-479e-a521-b9385e70ac39 user=SA]\n2023-10-09 01:35:42.845 DEBUG 39820 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@210158726 wrapping conn0: url=jdbc:h2:mem:898ca3c7-2d7b-479e-a521-b9385e70ac39 user=SA] after transaction\n2023-10-09 01:35:42.845  INFO 39820 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 트랜잭션 커밋 완료\n```\n\n- 트랜잭션 획득 후 롤백\n```java\n    @Test\n    void rollback() {\n        log.info(\"트랜잭션 시작\");\n        TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute());\n\n        log.info(\"트랜잭션 롤백 시작\");\n        txManager.rollback(status);\n        log.info(\"트랜잭션 롤백 완료\");\n    }\n```\n\n- 결과\n```text\n2023-10-09 01:36:09.332  INFO 39829 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 트랜잭션 시작\n2023-10-09 01:36:09.332 DEBUG 39829 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-09 01:36:09.333 DEBUG 39829 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1688786293 wrapping conn0: url=jdbc:h2:mem:bb802044-3985-45f0-8e29-7a6bd92807a2 user=SA] for JDBC transaction\n2023-10-09 01:36:09.333 DEBUG 39829 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1688786293 wrapping conn0: url=jdbc:h2:mem:bb802044-3985-45f0-8e29-7a6bd92807a2 user=SA] to manual commit\n2023-10-09 01:36:09.333  INFO 39829 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 트랜잭션 롤백 시작\n2023-10-09 01:36:09.333 DEBUG 39829 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback\n2023-10-09 01:36:09.333 DEBUG 39829 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@1688786293 wrapping conn0: url=jdbc:h2:mem:bb802044-3985-45f0-8e29-7a6bd92807a2 user=SA]\n2023-10-09 01:36:09.333 DEBUG 39829 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1688786293 wrapping conn0: url=jdbc:h2:mem:bb802044-3985-45f0-8e29-7a6bd92807a2 user=SA] after transaction\n2023-10-09 01:36:09.333  INFO 39829 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 트랜잭션 롤백 완료\n```\n\n## 스프링 트랜잭션 전파2 - 트랜잭션 두 번 사용\n\n- 각각의 트랜잭션이 시작되면 서로에게 영향이 없다.\n```java\n    @Test\n    void double_commit_rollback() {\n        log.info(\"트랜잭션1 시작\");\n        TransactionStatus tx1 = txManager.getTransaction(new DefaultTransactionAttribute());\n        log.info(\"트랜잭션1 커밋\");\n        txManager.commit(tx1);\n\n        log.info(\"트랜잭션2 시작\");\n        TransactionStatus tx2 = txManager.getTransaction(new DefaultTransactionAttribute());\n        log.info(\"트랜잭션2 롤백\");\n        txManager.rollback(tx2);\n    }\n```\n\n- 결과\n```text\n2023-10-09 17:48:42.132  INFO 46554 --- [           main] hello.springtx.propogation.BasicTxTest   : 트랜잭션1 시작\n2023-10-09 17:48:42.132 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-09 17:48:42.133 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@2034388773 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA] for JDBC transaction\n2023-10-09 17:48:42.133 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@2034388773 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA] to manual commit\n2023-10-09 17:48:42.133  INFO 46554 --- [           main] hello.springtx.propogation.BasicTxTest   : 트랜잭션1 커밋\n2023-10-09 17:48:42.133 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit\n2023-10-09 17:48:42.133 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@2034388773 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA]\n2023-10-09 17:48:42.133 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@2034388773 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA] after transaction\n2023-10-09 17:48:42.134  INFO 46554 --- [           main] hello.springtx.propogation.BasicTxTest   : 트랜잭션2 시작\n2023-10-09 17:48:42.134 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-09 17:48:42.134 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@1607788159 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA] for JDBC transaction\n2023-10-09 17:48:42.134 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@1607788159 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA] to manual commit\n2023-10-09 17:48:42.135  INFO 46554 --- [           main] hello.springtx.propogation.BasicTxTest   : 트랜잭션2 롤백\n2023-10-09 17:48:42.135 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback\n2023-10-09 17:48:42.135 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@1607788159 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA]\n2023-10-09 17:48:42.135 DEBUG 46554 --- [           main] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@1607788159 wrapping conn0: url=jdbc:h2:mem:1e9af37d-dec6-41cf-a33c-1625aea75801 user=SA] after transaction\n```\n\n\u003e [!NOTE]\n\u003e \n\u003e 로그를 살펴보면 트랜잭션1, 2 모두 동일한 `conn0` 커넥션을 사용하고 있다. \n\u003e 이 둘은 서로 다른 커넥션으로 구분되어야 한다. \n\u003e 따라서 히카리 커넥션 풀에서 커넥션을 획득하면 실제 커넥션을 그대로 반환하지 않고, 내부 관리를 위해 히카리 프록시 커넥션 객체를 생성해서 반환한다. \n\u003e 이렇게 프록시 객체의 주소를 통해 동일한 커넥션을 따로 사용하는 것처럼 구분지을 수 있는 것이다.\n\n## 스프링 트랜잭션 전파3 - 전파 기본\n\n#### 외부 트랜잭션이 수행중인데, 내부 트랜잭션이 추가로 수행되는 경우\n- 스프링은 이 경우 외부 기본적으로 트랜잭션과 내부 트랜잭션을 묶어서 하나의 트랜잭션을 만들어 준다.\n- 스프링은 이 경우 이해를 돕기 위해 논리/물리 트랜잭션 개념을 분리하여 설명한다. \n- `물리 트랜잭션`\n  - 실제 DB에 적용되는 트랜잭션을 의미한다.\n- `논리 트랜잭션`\n  - 하나의 물리 트랜잭션으로 묶인 트랜잭션을 의미한다.\n  \n![img.png](img/img_5.png)\n\n\u003e [!IMPORTANT]\n\u003e \n\u003e 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.\n\u003e 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션은 롤백된다.\n\n## 스프링 트랜잭션 전파4 - 전파 예제\n\n#### 요청 흐름\n![img.png](img/img_6.png)\n\n#### 응답 흐름\n![img_1.png](img/img_7.png)\n\n- 결과\n```text\n2023-10-09 19:29:01.317  INFO 47493 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 외부 트랜잭션 시작\n2023-10-09 19:29:01.319 DEBUG 47493 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-09 19:29:01.319 DEBUG 47493 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@677726975 wrapping conn0: url=jdbc:h2:mem:ae3e24c9-b0dc-43e1-8474-ef435f000ce0 user=SA] for JDBC transaction\n2023-10-09 19:29:01.320 DEBUG 47493 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@677726975 wrapping conn0: url=jdbc:h2:mem:ae3e24c9-b0dc-43e1-8474-ef435f000ce0 user=SA] to manual commit\n2023-10-09 19:29:01.320  INFO 47493 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : outer.isNewTransaction() = true\n2023-10-09 19:29:01.320  INFO 47493 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 내부 트랜잭션 시작\n2023-10-09 19:29:01.320 DEBUG 47493 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction  // 신규 트랜잭션이 존재하는 경우 참여만 한다.\n2023-10-09 19:29:01.320  INFO 47493 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : inner.isNewTransaction() = false\n2023-10-09 19:29:01.320  INFO 47493 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 내부 트랜잭션 커밋  // 내부 트랜잭션이 커밋되어도 실제 물리 트랜잭션에 커밋이 되지 않는다.\n2023-10-09 19:29:01.320  INFO 47493 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 외부 트랜잭션 커밋  \n2023-10-09 19:29:01.320 DEBUG 47493 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit\n2023-10-09 19:29:01.320 DEBUG 47493 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@677726975 wrapping conn0: url=jdbc:h2:mem:ae3e24c9-b0dc-43e1-8474-ef435f000ce0 user=SA]\n2023-10-09 19:29:01.320 DEBUG 47493 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@677726975 wrapping conn0: url=jdbc:h2:mem:ae3e24c9-b0dc-43e1-8474-ef435f000ce0 user=SA] after transaction\n```\n\n\u003e [!IMPORTANT]\n\u003e\n\u003e 트랜잭션 매니저에 커밋을 호출한다고 항상 실제 커넥션에 물리 커밋이 발생하지 않는다.\n\u003e 신규 트랜잭션인 경우에만 실제 물리 커넥션을 사용해서 물리 커밋과 롤백을 수행한다.\n\u003e 신규 트랜잭션이 아니라면 실제 물리 커넥션을 사용하지 않는다.\n\u003e 따라서 내부 트랜잭션이 추가로 사용되면, 트랜잭션 매니저를 통해 논리 트랜잭션을 관리하고 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋되는 것이다.\n\n## 스프링 트랜잭션 전파5 - 외부 롤백\n\n- 논리 트랜잭션이 하나라도 롤백되면 전체 물리 트랜잭션은 롤백된다.\n- 앞서 봤듯이 외부 트랜잭션에서 시작한 물리 트랜잭션의 범위는 내부 트랜잭션까지 사용되므로, 외부 트랜잭션이 롤백되면 모든 내부 트랜잭션도 롤백된다.\n\n#### 응답 흐름\n![img.png](img/img_8.png)\n\n## 스프링 트랜잭션 전파5 - 내부 롤백\n\n- 내부 트랜잭션이 롤백되면 해당 트랜잭션은 외부 트랜잭션에 `rollback-only` 마크를 트랜잭션 동기화 매니저에 기록한다.\n```text\n2023-10-09 22:15:18.220 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Participating transaction failed - marking existing transaction as rollback-only\n```\n\n- 트랜잭션 매니저는 신규 트랜잭션인 경우 실제 커넥션에 커밋을 호출한다. 이때 트랜잭션 동기화 매니저에 롤백 전용(`rollback-only`)마크가 있는지 확인하다.\n- 시스템 입장에서는 커밋을 호출했지만 롤백되었다는 사실을 분명하게 알려줘야 한다.\n- 스프링은 이 경우 `UnexpectedRollbackException` 런타임 예외를 던져 커밋을 시도했지만 예상치 못한 롤백이 발생했음을 알린다.\n\n- 결과\n```text\n2023-10-09 22:15:18.218  INFO 49945 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 외부 트랜잭션 시작\n2023-10-09 22:15:18.219 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-09 22:15:18.219 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@694943195 wrapping conn0: url=jdbc:h2:mem:f8497e3a-b856-41ac-bf41-371c2e3df9e4 user=SA] for JDBC transaction\n2023-10-09 22:15:18.220 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@694943195 wrapping conn0: url=jdbc:h2:mem:f8497e3a-b856-41ac-bf41-371c2e3df9e4 user=SA] to manual commit\n2023-10-09 22:15:18.220  INFO 49945 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : outer.isNewTransaction() = true\n2023-10-09 22:15:18.220  INFO 49945 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 내부 트랜잭션 시작\n2023-10-09 22:15:18.220 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Participating in existing transaction\n2023-10-09 22:15:18.220  INFO 49945 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : inner.isNewTransaction() = false\n2023-10-09 22:15:18.220  INFO 49945 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 내부 트랜잭션 롤백\n2023-10-09 22:15:18.220 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Participating transaction failed - marking existing transaction as rollback-only\n2023-10-09 22:15:18.220 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Setting JDBC transaction [HikariProxyConnection@694943195 wrapping conn0: url=jdbc:h2:mem:f8497e3a-b856-41ac-bf41-371c2e3df9e4 user=SA] rollback-only\n2023-10-09 22:15:18.220  INFO 49945 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 외부 트랜잭션 커밋\n2023-10-09 22:15:18.242 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Global transaction is marked as rollback-only but transactional code requested commit\n2023-10-09 22:15:18.242 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback\n2023-10-09 22:15:18.242 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@694943195 wrapping conn0: url=jdbc:h2:mem:f8497e3a-b856-41ac-bf41-371c2e3df9e4 user=SA]\n2023-10-09 22:15:18.243 DEBUG 49945 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@694943195 wrapping conn0: url=jdbc:h2:mem:f8497e3a-b856-41ac-bf41-371c2e3df9e4 user=SA] after transaction\n```\n\n## 스프링 트랜잭션 전파7 - REQUIRES_NEW\n\n#### 요청 흐름\n![img.png](img/img_9.png)\n\n#### 응답 흐름\n![img.png](img/img_10.png)\n\n- 테스트\n```java\nlog.info(\"내부 트랜잭션 시작\");\nDefaultTransactionAttribute definition = new DefaultTransactionAttribute();\ndefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);\nTransactionStatus inner = txManager.getTransaction(definition);\nlog.info(\"inner.isNewTransaction() = {}\", inner.isNewTransaction());  // true\nlog.info(\"내부 트랜잭션 롤백\");\ntxManager.rollback(inner);\n```\n\n- 결과\n```text\n2023-10-09 22:29:18.158  INFO 50261 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 외부 트랜잭션 시작\n2023-10-09 22:29:18.159 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [null]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-09 22:29:18.159 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@181211485 wrapping conn0: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA] for JDBC transaction\n2023-10-09 22:29:18.160 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@181211485 wrapping conn0: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA] to manual commit\n2023-10-09 22:29:18.160  INFO 50261 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : outer.isNewTransaction() = true\n2023-10-09 22:29:18.160  INFO 50261 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 내부 트랜잭션 시작\n2023-10-09 22:29:18.160 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Suspending current transaction, creating new transaction with name [null]\n2023-10-09 22:29:18.160 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Acquired Connection [HikariProxyConnection@581639050 wrapping conn1: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA] for JDBC transaction\n2023-10-09 22:29:18.160 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Switching JDBC Connection [HikariProxyConnection@581639050 wrapping conn1: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA] to manual commit\n2023-10-09 22:29:18.161  INFO 50261 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : inner.isNewTransaction() = true\n2023-10-09 22:29:18.161  INFO 50261 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 내부 트랜잭션 롤백\n2023-10-09 22:29:18.161 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Initiating transaction rollback\n2023-10-09 22:29:18.161 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Rolling back JDBC transaction on Connection [HikariProxyConnection@581639050 wrapping conn1: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA]\n2023-10-09 22:29:18.161 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@581639050 wrapping conn1: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA] after transaction\n2023-10-09 22:29:18.161 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Resuming suspended transaction after completion of inner transaction\n2023-10-09 22:29:18.161  INFO 50261 --- [    Test worker] hello.springtx.propogation.BasicTxTest   : 외부 트랜잭션 커밋\n2023-10-09 22:29:18.161 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Initiating transaction commit\n2023-10-09 22:29:18.161 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Committing JDBC transaction on Connection [HikariProxyConnection@181211485 wrapping conn0: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA]\n2023-10-09 22:29:18.161 DEBUG 50261 --- [    Test worker] o.s.j.d.DataSourceTransactionManager     : Releasing JDBC Connection [HikariProxyConnection@181211485 wrapping conn0: url=jdbc:h2:mem:c1b7c09b-2d41-468e-ba48-13c43218bc6b user=SA] after transaction\n```\n\n- 외부/내부 트랜잭션이 서로 다른 물리 커넥션을 획득하고 각각의 트랜잭션을 사용하고 있다.\n- `REQUIRES_NEW` 옵션으로 외부 트랜잭션과 내부 트랜잭션을 완전히 분리해서 각각 별도의 물리 트랜잭션으로 사용할 수 있다.\n- 새로운 트랜잭션을 호출하면서 `conn1`은 보류되고 새로운 `conn2`를 내부 트랜잭션이 완료될 때까지 사용된다.\n\n\u003e [!NOTE]\n\u003e \n\u003e `REQUIRES_NEW` 옵션을 사용할 경우, 실제 DB 커넥션을 동시에 2개 사용하고 있다.\n\u003e 따라서 성능이 중요하거나 트래픽이 몰리는 경우 커넥션의 수가 부족하여 장애가 발생할 수도 있으니 조심해야 한다.\n\n## 트랜잭션 전파 활용2 - 커밋, 롤백\n\n#### Service: Transaction Off, Repository: Transaction On\n- 따라서 회원을 저장할 때, 로그를 저장할 때 서로 다른 별도의 트랜잭션 안에서 동작한다.\n- 로그를 저장할 때 예외가 발생하더라도 회원정보는 롤백되지 않고 저장된다.\n\n- 결과\n```text\n2023-10-10 18:55:10.577  INFO 60817 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 시작 ==\n2023-10-10 18:55:10.579 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.propogation.MemberRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-10 18:55:10.580 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(1846735756\u003copen\u003e)] for JPA transaction\n2023-10-10 18:55:10.581 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@38197e82]\n2023-10-10 18:55:10.581 TRACE 60817 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.MemberRepository.save]\n2023-10-10 18:55:10.586  INFO 60817 --- [    Test worker] h.springtx.propogation.MemberRepository  : member 저장\n2023-10-10 18:55:10.588 DEBUG 60817 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 18:55:10.614 TRACE 60817 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.MemberRepository.save]\n2023-10-10 18:55:10.615 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-10 18:55:10.615 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(1846735756\u003copen\u003e)]\n2023-10-10 18:55:10.622 DEBUG 60817 --- [    Test worker] org.hibernate.SQL                        : insert into member (username, id) values (?, ?)\n2023-10-10 18:55:10.627 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(1846735756\u003copen\u003e)] after transaction\n2023-10-10 18:55:10.628  INFO 60817 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 종료 ==\n2023-10-10 18:55:10.628  INFO 60817 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 시작 ==\n2023-10-10 18:55:10.628 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.propogation.LogRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-10 18:55:10.628 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(1427350635\u003copen\u003e)] for JPA transaction\n2023-10-10 18:55:10.628 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@5759edff]\n2023-10-10 18:55:10.628 TRACE 60817 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.LogRepository.save]\n2023-10-10 18:55:10.631  INFO 60817 --- [    Test worker] h.springtx.propogation.LogRepository     : log 저장\n2023-10-10 18:55:10.631 DEBUG 60817 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 18:55:10.632  INFO 60817 --- [    Test worker] h.springtx.propogation.LogRepository     : log 저장 시 예외 발생\n2023-10-10 18:55:10.634 TRACE 60817 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.LogRepository.save] after exception: java.lang.RuntimeException: 예외 발생\n2023-10-10 18:55:10.634 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback\n2023-10-10 18:55:10.635 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(1427350635\u003copen\u003e)]\n2023-10-10 18:55:10.635 DEBUG 60817 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(1427350635\u003copen\u003e)] after transaction\n2023-10-10 18:55:10.714 DEBUG 60817 --- [    Test worker] org.hibernate.SQL                        : select member0_.id as id1_1_, member0_.username as username2_1_ from member member0_ where member0_.username=?\n2023-10-10 18:55:10.720 DEBUG 60817 --- [    Test worker] org.hibernate.SQL                        : select log0_.id as id1_0_, log0_.message as message2_0_ from log log0_ where log0_.message=?\n```\n\n![img.png](img/img_11.png)\n\n\u003e [!NOTE]\n\u003e \n\u003e 이 경우 회원은 저장되지만, 회원 이력 로그는 롤백된다.\n\u003e 따라서 `데이터 정합성` 문제가 발생할 수 있다.\n\n## 트랜잭션 전파 활용3 - 단일 트랜잭션\n\n#### Service: Transaction On, Repository: Transaction Off\n- service가 시작할 때 모든 로직을 하나의 트랜잭션으로 묶인다.\n\n```text\n2023-10-10 21:47:30.166 DEBUG 62912 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.propogation.MemberService.joinV1]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-10 21:47:30.166 DEBUG 62912 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(2122810288\u003copen\u003e)] for JPA transaction\n2023-10-10 21:47:30.168 DEBUG 62912 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@7c47b0f8]\n2023-10-10 21:47:30.168 TRACE 62912 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.MemberService.joinV1]\n2023-10-10 21:47:30.173  INFO 62912 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 시작 ==\n2023-10-10 21:47:30.175  INFO 62912 --- [    Test worker] h.springtx.propogation.MemberRepository  : member 저장\n2023-10-10 21:47:30.178 DEBUG 62912 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 21:47:30.202  INFO 62912 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 종료 ==\n2023-10-10 21:47:30.202  INFO 62912 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 시작 ==\n2023-10-10 21:47:30.204  INFO 62912 --- [    Test worker] h.springtx.propogation.LogRepository     : log 저장\n2023-10-10 21:47:30.204 DEBUG 62912 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 21:47:30.205  INFO 62912 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 종료 ==\n2023-10-10 21:47:30.205 TRACE 62912 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.MemberService.joinV1]\n2023-10-10 21:47:30.205 DEBUG 62912 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-10 21:47:30.205 DEBUG 62912 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(2122810288\u003copen\u003e)]\n2023-10-10 21:47:30.211 DEBUG 62912 --- [    Test worker] org.hibernate.SQL                        : insert into member (username, id) values (?, ?)\n2023-10-10 21:47:30.213 DEBUG 62912 --- [    Test worker] org.hibernate.SQL                        : insert into log (message, id) values (?, ?)\n2023-10-10 21:47:30.214 DEBUG 62912 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(2122810288\u003copen\u003e)] after transaction\n2023-10-10 21:47:30.266 DEBUG 62912 --- [    Test worker] org.hibernate.SQL                        : select member0_.id as id1_1_, member0_.username as username2_1_ from member member0_ where member0_.username=?\n2023-10-10 21:47:30.294 DEBUG 62912 --- [    Test worker] org.hibernate.SQL                        : select log0_.id as id1_0_, log0_.message as message2_0_ from log log0_ where log0_.message=?\n```\n\n#### Repository의 트랜잭션을 따로 분리하고 싶다면?\n- 단순한 해결 방법은 트랜잭션이 있는 메서드와 없는 메서드를 따로 만들어서 호출 시점에 적절한 것을 선택하도록 설계해야 한다.\n- 그러나 좋지 않은 방법이고, 이를 해결할 수 있는 것이 `트랜잭션 전파`를 활용하는 것이다.\n\n## 트랜잭션 전파 활용4 - 전파 커밋\n\n#### Service: Transaction On, Repository: Transaction On\n- 모든 곳에 트랜잭션 어노테이션이 붙어있으므로 모든 논리 트랜잭션을 커밋해야 물리 트랜잭션도 커밋된다.\n- 하나라도 롤백되면 물리 트랜잭션은 롤백된다.\n![img.png](img/img_12.png)\n![img.png](img/img_13.png)\n\n```text\n2023-10-10 22:38:57.509 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.propogation.MemberService.joinV1]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-10 22:38:57.509 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(349399986\u003copen\u003e)] for JPA transaction\n2023-10-10 22:38:57.511 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@7b7b1448]\n2023-10-10 22:38:57.511 TRACE 63641 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.MemberService.joinV1]\n2023-10-10 22:38:57.517  INFO 63641 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 시작 ==\n2023-10-10 22:38:57.517 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(349399986\u003copen\u003e)] for JPA transaction\n2023-10-10 22:38:57.517 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction\n2023-10-10 22:38:57.518 TRACE 63641 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.MemberRepository.save]\n2023-10-10 22:38:57.520  INFO 63641 --- [    Test worker] h.springtx.propogation.MemberRepository  : member 저장\n2023-10-10 22:38:57.523 DEBUG 63641 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 22:38:57.550 TRACE 63641 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.MemberRepository.save]\n2023-10-10 22:38:57.550  INFO 63641 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 종료 ==\n2023-10-10 22:38:57.550  INFO 63641 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 시작 ==\n2023-10-10 22:38:57.550 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(349399986\u003copen\u003e)] for JPA transaction\n2023-10-10 22:38:57.550 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction\n2023-10-10 22:38:57.551 TRACE 63641 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.LogRepository.save]\n2023-10-10 22:38:57.554  INFO 63641 --- [    Test worker] h.springtx.propogation.LogRepository     : log 저장\n2023-10-10 22:38:57.554 DEBUG 63641 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 22:38:57.554 TRACE 63641 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.LogRepository.save]\n2023-10-10 22:38:57.554  INFO 63641 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 종료 ==\n2023-10-10 22:38:57.555 TRACE 63641 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.MemberService.joinV1]\n2023-10-10 22:38:57.555 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-10 22:38:57.555 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(349399986\u003copen\u003e)]\n2023-10-10 22:38:57.561 DEBUG 63641 --- [    Test worker] org.hibernate.SQL                        : insert into member (username, id) values (?, ?)\n2023-10-10 22:38:57.564 DEBUG 63641 --- [    Test worker] org.hibernate.SQL                        : insert into log (message, id) values (?, ?)\n2023-10-10 22:38:57.565 DEBUG 63641 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(349399986\u003copen\u003e)] after transaction\n2023-10-10 22:38:57.621 DEBUG 63641 --- [    Test worker] org.hibernate.SQL                        : select member0_.id as id1_1_, member0_.username as username2_1_ from member member0_ where member0_.username=?\n2023-10-10 22:38:57.647 DEBUG 63641 --- [    Test worker] org.hibernate.SQL                        : select log0_.id as id1_0_, log0_.message as message2_0_ from log log0_ where log0_.message=?\n```\n\n## 트랜잭션 전파 활용5 - 전파 롤백\n\n#### 내부 논리 트랜잭션에서 예외가 발생한 경우\n- rollback-only 마크를 트랜잭션 매니저에 남긴다.\n- 따라서 물리 트랜잭션을 커밋하는 시점에 해당 마크때문에 물리 트랜잭션이 롤백된다.\n\u003e 현재 예제에서는 RuntimeException을 던지기 때문에 트랜잭션 매니저가 실제로 rollback-only 마크를 참고하지는 않는다.\n\n![img.png](img/img_14.png)\n\n## 트랜잭션 전파 활용6 - 복구 REQUIRED\n\n- 앞서 회원가입과 로그를 저장하는 과정을 하나의 트랜잭션으로 묶어서 데이터 정합성 문제를 해결했다.\n- 그런데 로그를 기록하는 과정에 에러가 자주 발생하여 회원가입을 실패하는 경우가 많아졌고, 회원 이탈로 이어진다고 하자.\n- 로그 기록을 추후에 복구할 수 있는 방식으로 교체를 한다고 하자.\n\n#### 회원 가입을 시도한 로그를 남기는데, 실패하더라도 회원가입은 유지되도록 요구사항을 변경\n\u003e [!IMPORTANT]\n\u003e \n\u003e 아래 코드를 보면, 로그를 기록하다가 예외가 발생하면 처리를 하고 정상흐름을 진행시킨다.\n\u003e 이렇게 된다면 회원가입은 정상적으로 진행되고 로그만 기록하는데 실패할 것 같다.(많은 개발자가 이 실수를 하니 조심하자.)\n\u003e 그러나 실제로는 커밋되지 않고 전체 트랜잭션이 롤백된다.\n\n```java\n    @Transactional\n    public void joinV2(String username) {\n        Member member = new Member(username);\n        Log logMessage = new Log(username);\n\n        log.info(\"== memberRepository 호출 시작 ==\");\n        memberRepository.save(member);\n        log.info(\"== memberRepository 호출 종료 ==\");\n\n        log.info(\"== logRepository 호출 시작 ==\");\n        // 로그 저장에 실패해서 롤백하지 않도록\n        try {\n            logRepository.save(logMessage);\n        } catch (RuntimeException e) {\n            log.info(\"log 저장에 실패했습니다. logMessage={}\", logMessage.getMessage());\n            log.info(\"정상 흐름 반환\");\n        }\n        log.info(\"== logRepository 호출 종료 ==\");\n    }\n```\n\n#### 내부 트랜잭션에서의 롤백\n- 내부 트랜잭션에서 롤백이 발생하면 `rollback-only` 마크를 트랜잭션 매니저에게 기록하도록 한다.\n- 따라서 우리가 예외를 처리하여 정상흐름으로 동작하도록 설계했더라도, 물리 트랜잭션이 커밋되는 시점에 해당 마크때문에 롤백되고 `UnexpectedRollbackException` 예외를 발생시킨다.\n```text\n2023-10-10 22:58:22.081  INFO 64153 --- [    Test worker] h.springtx.propogation.MemberService     : log 저장에 실패했습니다. logMessage=로그예외_recoverException_fail\n2023-10-10 22:58:22.081  INFO 64153 --- [    Test worker] h.springtx.propogation.MemberService     : 정상 흐름 반환\n2023-10-10 22:58:22.081  INFO 64153 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 종료 ==\n2023-10-10 22:58:22.082 TRACE 64153 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.MemberService.joinV2]\n2023-10-10 22:58:22.082 DEBUG 64153 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-10 22:58:22.082 DEBUG 64153 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(179915314\u003copen\u003e)]\n2023-10-10 22:58:22.082 DEBUG 64153 --- [    Test worker] cResourceLocalTransactionCoordinatorImpl : On commit, transaction was marked for roll-back only, rolling back\n```\n\n- 흐름\n![img.png](img/img_15.png)\n\n## 트랜잭션 전파 활용7 - 복구 REQUIRES_NEW\n\n#### 트랜잭션 옵션 설정\n- 로그를 저장하는 과정의 트랜잭션을 아예 분리시키는 방법이다.\n- 로그를 저장하다 롤백이 발생해도 별도의 트랜잭션으로 분리되어 있으므로 물리 트랜잭션은 문제없이 커밋된다.\n- 그러나 하나의 HTTP 요청에 2개의 DB 커넥션을 사용하고 있으므로 성능 하락 문제를 신경써야 한다.\n```java\n    @Transactional(propagation = Propagation.REQUIRES_NEW)\n    public void save(Log logMessage) {\n        log.info(\"log 저장\");\n        em.persist(logMessage);\n        if (logMessage.getMessage().contains(\"로그예외\")) {\n            log.info(\"log 저장 시 예외 발생\");\n            throw new RuntimeException(\"예외 발생\");\n        }\n    }\n```\n\n- 결과\n```text\n2023-10-10 23:17:20.025 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [hello.springtx.propogation.MemberService.joinV2]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT\n2023-10-10 23:17:20.025 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(349399986\u003copen\u003e)] for JPA transaction\n2023-10-10 23:17:20.027 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@7b7b1448]\n2023-10-10 23:17:20.027 TRACE 64498 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.MemberService.joinV2]\n2023-10-10 23:17:20.031  INFO 64498 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 시작 ==\n2023-10-10 23:17:20.032 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(349399986\u003copen\u003e)] for JPA transaction\n2023-10-10 23:17:20.032 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Participating in existing transaction\n2023-10-10 23:17:20.032 TRACE 64498 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.MemberRepository.save]\n2023-10-10 23:17:20.034  INFO 64498 --- [    Test worker] h.springtx.propogation.MemberRepository  : member 저장\n2023-10-10 23:17:20.037 DEBUG 64498 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 23:17:20.057 TRACE 64498 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.MemberRepository.save]\n2023-10-10 23:17:20.058  INFO 64498 --- [    Test worker] h.springtx.propogation.MemberService     : == memberRepository 호출 종료 ==\n2023-10-10 23:17:20.058  INFO 64498 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 시작 ==\n2023-10-10 23:17:20.058 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Found thread-bound EntityManager [SessionImpl(349399986\u003copen\u003e)] for JPA transaction\n2023-10-10 23:17:20.058 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Suspending current transaction, creating new transaction with name [hello.springtx.propogation.LogRepository.save]\n2023-10-10 23:17:20.058 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Opened new EntityManager [SessionImpl(484103705\u003copen\u003e)] for JPA transaction\n2023-10-10 23:17:20.058 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@319058ce]\n2023-10-10 23:17:20.058 TRACE 64498 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Getting transaction for [hello.springtx.propogation.LogRepository.save]\n2023-10-10 23:17:20.061  INFO 64498 --- [    Test worker] h.springtx.propogation.LogRepository     : log 저장\n2023-10-10 23:17:20.061 DEBUG 64498 --- [    Test worker] org.hibernate.SQL                        : call next value for hibernate_sequence\n2023-10-10 23:17:20.062  INFO 64498 --- [    Test worker] h.springtx.propogation.LogRepository     : log 저장 시 예외 발생\n2023-10-10 23:17:20.063 TRACE 64498 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.LogRepository.save] after exception: java.lang.RuntimeException: 예외 발생\n2023-10-10 23:17:20.064 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction rollback\n2023-10-10 23:17:20.064 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Rolling back JPA transaction on EntityManager [SessionImpl(484103705\u003copen\u003e)]\n2023-10-10 23:17:20.065 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(484103705\u003copen\u003e)] after transaction\n2023-10-10 23:17:20.065 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Resuming suspended transaction after completion of inner transaction\n2023-10-10 23:17:20.065  INFO 64498 --- [    Test worker] h.springtx.propogation.MemberService     : log 저장에 실패했습니다. logMessage=로그예외_recoverException_success\n2023-10-10 23:17:20.065  INFO 64498 --- [    Test worker] h.springtx.propogation.MemberService     : 정상 흐름 반환\n2023-10-10 23:17:20.065  INFO 64498 --- [    Test worker] h.springtx.propogation.MemberService     : == logRepository 호출 종료 ==\n2023-10-10 23:17:20.065 TRACE 64498 --- [    Test worker] o.s.t.i.TransactionInterceptor           : Completing transaction for [hello.springtx.propogation.MemberService.joinV2]\n2023-10-10 23:17:20.065 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Initiating transaction commit\n2023-10-10 23:17:20.065 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(349399986\u003copen\u003e)]\n2023-10-10 23:17:20.070 DEBUG 64498 --- [    Test worker] org.hibernate.SQL                        : insert into member (username, id) values (?, ?)\n2023-10-10 23:17:20.072 DEBUG 64498 --- [    Test worker] o.s.orm.jpa.JpaTransactionManager        : Closing JPA EntityManager [SessionImpl(349399986\u003copen\u003e)] after transaction\n2023-10-10 23:17:20.125 DEBUG 64498 --- [    Test worker] org.hibernate.SQL                        : select member0_.id as id1_1_, member0_.username as username2_1_ from member member0_ where member0_.username=?\n2023-10-10 23:17:20.153 DEBUG 64498 --- [    Test worker] org.hibernate.SQL                        : select log0_.id as id1_0_, log0_.message as message2_0_ from log log0_ where log0_.message=?\n```\n\n#### 해결책\n- 커넥션을 두 개 사용하는 문제를 구조적으로 해결하는 방법 중 하나는 `퍼사드 패턴`을 사용하는 것이다.\n![img.png](img/img_16.png)\n- 그림과 같이 별도의 클래스에서 회원가입과 로그 기록 로직을 별도로 호출하게끔 하면 하나의 커넥션을 이용하여 처리가 가능해진다.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimhani1%2Fspring_transaction_db2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsimhani1%2Fspring_transaction_db2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsimhani1%2Fspring_transaction_db2/lists"}