✨ 들어가기
코드에 대한 설계를 자신할 수 있다는 것은 소프트웨어 설계를 잘했다는 이야기이다.
그렇다면, 소프트웨어 설계는 어떻게 잘할 수 있는 것일까...?
바로 이를 도와주는 것이 계층형 설계이다. 지금부터는 계층형 설계가 무엇이고, 어떻게 활용하면 되는지 차근차근 살펴보자.
💡 계층형 설계란..?
위의 예시처럼 소프트웨어를 고치고, 읽고, 테스트하고, 재사용하기 쉬운 코드를 만드는 것이 바로 계층이다.
사실, 각 계층을 정확히 구분하기는 어렵지만 계층을 구분하기 위한 다양한 변수를 찾고, 찾은 것을 기반으로 어떻게 이를 활용해야 하는지 알아내야 한다.
좋은 설계를 위한 신호를 찾는 방법을 입력과 출력으로 나눈 것이다.
- 입력
함수 본문 | 계층 구조 | 함수 시그니처 |
길이 | 화살표 길이 | 함수명 |
복잡성 | 응집도 | 인자 이름 |
구체화 단계 | 구체화 단계 | 인잣값 |
함수 호출 | 리턴값 | |
프로그래밍 언어의 기능 사용 |
- 출력
조직화 | 구현 | 변경 |
새로운 함수를 어디에 놓을지 결정 | 구현 바꾸기 | 새 코드를 작성할 곳 선택하기 |
함수를 다른 곳으로 이동 | 함수 추출하기 | 적절한 수준의 구체화 단계 결정하기 |
데이터 구조 바꾸기 |
이와 더불어 계층형 설계 패턴은 4가지이다.
- 직접 구현 👉 함수 시그니처가 나타내고 있는 문제를 함수 본문에서 적절하게 구체화해야 한다.
- 추상화 벽 👉 인턴페이스를 사용하여 세부 구현을 감춘다.
- 작은 인터페이스 👉 시스템이 커질수록 비즈니스 개념을 나타내는 인터페이스는 작고 강력한 동작으로 구성해야 한다.
- 편리한 계층 👉 소프트웨어를 더 빠르고 고품질로 제공하는 데 도움이 되는 계층에 시간을 투자해야 한다.
패턴 각각에 대해 신호들을 살펴보자.
1️⃣ 직접 구현
function freeTieClip(cart){
let isTie = false;
let isTieClip = false;
for(let i = 0 ; i < cart.length ; i++){
const item = cart[i];
if(item.name === "tie"){
isTie = true;
}
if(item.name === "tie clip"){
isTieClip = true;
}
}
if(isTie && !isTieClip){
const tieClip = make_item("tie clip",0);
return add_item(cart, tieClip);
}
return cart;
}
단순한 기능이다. 넥타이나 클립이 있는지 확인하여 넥타이 클립을 추가하거나 기존의 cart를 반환하는 함수이다.
하지만, freeTieClip에서는 장바구니가 할 일만 하면되는데, 그 안에서 제품 확인을 시도하고 있다.
해당 코드의 계층 구조를 도식화 한 그림이다.
그래서 먼저 제품 여부 확인하는 함수를 따로 분리해보자.
function isInCart(cart, name){
for(let i = 0 ; i < cart.length ; i++){
const item = cart[i];
if(item.name === name){
return true;
}
}
return false;
}
function freeTieClip(cart){
let isTie = isInCart(cart, "tie");
let isTieClip = isInCart(cart, "tie clip");
if(isTie && !isTieClip){
const tieClip = make_item("tie clip",0);
return add_item(cart, tieClip);
}
return cart;
}
반복문을 추출하여 isInCart라는 제품 확인 함수를 만들었다.
이에 대한 추상화 도식화를 해보면,
한 단계 아래의 계층도 분석해보자.
해당 계층에서 새로운 함수를 추가해보면 어떻게 될까..?
추가할 아이템 제거 함수는 다음과 같다.
function remove_item_by_name(cart, name){
let idx = null;
for(let i = 0; i<cart.length ; i++){
if(cart[i].name === name) idx = i;
}
if(idx !== null)
return removeItems(cart, idx,1);
return cart;
}
이에 대한 계층은 아래와 같다.
앞선 freeTieClip( )함수 옆에 remove_item_by_name( )그래프를 붙인다면, 어느 계층으로 들어가야 할까?
freeTieClip( )위의 계층, 동일한 계층, 사이의 계층, 가장 낮은 계층, 가장 낮은 계층 밑의 새로운 계층
후보는 5가지이다.
생각해보면 답은 쉽다.
remove_item_by_name( )과 freeTieClip( )는 for loop, array index 계층을 공유하고 있다.
그리고 removeItems( )와 add_element_last()는 동일한 계층임을 알 수 있다.
따라서 이를 합친 도식화는 아래와 같다.
추가 팁은 함수 이름으로 함수가 어느 곳에 위치할지 결정하기 위한 정보로 쓸 수 있다는 것이다.
이를 통해 알 수 있는 것은 같은 계층에 있는 함수는 같은 목적을 가져야 한다는 사실이다.
잘 나뉘어진것 같지만 한 가지 불편한 사실이 있다.
remove_item_by_name에서 2단계 아래의 계층에 접근하게 되었다는 것이다.
우리는 이를 통해 한 번더 계층화를 진행해야 함을 알 수 있게 되었다.
function indexOfItem(cart, name){
for(let idx= 0 ; idx< cart.length;idx++){
if(cart[idx].name === name)
return idx;
}
return null;
}
function remove_item_by_name(cart, name){
let idx = indexOfItem(cart,name);
if(idx !== null)
return removeItems(cart, idx,1);
return cart;
}
for loop와 array index를 사용하는 indexOfItem 함수를 따로 작성하여 계층을 분리하였다.
이를 통해 개선된 함수 모식도이다.
이렇게 직접 구현 패턴을 사용하면, 특정 구체화 단계에 집중할 수 있고, 함수를 추출하여 더 일반적인 함수로 만들수도 있다.
더불어 함수를 빼내면서 indexOfItem( )과 같이 재사용할 수 있는 함수들이 만들어진다.
2️⃣ 추상화 벽
추상화벽은 세부 구현을 감춘 함수로 이루어진 계층이다. 이 말은 추상화 벽에 있는 함수를 사용할 때는 구현을 몰라도 함수를 쓸 수 있다는 것이다. 앞선 예제에서 remove_item_by_name 안에서 호출되는 indexOfItem( )의 내부를 구조를 해당 함수는 알 필요가 없다.
그저 입력에 대한 출력값만 사용하면 되는 것이다.
흔하게 사용하고 있는 라이브러리나 API를 추상화 벽이라고 이해할 수도 있다.
하지만, 모든 곳에 추상화벽을 사용하는 것보다는 기준을 가지고 사용하는 것이 좋다.
아래의 4가지 이유로 사용한다면 추상화 벽이 제 역할을 할 수 있을 것이다.
- 쉽게 구현을 바꾸기 위해 사용한다.
- 구현에 대한 확신이 없는 경우, 추상화 벽을 사용하면 구현을 간접적으로 사용할 수 있다.
- 예를 들면, 서버에서 데이터를 받아서 처리해야 하지만, 준비가 되지 않아 임시 데이터를 사용해야 하는 경우 좋다.
하지만, 만약을 대비해 코드를 만드는 습관은 지양해야 한다.
- 코드를 읽고 쓰기 쉽게 만들기 위해 사용한다.
- 세부적인 것을 신경쓰지 않기 위해 사용한다.
- 예를 들면, 초깃값을 정확히 입력했는가..? 등과 같은 세부내용은 감춰지게 된다.
- 팀 간에 조율해야 할 것을 줄이기 위해 사용한다.
- 주어진 문제에 집중하기 위해 사용한다.
- 해결하려는 문제의 구체적인 부분을 무시할 수 있다.
잊지 말아야 할 것은 추상화 단계의 상/하위의 코드들은 서로 의존하지 않아야 한다는 것이다.
어느 부분을 추상화 할 지는 개발자의 몫이다.
3️⃣ 작은 인터페이스
작은 인터페이스는 새로운 코드를 추가할 위치에 관한 것이다.
인터페이스를 최소화 할 수록 하위 계층에 불필요한 기능이 쓸데없이 커지는 것을 막을 수 있다.
예제를 통해 이해해보자.
만약, 장바구니의 품목들의 합이 10,000원 이상이고 사과를 담았다면 사과를 10% 할인해주는 이벤트를 진행한다고 하자.
이를 구현하는 위치는 추상화 벽이거나, 추상화 벽 위에 있는 계층 둘 중 하나이다.
// 👇 추상화 벽에 만들기
function getAppleDiscount(cart){
let total = 0;
const cart_names = Object.keys(cart);
for(let i = 0 ; i < cart_names.length ; i++){
const item = cart[cart_names[i]];
total += item.price;
}
return total > 10000 && cart.hasOwnProperty("apple");
}
// 👇 추상화 벽 위에 만들기
function getAppleDiscount(cart){
const total = calTotal(cart);
const isApple = isInCart("apple");
return total > 10000 && isApple;
}
코드로 살펴본 결과, 어느 위치가 더 좋아보이는가..?
추상화 벽 위에 만든 것이 좋아보인다.
...
왜?
...
첫 번째 방식으로 진행한다면, 시스템의 하위 계층 코드가 늘어나기 때문에 변경에 취약한 코드되어서 추상화 벽의 장점을 악화시키게 된다.
하지만, 두 번째 방식을 사용한다면, 위의 문제점이 발생할 가능성이 낮아진다.
추상화 벽을 작게 만들어야 하는 이유를 알 수 있다.
- 추상화 벽에 코드가 많을수록 구현이 변경되었을때, 수정해야 할 것이 많아진다.
- 추상화 벽에 있는 코드는 낮은 수준의 코드이기에 더 많은 버그가 존재할 수 있다.
- 낮은 수준의 코드는 이해하기 어렵다.
실전에서 이를 적용하는 것이 중요하지만, 실제로 이상적인 모습을 보여주는 곳을 드물다.
따라서 현실적으로 작게 나누려는 노력을 중요하게 여기면서 함수를 작성하는 태도가 중요할 것이다.
4️⃣ 편리한 계층
앞서 살펴본 3가지 패턴과 다르게 현실적이고 실용적인 측면을 가진 방법이다.
단순하다.
지금 작성하고 있는 코드가 편리한가요? 그렇지 않다면 설계를 진행하세요. 편리하다고 느낀다면 설계를 멈추어도 된다는 것입니다.
편리한 계층을 생각하며 개발에 임하다보면, 코드에 대한 설계를 언제 멈추어야 할 지 알 수 있을 것이다.
🌈 마무리하며
추상화 벽 패턴을 사용한다면 세부적인 것을 숨길 수 있기 때문에 구현하는 목적에만 집중해서 개발을 진행할 수 있을 것이다.
어떠한 기능을 개발할 때, 함수 내부의 기능들을 단위별로 쪼개면서 모식도로 이를 계층화 하는 방법을 연습하는 것이 필요해 보인다.
더불어, 호출 그래프 구조를 통해 규칙을 얻고, 해당 규칙을 가지고 테스트를 진행하는 방식을 쉽게 알아낼 수 있을 것으로 생각된다.
'WEB > Insight' 카테고리의 다른 글
FEConf 2024 - Lightning Speaker를 통해 (2) | 2024.09.01 |
---|---|
모델 계층으로 유연하게 컴포넌트 관리하기 ( feat.FEW ) (1) | 2024.08.11 |
[ 쏙쏙 들어오는 함수형 코딩 ] Function 액션/계산/데이터 (0) | 2023.12.06 |
[ 좋은 코드, 나쁜 코드 ] 가독성 높은 코드를 작성하라 (0) | 2023.11.02 |
CORS (0) | 2023.07.09 |