함수를 만들 때 유의해야 하는 여러 가지 규칙을 알아보자.
SOLID 원칙에도 비슷한 내용이 있다. 한 클래스는 하나의 책임만 가져야 한다. 함수든 클래스든 인터페이스든 적당한 규모로 자르는 것이 중요하다는 것을 모르는 개발자는 없을 것이다. 그 중에서도 함수는 프로그램의 가장 작은 단위로 자리 잡았고, 어떤 개발자에게 물어보든 긴 함수는 끔찍하다고 대답할 것이다.
그렇다면 어느 정도로 짧게 만들어야 할까?
들여쓰기로 생성되는 블록, 이라고 하면 여러 가지가 생각날 것이다. 쉽게 말해 한 함수에는 블록이 하나만 존재해야 한다.
if/else, while, for... 이 블록들은 특히나 중첩이 생기면 안 된다. 많이 양보해서 2단이면 되지 않겠냐고 저자는 말한다.
SOLID 원칙을 너무 빨리 꺼낸 것 같다. 아무래도 SRP 단일 책임 원칙은 이 부제에 조금 더 적합한 것 같다. 한 함수에 하나의 업무만 맡기라는 말은 많이 들은 것 같은데, 그 기준을 모르겠다는 게 문제다. 이때 제시하는 방법이 '추상화 수준을 하나로 만들어라'이다.
책에서는 내려가기 규칙을 적용한 코드를 작성하라고 하는데, 책을 읽듯 코드도 위에서 아래로 읽었을 때 자연스러워야 한다는 것이다.
마치 Top-Down 방식을 연상시킨다. 아래로 갈수록 점점 구체화되는 것이 특징이다. 이렇게 같은 추상화 수준을 유지하는 것들만 함수로 묶으면 된다.
이름이 짧고 의미 없는 것보다는 길게 서술하여 검색도 잘 되고 이해도 잘 되게 하는 것이 훨씬 좋다고 한다. 처음 코딩을 시작할 때부터 이름 짓기에 많은 시간을 허비(?)했다. 덕분에 지금은 좋은 변수 이름을 손민수하여 잘 쓰고 있기는 하지만, 좋은 이름을 짓는 건 매번 어려운 것 같다.
함수에서 이상적인 인수 개수는 0개라고 한다. (지금까지 내가 쓴 코드는)
함수에서 4개 이상의 인수는 어떠한 이유에서도 사용하면 안 된다고 한다.
여러가지 기능을 처리하기 위해 플래그를 따로 받는 것은 시작부터 대놓고 여러 기능을 처리하겠다고 선언하는 것과 다름 없기 때문에 쓰지 않는 게 좋다.
ex) String.format("%s worked %.2f hours.", name, hours);
존재하는지 확인하고, 존재하면 명령하는 함수는 한 번에 작성하면 안 된다.
if (set("username", "unclebob"))...
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
이럴 때 사용하는 게 Try/Catch 블록이다. 이때 걱정되는 건 내가 만든 함수가 이런 식으로 지저분해진다는 것이다.
try {
// 내가 만든 함수
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
logger.log(e.getMessage());
}
try/catch문이 하나가 아니라면 전체적인 코드 구조가 눈에 들어오지 않는다.
이때 저자는 내가 쓴 코드를 한 번 더 함수로 묶는 것을 권장한다.
public void delete(Page page) {
try {
deletePageAndAllReferences(page); // 내가 작성했던 함수
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
public void logError(Exception e) {
logger.log(e.getMessage());
}
같은 알고리즘이 반복되는 광경을 구경만 해서는 안 된다. 그 알고리즘을 고치기 위해 서로 다른 n개의 코드를 똑같이 고쳐야 한다면, 그것보다 귀찮은 일은 없을 것이다.
요즈음 스프링 스터디에서 자바를 다시 하는 탓에 정말 오랜만에 함수지향언어가 아닌 객체지향언어를 사용하여 코딩했다. 인공지능 때문에 파이썬 쓰고 근 1년 간 웹 개발만 주구장창 하느라 자바스크립트만 썼는데, 자바를 쓰니까 속이 후련해지는 기분이었다.
자바스크립트에서는 재사용성을 극대화할 수 있는 수단이 많지 않기 때문에(기껏해야 인터페이스지만 나는 그마저도 잘 안 쓴다) 이번에는 한 함수에 하나의 기능만 들어가야 한다는 말에 가장 집중했다.(자바스크립트에서도 적용하기 쉬울 것 같아서)
아래의 코드는 사용자가 폼을 제출할 때 실행되는 코드로, 사용자가 작성한 내용을 FormData 형식의 데이터 형식에 담아 서버에 post 요청을 보내는 코드다. 이때 '한 함수는 하나의 기능만 해야 한다'는 규칙이 깨져버린다. 바로 데이터 가공 + 데이터 전송 작업이 한 함수에 다 들어가있기 때문이다.
const submit = async (values) => {
const value = values || {};
const frm = new FormData();
const photoFile = document.getElementById("image");
frm.append("image", photoFile.files[0]);
frm.append("title", value.name);
frm.append("placetype", value.place === 'offline' ? true : false);
frm.append("category", value.category);
frm.append("content", value.content);
frm.append("writer", userId);
axios.post("/api/class/post", frm, {
headers: {"Content-Type": "multipart/form-data"},
}).then((response) => {
console.log(response);
alert("글 작성이 완료되었습니다!");
navigate("/");
});
};
const createSubmitData = (data) => {
const value = data || {};
const frm = new FormData();
const photoFile = document.getElementById("image");
frm.append("image", photoFile.files[0]);
frm.append("title", value.name);
frm.append("placetype", value.place === 'offline' ? true : false);
frm.append("category", value.category);
frm.append("content", value.content);
frm.append("writer", userId);
}
const submit = async (values) => {
const data = createSubmitData(values);
axios.post("/api/class/post", data, {
headers: {"Content-Type": "multipart/form-data"},
}).then((response) => {
console.log(response);
alert("글 작성이 완료되었습니다!");
navigate("/");
});
};