barded

[어디가게] 컨트롤러에서 여러 서비스를 불러야 하는경우 본문

프로젝트/어디가게

[어디가게] 컨트롤러에서 여러 서비스를 불러야 하는경우

barded 2024. 3. 18. 04:20

유저의 회원가입을 진행하는 경우 SMTP를 사용하여 이메일 인증을 받도록 처리하였고 이를 EmailService라고 명명하여 서비스로 빼놓았다.

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class EmailService {

    private final JavaMailSender javaMailSender;
    private final JwtProvider jwtProvider;

    public MimeMessage createAuthorizationEmail(String to, String token) {
        MimeMessage message = javaMailSender.createMimeMessage();
        try {
            message.addRecipients(MimeMessage.RecipientType.TO, to);
            message.setSubject("where2go 이메일 인증");
            String msg = "";
            String url = "<http://localhost:8080/customer/email-verification/>";
            msg += "<h1 style=\\"font-size: 30px; padding-right: 30px; padding-left: 30px;\\">이메일 주소 확인</h1>";
            msg += "<p style=\\"font-size: 17px; padding-right: 30px; padding-left: 30px;\\">아래 링크를 눌러 확인..</p>";
            msg += "<div style=\\"padding-right: 30px; padding-left: 30px; margin: 32px 0 40px;\\"><table style=\\"border-collapse: collapse; border: 0; background-color: #F4F4F4; height: 70px; table-layout: fixed; word-wrap: break-word; border-radius: 6px;\\"><tbody><tr><td style=\\"text-align: center; vertical-align: middle; font-size: 30px;\\">";
            msg += "<a href=\\"" + url + token + "\\">where2go 인증하기 </a>";
            msg += "</td></tr></tbody></table></div>";

            message.setText(msg, "utf-8", "html");
        } catch (MessagingException e) {
            throw new BaseException(ExceptionCode.EMAIL_SERVER_ERROR);
        }

        return message;
    }

    public void sendAuthorizationEmail(String to) {
        MimeMessage message = createAuthorizationEmail(to, jwtProvider.generateAccessTokenByEmail(to));
        try {
            javaMailSender.send(message);
        } catch (MailException es) {
            throw new BaseException(ExceptionCode.INVALID_EMAIL);
        }
    }

}

이후 EmailService를 어디에서 사용할지 고민을 했었는데 방식은 두가지가 있었다.

  1. CustomerService 안에서 EmailService 호출
  2. CustomerController에서 CustomerService호출 후 EmailService 호출

CustomerService 안에서 EmailService 호출

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CustomerService {

    private final CustomerRepository customerRepository;
		private final EmailService emailService;
    private final RedisUtil redisUtil;

    @Transactional
    public void signup(CustomerDto.SignupRequest customerSignupRequestDto) {

        if (customerRepository.findByEmail(customerSignupRequestDto.getEmail()).isPresent()) {
            throw new BaseException(ExceptionCode.DUPLICATED_USER);
        }

        Customer customer = Customer.builder()
            .email(customerSignupRequestDto.getEmail())
            .password(customerSignupRequestDto.getPassword())
            .name(customerSignupRequestDto.getName())
            .phoneNumber(customerSignupRequestDto.getPhoneNumber())
            .nickname(customerSignupRequestDto.getNickname())
            .build();

        customer.hashPassword(passwordEncoder);
        
        emailService.sendAuthorizationEmail(customer.getEmail());

        customerRepository.save(customer);
    }
}

위처럼 서비스안에서 다른 서비스를 호출하는 경우

  1. CustomerService의 책임이 불분명하다라고 생각하였다.
  2. 다른 경우에 ASerivce에서 BService를 참조하고 BService에서 ASerivce를 참조하는 순환참조가 발생할 수 있다고 생각하였다.

CustomerController에서 CustomerService호출 후 EmailService 호출

@RestController
@RequestMapping("/customer")
@RequiredArgsConstructor
public class CustomerController {

    private final CustomerService customerService;
    private final EmailService emailService;

    @PostMapping("/signup")
    public ResponseEntity<BaseResponse<String>> signup(@RequestBody CustomerDto.SignupRequest signupRequestDto) {
        customerService.signup(signupRequestDto);
        emailService.sendAuthorizationEmail(signupRequestDto.getEmail());
        return ResponseEntity
            .status(HttpStatus.OK)
            .body(new BaseResponse<>(HttpStatus.OK.value(), "회원가입이 완료되었습니다.", null));
    }
}

위처럼 컨트롤러에서 여러 서비스를 호출하는 경우

여러 서비스를 하나의 트랜잭션으로 묶을 수 없다는 매우 큰 단점이 있었다.

그러면 어떠한 구조를 선택해야할까?

고민하던중 최선의 방법은 Service Layer을 구분하여 Service간 의존하는 것이라고 생각되었다.

Service사이에서도 Layer을 구분하여 하나의 의존성 방향을 정해서 사용하는 것이다.

위의 그림에서 ModuleService는 Repository만을 의존하며, 하나의 기능만을 담당한다.

ComponentSerivce는 ModuleService만을 의존하며 ModuleService의 조합으로 복합 기능을 담당한다.

이를 통해 ComponentService에 트랜잭션을 적용하여 여러 기능을 하나의 트랜잭션 단위에서 동작 가능하도록 하였고, ModuleService는 독립적으로 사용 가능하기 때문에, 필요에 따라 재사용이 가능하다.

@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class CustomerSignUpService{

    private final CustomerService customerService;
		private final EmailService emailService;
		
		@Transactional
		public void signup(CustomerDto.SignupRequest signupRequestDto) {
        customerService.signup(signupRequestDto);
        emailService.sendAuthorizationEmail(signupRequestDto.getEmail());
    }
}

위를 통해 여러 방법의 장단점을 확인해 보았다.

하지만 정답은 없다!

단순한 조회의 경우 첫번째 방법이 좋을 수도 있고, 간단한 규모의 프로젝트 경우 두번째, 모든것을 가져가고 싶다면 세번째 방법이 제일 나을것 같다.

따라서 자기만의 혹은, 팀의 기준을 하나로 정하고 개발을 해나아 간다면 큰 문제가 없다고 생각한다.