본문 바로가기
노트/F-lab

가변 인자를 다루기 위한 Functional Interface 활용

by soro.k 2023. 7. 1.

들어가기 전에

버즈덤 프로젝트에서는 [RabbitMQ] 설치 및 적용기에 작성한 것처럼 Feed 기능을 구현했다. 이때 템플릿을 지정해놓고 스케줄 정보만 지정된 위치의 문자열에 치환할 수 있도록 아래와 같이 MessageTemplate 클래스에 상수로 템플릿 메시지를 선언해서 사용했다.

 

MessageTemplate

public class MessageTemplate {
    // 치환해야 할 문자열이 한 개일 때
    public static final String CREATE_SCHEDULE = "%s님이 {0}로 코칭 일정을 신청했습니다.";
    // 치환해야 할 문자열이 두 개일 때
    public static final String UPDATE_SCHEDULE = "%s님이 {0}에서 {1}로 코칭 일정을 변경했습니다.";
}

 

위의 코드에서 중점적으로 봐야할 것은 치환해야 하는 문자열의 개수가 달라지는 것이다. 그러니까 전달해야 할 매개변수의 수가 달라지는 것인데 이럴 때는 어떻게 구현하는 게 좋을까?

 

이번 글에서는 코드 리뷰를 통해 매개변수의 수에 의존적인 코드를 개선시키는 과정 중에 Functional Interface를 어떻게 활용했는지, 그리고 내가 어떤 점들을 놓쳤었는지를 기록하고자 한다.

 

 

🧐 기존 코드는 어땠을까

내가 생성한 클래스는 다음과 같다. 핵심 코드 외의 유틸성 클래스 또한 변경될 예정이기 때문에 길지만 모두 가져왔다.

  • SchedlueEventDto : 메시지 큐에 전달하기 위한 DTO 객체
  • MessageUtil : LocalDateTime을 원하는 문자열 포맷으로 변경하기 위한 메소드 구현
  • ScheduleEventManager : 매개변수를 전달받아 메시지 큐에 전달하기 위한 ScheduleEventDto를 생성하여 반환하는 메소드 구현
  • ScheduleEventDetails : 매개변수가 하나일 때 사용할 VO 객체
  • UpdateScheduleEventDetails : 매개변수가 두 개일 때 사용할 VO 객체

처음에 의도했던 것

 

1. ScheduleEventDto

public record ScheduleEventDto (
    long senderId,
    long receiverId,
    String feedMessage
) { 
}

 

2. MessageUtil

public class MessageUtil {

    public String convertToString(LocalDateTime scheduleDateTime) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATETIME_FORMAT);

        return scheduleDateTime.format(formatter);
    }
}
  • DATETIME_FORMAT : "yyyy년 MM월 dd일 HH:mm"

 

3. ScheduleEventDetails

public class ScheduleEventDetails {

    private Long senderId;
    private Long receiverId;
    private LocalDateTime scheduleDateTime;

    ...
}

 

4. UpdateScheduleEventDetails

public class UpdateScheduleEventDetails {

    private ScheduleEventDetails scheduleEventDetails;
    private LocalDateTime newCoachingDateTime;

    ...

}

기존 ScheduleEventDeatils를 활용하되 변경된 스케줄 정보를 추가해서 가지고 있다.

 

 

5. ScheduleEventManager

public class ScheduleEventManager {

    ...

    public ScheduleEventDto createByScheduleDetails(ScheduleEventDetails schedule, String template) {
        String convertedDateTime = messageUtil.convertToString(schedule.getScheduleDateTime());

        String message = MessageFormat.format(template, convertedDateTime);

        return new ScheduleEventDto(
            schedule.getSenderId(),
            schedule.getReceiverId(),
            message
        );
    }

    public ScheduleEventDto createByUpdateScheduleDetails(UpdateScheduleEventDetails updateScheduleEventDetails, String template) {
        ScheduleEventDetails scheduleEventDetails = 
                updateScheduleEventDetails.getScheduleEventDetails();
        String current = 
                messageUtil.convertToString(scheduleEventDetails.getScheduleDateTime());
        String newSchedule = 
                messageUtil.convertToString(updateScheduleEventDetails.getNewCoachingDateTime());

        String message = MessageFormat.format(template, current, newSchedule);

        return new ScheduleEventDto(
            scheduleEventDetails.getSenderId(),
            scheduleEventDetails.getReceiverId(),
            message
        );
    }
}

여기서 보면 두 메소드의 전체적인 로직은 똑같으나 UpdateScheduleEventDetails와 같이 변경된 스케줄 정보, 즉 매개변수가 추가됨에 따라 메소드를 추가해서 사용하고 있다.

 

 

6. MenteeScheduleService

MenteeScheduleService에서는 각각 다른 VO 객체를 생성하고 다른 메소드를 호출하여 사용하고 있다.

 

일정을 저장하는 메소드

@Transactional
public MenteeScheduleResponseDto saveMenteeSchedule(long menteeId, long coachingScheduleId) {
    
    ...
    
    ScheduleEventDetails scheduleEventDetails = ScheduleEventDetails.of(menteeId, 
        	coachSchedule.getCoachId(), coachSchedule.getPossibleDateTime());
    feedMessageProducer.produceScheduleEvent(scheduleEventManager
    	.createByScheduleDetails(scheduleEventDetails, CREATE_SCHEDULE));

    return MenteeScheduleResponseDto.from(menteeSchedule);
}

 

  • ScheduleEventDeatils 객체 생성
  • ScheduleEventManager 클래스에서 ScheduleEventDetails를 매개변수로 받는 메소드 호출

 

일정을 변경하는 메소드

@Transactional
public void updateMenteeSchedule(long menteeId, long currentCoachingId, long newCoachingId) {

    ...

    UpdateScheduleEventDetails updateScheduleEventDetails = UpdateScheduleEventDetails
    	.of(menteeId, currentCoachSchedule.getCoachId(),
        currentCoachSchedule.getPossibleDateTime(), newCoachSchedule.getPossibleDateTime());
    feedMessageProducer.produceScheduleEvent(scheduleEventManager
    	.createByUpdateScheduleDetails(updateScheduleEventDetails, UPDATE_SCHEDULE));
}
  • UpdateScheduleEventDetails 객체 생성
  • ScheduleEventManager 클래스에서 UpdateScheduleEventDetails를 매개변수로 받는 메소드 호출

 

😩 매개변수가 계속 늘어난다면

매개변수가 추가됨에 따라 계속 생성되는 VO 객체와 메소드라니 생각만 해도 이상하다. 확장성과 유연성을 고려해서 구현해야 한다는 걸 알면서도 방법이 떠오르지 않았다. 하지만 우선은 기능 명세 상 템플릿이 더 이상 추가되지 않을 것이고 최대 매개변수는 2개이기 때문에 조금은 합리화를 하며 코드를 올렸다. 그리고 역시나 멘토님께서 이 부분을 짚어주셨다.

 

멘토님이 남겨주신 코드 리뷰

 

 

❓ Functional Interface를 어떻게 활용하지

사실 Fucntional Interface를 이렇게 활용해 본 적은 처음이라 감이 잘 오지 않았다. 자바에서 기본적으로 제공하는 클래스들을 보기도 하고 커스텀으로 만들어 보기도 하면서 계속해서 고민 하다가 우선은 의존적인 메소드와 객체를 제거하는 것에 목적을 두고 코드를 작성했다.

 

1. ScheduleEventDetails

public class ScheduleEventDetails {

    private Long senderId;
    private Long receiverId;
    private String template;

    ...

}
  • 기존에 매개변수가 달라 생성했던 UpdateEventDetails 클래스는 제거하고 ScheduleEventDetails를 같이 사용하도록 했다.
  • 일정 컬럼은 제거하고 템플릿 메시지 컬럼을 추가했다.

 

2. MessageFormatter

@FunctionalInterface
public interface MessageFormatter {
    String format(String template, String ...args);
}

매개변수가 달라지는 것에 대응하기 위해 가변인자를 받을 수 있는 Functional Interface를 생성했다.

 

 

3. ScheduleEventManager

public class ScheduleEventManager {

    ...

    public ScheduleEventDto createByScheduleDetails(ScheduleEventDetails eventDetails, MessageFormatter messageFormatter,
                                                    LocalDateTime ...args) {
        String[] convertedDateTime = Arrays.stream(args)
            .map(messageUtil::convertToString)
            .toArray(String[]::new);

        return new ScheduleEventDto(
            eventDetails.getSenderId(),
            eventDetails.getReceiverId(),
            messageFormatter.format(eventDetails.getTemplate(), convertedDateTime)
        );
    }
}
  • 기존의 createByUpdateScheduleDetails는 제거했다.
  • 서비스에서 매개변수의 수에 맞게 생성한 MessageFormatter, 그리고 가변인자로 매개변수를 전달 받아 문자열을 치환했다. 

 

4. MenteeScheduleService

// 변경 전 코드
UpdateScheduleEventDetails updateScheduleEventDetails = UpdateScheduleEventDetails
    	.of(menteeId, currentCoachSchedule.getCoachId(),
        currentCoachSchedule.getPossibleDateTime(), newCoachSchedule.getPossibleDateTime());
    feedMessageProducer.produceScheduleEvent(scheduleEventManager
    	.createByUpdateScheduleDetails(updateScheduleEventDetails, UPDATE_SCHEDULE));
        
// 변경 후 코드
ScheduleEventDetails scheduleEventDetails = 
    ScheduleEventDetails
    	.of(menteeId, currentCoachSchedule.getCoachId(), UPDATE_SCHEDULE);

MessageFormatter updateScheduleFormatter = (template, args) -> 
    MessageFormat.format(template, args[0], args[1]);
    

ScheduleEventDto scheduleEventDto = scheduleEventManager.createByScheduleDetails(
	scheduleEventDetails, updateScheduleFormatter, 
    currentCoachSchedule.getPossibleDateTime(), 
    newCoachSchedule.getPossibleDateTime());

feedMessageProducer.produceScheduleEvent(scheduleEventDto);

MessageFormatter에 문자열을 치환할 때의 매개변수 개수를 지정하고 일정 정보를 전달해서 해당 정보들이 각 위치에 차례대로 치환될 수 있도록 했다. 

 

의도대로 매개변수의 개수에 의존적인 메소드와 객체는 제거했지만 오히려 코드가 지저분해졌다는 느낌이 들었다. 그리고 코드 리뷰에서 찜찜했던 부분들에 대해 아래와 같은 코멘트를 받았다.

  • 하나의 메소드로 만들려는 의도는 이해하지만 가변인자를 사용하는 이유를 모르겠다.
  • MessageFormat에서 사용하는 args[0]과 args[1]도 정확히 어떤 값이 들어가는지 알 수 없기 때문에 모호하다.

 

더불어서 멘토님이 말씀하신 Functional Inteface 활용 의도와 맞고 책임과 관심사가 제대로 분리된 코드를 직접 보여주시고 설명해 주셔서 이번에 내가 놓친 게 뭐였는지 정확하게 파악할 수 있었다.

  • Functional Interface 활용의 의도를 제대로 파악하지 못함
  • 객체의 책임과 및 관심사 분리가 제대로 이루어지지 않음

 

 

❗️ 최종의 최종 최종.code

기존 코드와 비교하여 최종적으로 제거한 클래스들은 다음과 같다. 내부 코드들이 모두 매개변수의 개수에 의존적이었다.

  • ScheduleEventManager 
  • ScheduleEventDetails 
  • UpdateScheduleEvnetDetails

 

1. ScheduleEventDto

public record ScheduleEventDto (
	long senderId,
	long receiverId,
	String feedMessage
) {
}

 

2. FeedMessageProducer

// 변경 전 코드
public void produceScheduleEvent(ScheduleEventDto scheduleEventDto) {
	rabbitTemplate.convertAndSend(EXCHANGE, SCHEDULE_ROUTING_KEY, scheduleEventDto);
 }
 
// 변경 후 코드
public void produceScheduleEvent(Long senderId, Long receiverId, 
                    		Supplier<String> messageSupplier) {
    rabbitTemplate.convertAndSend(EXCHANGE, SCHEDULE_ROUTING_KEY, ScheduleEventDto.builder()
        .senderId(senderId)
        .receiverId(receiverId)
        .feedMessage(messageSupplier.get())
        .build()
    );
}
  • Supplier<T>를 활용해서 지정된 값을 get() 메소드로 가져온다.
  • 이전에는 ScheduleEventDto 객체를 미리 생성하고 전달했지만 이제는 정보를 모두 전달 받아 보낼 때 객체를 생성한다. 해당 메시지를 보내는 주체에게 객체 생성의 책임을 위임한 것이다. (멘토님이 말씀하시는 핵심 리팩토링 부분 ⭐️)

 

3. MenteeScheduleService

// 변경 전 코드
ScheduleEventDetails scheduleEventDetails = 
    ScheduleEventDetails
    	.of(menteeId, currentCoachSchedule.getCoachId(), UPDATE_SCHEDULE);

MessageFormatter updateScheduleFormatter = (template, args) -> 
    MessageFormat.format(template, args[0], args[1]);
    

ScheduleEventDto scheduleEventDto = scheduleEventManager.createByScheduleDetails(
	scheduleEventDetails, updateScheduleFormatter, 
    currentCoachSchedule.getPossibleDateTime(), 
    newCoachSchedule.getPossibleDateTime());

feedMessageProducer.produceScheduleEvent(scheduleEventDto);


// 변경 후 코드
String currentDateTime = messageUtil.convertToString(currentCoachSchedule.getPossibleDateTime());
String newDateTime = messageUtil.convertToString(newCoachSchedule.getPossibleDateTime());
feedMessageProducer.produceScheduleEvent(menteeId, currentCoachSchedule.getCoachId(), () ->
    MessageFormat.format(UPDATE_SCHEDULE, currentDateTime, newDateTime));

이벤트가 발생했을 때 메시지가 발송되는 것이기 때문에 문자열을 치환하여 메시지를 만드는 것은 메시지를 발송하는 측의 관심사라고 보고 코드를 개선했다. 이렇게 하다 보니 확연히 코드의 수가 줄고 관심사를 제대로 분리할 수 있었다.

 

 

 

마무리

굉장히 부끄러운 코드이지만 부족했기에 더 배울 수 있었던 값진 경험이었다고 생각하고 다시는 실수하지 않기를 바라는 마음에서 글을 작성했다. 이전에도 객체의 책임에 대한 고민이나 확장성, 유연성 등을 고려하지 않았던 것은 아니지만 많이 부족했던 것 같아서 관련된 책도 보면서 더 배워야겠다는 다짐을 했다.