[Spring/SpringBoot] Factory Design Pattern을 이용한 서비스 관리

이수진·2023년 3월 13일
0
post-thumbnail

✅ Factory Design Pattern 이란?

입력받은 값, 상황에 따라 일부 비즈니스 로직을 다르게 구현해야 하는 경우에
if - else if - else if - … - else 로 코드를 짜게 되면
예를들어 분기하는 조건이 없어지거나 추가되면 이를 사용하게 되는 부분을 모두 찾아 이에 맞게 수정해주어야 합니다
또 코드에서 공통되는 부분이 있는데, 이를 똑같이 작성해주어야 하는 비효율성이 발생합니다

이를 Factory Design Pattern을 이용하여 관리하고자 합니다

활용성

  • 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없을 때
  • 생성할 객체를 기술하는 책임을 자신의 서브클래스가 지정했으면 할 때

✅ Factory Design Pattern을 사용하는 이유?

1. 객체 생성과 관리를 캡슐화하여 유연성을 높인다

Factory 패턴은 객체 생성에 대한 책임을 전담하는 팩토리 클래스를 이용하여 객체를 생성합니다.
이렇게 하면 객체 생성과 관리를 캡슐화하여 유연성을 높일 수 있습니다.
예를 들어, 객체 생성 방식이 변경될 경우에도 클라이언트 코드를 수정할 필요 없이 팩토리 클래스만 수정하면 됩니다. 또한, 객체 생성에 필요한 정보를 외부에서 주입받을 수 있기 때문에 객체 생성과 관리에 대한 유연성을 높일 수 있습니다.

2. 객체 생성과 관리를 중앙 집중화하여 코드 중복을 방지한다

Factory 패턴은 객체 생성과 관리를 중앙 집중화하여 코드 중복을 방지할 수 있습니다.
예를 들어, 여러 곳에서 객체를 생성해야 할 경우에 Factory 클래스를 이용하여 중복된 코드를 제거할 수 있습니다.

3. 다형성을 지원하여 객체 간의 결합도를 낮추고 확장을 용이하게 합니다

Factory 패턴은 다형성을 지원하기 때문에 객체 간의 결합도를 낮출 수 있습니다.
팩토리 클래스는 인터페이스를 이용하여 객체를 생성하기 때문에 클라이언트 코드는 구체 클래스가 아닌 인터페이스를 이용하여 객체를 생성할 수 있습니다.
이렇게 하면 객체 간의 결합도를 낮출 수 있고, 코드의 재사용성을 높일 수 있습니다.


✅ 프로젝트에 적용해보기

  • if-else 문으로 여러 분기에 따라서 장황스럽게 구현된 코드를 Factory Pattern을 활용하여 유지보수와 생산성이 용이하도록 구현하려고 했습니다
  • Factory를 적용한 프로젝트의 구조는 다음과 같습니다

우선 DateTime -> AvailableDateTime 으로 바꾸어야 하는 상황이고,
이를 바꾸는 방법은 2가지가 존재합니다.

  • 시간이 포함된 경우
    * DateTimeConvertor를 이용
  • 시간이 포함되지 않은 경우
    * DateConvertor를 이용

따라서 DateTimeToAvailableDateTimeConvertor 를 interface 로 두고,
DateTimeConvertor, DateConvertor 가 이를 상속받아 구체화하도록 하였습니다.

코드는 다음과 같습니다.

// DateTimeToAvailableDateTimeConvertor.java

public interface DateTimeToAvailableDateTimeConvertor {

    List<AvailableDateTime> convert(TimeBlock timeBlock,
                                    List<LocalDateTime> dateTimes);
}

그리고 이 인터페이스를 상속받은 각각의 DateTimeConvertor와 DateConvertor 구현체는 다음과 같습니다.

각각을 bean으로 등록해줍니다.

// DateTimeConvertor.java

@Component
public class DateTimeConvertor implements DateTimeToAvailableDateTimeConvertor {

    @Override
    public List<AvailableDateTime> convert(TimeBlock timeBlock,
                                           List<LocalDateTime> dateTimes) {
        ...
    }
}
// DateConverter.java

@Component
public class DateConvertor implements DateTimeToAvailableDateTimeConvertor {

    @Override
    public List<AvailableDateTime> convert(TimeBlock timeBlock,
                                           List<LocalDateTime> dateTimes) {
        ....
    }
}

그리고 Converter를 호출할 Factory component를 만들었고,
이것의 이름을 DateTimeToAvailableDateTimeConverterFactory 라 지었습니다.

Factory component 에서 getInstance() 로 converter를 호출을 하면,
상황에 맞게 필요한 converter가 호출이 될 것입니다.

인스턴스화에 대한 책임을 팩토리 클래스가 맡게 됩니다.

Factory class 에 대한 코드는 다음과 같습니다.

// DateTimeToAvailableDateTimeConverterFactory

@Component
@RequiredArgsConstructor
public class DateTimeToAvailableDateTimeConvertorFactory {

    private final Map<String, DateTimeToAvailableDateTimeConvertor> convertors;

    public DateTimeToAvailableDateTimeConvertor getInstance(boolean hasTime) {
        if (hasTime) {
            return convertors.get("dateTimeConvertor");
        }
        return convertors.get("dateConvertor");
    }
}

그리고 서비스 로직에서 이를 호출하는 부분은 다음과 같습니다.

@Service
@Transactional
@RequiredArgsConstructor
public class TimeBlockService {

    private final DateTimeToAvailableDateTimeConvertorFactory dateTimeToAvailableDateTimeConvertorFactory;

    public void replace(String roomUuid, TimeReplaceRequest timeReplaceRequest) {
        ...

        DateTimeToAvailableDateTimeConvertor dateTimeToAvailableDateTimeConvertor = dateTimeToAvailableDateTimeConvertorFactory
                .getInstance(timeReplaceRequest.getHasTime());
        List<AvailableDateTime> availableDateTimes = dateTimeToAvailableDateTimeConvertor.convert(timeBlock, timeReplaceRequest.getAvailableDateTimes());

        ...
    }

TimeBlockService에서는 DateTimeToAvailableDateTimeConvertorFactory를 주입을 받고,
Factory에게 두 convertor 중 어느 convertor를 쓸 지 책임을 넘기게 됩니다.

서비스 비즈니스 로직이 if 문과 여러 조건 분기로 나뉘어 작성될 수도 있지만,
이렇게 Factory Pattern을 이용해 더욱 가독성 높고 더 깔끔하게 코드를 작성할 수 있을 뿐만 아니라
인스턴스 생성에 대한 변경이 발생하더라도 팩토리 클래스를 찾아 변경할 수 있고,
인스턴스를 생성할 때 구상 클래스가 아닌 인터페이스만을 필요로 하므로 유연성과 확장성이 뛰어난 코드를 만들 수 있습니다.


이것을 검증하기 위한 테스트코드는 다음과 같습니다.
Factory에서 time 값이 있는지 없는지에 따라 서로 다른 convertor를 호출하며 비즈니스 로직이 달라지는데,
올바른 converter를 호출하는지 이에 대한 테스트는 다음과 같습니다.

@SpringBootTest
public class DateTimeToAvailableDateTimeConvertorFactoryTest {

    @Autowired
    private DateTimeToAvailableDateTimeConvertorFactory dateTimeToAvailableDateTimeConvertorFactory;

    @Test
    void hasTime이_true이면_dateTimeConvertor를_반환한다() {
        DateTimeToAvailableDateTimeConvertor instance = dateTimeToAvailableDateTimeConvertorFactory.getInstance(true);
        assertThat(instance).isInstanceOf(DateTimeConvertor.class);
    }

    @Test
    void hasTime이_false이면_dateConvertor를_반환한다() {
        DateTimeToAvailableDateTimeConvertor instance = dateTimeToAvailableDateTimeConvertorFactory.getInstance(false);
        assertThat(instance).isInstanceOf(DateConvertor.class);
    }
}
profile
꾸준히, 열심히, 그리고 잘하자

0개의 댓글