Backend/☕️ Java

함수형 인터페이스를 사용하여 if 문 제거하기

Hugehoo 2022. 7. 10. 22:15

before 리팩토링 _  EventController.transactReview() 

클라이언트 단에서 넘어오는 reviewDto 객체는 action 필드를 가집니다.

그리고 이 action 필드는 ADD, MOD, DELETE 3가지 값을 가지며, 값에 따라 수행되는 메서드가 달라집니다. 

즉 DTO 필드 값에 따라 Controller layer 에서 호출해야하는 Service layer 의 메서드가 분기됩니다. 

public class EventDTO {

    public static class REVIEW {

        private String type;
        
        private String action;
        
        ...

    }
}


@PostMapping
public ReviewDto transactReview(
        @Valid @RequestBody EventDTO.REVIEW reviewDto
) {
    if (reviewDto.getAction().equals("ADD")) {
        return serviceImpl.add(reviewDto);
    } else if (reviewDto.getAction().equals("MOD")) {
        return serviceImpl.mod(reviewDto);
    } else if (reviewDto.getAction().equals("DELETE")) {
        return serviceImpl.delete(reviewDto);
    }
}

 

위 코드의 문제점

  1. 중첩된 if 문으로 코드 가독성이 떨어집니다.
  2. 현재는 ADD, MOD, DELETE 세가지 분기문만 존재하지만 추후에 조건문이 더 추가될 수 도 있는 점도 고려해야합니다.

 

해결

함수형 인터페이스를 사용해 중첩된 if 문 코드를 리팩토링 할 수 있습니다.

 

우선 if 문으로 도배된 위 코드를 리팩토링 해보겠습니다.

 

EventService Interface

EventService 라는 인터페이스를 정의합니다. 해당 인터페이스는 getEventType(), transaction() 2가지 메서드를 선언합니다.

getEventType 은 위 코드의 reviewDto.getAction() 에서 어떤 Event 를 리턴받을지 정의한 코드이며,

transaction 은 serviceImpl 클래스 내부의 add(), mod(), delete() 메서드를 추상화 한 것입니다.

public interface EventService {

    Event getEventType();
    Review transaction(EventDTO.REVIEW reviewDto);

}

 

EventFactory

그리고 EventFactory 클래스를 생성합니다.

해당 클래스의 생성자엔 List<EventServices> 를 인자로 받는데, 이 리스트에는 EventService 인터페이스를 구현한 객체들이 포함됩니다. 

@Component
public class EventFactory {

    private final Map<Event, Function<EventDTO.REVIEW, Review>> eventServiceMap = new HashMap<>();

    public EventFactory(List<EventService> eventServices) {
        if (CollectionUtils.isEmpty(eventServices)) {
            throw new IllegalArgumentException("존재하지 않는 method 입니다");
        }
        for (EventService service : eventServices) {
            this.eventServiceMap.put(service.getEventType(), 
            (EventDTO.REVIEW review) -> service.transaction(review)
            );
        }
    }

    public Function<EventDTO.REVIEW, Review> getEvent(Event event) {
        return eventServiceMap.get(event);
    }

}

 

eventServiceMap 은 Map 타입으로, 두번째 인자에서 Java8 의 함수형 인터페이스인 Function 을 받습니다.

Function 함수형 인터페이스 : 매개변수와 리턴값이 둘다 존재하는 인터페이스
Function<T, R> 
R apply(T t) // 객체 T 를 객체 R 로 매핑한다.

제가 적용할 코드에 Function 을 적용한다면, DTO 인 EventDTO.REVIEW 객체를 Review 객체로 매핑합니다.

 

이제 eventServiceMap 의 두번째 인자에 Function 을 넣어줘야 합니다. 아래 코드를 보면 람다식을 통해 EventDTO.REVIEW 객체를 인자로 하여 transaction() 메서드에 넣어줍니다. EventService 인터페이스의 transaction() 메서드는 리턴타입이 Review 이므로, Function<T, R> 에서 T 객체가 R 객체로 매핑되는 법칙도 성립시킬 수 있습니다.

// 기존
   for (EventService service : eventServices) {
        this.eventServiceMap.put(service.getEventType(), 
        (EventDTO.REVIEW review) -> service.transaction(review)
        );
    }
    
    
// 리팩토링
   for (EventService service : eventServices) {
	Function<EventDTO.REVIEW, Review> transaction = service::transaction;
	this.eventServiceMap.put(service.getEventType(), transaction);
   }

이렇게 하여 eventServiceMap 컬렉션에 각각의 eventType 에 맞는 행위(메서드)를 매핑할 수 있습니다.

이제 eventServiceMap.get("ADD") , eventServiceMap.get("MOD") , eventServiceMap.get("DELETE")  에 상응하는 transaction 을 정의하면 됩니다.

 

 

Event enum

public enum Event {

    ADD("ADD"),
    MOD("MOD"),
    DELETE("DELETE");

    final String event;

    Event(String event) {
        this.event = event;
    }
}

 

EventServiceImpl

@Service
@RequiredArgsConstructor
public class EventServiceImpl {

    private final EventFactory eventFactory;

    public ReviewDto getEvent(EventDTO.REVIEW reviewDto) {
        Event event = Event.valueOf(reviewDto.getAction());
        Review review = eventFactory.getEvent(event).apply(reviewDto);
        return ReviewDto.of(review);
    }
}

거의 다왔습니다.

생성자 주입 방식으로 EventFactory 클래스를 주입합니다.

주입받은 eventFactory 객체의 getEvent() 메서드를 사용하는데, 해당 메서드의 리턴타입은 Function<EventDTO.REVIEW, Review> 으로 함수가 반환되고, 리턴된 함수는 .apply() 를 호출해야만 실행됩니다. 

리턴된 함수 -> (EventDTO.REVIEW review) -> service.transaction(review) 이런 형태의 함수가 리턴되는데 apply() 메서드의 인자가 곧 service.transaction() 의 인자가 됩니다. 

 

 

리팩토링된 EventController 의 transactReview() 

리팩토링 이전에는 해당 컨트롤러에서 reviewDto의 값에 따른 메서드를 호출해야 했기 때문에 if 문이 덕지덕지 붙어있었습니다. 

리팩토링 후에는, 위에서 생성한 EventServiceImpl 객체를 주입받으면서 reviewDto 를 인자로 넣어 getEvent() 메서드를 호출합니다. reviewDto 값에 따른 분기 로직을 모두 걷어낼 수 있게 됐습니다.

@RestController
@RequiredArgsConstructor
public class EventController {

    private final EventServiceImpl eventServiceImpl;

    @PostMapping
    public ReviewDto transactReview(
            @Valid @RequestBody EventDTO.REVIEW reviewDto
    ) {
        return eventServiceImpl.getEvent(reviewDto);
    }
}

 

 

EventService Interface 를 구현하는 클래스

각각의 구현클래스는 EventService 의 인터페이스를 상속받으며, getEventType, transaction 메서드를 오버라이드 합니다.

@Component
@RequiredArgsConstructor
public class ReviewAddService implements EventService {

    @Override
    public Event getEventType() {
        return Event.ADD;
    }

    @Override
    @Transactional
    public Review transaction(EventDTO.REVIEW reviewDto) {
	...
    }
}


@Component
@RequiredArgsConstructor
public class ReviewModService implements EventService {

    @Override
    public Event getEventType() {
        return Event.MOD;
    }

    @Override
    @Transactional
    public Review transaction(EventDTO.REVIEW reviewDto) {
	...
    }
}


@Component
@RequiredArgsConstructor
public class ReviewDeleteService implements EventService {

    @Override
    public Event getEventType() {
        return Event.DELETE;
    }

    @Override
    @Transactional
    public Review transaction(EventDTO.REVIEW reviewDto) {
	...
    }
}

 

 

"변경에는 닫혀있고, 확장에는 열려있는 코드를 작성하라"는 OCP 원칙은, 쉽게 말해 기존의 코드를 변경하지 않으면서 기능을 추가 할 수 있도록 설계하는 것입니다. 자 기능을 추가 한다는게 무엇일까요? 예를 들어 reviewDto 의 값 중 OVERRIDE 라는 값이 추가된다면, 기존 if 문 구조에서는 else if 구문을 추가하는 식으로 기존 코드를 수정해야만 했습니다.

하지만 함수형 인터페이스를 적용한 지금의 코드에서는 EventFactory 나 EventController, EventServiceImpl 코드를 전혀 수정하지 않은채 그저 EventService 를 상속받는 새로운 클래스만 생성하면 됩니다. OCP 원칙을 지킬 수 있게 됐습니다. 

 

정리

if 문으로 중첩된 구조를 없애는 리팩토링을 진행했습니다. 인터페이스로 공통의 관심사를 추상화했고, 함수형 인터페이스로 코드를 더 간단하게 리팩토링 할 수 있었습니다. 전체적인 구조의 복잡도는 올라갔지만, 객체지향 원칙에 입각한 코드를 작성하는 시도를 해볼 수 있었습니다. 

 

 

 

+

포텐셜 넘치는 동료가 적용한 사례도 보시면 더 잘 이해되실 겁니당 😙

https://zzzinho.tistory.com/135

 

[Server] 함수형 인터페이스로 리팩토링 (feat. Hugehoo)

개발기보다 먼저 업로드되는 리팩토링기 🤓 정열적인 크로스피터이자 어마무시 백엔드 개발자 Hugehoo님의 함수형 인터페이스를 사용하여 if 문제거하기 포스팅을 봤는데 토이 프로젝트에 적용

zzzinho.tistory.com