[{"data":1,"prerenderedAt":1664},["ShallowReactive",2],{"post-document:\u002Fjava\u002Fspring\u002F2026\u002F08\u002F21\u002Fjpa-transaction-concurrency\u002F":3},{"id":4,"title":5,"body":6,"categories":1641,"date":1643,"description":1644,"extension":1645,"image":1646,"key_concepts":1646,"last_modified_at":1646,"legacyPath":1647,"meta":1648,"navigation":263,"part":1650,"path":1651,"published":263,"robots":1646,"seo":1652,"series":1653,"stem":1654,"strengths":1646,"summary":1646,"tags":1655,"tradeoffs":1646,"__hash__":1663},"posts\u002Fposts\u002FBackend\u002FSPRING\u002FSpring\u002F2026-08-21-jpa-transaction-concurrency.md","Spring 기초 Part 9: JPA 트랜잭션과 동시성 제어",{"type":7,"value":8,"toc":1619},"minimark",[9,13,20,23,34,37,42,156,158,162,223,225,232,237,301,304,310,357,363,373,375,379,389,392,435,438,443,445,449,452,489,492,533,536,538,545,548,621,626,631,633,637,667,673,676,678,682,696,755,762,786,789,812,817,862,865,867,871,874,917,922,949,962,967,991,994,997,1000,1002,1006,1011,1081,1083,1087,1090,1143,1165,1171,1173,1177,1180,1241,1244,1276,1279,1285,1307,1310,1312,1316,1319,1352,1394,1414,1417,1419,1423,1477,1480,1503,1510,1512,1516,1584,1591,1595,1615],[10,11,5],"h2",{"id":12},"spring-기초-part-9-jpa-트랜잭션과-동시성-제어",[14,15,16],"blockquote",{},[17,18,19],"p",{},"트랜잭션은 여러 데이터베이스 작업을 하나의 성공 또는 실패 단위로 묶는 것이다.",[17,21,22],{},"모든 작업이 성공하면 커밋하고, 중간에 실패하면 전체 작업을 롤백한다.",[24,25,31],"pre",{"className":26,"code":28,"language":29,"meta":30},[27],"language-text","주문 생성\n   ↓\n재고 감소\n   ↓\n결제 정보 저장\n   ↓\n모두 성공 → COMMIT\n하나라도 실패 → ROLLBACK\n","text","",[32,33,28],"code",{"__ignoreMap":30},[35,36],"hr",{},[38,39,41],"h3",{"id":40},"_1-먼저-알아둘-단어","1. 먼저 알아둘 단어",[43,44,45,61],"table",{},[46,47,48],"thead",{},[49,50,51,55,58],"tr",{},[52,53,54],"th",{},"용어",[52,56,57],{},"정의",[52,59,60],{},"쉽게 말하면",[62,63,64,76,87,98,112,123,134,145],"tbody",{},[49,65,66,70,73],{},[67,68,69],"td",{},"트랜잭션",[67,71,72],{},"여러 DB 작업을 묶은 논리적인 작업 단위",[67,74,75],{},"주문 저장과 재고 감소를 함께 처리",[49,77,78,81,84],{},[67,79,80],{},"커밋",[67,82,83],{},"트랜잭션의 변경 내용을 최종 확정",[67,85,86],{},"성공한 작업을 DB에 반영",[49,88,89,92,95],{},[67,90,91],{},"롤백",[67,93,94],{},"트랜잭션의 변경 내용을 취소",[67,96,97],{},"실패 전 상태로 복구",[49,99,100,103,106],{},[67,101,102],{},"트랜잭션 경계",[67,104,105],{},"트랜잭션이 시작되고 끝나는 범위",[67,107,108,111],{},[32,109,110],{},"@Transactional"," 메서드",[49,113,114,117,120],{},[67,115,116],{},"전파",[67,118,119],{},"이미 트랜잭션이 있을 때 참여 방식을 정하는 규칙",[67,121,122],{},"기존 트랜잭션 참여 또는 새로 생성",[49,124,125,128,131],{},[67,126,127],{},"격리 수준",[67,129,130],{},"다른 트랜잭션의 변경을 어디까지 볼지 정하는 수준",[67,132,133],{},"커밋된 값만 읽기",[49,135,136,139,142],{},[67,137,138],{},"동시성",[67,140,141],{},"여러 작업이 같은 시간에 실행되는 성질",[67,143,144],{},"두 요청이 동시에 재고 차감",[49,146,147,150,153],{},[67,148,149],{},"락",[67,151,152],{},"같은 데이터의 동시 접근을 제어하는 장치",[67,154,155],{},"수정이 끝날 때까지 다른 작업 대기",[35,157],{},[38,159,161],{"id":160},"_2-acid-원칙","2. ACID 원칙",[43,163,164,177],{},[46,165,166],{},[49,167,168,171,174],{},[52,169,170],{},"원칙",[52,172,173],{},"의미",[52,175,176],{},"쉬운 예시",[62,178,179,190,201,212],{},[49,180,181,184,187],{},[67,182,183],{},"원자성(Atomicity)",[67,185,186],{},"작업이 모두 성공하거나 모두 실패해야 한다.",[67,188,189],{},"출금과 입금이 함께 처리되어야 한다.",[49,191,192,195,198],{},[67,193,194],{},"일관성(Consistency)",[67,196,197],{},"실행 전후에도 데이터 규칙을 만족해야 한다.",[67,199,200],{},"재고가 음수가 되지 않아야 한다.",[49,202,203,206,209],{},[67,204,205],{},"격리성(Isolation)",[67,207,208],{},"동시에 실행되는 트랜잭션의 중간 상태가 서로에게 영향을 주지 않아야 한다.",[67,210,211],{},"다른 사용자의 미완료 결제를 읽지 않는다.",[49,213,214,217,220],{},[67,215,216],{},"지속성(Durability)",[67,218,219],{},"커밋한 결과는 장애가 발생해도 보존돼야 한다.",[67,221,222],{},"완료된 주문이 DB에 남는다.",[35,224],{},[38,226,228,229,231],{"id":227},"_3-transactional-동작-원리","3. ",[32,230,110],{}," 동작 원리",[17,233,234,236],{},[32,235,110],{},"은 Spring이 트랜잭션 경계를 관리하도록 지정하는 어노테이션이다.",[24,238,242],{"className":239,"code":240,"language":241,"meta":30,"style":30},"language-java shiki shiki-themes github-light github-dark","@Service\npublic class OrderService {\n\n    @Transactional\n    public void createOrder(Order order, Product product) {\n        orderRepository.save(order);\n        product.decreaseStock(1);\n    }\n}\n","java",[32,243,244,252,258,265,271,277,283,289,295],{"__ignoreMap":30},[245,246,249],"span",{"class":247,"line":248},"line",1,[245,250,251],{},"@Service\n",[245,253,255],{"class":247,"line":254},2,[245,256,257],{},"public class OrderService {\n",[245,259,261],{"class":247,"line":260},3,[245,262,264],{"emptyLinePlaceholder":263},true,"\n",[245,266,268],{"class":247,"line":267},4,[245,269,270],{},"    @Transactional\n",[245,272,274],{"class":247,"line":273},5,[245,275,276],{},"    public void createOrder(Order order, Product product) {\n",[245,278,280],{"class":247,"line":279},6,[245,281,282],{},"        orderRepository.save(order);\n",[245,284,286],{"class":247,"line":285},7,[245,287,288],{},"        product.decreaseStock(1);\n",[245,290,292],{"class":247,"line":291},8,[245,293,294],{},"    }\n",[245,296,298],{"class":247,"line":297},9,[245,299,300],{},"}\n",[17,302,303],{},"Spring의 선언적 트랜잭션은 일반적으로 AOP Proxy를 통해 동작한다.",[17,305,306],{},[307,308,309],"strong",{},"Transactional Proxy 실행 흐름",[24,311,315],{"className":312,"code":313,"language":314,"meta":30,"style":30},"language-mermaid shiki shiki-themes github-light github-dark","flowchart TD\n    C[호출자] --> P[Transactional Proxy]\n    P --> B[트랜잭션 시작]\n    B --> S[실제 Service 메서드]\n    S --> Q{정상 종료?}\n    Q -->|예| F[flush]\n    F --> COMMIT[commit]\n    Q -->|롤백 대상 예외| ROLLBACK[rollback]\n","mermaid",[32,316,317,322,327,332,337,342,347,352],{"__ignoreMap":30},[245,318,319],{"class":247,"line":248},[245,320,321],{},"flowchart TD\n",[245,323,324],{"class":247,"line":254},[245,325,326],{},"    C[호출자] --> P[Transactional Proxy]\n",[245,328,329],{"class":247,"line":260},[245,330,331],{},"    P --> B[트랜잭션 시작]\n",[245,333,334],{"class":247,"line":267},[245,335,336],{},"    B --> S[실제 Service 메서드]\n",[245,338,339],{"class":247,"line":273},[245,340,341],{},"    S --> Q{정상 종료?}\n",[245,343,344],{"class":247,"line":279},[245,345,346],{},"    Q -->|예| F[flush]\n",[245,348,349],{"class":247,"line":285},[245,350,351],{},"    F --> COMMIT[commit]\n",[245,353,354],{"class":247,"line":291},[245,355,356],{},"    Q -->|롤백 대상 예외| ROLLBACK[rollback]\n",[24,358,361],{"className":359,"code":360,"language":29,"meta":30},[27],"Proxy가 메서드 호출을 받음\n          ↓\n트랜잭션 시작\n          ↓\n실제 Service 메서드 실행\n          ↓\n정상 종료          롤백 대상 예외\n   ↓                    ↓\nCOMMIT               ROLLBACK\n",[32,362,360],{"__ignoreMap":30},[17,364,365,366,369,370,372],{},"같은 객체 안에서 ",[32,367,368],{},"this.method()"," 형태로 호출하면 Proxy를 거치지 않아 새 ",[32,371,110],{}," 설정이 적용되지 않을 수 있다.",[35,374],{},[38,376,378],{"id":377},"_4-service에-트랜잭션을-적용하는-이유","4. Service에 트랜잭션을 적용하는 이유",[17,380,381,384,385,388],{},[32,382,383],{},"JpaRepository","의 기본 저장, 수정, 삭제 메서드에도 트랜잭션이 적용되어 있다. ",[32,386,387],{},"save()"," 하나만 호출하는 작업은 자체 트랜잭션으로 실행될 수 있다.",[17,390,391],{},"하지만 하나의 비즈니스 작업은 여러 Repository 호출로 구성될 수 있다.",[24,393,395],{"className":239,"code":394,"language":241,"meta":30,"style":30},"@Transactional\npublic void transfer(Long fromId, Long toId, int amount) {\n    Account from = accountRepository.findById(fromId).orElseThrow();\n    Account to = accountRepository.findById(toId).orElseThrow();\n\n    from.withdraw(amount);\n    to.deposit(amount);\n}\n",[32,396,397,402,407,412,417,421,426,431],{"__ignoreMap":30},[245,398,399],{"class":247,"line":248},[245,400,401],{},"@Transactional\n",[245,403,404],{"class":247,"line":254},[245,405,406],{},"public void transfer(Long fromId, Long toId, int amount) {\n",[245,408,409],{"class":247,"line":260},[245,410,411],{},"    Account from = accountRepository.findById(fromId).orElseThrow();\n",[245,413,414],{"class":247,"line":267},[245,415,416],{},"    Account to = accountRepository.findById(toId).orElseThrow();\n",[245,418,419],{"class":247,"line":273},[245,420,264],{"emptyLinePlaceholder":263},[245,422,423],{"class":247,"line":279},[245,424,425],{},"    from.withdraw(amount);\n",[245,427,428],{"class":247,"line":285},[245,429,430],{},"    to.deposit(amount);\n",[245,432,433],{"class":247,"line":291},[245,434,300],{},[17,436,437],{},"출금과 입금은 함께 성공하거나 함께 실패해야 한다. 따라서 Repository 메서드 하나가 아니라 Service의 비즈니스 작업 전체를 트랜잭션으로 묶는다.",[14,439,440],{},[17,441,442],{},"트랜잭션 경계는 데이터 접근 메서드 한 번이 아니라 하나의 완전한 업무 단위를 기준으로 잡는다.",[35,444],{},[38,446,448],{"id":447},"_5-트랜잭션-범위는-짧게-유지한다","5. 트랜잭션 범위는 짧게 유지한다",[17,450,451],{},"트랜잭션 안에서 외부 API나 파일 작업을 오래 수행하면 DB Connection과 Lock을 오랫동안 점유할 수 있다.",[24,453,455],{"className":239,"code":454,"language":241,"meta":30,"style":30},"@Transactional\npublic void processOrder(Order order) {\n    orderRepository.save(order);\n\n    externalPaymentApi.call(); \u002F\u002F 응답이 오래 걸릴 수 있음\n    uploadReceiptFile();       \u002F\u002F 파일 I\u002FO\n}\n",[32,456,457,461,466,471,475,480,485],{"__ignoreMap":30},[245,458,459],{"class":247,"line":248},[245,460,401],{},[245,462,463],{"class":247,"line":254},[245,464,465],{},"public void processOrder(Order order) {\n",[245,467,468],{"class":247,"line":260},[245,469,470],{},"    orderRepository.save(order);\n",[245,472,473],{"class":247,"line":267},[245,474,264],{"emptyLinePlaceholder":263},[245,476,477],{"class":247,"line":273},[245,478,479],{},"    externalPaymentApi.call(); \u002F\u002F 응답이 오래 걸릴 수 있음\n",[245,481,482],{"class":247,"line":279},[245,483,484],{},"    uploadReceiptFile();       \u002F\u002F 파일 I\u002FO\n",[245,486,487],{"class":247,"line":285},[245,488,300],{},[17,490,491],{},"가능하면 DB 작업의 경계를 명확하게 분리한다.",[43,493,494,504],{},[46,495,496],{},[49,497,498,501],{},[52,499,500],{},"작업",[52,502,503],{},"처리 방향",[62,505,506,517,525],{},[49,507,508,511],{},[67,509,510],{},"DB 조회와 변경",[67,512,513,514,516],{},"필요한 범위에 ",[32,515,110],{}," 적용",[49,518,519,522],{},[67,520,521],{},"파일 저장",[67,523,524],{},"DB 트랜잭션 밖에서 실행 고려",[49,526,527,530],{},[67,528,529],{},"외부 API 호출",[67,531,532],{},"DB 트랜잭션 밖에서 실행하고 실패 복구 설계",[17,534,535],{},"DB 커밋과 외부 시스템 호출은 하나의 로컬 트랜잭션으로 원자적으로 묶이지 않는다. 실제 시스템에서는 재시도, 보상 처리, 이벤트, Outbox 패턴 등을 추가로 고려할 수 있다.",[35,537],{},[38,539,541,542],{"id":540},"_6-readonly-true","6. ",[32,543,544],{},"readOnly = true",[17,546,547],{},"클래스에 조회 전용 설정을 두고 쓰기 메서드에서 덮어쓸 수 있다.",[24,549,551],{"className":239,"code":550,"language":241,"meta":30,"style":30},"@Service\n@Transactional(readOnly = true)\npublic class UserService {\n\n    public UserResponse getUser(Long id) {\n        User user = userRepository.findById(id).orElseThrow();\n        return UserResponse.from(user);\n    }\n\n    @Transactional\n    public Long createUser(User user) {\n        return userRepository.save(user).getId();\n    }\n}\n",[32,552,553,557,562,567,571,576,581,586,590,594,599,605,611,616],{"__ignoreMap":30},[245,554,555],{"class":247,"line":248},[245,556,251],{},[245,558,559],{"class":247,"line":254},[245,560,561],{},"@Transactional(readOnly = true)\n",[245,563,564],{"class":247,"line":260},[245,565,566],{},"public class UserService {\n",[245,568,569],{"class":247,"line":267},[245,570,264],{"emptyLinePlaceholder":263},[245,572,573],{"class":247,"line":273},[245,574,575],{},"    public UserResponse getUser(Long id) {\n",[245,577,578],{"class":247,"line":279},[245,579,580],{},"        User user = userRepository.findById(id).orElseThrow();\n",[245,582,583],{"class":247,"line":285},[245,584,585],{},"        return UserResponse.from(user);\n",[245,587,588],{"class":247,"line":291},[245,589,294],{},[245,591,592],{"class":247,"line":297},[245,593,264],{"emptyLinePlaceholder":263},[245,595,597],{"class":247,"line":596},10,[245,598,270],{},[245,600,602],{"class":247,"line":601},11,[245,603,604],{},"    public Long createUser(User user) {\n",[245,606,608],{"class":247,"line":607},12,[245,609,610],{},"        return userRepository.save(user).getId();\n",[245,612,614],{"class":247,"line":613},13,[245,615,294],{},[245,617,619],{"class":247,"line":618},14,[245,620,300],{},[17,622,623,625],{},[32,624,544],{},"는 조회 전용 의도를 표현하고 JPA 구현체나 DB 설정에 따라 flush와 변경 감지 비용을 줄이는 최적화에 활용될 수 있다.",[14,627,628],{},[17,629,630],{},"모든 환경에서 쓰기 SQL을 완전히 차단하는 보안 장치는 아니다.",[35,632],{},[38,634,636],{"id":635},"_7-flush와-commit","7. flush와 commit",[43,638,639,649],{},[46,640,641],{},[49,642,643,646],{},[52,644,645],{},"구분",[52,647,648],{},"역할",[62,650,651,659],{},[49,652,653,656],{},[67,654,655],{},"flush",[67,657,658],{},"영속성 컨텍스트의 변경 내용을 SQL로 DB에 전달",[49,660,661,664],{},[67,662,663],{},"commit",[67,665,666],{},"트랜잭션 변경 내용을 최종 확정",[24,668,671],{"className":669,"code":670,"language":29,"meta":30},[27],"Entity 변경\n   ↓\nflush\n   ↓\nUPDATE SQL 전달\n   ↓\ncommit\n",[32,672,670],{"__ignoreMap":30},[17,674,675],{},"flush가 실행된 뒤에도 commit 전에 오류가 발생하면 rollback할 수 있다.",[35,677],{},[38,679,681],{"id":680},"_8-예외와-롤백-규칙","8. 예외와 롤백 규칙",[17,683,684,685,687,688,691,692,695],{},"Spring의 ",[32,686,110],{},"은 기본적으로 ",[32,689,690],{},"RuntimeException","과 ",[32,693,694],{},"Error","가 메서드 밖으로 전달될 때 롤백한다.",[43,697,698,711],{},[46,699,700],{},[49,701,702,704,707],{},[52,703,645],{},[52,705,706],{},"예시",[52,708,710],{"align":709},"center","기본 동작",[62,712,713,726,739],{},[49,714,715,719,724],{},[67,716,717],{},[32,718,694],{},[67,720,721],{},[32,722,723],{},"OutOfMemoryError",[67,725,91],{"align":709},[49,727,728,732,737],{},[67,729,730],{},[32,731,690],{},[67,733,734],{},[32,735,736],{},"IllegalArgumentException",[67,738,91],{"align":709},[49,740,741,744,753],{},[67,742,743],{},"Checked Exception",[67,745,746,749,750],{},[32,747,748],{},"IOException",", ",[32,751,752],{},"SQLException",[67,754,80],{"align":709},[17,756,757,758,761],{},"Checked Exception도 롤백하려면 ",[32,759,760],{},"rollbackFor","를 지정한다.",[24,763,765],{"className":239,"code":764,"language":241,"meta":30,"style":30},"@Transactional(rollbackFor = Exception.class)\npublic void process() throws MyCustomException {\n    \u002F\u002F DB 작업\n}\n",[32,766,767,772,777,782],{"__ignoreMap":30},[245,768,769],{"class":247,"line":248},[245,770,771],{},"@Transactional(rollbackFor = Exception.class)\n",[245,773,774],{"class":247,"line":254},[245,775,776],{},"public void process() throws MyCustomException {\n",[245,778,779],{"class":247,"line":260},[245,780,781],{},"    \u002F\u002F DB 작업\n",[245,783,784],{"class":247,"line":267},[245,785,300],{},[17,787,788],{},"특정 예외를 롤백 대상에서 제외할 수도 있다.",[24,790,792],{"className":239,"code":791,"language":241,"meta":30,"style":30},"@Transactional(noRollbackFor = NotificationException.class)\npublic void process() {\n    \u002F\u002F DB 작업\n}\n",[32,793,794,799,804,808],{"__ignoreMap":30},[245,795,796],{"class":247,"line":248},[245,797,798],{},"@Transactional(noRollbackFor = NotificationException.class)\n",[245,800,801],{"class":247,"line":254},[245,802,803],{},"public void process() {\n",[245,805,806],{"class":247,"line":260},[245,807,781],{},[245,809,810],{"class":247,"line":267},[245,811,300],{},[813,814,816],"h4",{"id":815},"예외를-메서드-안에서-잡으면","예외를 메서드 안에서 잡으면",[24,818,820],{"className":239,"code":819,"language":241,"meta":30,"style":30},"@Transactional\npublic void process() {\n    try {\n        orderRepository.save(order);\n        paymentService.pay();\n    } catch (RuntimeException exception) {\n        \u002F\u002F 예외를 다시 던지지 않음\n    }\n}\n",[32,821,822,826,830,835,839,844,849,854,858],{"__ignoreMap":30},[245,823,824],{"class":247,"line":248},[245,825,401],{},[245,827,828],{"class":247,"line":254},[245,829,803],{},[245,831,832],{"class":247,"line":260},[245,833,834],{},"    try {\n",[245,836,837],{"class":247,"line":267},[245,838,282],{},[245,840,841],{"class":247,"line":273},[245,842,843],{},"        paymentService.pay();\n",[245,845,846],{"class":247,"line":279},[245,847,848],{},"    } catch (RuntimeException exception) {\n",[245,850,851],{"class":247,"line":285},[245,852,853],{},"        \u002F\u002F 예외를 다시 던지지 않음\n",[245,855,856],{"class":247,"line":291},[245,857,294],{},[245,859,860],{"class":247,"line":297},[245,861,300],{},[17,863,864],{},"메서드가 정상 반환하면 Proxy는 예외가 발생했다는 사실을 알 수 없다. 롤백이 필요하다면 예외를 다시 던지거나 트랜잭션을 rollback-only 상태로 표시해야 한다.",[35,866],{},[38,868,870],{"id":869},"_9-트랜잭션-전파","9. 트랜잭션 전파",[17,872,873],{},"전파(Propagation)는 이미 트랜잭션이 있을 때 새로 호출된 메서드가 그 트랜잭션을 어떻게 사용할지 정하는 규칙이다.",[43,875,876,889],{},[46,877,878],{},[49,879,880,883,886],{},[52,881,882],{},"옵션",[52,884,885],{},"설명",[52,887,888],{},"사용 예시",[62,890,891,904],{},[49,892,893,898,901],{},[67,894,895],{},[32,896,897],{},"REQUIRED",[67,899,900],{},"기존 트랜잭션이 있으면 참여하고, 없으면 새로 만든다.",[67,902,903],{},"대부분의 Service 로직",[49,905,906,911,914],{},[67,907,908],{},[32,909,910],{},"REQUIRES_NEW",[67,912,913],{},"기존 트랜잭션을 일시 중단하고 독립된 새 트랜잭션을 만든다.",[67,915,916],{},"독립적으로 저장할 감사 로그",[813,918,920],{"id":919},"required",[32,921,897],{},[24,923,925],{"className":239,"code":924,"language":241,"meta":30,"style":30},"@Transactional\npublic void createOrder() {\n    orderRepository.save(order);\n    paymentService.pay();\n}\n",[32,926,927,931,936,940,945],{"__ignoreMap":30},[245,928,929],{"class":247,"line":248},[245,930,401],{},[245,932,933],{"class":247,"line":254},[245,934,935],{},"public void createOrder() {\n",[245,937,938],{"class":247,"line":260},[245,939,470],{},[245,941,942],{"class":247,"line":267},[245,943,944],{},"    paymentService.pay();\n",[245,946,947],{"class":247,"line":273},[245,948,300],{},[17,950,951,954,955,957,958,961],{},[32,952,953],{},"paymentService.pay()","도 ",[32,956,897],{},"라면 바깥 트랜잭션에 참여한다. 내부 작업이 rollback-only로 표시되면 최종 커밋 시 ",[32,959,960],{},"UnexpectedRollbackException","이 발생할 수 있다.",[813,963,965],{"id":964},"requires_new",[32,966,910],{},[24,968,970],{"className":239,"code":969,"language":241,"meta":30,"style":30},"@Transactional(propagation = Propagation.REQUIRES_NEW)\npublic void saveAuditLog(AuditLog log) {\n    auditLogRepository.save(log);\n}\n",[32,971,972,977,982,987],{"__ignoreMap":30},[245,973,974],{"class":247,"line":248},[245,975,976],{},"@Transactional(propagation = Propagation.REQUIRES_NEW)\n",[245,978,979],{"class":247,"line":254},[245,980,981],{},"public void saveAuditLog(AuditLog log) {\n",[245,983,984],{"class":247,"line":260},[245,985,986],{},"    auditLogRepository.save(log);\n",[245,988,989],{"class":247,"line":267},[245,990,300],{},[17,992,993],{},"새로운 물리 트랜잭션을 사용하므로 외부 트랜잭션과 독립적으로 커밋하거나 롤백할 수 있다.",[17,995,996],{},"다만 외부 트랜잭션의 자원은 유지한 채 새 DB Connection을 추가로 사용할 수 있다. 남발하면 Connection Pool 부족이나 Lock 대기가 발생할 수 있다.",[17,998,999],{},"또한 같은 객체의 내부 호출로는 전파 설정이 적용되지 않을 수 있으므로 별도 Bean을 통해 호출해야 한다.",[35,1001],{},[38,1003,1005],{"id":1004},"_10-동시성-문제","10. 동시성 문제",[17,1007,1008,1010],{},[32,1009,110],{},"은 작업 범위를 묶어주지만 모든 동시성 문제를 자동으로 해결하지는 않는다.",[43,1012,1013,1024],{},[46,1014,1015],{},[49,1016,1017,1020,1022],{},[52,1018,1019],{},"문제",[52,1021,885],{},[52,1023,706],{},[62,1025,1026,1037,1048,1059,1070],{},[49,1027,1028,1031,1034],{},[67,1029,1030],{},"갱신 손실(Lost Update)",[67,1032,1033],{},"나중에 저장한 값이 먼저 저장한 값을 덮어씀",[67,1035,1036],{},"두 요청이 동시에 포인트 증가",[49,1038,1039,1042,1045],{},[67,1040,1041],{},"더티 리드(Dirty Read)",[67,1043,1044],{},"커밋되지 않은 값을 다른 트랜잭션이 읽음",[67,1046,1047],{},"곧 롤백될 값을 조회",[49,1049,1050,1053,1056],{},[67,1051,1052],{},"반복 읽기 불일치",[67,1054,1055],{},"같은 행을 다시 읽었을 때 값이 달라짐",[67,1057,1058],{},"포인트가 100에서 200으로 변경",[49,1060,1061,1064,1067],{},[67,1062,1063],{},"유령 읽기(Phantom Read)",[67,1065,1066],{},"같은 조건의 재조회에서 행 개수가 달라짐",[67,1068,1069],{},"회원이 10명에서 11명으로 증가",[49,1071,1072,1075,1078],{},[67,1073,1074],{},"경쟁 상태(Race Condition)",[67,1076,1077],{},"실행 순서에 따라 업무 결과가 달라짐",[67,1079,1080],{},"동시 재고 감소로 음수 발생",[35,1082],{},[38,1084,1086],{"id":1085},"_11-격리-수준","11. 격리 수준",[17,1088,1089],{},"격리 수준은 다른 트랜잭션의 변경 내용을 어디까지 볼 수 있는지 정한다.",[43,1091,1092,1101],{},[46,1093,1094],{},[49,1095,1096,1098],{},[52,1097,127],{},[52,1099,1100],{},"특징",[62,1102,1103,1113,1123,1133],{},[49,1104,1105,1110],{},[67,1106,1107],{},[32,1108,1109],{},"READ_UNCOMMITTED",[67,1111,1112],{},"커밋하지 않은 데이터도 읽을 수 있다.",[49,1114,1115,1120],{},[67,1116,1117],{},[32,1118,1119],{},"READ_COMMITTED",[67,1121,1122],{},"커밋된 데이터만 읽어 Dirty Read를 방지한다.",[49,1124,1125,1130],{},[67,1126,1127],{},[32,1128,1129],{},"REPEATABLE_READ",[67,1131,1132],{},"같은 행을 반복 조회할 때 같은 결과를 보장한다.",[49,1134,1135,1140],{},[67,1136,1137],{},[32,1138,1139],{},"SERIALIZABLE",[67,1141,1142],{},"트랜잭션을 순차 실행한 것처럼 강하게 격리한다.",[24,1144,1146],{"className":239,"code":1145,"language":241,"meta":30,"style":30},"@Transactional(isolation = Isolation.READ_COMMITTED)\npublic void process() {\n    \u002F\u002F DB 작업\n}\n",[32,1147,1148,1153,1157,1161],{"__ignoreMap":30},[245,1149,1150],{"class":247,"line":248},[245,1151,1152],{},"@Transactional(isolation = Isolation.READ_COMMITTED)\n",[245,1154,1155],{"class":247,"line":254},[245,1156,803],{},[245,1158,1159],{"class":247,"line":260},[245,1160,781],{},[245,1162,1163],{"class":247,"line":267},[245,1164,300],{},[17,1166,684,1167,1170],{},[32,1168,1169],{},"Isolation.DEFAULT","는 사용하는 DB의 기본 격리 수준을 따른다. 격리 수준을 높이면 정합성은 강해지지만 대기와 성능 비용이 증가할 수 있다.",[35,1172],{},[38,1174,1176],{"id":1175},"_12-낙관적-락","12. 낙관적 락",[17,1178,1179],{},"낙관적 락은 충돌이 드물다고 가정하고 버전 값으로 수정 충돌을 감지한다.",[24,1181,1183],{"className":239,"code":1182,"language":241,"meta":30,"style":30},"@Entity\npublic class Product {\n\n    @Id\n    @GeneratedValue(strategy = GenerationType.IDENTITY)\n    private Long id;\n\n    private int stock;\n\n    @Version\n    private Long version;\n}\n",[32,1184,1185,1190,1195,1199,1204,1209,1214,1218,1223,1227,1232,1237],{"__ignoreMap":30},[245,1186,1187],{"class":247,"line":248},[245,1188,1189],{},"@Entity\n",[245,1191,1192],{"class":247,"line":254},[245,1193,1194],{},"public class Product {\n",[245,1196,1197],{"class":247,"line":260},[245,1198,264],{"emptyLinePlaceholder":263},[245,1200,1201],{"class":247,"line":267},[245,1202,1203],{},"    @Id\n",[245,1205,1206],{"class":247,"line":273},[245,1207,1208],{},"    @GeneratedValue(strategy = GenerationType.IDENTITY)\n",[245,1210,1211],{"class":247,"line":279},[245,1212,1213],{},"    private Long id;\n",[245,1215,1216],{"class":247,"line":285},[245,1217,264],{"emptyLinePlaceholder":263},[245,1219,1220],{"class":247,"line":291},[245,1221,1222],{},"    private int stock;\n",[245,1224,1225],{"class":247,"line":297},[245,1226,264],{"emptyLinePlaceholder":263},[245,1228,1229],{"class":247,"line":596},[245,1230,1231],{},"    @Version\n",[245,1233,1234],{"class":247,"line":601},[245,1235,1236],{},"    private Long version;\n",[245,1238,1239],{"class":247,"line":607},[245,1240,300],{},[17,1242,1243],{},"JPA는 수정할 때 조회 당시 버전을 조건에 포함한다.",[24,1245,1249],{"className":1246,"code":1247,"language":1248,"meta":30,"style":30},"language-sql shiki shiki-themes github-light github-dark","UPDATE product\nSET stock = 9,\n    version = 2\nWHERE id = 1\n  AND version = 1;\n","sql",[32,1250,1251,1256,1261,1266,1271],{"__ignoreMap":30},[245,1252,1253],{"class":247,"line":248},[245,1254,1255],{},"UPDATE product\n",[245,1257,1258],{"class":247,"line":254},[245,1259,1260],{},"SET stock = 9,\n",[245,1262,1263],{"class":247,"line":260},[245,1264,1265],{},"    version = 2\n",[245,1267,1268],{"class":247,"line":267},[245,1269,1270],{},"WHERE id = 1\n",[245,1272,1273],{"class":247,"line":273},[245,1274,1275],{},"  AND version = 1;\n",[17,1277,1278],{},"다른 트랜잭션이 먼저 수정해 버전이 달라졌다면 충돌 예외가 발생한다.",[24,1280,1283],{"className":1281,"code":1282,"language":29,"meta":30},[27],"A와 B가 version = 1 조회\n        ↓\nA가 수정하고 version = 2로 커밋\n        ↓\nB가 version = 1 조건으로 수정\n        ↓\n수정 실패와 낙관적 락 예외\n",[32,1284,1282],{"__ignoreMap":30},[43,1286,1287,1297],{},[46,1288,1289],{},[49,1290,1291,1294],{},[52,1292,1293],{},"장점",[52,1295,1296],{},"단점",[62,1298,1299],{},[49,1300,1301,1304],{},[67,1302,1303],{},"DB Lock을 오래 잡지 않아 읽기가 많은 환경에 유리",[67,1305,1306],{},"충돌 시 예외 처리와 제한된 재시도 필요",[17,1308,1309],{},"재시도할 때는 최신 데이터를 새 트랜잭션에서 다시 조회하고 업무 조건을 다시 검사해야 한다.",[35,1311],{},[38,1313,1315],{"id":1314},"_13-비관적-락","13. 비관적 락",[17,1317,1318],{},"비관적 락은 충돌이 자주 발생한다고 가정하고 조회 시점에 DB Lock을 획득한다.",[24,1320,1322],{"className":239,"code":1321,"language":241,"meta":30,"style":30},"public interface ProductRepository extends JpaRepository\u003CProduct, Long> {\n\n    @Lock(LockModeType.PESSIMISTIC_WRITE)\n    @Query(\"select p from Product p where p.id = :id\")\n    Optional\u003CProduct> findByIdWithWriteLock(@Param(\"id\") Long id);\n}\n",[32,1323,1324,1329,1333,1338,1343,1348],{"__ignoreMap":30},[245,1325,1326],{"class":247,"line":248},[245,1327,1328],{},"public interface ProductRepository extends JpaRepository\u003CProduct, Long> {\n",[245,1330,1331],{"class":247,"line":254},[245,1332,264],{"emptyLinePlaceholder":263},[245,1334,1335],{"class":247,"line":260},[245,1336,1337],{},"    @Lock(LockModeType.PESSIMISTIC_WRITE)\n",[245,1339,1340],{"class":247,"line":267},[245,1341,1342],{},"    @Query(\"select p from Product p where p.id = :id\")\n",[245,1344,1345],{"class":247,"line":273},[245,1346,1347],{},"    Optional\u003CProduct> findByIdWithWriteLock(@Param(\"id\") Long id);\n",[245,1349,1350],{"class":247,"line":279},[245,1351,300],{},[24,1353,1355],{"className":239,"code":1354,"language":241,"meta":30,"style":30},"@Transactional\npublic void decreaseStock(Long productId, int quantity) {\n    Product product = productRepository\n            .findByIdWithWriteLock(productId)\n            .orElseThrow();\n\n    product.decreaseStock(quantity);\n}\n",[32,1356,1357,1361,1366,1371,1376,1381,1385,1390],{"__ignoreMap":30},[245,1358,1359],{"class":247,"line":248},[245,1360,401],{},[245,1362,1363],{"class":247,"line":254},[245,1364,1365],{},"public void decreaseStock(Long productId, int quantity) {\n",[245,1367,1368],{"class":247,"line":260},[245,1369,1370],{},"    Product product = productRepository\n",[245,1372,1373],{"class":247,"line":267},[245,1374,1375],{},"            .findByIdWithWriteLock(productId)\n",[245,1377,1378],{"class":247,"line":273},[245,1379,1380],{},"            .orElseThrow();\n",[245,1382,1383],{"class":247,"line":279},[245,1384,264],{"emptyLinePlaceholder":263},[245,1386,1387],{"class":247,"line":285},[245,1388,1389],{},"    product.decreaseStock(quantity);\n",[245,1391,1392],{"class":247,"line":291},[245,1393,300],{},[43,1395,1396,1404],{},[46,1397,1398],{},[49,1399,1400,1402],{},[52,1401,1293],{},[52,1403,1296],{},[62,1405,1406],{},[49,1407,1408,1411],{},[67,1409,1410],{},"충돌이 잦은 상황에서 동시 수정을 순서대로 처리하기 쉬움",[67,1412,1413],{},"대기 시간, 성능 저하, Timeout과 Deadlock 가능",[17,1415,1416],{},"Lock의 실제 동작은 DB 종류와 격리 수준에 따라 달라질 수 있다.",[35,1418],{},[38,1420,1422],{"id":1421},"_14-해결-방법-선택","14. 해결 방법 선택",[43,1424,1425,1435],{},[46,1426,1427],{},[49,1428,1429,1432],{},[52,1430,1431],{},"해결 방법",[52,1433,1434],{},"적합한 상황",[62,1436,1437,1445,1453,1461,1469],{},[49,1438,1439,1442],{},[67,1440,1441],{},"낙관적 락",[67,1443,1444],{},"충돌은 드물고 읽기가 많은 수정",[49,1446,1447,1450],{},[67,1448,1449],{},"비관적 락",[67,1451,1452],{},"좌석 예약처럼 같은 데이터 충돌이 잦은 작업",[49,1454,1455,1458],{},[67,1456,1457],{},"원자적 쿼리",[67,1459,1460],{},"단순한 증가나 감소를 한 SQL로 처리할 수 있는 작업",[49,1462,1463,1466],{},[67,1464,1465],{},"Unique 제약조건",[67,1467,1468],{},"이메일, 주문 번호 등의 중복 방지",[49,1470,1471,1474],{},[67,1472,1473],{},"격리 수준 조정",[67,1475,1476],{},"트랜잭션 사이의 조회 일관성이 중요한 작업",[17,1478,1479],{},"단순 재고 감소는 조건을 포함한 원자적 SQL로 처리할 수도 있다.",[24,1481,1483],{"className":1246,"code":1482,"language":1248,"meta":30,"style":30},"UPDATE product\nSET stock = stock - 1\nWHERE id = 1\n  AND stock > 0;\n",[32,1484,1485,1489,1494,1498],{"__ignoreMap":30},[245,1486,1487],{"class":247,"line":248},[245,1488,1255],{},[245,1490,1491],{"class":247,"line":254},[245,1492,1493],{},"SET stock = stock - 1\n",[245,1495,1496],{"class":247,"line":260},[245,1497,1270],{},[245,1499,1500],{"class":247,"line":267},[245,1501,1502],{},"  AND stock > 0;\n",[17,1504,1505,1506,1509],{},"수정된 행이 ",[32,1507,1508],{},"0","개라면 재고가 없거나 상품이 없는 경우로 처리한다.",[35,1511],{},[38,1513,1515],{"id":1514},"핵심-정리","핵심 정리",[43,1517,1518,1528],{},[46,1519,1520],{},[49,1521,1522,1525],{},[52,1523,1524],{},"개념",[52,1526,1527],{},"한 줄 정리",[62,1529,1530,1537,1546,1556,1563,1570,1577],{},[49,1531,1532,1534],{},[67,1533,69],{},[67,1535,1536],{},"여러 DB 작업을 하나의 성공 또는 실패 단위로 묶음",[49,1538,1539,1543],{},[67,1540,1541],{},[32,1542,110],{},[67,1544,1545],{},"Proxy가 트랜잭션의 시작, 커밋, 롤백을 관리",[49,1547,1548,1550],{},[67,1549,91],{},[67,1551,1552,1553,1555],{},"기본적으로 unchecked exception과 ",[32,1554,694],{},"가 전달될 때 실행",[49,1557,1558,1560],{},[67,1559,116],{},[67,1561,1562],{},"기존 트랜잭션 참여 여부와 새 트랜잭션 생성을 결정",[49,1564,1565,1567],{},[67,1566,127],{},[67,1568,1569],{},"다른 트랜잭션의 변경을 어디까지 볼지 결정",[49,1571,1572,1574],{},[67,1573,1441],{},[67,1575,1576],{},"버전으로 수정 충돌 감지",[49,1578,1579,1581],{},[67,1580,1449],{},[67,1582,1583],{},"DB Lock으로 동시 수정을 제한",[14,1585,1586],{},[17,1587,1588,1590],{},[32,1589,110],{},"만 붙인다고 모든 문제가 해결되지는 않는다. 업무 단위, 롤백 규칙, 전파, 격리 수준과 동시성 제어 방법을 함께 선택해야 한다.",[38,1592,1594],{"id":1593},"참고-자료","참고 자료",[1596,1597,1598,1608],"ul",{},[1599,1600,1601],"li",{},[1602,1603,1607],"a",{"href":1604,"rel":1605},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fdata-access\u002Ftransaction\u002Fdeclarative.html",[1606],"nofollow","Spring Framework 선언적 트랜잭션 공식 문서",[1599,1609,1610],{},[1602,1611,1614],{"href":1612,"rel":1613},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fdata-access\u002Ftransaction\u002Fdeclarative\u002Ftx-propagation.html",[1606],"Spring Framework 트랜잭션 전파 공식 문서",[1616,1617,1618],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":30,"searchDepth":254,"depth":254,"links":1620},[1621],{"id":12,"depth":254,"text":5,"children":1622},[1623,1624,1625,1627,1628,1629,1631,1632,1633,1634,1635,1636,1637,1638,1639,1640],{"id":40,"depth":260,"text":41},{"id":160,"depth":260,"text":161},{"id":227,"depth":260,"text":1626},"3. @Transactional 동작 원리",{"id":377,"depth":260,"text":378},{"id":447,"depth":260,"text":448},{"id":540,"depth":260,"text":1630},"6. readOnly = true",{"id":635,"depth":260,"text":636},{"id":680,"depth":260,"text":681},{"id":869,"depth":260,"text":870},{"id":1004,"depth":260,"text":1005},{"id":1085,"depth":260,"text":1086},{"id":1175,"depth":260,"text":1176},{"id":1314,"depth":260,"text":1315},{"id":1421,"depth":260,"text":1422},{"id":1514,"depth":260,"text":1515},{"id":1593,"depth":260,"text":1594},[241,1642],"spring","2026-08-21 09:00:00 +0900","Transactional의 동작, 커밋과 롤백, 전파, 격리 수준, 낙관적 락과 비관적 락을 하나의 흐름으로 정리한다.","md",null,"\u002Fjava\u002Fspring\u002F2026\u002F08\u002F21\u002Fjpa-transaction-concurrency\u002F",{"layout":1649},"post","9","\u002Fposts\u002Fbackend\u002Fspring\u002Fspring\u002F2026-08-21-jpa-transaction-concurrency",{"title":5,"description":1644},"Spring 기초","posts\u002FBackend\u002FSPRING\u002FSpring\u002F2026-08-21-jpa-transaction-concurrency",[1656,1657,1658,1659,1660,1661,1662],"Java","Spring","JPA","Transaction","Transactional","Concurrency","Lock","F5cZvLdT0E_q3jgDA9jFDoSOatWFs-ekUvufSBj9cCs",1788744786434]