박수완
1. 📌 핵심 개념 정리
✅ 요약하기
-
작게 만들어라
함수를 만드는 첫째 규칙은 '작게'다. 함수를 만드는 둘째 규칙은 '더 작게'다. 이 규칙은 근거를 대기가 곤란하다.
- 리팩토링 한 버전
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite
) throws Exception {
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage) {
wikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
pageData/setContent(newPageContent.toString());
}
return pageData.getHtml();
}
함수가 설정 페이지와 해제 페이지 테스트 페이지를 페이지에 넣은 후 해당 테스트 페이지를 HTML로 랜더링한다는 사실을 짐작할 수 있다.
- 작게 만들어라
public static String renderPageWithSetupsAndTeardowns(
PageData pageData, boolean isSuite) throws Exception {
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
함수가 짧아지며 훨씬 눈에 잘 띄고 어떤 기능을 하는 함수인지 한 눈에 알 수 있게 되었다. 하지만 이 코드보다 훨씬 짧아야 한다. 다시 말해 if문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미이다.
-
한 가지만 해라
함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한가지만을 해야 한다.지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다. 어쨌거나 우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나워 수행하기 위해서이다.
-
함수당 추상화 수준은 하나로
함수가 확실하 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상하 수준이 동일 해야 한다. 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부사항인인지 구분하기 어려운 탓이다.
-
위에서 아래로 코드 읽기: 내려가기 규칙
코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.
-
Switch문
switch 문은 작게 만들기 어렵다. 또한 '한 가지' 작업만 하는 switch 문도 만들기 어렵다. 본질적으로 switch 문은 N가지를 처리한다. 하지만 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다.
- 개선 전
public Money calculatePay(Employee e) throws InvalidEmployeeType { switch (e.type) { case COMMISSIONED: return calculateCommissionedPay(e); case HOURLY: return calculateHourlyPay(e); case SALARIED: return calculateSalariedPay(e); default: throw new InvalidEmployeeType(e.type); } }
위 함수에는 몇 가지 문제가 있다. 첫째, 함수가 길다. 새 직원 유형을 추가하면 더 길어진다. 둘째, '한 가지' 작업만 수행하지 않는다. 셋째, SRP를 위반한다. 넷째, OCP를 위반한다.
- 개선 후
public abstract class Employee { public abstract boolean isPayday(); public abstract Money calculatePay(); public abstract void deliverPay(Money pay); } ----------------- public interface EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType; } ----------------- public class EmployeeFactoryImpl implements EmployeeFactory { public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType { switch (r.type) { case COMMISSIONED: return new CommissionedEmployee(r) ; case HOURLY: return new HourlyEmployee(r); case SALARIED: return new SalariedEmploye(r); default: throw new InvalidEmployeeType(r.type); } } }
이렇게 상속 관계로 숨긴 후에는 절대로 다른 코드에 노출하지 않는다.
-
-
서술적인 이름을 사용하라!
워드가 말했던 원칙을 기억하는가? "코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끅한 코드라 불러도 되겠다." 한 가지만 하는 작은 함수에 좋은 이름을 붙인다면 이런 원틱을 다성함에 있어 이미 절반은 성공했다. 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
- 부수 효과를 일으키지 마라!
함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓도 하니까 때로는 예상치 못하게 클래스 변수를 수정한다. 때로는 함수로 넘어온 인수나 시스템 전역변수를 수정한다. 어느 쪽이든 교해로운 거짓말이다.
- 개선 전
public class UserValidator {
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password) {
User user = UserGateway.findByName(userName);
if (user != User.NULL) {
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase)) {
Session.initialize();
return true;
}
}
return false;
}
}
여기서 함수가 일으키는 부수 효과는 Session.initialize() 호출이다. checkPassword 함수는 이름 그대로 암호를 확인한다. 이름만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다. 그래서 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 새션 정보를 지워버릴 위험에 처한다.
출력 인수 일반적으로 우리는 인수를 함수 입력으로 해석한다.
appendFooter(s);
이 함수는 무언가에 s를 바닥글로 첨부할까?
public void appendFooter(StringBuffer report)
인수 s가 출력 인수라는 사실은 분명하지만 함수 선언부를 찾아보고 나서야 알 수 있다. 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다. 출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문이다.
report.appendFooter();
일반적으로 출력 인수는 피해야 한다. 함수에서 변경해야 한다면, 함수가 속한 객체 상태를 변경하는 방식을 택한다.
-
명령과 조회를 분리하라
함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 둘다 하면 안된다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다.
public boolean set(String attribute, String value);
이 함수는 아름이 attribute인 속성을 찾아 value로 설정한 후 성공하면 true를 반환하고 실패하면 false를 반환한다.
if (set("username", "unclebob"))...
함수를 호출하는 코드만 봐서는 의미가 모호하디. "set" 이라는 단어가 동사인지 형용사인지 분간하기 어려운 탓이다.
if (attributeExisits("username")) {
setAttribute("username", "unclebob");
...
}
명령과 조회문을 분리해 혼란을 애초에 뿌리 뽑는 방법이 진짜 해결책이다.
-
오류 코드보다 예외를 사용하라
명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. 자칫하면 if문에서 명령을 표현식으로 사용하기 쉬운 탓이다. 따라서 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.
-
Try/Catch 블록 뽑아내기
try/catch 블록은 원래 추하다. 코드 구조에 혼란을 일으키며 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다.
public void delete(Page page) { try { deletePageAndAllReferences(page); } catch (Exception e) { logError(e); } } private void deletePageAndAllReferences(Page page) throws Exception { deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e) { logger.log(e.getMessage()); }
-
오류 처리도 한가지 작업이다.
험수는 '한 가지' 작업만 해야 한다. 오류 처리도 '한 가지' 작업에 속한다. 그러므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.
-
Error.java 의존성 자석
오류 코드를 반환한다는 이야기는 클래스든 열거형 변수든. 어디선가 오류 코드를 정의한다는 뜻이다.
public enum Error { OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT; }
오류를 처리하는 곳곳에서 오류코드를 사용한다면 enum class를 쓰게 되는데 이런 클래스는 의존성 자석이므로, 새 오류코드를 추가하거나 변경할 때 코스트가 많이 필요하다. 그러므로 예외를 사용하는 것이 더 안전하다.
-
- 반복하지 마라
중복은 소프트웨어에서 모든 악의 근원이다. 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다. 중복을 제거할 목적으로 관계형 데이터베이스에 정규 형식을 만들었다. 구조적 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앤다. 구조적 프로그래밍, AOP, COP 모두 어떤 면에서 중복 제거 전략이다.
-
구조적 프로그래밍
함수는 return문이 하나여야 한다. 루프 안에서 break나 continue를 사영해선 안되며 goto는 절대 안된다.
-
함수를 어떻게 짜죠?
소프트웨어를 짜는 행위는 어느 글짓기와 비슷하다. 논문이나 기사를 작성할 때는 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 내가 함수를 짤 때도 마찬가지다 처음에는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 아주 길다. 이름은 즉흥적이고 코드는 중복된다. 하지만 나는 그 서투른 코드를 빠짐없이 테스트 하는 단위 테스트 케이스도 만든다. 최종적으로 이 장에서 설명한 규칙을 따르는 함수가 얻어진다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
- 개념 또는 원칙의 이름
-
어려웠던 부분
AOP, COP란 무엇인가? -
궁금한 점
AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍) AOP는 핵심 비즈니스 로직(Core Logic)과 부가적인 관심사(Cross-cutting Concerns) 를 분리하여, 코드의 재사용성을 높이고, 유지보수를 쉽게 만드는 프로그래밍 패러다임입니다.
AOP의 주요 개념
Aspect(애스펙트): 부가적인 기능(로깅, 트랜잭션 관리 등)을 모듈화한 것.
Join Point(조인 포인트): 애스펙트가 적용될 수 있는 지점(메서드 실행, 예외 발생 등).
Pointcut(포인트컷): 애스펙트를 적용할 특정 조건을 정의.
Advice(어드바이스): 애스펙트가 실행될 구체적인 동작(메서드 실행 전/후, 예외 발생 시 등).
Weaving(위빙): 핵심 코드와 애스펙트를 결합하는 과정.
장점
중복 코드 제거 (로깅, 트랜잭션 관리 같은 부가적인 기능을 분리 가능)
핵심 로직과 부가 기능이 분리되어 유지보수 용이
COP(Context-Oriented Programming, 문맥 지향 프로그래밍) COP는 실행되는 문맥(Context)에 따라 동적으로 프로그램의 동작을 변경하는 프로그래밍 기법입니다.
AOP가 주로 부가적인 기능(로깅, 보안, 트랜잭션 관리 등)을 모듈화하는 데 집중하는 반면, COP는 동적인 문맥(Context)에 따라 동작을 변경하는 데 초점을 맞춥니다.
COP의 주요 개념 Context(문맥): 애플리케이션의 현재 상태(사용자의 위치, 시간, 디바이스 종류 등).
Layer(레이어): 특정 문맥에서 활성화될 동작을 정의.
Activation(활성화): 특정 문맥이 충족될 때 적절한 레이어를 활성화하여 동작을 변경.
장점
사용자 환경(시간, 장소, 디바이스 등)에 따라 동적으로 동작 변경 가능
애플리케이션의 유연성을 높이고, 사용자 맞춤형 서비스 구현에 유리
-
3. 📚 참고 사항
📢 논의하기
- 논의하고 싶은 주제
-
주제
추상화 패턴으로 함수를 작성 -
설명
복잡한 비즈니스 로직은 추상화 패턴을 사용하여 코드를 작성하면 함수를 따라가기 어려운 경우가 있다. 가독성있는 함수를 작성하기 위해서는 어떻게 해야할까?한 클래스 파일에는 한 가지 기능만 담겨야 하는가?
-