2차 공부/TIL

24.10.14 supabase.insert, promise.all과 transaction

공대탈출 2024. 10. 15. 00:56
    !!eduArray.length &&
      eduArray.forEach(async (edu) => {
        const eduFormData = {
          post_id: postId,
          user_uuid: userId,
          ...edu
        };
        const { data: eduPostDtat, error: eduPostError } = await browserClient
          .from('post_detail_education')
          .insert([eduFormData]);
      });

    !!expArray.length &&
      expArray.forEach(async (exp) => {
        const expFormData = {
          post_id: postId,
          user_uuid: userId,
          ...exp
        };
        const { data: expPostDtat, error: expPostError } = await browserClient
          .from('post_detail_experience')
          .insert([expFormData]);
      });

    !!licArray.length &&
      licArray.forEach(async (lic) => {
        const licFormData = {
          post_id: postId,
          user_uuid: userId,
          ...lic
        };
        const { data: licPostDtat, error: licPostError } = await browserClient
          .from('post_detail_license')
          .insert([licFormData]);
      });

이번 팀프로젝트에서 이력서에 들어가는 학력, 경력, 자격증 관련 데이터뭉치들을 supabase에 올려주는 작업을 진행했다.

근데 세개를 한번에 하나씩 실행하다보니 좀 비효율적이라는 생각이 들었다.

그래서 여러 promise요청을 병렬적으로 실행시켜주는 promise.all을 사용해보고자 공식 문서를 읽어보았다.

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Promise.all()의 괄호안에 배열속에 어떤 요청들을 넣어주면 병렬적으로 그 요청이 실행되는 것이다.

 

하지만 이런 의문이 들었다.

const a = new Promise(resolve=>resolve(3초가 걸리는 작업))
const b = new Promise(resolve=>resolve(20초가 걸리는 작업))
const c = new Promise(resolve=>resolve(3초가 걸리는 작업))

Promise.all([a,b,c]).then(...).catch(...)

이렇게 여러 작업을 한번에 진행할 때 a,c는 b보다 일찍 fulfilled될 것이다.

그렇다는 의미는 셋 다 post요청일 때 이미 a,c요청에 대한 데이터가 DB에 저장된다는 것이다.

그리고 b에서 오류가 발생해버린다면? 그런데 세 데이터가 모두 한 데이터와 연관이 있어서 하나라도 실패하면 무의미해진다면?

그럼이제 귀찮아지는거다

어쨋든 한번 화면으로 살펴보자

const a = new Promise((resolve) => {
    setTimeout(() => {
        resolve(console.log("a끝!"));
    }, 3000);
});

const b = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(console.log("b는 끝내지 못했어!"));
        // resolve(console.log("b끝!"));
    }, 6000);
});

const c = new Promise((resolve) => {
    setTimeout(() => {
        resolve(console.log("c끝!"));
    }, 4000);
});

Promise.all([a, b, c])
    .then(() => console.log("then"))
    .catch(() => console.log("catch"));

이런 코드가 있다고 가정해보자. 실패할 예정인 b로직은 6초후에 실패하고 성공할 예정인 a,c로직은 3,4초뒤에 성공한다

mdn에 애매하게 설명해둔 설명대로라면 모두 실행되지않고 b가 실패해 b는 끝내지 못했어! 만 출력될 것 같지만

실상은 그렇지 않다.

그럼 Promise.all은 도대체 어떤 녀석일까

말 그대로 모든 요청을 한번에 .then이나 .catch로 보내는 것이다.

내부에서는 따로따로 요청이 처리되고 후 로직을 기다리고 있는 것

    const a =
      !!eduArray.length &&
      eduArray.forEach(async (edu) => {
        //...
        const { data: eduPostDtat, error: eduPostError } = await browserClient
          .from('post_detail_education')
          .insert([eduFormData]);
      });

    const b =
      !!expArray.length &&
      expArray.forEach(async (exp) => {
        //...
        const { data: expPostDtat, error: expPostError } = await browserClient
          .from('post_detail_experience')
          .insert([expFormData]);
      });

    const c =
      !!licArray.length &&
      licArray.forEach(async (lic) => {
        //...
        const { data: licPostDtat, error: licPostError } = await browserClient
          .from('post_detail_license')
          .insert([licFormData]);
      });
    const d = new Promise((reject) => {
      setTimeout(() => reject('실패!'), 5000);
    });
    Promise.all([a, b, c, d])
      .then((res) => console.log('res :>> ', res))
      .catch((err) => console.log('err :>> ', err));
  };

이렇게 supabase에 insert하는 요청을 Promise.all로 처리한다면 insert인 post요청이 각각 일어나고, 결국엔 catch로 에러처리가 될 것이다.

이 데이터를 각각의 table에 insert하는 요청은 일어나고, d라는 Promise가 reject되므로 catch로 이어지는 것이다.

 

실제로 데이터는 각 테이블에 들어갔지만, catch처리된 모습을 볼 수 있다.

그렇다면 저렇게 의미없어진(연결이 끊긴)데이터를 어떻게 처리해야할까?

 

저런 요청단위를 DB에선 Transaction이라고 부른다고 한다.

https://smin1620.tistory.com/325

DB의 상태를 변화시키기 위해 수행하는 작업 단위라고 하는데 뭐 어쨌든 깊이 들어가지않겠다 BE의 영역이기 때문에

https://smin1620.tistory.com/325

결국 난 위 사진과 같은 rollback의 기능이 필요하다.

const insertEducation = async (eduArray, postId, userId) => {
  try {
    await Promise.all(
      eduArray.map(async (edu) => {
        const eduFormData = {
          post_id: postId,
          user_uuid: userId,
          ...edu,
        };
        const { error: eduPostError } = await browserClient
          .from("post_detail_education")
          .insert([eduFormData]);

        if (eduPostError) throw new Error("Education insert failed");
      })
    );
    return { success: true };
  } catch (error) {
    return { success: false, error: "education", message: error.message };
  }
};

const insertExperience = async (expArray, postId, userId) => {
  try {
    await Promise.all(
      expArray.map(async (exp) => {
        const expFormData = {
          post_id: postId,
          user_uuid: userId,
          ...exp,
        };
        const { error: expPostError } = await browserClient
          .from("post_detail_experience")
          .insert([expFormData]);

        if (expPostError) throw new Error("Experience insert failed");
      })
    );
    return { success: true };
  } catch (error) {
    return { success: false, error: "experience", message: error.message };
  }
};

const insertLicense = async (licArray, postId, userId) => {
  try {
    await Promise.all(
      licArray.map(async (lic) => {
        const licFormData = {
          post_id: postId,
          user_uuid: userId,
          ...lic,
        };
        const { error: licPostError } = await browserClient
          .from("post_detail_license")
          .insert([licFormData]);

        if (licPostError) throw new Error("License insert failed");
      })
    );
    return { success: true };
  } catch (error) {
    return { success: false, error: "license", message: error.message };
  }
};

const handleRollback = async (postId) => {
  // 삽입 실패 시 rollback 처리
  console.log("Rolling back changes...");
  await browserClient.from("post_detail_education").delete().eq("post_id", postId);
  await browserClient.from("post_detail_experience").delete().eq("post_id", postId);
  await browserClient.from("post_detail_license").delete().eq("post_id", postId);
  console.log("Rollback completed");
};

const runInserts = async () => {
  const postId = "post123"; // 예시 postId
  const userId = "user123"; // 예시 userId

  const eduArray = [{ title: "edu1" }, { title: "edu2" }];
  const expArray = [{ title: "exp1" }];
  const licArray = [{ title: "lic1" }];

  try {
    // Promise.all로 모든 삽입 작업 실행
    const results = await Promise.all([
      insertEducation(eduArray, postId, userId),
      insertExperience(expArray, postId, userId),
      insertLicense(licArray, postId, userId),
    ]);

    // 실패한 작업이 있는지 확인
    const failed = results.find((result) => !result.success);
    if (failed) {
      throw new Error(`Failed on ${failed.error}: ${failed.message}`);
    }

    console.log("All inserts successful");
  } catch (err) {
    console.log("Error occurred:", err.message);
    await handleRollback(postId); // 실패 시 롤백
  }
};

runInserts();

뭐 이렇게 처리해야한다고 한다. 솔직히말하면 감당이 안된다. 조금 더 Promise.all을 사용해야하는지 고민을 해봐야할것같다 ㅋㅋ