If 분기문 줄여보기 - Factory Pattern

ifi9·2023년 4월 3일
0
post-custom-banner

토이 프로젝트로 소켓을 사용한 채팅 웹앱을 만들어 보고 있었다. 처음에는 너무 단순한 생각으로 '채팅 내용만 소켓으로 보내는 것이겠지?'라는 생각에 채팅 관련된 처리만 하며 지금 당장의 2~3개의 else-if 문은 괜찮을 것이라는 안일한 생각을 가졌었다.
아래는 해당 토이 프로젝트 소스 코드의 일부이다.

if(messageType.equals(MessageType.SEND_MESSAGE)) {
	ChannelChat channelChat = channelChatService.createChannelChat(request);
	return modifiedMessage(payload, channelChat);
}
else if(messageType.equals(MessageType.EDIT_MESSAGE)) {
	ChannelChat channelChat = channelChatService.changeChannelChatInfo(request);
	return modifiedMessage(payload, channelChat);
}
else {
	log.error("message type : {}", messageType);
	return message;
}

그런데 진행을 하다 보니 내가 한 어떠한 행위를 다른 사람도 실시간으로 알아야 한다는 것(내가 한 행위뿐만 아니라 타인의 행위을 나도 알아야 함)을 깨닫게 되었고, 그 해결책으로 나는 '소켓으로 바로바로 알리자'라는 방식으로 구현을 진행하였다.
이렇게 정하고 나니 채팅에 첨부된 파일 업로드, 로그인/로그아웃 알림, 프로필 사진의 변경 등 많은 기능들이 소켓 핸들러를 거쳐야 되었으며, 위에서 괜찮을 것이라고 생각했던 If 분기문이 엄청나게 불어날 예정이 되어버린 것이다.

채팅 기능 구현 이후 파일 업로드 기능을 만들던 도중에 더는 방치해선 안되겠다고 생각이 되어서, 이를 줄여보기 위한 고민을 하게 되었다.

Enum Class

사실 위의 문제는 처음 만들 때부터 음.. 언젠간 늘어나겠지? 하면서 어떻게 할지 고민하다가 타입별 다른 연산하는 방법을 본 기억만 가지고 글을 다시 확인하지도 않고, 어떻게 사용해야 하는 지도 모르는 채로 무작정 Enum Class를 만들어서 관리를 하고 있었다.

아래부터는 모두 예제 소스 코드들이며, package에 대한 정보는 아래 캡처로 대체한다.Enum Class에는 정해진 MemberType 값인 A, B, C, D가 있다.

public enum MemberType {
	// 추상 메서드는 예시용이며 사용되지 않았음
    A {
        @Override
        void memberTypeHandle() {
            //
        }
    },
    B {
        @Override
        void memberTypeHandle() {
            // 
        }
    },
    C {
        @Override
        void memberTypeHandle() {
            //
        }
    },
    D {
        @Override
        void memberTypeHandle() {
            //
        }
    };

    abstract void memberTypeHandle();
}

그리고 Component를 등록하였고, 각각 run()이라는 메서드가 있다.

@Slf4j
@Component
public class AComponent {

    public void run() {
        log.info("####### AComponent #######");
    }

}

@Slf4j
@Component
public class BComponent {

    public void run() {
        log.info("####### BComponent #######");
    }

}

@Configuration
@ComponentScan(basePackages = "etc.factory")
public class AppConfig {

    @Bean
    public AComponent aComponent() {
        return new AComponent();
    }

    @Bean
    public BComponent bComponent() {
        return new BComponent();
    }

}

마지막으로 위의 Component들을 사용하는 SampleFactory Class와 AnyRequest Class 들이다.
AnyRequest 객체 안에 위에서 본 MemberType Enum Class를 속성으로 가지고 있다.

@Getter
public class AnyRequest {

    private String email;
    private String addr;
    private MemberType memberType;

    public AnyRequest(String email, String addr, MemberType memberType) {
        this.email = email;
        this.addr = addr;
        this.memberType = memberType;
    }
}

@Slf4j
@Component
public class SampleFactory {

    @Autowired
    private AComponent aComponent;

    @Autowired
    private BComponent bComponent;

    public static void main(String[] args) {
        AnyRequest anyRequest = new AnyRequest("userEmail", "userAddr", MemberType.A);
//        AnyRequest anyRequest = new AnyRequest("userEmail", "userAddr", MemberType.B);
//        AnyRequest anyRequest = new AnyRequest("userEmail", "userAddr", MemberType.D);

        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        SampleFactory sf = ac.getBean(SampleFactory.class);
        sf.handleRequest(anyRequest);
    }

    private void handleRequest(AnyRequest request) {
        if (MemberType.A == request.getMemberType()) {
            aComponent.run();
            log.info("Member Type : {}", request.getMemberType());
        }
        else if (MemberType.B == request.getMemberType()) {
            bComponent.run();
            log.info("Member Type : {}", request.getMemberType());
        }
        else {
            log.error("Member Type : {}", request.getMemberType());
        }
    }

}

너무 막연한 생각으로 Enum Class를 추가했었다 보니, Enum을 통해 분기문을 줄이는 과정 중에 내가 원하는 것은 Enum 값에 따른 내부 값을 수정하는 것이 아닌 것이 문제였다.
abstract 메서드 혹은 Functional Interface를 어떻게든 활용할 수 있겠지?라는 생각이었지만 내가 원하는 것은 비즈니스 로직을 타입별로 분기를 해야 했기 때문이다.

그래서 위의 Enum Class 예제 소스와 같은 방식으로는 해결이 힘들 것이라고 생각을 했고 다른 방법을 강구했다.

Factory Pattern

이전에 아는 분의 github repository를 구경한 적이 있었는데 '와 이걸 이렇게 줄이는구나'했던 것이 떠올랐다. 다시 한번 확인을 해보니 Factory Pattern 같았고, 내가 지금 사용하고 있는 구조를 활용할 수 있겠다고 생각이 되었다.

주요 특징 및 장점

  • 코드 유지 보수성을 높일 수 있다.
  • 객체 생성 코드를 중앙 집중화하므로 코드 중복을 줄일 수 있다.
  • 객체 생성 방식이 변경되어도 객체 생성 코드를 수정할 필요 없다.
  • 다형성을 활용할 수 있어 객체 생성 로직을 담당하는 코드와 객체 사용 코드를 분리할 수 있다.

단점

  • 객체 생성 로직을 분리함으로써 코드의 가독성이 떨어질 수 있다.
  • 패턴을 구현하는 데에 있어 추가적인 클래스 및 소스 코드가 필요할 수 있다.

추가 및 변경된 소스 코드

public interface MemberTypeInterface {
    boolean convertable(MemberType memberType);
    void handle();
}

@Component
@RequiredArgsConstructor
public class MemberTypeFactory {
    private final Set<MemberTypeInterface> memberTypeInterfaces;

    public MemberTypeInterface getMemberTypeInterface(MemberType memberType) {
        return memberTypeInterfaces.stream()
                .filter(memberTypeInterface -> memberTypeInterface.convertable(memberType))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException(memberType.name()));
    }

}

MemberTypeInterface의 구현체들 중에 convertable 메서드의 반환 값으로 true인 것이 있다면, 그 구현체를 사용하는 내용이다.

@Slf4j
@Component
public class AComponent implements MemberTypeInterface {

    @Override
    public boolean convertable(MemberType memberType) {
        return MemberType.A == memberType;
    }

    @Override
    public void handle() {
        log.info("####### handle AComponent #######");
    }

    public void run() {
        log.info("####### AComponent #######");
    }

}

BComponent에도 MemberTypeInterface를 구현하였고, 위와 비슷한 소스 코드를 담고 있다.

// SampleFactory.class

@Autowired
private MemberTypeFactory memberTypeFactory;

...

private void handleRequest(AnyRequest request) {
	MemberTypeInterface memberTypeInterface = memberTypeFactory.getMemberTypeInterface(request.getMemberType());
    memberTypeInterface.handle();
}

이렇게 MemberTypeFactory의 getMemberTypeInterface 메서드를 통해서 Enum 값에 해당하는 Component를 반환을 한다면 원하는 로직을 태울 수 있게 되었으며, 앞으로 계속 늘어날 If 분기문을 2줄로 줄일 수 있게 되었다.

마치며

이 글에서는 ~~하겠지?라는 불확실한 추측만으로 토이 프로젝트를 진행했다는 것을 볼 수 있다. 앞으로는 무엇을 하기 전에는 나중에 고친다는 생각보다는 고민해서 처음부터 조금 더 좋은 방법을 선택할 수 있도록 노력해야겠다.

줄이는 과정 중에 아쉬웠던 점도 있다. 미처 생각하지 못한 방법이 있을 듯한데, 아직까지 떠오르지 않아 수정을 못하고 있다.
A라는 enum 값과 B라는 enum 값이 똑같은 AComponent를 호출하고 각각 다른 메서드를 호출해야 하는 경우(글 최상단의 토이프로젝트 소스 코드 예시)가 있었다. 이 경우에는 내부에서 사용할 무언가를 또 만들어야 하는 것인지 너무 오버하는 것인지를 판단하지 못했다.
결국 convertable 메서드부터 || 논리 연산을 통해 A, B 모두 통과시키고, handle 메서드 내부에는 If 분기문을 통해 2가지 비즈니스 로직을 호출하는 것으로 되어있다. 이것에 대한 점은 잊지 않고 계속 고민해 보고 있으며 더 깔끔하게 수정할 수 있다면 반드시 해봐야겠다.

post-custom-banner

0개의 댓글