들어가기 전에
버즈덤 프로젝트에서는 [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));
이벤트가 발생했을 때 메시지가 발송되는 것이기 때문에 문자열을 치환하여 메시지를 만드는 것은 메시지를 발송하는 측의 관심사라고 보고 코드를 개선했다. 이렇게 하다 보니 확연히 코드의 수가 줄고 관심사를 제대로 분리할 수 있었다.
마무리
굉장히 부끄러운 코드이지만 부족했기에 더 배울 수 있었던 값진 경험이었다고 생각하고 다시는 실수하지 않기를 바라는 마음에서 글을 작성했다. 이전에도 객체의 책임에 대한 고민이나 확장성, 유연성 등을 고려하지 않았던 것은 아니지만 많이 부족했던 것 같아서 관련된 책도 보면서 더 배워야겠다는 다짐을 했다.
'노트 > F-lab' 카테고리의 다른 글
[Redis] 설치 및 간단한 테스트 해보기 (Ubuntu 18.04) (0) | 2023.06.28 |
---|---|
동시성 제어와 DB 설계의 고민 (0) | 2023.06.23 |
Github Actions: CI/CD 구축 (feat. Docker) (0) | 2023.06.21 |
RabbitMQ를 활용한 메시지 처리 방식 구현: 설치부터 적용까지 (0) | 2023.06.17 |
Github Actions: CI/CD 구축 (feat.shell script) - 실전편 (2) | 2023.06.04 |