ทำไมการส่งอีเมลใน API Request ถึงเป็นกับดักที่มือใหม่ควรเลิกทำ

10 นาที 11 views บันทึกเป็น PDF
ทำไมการส่งอีเมลใน API Request ถึงเป็นกับดักที่มือใหม่ควรเลิกทำ

เลิกเขียนโค้ดส่งอีเมลค้างไว้ใน API จนผู้ใช้ต้องรอนาน! มาเรียนรู้วิธีแยกงานส่งอีเมลไปไว้ใน Queue เพื่อให้แอปของคุณทำงานเร็วขึ้นและเสถียรขึ้นกว่าเดิม

ทำไมคุณไม่ควรส่งอีเมลระหว่างรอ API ตอบกลับ

เวลาเราสร้างระบบสมัครสมาชิก (Signup) สิ่งแรกที่มือใหม่มักทำคือการเขียนโค้ดให้ทำงานเรียงกันไปเป็นเส้นตรง เมื่อผู้ใช้กดปุ่มสมัคร ระบบจะบันทึกข้อมูลลงฐานข้อมูล (Database - ที่เก็บข้อมูล) จากนั้นก็สั่งให้ระบบอีเมลส่งจดหมายต้อนรับทันที แล้วค่อยตอบกลับผู้ใช้ว่าสมัครเสร็จแล้ว นี่คือวิธีที่ดูเหมือนจะง่ายและทำตามได้ไม่ยาก แต่ในโลกความเป็นจริงที่คนใช้งานพร้อมกันหลายคน วิธีนี้อาจกลายเป็นฝันร้ายของโปรแกรมเมอร์ได้เลย

การส่งอีเมลเป็นกระบวนการที่กินเวลาและไม่แน่นอน Latency (ความหน่วงหรือเวลาที่ใช้ในการรอคอย) ของผู้ให้บริการส่งอีเมลนั้นสูงกว่าการเขียนข้อมูลลงฐานข้อมูลมาก หากคุณให้ผู้ใช้รอจนกว่าอีเมลจะส่งออกไปสำเร็จ แอปพลิเคชันของคุณจะช้าลงอย่างเห็นได้ชัด และหากผู้ให้บริการอีเมลล่มหรือทำงานช้า ระบบของคุณก็จะหยุดชะงักตามไปด้วย นี่คือเหตุผลว่าทำไมเราถึงไม่ควรเอาการส่งอีเมลไปรวมไว้ในขั้นตอนเดียวกับที่ผู้ใช้กำลังรอคำตอบจาก API (ช่องทางเชื่อมต่อระหว่างแอปพลิเคชัน)

ลองนึกภาพว่าคุณไปสั่งอาหารที่ร้านแห่งหนึ่ง แล้วพนักงานบอกว่าคุณต้องยืนรออยู่หน้าเคาน์เตอร์จนกว่าเชฟจะทำอาหารเสร็จและพนักงานจะเดินไปเสิร์ฟถึงโต๊ะคุณก่อน ถึงจะอนุญาตให้คุณไปนั่งที่โต๊ะได้ ถ้าวันไหนลูกค้าเยอะหรือเชฟทำอาหารช้า แถวหน้าร้านก็จะยาวเหยียดและลูกค้าคนอื่นก็เข้าไม่ได้ การเขียนโค้ดส่งอีเมลไว้ใน Request (คำขอจากผู้ใช้) ก็เหมือนกับการให้ลูกค้าคนนั้นยืนรอจนกว่าอีเมลจะส่งออกไปนั่นเอง

รูปแบบโค้ดที่ดูเหมือนจะดีแต่แอบแฝงอันตราย

ลองมาดูตัวอย่างโค้ดที่มือใหม่มักเขียนกัน ซึ่งมันทำงานได้ดีตอนรันบนเครื่องตัวเอง แต่จะเริ่มมีปัญหาเมื่อนำไปใช้จริง โค้ดชุดนี้จะทำงานแบบ Synchronous (การทำงานแบบเรียงลำดับที่ต้องรอให้งานหนึ่งเสร็จก่อนถึงจะทำต่อไปได้) ซึ่งเป็นจุดเริ่มต้นของปัญหาคอขวดในระบบ

// โค้ดสมัครสมาชิกแบบที่มือใหม่ชอบใช้
app.post('/signup', async (c) => {
  const body = await c.req.json();
  // 1. บันทึกข้อมูลลงฐานข้อมูล
  const user = await db.users.create({ email: body.email });
  
  // 2. สั่งส่งอีเมลทันที (จุดที่ทำให้ระบบช้า)
  await emailClient.send({ 
    to: user.email, 
    subject: 'ยินดีต้อนรับ' 
  });
  
  // 3. ตอบกลับผู้ใช้
  return c.json({ message: 'สมัครสำเร็จ' }, 201);
});

ในโค้ดนี้ บรรทัดที่เรียกใช้ emailClient.send คือกับดักสำคัญ ระบบจะหยุดทำงานตรงนี้จนกว่าเซิร์ฟเวอร์อีเมลจะตอบกลับมาว่าส่งสำเร็จแล้ว หากเซิร์ฟเวอร์อีเมลใช้เวลา 3 วินาที ผู้ใช้ของคุณก็ต้องรอหน้าจอนิ่งๆ ไป 3 วินาทีเต็มๆ ซึ่งนานเกินไปสำหรับแอปพลิเคชันสมัยใหม่ที่ควรตอบสนองภายในไม่กี่ร้อยมิลลิวินาที

นอกจากเรื่องความช้าแล้ว หากบรรทัดส่งอีเมลเกิดข้อผิดพลาดขึ้นมา ระบบจะโยน Error (ข้อผิดพลาดที่เกิดขึ้นขณะทำงาน) ออกมาทันที ทำให้ผู้ใช้เห็นหน้าจอแจ้งเตือนว่าสมัครไม่สำเร็จ ทั้งที่จริงแล้วข้อมูลผู้ใช้ถูกบันทึกลงฐานข้อมูลไปแล้ว การจัดการกับข้อผิดพลาดแบบนี้ในโค้ดฝั่งเดียวจะทำให้ระบบของคุณซับซ้อนและดูแลรักษายากขึ้นเรื่อยๆ

ปัญหาที่เกิดขึ้นเมื่อระบบต้องรองรับผู้ใช้จำนวนมาก

เมื่อคุณมีผู้ใช้หลักพันหรือหลักหมื่น ความผิดพลาดของระบบภายนอกจะส่งผลกระทบโดยตรงต่อแอปของคุณ หากผู้ให้บริการอีเมลมีปัญหา (Outage) ระบบสมัครสมาชิกของคุณจะพังตามไปด้วยทันที ผู้ใช้จะกดสมัครไม่ได้เพราะ API ของคุณกำลังติดแหง็กอยู่ที่การพยายามส่งอีเมลที่ไม่มีวันสำเร็จ

อีกเรื่องที่น่ากลัวคือ Rate Limits (ข้อจำกัดจำนวนครั้งในการเรียกใช้งาน) ผู้ให้บริการอีเมลมักจะมีกฎว่าส่งได้กี่ฉบับต่อนาที หากวันหนึ่งมีคนสมัครเข้ามาพร้อมกันเยอะๆ ระบบของคุณจะโดนบล็อกการส่งอีเมล และนั่นจะกลายเป็นเหตุผลที่ทำให้ API ของคุณส่งค่า 500 (รหัสแจ้งข้อผิดพลาดจากเซิร์ฟเวอร์) กลับไปหาผู้ใช้ ทั้งที่จริงๆ แล้วระบบฐานข้อมูลของคุณไม่ได้มีปัญหาอะไรเลย

การปล่อยให้ผู้ใช้รอคอยนานเกินไปจะทำให้เกิดพฤติกรรม "กดย้ำ" เมื่อคนเห็นหน้าจอค้างนานๆ เขามักจะกดปุ่มสมัครซ้ำหลายรอบ ผลลัพธ์คือคุณอาจจะได้ข้อมูลผู้ใช้ซ้ำซ้อนในฐานข้อมูล หรือแย่ไปกว่านั้นคือส่งอีเมลต้อนรับไปหาเขาถึง 5 ฉบับ ทำให้ภาพลักษณ์ของแอปดูไม่เป็นมืออาชีพและสร้างความสับสนให้ผู้ใช้งาน

กฎทอง: API ควรทำหน้าที่แค่ "จองงาน" ไม่ใช่ "ลงมือทำ"

วิธีแก้ปัญหาที่ดีที่สุดคือการเปลี่ยนวิธีคิด ให้มองว่าการส่งอีเมลเป็น Side Effect (งานที่ต้องทำตามมาหลังจากงานหลักเสร็จสิ้น) ซึ่งไม่จำเป็นต้องทำทันทีในวินาทีนั้น เราควรแยกงานส่งอีเมลออกจากเส้นทางการตอบสนองของผู้ใช้ โดยใช้สิ่งที่เรียกว่า Queue (คิวหรือแถวลำดับงานที่รอการประมวลผล) เข้ามาช่วย

หลักการคือเมื่อผู้ใช้กดสมัคร API ของคุณจะทำแค่สองอย่าง คือบันทึกข้อมูลลงฐานข้อมูลและ "หย่อนงาน" ลงในคิว จากนั้นก็ตอบกลับผู้ใช้ทันทีว่า "รับเรื่องแล้ว" ส่วนงานส่งอีเมลจะมี Worker (โปรแกรมหรือกระบวนการที่คอยดึงงานจากคิวไปทำต่อ) มาคอยหยิบงานในคิวไปจัดการทีละอย่างตามความเร็วที่มันทำได้

วิธีนี้ทำให้ API ของคุณทำงานได้เร็วมาก เพราะมันไม่ต้องรอการตอบกลับจากระบบภายนอกอีกต่อไป ต่อให้ระบบอีเมลจะล่มหรือช้าแค่ไหน ผู้ใช้ของคุณก็ยังสมัครสมาชิกได้ปกติ ส่วนอีเมลก็จะถูกส่งออกไปเมื่อระบบอีเมลพร้อมใช้งานอีกครั้ง นี่คือหัวใจสำคัญของการออกแบบระบบที่รองรับการขยายตัว (Scalability) ในอนาคต

วิธีเปลี่ยนมาใช้ระบบคิว (Background Queue)

ในการเขียนโค้ดจริงเรามักใช้เครื่องมืออย่าง BullMQ ควบคู่กับ Redis (ระบบเก็บข้อมูลในหน่วยความจำที่ทำงานได้เร็วมาก) เพื่อทำหน้าที่เป็นคิวเก็บงาน ลองดูตัวอย่างการปรับแก้โค้ดให้เป็นแบบ Asynchronous (การทำงานที่ไม่ต้องรอผลลัพธ์ทันที) ดังนี้

// โค้ดที่ใช้ระบบคิว (แบบที่ถูกต้อง)
app.post('/signup', async (c) => {
  const body = await c.req.json();
  const user = await db.users.create({ email: body.email });

  // ส่งงานเข้าคิวแทนการส่งอีเมลโดยตรง
  await emailQueue.add('send-welcome-email', { 
    email: user.email 
  });

  // ตอบกลับผู้ใช้ทันที
  return c.json({ message: 'สมัครสำเร็จ' }, 202);
});

ในโค้ดนี้บรรทัด emailQueue.add จะทำการบันทึกข้อมูลงานลงใน Redis ซึ่งใช้เวลาเพียงเสี้ยววินาที API ของเราจึงสามารถส่งคำตอบ 202 (สถานะยอมรับคำขอแต่ยังประมวลผลไม่เสร็จ) กลับไปหาผู้ใช้ได้ทันที ทำให้ผู้ใช้รู้สึกว่าระบบทำงานเร็วและตอบสนองดีมาก

ส่วนงานส่งอีเมลจะถูกย้ายไปที่ไฟล์ worker.js ซึ่งเป็นโปรแกรมแยกต่างหากที่คอยวนลูปดึงงานจากคิวมาทำ หากงานชิ้นไหนส่งไม่สำเร็จ เรายังสามารถตั้งค่าให้มัน Retry (การลองทำซ้ำ) ได้โดยอัตโนมัติ ซึ่งเป็นเรื่องที่ทำไม่ได้เลยหากส่งอีเมลใน Request หลัก

ข้อควรระวังและแนวปฏิบัติที่ดี

แม้การใช้คิวจะดีมาก แต่คุณต้องระวังเรื่อง Idempotency (คุณสมบัติที่การทำซ้ำกี่ครั้งก็ได้ผลลัพธ์เท่าเดิม) เพราะระบบคิวอาจจะหยิบงานเดิมมาทำซ้ำหากมีข้อผิดพลาดเกิดขึ้น คุณควรมีตัวระบุงานที่ชัดเจน เช่น ใช้ user_id เป็นกุญแจสำคัญ เพื่อป้องกันไม่ให้ผู้ใช้ได้รับอีเมลต้อนรับซ้ำหลายรอบ

นอกจากเรื่องอีเมลแล้ว งานอื่นๆ ที่ควรแยกออกมาทำในคิว ได้แก่ การย่อขนาดรูปภาพ การสร้างไฟล์ PDF การส่งข้อมูลไปยังระบบวิเคราะห์ข้อมูล (Analytics) หรือการเรียกใช้ AI API ที่กินเวลานาน หลักการนี้ใช้ได้กับงานทุกอย่างที่ผู้ใช้ไม่จำเป็นต้องรอผลลัพธ์ในทันที

สุดท้ายอย่าลืมตรวจสอบสถานะของงานในคิวอยู่เสมอ ควรมีการตั้งค่า Dead Letter Queue (คิวสำหรับเก็บงานที่พยายามทำซ้ำจนเกินกำหนดแล้วแต่ยังไม่สำเร็จ) ไว้ด้วย เพื่อให้คุณสามารถกลับมาตรวจสอบได้ว่ามีอีเมลฉบับไหนที่ส่งไม่ไปจริงๆ และเข้าไปแก้ไขปัญหาได้อย่างตรงจุด

สรุป

การส่งอีเมลภายใน API Request คือกับดักที่โปรแกรมเมอร์มือใหม่มักพลาด เพราะมันทำให้ระบบช้าและเปราะบางต่อความผิดพลาดของระบบภายนอก การย้ายงานเหล่านี้ไปไว้ในระบบคิวจะช่วยให้ API ของคุณทำงานได้รวดเร็วและมีความน่าเชื่อถือสูงขึ้น แม้ในวันที่ผู้ให้บริการอีเมลของคุณจะมีปัญหา ระบบของคุณก็ยังคงให้บริการผู้ใช้ต่อไปได้

หากคุณกำลังทำโปรเจกต์ส่วนตัวหรือโปรเจกต์สมัครงาน ให้ลองฝึกใช้ระบบคิวตั้งแต่ตอนนี้ แม้จะดูเหมือนยุ่งยากกว่าการเขียนโค้ดบรรทัดเดียว แต่มันเป็นทักษะสำคัญที่บริษัทไอทีชั้นนำมองหา เพราะมันแสดงให้เห็นว่าคุณเข้าใจวิธีการออกแบบระบบที่มีประสิทธิภาพและรองรับการใช้งานจริงได้

ตัวอย่างการนำไปใช้จริง: หากคุณทำโปรเจกต์แอปแชท แทนที่จะส่งการแจ้งเตือน (Push Notification) ทันทีที่ข้อความเข้า ให้คุณลองใช้ระบบคิวมาจัดการแทน เพื่อให้มั่นใจว่าต่อให้คนส่งข้อความพร้อมกันเป็นหมื่นคน แอปของคุณก็จะไม่ค้างและผู้ใช้จะได้รับการแจ้งเตือนอย่างครบถ้วนแน่นอน


ที่มา: Why You Should Never Send Emails Inside Your API Requests — freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

แชร์บทความ

Facebook X LINE

บทความที่เกี่ยวข้อง

จัดการเซิร์ฟเวอร์ผ่าน VS Code ให้ง่ายขึ้นด้วย Easy SSH พร้อมฟีเจอร์โหลดไฟล์ผ่านคลิกเดียว

จัดการเซิร์ฟเวอร์ผ่าน VS Code ให้ง่ายขึ้นด้วย Easy SSH พร้อมฟีเจอร์โหลดไฟล์ผ่านคลิกเดียว

เบื่อไหมที่ต้องสลับหน้าจอไปมาเพื่อจัดการเซิร์ฟเวอร์? มาลองใช้ Easy SSH ปลั๊กอิน VS Code ที่ช่วยให้คุณรีโมทผ่าน Terminal ได้สะดวก แถมโหลดไฟล์ได้ง่ายแค่กด Ctrl+click

ที่มา: DEV Community

1 hour ago 11 นาที
2 views
วิธีดึงข้อมูลราคาจาก Google Hotels ด้วย API สำหรับนักพัฒนา

วิธีดึงข้อมูลราคาจาก Google Hotels ด้วย API สำหรับนักพัฒนา

อยากทำแอปท่องเที่ยวแต่ดึงข้อมูลราคาจาก Google Hotels ไม่ได้? มาดูวิธีใช้ Apify Actor ช่วยดึงข้อมูลแบบอัตโนมัติด้วย Python ง่ายๆ ไม่ต้องกลัวเว็บพัง

ที่มา: DEV Community

5 hours ago 8 นาที
3 views
วิธีเช็กความพร้อมโปรเจกต์ก่อนปล่อยงานจริงด้วย ReleaseReady

วิธีเช็กความพร้อมโปรเจกต์ก่อนปล่อยงานจริงด้วย ReleaseReady

เคยไหม? โค้ดรันได้ในเครื่องแต่พอปล่อยจริงกลับพัง! มาดูวิธีตรวจสอบความพร้อมของโปรเจกต์ก่อนอัปขึ้น GitHub ด้วยเครื่องมือ ReleaseReady กัน

ที่มา: DEV Community

9 hours ago 9 นาที
4 views