프로젝트 마감 1주일 정도를 앞두고, 기존에 보류했던 ‘알림’ 기능을 급하게 구현해야하는 상황에 놓였습니다. 그러나 일부 데이터 검증 로직에 의해 다른 도메인과의 의존성 문제가 발생하여 아키텍처를 훼손할 위험이 존재했었습니다. 이글에서는 제가 기존코드를 가능한 오염시키지 않으면서 기능을 빠르게 구현하고, 추후 리팩토링까지 고려한 임시 코드 작성 전략에 대해 이야기 하려고 합니다.
문제 상황
알림 기능의 핵심 로직은 eventEmitter를 통해 요청을 받아 알림 점보를 DB에 생성하는 것입니다. 하지만 DB에 데이터를 생성할때 외래키 제한에 관한 오류가 발생하는것을 미연에 방지하기 위해서 유효성 검사를 진행해야 합니다.
그러나 유효성 검증에 필요한 데이터가 모두 다른 팀원이 담당하는 외부 도메인(User,Post등)에 존재한다는 점이었습니다.
제가 고려했던 선택지는 아래와 같은 한계가 있었습니다.
- 외부 도메인 직접 수정 : 담당자가 아니므로 PR시 충돌(Conflict)이 발생할 가능성이 높고, 일부 도메인은 아키텍처 구조 위반으로 리팩토링이 시급한 상태여서 코드 추가 시 예상치 못한 부작용(Side Effect)가 발생할 위험이 존재했습니다.
- 도메인 담당자에게 검증 함수 추가 요청 : 각 담당자에게 유효성 검증 함수를 요청하고, 리뷰를 받고, 머지를 하는 과정은 남은 1주일의 데드라인을 초과할 가능성이 높았습니다.
- Alarm 도메인에서 외부 도메인 DB 직접 조회 : 당장은 가장 빠르고 이상적인 방법이라고 생각될수도 있으나, 도메인 분리 원칙을 위배하여 아키텍처를 훼손하고 추후 유지보수를 극도로 어렵게 만드는 최악의 선택지였습니다.
해결책
고민 끝에, ‘의도적으로 기술 부채를 생성하되, 명확하게 격리하고 관리하기’라는 전략을 선택했습니다.
alarm.temp.repository.js
생성 : Alarm 도메인 내부에 외부 도메인의 DB 테이블에 접근하는 임시 레포지토리 파일을 만듭니다. 이 파일의 목적은 오직 ‘외부 도메인의 테이블로부터의 유효성 검증’으로 한정합니다.- 서비스 레이어에서 임시 함수 호출 : Alarm 서비스 로직에서는 이 임시 레포지토리의 검증 함수를 호출하여 유효성 검사를 수행합니다.
- 향후 리팩토링 계획 수립 : 데드라인 및 시연 이후, 각 도메인 담당자에게 정식 검증 함수 작성을 요청합니다.
- 임시 코드 제거 : 정식 함수가 각 도메인에 구현되면, 서비스 레이어의 호출코드를 정식 함수로 교체하고,
alarm.temp.repository.js
파일은 완전히 제거합니다.
이 방법을 통해서 아래와 같은 이점을 얻을 수 있습니다.
- 핵심 로직 보호 : 기존 Alarm 도메인의 핵심 로직을 거의 오염시키지 않고, 신규 기능을 빠르게 구현할 수 있습니다.
- 리팩토링 용이성 : 임시 코드가 별도의 모듈로 명확히 분리되어 있어, 추후 수정시 대규모 코드 변경 없이 해당 파일만 교체하면 되므로 리팩토링에 대한 수고가 줄어듭니다. 현재 상황에서는 위 다이어그램과 같이 임시 레포지토리를 제거하고 다른 도메인의 검증 함수만 연결해주면 됩니다.
구현 코드
임시 레포지토리를 활용하는 서비스 레이어
alarm.service.js
파일에서는 다른 도메인 검증이 필요할 때 alarm.temp.repository.js
의 함수들을 호출합니다.
// alarm/service/alarm.service.js
import {
validateNoticeId,
validatePlanId,
validatePostId,
} from "../repository/alarm.temp.repository.js";
// ... (기존 코드)
export const sendAlarm = async (targetId, type, userId) => {
// ... (기존 유효성 검사 로직 생략)
// 임시 레포지토리의 검증함수 활용
if (
Object.hasOwn(targetId, "noticeId") &&
!(await validateNoticeId(targetId.noticeId))
) {
return eventEmitter.emit(
"ERROR",
new InvalidInputValueError("올바르지 않은 입력값입니다. (noticeId)")
);
}
if (
Object.hasOwn(targetId, "postId") &&
!(await validatePostId(targetId.postId))
) {
return eventEmitter.emit(
"ERROR",
new InvalidInputValueError("올바르지 않은 입력값입니다. (postId)")
);
}
if (
Object.hasOwn(targetId, "planId") &&
!(await validatePlanId(targetId.planId))
) {
return eventEmitter.emit(
"ERROR",
new InvalidInputValueError("올바르지 않은 입력값입니다. (planId)")
);
}
// ... (이하 생략)
};
격리된 임시 레포지토리
이 파일 상단에는 이 코드가 임시적이며, 왜 만들어졌고, 이후 어떻게 처리되어야하는지에 대한 명확한 경고 주석을 추가하여 다른 팀원들도 해당 파일이 임시 파일임을 바로 인지하도록 하였습니다.
/**
* @Warning : 이 파일은 임시 파일 입니다. 추후 리펙토링 시 원래 도메인에 맞게 다시 작성해야합니다.
* 이곳에 포함된 함수는 모두 다른 도메인의 DB를 직접 참조하고 있습니다.
* 따라서 시현 이후 교체가 필요합니다.
*/
import { prisma } from "../../db.config.js";
// Notice 도메인의 notices 테이블을 직접 조회
export const validateNoticeId = async (noticeId) => {
const notice = await prisma.crewNotice.findUnique({
select: {
id: true,
},
where: {
id: noticeId,
},
});
if (!notice) {
return false;
}
return true;
};
// Post 도메인의 posts 테이블을 직접 조회
export const validatePostId = async (postId) => {
const post = await prisma.crewPost.findUnique({
select: {
id: true,
},
where: {
id: postId,
},
});
if (!post) {
return false;
}
return true;
};
// Plan 도메인의 plans 테이블을 직접 조회
export const validatePlanId = async (planId) => {
const plan = await prisma.crewPlan.findUnique({
select: {
id: true,
},
where: {
id: planId,
},
});
if (!plan) {
return false;
}
return true;
};
결론
기술 부채도 계획적으로!
소프트웨어 개발에서 아키텍처 원칙을 지키는것이 제일 중요하긴하나 실무에서는 여러가지 요인들(특히 데드라인)로 인해 지키는것이 어려운 경우도 많습니다. 그러나 위와 같은 방법으로 컨트롤 가능한 기술부채를 만들어 냄으로써 아키텍처 원칙과 비즈니스적 목표의 균형을 어느정도 잡을 수 있을 것으로 생각합니다. 무작정 원칙을 어기는 것이 아니라 기술 부채를 의도적으로 만들고, 이를 다시 해결할 계획을 세우는 전략은 실무에 유연하게 대처할 수 있는 개발자가 되기위한 길이라고 생각합니다.