본문 바로가기
Spring

Spring boot - thymeleaf를 통한 html 파일 다국어 이메일 전송

by khds 2024. 9. 29.

 

들어가기

 

프로젝트 진행 중 메일 전송 기능을 진행하고 있다. 문제는 글로벌 사용자를 대상으로 하고 있기 때문에, 언어별 html 파일이 존재해야 한다. 하지만 언어별 html 파일을 만들고 비즈니스 로직에서 분기점을 만드는 방법은 번거로운 방법이며 가독성이 떨어질 것이다. 그래서 MessageSource를 활용하려고 한다. html 파일은 하나만 만들어 두고, 언어별 messages.properties 파일 내 html 텍스트를 지정하는 것이다. 

 

이 글에선 이메일 전송을 위한 thymeleaf를 통한 html 파일 작성 시 한국어, 일본어, 영어 버전으로 작성하는 과정을 담고 있다. 

MessageSource에 대한 내용은 https://khdscor.tistory.com/133를 참고하길 바란다.

 

Spring boot에서 MessageSource와 Locale을 활용한 다국어 에러 처리

들어가기 현재 진행하는 프로젝트는 한국인만이 아니라 외국인 대상으로도 출시를  목표로 하고 있다. 그렇기에 평소에 하던 것처럼 한국어로만 앱을 구성하는 것이 아닌, 다국어 버전으로 작

khdscor.tistory.com

 

이메일 전송 기능에 대한 내용은 https://khdscor.tistory.com/131를 참고하길 바란다.

 

Spring boot - email 발송 기능 구현(with Gmail)

들어가기 spring boot를 통해 프로젝트를 진행하던 중 초대장을 메일로 전송하는 기능을 구현해야 하여 메일 전송에 대해서 알아보고자 한다. 일반 웹 통신(HTTP)와 다르게 메일 전송을 위해서는 SM

khdscor.tistory.com

 

 

본론

1. html 및 properties 파일 작성

 

본격적으로 진행하기 전 thymeleaf 템플릿 엔진을 사용하는 html 파일과 메일 전송 시 화면을 확인해 보자.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
  <div style="font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f9f9f9; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #dddddd;">
    <div style="text-align: center; padding: 20px 0;">
      <img src='cid:image1' alt="keytrip logo" style="max-width: 100%; height: auto;">
    </div>
    <p style="text-align: center; font-size: 18px; color: #333333; margin: 0; line-height: 1.6;">
      <strong th:text="${leaderName}"></strong>님이 <strong th:text="${campaignName}"></strong> 캠페인에 당신을 초대했습니다! 🎉<br>
      함께 즐거운 시간을 보내고 유익한 경험을 나누는 기회를 놓치지 마세요.
    </p>
    <div style="text-align: center; margin: 20px 0; padding: 20px; background-color: #ffffff; border-radius: 8px;">
      <h3 style="font-size: 16px; color: #0012ce; margin: 0 0 10px 0;">초대 세부사항:</h3>
      <p style="font-size: 16px; color: #333333; margin: 0; line-height: 1.6;">
        캠페인 이름: <strong th:text="${campaignName}"></strong><br>
        캠페인 코드: <strong th:text="${campaignCode}"></strong>
      </p>
      <p style="font-size: 14px; color: #555555; margin: 20px 0 0 0;">
        앱내에서 캠페인 코드를 입력하거나, 아래 버튼을 클릭하여 캠페인에 참여하세요.
      </p>
      <div style = "margin-top: 20px;">
        <a href="#"
           style="display: inline-block; padding: 12px 24px; width: 220px; font-size: 16px; color: #ffffff; background-color: #0012ce; text-decoration: none; border-radius: 4px;">초대
          수락</a>
      </div>
    </div>
    <p style="text-align: center; font-size: 14px; color: #555555; line-height: 1.6; margin: 20px 0;">
      초대를 수락하면, 캠페인에 대한 자세한 정보와 다음 단계에 대해 안내해 드리겠습니다.<br>
      궁금한 점이 있거나 도움이 필요하시면 언제든지 [지원팀 연락처]로 문의해 주세요.
    </p>
    <p style="font-size: 14px; color: #555555; text-align: center; line-height: 1.6;">
      감사합니다,<br>
      keytrip 팀 드림
    </p>
  </div>
</body>
</html>



실제 서비스에서 사용하고 있는 화면이다. 위와 같이 html 파일을 작성한다면 사용자 언어에 상관없이 항상 한국어 버전으로만 이메일이 전송될 것이다. 

이제 언어별 html 파일이 이메일 전송될 수 있도록 몇 가지만 추가해 보자.

언어별 properties 파일은 한국어, 영어, 일본어만 작성을 하였고 아래와 같은 이름으로 지정하였다.

 

이름을 "email"로 지정을 하였으므로 MesssageSourceConfig 파일에 추가적인 작업을 진행해 주었다.

@Component
public class MessageSourceConfig {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames(
            "classpath:messages/email"
        );
        messageSource.setDefaultEncoding(StandardCharsets.UTF_8.toString());
        return messageSource;
    }
}

 

html 파일은 서론에서 언급한 바와 같이 오직 하나의 파일만을 준다. 

 

그렇다면 properties와 html 파일 내부는 어떻게 작성되어야 할까? 이는 messageSource로 다국어 버전을 만들 때와 동일하게 진행한다. html에서 사용하는 텍스트들을 properties 내 변수로 지정하고, html에서는 지정한 변수를 넣어주면 된다. 아래는 위에서 보인 html 파일의 텍스트들을 모두 변수로 표현하여 작성한 properties 파일이다.

invite-campaign.title={0}에 초대되었습니다! 🎉
invite-campaign.text1=<strong>{0}</strong>님이 <strong>{1}</strong> 캠페인에 당신을 초대했습니다! 🎉
invite-campaign.text2=함께 즐거운 시간을 보내고 유익한 경험을 나누는 기회를 놓치지 마세요.
invite-campaign.text3=초대 세부사항:
invite-campaign.text4=캠페인 이름: <strong>{0}</strong>
invite-campaign.text5=캠페인 코드: <strong>{0}</strong>
invite-campaign.text6=앱내에서 캠페인 코드를 입력하거나, 아래 버튼을 클릭하여 캠페인에 참여하세요.
invite-campaign.text7=초대 수락
invite-campaign.text8=초대를 수락하면, 캠페인에 대한 자세한 정보와 다음 단계에 대해 안내해 드리겠습니다.
invite-campaign.text9=궁금한 점이 있거나 도움이 필요하시면 언제든지 <a href="mailto:jamsuhamHI@gmail.com" style="color: #1a73e8; text-decoration: none; font-weight: bold;">jamsuhamHI@gmail.com</a>로 문의해 주세요.
invite-campaign.text10=감사합니다.<br>keytrip 팀 드림

 

invite-campaign=Invited to {0}! 🎉
invite-campaign.text1=<strong>{0}</strong> invited you to the <strong>{1}</strong> campaign! 🎉
invite-campaign.text2=Don't miss the opportunity to have fun together and share beneficial experiences.
invite-campaign.text3=Invitation Details:
invite-campaign.text4=Campaign Name: <strong>{0}</strong>
invite-campaign.text5=Campaign Code: <strong>{0}</strong>
invite-campaign.text6=Enter your campaign code within the app, or click the button below to participate in the campaign
invite-campaign.text7=Accept invitation
invite-campaign.text8=Once we accept the invitation, we will provide you with more information about the campaign and next steps.
invite-campaign.text9=If you have any questions or need help, please feel free to contact <a href="mailto:jamsuhamHI@gmail.com" style="color: #1a73e8; text-decoration: none; font-weight: bold;">jamsuhamHI@gmail.com</a>.
invite-campaign.text10=Thank you.<br>from Keytrip team

 

invite-campaign 은 이메일 제목을 의미하며 java 로직에서 사용할 것이고, text1~10까지 html 내에 들어갈 것이다. 코드를 보면 알겠지만 {0}, {1}과 같이 동적으로 값을 넣을 수 있을 뿐만 아니라 html 태그를 넣어서 진행할 수 있다. 이에 따라 html 파일에서 적용하는 방식이 달라지는데, 아래의 html 파일을 확인해 보자.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div
    style="font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f9f9f9; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #dddddd;">
  <div style="text-align: center; padding: 20px 0;">
    <img src='cid:image1' alt="keytrip logo" style="max-width: 100%; height: auto;">
  </div>
  <p style="text-align: center; font-size: 18px; color: #333333; margin: 0; line-height: 1.6;">
    <span th:utext="#{invite-campaign.text1(${leaderName}, ${campaignName})}"></span><br>
    <span th:text="#{invite-campaign.text2}"></span>
  </p>
  <div
      style="text-align: center; margin: 20px 0; padding: 20px; background-color: #ffffff; border-radius: 8px;">
    <h3 style="font-size: 16px; color: #0012ce; margin: 0 0 10px 0;"><span th:text="#{invite-campaign.text3}"></span></h3>
    <p style="font-size: 16px; color: #333333; margin: 0; line-height: 1.6;">
      <span th:utext="#{invite-campaign.text4(${campaignName})}"></span><br>
      <span th:utext="#{invite-campaign.text5(${campaignCode})}"></span>
    </p>
    <p style="font-size: 14px; color: #555555; margin: 20px 0 0 0;">
      <span th:text="#{invite-campaign.text6}"></span>
    </p>
    <div style="margin-top: 20px;">
      <a href="#"
         style="display: inline-block; padding: 12px 24px; width: 220px; font-size: 16px; color: #ffffff; background-color: #0012ce; text-decoration: none; border-radius: 4px;">
        <span th:text="#{invite-campaign.text7}"></span>
      </a>
    </div>
  </div>
  <p style="text-align: center; font-size: 14px; color: #555555; line-height: 1.6; margin: 20px 0;">
    <span th:text="#{invite-campaign.text8}"></span><br>
  <p style="text-align: center; color: #555555; line-height: 1.6; margin: 20px 0;">
  <span th:utext="#{invite-campaign.text9}"></span>
  </p>
  </p>
  <p style="font-size: 14px; color: #555555; text-align: center; line-height: 1.6;">
    <span th:utext="#{invite-campaign.text10}"></span>
  </p>
</div>
</body>
</html>

 

html 파일 내 텍스트를 직접 작성하지 않았고, properties 내 변수들을 활용하였다. span 태그에 th:text, th:utext 두 속성을 추가하였는데, 각각 thymeleaf에서 사용하는 태그로 th:text는 일반적인 텍스트, th:utext는 변수 내 html 태그가 포함되어 있을 경우에 사용한다. 

th:utext는 <br> 태그와 같이 개행도 포함할 수 있기에, 단순히 한 문장만이 아니라 여러 문장도 하나의 변수에 저장할 수 있다.

이렇게 html과 properties 파일 작성을 완료하였다. 이제 실제 비즈니스 로직에서 언어를 어떻게 지정하는지 확인해 보자.

 

 

2. java 코드 작성

 

본격적으로 이메일 전송 로직을 살펴보겠다. 아래는 간단한 이메일 전송 로직이며, 다국어 적용 관련한 코드만 설명하고 넘어가겠다. 주로 살펴볼 부분은 주석으로 '다국어 적용(n)'를 남겨두었다.

(코드에 대한 상세한 설명은 https://khdscor.tistory.com/131를 참고하길 바란다.)

private MessageSource messageSource;
private static JavaMailSender javaMailSender;
private static SpringTemplateEngine templateEngine;
...

private void sendMultiEmailWithImages(
    List<String> receivers,
    Map<String, String> emailContent, 
    List<String> imagePaths) {
    try {
        // 이메일 전송을 위한 MimeMessageHelper 객체 생성
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        // true는 multipart 파일이 있는지 없는지를 나타내며, 이미지 파일이 있기 때문에 true로 지정
        MimeMessageHelper msgHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
        
        // 다국어 적용 (1)
        Locale locale = LocaleContextHolder.getLocale();
        
        // 다국어 적용 (2)
        Context context = new Context(locale);
        
        // 템플릿에 매핑된 값을 설정
        emailContent.forEach(context::setVariable);
        // 템플릿을 처리하여 이메일 본문 생성
        String emailBody = templateEngine.process("invite-campaign", context);
        // 메일 제목, 본문, 이메일 주소, 이미지 파일 지정
        
        // 다국어 적용 (3)
        msgHelper.setSubject(messageSource.getMessage("invite-campaign.title", new Object[]{"캠페인 이름"}, locale));
        
        msgHelper.setText(emailBody, true);
        msgHelper.setTo(receivers.toArray(new String[receivers.size()]));
        // 이미지 리스트를 반복하고 각 이미지에 대해 addInline 메소드를 호출
        for (int i = 0; i < imagePaths.size(); i++) {
            String imagePath = imagePaths.get(i);
            String contentId = "image" + (i + 1);
            msgHelper.addInline(contentId, new ClassPathResource(imagePath));
        }
        // 이메일 전송
        javaMailSender.send(msgHelper.getMimeMessage());
    } catch (MessagingException e) {
        throw new RuntimeException("에러 발생");
    }
}

 

다국어를 적용하기 위해 추가 및 수정할 부분은 3가지이다. 

첫 번째로, Locale 값을 가져와야 한다. LocaleContextHolder.getLocale()를 통해 현재 스레드에 설정된 언어에 해당하는 Locale를 가져오자.(Locale에 대해 궁금한 내용은 MessageSource에 대해 작성한 이전 글을 참고하길 바란다. )

// 다국어 적용 (1)
Locale locale = LocaleContextHolder.getLocale();

 

두 번째로, 이메일 본문을 생성하기 위한 Context를 선언 시 가져온 locale를 매개변수로서 넣어준다. 이메일 본문을 만들 때,  따로 설정하는 것 없이 invite-campaign.html 파일 내에 locale에 해당하는 properties에 작성된 변수가 자동으로 들어간다.

// 다국어 적용 (2)
Context context = new Context(locale);
String emailBody = templateEngine.process("invite-campaign", context);

 

마지막으로 이메일 제목을 넣어줄 때, messageSource.getMessage()를 활용하여 locale에 해당하는 title 값을 넣어준다.

// 다국어 적용 (3)
msgHelper.setSubject(messageSource.getMessage("invite-campaign.title", new Object[]{"캠페인 이름"}, locale));

 

정말 간단하지 않은가?

분기 로직을 직접 구현하는 것보다 messageSource를 활용하여 언어별 properties를 작성하는 것이 가독성이 더욱 향상된 것을 알 수 있고, html 파일을 하나만 두고 언어별 properties 파일을 생성한 점에서 수정이 쉽다는 장점이 있다. 디만 관리해야 할 properties 파일이 늘어난다는 단점도 있기에 프로젝트 팀원들의 의사에 맞게 선택하여 진행하면 될 것이다. 

 

아래는 실제 언어별 전송된 메시지 이미지이다.

한국어

 

영어

 

일본어

 

마지막으로 LocaleContextHorlder 사용 시 주의 사항에 대해 언급하겠다.

바로 LocaleContextHolder의 setLocale(), getLocale()는 동일한 스레드에서만 같은 인식을 한다는 점이다. 

실제로 프로젝트를 진행 중 겪은 사항으로 @Async를 사용하는 특정 메서드를 구현한 적이 있다. 하지만 해당 어노테이션을 지정한 메서드에서 getLocale()를 호출하였지만 setLocale를 통해 지정했던 locale가 호출되지 않았다. 그 이유는 @Async를 사용한 메서드는 이전과 다른 스레드로 실행되었기 때문이다. 이는 메서드를 호출할 때, locale을 매개변수 값으로 건네주는 방법을 통해 해결할 수 있다. 이처럼 @Async와 같이 다른 스레드로 실행될 때는 locale 사용에 주의해야 한다.

 

 

결론

 

이렇게 간단하게 thymeleaf html을 다국어별로 작성해 보았다. 확실히 html 파일을 3개 만드는 것보다 가독성 있고 텍스트별로 구분하니 관리하기가 쉬워진다는 장점이 있다. 

text1~12로 명칭을 한 것은 좋은 것이 아닌 것 같지만, 적당한 명칭 구분이 힘들어 이렇게 지정하였다. 다른 더 좋은 방식이 있는지 다음에 한 번 찾아봐야겠다.

 

잘못된 내용이 있거나, 궁금한 것은 댓글로 질문하여 주시면 감사합니다!