개발놀이터

@Transactional로 분산 트랜잭션을 구현할 수 있을까? 본문

CS 지식/데이터베이스

@Transactional로 분산 트랜잭션을 구현할 수 있을까?

마늘냄새폴폴 2024. 3. 22. 23:50

약 2년전 시작했던 프로젝트인 온라인 쇼핑몰 프로젝트는 제가 취직을 함으로써 종료되었고 더이상 건드리지 않았습니다. 하지만 사이드프로젝트로서 나중에 이직할때 도움이 되고자 코드 리팩토링을 진행하게 되었습니다. 

 

리팩토링을 진행하던 중에 조회수를 증가시킴과 동시에 쿠키에 조회했다는 정보를 넣어 조회수 중복 증가를 방지하는 로직을 발견했고 그 부분에서 리팩토링할 부분이 있었습니다. 

 

바로 @Transcational 의 남용이었죠. 

 

@Service
public class ClickDuplicationPreventService {

	@Transactional
    public void viewCountUp(Item item, HttpServletRequest request, HttpServletResponse response) {
        Cookie oldCookie = null;
        Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if (cookie.getName().equals("postView")) {
                    oldCookie = cookie;
                }
            }
        }

        if (oldCookie != null) {
            if (!oldCookie.getValue().contains("[" + item.getId() + "]")) {
                item.addClick();		// << 이 부분이 업데이트 쿼리가 날아가는 시점
                oldCookie.setValue(oldCookie.getValue() + "_[" + item.getId() + "]");
                oldCookie.setPath("/");
                oldCookie.setMaxAge(60 * 10);
                response.addCookie(oldCookie);
            }
        }
        else {
            item.addClick();
            Cookie newCookie = new Cookie("postView", "[" + item.getId() + "]");
            newCookie.setPath("/");
            newCookie.setMaxAge(60 * 10);
            response.addCookie(newCookie);
        }
    }
}

 

이 프로젝트를 작성할 때에는 스프링 내부적으로 @Transactional을 적었을 때 어떻게 작동하는지에 대한 이해가 적었기 때문에 "쿼리를 날린다 = @Transactional을 적는다" 라는 단순한 사고로 코딩을 했습니다. 

 

하지만 약 6개월 전 이 글을 보고 큰 깨달음을 얻었습니다. 

 

https://woonys.tistory.com/entry/Transactional%EC%9D%80-%EB%A7%8C%EB%8A%A5%EC%9D%B4-%EC%95%84%EB%8B%99%EB%8B%88%EB%8B%A4-1-Transaction%EC%9D%98-%EA%B0%9C%EB%85%90%EA%B3%BC-%ED%8A%B8%EB%A0%88%EC%9D%B4%EB%93%9C%EC%98%A4%ED%94%84

 

@Transactional은 만능이 아닙니다 - 1: Transaction의 개념과 트레이드오프

Introduction 현재 작업 중인 프로젝트와 관련해 올렸던 PR에서 아래와 같은 리뷰를 받았다. Can you do it without getting @Transactional? @Transactional is not cheaper. Please think about why we use Transaction on RDBMS DB. 왜 이

woonys.tistory.com

 

이 글의 요지는 @Transactional은 사용해야하는 순간이 있고 모든 순간에 @Transactional을 사용하는 것은 좋지 않다는 내용입니다. 

 

자세한 내용은 위의 블로그를 참고해주세요!

 

결론은 @Transactional은 여러개의 데이터베이스 연산이 원자성을 가져야할 때 그것을 @Transactinal로 묶어줌으로써 각기 다른 연산에 대해 일관성을 유지시켜주는 것이 핵심입니다. 

 

즉, 단순한 update 작업에 @Transactional을 사용하는 것은 옳지 못하다는 것입니다. 이는 단순한 insert 작업이나 단순한 delete 작업도 마찬가지입니다. 

 

만약 Spring Data JPA를 사용하고 있다면 JpaRepository를 상속하는 것으로 Repository 계층에서 트랜잭션이 생성되기 때문에 여러개의 연산을 하나의 연산으로 묶을 필요가 없다면 굳이 Repository 계층에 있는 트랜잭션을 Service 계층으로 확장시킬 필요가 없는 것이죠. 

 

그냥 쿼리 한줄 Service 계층으로 확장하는게 뭐 어때서? 라고 생각할 수도 있지만 만약 Service 로직을 실행하는 시간이 길어서 데이터배이스 커넥션이 해당 로직이 종료될 때까지 커넥션풀에 반환되지 않는다면 데이터베이스 커넥션 풀이 사용할 수 있는 커넥션이 없어질 수 있습니다. 

 

 

@Transactional로 분산 트랜잭션까지 커버 가능?

이 포스팅의 요지는 리팩토링 했다는 얘기가 아닙니다. @Transactional이 트랜잭션을 스프링 내부적으로 관리하기위해 논리트랜잭션과 물리트랜잭션이라는 가상의 트랜잭션으로 개념을 추상화하여 트랜잭션의 원자성을 보장하고 있다는 것에 대한 포스팅을 한 적이 있습니다. 

 

https://coding-review.tistory.com/425

 

트랜잭션 전파 (feat. 논리 트랜잭션, 물리 트랜잭션)

이번 포스팅에선 트랜잭션 전파에 대해서 알아보도록 하겠습니다. 사실 트랜잭션 전파에 대해서는 어느정도 알고 있는 부분이 있었는데 제가 아는 수준은 "트랜잭션 전파를 이용하면 데이터 정

coding-review.tistory.com

 

이 포스팅의 내용은 물리트랜잭션은 실질적 데이터베이스 커밋과 롤백을 담당하고 논리트랜잭션은 논리적인 트랜잭션으로서 롤백상황을 체크하고 물리트랜잭션에게 "지금 이 트랜잭션 연산은 롤백해야해!" 라고 알려주는 역할을 합니다. 

 

 

이 내용을 떠올리니 한가지 궁금한 것이 생겼습니다. 

 

'어? 이거 개념을 조금만 확장하면 분산 시스템에서도 사용할 수 있는거 아냐?'

 

예를 들어서 각각의 논리트랜잭션이 서로 다른 데이터베이스에 연결된 마이크로서비스라면 이 개념을 확장할 경우 분산 트랜잭션을 흉내낼 수 있지않을까? 

 

 

분산 시스템에서 트랜잭션 관리

이 궁금증을 해결하기 위해서 분산시스템에서 어떻게 트랜잭션을 관리하는지에 대해서 알아볼 필요가 있다고 판단하여 구글링을 했습니다. 

 

이번엔 조금 지엽적인 내용을 공부할 예정이었기 때문에 GPT에게 물어보지 않고 구글링을 하였습니다. 

 

분산 트랜잭션 : 2PC 알고리즘

2PC는 two phase commit 의 약자로서 쉽게 설명해서 각각의 트랜잭션을 커밋하기 전에 모든 트랜잭션이 준비완료를 외치기 전까지 커밋을 미루는 알고리즘입니다. 

 

이 알고리즘을 사용하면 쉽게 분산 시스템에서 트랜잭션을 관리할 수 있기 때문에 제일 먼저 떠올리는 해결책이라고합니다. 하지만 2PC에는 치명적인 단점이 있습니다. 

 

바로 준비완료를 외치는 단계인 1 페이즈에서 데이터베이스 lock을 이용해 대기하고 있다는 점입니다. 

 

이 문제는 멀티스레드 환경에서 사용자 요청이 중복되는 경우 데드락을 유발할 수 있고 이 데드락을 해결하기 위해서는 분산 시스템에서 트랜잭션을 관리하는 것보다 더 어렵다고 합니다. (해당 포스팅의 저자 개인적인 생각)

 

SAGA 패턴

분산 시스템에선 2PC보단 SAGA패턴이라는 것을 이용해서 트랜잭션을 관리합니다. 

 

SAGA의 철학은 각기 다른 서비스에서 생기는 커밋에 대해 예외상황이 발생할 경우 롤백 처리를 어떻게 쉽게 처리할 것인지 고민한 방법론입니다. 즉, 원자성을 분산 시스템에서 어떻게 유지할것이냐 그것이 문제인 것이죠. 

 

해당 논문에서 저자는 SAGA 패턴에서 원자성을 유지하기 위해선 Backward Recovery, Forward Recovery (저는 후위회복, 전위회복이라고 번역하였습니다.) 가 필요하다고 설명했습니다. 

 

Backward Recovery (후위 회복)

후위 회복은 만약 중간에서 예외가 발생했다면 커밋 이전 상태로 롤백하는 것을 말합니다. 후위 회복의 핵심은 모든 서비스에 대해 이전 상태를 보관하고 있어야한다는 것이죠. 

 

Forward Recovery (전위 회복)

전위 회복은 예외가 발생한 지점에 포인트를 찍어 그 포인트로 되돌아가 트랜잭션을 재실행하는 것이 핵심 전략입니다. 전위 회복에서의 핵심은 트랜잭션을 재실행하는 로직이 들어가야한다는 것이죠. 

 

 

Ochestration 방식과 Choreographed 방식

SAGA 패턴도 두 가지 종류가 있는데 하나는 Ochestration 방식, Choreographed 방식 이렇게 두가지 방식이 있습니다. 둘의 차이점은 각각의 서비스가 얼마나 디커플링되어있냐는 것입니다. 

Ochestration 방식

 

Ochestration 방식은 중앙에서 모든 서비스들의 로직을 관리하고 컨트롤하는 중앙집권방식으로서 각각의 서비스를 디버깅할 때 추적하기 쉽다는 장점이 있습니다. 

 

Choreographed 방식

 

Choreographed 방식은 각서비스들이 서로를 신뢰하고(?) 작업을 맡기는 것을 선택했습니다. 이 방식은 서비스들이 디커플링 되어있어 이상적인 분산 시스템의 형태라고 할 수 있습니다. 

 

해당 포스팅의 저자는 Ochestration 방식과 Choreographed 방식 중 선택해야한다면 그건 우리 팀의 상황에 맞게 선택해야한다고 조심스럽게 추천합니다. 

 

만약 하나의 팀에서 분산 시스템을 관리하고 있는 경우엔 Ochestration 방식을 추천하고, 팀이 여러개 있고 각각의 팀이 분산 시스템의 한 부분을 맡고있다면 Choreographed 방식을 사용하는 것이 일처리가 수월하다는 평가를 남겼습니다. 

 

즉, 규모가 어느정도 있어 분산 시스템으로 서비스를 관리하지만 팀 규모가 대기업만큼 크지 않은 경우엔 Ochestration 방식을, 네카라쿠배처럼 서비스가 굉장히 커서 각기다른 팀이 분산 시스템을 관리해야 한다면 Choreographed 방식을 선택하면 좋을 것 같습니다. 

 

 

그래서... @Transactional로 분산 시스템 구현 가능...?

결국 이 포스팅에서 하고싶었던 말...

 

가능하지만 추천하는 방법은 아니다...

 

@Transactional 어노테이션을 메서드에 적는 순간 로컬 트랜잭션과 글로벌 트랜잭션으로 생성됩니다. 글로벌 트랜잭션으로 생성된 것은 분산 시스템에서 트랜잭션을 하나로 모아주는 역할을 수행할 수도 있습니다. 

 

하지만!

우리에겐 분산 시스템에 최적화된 프레임워크인 Spring Cloud 가 있습니다. Spring Cloud 는 분산 시스템에서 트랜잭션을 관리할 수 있는 복잡한 로직들을 추상화하여 개발자로 하여금 이를 쉽게 접근할 수 있게 잘 만들어져있다고 합니다. (출처 : ChatGPT 4.0)

 

그리고 @Transactional 로 분산 시스템의 트랜잭션을 관리하는 것은 굉장히 복잡하고 유지보수하기도 굉장히 굉장히 힘들다고 하니 굳이 사용할 이유는 없을 것 같습니다. 

 

 

번외) NoSQL에서 트랜잭션관리

분산 시스템에서 트랜잭션 처리에 대한 궁금증이 해결되자마자 바로 떠오른 궁금증입니다. NoSQL에서도 트랜잭션처리가 될까? 

 

제가 배운 이론상으로는 "NoSQL은 느슨한 ACID인 BASE속성으로 Eventual Consistency를 도입하였기 때문에 높은 가용성을 보이지만 강한 일관성을 기대하기 힘들다." 라고 배웠고 그렇게 알고있었습니다. 

 

하지만 스택오버플로우에 올라온 답변을 통해 대충 짐작할 수는 있었습니다. 

 

https://stackoverflow.com/questions/2212230/transactions-in-nosql

 

Transactions in NoSQL?

I'm looking into NoSQL for scaling alternatives to a database. What do I do if I want transaction-based things that are sensitive to these kind of things?

stackoverflow.com

 

요컨대 "MongoDB같은 경우는 원한다면 선택적으로 원자성을 보장받을 수 있다." 라는 결론에 이릅니다. 

 

데이터베이스의 세계는 결국 수렴진화하는 결과를 맞이했습니다. 

 

ACID를 무기로 강한 일관성을 자랑하던 RDBMS 진영에선 수평확장에 대한 사용자(개발자)의 니즈를 받아들여 Postgres 에서 분산 데이터베이스에 특화된 샤딩을 지원하고,  BASE를 무기로 높은 가용성과 높은 성능을 자랑하던 NoSQL 진영에선 MongoDB에서 데이터를 관계형으로 관리하고 싶은 사용자의 니즈를 받아들여 4.x에선 JOIN을 지원함과 더불어 국지적으로 원자성을 보장받을 수 있게 되었습니다. 

 

마치며

트랜잭션을 아직 실무에서 깊이있게 다루진 못했습니다. 기회가 된다면 실무에서도 깊이있게 사용해보고 더 많은 상황을 겪어보고 싶은 것이 개인적인 바람입니다. 

 

물론 분산 시스템에서 트랜잭션을 관리하는 것은 지금 당장은 어렵겠지만 후에 이직할 때 면접 질문으로 충분히 나올 수 있을 것 같습니다. 

 

이번 포스팅은 여기서 마치도록 하겠습니다. 오늘도 즐거운 하루 되세요~

 

 

출처

https://medium.com/javarevisited/transactional-annotation-in-spring-framework-d571e91bf6bb

 

Transactional annotation in Spring Framework

When working with relational databases in Spring applications, it’s common to perform multiple database operations within a single…

medium.com

 

\https://medium.com/nerd-for-tech/transactions-in-distributed-systems-b5ceea869d7d

 

Transactions in distributed systems

What are Transactions?

medium.com

 

https://chat.openai.com/share/73118ea3-4d07-4392-9307-8d28145ea5c8

 

ChatGPT

A conversational AI system that listens, learns, and challenges

chat.openai.com