Email 기능 구현 - 백엔드

2023. 9. 12. 18:32자바스크립트/타입스크립트

반응형

#1. 개요

  • 기존 이메일 전송 시 발신자 no-reply@회사명.net 로 밖에 전송이 안 됐었다.
    • smtp DB table에서 가져온(추가된) 발신 이메일 리스트 중 하나로 전송할 수 있게끔 구현하였다.
    • smtp 설정을 해야만 이 이메일들로 발신 할 수 있는 데, 이를 위한 사용자 가이드를 구현하였다.
  • 이메일 템플릿을 사용자가 미리 등록할 수 있게끔 구현하여 메일 전송 시 번거롭지 않게 보낼 수 있게끔 도와준다.
    • email template DB table
  • 이메일 수신 사용자가 여러명이 될 수 있도록 구현하였다.
  • email 전송 전용 서버를 구현하여, VC works 서버와 이메일 전송 서버를 분리한다. 따라서 대규모 이메일 발송 시에도 VC works 서버의 서비스 성능에 영향을 끼치지 않도록 한다.
  • 이메일 전송 기능을 기존 메뉴에서 분리시킨다.
    • 다른 메뉴(모듈)에서도 해당 이메일 모듈로 쉽게 이메일을 보낼 수 있게 만든다.

#2. 분석

1. nodemailer

사용자가 회원가입 시, 입력한 이메일이 유효한지 검증하는 상황 혹은 사용자에게 임시 비밀번호를 전달하는 경우 등등 서버 측에서 사용자에게 이메일을 보내야하는 경우가 존재. 이때, Node에서 사용할 수 있는 유용한 모듈이 바로 Nodemailer

  • node.js에서 메일 쉽게 보낼 수 있도록 도와주는 모듈
  • smtp를 포함한 다양한 전송 방식을 지원
  • 메일을 보낼 transporter 객체 생성
    • host, port, auth의 정보를 객체로 만들어서 createTransport에 매개변수로 넣어줌
    • 이정보는 사용자의 정보와 상관 없음. 즉 하나로 통일 되어 있으면 된다
host: host,
port: port,
auth: {
   user: auth.user,
   pass: auth.password,
},
let transporter = nodemailer.createTransport(configuration[, mail options])

 

  • 메일 보내기
    • from, to, subject(이메일 title), text(이메일 바디)등을 객체로 생성해서 같이 보냄
const mailOptions = {
      from: mailingInfo.from,
      to: mailingInfo.target.map((el) => el.email),
      subject: mailingInfo.title,
      text: mailingInfo.body,
    };
const info = await transporter.sendMail(mailOptions);

2. microservice

  • 개별 microservice는 단일 목적으로만 존재하며 독립적이고, 다른 인스턴스 및 서비스와 무관한 서비스.
  • microservice 아키텍처 스타일로 app을 구축할 때 접근 방식은 둘 이상의 microservice로 구성된 단일 app을 개발하는 것임.
  • 각 microservice는 별도로 개발되며, 완료된 app은 모든 microservice의 합계다.
  • the finished application is the sum of all the microservices
  • microservice, 또는 모듈은 서로 분리되어 있지만 여전히 통신 가능.
  • 교차종속성 (cross - dependency)는 microservice 아키텍처에서 일반적이다
    • 즉, 다른 서비스의 도움 없이 단일 서비스를 수행할 수 없음
  • 연결하는 방법
    • message broker(amqp)
      • Brokers, Message queuing 및 RabbitMQ
    • remote prodecure call(RPC)
    • REST API
    Untitled.png
  • message queuing
    • message queue를 사용하면 app의 일부가 메시지를 대기열에 비동기식으로 푸시하고, 올바른 대상으로 전달 되는 지 확인 가능
    • message queuing을 구현하려면 rabbitMQ와 같은 메시지 브로커가 좋은 옵션.
    • 메시지 브로커는 수신 서비스가 사용 중이거나, 연결이 끊어졌을 때 임시 메시지 저장소를 제공한다.
    Untitled.png
  • 브로커와의 통신처리
    • 메시지 브로커는 microservice의 중개자(exchange) 역할을 하며, 한 app(producer)에서 메시지를 수신하여, 다른 app(consumer)에게 메시지를 전달하여 작업을 수행함.
    • 예를 들어, 메시지 브로커를 사용하면 메시지가 큐에 직접 게시되지 않음
      • 대신 producer가 exchange로 메시지를 보냄.
      • exchange의 역할은 producer가 app의 메시지를 전달받고, 올바른 메시지 큐로 전달하는 것임
      • 메시지는 consumer 가 해당 메시지를 받을 때까지 큐에 있고, 받으면 삭제 된다.
    • 고를 수 있는 메시지 브로커는 다양한데, 요구사항에 따라 고르면 좋다. rabbitMQ와 apache kafka는 두 개의 오픈 소스 message 브로커이다.

3. amqp

  • pub / sub
    • producer 는 어떤 메시도 큐로 직접 보내지 않고 , exchange(broker)를 보낸다.
    • producer는 내가 만든 메시지가 어떤 대기 큐로 전달 될 지 모름
  • exchange - producer로부터 메시지를 받아서 큐들에게 푸시한다.
    • 어떤 큐에게 푸시할 건지는 exchange type에 의해 정해짐.
    • producer → exchange → queue
    • fanout - exchange가 알고 있는 모든 큐들에게 메시지 전송
    • 사용법
      • ch.sendToQueue()를 쓰지 않고
        • 큐에게 direct로 보냄
      • ch.publish(exchange 이름, 큐 이름) 사용
        • exchange에게 보냄

Untitled.png

💡 email의 amqp는 email의 amqp serviceCollection과 일일 대응한다.
즉 sub하는 애에 대해서 처리를 해줘야 한다.

4. 코드 분석 및 설계

모듈명 기능 타 모듈과의 관계 기타
app - email email 발송만 담당하는 server. biz-email 실행 app-fungul 서버와 별개로 실행
biz - admin smtp, 템플릿 관리 / 이메일 전송 정보 전송 amqp를 통해 해당 정보들 biz-email로 전송  
biz - email 이메일 전송 정보, log등 DB에 기입 / 이메일 실제 전송 module - email 의 기능들을 사용 email Request DB만 의존
module - email email 전송 기능 구현 biz-email에게 기능 제공  
module - amqp amqp 전송 정보 제공 biz-email에게 기능 제공  
  • app - email
    • email 서버 실행
  • biz - admin
    • smtp관리 / 템플릿관리 / 이메일 사용자 주소록 / 피투자회사 주소록을 통한 이메일 전송
      • 모든 템플릿 관리가 admin에서 이뤄짐 → 중요
      • type상관없이 다여기서 이뤄짐
    • smtp(발송이메일주소) , 수신 이메일 주소, 이메일 템플릿을 email로 amqp로 전달
      • emailTemplateDB
      • emailSmtpDB
      • 이메일 전송 과정은 admin말고 다른 데에서도 많이 이뤄짐
    • dao / router / service /
  • biz - email
    • email 실제 발송
      • emailRequestDB
    • amqp로 받아온 정보를 service-amqp-insertOneRequest()에서 request로 발송 정보 insert하고 module email에서 가져온 mailSender 함수로 이메일 전송.
  • module - email
    • email template 기입 담당
      • 실제 template을 여기다 쌓아가면 됨
    • nodemailer와 연결하여서 mailSender함수 제공
  • module - mongodb
    • email request
      • 단일 로그 / smtp 정보 취소
    • email smtp
    • email template
      • default template listup
      • DB로 관리할 필요가 있을 까?0
  • module - api
    • email 추가 - amqp에서 전달용
    • admin - email (수신정보, 발신정보, 템플릿 id 등 수신)
    • admin - smtp (smtp 정보 관리)

#3. 문제해결

1. 의존성 문제

  • module 구성 후 처음 실행 시 해당 위치에다가 rush add -p @ddock/email (아니면 다른 module 이름)
    • 문제 있으면 root에다 ./build
    • 혹은 각 페이지에다 rushx build
    • 각 module 자체 오류가 있는 거 일수도 있음 그러면 해당 페이지에서 build해서 오류를 잡아내면 된다
  • 모듈을 import할 때, 상대주소로 가져오지 말고, 위에거를 통해서 @ddock/moduleName을 package.json에 추가한 다음 가져오게끔 한다.
  • compound update
    • 안 되는 부분 node_module삭제하는 것도 방법
  • dependency cycle
    • 컴포넌트 의존성 그래프에 순환이 있어서는 안 된다
      • 순환이 없고, 단방향이어야 한다
    • 컴포넌트에서 시작했을때 의존성 관계를 따라가면서 최초의 컴포넌트로 되돌아갈 수 없다
    • 문제점
      • 순환 되는 컴포넌트들은 서로 의존하기에 하나의 거대한 컴포넌트로 봐야 함
      • 즉 하나를 테스트를 할 때 같이 순환하는 컴포넌트들까지도 빌드하고 통합해야한다

2. 암호화 문제

  • Error: Trying to add data in unsupported state 문제
    • 보안 관련 문제
    • twoWayEncrypt할 때 update할 때와 createCipher할 시점이 매번 되어야한다
    Thats mainly because every time we run the encrypt or decrypt we should repeat crypto.createCipher('aes192', secrateKey);
     and crypto.createDecipher('aes192', secrateKey);
    • 두 가지 문제
      • createCipher와 update 및 final은 같이 실행되어야 함
        • 현재버전을 사용하면 이 문제는 해결이 됨
      • 또한 어떤 대상을 암호화할 때마다 새로 실행되어야 한다
        • 그러나 여전히 현재 버전도 이 문제를 해결해주지 않음
      • 따라서 아예 호출하는 것에서 currying함수를 통해 구현하는 것이 옳다.
//currying 버전
const twoWayDecrypt = (value: string) => {
    return crypto.twoWayDecrypt(value, env.secret.key, env.secret.iv);
};
//현재버전
export const twoWayEncryptFactory = (secretKey: string, ivKey: string) => {
  return (text: string) => {
    const key = getKey(secretKey);
    const iv = getIv(ivKey);
    const cipher = createCipheriv(twoWayAlgorithm, key, iv);
    return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
  };
};
//구버전
export const twoWayEncryptFactory = (secretKey: string, ivKey: string) => {
  const key = getKey(secretKey);
  const iv = getIv(ivKey);
  const cipher = createCipheriv(twoWayAlgorithm, key, iv);
  return (text: string) => cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
};

#4. 개선점

1. 메일 전송 속도

1) 발송 요청당 한 번의 amqp 통신

  • 여러 메뉴에서 기획설계대로 이메일을 보낼 수 있게끔 email 기능을 따로 email 모듈에 구현을 하였는데, 이를 위해 amqp 통신으로 admin등 메뉴들로부터 email 모듈로 이메일 발송 정보를 전달한다.
  • amqp 통신을 딱 한 번만 하여 수신인 리스트를 포함한 모든 정보들을 넘겨준다. 따라서 amqp 통신을 매 수신인 마다 하는 것이 아닌 발신요청 당 한 번 씩 하게 되어서 이메일 전송 속도를 크게 향상시킬 수 있다.
  • DB 상 나타나는 이 방법 사용 시 각 수신인 당 이메일 전송 시간:
    • 정확히는 log기록 용 이메일 발송 요청 정보 DB에 기입 되는 시간이다.
    • 그러나 이 두 시간은 차이가 거의 미비하기에 동일시하여 시간측정이 가능하다.
    • 두 수신인 간 이메일 전송 시간 차는 대략 1.5초다.

Untitled.png

2) 분리된 email 서버

  • email 서버를 app - email에 따로 구현하여 email 전송 용 서버를 기존 vc works 서버에서 분리시킨다. 외부 api에 접속해 email 전송 통신을 하는 email 서버는 리소스를 많이 소모할 것이기에 (특히 대규모 이메일 전송 시), email 서버를 분리시켜 email 전송 시간을 단축시킬 수 있을 뿐더러, 기존 VC works의 기타 서비스들이 원활하게 제공 될 수 있도록 도와준다.

2. 보안

1) smtp 정보

  • smtp 등록
    • 네이버
    • 💡 네이버 → 메일 → 환경설정 → POP3/smtp설정 → POP3/smtp설정 → POP3/SMTP사용함
      • SMTP host - smtp.naver.com
      • SMTP port - 465
      • SMTP user - 본인 네이버 이메일 주소
      • SMTP password - 본인 네이버 비밀번호
    • gmail
    • 💡 google -> 계정 -> 보안 -> google에 로그인 -> 앱 비밀번호 -> 앱선택 ( 메일) -> 기기 선택 (본인 PC 기종 ) -> 생성 -> 앱 비밀번호가 password
      • SMTP host - smtp.gmail.com
      • SMTP port - 587
      • SMTP user - 본인 gmail email 주소
      • SMTP password - 생성된 앱 비밀번호
  • 해당 정보들이 DB에 그대로 대입이 되고 있었는데 이는 보안 취약점이 된다.
  • host, port, user, password(특히 보안 되어야 함) 등 정보를 하나의 문자열로 암호화를 시켜 보안을 만족시킬 뿐더러 DB 상 공간 소모도 줄일 수 있음.
    • 암호화, 복호화의 구현 난이도는 다소 올라가지만 DB 공간 리소스를 절약할 수 있음

3. 확장성

1) 여러 수신자에게 메일 전송 가능

  • payload는 여러명이 올 수 있도록 (user→admin / admin → email 둘다 마찬가지)
  • 각 발신자 마다 이메일 템플릿에 정보가 다르게 기입 될 수 있음.
    • 예) 회사명, 수신자 성함 등
  • 이를 위해 email 모듈에서 발신 전 정보 기입을 하나씩 하고, 이메일 전송은 발신자마다 하게 됨.
    • 따라서 다수의 수신자에게 각자의 회사명, 이름을 이메일 본문에 기입을 하며, 한번에 전송을 할 수 있게 구현함.
//payload

//변경 전
targetEmail: string;
targetName: string;
targetCompany: string;
//변경 후
target: {
  email: string;
  name: string;
  company: string;
}[];

//EmailRequestDocument는 오히려 반대로
//변경 후
mailingInfo: {
    targetEmail: string;
    targetName: string;
    targetCompany: string;
    from: string;
    title: string;
    body: string;
};

2) 템플릿 종류에 따른 사용자 입력 정보 대입 기능 공통화

  • 여러명에게 이메일을 전송을 할 때도 amqp 통신은 한 번으로 모든 수신자 리스트를 넘겨주기에, email 모듈에서 템플릿에 각 수신자에게 해당하는 입력 정보를 기입해야함.
  • 이를 통해 다른 메뉴에서는 따로 이메일 템플릿에 정보를 기입할 필요가 없이, 받은 정보들과 각 메뉴에서 필요한 이메일 템플릿 정보만 넘겨주면 됨.
  • 따라서 다른 메뉴에서도 이메일 전송하기가 간편해진다.

3) email 전송 기능 모듈 분리

  • email 전송 기능을 기존 admin 기능에서 모듈로 분리를 시켜서, 다른 모듈에서 이메일 전송 시 따로 이메일 전송 기능을 구현해야하는 것이 아니라, 해당 email 모듈을 그대로 사용하면 된다.
  • 또한 admin에서 구현한 smtp, 템플릿을 그대로 가져다 쓰면 새로운 모듈에서는 email 관련하여 새로 구현할 것이 많지 않다.

4. 자율도

1) 개인 발송 이메일 설정 가능 - smtp

  • 관리자 페이지의 smtp 관리를 통해 사용자가 원하는 발신 이메일로 이메일을 발신할 수 있게끔 해준다.

2) 개인 이메일 템플릿 등록 가능

  • 각 종류 별로 VC works에서 기본적으로 제공하는 이메일 템플릿 제외, 사용자가 직접 원하는 이메일 템플릿으로 이메일 전송을 할 수 있다.
  • 각 종류 별로 기본 템플릿으로 설정할 수 있는 개수는 딱 하나이며, 어떤 종류에 새로운 템플릿을 추가하여 기본 템플릿으로 설정하였다면, 다른 템플릿은 자동으로 기본 템플릿이 아니게 되도록 구현을 하였다. 이는 수정 시에도 마찬가지다.
  • 사용자는 이메일 전송 페이지에서 모든 템플릿을 다 확인하여 제일 알맞는 템플릿을 고르는 것이 아닌 각 템플릿 종류 마다 기본으로 설정한 템플릿으로 바로 고르게끔 구현하였다. 사용자 입장에서 수신자만 설정하고, 템플릿 종류만 고르면 바로 전송이 쉽게 되게끔 구현한 것이다.

 

반응형