MSA 와 BFF 아키텍처 설명 부분에 대한 문의
강의 2강에 대한 노트에 대한 질문입니다.
💡[CS 및 실무 적용 Point: MSA와 BFF 아키텍처] 현대의 마이크로서비스 아키텍처(MSA)에서 Node.js는 특히 BFF(Backend For Frontend) 패턴의 핵심 기술로 사용됩니다. 브라우저의 DOM을 다루던 언어적 특성을 살려 클라이언트(React, Vue 등)의 요청을 쉽게 파싱하고, 이식된 운영체제 제어 능력(http, fs)을 통해 백엔드의 여러 마이크로서비스(Java, Python 등)로 비동기 네트워크 요청을 뿌린 뒤, 그 결과를 모아 빠르게 클라이언트에게 반환하는 완벽한 미들웨어 역할을 수행합니다.
해당 설명에서 BFF로 NodeJs를 유용한 이유는 I/O 입출력 특출나기 때문에, BFF로 사용하면, API 요청을 받아서 마이크로 서비스에 통신 요청을 보내는 용도로 사용한다는 건가요?
한번 더 거쳐가면 더 성능이 안 좋은 거 아닌가요?
아니면 원래 SpringBoot로 개발되었던 웹 사이트를 NodeJs로 구현한다면 더 좋다는 의미일까요?
Answer 1
0
안녕하세요 윤수혁 (나무펜)님, 남겨주신 질문을 보니 강의에서 의도했던 아키텍처의 고민 지점을 아주 날카롭게 짚어주셨네요. 정말 좋은 질문입니다. 질문해주신 내용들을 보면, 마이크로서비스 구조에서 중간에 Node.js가 하나 더 끼어들면 통신 과정이 길어져 성능이 떨어지지 않을지, 그리고 기존의 무거운 백엔드를 전부 Node.js로 대체하라는 의미인지 두 가지 지점에서 혼동이 오셨을 텐데 이 부분을 명확하게 짚어보겠습니다.
먼저 강의에서 언급한 대로 Node.js를 마이크로서비스에 통신 요청을 보내는 용도로 사용하는 것이 맞는지 물어보신 부분은 정확하게 이해하신 것이 맞습니다. Node.js의 논블로킹 I/O 모델은 뒤쪽의 여러 서버로 수십 개의 네트워크 요청을 동시에 던져놓고 기다리는 작업에 최적화되어 있기 때문입니다. 스레드를 무겁게 생성하지 않고 비동기적으로 처리하므로, 수많은 클라이언트의 요청을 받아 뒷단의 마이크로서비스들로 가볍게 토스해 주는 가벼운 미들웨어, 즉 라우터 및 애그리게이터 역할에 이보다 더 좋은 선택지가 없습니다.
// Node.js 18+ 네이티브 fetch 기준
app.get('/api/dashboard', async (req, res) => {
const [users, orders] = await Promise.all([
fetch('http://internal-user-service/users'),
fetch('http://internal-order-service/orders')
]);
res.json({ users, orders });
});
위 코드는 Node.js가 어떻게 여러 백엔드 서버에 동시에 요청을 보내는지 보여주는 아주 간단한 예시입니다. 자바스크립트의 비동기 처리 방식인 Promise.all을 사용하면 유저 정보와 주문 정보를 가져오는 두 개의 독립적인 네트워크 요청이 동시에 출발하게 됩니다. 만약 하나의 응답이 올 때까지 스레드가 멈춰서 기다려야 했다면 사용자는 두 배의 시간을 기다려야 했겠지만, Node.js는 스레드를 멈추지 않고 병렬로 처리한 뒤 결과가 모두 도착하면 하나의 JSON으로 묶어 클라이언트에게 반환합니다.
여기서 중간에 한 번 더 거쳐가면 네트워크 구간이 추가되어 지연이 발생하고 성능이 더 안 좋아지는 것은 아닌지 합리적인 의문이 들 수 있으며, 이 부분이 가장 혼동하기 쉬운 지점입니다. 단순하게 거치는 횟수가 늘어난다는 점만 보면 우려하실 수 있지만, 실제 대규모 서비스 환경에서는 이것이 오히려 성능을 압도적으로 끌어올리는 핵심 장치가 됩니다. 이를 직관적으로 이해하기 위해 규모가 큰 고급 레스토랑을 상상해 보시면 좋습니다. 손님이 스테이크와 샐러드, 그리고 수프를 동시에 먹고 싶을 때 웨이터가 없다면 손님이 직접 뜨거운 열기가 가득한 주방으로 들어가 고기 굽는 파트, 샐러드 파트, 수프 파트를 일일이 돌아다니며 음식을 받아와야 할 것입니다.
// Node.js 18+ 네이티브 fetch 기준
async function fetchMyPageData() {
const user = await fetch('https://api.domain.com/users/me');
const orders = await fetch('https://api.domain.com/orders/recent');
const cart = await fetch('https://api.domain.com/cart/summary');
}
이 코드는 앞선 비유에서 웨이터가 없을 때, 즉 프론트엔드에서 일어나는 상황을 보여줍니다. 손님에 해당하는 스마트폰 기기는 배터리 소모에 민감하고 외부의 5G나 LTE 네트워크는 내부망에 비해 속도가 느리고 불안정합니다. 그런데 클라이언트가 유저 정보, 주문 내역, 장바구니 데이터를 얻기 위해 이 느리고 불안정한 외부 인터넷망을 세 번이나 왕복해야 합니다. 이렇게 되면 화면이 그려지는 데 시간이 훨씬 오래 걸리고 사용자는 화면이 버벅거린다고 느끼게 됩니다.
여기서 주방의 각 파트들은 뒤쪽에 있는 마이크로서비스들이며, 부지런한 웨이터가 바로 Node.js로 만든 BFF 서버입니다. 부지런하고 능숙한 웨이터인 BFF가 있다면 클라이언트는 불안정하고 먼 길을 여러 번 오갈 필요 없이 무조건 단 한 번만 요청을 보내면 됩니다.
// 클라이언트 측 코드 (외부망 1회 호출)
async function fetchMyPageBFF() {
const myPageData = await fetch('https://api.domain.com/bff/mypage');
}
// BFF 서버(Node.js) 측 코드 (내부망 병렬 호출, Node.js 18+ 네이티브 fetch 기준)
app.get('/bff/mypage', async (req, res) => {
const [user, orders, cart] = await Promise.all([
fetch('http://10.0.1.12:8080/users/me'),
fetch('http://10.0.1.13:8080/orders'),
fetch('http://10.0.1.14:8080/cart')
]);
res.json({ userInfo: user, recentOrders: orders, cartSummary: cart });
});
위 코드를 보시면 클라이언트는 복잡한 세 번의 호출 대신, BFF 서버로 단 한 번의 호출만 수행합니다. 요청을 받은 BFF 서버는 AWS VPC와 같이 지연율이 매우 낮은 초고속 내부망에 위치해 있기 때문에, 백엔드 서버들과 아주 빠르게 통신할 수 있습니다. 찰나의 순간에 모은 세 가지 데이터를 이쁘게 하나의 쟁반에 합쳐서 클라이언트에게 딱 한 번만 내려주게 됩니다. 결과적으로 지연이 심한 외부 인터넷 구간을 타는 횟수를 극단적으로 줄여주고, 모바일 기기의 배터리와 데이터 소모까지 아껴주기 때문에 체감 성능이 훨씬 좋아지는 것입니다. 이것이 최근 Next.js 같은 프레임워크가 자체 API 라우트를 두어 BFF 역할을 겸하게 하는 이유이기도 합니다.
그렇다면 두 번째로 혼동하셨던 부분, 즉 기존에 SpringBoot로 개발되었던 무거운 웹 사이트를 통째로 Node.js로 바꾸는 것이 더 좋다는 의미인지에 대해 짚어보겠습니다. 현업에서는 결코 그렇게 접근하지 않으며, 실무에서 SpringBoot와 Node.js는 서로를 밀어내는 경쟁자가 아니라 철저한 분업 파트너입니다. 앞선 레스토랑 비유로 치면 SpringBoot는 무거운 요리를 책임지는 메인 셰프와 같습니다. 복잡한 트랜잭션 관리나 무거운 비즈니스 로직 연산, 그리고 데이터베이스와의 안정적인 연결에 있어서 Java와 SpringBoot는 여전히 대체 불가능한 강력함을 가집니다. 따라서 결제, 주문, 정산 같은 무거운 코어 도메인은 계속해서 SpringBoot로 단단하게 구축하게 됩니다.
대신 Node.js는 프론트엔드와 맞닿은 최전선에 배치하여 홀 전체를 관리하게 합니다. 화면 UI가 바뀔 때마다 API 응답 형태도 계속 바뀌어야 하는데, 프론트엔드 개발자가 백엔드의 SpringBoot 개발자에게 매번 이 필드를 추가해 달라거나 저 필드를 빼달라고 부탁하게 되면 심각한 개발 병목이 생길 수밖에 없습니다. 이때 Node.js를 BFF로 두면 자바스크립트와 타입스크립트에 익숙한 프론트엔드 팀이 직접 BFF 서버의 코드를 수정해 원하는 대로 데이터를 조립해서 가져갈 수 있게 됩니다.
app.get('/bff/mypage', async (req, res) => {
const rawUserData = await fetchUserFromSpring();
const uiFriendlyData = {
displayName: `${rawUserData.firstName} ${rawUserData.lastName}`,
isVip: rawUserData.point > 10000,
hasUnreadAlarms: rawUserData.alarms.length > 0
};
res.json(uiFriendlyData);
});
이 코드를 보시면, 백엔드의 SpringBoot 서버에서 넘겨준 날것의 데이터를 프론트엔드가 화면을 그리기 편한 형태로 직접 변환하고 있습니다. 백엔드 팀은 데이터베이스의 구조나 비즈니스 로직만 신경 쓰면 되고, 프론트엔드 팀은 화면에 필요한 데이터를 BFF에서 직접 조립하면 됩니다. 기술적인 성능 향상뿐만 아니라 팀 간의 소통 비용을 극적으로 줄이고 프론트엔드 팀의 자율성을 극대화하는 이런 조직적인 이유가 실무에서는 아주 크게 작용합니다.
조금 더 피부에 와닿도록 실제 대형 쇼핑몰의 실무 환경을 예로 들어 두 기술이 어떻게 완벽하게 협력하는지 구체적인 상황을 그려보겠습니다.
고객이 장바구니에서 '결제하기' 버튼을 누르는 순간, 백엔드에서는 재고를 정확히 차감하고, 쿠폰 사용 이력을 데이터베이스에 기록하며, 외부 PG사(결제 대행사)와 통신해 금액을 승인받는 매우 무겁고 민감한 작업이 일어납니다. 만약 100명의 고객이 동시에 마지막 남은 한정판 신발 1개를 결제하려고 한다면(동시성 이슈), 단 한 명에게만 결제가 승인되고 나머지 99명은 튕겨내야 합니다. 이처럼 단 1원의 오차도 허용되지 않는 강력한 트랜잭션(Transaction) 관리와 데이터 무결성이 필수적인 '주문/결제 코어 서비스'는 굳건한 생태계를 가진 Java와 SpringBoot로 아주 견고하게 구축합니다.
하지만 결제가 성공적으로 끝난 직후, 사용자에게 보여줄 '주문 완료 영수증 화면'을 렌더링하는 상황을 생각해 볼까요? 이 화면을 예쁘게 그리려면 방금 SpringBoot가 만들어낸 '주문 번호와 결제 금액'뿐만 아니라, '고객의 남은 적립금(회원 서비스)', '내일 도착 예정 시간(배송 서비스)', 그리고 '함께 사면 좋은 추천 상품(AI 추천 서비스)' 데이터가 동시에 필요합니다. 이때 화면을 담당하는 최전선의 Node.js(BFF)가 등판합니다. Node.js는 뛰어난 비동기 처리 능력을 바탕으로 이 4개의 마이크로서비스에 동시에 데이터 요청을 쫙 뿌리고, 찰나의 순간에 모아온 데이터를 프론트엔드가 요구하는 정확한 규격으로 조립해 모바일 앱으로 딱 한 번만 쏴줍니다. 무거운 비즈니스 로직과 데이터베이스 락(Lock)은 SpringBoot가 전담하고, 수많은 네트워크 I/O와 화면 데이터 조립은 Node.js가 전담하는 완벽한 실무 분업 체계가 완성되는 것입니다.
결론적으로 BFF로서의 Node.js는 기존 백엔드 서버를 걷어내고 그 자리를 차지하는 것이 아닙니다. 클라이언트와 무거운 백엔드 사이에서 네트워크 병목을 줄이고, 프론트엔드 입맛에 맞게 데이터를 예쁘게 가공해 주는 빠르고 똑똑한 프론트 데스크이자 훌륭한 웨이터 역할을 한다고 이해해 주시면 완벽합니다. 참고해주세요!
마지막으로 이렇게 좋은 질문 남겨주셔서 진심으로 감사드립니다. 현업의 아키텍처까지 깊게 고민하시며 공부하시는 모습에 저도 큰 자극을 받으며, 앞으로의 성장에 조금이나마 더 도움이 되고 싶습니다. 혹시 지금 수강 중이신 강의 외에도 저희 '우리동네코딩스튜디오'에서 제공하는 나머지 Node.js Part 2, 3, 4 강의나 React, Next.js 등 다른 강의 중 수강을 희망하시는 과목이 있으시다면 언제든 jeony0535@naver.com으로 편하게 연락 남겨주세요. 치열하게 고민하시며 공부하시는 윤수혁 님께는 제가 드릴 수 있는 최대의 할인을 적용하여 전달해 드리겠습니다. 다시 한번 감사드리며, 즐거운 코딩 되시길 진심으로 응원합니다!
강의 목표
0
5
0
수업자료 링크요청
0
16
1
개념과 격벽에 대한 이해를 팀과 공유하고 합의하는 방법
1
34
2
클라우드 프론트 멀티 오리진의 의미가 정확히 무엇인가요?
0
46
2
강의 노트 위주로 학습
0
28
1
접근 가능한 카트 목록을 조회하는 API는 없는걸까요?
1
47
2
33강 14번 질문 있습니다.
0
46
1
쿠키 생성 관련
0
58
3
프로덕션 부분 사용 불가
0
56
2
JWT auth error invalid token
0
54
2
현재 미니케이스 자동완성 검색창 설계1 강의를 보고있습니다.
-1
59
2
경력기술서는 어떤 방향이 좋을까요?
0
50
2
in-app purchasing 버튼의 install 버튼이 비활성화되어있을때 어떻게 해결해야하나요 ?
0
42
2
마감 인 경우.
0
52
1
addToFile function에서 path를 사용해 새로운 파일을 생성
0
45
1
[46강] EventEmitter를 활용한 10가지 패턴 중 플러그인 아키텍처
0
42
2
강의가 누락된것 같습니다.
0
48
2
용어 발음법이 계속 바뀌는 것 같은데 이런 부분들 개선이 가능할까요...?
0
63
1
call stack 표현이 잘못표현된것이 아닌가요?
0
95
2
혹시 다음 강의 제작 예정된 것들이 있을까요?
0
114
1
1강 질문
0
83
2
ai가 만든 강의인가요?
0
179
1
3강 질문
0
94
2
2강 nodejs 3단계 설명 질문
0
106
1

