현 포스팅은 맛침반 매장 등록 기능을 구현하면서 생겼던 의문과 해결한 흐름을 정리한다.
매장을 등록하는 기능에 매장을 등록하면서, 매장의 메뉴와 매장의 사진을 같이 등록해야 하는 상황에서 하나의 요청/응답에 여러 로직이 존재한다면 어떻게 처리해야 할까?
하나의 요청에 모든 로직을 넣어 처리해야 하나?
아니면 요청 당 하나의 로직을 설계해서 처리해야 하나?
전자의 경우 모든 로직을 넣어 처리하면 입력 값이 많아지고 코드가 많아진다.
후자의 경우 로직이 각각 진행되기 때문에 트랜잭션 처리가 힘들어진다.
사실 이를 고민하면서 가장 신경썼던 부분은 "사용자의 변심으로 인한 중단"이었다.
사용자가 변심으로 인해 중간에 중단하게 된다면?
매장 등록은 정상적으로 됐지만 메뉴 등록에서 변심으로 인해 취소해버린다면, 매장 등록으로 저장된 데이터는 어떻게 처리해야 할까?
하지만 그렇다고 모든 정보를 한 번에 담는 것도 좋은 선택은 아닌 것 같다.
만약 입력하는 정보 중 DB에서 중복을 확인해야 하는 데이터가 존재한다면?
모든 정보를 입력하고 돌리기에는 부담이 될 수 있다.
이 부분에 대해 okky에 질문 글을 올렸다.
심지어 질문 글도 잘못 썼다.
여러 기능을 하나의 api에 라고 하는게 아닌 endpoint, 아니면 요청이라고 말했어야 할 것 같다.
api는 하나고 api에 접근할 수 있도록 하는 url이 endpoint니깐!!
부끄러워지는 시간이다.
사실 이 답변들을 이해하는데 오래 걸렸고, 내가 잘 해석했는지도 잘 모르겠다.
내가 이해한 내용으로 한 번 작성해보겠다.

@Service
public class TestService {
@Transactional
public void testMethod() {
feature1();
feature2();
feature3();
}
}
-> 분리해서 사용해라. 그러나 트랜잭션이 중요하다면 하나의 요청에 여러 기능을 넣은 후, 한 번에 처리하는 서비스 메서드를 만들어 처리하라.
-> 개발을 할 때 어떤 부분을 중요시 여기냐에 따라서 구조가 달라질 것 같다.

1번: 여러 기능을 하나의 요청에
2번: 요청 당 하나의 기능만
원자화
- 트랜잭션 ACID의 A, 원자성인 것 같다.
- 원자성은 트랜잭션이 작업의 모든 단계를 완전히 수행하거나 아무 작업도 수행하지 않은 상태로 유지되어야 함을 의미한다.
"개별 기능으로 원자화 시키는데"
- 원자성은 어떤 것이 더 이상 쪼개질 수 없는 성질을 의미하기도 한다.
- 다시 말하면 어떤 작업이 프로그램 안에서 가장 작은 단위라서 더 이상 다른 작업으로 나눌 수 없다는 것이다.
- 그러니 이 말은 개별 기능으로 쪼개질 수 없을 때까지 쪼개서 기능을 나타내라고 한 것 같다.
-> 요청 당 하나의 기능을 사용하는 것을 전재로 한다.
-> 단, 프로젝트에 따라서 혹은 요구사항, 성능에 따라서 일부는 결합할 수 있다.
(요청 당 하나의 기능이 "무조건"은 아니라는 의미인 것 같다. 예를 들어서 매장을 등록하는 기능에 사진을 등록하는 기능도 넣을 수 있다는 의미가 아닐까?)

-> 트랜잭션이 문제라면 검증 작업과 배포 작업을 나눠봐라.
-> 이렇게 하는 이유는 사용자의 변심이나 실수로 인해 작업 결과가 훼손되면 안되기 때문이다.
이 당시에는 Okky 글을 이해하지 못해서 Okky 글을 보여드리면서 어떤 의미인지, 그리고 어떻게 생각을 하시는지 의견을 물었다.
"저번 프로젝트에 같은 고민을 했었고, 그 때는 프론트엔드에서 하나의 요청에 모든 로직을 담아달라고 부탁하셔서 그렇게 해드렸다. 하지만 다시 만든다면 다른 방법을 생각했을 것 같다."
팀장님의 지난 프로젝트의 코드를 본 적 있었는데, 하나의 메서드에 거의 40~50줄 가량의 기능이 처리된 것을 봤다. 팀장님도 지난 프로젝트에 있어서 아쉬움이 많다고 하셨으며, 이런 부분에 있어서도 기회가 된다면 수정을 하고 싶으시다고 말해주셨다.
대신 팀장님은 프론트엔드분의 요청을 구현하는 것을 핵심이라고 생각하셨기 때문에 하나의 요청에 여러 기능을 넣은 것이다. 하지만 하나의 기능에 모든 걸 처리하는 것은 썩 좋은 생각은 아닌 것 같다.
"여러 요청을 하나의 트랜잭션으로 묶는 것은 굉장히 어려운 일이라고 알고 있다. 만약 하나의 요청에 하나의 기능을 넣고 싶다면 어느정도 트랜잭션을 포기해야 할 것이며, 트랜잭션을 보완하는 다른 방안을 생각해야 할 것 같다."
팀원님은 따로 고민을 해보신적이 없으셔서 어떤 방식이 좋을지는 모르겠다고 하셨으며, 대신 여러 요청에 대한 하나의 트랜잭션에 대해 설명해주셨다. 그리고 어떤 것을 중요시 여기냐에 따라서 포기할 수밖에 없는 상황도 생길 거라고 하셨다. (모든게 완벽한 기능은 없을 것이다.)
물론 이번 프로젝트에서 해당 트랜잭션을 다뤄보면 좋겠지만, 여러 키워드로 검색해봐도 정보가 나오지 않아 이는 리팩토링 때 생각해봐야겠다.
핵심은 사용자 비즈니스 플로우는 사용자의 변심과 단순한 실수로 작업 결과는 훼손되지 않아야 한다는 것이었다.
내가 처음부터 트랜잭션을 고려했기 때문에 이런 고민을 했는데, 정작 왜 트랜잭션을 사용해야 하는지, 트랜잭션은 무엇을 위한 것인지를 떠올리지 못했다.
트랜잭션이라는 것은 결국 문제를 해결하기 위한 도구로 사용되며, 그것은 작업 결과 훼손을 방지하기 위해서였다. 변심을 통해 취소된다면 작업 결과에 영향이 가지 않도록 모든 작업을 취소하는 것이 트랜잭션의 의의였다.
그렇다면 트랜잭션을 고려한, 즉 사용자의 변심이 작업 결과에 영향을 미치지 않도록 하는 방법은 뭐가 있을까?
이 문제를 고민하면서 여러 토이 프로젝트의 API 명세서를 찾아봤지만, 검증만을 위한 API는 존재하지 않았다. 하지만 단계별로 검증해야 하는 것은 변치 않았다.
예를 들어서 (예시와 현재 고민하는 문제의 핀트가 다르긴 하지만) 공동인증서를 발급한다고 해보자. (얼마전에 공동인증서 재발급 받아서 생각나는 대로만 말해보겠다.)
공동인증서를 발급하기 위해선 먼저 본인 인증을 진행한다. 본인 인증은 말 그대로 입력한 아이디와 비밀번호, 주민등록번호에 대해 본인이 맞는지 검증 작업만을 거친다. 그리고 본인 인증을 거친 후에 심적 변화로 취소한다고 해도 아무런 일도 일어나지 않는다.
그 다음에는 계좌에 대한 본인 인증이 들어간다. 보안 카드의 숫자를 입력하고, ARS로 본인 인증을 한다. 이 과정에서 심적 변화로 취소해도 아무런 일도 일어나지 않는다. 단지 검증 작업일 뿐이다.
공동인증서를 발급받기 전에는 미리보기가 주어진다. "정말로 공동인증서를 발급하시겠습니까?" 느낌으로 물어보며, 사용자가 "발급" 버튼을 누르게 되면 공동인증서가 최종 발급이 될 것이다.
-> 결국 최종 발급 버튼을 누르기 전까지 어떤 데이터에도 영향이 가면 안된다.
결과에 영향을 주지 않는 검증 작업은 어쩌면 단계별로 나눠서 하는게 좋을지도 모른다.
프론트엔드에서도 가벼운 검증이라면 가능하며, 어쩌면 거의 대부분은 프론트엔드에서 끝날지 모른다. 예를 들어서 공백으로 입력됐다거나, 숫자만 입력해야 하는 부분에 문자를 입력했다거나 등 간단한 검증은 프론트엔드에서도 처리해야 한다. 그렇다면 백엔드에서는 뭘 검증할 수 있을까?
매장 등록 기능에서만 설명하자면 다음과 같다.
전 단계에서 매장 검증, 매장 이미지 검증, 메뉴 검증이 이루어졌다고 하자.
그렇다면 등록은 한꺼번에 처리돼야 할까, 검증과 마찬가지로 단계별로 처리해야 할까?
(여기서 단계별이라는 것은 엔드포인트를 나눈다는 말이다.)
등록 부분에서는 트랜잭션이 지켜져야 한다. 그렇기 때문에 하나의 요청에 여러 기능이 들어가도록 해야할 것 같다.
만약 단계별로 등록하게 된다면 트랜잭션이 지켜지지 않기 때문에 원점으로 돌아가게 된다.
이번 고민으로 왜 트랜잭션을 사용해야 하는지 알게 됐고 트랜잭션은 반드시 필요한 과정이 됐다. 그러므로 한꺼번에 모든 데이터를 보내서 처리해야 할 것이다.
다음과 같은 결정을 하게 된 이유는 미리보기였다.
모든 검증 과정을 거친 후 사용자가 등록 버튼을 누르게 되면 등록 버튼에 해당하는 기능은 바로 완료가 돼야 한다. 인증서 발급 버튼을 눌렀다면 인증서가 발급돼야 하며, 등록 버튼을 눌렀다면 등록이 돼야 한다.
이 과정에서는 사용자의 실수와 심적 변화의 영향이 없는 영역이다. 사용자는 실수 하지 않았으며, 심적 변화도 없었는 상태에서 등록 버튼을 눌렀으니, 등록 버튼에 대한 모든 과정을 완수해야 한다.
만약 등록 버튼을 눌렀을 때 시스템 자체 에러가 발생하면 모든 과정을 취소해야 하기 때문에 트랜잭션이 적용돼야 하고, 트랜잭션을 적용하기 위해선 하나의 요청에서 여러 엔티티를 등록해야 할 것이다.
검증 단계를 거친 데이터는 이미 검증이 된 상태일 것이다. 그렇기 때문에 데이터에 대한 검증 작업은 오히려 불필요할 수 있다. 하지만 그 외의 부분에 대한 검증은 필요할 것이다.
검증 단계에서 데이터 무결성에 대한 작업이 끝났다고 할 때, 등록 단계에서는 보안, 사용자 입력, 성능 고려 등에 대한 검증은 필요할 것이다.
이번 고민 시간은 "어떻게 구현할까"가 아닌 "왜 구현해야 하냐"를 고민했어야 했던 문제였다. 물론 내가 내린 답이 정답은 아닐 것이다. 아직 프론트엔드와 본격적인 협업에 들어가기 전이기 때문에 이부분에 대해서 프론트엔드분이 어떻게 생각하는지도 고려해야 할 것이다. 우선 프론트엔드분과 작업을 하기 전까지는 내가 생각한 방법대로 개발을 해볼 것이고, 프론트엔드분이랑 진하게 얘기를 나눠봐야 할 것 같다.