이메일 전송을 포함한 알림 기능 구현하기(with Spring Boot)

haaaalin·2023년 10월 2일
4
post-thumbnail

우리 프로젝트에 필요했던 기능

현재, 자신이 만든 프로젝트를 자랑하기도 하고, 프로젝트를 같이 할 팀원을 모집할 수 있는 서비스를 개발하고 있다. 우리는 아래처럼 쪽지를 이용해 프로젝트 팀에 지원한 사용자와 컨택할 수 있도록 진행할 예정이었다.

그렇다면 알림 기능은 무조건 필수로 개발해야 했다. 우리는 앱으로 개발하지 않고 웹으로 개발 중이었기 때문에 푸시 알림은 구현하지 않고, 아래 사진처럼 사이트에 들어가 있을 경우 아이콘의 숫자로 알림이 왔다는 사실을 확인할 수 있게끔 하려고 한다.

또, 여느 다른 서비스처럼 알림이 발생할 경우 이메일 알림도 있도록 개발할 예정이다.

그렇다면 구현해야 할 상세 기능을 정리해보자. (알림 기능을 구현하게 되어, 다른 기능에도 알림을 붙이기로 했다)

  • 다른 사용자가 본인을 팔로우 했을 때 알림을 받을 수 있다
  • 프로젝트 모집 글을 올렸을 경우, 지원 신청이 발생했을 때, 알림을 받을 수 있다.
  • 쪽지 수신 시 알림을 받을 수 있다
  • 알림의 읽음 여부를 알 수 있다
  • 이메일 알림을 받을 수 있다

알림을 어떻게 구현할까?

알림 테이블을 만들자

아래와 같이 알림이 발생할 때마다 알림 데이터를 저장하는 테이블을 하나 생성했다. 알림이 발생했을 때, 이메일만 전송하는 방향도 있지만, 사용자의 나은 경험을 위해 쌓여있는 알림 목록을 보여주기 위함이었다.

추후에 너무 많은 데이터가 쌓일 것을 방지해, 오래된 알림 데이터는 배치를 돌려 일괄 삭제할 예정이다.

알림 타입에 따라 공통적으로 사용되는 값(고정 메시지, 아이콘 등)을 따로 관리해주기 위하여 type을 저장하고 있고, 웹사이트에서 알림 목록을 볼 때에도 알림 확인 여부에 따라 다른 UI로 나타내기 위해 is_read 필드를 추가했다.

알림이 발생하는 과정

우리 서비스는 아래 3가지 경우에 알림이 발생하고 있다.

  • 본인을 팔로우 했을 경우
  • 사용자가 올린 팀원 공고에 누군가가 지원했을 경우 / 팀원 지원 요청이 수락됐을 경우
  • 쪽지를 수신했을 경우

각 3가지 경우가 진행되는 로직 마지막에 notification 데이터 추가 및 이메일 전송을 진행하는 메서드를 추가했다. 아래 한 사용자가 다른 사용자를 follow 할 때 예시를 살펴보자.

@Transactional
public void addFollow(Long toId, Member loginUser) {
	 /**
    * follow를 추가 로직(생략)
    */
    NotificationType notificationType = NotificationType.FOLLOW;
    
		notificationType.setMessage(loginUser.getNickname(), "");
    NotificationDto notificationDto = NotificationDto.builder()
            .type(notificationType)
            .content(notificationType.getMessage())
            .build();

    followRepository.save(follow);
    notificationService.addNotification(notificationDto, toId);
}

follow를 추가하는 로직이니 type을 follow로 설정해주고 있고, 해당 유저의 닉네임을 얻어, 동적으로 알림 메시지를 설정한 후 NotificationServiceaddNotification 메서드를 호출해, 알림 데이터 저장 및 전송 로직을 진행하고 있다.

알림 메시지 설정은 다음과 같이 진행되고 있다.

  • 고정 메시지: “님이 팀 합류를 “
  • 동적으로 넣을 문자: 유저의 닉네임, 원하고 있습니다 OR 수락하였습니다.

⇒ “익명의 개발자님이 팀 합류를 원하고 있습니다.”

addNotification()

Notification 엔티티 저장 후, 알림 메일을 발송하고 있다.

@Transactional
public void addNotification(NotificationDto dto, Long memberId) {
    Member member = memberService.findMemberById(memberId);
    dto.setMember(member);
    Notification entity = notificationRepository.save(dto.toEntity());
    try {
        mailingService.sendNotificationEmail(entity);
    } catch (MessagingException e) {
        throw new BusinessException(SEND_EMAIL_FAIL);
    }
}

이때 주의해야 할 점은 이메일 전송은 굉~장히 느리니 비동기로 진행하도록 @Async 어노테이션을 이용해 비동기로 처리하게끔 하는 것을 추천한다.

이메일은 언제 전송하는 게 좋을까?

사실 이메일도 어떤 시점에 보내냐에 대한 고민을 많이 했었다.

Discord 또한 메일 전송 기능이 있는데, 채널에 메시지가 올 때마다 매번 전송하지 않고, 일정 시간 동안 확인 안 한 메시지가 쌓이게 되면 그때 채널에 확인 안 한 메시지가 n개 있습니다 이런 식으로 알림이 오는 걸 확인했다.

따라서 우리도 알림을 좀 쌓아뒀다가 스케줄러를 이용해 한 번에 이메일 전송을 할까? 도 생각했지만, 사실 이 서비스에서는 팔로우 기능은 그렇다 쳐도, 팀원을 모집하고, 쪽지를 주고 받는 기능은 알림과 행위의 시간 차가 너무 많이 나선 안될 것 같다고 판단했다.

결론은 행위가 발생함과 동시에 이메일을 보내자! 였다.

이메일은 어떻게 전송할까?

세팅하기

build.gradle에 의존성을 추가한다.

이때, 메일을 html로 구성하고 싶다면 thymeleaf도 함께 추가해주어야 한다.

implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'

application.yml 파일에 다음과 같이 mail 관련 설정도 추가한다.

mail:
    protocol: smtp
    host: smtp.gmail.com
    port: 587
    username: [메일 발신자 이메일]
    password: [앱 비밀번호]
    default-encoding: utf-8
    properties:
      mail:
        smtp:
          starttls:
            enable: true
          auth: true

mail.password에 들어가는 앱 비밀번호와 관련된 내용은 아래 블로그가 잘 설명해주고 있으니 참고하면 좋을 것 같다.

Google - Gmail SMTP 사용을 위한 세팅

html을 포함한 이메일

내용만 들어가 있는 이메일보다, 좀 더 알림 확인을 편하게 할 수 있도록 메일을 제공하고자 하였다. 따라서 html을 포함하고 있는 이메일을 전송하기로 했다.

따라서 아래와 같이 html 파일을 구성해 동적으로 데이터를 넣어 이메일을 전송했다. (아직 디자인은 미확정이다.. 백엔드가 디자인한 한계이다😂)

이렇게 무료로 이메일 템플릿을 제공하는 사이트도 있으니 참고하자

https://unlayer.com/templates

추후에 팔로우를 건 사용자의 프로필 사진이나, 짧은 소개글도 함께 html에 포함할 생각이다. 일단은 바로 우리 사이트로 접근할 수 있도록 버튼 하나를 넣어놨다.

이메일 템플릿 적용하기

이메일 템플릿 html 파일에서 핵심은 딱 아래 2줄이었다.

<img src="cid:notice-icon">
<span th:text="${content}">

<img src="cid:notice-icon">

: 추후에 이메일 전송 시에 설정을 통해 이메일에 이미지를 포함해서 전송할 수 있다.

<span th:text="${content}">

: 동적으로 텍스트를 넣을 수 있다.

자세한 건 아래 코드를 통해 알아보자.

MailingService

메일 전송 서비스를 컴포넌트로 생성해 주입받아 사용할 수 있도록 코드를 작성했다.

자세한 코드는 밑에서 살펴보고 주석을 통해 흐름만 이해해보자.

@Component
@RequiredArgsConstructor
public class MailingService {

    private final JavaMailSender javaMailSender;
    private final SpringTemplateEngine templateEngine;

    private static final String EMAIL_TITLE_PREFIX = "[Graphy] ";

    @Async
    public void sendNotificationEmail(Notification notification) throws MessagingException {
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");
				
				// 메일 제목 설정
        messageHelper.setSubject(EMAIL_TITLE_PREFIX + notification.getContent());
				// 메일 수신자 설정
        messageHelper.setTo(notification.getMember().getEmail());

				// html에 들어갈 동적데이터 설정하기
        HashMap<String, String> emailValues = new HashMap<>();
        emailValues.put("content", notification.getContent());
        String text = setContext(emailValues);

        messageHelper.setText(text, true);

				// 이메일에 포함될 이미지 설정
        messageHelper.addInline("logo", new ClassPathResource("static/images/image-2.png"));
        messageHelper.addInline("notice-icon", new ClassPathResource("static/images/image-1.png"));
				// 메일 전송
        javaMailSender.send(message);
    }

    private String setContext(Map<String, String> emailValues) {
        Context context = new Context();
        emailValues.forEach(context::setVariable);
        return templateEngine.process("email/index", context);
    }
}

[번외] JavaMailSender에 대해

JavaMailSender란?

스프링에서 제공하는 메일을 보다 손쉽게 보낼 수 있도록 해주는 API이다.

MailSender 인터페이스를 상속 받아, 본문에 HTML을 전송할 수 있도록 기능이 추가된 인터페이스이다.

MailSender는 SimpleMailMessage을 정의해 텍스트 메일만 발송할 수 있다.

MimeMessage

그렇다면, JavaMailSender가 추가적으로 제공하는 MimeMessage는 뭘까?

MimeMessage는 앞서 언급했듯이 HTML을 본문에 전송할 수 있다. Multipart 기능도 제공하고 있어 메일에 첨부파일도 같이 전송할 수 있다는 것이 장점이다.

profile
한 걸음 한 걸음 쌓아가자😎

0개의 댓글