Service - 이메일 보내기 가짜 기능 구현

jihan kong·2022년 9월 14일
0

JUnit5

목록 보기
14/25
post-thumbnail

본 시리즈는 메타 코딩님의 Junit 강의를 학습한 내용을 바탕으로 정리하였습니다.

Service layer 의 끝을 향해 나아가고 있다. 책을 등록, 조회, 삭제, 수정까지 왠만한 기능은 갖추었지만 뭔가 하나가 아쉽다. 사용자 편의적인 기능을 하나 추가하면 좋을 것 같다.

이메일 보내기

A라는 사용자가 책을 등록하게 되면 책이 등록되었다는 메일을 하나 전송해주면 어떨까?

  1. 책 등록
  2. 메일전송 리턴 값이
  • True면 3번으로
  • False면 Rollback
  1. 종료

이와 같은 방식으로 동작하는 이메일 기능을 구현해볼 수 있다.

그런데, 우리는 현재 책을 CRUD 하는 기능을 모두 구현했고 이제 이것을 테스트해야한다. 이메일 기능은 추가적인 기능에 불과하지만 CRUD는 필수적인 기능이다. 따라서 Unit test가 우선적이다.


진행이 안되는 상황...?

아직 구현되지 않은 기능 하나 때문에 우리의 테스트가 진행이 안될 수는 없는 노릇이다.

현업에서 같은 상황이라고 한다면 다른 개발자가 이메일 기능을 개발하고 있을 것이다.😄 우리는 일종의 Mock-up인 가짜 기능을 가진 메소드를 만들고 우리의 테스트를 진행하자.


어떻게 구현할까?

1. MailSender 라는 인터페이스를 생성한다.
2. 가짜 메일 이라는 구현체를 생성한다.
3. 메일 어댑터 를 만든다. 메일 어댑터는 아직 생성(구현)되지 않은 미래의 Mail 이 완성되면 그때 연결할 준비를 하고 있다.

따라서 우리의 ServiceDIP 에 따라 추상화된 인터페이스인 MailSender 에 의존하게 된다.


이메일 보내기 가짜 기능 구현

1. Mail & MailSender

먼저 다음과 같이 util 이라는 폴더를 만들고, Mail.java 파일을 생성한다.

Mail.java

package site.metacoding.junitproject.util;

// 아직 구현되지 않음
// public class Mail {
//    public boolean sendMail() {
       // 기능
//     return true;
//    }   
// }

Mail 구현체는 우리가 만드는 것이 아니다. 그러나, 일단 껍데기만 만들어만 놓자. Mail 은 아직 생성되지 않은 상태이다. 다음으로 util 폴더에 MailSender.java 를 생성한다.

MailSender.java

package site.metacoding.junitproject.util;

public interface MailSender {
    boolean send();
}

위에서 언급한대로 MailSender 는 인터페이스로 생성한다. 마지막으로 MailSenderAdapterMailSenderStub 을 생성한다.


2. MailSenderStub

MailSenderStub

package site.metacoding.junitproject.util;

import org.springframework.stereotype.Component;

// 가짜!
@Component  // IoC 컨테이너 등록
public class MailSenderStub implements MailSender {
    
    @Override
    public boolean send() {
        return true;
    }
}

stub 이란 ❓
stub이란 토막, 꽁초, 남은부분, 몽당연필이라는 뜻으로
dummy 객체가 마치 실제로 동작하는 것처럼 보이도록 만들어놓은 것입니다.
(출처 : https://beomseok95.tistory.com/294)

위의 그림대로 MailSenderStubMailSender 를 implements 하고 있는 것을 볼 수 있다.
MailSenderStub 은 쉽게 표현해서 자기가 MailSender 인양 행세를 하고 있는 것이다. 즉, 가짜이고 나중에 Mail 클래스가 완성이 되면 지워버리기만 하면 된다.


3. MailSenderAdapter

MailSenderAdapter

package site.metacoding.junitproject.util;

// 추후에 Mail 클래스가 완성되면 코드를 완성하면 됨.
public class MailSenderAdapter implements MailSender {
    
    // private Mail mail;

    // public MailSenderAdapter() {
    //     this.mail = new Mail();
    // }

    @Override
    public boolean send() {
        return true;
    }
}

MailSenderAdapter 또한 인터페이스인 MailSender 를 implements 하고 있다. MailSenderAdapter 의 경우, 현재 Mail 이 구현되지 않은 상태이기 때문에 Mail 속성을 사용할 수 없는 상태이다. 따라서 구현만 된다면 override 해서 사용할 수 있다.

자, 모든 Mail 관련 클래스들이 일단은 정의되었다. 그럼 이제 서비스에서 이를 어떻게 반영시켜야할까?


BookService 에서의 반영

BookService.java

package site.metacoding.junitproject.service;

import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.transaction.Transactional;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import site.metacoding.junitproject.domain.Book;
import site.metacoding.junitproject.domain.BookRepository;
import site.metacoding.junitproject.util.MailSender;
import site.metacoding.junitproject.web.dto.BookRespDto;
import site.metacoding.junitproject.web.dto.BookSaveReqDto;

@RequiredArgsConstructor
@Service
public class BookService {

    private final BookRepository bookRepository;
    private final MailSender mailSender;			                  //  1. 
    
    // 책 등록
    @Transactional(rollbackOn = RuntimeException.class)
    public BookRespDto 책등록하기(BookSaveReqDto dto) {
        Book bookPS = bookRepository.save(dto.toEntity());
        if (bookPS != null) {						                  // 2. 
            if (!mailSender.send()) {
                throw new RuntimeException("메일이 전송되지 않았습니다"); // 3.
            }
        }
        return new BookRespDto().toDto(bookPS);
    }

<코드 설명>
1. 전에 MailSenderStub 을 통해 IoC 컨테이너에 등록된 MailSender 를 가져온다.
2. bookPS 값이 Null 값이 아니라면 mailSender.send()
3. True가 리턴되지 않으면 예외처리를 통해 rollback


Mail 이 구현되고 난 후 (가정)

이제 우리는 다음을 가정해보자.

  1. Mail 이 구현됨. (주석처리한 부분을 구현)
  2. MailSenderAdapter 또한 다음과 같이 구현됨

MailSenderAdapter

package site.metacoding.junitproject.util;

@Component		// IoC 컨테이너 등록
public class MailSenderAdapter implements MailSender {
    
    private Mail mail;

    public MailSenderAdapter() {
         this.mail = new Mail();
     }

    @Override
    public boolean send() {
        return mail.sendMail();   // 외부 라이브러리인 mail.sendMail이 호출됨
    }
}

이렇게 된다면 MailSenderAdapter 가 IoC 컨테이너에 등록되는데 지금 컨테이너에는 MailSenderAdapter, MailSenderStub 이 두 개가
등록이 되어있는 상황이다.

그런데 둘의 타입이 모두 MailSender 이다.

📌 Singleton 관리

여기서 한 가지 이슈가 발생한다. 같은 타입이 두 개가 중복되어있기 때문이다. 스프링에서 IoC 컨테이너는 싱글톤으로 관리되어야만 한다. 즉, 클래스의 인스턴스가 딱 1개만 생성되어야 하는 것이다. 따라서 Adapter 만을 등록하고 StubComponent 어노테이션을 주석처리하거나 클래스를 아예 삭제하는 방식으로 싱글톤을 유지시켜야한다.

❓싱글톤(Singleton) 패턴의 특징과 장단점
https://tecoble.techcourse.co.kr/post/2020-11-07-singleton/

이제 우리는 BookService 를 건들 필요없이 MailSenderAdapter 만 관리하면 된다.

profile
학습하며 도전하는 것을 즐기는 개발자

0개의 댓글