함수형 인터페이스를 사용하여 if 문 제거하기
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);
}
}
위 코드의 문제점
- 중첩된 if 문으로 코드 가독성이 떨어집니다.
- 현재는 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