해당 프로젝트는 기업 내 표준 단어·용어를 사전처럼 관리하는 메타 시스템을 구축하는 프로젝트입니다.
메타 시스템이란 기업 내에서 테이블이나 컬럼을 설계할 때 데이터의 일관성을 지키기 위해 사용하는 일종의 '표준 사전'입니다. 만약 사내에서 '고객 ID'를 두고 어떤 팀은 ’cust_id’ 어떤 팀은 ’customer_no’로 제각각 명명한다면 데이터의 일관성이 무너지게 됩니다.
따라서 개발자, 데이터 분석가 등의 사용자들은 데이터베이스에 새로운 테이블, 컬럼의 생성이 필요한 경우 표준화된 단어·용어를 사용(없는 경우 단어·용어를 신청)하여 신청서를 작성하고 관리자가 해당 신청서를 검토한 후 반영하여 데이터의 일관성(표준)을 준수할 수 있도록 관리합니다.
관리자가 신청서를 검토하는 과정에서 보완이 필요하거나 반려 사유가 있을 때, 사용자와 소통할 수 있는 창구가 필요했습니다. 이를 위해 게시판의 댓글처럼 신청 내역에 의견을 남기는 기능을 구현했습니다.
핵심 요구사항
위 요구사항을 고려했을 때 떠오르는 두 가지 방법이 있었습니다.
가장 직관적인 방법은 서비스 로직 내에서 파라미터로 넘어온 값들을 확인하여 분기 처리를 하는 것입니다.
// 분기문 접근 시 예상되는 구조
public void sendCommentAlrm(ReqCommentEvent event, ReqCommentModel comment) {
String type = comment.getType();
// 1 depth: 이벤트(생성/수정/삭제) 분기
if (ReqCommentEvent.CREATED.equals(event)) {
// 2 depth: 도메인(컬럼/테이블/신청서 등) 분기
if ("col".equals(type)) {
// 컬럼 도메인 등록 알림 로직...
} else if ("tbl".equals(type)) {
// 테이블 도메인 등록 알림 로직...
} // ...
} else if (ReqCommentEvent.UPDATED.equals(event)) {
if ("col".equals(type)) {
// 컬럼 도메인 수정 알림 로직...
} // ...
}
// ... 이벤트와 도메인이 결합되어 끝없이 길어지는 중첩 조건문
}
이처럼 다수의 도메인 타입과 이벤트가 결합되면 조건문이 깊게 중첩되어 코드의 복잡도가 크게 증가합니다. 이는 곧 다음과 같은 구조적인 단점으로 이어집니다.
실제 구현으로 채택한 방법으로 다형성을 활용하여 각 타입별 알림 생성 로직을 독립적인 클래스로 분리하고, 실행 시점에 적절한 구현체를 찾아 실행하는 전략 패턴을 사용하는 것입니다.
public interface ReqCommentAlrmResolver {
ReqCommentType supportsType(); // 지원하는 대상 타입 (req, tbl, col 등)
ReqCommentEvent supportsEvent(); // 지원하는 이벤트 (생성, 수정, 삭제)
ReqCommentAlrmPayload resolve(ReqCommentModel comment); // 실제 알림 페이로드 조립
}// 컬럼 도메인 구현체
@Component
@RequiredArgsConstructor
public class ColumnCommentCreatedAlrmResolver implements ReqCommentAlrmResolver {
private final ReqCommentAlrmUserService reqCommentAlrmUserService;
@Override
public ReqCommentAlrmPayload resolve(ReqCommentModel comment) {
return ReqCommentAlrmPayload.builder()
.alrmType(CommonConstants.ALRM.STD_REVIEW_COL_COMMENT_REG) // 컬럼 전용 알림 타입 지정
.alarmParam(ReqCommentAlrmParamService.buildParam(comment))
.receivers(reqCommentAlrmUserService.getReceivers(comment))
.build();
}
@Override
public ReqCommentEvent supportsEvent() { return ReqCommentEvent.CREATED; }
@Override
public ReqCommentType supportsType() { return ReqCommentType.col; }
} 신청서(Request) 타입의 경우에는 신청서 상세 정보를 조회하는 등 복잡한 과정을 거치게 되는데, 이러한 도메인 특화 로직이 전략 클래스 내부로 안전하게 격리됩니다.@Component를 통해 List<ReqCommentAlrmResolver>로 모든 전략 구현체를 한 번에 주입받은 뒤, 빠른 조회를 위해 EnumMap에 캐싱해둡니다.@Component
public class ReqCommentArlmResolverRegistry {
private final EnumMap<ReqCommentType, EnumMap<ReqCommentEvent, ReqCommentAlrmResolver>> registry;
public ReqCommentArlmResolverRegistry(List<ReqCommentAlrmResolver> resolvers) {
registry = new EnumMap<>(ReqCommentType.class);
// 등록된 모든 전략을 순회하며 EnumMap에 매핑
for (ReqCommentAlrmResolver r : resolvers) {
registry.computeIfAbsent(r.supportsType(), t -> new EnumMap<>(ReqCommentEvent.class));
var byEvent = registry.get(r.supportsType());
byEvent.put(r.supportsEvent(), r);
}
}
// 타입과 이벤트에 맞는 전략을 O(1)로 반환
public ReqCommentAlrmPayload resolve(ReqCommentType type, ReqCommentEvent event, ReqCommentModel c) {
return registry.get(type).get(event).resolve(c);
}
}실제 알림 발송을 통제하는 ReqCommentAlrmService에서는 if-else 분기문이 사라졌습니다.
@Service
@RequiredArgsConstructor
public class ReqCommentAlrmService {
private final AlrmService alrmService;
private final ReqCommentArlmResolverRegistry registry;
private void send(ReqCommentEvent event, ReqCommentModel comment) {
ReqCommentType type = ReqCommentType.fromCode(comment.getType());
// Registry에게 알림 데이터(Payload) 조립을 위임 (다형성 활용)
ReqCommentAlrmPayload payload = registry.resolve(type, event, comment);
// 조립된 페이로드로 공통 알림 발송
alrmService.sendDpAlarm(payload.getAlrmType(), null, payload.getAlarmParam(), payload.getReceivers());
}
public void sendCreateCommentAlrm(ReqCommentModel c) {
send(ReqCommentEvent.CREATED, c);
}
}
복잡하게 얽혀있던 분기 로직을 전략 패턴으로 리팩토링하여 각 도메인과 로직의 결합도를 획기적으로 낮출 수 있었습니다. '새로운 신청 도메인'이 생기거나 '댓글 수정/삭제 알림' 등의 요구사항이 추가되더라도 기존 코드를 수정할 필요가 없어졌습니다.ReqCommentAlrmResolver 인터페이스를 구현한 새로운 클래스 하나만 추가하면, 스프링에서 알아서 빈으로 등록하고 Registry가 이를 매핑해 주기 때문입니다. 객체지향 설계의 핵심 원칙을 실무에 적용해 시스템의 유연성을 확보한 뜻깊은 경험이었습니다.