여기에 메일 관련된 기능을 추가해보자.
일단 유저 정보에 이메일을 추가로 저장해야 할 것이다.
일단 유저 클래스와 유저 Dao 클래스를 수정해보자.
@Builder
@Getter
@Setter
@EqualsAndHashCode
public class User {
private Level level;
private Integer login;
private Integer recommend;
private String id;
private String name;
private String password;
private String email;
public void upgradeLevel() {
this.setLevel(Level.from(this.getLevel().getValue() + 1));
}
public boolean isUpgradeLevel() {
Level level = this.level;
return switch (level) {
case BRONZE -> this.login >= SILVER_LOGIN_COUNT;
case SILVER -> this.recommend >= GOLD_RECOMMEND_COUNT;
case GOLD -> false;
};
}
}
public class UserDaoImpl implements UserDao {
private final JdbcTemplate jdbcTemplate;
private final RowMapper<User> userRowMapper = (rs, rowNum) -> User.builder()
.id(rs.getString("id"))
.name(rs.getString("name"))
.password(rs.getString("password"))
.login(rs.getInt("login"))
.level(Level.from(rs.getInt("level")))
.recommend(rs.getInt("recommend"))
.email(rs.getString("email"))
.build();
public void update(User user) {
this.jdbcTemplate.update(
"update users set name = ?, password = ?, level = ?, login = ?, recommend = ?, email = ? " +
"where id = ?",
user.getName(),
user.getPassword(),
user.getLevel().getValue(),
user.getLogin(),
user.getRecommend(),
user.getEmail(),
user.getId()
);
}
public UserDaoImpl(
DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void add(User user){
this.jdbcTemplate.update(
"insert into users(id, name, password, level, login, recommend, email) values(?, ?, ?, ?, ?, ?, ?)",
user.getId(),
user.getName(),
user.getPassword(),
user.getLevel().getValue(),
user.getLogin(),
user.getRecommend(),
user.getEmail()
);
}
public void deleteAll(){
this.jdbcTemplate.update("delete from users");
}
public User get(String id){
return jdbcTemplate.queryForObject(
"select * from users where id = ?", userRowMapper, id);
}
public int getCount() {
Integer result = jdbcTemplate.queryForObject("select count(*) from users", Integer.class);
return result == null ? 0 : result;
}
public List<User> getAll() {
return this.jdbcTemplate.query(
"SELECT * FROM users ORDER BY id", userRowMapper);
}
}
일단 수정하고 테스트를 실행해보니 성공한다.
JavaMail을 이용한 메일 발송 기능
이제 메일 관련 코드를 추가해보자.
메일을 발송한다는 임무를 가지는 클래스를 하나 더 만들었다.
public class MailSender {
public void sendUpgradeEmail(User user){
Properties properties = new Properties();
properties.put("mail.smtp.host", "mail.seungkyu.com");
Session session = Session.getInstance(properties);
MimeMessage mimeMessage = new MimeMessage(session);
try
{
mimeMessage.setFrom(new InternetAddress("seungkyu@seungkyu.com"));
mimeMessage.addRecipients(Message.RecipientType.TO, InternetAddress.parse(user.getEmail()));
mimeMessage.setSubject("승급 안내");
mimeMessage.setText(user.getLevel() + "로 승급하셨습니다.");
Transport.send(mimeMessage);
}
catch (MessagingException e)
{
throw new RuntimeException(e);
}
}
}
그리고는 upgradeLevel() 메서드에
private void upgradeLevel(User user) {
user.upgradeLevel();
userDao.update(user);
mailSender.sendUpgradeEmail(user);
}
메일을 전송하는 코드를 추가해준다.
JavaMail이 포함된 코드의 테스트
근데 여기서 테스트를 어떻게 수행할까?
일단 테스트를 위해서는 서버가 준비되어 있어야 한다.(지금 SMTP 서버가 없기는 하다...)
그리고 테스트를 할 때마다 메일이 발송되는 것도 웃기지 않은가?
사실 메일 서비스는 테스트를 할 필요가 없다.
javaMail은 스프링에서 검증된 API이며
사실 서버로 보내진 경우에 메일이 보내지지 않는 것은, 여기서 테스트한다고 바뀌지 않는다.
해당 서버의 문제까지 테스트를 할 필요는 없지 않은가.
그렇기에 테스트 단계에서는 실제로는 메일이 전송되지 않는 다른 클래스를 사용해도 괜찮다.
테스트를 위한 서비스 추상화
지금은 메일 전송을 다음과 같이 사용하고 있다.
public class UserServiceImpl implements UserService{
private final UserDao userDao;
private final PlatformTransactionManager transactionManager;
private final MailSender mailSender;
}
늘 그렇듯 이러한 부분은 추상화 인터페이스로 만들고 거기에 의존해야 후에 교체가 가능하다.
public interface MailSender {
void sendUpgradeEmail(User user);
void sendUpgradeEmail(User[] users);
}
그렇기에 MailSender를 인터페이스로 만들고, MailSenderImpl로 구현해서 빈에 등록했다.
테스트는 굳이 메일이 전송되지 않기를 바란다
그렇기에 그냥 로그만 작성하도록 클래스를 구현해서 상속받았다.
public class MailSenderTest implements MailSender {
public void sendUpgradeEmail(@NonNull User user) {
System.out.println("sendUpgradeEmail 호출");
System.out.println(user);
}
public void sendUpgradeEmail(@NonNull User[] users) {
System.out.println("sendUpgradeEmail 호출");
for (User user : users) System.out.println(user);
}
}
이렇게 하고 테스트를 실행해보니, 이렇게 성공적으로 로그가 남는 것을 볼 수 있었다.
현재의 MailSender는 다음과 같이 이용하고 있다.
MailSenderImpl은 JavaMail을 이용하고 있으며, 만약 다른 API를 사용해야 하는 경우 MailSender를 또 구현해서 등록하면 된다.
이제 여기서 안타까운 부분이 있다.
만약 1~5번 유저를 업그레이드 하던 중, 3번 유저에게 메일만 전송하고 업그레이드가 되지 않았다면?
이런 부분에서 무결성에 대한 문제가 나올 수 있다.
이런 문제에 관해서는 2가지 방법이 있다고 한다.
- 업그레이드 사용자를 별도의 목록에 저장해두고, 업그레이드가 끝나면 메일을 일괄 전송
- MailSender 자체를 확장해서 트랜잭션을 적용
일단 이 방법 중 가능한 방법을 사용하면 될 거 같고, 이런 JavaMail조차 추상화하면 큰 장점이 있다.
그렇기에 적극적으로 추상화의 방법을 사용해보도록 하자.
테스트 대역
테스트에서는 메일이 전송되지 않고, 단순하게 로그만 남도록 했다.
스프링의 DI를 통해 테스트에서는 다른 빈을 주입받도록 했기에 가능한 일이었다.
이렇게 테스트용으로 사용되는 클래스들이 존재한다.
이렇게 UserService와 같이 테스트의 대상이 의존하는 클래스들이다.
이렇게 빠르게 간편하게 테스트를 도와주도록 주입해주는 클래스들을 테스트 대역이라고 부른다.
현재 우리의 MailSenderTest는 모든 메서드가 void지만, 가끔은 테스트 대역들이 값을 리턴해야 하는 경우가 있다.
그럴 때는 대역들에게 테스트에 필요한 값들을 리턴하도록 만들 수 있다.
필요하다면 대역들이 입력받는 값도 검증해 볼 수 있다.
목 오브젝트라고 하며 테스트가 정상적으로 실행되도록 도와주면서, 테스트의 결과를 검증하는데 사용할 수도 있다.
사실 mockito를 사용하면 바로 끝나지만, 그래도 MailSenderTest를 수정해서 목 오브젝트 느낌으로 만들어보자.
@Getter
public class MailSenderTest implements MailSender {
private final List<String> history = new ArrayList<>();
public void sendUpgradeEmail(@NonNull User user) {
history.add(user.getEmail());
}
public void sendUpgradeEmail(@NonNull User[] users) {
System.out.println("sendUpgradeEmail 호출");
for (User user : users) System.out.println(user);
}
}
이렇게 메일을 전송할 때마다, 일단 내부 리스트에서 저장을 해두었다.
그리고 메일을 모두 전송한 후, history와 맞는지 체크해보았다.
@Getter
public class MailSenderTest implements MailSender {
private final List<String> history = new ArrayList<>();
public void sendUpgradeEmail(@NonNull User user) {
history.add(user.getEmail());
}
public void sendUpgradeEmail(@NonNull User[] users) {
System.out.println("sendUpgradeEmail 호출");
for (User user : users) System.out.println(user);
}
}
@Test
public void upgradeLevels() {
userDao.deleteAll();
for(User user : users) userDao.add(user);
try{
userService.upgradeLevels();
}
catch (Exception ignored){
}
if(userService instanceof UserServiceImpl userServiceImpl) {
if (userServiceImpl.getMailSender() instanceof MailSenderTest mailSenderTest) {
Assertions.assertEquals(users.get(1).getEmail(), mailSenderTest.getHistory().get(0));
Assertions.assertEquals(users.get(3).getEmail(), mailSenderTest.getHistory().get(1));
}
else assert false;
Assertions.assertEquals(Level.BRONZE, userDao.get(users.get(0).getId()).getLevel());
Assertions.assertEquals(Level.SILVER, userDao.get(users.get(1).getId()).getLevel());
Assertions.assertEquals(Level.SILVER, userDao.get(users.get(2).getId()).getLevel());
Assertions.assertEquals(Level.GOLD, userDao.get(users.get(3).getId()).getLevel());
Assertions.assertEquals(Level.GOLD, userDao.get(users.get(4).getId()).getLevel());
}
else assert false;
}
계속 타입을 바꾸면서 했지만, mockito를 사용하면 이런 복잡한 과정은 필요없긴 하다.
이렇게 테스트만을 위해 존재하는 객체들이 있다.
빠르게 테스트를 위해 대충 사용해보았는데, mockito에 대해서 나중에 더 자세히 알아보도록 하자.
'백엔드 > 토비의 스프링' 카테고리의 다른 글
[토비의 스프링] 5.3 서비스 추상화와 단일 책임 원칙 (0) | 2025.06.09 |
---|---|
[토비의 스프링] 5.2 트랜잭션 서비스 추상화 (2) | 2025.06.09 |
[토비의 스프링] 5.1 사용자 레벨 관리 기능 추가 (0) | 2025.06.07 |
[토비의 스프링] 4.2 예외 전환 (1) | 2025.06.04 |
[토비의 스프링] 4.1 사라진 SQLException (1) | 2025.06.03 |