본문 바로가기
생각들

소프트웨어 원칙을 소통의 재료로 사용하기 (소프트웨어 개발 3대 원칙)

by Luke K 2023. 12. 26.

소프트웨어를 개발하며 중요하게 생각하는 주제가 있습니다.

의사소통하며 좋은 코드를 만들어가는 경험을 바탕으로 동료들과 신뢰를 만드는 것입니다.

그 과정을 함께하기 좋은 동료가 되기 위해 노력하고 있습니다.

어떻게 하면 근거를 바탕으로 소통할 수 있고 동료들이 좋은 커뮤니케이션을 하고 있다고 느낄 지 고민해봤습니다.

 

개발팀 내의 소통을 위한 좋은 도구중 하나가 소프트웨어 원칙들이라고 생각하고 있습니다.

소프트웨어 원칙들은 코드베이스의 발전을 위한 의견을 낼 때 좋은 근거가 되주는 것을 많이 경험하고 있습니다.

소프트웨어 설계 원칙들은 설득력을 가지게 된 지 시간이 오래 지나 관련 자료들을 찾기도 쉽고 강한 키워드가 되곤 합니다.

 

원칙들의 등장배경을 이해하기 위한 대전제가 있습니다.

소프트웨어는 자주 변합니다.

하드웨어는 A/S하는 경우를 제외하면 한번 판매한 제품이 바뀌지 않습니다.

이와 달리 소프트웨어는 주기적으로 업데이트 됩니다.

주기가 빠른 소프트웨어 서비스의 경우에는 하루에 한번 이상 업데이트 되는 경우도 많습니다.

 

소프트웨어 엔지니어는 자주 업데이트 되는 소프트웨어를 개발을 한다는 특수성을 바탕으로 개발해야합니다.

기존에 있는 코드베이스에 새로운 것을 추가해도 문제가 생기지 않아야합니다.

또 코드양이 점점 늘어난다고 작업속도가 줄어들지 않도록 해야합니다.

 

그런 전제로 재료가 될만한 소프트웨어 원칙들의 등장배경과 지키지 않았을 때의 예시와 원칙을 지켜 개선하는 과정들을 정리해보려합니다.

첫번째 재료는 소프트웨어 개발 3대 원칙입니다.

 

소프트웨어 개발 3대 원칙

KISS (Keep It Short and Simple)

이 원칙은 코드를 짧고 단순하게 관리하라는 원칙입니다.

개발하다보면 복잡한 로직을 구현해야하는 순간들이 생깁니다.

그럴 때 복잡한 로직을 하나로 관리하면 KISS원칙을 지키지 못하게 됩니다.

예시를 살펴봅시다.

function processData(input) {
    let temp = input.split('|');
    let result = [];
    for (let i = 0; i < temp.length; i++) {
        let innerTemp = temp[i].split(',');
        for (let j = 0; j < innerTemp.length; j++) {
            innerTemp[j] = parseInt(innerTemp[j]);
            if (!isNaN(innerTemp[j])) {
                if (innerTemp[j] % 2 === 0) {
                    innerTemp[j] = innerTemp[j] * 2;
                } else {
                    innerTemp[j] = innerTemp[j] - 1;
                }
            } else {
                innerTemp[j] = 0;
            }
        }
        result.push(innerTemp);
    }
    return result.map(a => a.reduce((acc, val) => acc + val, 0)).join('-');
}

console.log(processData("1,2,3|4,5,6|7,8"));

 

이 코드를 보셨을 때 느끼는 느낌이 어떠실까요? (정독하진 않으셨으면 좋겠습니다.)

복잡하고 읽기 싫은 느낌을 받으셨나요?

이 코드에는 사실 필요없는 부분들이 있고 코드의 흐름이 분리돼있지 않습니다.

필요없는 중첩 for문과 분기문, 사용하지 않아도 되는 innerTemp라는 배열의 생성등을 포함하고 있습니다.

그렇다면 SMALL하게 필요없는 로직을 제외하고 SIMPLE하게 로직을 분리해보겠습니다.

function processNumber(number) {
    let num = parseInt(number);
    if (isNaN(num)) return 0;
    return num % 2 === 0 ? num * 2 : num - 1;
}

function processGroup(group) {
    return group.split(',').map(processNumber).reduce((acc, val) => acc + val, 0);
}

function processData(input) {
    return input.split('|').map(processGroup).join('-');
}

console.log(processData("1,2,3|4,5,6|7,8"));

 

이제 우리는 각각의 함수를 보고 이해하기 편해졌습니다. 

그룹을 나누고 -> 그룹을 가공하고 -> 하이푼으로 합친다.

그룹을 가공하는 것은 

','를 기준으로 그룹을 개별숫자로 나누고 -> 개별 숫자를 짝수와 홀수를 나누어 계산하고 -> 합칩니다.

불필요한 코드들도 제거됐네요.

우리는 여기에 로직이 추가될 때 어디에 추가돼야할 지 파악하기 쉬워졌습니다.

 

이제 이렇게 하나의 함수에 로직이 몰려있고 필요없는 부분이 존재하는 경우, 이런 제안을 드릴 수 있을 것 같습니다.

하나의 함수에 로직이 많아 로직을 파악하는데에 시간이 오래걸립니다.

불필요한 코드들을 제외하고 단순하게 만들 수 있을 것 같습니다. 

그룹에 대한 로직과 각 숫자를 처리하는 로직을 분리해보면 어떨까요?

 

DRY (Don't Repeat Yourself)

이 원칙은 반복되는 코드가 있으면 분리하라는 내용입니다.

저는 이 원칙을 좋아하지만 이 원칙을 무작정 지키려다보니 문제가 되는 경우들도 있었습니다.

이 원칙을 지키면 문제가 되는 경우를 먼저 보여드리고 싶습니다.

Array.map 함수가 반복되는걸 발견했습니다.  

DRY원칙에 따라서 함수로 반복되는 코드로 분리하려고 합니다.

function processList(items, action) {
    return items.map(item => action(item));
}

const numbers = [1, 2, 3, 4, 5];
const double = num => num * 2;
const doubledNumbers = processList(numbers, double);

const strings = ["apple", "banana", "cherry"];
const uppercasedStrings = processList(strings, str => str.toUpperCase());

오 잘 분리되고 이름이 생겨 알아보기 쉽다라고 생각실 분도 계실 수 있다고 생각합니다. 

제가 이렇게 생각했다 리뷰를 받고 고쳤을 때 훨씬 편해졌던 경험이 있어 공유드리고 싶습니다.

우리는 협업을 하거나 퇴사를 하여 코드를 인수인계 드릴 수도 있습니다.

map은 자바스크립트 자체에서 제공하는 함수입니다.

하지만 우리는 processList로  map을 추상화하여 우리만 아는 코드가 됐습니다.

processList를 무시하고 map만 사용하면 같은 map이 다른 형태로 혼용되게 됩니다.

또 우리는 typescript를 사용한다면 typescript가 추론하는 map의 타입 추론 시스템을 그대로 이용하지 못하게 됩니다.

이 경우 콜백함수에 타입을 추가적으로 지정하여 굳이 필요없을 코드가 계속 늘어나게 됩니다.

 

그렇기 때문에 이런 부분을 고려하면서 DRY 원칙을 지켜 분리를 신경써야한다고 생각하고 있습니다.

제 경우에는 분리를 하기 전에 다음과 같은 점을 고려해서 분리하면 좀 좋았던 것 같습니다.

재사용 가능성이 높을까?

유틸성이 강하면서 작은 단위인가? (포맷팅 등)

비슷한 내용들을 한 파일에서 관리하는 게 더 좋지 않을까?

설정에 관련된 부분인가?

코드에서 분리를 직접하거나 팀원분께 분리를 권유드릴 땐 다음과 같이 소통할 수 있을 것 같습니다.

이런 날짜 형식의 문자열을 포매팅하는 코드는 유틸성이 강하고 재사용성이 높으므로 작은 단위로 분리하여 관련 파일이 있는 곳으로 이동시키면 필요한 함수가 이미 존재하는 지 확인하기 좋을 것 같습니다!

 

3원칙 중 마지막 원칙은 이름이 특이합니다. 

YAGNI 원칙 (You Ain't Gonna Need It)

야그니 원칙을 지키지 않으면 야근을 할수도?

이 원칙은 현재 필요없는 코드를 작성하지 말자는 뜻입니다.

YAGNI 원칙은 왜 등장했을까요?

위키 백과에 따르면 고객이 원하는 양질의 소프트웨어를 빠른 시간안에 전달하는 것이라는 목적을 가진 익스트림 프로그래밍에서 나온 원칙입니다.

익스트림 프로그래밍의 공동 설립자인 론 제프리스는 이런 말을 했다고 합니다.

"실제로 필요할 때 무조건 구현하되, 그저 필요할 것이라고 예상할 때에는 절대 구현하지 말라"

제 관점으로 해석해보겠습니다.

처음에 전제했듯 소프트웨어는 자주 변화합니다.

그리고 엔지니어는 사용자들에게 빠른 시간안에 양질의 소프트웨어를 전달하는 것이 목표입니다.

필요할 것이라고 생각한 코드는 나중에 필요하지 않게 될 가능성이 높습니다.

또 정말 필요하다면 그 때 구현하면 됩니다. 

그렇지 않은 경우에 필요없는 코드를 처리하는 데 시간을 쓰게 됩니다.

또 필요없는 함수가 한두개씩 쌓이는 걸 방치하면 어느새 필요없는 많은 코드에 쓰임을 관리하기 위해 양질이지 못한 소프트웨어와 유지보수에 더 많은 시간을 투자해야하게 됩니다.

 

한번 어떤 경우인지 살펴보기 위해 이런 기획이 있다고 가정해봅시다.

"사용자는 웹 페이지에서 현재 시간을 확인할 수 있어야 합니다."

function scheduleAppointment(date) {
    console.log(`Appointment scheduled for ${date}`);
}

function showCurrentTime() {
    const now = new Date();
    console.log("Current Time: ", now.toLocaleTimeString());

    // 현재 기획에서 요구하지 않는 미래 일정 예약 기능
    scheduleAppointment(new Date(now.getTime() + 3600000)); // 1시간 후
}

showCurrentTime();

하지만 이 요구사항을 위해 개발자는 이런 코드를 개발하였습니다.

나중에 사용될 일정 예약 기능을 미리 개발해둔 것이죠.

이러면 우리는 사용하지 않는 기능에 대한 관리를 해야합니다.

또 정확한 요구사항이 없는 상태에서 만들어진 코드이기 때문에 개발자 혼자 생각한 360,000(1시간)ms 라는 아무도 이유를 모르는 암호가 추가된 것도 볼 수 있습니다.

 

그러면 우리는 YAGNI 원칙을 바탕으로도 의사소통할 수 있을 것 같습니다.

저는 다음과 같이 제안드리는 게 떠오릅니다.

코드를 살펴보니, 예약시간을 노출하는 기능에 대한 구현이 포함된 것 같습니다.

이 기능은 현재 프로젝트 요구사항에는 포함돼있지 않은 것으로 보입니다.

현재 필요하지 않은 기능은 추후 필요할 때 추가해서 관리 포인트를 줄이는 것이 어떨까요?

 

마무리

소프트웨어 개발 3대 원칙이 무엇인지, 이는 왜 필요하며 이를 어떻게 소통의 재료로 이용할 수 있을 지 정리해보았습니다.

코드에 대한 의견을 낼 때 저의 경우에는 뚜렷한 주관을 내기에는 어려운 경우가 많았습니다.

이럴 때 원칙을 바탕으로 소통하다보니, 특정 경우에 따라 원칙이 정답일 때가 있고 오답일 때가 있는 데이터가 쌓여갔습니다.

이런 데이터가 팀원들에게 저를 이해하게 하고 제가 팀원들을 이해하게 하는 것에 도움을 줬습니다.

또 시간이 지날 수록 이럴 땐 이게 정답이고 저럴 땐 저게 정답이구나 하는 감이 늘어 신뢰를 형성하는 데 도움을 주는 것 같습니다.

 

하지만 3대원칙 만으로 의사소통하기엔 너무 다양한 문제와 코드가 있는 것 같습니다.

3대 원칙만을 이용하여 소통하기 보다는 여러 원칙들의 조합을 바탕으로 소통하면 더 많은 경우의 생각을 공유할 수 있습니다.

그렇기 때문에 다음 글은 SOLID 원칙을 이용하여 소통하는 방법을 정리해보려고 합니다.

긴 글 읽어주셔 감사합니다.

 

댓글