[Week 1-2] 비즈니스 로직을 제압하는 개발자가 퇴근을 한다
2-1) 지옥으로 가는 길은 "프론트에서 해드릴게요"로 포장되어 있다
“지옥으로 가는 길은 선의로 포장되어 있다”
- 서양 속담
프론트 엔드 개발자라면 클릭을 하지 않을 수 없는 소제목이었다.
프론트에서 해드릴게요? 협업에 관한 이야기인가 워크에씩에 관한 이야기인가?
위의 한 문장을 단 하나의 상황으로 완벽히 이해시켜 주셨다.
< 프로젝트 진행 중 >
FE : BE님 상품 목록 조회 API는 언제까지 가능하세요?
BE : 아.. 이게 처음 해보는 거라 시간이 좀...
FE : 음 그럼 다른 거 먼저 하시고 해당 부분은 프론트에서 할게요.
BE : 앗 감사합니다 ㅎㅎ
사실 처음 예시를 들어주셨을 때 뭐가 문제인지 크게 느껴지지 않았다.
그동안 진행했던 개인/팀 프로젝트들은 백엔드 없이 프론트로만 작업을 했으므로 당연했을지도 모른다.
Node를 배우고도 SQL문을 사용해서 WHERE... JOIN... 하는 것보다 filter + sort + map 하는 것이 손에 익어있고 편했으니 말이다.
그래서 풀스택 환경에서 백에서 데이터를 어느 수준까지 파싱하고 필터링해서 넘겨줄지 혹은 데이터를 뭉탱이로 받아와서 프론트에서 처리해야 하는지 기준이 서지 않았다. (랜더링 시간이 더 걸리지 않을까? 데이터를 더 많이 소비하지 않을까?)
const Products = () => {
const allProducts = useProducts(); // 상품 관련 커스텀 훅
const user = useUser(); // 유저의 정보와 관련된 커스텀 훅
const isCoat = product.type === "coat";
const isAutumn = dayjs().month() >= 9 && dayjs().month() <= 11;
const isRecommended = isCoat && user.gender === "female" && isAutumn;
// 코트 상단 노출을 위해 분리 해줌
const { coats, products } = allProducts.reduce((acc, product) => {
if (product.type === "coat") {
return { ...acc, coats: acc.coats.concat(product) } }
}
return { ...acc, products: acc.products.concat(product) } }
}, { coats: [], products: [] })
return (
<>
{isRecommended
? <>
{[coats, products].flatMap((product) => <ProductCard product={product} />)}
</>
: allProducts((product) => <ProductCard product={product} />)}
}
</>
)
}
const ProductCard = ({ product, isRecommended }) => {
const price = isRecommended ? product.price * 0.95 : product.price;
return /** 상품 카드 관련 코드 */
}
설명을 위해 추천 로직 코드를 보여주셨는데 익숙하다 한눈에 봐도 나랑 닮아있다는 생각이 들었다.
그렇다면 뭐가 문제였을까...
1. 추천 상품에 대한 책임이 Products와 ProductCard 양쪽으로 분산되어 관리 포인트가 늘어났다. 만약 추천 대상이나 조건이 변경될 경우 두 컴포넌트 모두 변경의 대상이 된다.
2. Products의 임무는 상품 목록을 랜더링 하는 것인데 서버에서 받아와 뿌려주는 단순한 역할을 할수록 좋은 컴포넌트이다. 따라서 '추천'에 대한 로직의 분량이 너무 많이 차지하고 있다.
3. 추가로 앱에서도 같은 로직을 구현한다면 로직을 재작성해야 하므로 동일한 문제가 발생한다.
4. 핵심 비즈니스 로직이 외부로 노출되면서 프론트엔드 측 요청을 조작하여 할인 상품을 변경할 수 있어 보안에 문제가 발생할 수 있다.
const Products = () => {
const products = useProducts();
return products.map((product) => <ProductCard product={product} />)
}
const ProductCard = ({ product }) => {
return (
<li>
<span>정상가: {product.price.original}</span>
<span>할인가: {product.price.discounted}</span>
</li>
)
}
백에서 작업해야 할 부분을 다이어트 한 코드이다.
우선 프론트에서 유저 정보를 확인할 필요가 없고 추천 상품에 대한 로직도 없어졌다. 또한 products를 받아올 때 추천 상품을 가장 먼저 받아오기 때문에 알아서 상단에 노출이 되며 가격 또한 할인이 적용이 된 후 넘어오기 때문에 할인 로직을 작성할 필요가 없어져 보안 위험도 사라졌다.
헥사고날 아키텍처에서는 사용자 인터페이스나 데이터베이스 모두 비즈니스 로직으로부터 분리해야 하는 외부 요소로 취급한다. 이는 비즈니스 로직이 외부 요소에 의존하지 않고 프레젠테이션 계층과 데이터 소스 계층이 도메인 계층에 의존하도록 만들어야 한다는 것이다.
그렇기 때문에 의존성을 낮추어 업데이트에 유연성을 가지게 되고 비즈니스 로직을 독립적으로 테스트할 수 있어 유지보수성에 장점을 갖게 된다.
https://speakerdeck.com/soyoung210/heonjibjulge-saejibdao-riaegteu-peurojegteu-gujojojeong
2-2) 비즈니스 로직이란?
비즈니스 / 도메인 로직과 UI / 프레젠테이션 로직에 대해 알아보았는데 간단하게 구분이 가능한 로직이 있는 반면 아직 판단하기에 애매한 로직들도 많다.
비즈니스로직과 UI로직을 구분할 기준은 도메인 모델 수정 여부이다.
- 헥사고날 아키텍쳐
그게 뭔데요?
비즈니스 로직
1. 첫 구매 고객에게 총금액의 10%를 할인해 준다.
-> 현재 로그인되어 있는 계정의 구매 이력을 확인한 후 없을 경우 할인 적용
2. 호텔 예약 웹사이트에서 선택한 가격대의 호텔만 출력되도록 한다.
-> 해당 날짜의 호텔을 모니터링하고, 고객이 설정한 가격대에 일치하는 호텔들만 표시
3. 음악 스트리밍 서비스에서 사용자가 즐겨 듣는 음악을 기반으로 추천 음악 플레이리스트 생성
-> 그동안 들었던 음악을 기록하고 분석하여 비슷한 음악을 찾아 추천해 주는 로직
UI 로직
1. 쇼핑몰 웹사이트에서 <더 보기> 버튼 클릭 시 상품이 추가로 10개가 더 로드
-> <더 보기> 버튼 클릭 시 해당 섹션의 DOM을 업데이트
2. 갤러리 웹사이트에서 이미지 클릭 시 해당 이미지에 대한 소개 팝업 출력
-> 이미지 클릭 시 이벤트를 감지하여 팝업창을 렌더링
3. 설문조사 폼을 작성하고 <제출> 버튼 클릭 시 작성 내용이 전달
-> 폼 데이터를 수집하고 서버로 전송하여 처리 요청을 보내는 HTML 폼 기능
예시를 몇 가지 확인해 보니 어느 정도 감이 오게 된다.
명확하게 설명할 수는 없지만 대략 이런 기능을 하면 이거고 단순하게 이런 기능을 하면 이거야라곤 말할 수 있을 것 같다.
그래서 왜 구분해야 하는 건데용?
한마디로 말하자면 내가 만든 뚱뚱한 컴포넌트 혹은 함수를 다이어트시키는데 필요다하고 할 수 있다.
위에서 예시를 봤듯이 상품을 출력하는 컴포넌트에 다른 로직을 잔뜩 집어넣고 하나의 컴포넌트에서 추가적인 요구사항들을 계속해서 작성하니 보기 좋지도 않고 먹기에도 좋지 않은 코드가 되어버리는 것이다.
카레를 주문하고 추가로 마라탕을 주문했다고 카레가 담긴 냄비에 마라탕을 조리하면 안 된다.
https://medium.com/@junep/프론트엔드-아키텍처-business-logic의-분리-adc10ae881ab
2-3) [실전 스킬 2] 물과 기름처럼 비즈니스 로직 격리하기
안타깝지만 카레와 마라탕이 섞이면 비가역적인 형태가 되지만 다행히도 우리의 코드는 카레마라탕에서 카레와 마라탕을 분리할 수 있다.
하지만 분리도 잘해야 하는 것이 나름 분리한다고 열심히 했는데 분모자가 들어간 카레 혹은 카레향이 나는 마라탕이 될 수도 있기 때문이다.
그렇기 때문에 로직 격리를 위해 추상화와 모듈성을 기억해야 한다. 소프트웨어공학, 데이터베이스에서 봤던 추상화, 모듈화, 결합도, 응집도, 정규화... 등 떠오르게 한다.
컴퓨터 과학에서 추상화(abstraction)는 복잡한 자료, 모듈, 시스템 등으로부터 핵심적인 개념 또는 기능을 간추려 내는 것을 말한다.
컴퓨터 과학에서 모듈성은 컴퓨터 프로그램의 한 속성으로, 모듈이라고 불리는 것으로 서로 분리되어 작성되는 성질이다.
- 위키백과
추상화는 코드를 일반화된 형태로 개념화하는 과정이다. 해당 코드가 무엇을 하는지 그리고 왜 필요한지 우선 파악해야 할 것이다.
좋은 추상화란 코드 뒤에 숨은 의도를 파악하고 쓰임새에 맞도록 적절한 단위와 형태를 만들어 나가는 과정
입니다.
많이 알고 있는 디자인 패턴 중 MVC 패턴의 경우 데이터와 비즈니스 로직이 담겨있는 Model, 화면을 처리하는 View, 모델과 뷰로 명령을 전달하는 Controller로 구성되어 있다.
이렇게 구분해서 관리하는 것이 재사용성과 확장성에 좋기 때문에 적용하고 있는 것이다. 그렇기에 추상화하고자 하는 코드가 어떤 기능을 하는지 알고 있어야 한다.
추상화를 위한 증가함수 예시
function incrementQuantity(item: Item) {
const quantity = item.quantity;
// quantity 업데이트를 위한 newQuantity 선언과 할당
const newQuantity = quantity + 1;
// 기존 item.quantity를 newQuantity(quantity + 1)로 업데이트
const newItem = objectSet(item, 'quantity', newQuantity);
// 업데이트 된 객체를 return
return newItem;
}
위 코드방식으로 계산기 애플리케이션을 구현한다면 우선 함수를 여러 개 만들어야 할 것이다. (더하기 함수, 빼기 함수, 곱하기 함수, 나누기 함수...)
이 상황에서 추상화를 고려해 리펙터링을 한다면 매개변수 Num1과 Num2를 받아 연산을 한 후 업데이트한다는 공통점을 찾을 수 있다.
다시 예시 코드로 돌아와서 공통점을 뽑아내 update라는 함수로 만든다면 incrementField / decrementField 함수들을 보다 간결하게 작성할 수 있다.
function update(
object: Record<string, any>,
key: string,
modify: (value: any) => any
) {
const value = object[key];
const newValue = modify(value);
const newObject = objectSet(item, key, newValue);
return newObject;
}
function incrementField(item: Item, field: string) {
return update(item, field, function (value) {
return value + 1;
});
}
추상화를 좀 더 쉽게 이해하기 위해 캡슐화에 대해 알아볼 필요가 있다.
캡슐화(영어: encapsulation)는 객체 지향 프로그래밍에서 다음 2가지 측면이 있다
객체의 속성(data fields)과 행위(메서드, methods)를 하나로 묶고, 실제 구현 내용 일부를 내부에 감추어 은닉한다.
- 위키백과
간단하게 1학년 3반 교실(캡슐)엔 3반 학생과 3반 담임선생님만 있는 게 좋다는 것이다. 3반 학생이 다른 반 여기저기를 돌아다닌다면 4반 학생수는 +1이 될 것이고 3반 학생수는 -1이 될 것(무결성)이다. 또한 3반엔 담임선생님이 존재하므로 안전하지만 교실을 벗어난다면 위험한 상황(보안)이 발생할 수도 있다.
const ToggleButton = () => {
const [isActive, setIsActive] = useState(false);
const toggle = () => {
setIsActive(!isActive);
};
return (
<button onClick={toggle}>
{isActive ? 'Active' : 'Inactive'}
</button>
);
};
// 캡슐화 후
const ToggleButton = () => {
const [isActive, setIsActive] = useState(false);
const toggle = () => {
setIsActive(!isActive);
};
return <ButtonDisplay isActive={isActive} toggle={toggle} />;
};
const ButtonDisplay = ({ isActive, toggle }) => {
return (
<button onClick={toggle}>
{isActive ? 'Active' : 'Inactive'}
</button>
);
};
https://fe-developers.kakaoent.com/2022/221020-component-abstraction/
https://smartstudio.tech/bringing-consistency-to-broken-ui-layer/
📝 오늘의 3줄 요약
1. 비즈니스 로직과 UI로직을 구분해야 좋은 코드가 된다.
2. 그렇기 위해선 추상화와 캡슐화에 대한 선행이 필요하다.
3. 코드의 다이어트와 벌크업 판단을 도와주는 인바디 기계가 갖고 싶다.