본문 바로가기
Project/Trouble Shooting

DIP를 적용하여 리팩토링 해보기

by yoon_seon 2024. 6. 21.

프로젝트 진행하면서 DIP를 적용하여 리팩토링을 한 이야기를 해보려고 합니다.

 

이야기에 앞서, DIP의 정의는 아래와 같습니다.

 

의존 역전 원칙(Dependency Inversion Principle, DIP)

DIP는 객체지향 설계 원칙인 SOLID 원칙 중 하나로, 고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야 한다는 원칙입니다.

DIP의 핵심은 구체적인 구현보다 추상화된 인터페이스나 추상 클래스에 의존하도록 설계해야 한다는 것인데요.

DIP를 적용한다면 객체가 구체적인 구현에 의존하지 않고 인터페이스나 추상화에 의존하여 구현의 변경이나 교체를 간편할 수 있기에 전체적인 시스템의 유연성을 높이고, 변경에 더 잘 적응할 수 있어 코드의 의존성 관리를 개선하고, 변화에 유연하게 대응할 수 있는 소프트웨어 구조를 구축할 수 있습니다.

 

 

그러면 DIP가 적용되지않은 문제의 코드를 살펴보겠습니다.

 

진행중인 프로젝트는 계층형 아키텍처를 적용하고 있기에 표현계층(Presentation), 응용계층(Application), 도메인계층(Domain), 구현계층(Infrastructure)으로 이루어져 있습니다. 각 계층은 상위 계층으로 단방향 의존 합니다.

 

문제점 확인하기

문제의 구현 코드

Application 계층의 회원가입을 담당하는 MemberSignupService 클래스 입니다.

package io.study.springbootlayered.api.member.application;

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

    private final MemberProcessor memberProcessor;
    private final JavaMailSender mailSender;

    @Transactional
    public MemberSignupDto.Info signup(final MemberSignupDto.Command request) {
        /** 회원 가입 **/
        MemberSignupDto.Info info = memberProcessor.register(request);

        /** 회원가입 완료 후 이메일 전송 **/
        String registeredEmail = info.getEmail();
        sendMail(toEmailArray(registeredEmail), "회원가입 완료 안내", "회원가입이 완료되었습니다.");

        return info;
    }

    private String[] toEmailArray(String... email) {
        return email;
    }

    private void sendMail(String[] toEmailArray, String title, String content) {
        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");

            messageHelper.setTo(toEmailArray);
            messageHelper.setSubject(title);
            messageHelper.setFrom(new InternetAddress("noreply.yoonseon3@gmail.com", "이윤선", "UTF-8"));
            messageHelper.setText(content, true);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

        mailSender.send(message);
    }
}

 

signup메서드는 memberProcessor.register메서드를 호출하여 회원을 등록하고,

회원가입한 이메일을 추출하여 sendMail메서드를 통해 회원에게 회원가입 완료 메일을 전송합니다.

 

문제 상황

MemberSignupServiceJavaMailSender에 직접적으로 의존하고 있어 크게 2가지의 문제점이 발생합니다.

  1. 만약, 메일 전송하는 방식을 JavaMailSender가 아닌 다른 방식으로 변경해야 할 경우 MemberSignupService 클래스를 직접 수정해야 한다.
  2. MemberSignupService에 메일 전송 기능 까지 구현되어 있어서 회원가입 비즈니스 로직에 집중되어 있지 않다.

 

이 문제를 위에서 언급한 DIP를 통해 해결해보도록 하겠습니다.

 

 

DIP 적용하기

1. 인터페이스 추출

Domain 계층에 JavaMailSender를 구현할 인터페이스를 생성하고 추상메서드를 정의합니다.

package io.study.springbootlayered.api.member.domain.mail;

public interface MailService {
    
    void sendMail(String[] toEmailArray, String title, String content);
    
}

 

2. 인터페이스를 구현한 구현 클래스 생성

infrastucture 계층에  MailService 인터페이스를 implements한 구현 클래스를 생성하고 추상 메서드를 Override합니다.

package io.study.springbootlayered.api.member.infrastructure;

@Component
@RequiredArgsConstructor
public class JavaMailService implements MailService {

    private final JavaMailSender javaMailSender;

    @Override
    public void sendMail(String[] to, String title, String content) {

    }
}

 

3. MemberSignupService 클래스의 sendMail 메서드를 구현 클래스로 이동

리팩토링 전 문제가 되었던 MemberSignupService 의  sendMail 메서드를 새로 생성한 JavaMailService의 sendMail메서드로 이동해줍니다.

package io.study.springbootlayered.api.member.infrastructure;

@Component
@RequiredArgsConstructor
public class JavaMailService implements MailService {

    private final JavaMailSender mailSender;

    @Override
    public void sendMail(String[] toEmailArray, String title, String content) {
        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper messageHelper = new MimeMessageHelper(message, true, "UTF-8");

            messageHelper.setTo(toEmailArray);
            messageHelper.setSubject(title);
            messageHelper.setFrom(new InternetAddress("noreply.yoonseon3@gmail.com", "이윤선", "UTF-8"));
            messageHelper.setText(content, true);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }

        mailSender.send(message);
    }
}

 

4. MemberSignupService 클래스 JavaMailSender 의존성 제거

이제 MemberSignupService 클래스에서 문제가 되었던 JavaMailSender 를 제거하고 위에서 생성한 인터페이스를 의존하도록 변경하고 JavaMailSender 를 직접 사용하던 sendMail메서드가 아닌 새로 생성한 인터페이스의 sendMail를 호출하도록 변경합니다.

package io.study.springbootlayered.api.member.application;

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

    private final MemberProcessor memberProcessor;
    private final MailService mailService;

    @Transactional
    public MemberSignupDto.Info signup(final MemberSignupDto.Command request) {
        /** 회원 가입 **/
        MemberSignupDto.Info info = memberProcessor.register(request);

        /** 회원가입 완료 후 이메일 전송 **/
        String registeredEmail = info.getEmail();
        mailService.sendMail(toEmailArray(registeredEmail), "회원가입 완료 안내", "회원가입이 완료되었습니다.");

        return info;
    }

    private String[] toEmailArray(String... email) {
        return email;
    }
}

 

개선된 사항

기존의 언급되었던 문제점을 아래와 같이 개선할 수 있었습니다.

  1. 메일 전송 방식이 변경되더라도 MemberSignupService 클래스를 수정하지 않아도 된다.
  2. 구체적인 메일 전송 방식은 MailService인터페이스를 구현한 클래스에서 관리하며, 이를 통해 다양한 메일 전송 방식을 쉽게 추가하거나 교체할 수 있다.
  3. MemberSignupService에 메일 관련 기능이 제거되어 회원 가입 비즈니스 로직에만 집중할 수 있다.

 

결론

MemberSignupService클래스의 JavaMailSender에 대한 직접적인 의존성을 제거하고, 메일 전송 기능을 추상화한 MailService인터페이스에 의존하도록 변경하였습니다.

이로 인해 객체 간의 의존 관계가 느슨해지고, 코드의 유연성과 확장성이 크게 향상되었습니다.

 

앞으로도 DIP를 포함한 객체지향 설계 원칙을 올바르게 이해하고 적용 및 리팩토링 해 나가면서 다른 개발자가 제 코드를 봤을 때 비즈니스 로직을 빠르게 파악할 수 있도록 하고, 소프트웨어의 유연성과 확장성 극대화 시켜 개발 생산성을 높힐 수 있도록 노력해야할 것 같습니다.

 

 

++

위 코드는 회원가입과 메일전송이 동기로 호출되고 있는데, 다음 포스팅에서는 스프링의 EventListener를 사용해서 리팩토링 해보도록 하겠습니다.

댓글