📌 11장 시스템

시스템 제작과 시스템 사용을 분리하라

  • 소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 연결하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.
  • 초기화 지연(Lazy Initialization), 계산 지연(Lazy Evaluation)
    • 장점
      • 실제 필요하기 전까지 객체를 생성하지 않기 떄문에 불필요한 부하가 걸리지 않는다. 애플리케이션 시작 시간이 빨라진다.
      • 어떤 경우에도 null 포인터를 반환하지 않는다.
    • 단점
      • 메소드 호출 이전에 적절한 테스트 전용 객체를 sevice 필드에 저장해야 한다.
  • 책임이 둘이라는 말은 메소드가 작업을 두 가지 이상 수행하는 것 -> 단일 책임 원칙(SRP) 위반

Main 분리

  • 시스템 생성과 시스템 사용을 분리하는 방법
  • 생성과 간련된 코드는 모두 main이나 main이 호출하는 모듈로 옮기고
  • main 함수에서 시스템에 필요한 객체를 생성한 후 애플리케이션에 남긴다.

팩토리

  • 객체가 생성되는 시점을 애플리케이션이 결정해야하는 경우
  • 추상 팩토리 패턴 사용 : 주문처리 시스템에서 애플리케이션이 LineItem 인스턴스를 생성해서 Order에 추가
  • LineItem 생성 시점은 애플리케이션이 결정하지만, 생성 코드는 애플리케이션이 모른다.

의존성 주입(Dependency Injection)

  • 사용과 제작을 분리하는 강력한 메커니즘
  • 제어의 역전(Inversion of Control, IoC) 기법을 의존성 관리에 사용한 메커니즘
    • 제어의 역전 : 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다.
    • 새로운 객체가 넘겨받은 책임만 맡기 때문에 단일 책임 원칙(SRP)을 만족한다.
    • 의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임을 지지 않는다.
    • 초기 설정으로 시스템 전체에서 필요하기 떄문에 컨테이너를 사용한다.
  • DI 컨테이너는 (대개 요청이 들어올 때마다) 필요한 객체의 인스턴스를 만들고, 생성자 인수나 설정자 메서드를 사용해 의존성을 설정한다.
  • 실제 생성 객체 유형은 설정 파일에서 지정하거나 특수 생성 모듈에서 코드로 명시한다.
  • 스프링 프레임워크의 경우
    • 자바 DI 컨테이너 사용, 객체 사이 의존성은 XML 파일에 정의
    • 자바 코드에서는 이름으로 특정한 객체를 요청한다.

횡단(cross-cutting) 관심사

  • 영속성과 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다.
  • 모든 객체가 전반적으로 동일한 방식을 이용하게 만들어야 한다.
  • ex. 특정 DBMS나 독자적인 파일 사용, 테이블과 열은 같은 명명 관례, 일관적인 트랜잭션
  • 횡단 관심사 : 온갖 객체로 흩어진 영속성 구현 코드들을 모듈화한다.
    • 영속성 프레임워크 모듈화
    • 도메인 논리 모듈화
  • AOP(Aspect Oriented Programming, AOP) 관점 지향 프로그래밍
    • 횡단 관심사에 대처해 모듈성을 확보하는 일반적인 방법론
    • 특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꾸어야 한다.
    • Ex. 영속성
    • 영속적으로 저장할 객체와 속성을 선언하고, 영속성 책임을 영속성 프레임워크에 위임한다.
    • AOP 프레임워크는 대상 코드에 영향을 미치지 않는 상태로 동작 방식을 변경한다.

자바 프록시

  • 개별 객체나 클래스에서 메소드 호출을 감싸는 경우가 좋은 예시
  • JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다.
  • 클래스 프록시를 사용하려면 바이트 코드 처리 라이브러리가 필요하다.
    • ex. CGLIB, ASM, Javassist
  • 프록시의 단점 : 많은 코드의 양과 크기 -> 클린 코드 작성의 어려움

순수 자바 AOP 프레임워크

  • 자바 프레임워크(스프링 AOP, JBoss AOP 등등)는 내부적으로 프록시를 사용한다.
  • POJO
    • 스프링 비즈니스 논리를 POJO로 구현
    • 순수하게 도메인에 초점을 맞춘다.
    • 엔터프라이즈 프레임워크에 의존하지 않는다.
    • 상대적으로 단순하기 때문에 유지보수가 편하다.

  • Bank 도메인 객체는 DAO(Data Accessor Object)으로 프록시 되었다.
  • DAO 객체는 JDBC 자료 소스로 프록시되었다.
  • 클라이언트는 Bank 객체의 getAccounts()를 호출한다고 생각하지만, 실제로는 Bank POJO 기본 동작을 확장한 중첩 DECORATOR(데코레이터) 객체 집합의 가장 외곽과 통신한다.

AspectJ 관점

  • AspectJ 언어 : 관심사를 관점으로 분리하는 가장 강력한 도구
  • 관점 분리의 여러 도구 집합을 제공하지만, 새 도구를 사용하고 문법과 사용법을 익혀야 하는 단점이 있다.

결론

  • 깨끗하지 못한 아키텍처는 도메인 논리를 흐리고, 제품 품질이 떨어진다.
  • 버그가 생기기 쉽고, 기민성이 떨어지면 생산성이 낮아지고 TDD 제공 장점이 사라진다.
  • 모든 추상화 단계의 의도는 명확히 표현해야하기 때문에 POJO를 작성하고, 관점 혹은 관점과 유사한 매커니즘을 사용하여 각 구현 관심사를 분리해야한다.

📌 12장 창발성

창발적 설계로 깔끔한 코드를 구현하자

  • 단순한 설계 규칙
  1. 모든 테스트를 실행해라
    • 테스트 케이스를 많이 작성할수록 DIP와 같은 원칙을 적용하고 DI, 인터페이스, 추상화 같은 도구를 사용해 결합도를 낮춘다.
  2. 중복을 없앤다.
    • 템플릿 메소드 패턴(Template method)으로 중복 제거
abstract public class VacationPolicy { 
    public void accrueVacation() {
        calculateBaseVacationHours() ; 
        alterForLegalMinijnums() ; 
        applyToPayroll();
    }
    
    private void calculateBaseVacationHours() .{*/./.}; 
    abstract protected void alterForLega!Minimums(); 
    private void applyToPayrolK) .{*/./.};
 }
    
public class USVacationPolicy extends VacationPolicy { 
    @Override protected void alterForLegalMinimums() {
    // 미국 최소 법정 일수를 사용한다. 
    }
}   
  
public class EUVacationPolicy extends VacationPolicy { 
    @Override protected void alterForLegalMinimums() {
    // 유럽연합 최소 법정 일수를 사용한다. }
    }
}
  1. 프로그래머 의도를 표현한다.
    • 좋은 이름 선택
    • 함수와 클래스 크기 줄이기
    • 표준 명칭 사용
    • 단위 테스트 작성
  2. 클래스와 메소드 수를 최소로 줄인다.

📌 13장 동시성

동시성이 필요한 이유?

  • 동시성은 결합(coupling)을 없애는 전략이다.
  • 무엇(what), 언제(when)를 분리하는 전략
  • 작업 처리량(throughput)을 개선 해야 하는 경우, 다중 스레드 알고리즘으로 수집기 성능을 높일 수 있다.
  • 한 번에 한 사용자를 처리하는 시스템인 경우, 사용자가 늘어나면 시스템 응답 속도가 느려진다. 많은 사용자를 동시에 처리하면 시스템 응답 시간 개선 가능
  • 동시성은 항상 성능을 높여주지 않는다.
    • 동시성은 여러 개의 프로세서를 동시에 처리할 독립적인 계산이 많은 경우에만 성능이 높아진다.
  • 동시성을 구현하면 설계가 변한다.
    • 단일 스레드 시스템과 다중 스레드 시스템은 설계가 판이하게 다르다.
    • 일반적으로 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다.
  • 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없지 않다.
    • 컨테이너 동작 원리, 동시 수정 방법, 데드락 등과 같이 문제를 회피할 수 있는 지 알아야 한다.
  • 동시성은 다소 부하를 유발한다.
    • 성능 측면 부하, 코드의 추가
  • 동시성은 복잡하다.
  • 일반적으로 동시성 버그는 재현하기 어렵다.
  • 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다.

동시성을 구현하기 어려운 이유

public class X {
    private int lastldllsed;
    
    public int getNextId() { 
        return ++lastldllsed;
    } 
}
  • astldUsed 필드를 42으로 설정하고, 두 스레드가 같은 변수를 동시 참조하면
  • 한 스레드는 43를 받고, 다른 스레드도 43을 받는다. lastIdUsed는 43이 된다.

동시성 방어 원칙

  • 단일 책임 원칙(Single Responsibility Principle)
    • 주어진 메소드, 클래스, 컴포넌트를 변경할 이유가 하나여야 한다.
    • 동시성 관련 코드는 다른 코드와 분리한다.
  • 따름 정리(corollary) : 자료 범위를 제한하라
    • 공유 객체를 사용하는 코드 내 임계영역(critical section)을 synchronized 키워드로 보호한다.
    • 자료를 캡슐화하고, 공유 자료를 최대한 줄여라
  • 따름 정리 : 자료 사본을 사용하라
    • 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다.
    • 하지만 사본으로 동기화를 피하게 되면, 사본 생성과 가비지 컬렉션이 성능 부하를 상쇄할 수 있다.
  • 따름 정리 : 스레드는 가능한 독립적으로 구현하라
    • 다른 스레드와 자료를 공유하지 않는다.
    • 각 스레드는 클라이언트 요청 하나를 처리한다.
    • 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다.

라이브러리를 이해해라 - 스레드 환경에 안전한 컬렉션

  • java.util.concurrent 패키지가 제공하는 클래스
  • 다중 스레드 환경에서 사용해도 안전하고, 성능이 좋다.
  • ConcurrentHashMap은 거의 모든 상황에서 HashMap보다 빠르다.

실행 모델을 이해해라

  • 기본 용어


실행 모델 종류

  • 생산자/소비자(Producer-Consumer)
    • 하나 이상 생산자 스레드가 정보를 생성해 버퍼(buffer)나 대기열(queue)에 넣는다.
    • 대기열은 한정된 자원
    • 생산자 스레드는 대기열에 빈 공간이 있어야 정보를 채우고, 소비자 스레드는 대기열에 정보가 있어야 가져온다.
    • 생산자 스레드와 소비자 스레드는 서로 대기열 정보에 관련한 시그널을 보낸다.
  • 읽기/쓰기(Readers-Writers)
    • 읽기 스레드를 위해 공유 자원을 사용하게 되면, 쓰기 스레드가 이 공유 자원을 갱신할 때 처리율(throughput) 문제가 생길 수 있다.
    • 처리율을 강조하면 기아(starvation) 현상이 생기거나 오래된 정보가 쌓인다.
    • 쓰기 스레드가 버퍼를 오랫동안 점유하게 되면 읽기 스레드는 버퍼를 기다리느라 처리율이 떨어진다.
  • 식사하는 철학자들(Dining Philosophers)

동기화하는 메소드 사이에 존재하는 의존성을 이해해라

  • 공유 객체 하나에는 메소드 하나만 사용하기
  • 공유 객체 하나에 여러 메소드가 필요한 경우
    • 클라이언트에서 잠금 : 클라이언트에서 첫 번째 메소드를 호출하기 전에 서버를 잠근다. 마지막 메소드를 호출할 때까지 잠금 유지
    • 서버에서 잠금 : 서버에서 "서버를 잠그고 모든 메소드를 호출한 후 잠금을 해제하는" 메소드를 구현한다. 클라이언트가 이 메소드를 호출한다.
    • 연결(Adpated) 서버 : 잠금을 수행하는 중간 단계를 생성한다.

동기화하는 부분을 작게 만들어라

  • 자바에서 synchronized 키워드를 사용하면 락을 설정한다.
  • 락은 스레드를 지연시키고 부하를 가중시키기 때문에 키워드를 남발하지 않는다.
  • 임계영역 개수를 최대한 줄이는 것이 좋지만, 필요 이상의 임계영역 크기는 스레드 간의 경쟁이 늘어나고 프로그램 성능이 떨어진다.

올바른 종료 코드는 구현하기 어렵다

  • ex. 데드락
    • 부모 스레드가 자식 스레드를 여러 개 만들고, 모두 끝나면 자원을 해제하고 종료하는 시스템
    • 자식 스레드 하나가 데드락에 걸리면 부모 스레드는 계속 기다리게되고, 프로그램은 종료할 수 없다.

스레드 코드 테스트하기

  • 테스트가 정확성을 보장하지는 않지만, 위험도를 낮출 수 있다.
  • 문제를 노출하는 테스트 케이스를 작성하라
  • 구체적인 지침
    • 말이 안되는 실패는 잠정적인 스레드 문제로 취급하라
    • 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
    • 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드르 ㄹ구현하라
    • 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
    • 프로세서 수보다 많은 스레드를 돌려보라
    • 다른 플랫폼에서 돌려보라
    • 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라
    • 직접 구현하기
    • 자동화

📌 14장 점진적인 개선

점진적으로 개선하다

  • 테스트 주도 개발(Test-Driven Development)
    • 언제 어느 때라도 시스템이 돌아가야 한다.
    • 내용이 변경되어도 시스템 변경 전과 똑같이 돌아가야 한다.
  • 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다.
  • 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 쉬워진다.

📌 7장 오류처리

오류 코드보다는 예외를 사용하라

  • if else 문으로 오류 코드를 호출하지 않고, try catch 으로 오류 발생시 예외 던지기

미확인(unchecked) 예외를 사용하라

  • 확인된 예외는 OCP(Open Closed Principle)을 위반한다.
    • 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다.

예외에 의미를 제공하라

  • 오류 메시지에 정보를 담아 예외와 함께 던진다.
  • 실패한 연산 이름과 실패 유형도 언급한다.
  • 애플리케이션이 로깅 기능을 사용하면, catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

호출자르 고려해 예외 클래스를 정의하라

  • 오류가 발생한 컴포넌트로 분류한다.
  • 유형으로도 분류가 가능하다. ex. 디바이스 실패, 네트워크 실패, 프로그래밍 오류
  • 여러 catch 문을 사용하는 것 보다, 호출하는 라이브러리 API를 감싸서 예외 유형 하나를 반환해준다.
LocalPort port = new LocalPort(12); 
try {
    port.open();
} catch (PortDeviceFailure e) {
    reportError(e);
    logger. log(e.getMessage(), e);
} finally {}

public class LocalPort {
    private ACMEPort innerPort;
    public LocalPort(int portNumber) { innerPort = new ACMEPort(portNumber);
}
    public void open () { 
        try {
            innerPort.open ();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e);
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e); } 
        catch (GMXError e) {
            throw new PortDeviceFailure(e);
        } 
    }
}
  • 다른 라이브러리로 갈아탈 경우 비용이 적어진다.
  • 프로그램 테스트가 쉬워진다.

정상 흐름을 정의하라

  • 외부 API를 감싸 독자적인 예외를 던지고, 코드 위에 처리기를 정의해 중단된 계산을 처리한다.
  • 중단이 적합하지 않은 경우 : ex. 비용 청구 애플리케이션
    • Special Case Pattern 특수 사례 패턴
    • 클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식 -> 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.
    • 예외가 논리를 어렵게 만드는 경우, 특수 상황을 처리할 필요가 없으면 더 개선될 것이다.
try {
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); 
    m_total += expenses.getTotaK);
} catch(MealExpensesNotFound e) { 
    m_total += getMealPerDiem();
}

// 개선 방법 : ExpenseReportDAO를 언제나 MealExpense 객체를 반환하도록 수정
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID()); m_total += expenses.getTotaK);

public class PerDiemMealExpenses implements MealExpenses { 
    public int getTotal() {
// 기본값으로 일일 기본 식비를 반환한다.
} }

null 을 반환하지 마라

  • 메서드로 null 을 전달하는 코드와 반환하는 코드는 피한다.

결론

  • 깨끗한 코드는 읽기 좋은 코드 + 높은 안정성
  • 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하자

📌 8장 경계

외부 코드 사용하기

  • Map은 제공하는 기능성과 유연성이 있지만, 다른곳으로 넘길 경우 clear 메소드로 Map 사용라면 누구나 Map 내용을 지울 권한이 있는 위험이 있다.
  • Map의 인터페이스가 변하게 되면, 수정할 코드가 많아진다.
  • Map을 깔끔하게 사용한 코드. 제네릭스의 사용 여부는 Sensor 안에서 결정한다.
public class Sensors {
    private Map sensors = new HashMap();
    
    public Sensor getById(String id) { 
        return (Sensor) sensors.get(id);
}
// 이하 생략 }

경계 살피고 익히기

  • 타사 라이브러리를 사용할 경우
    • 하루나 이틀 이상의 시간동안 문서를 읽고 사용법 결정
    • 코드를 작성 후 라이브러리가 예상대로 동작하는지 확인
  • 학습 테스트
    • 코드를 작성해 외부 코드를 호출하는 것보다 먼저 간단한 테스트 케이스 작성 후 외부 코드를 익히는 방법

log4j 익히기

  • log4j를 사용해서 패키지를 내려받고, "hello"를 출력하는 테스트 케이스
@Test
public void testLogCreate() {
    Logger logger = Logger.getLoggerC'MyLogger");
    logger, infoC'hello"); // Appender가 필요하다는 오류 발생
}

@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger"); 
    logger.removeAllAppenders(); 
    logger.addAppender(new ConsoleAppender(
        new PatternLayout("%p %t %m%n"),
        ConsoleAppender.SYSTEM_OUT)) ;
    logger. infoC'hello");
}

학습 테스트는 공짜 이상이다

  • 이해도를 높여주는 정확한 실험이다.
  • 패키지 새 버전이 나오면 학습 테스트를 돌려 차이를 확인한다.
  • 패키지가 예상대로 도는지 검증한다.

아직 존재하지 않는 코드를 사용하기

  • 경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계
  • 지식이 별로 없는 상태에서 시스템을 개발하려고 하는 경우, 하위 시스템과 먼 부분부터 작업을 진행
  • 송신기 API에서 CommunicationsController 분리
  • 다른 팀에서 송신기 API 정의 후에 TransmitterAdapter를 구현해 간극을 매운다.
  • ADAPTER 패턴(어댑터 패턴) 으로 API 사용을 켑슐화 해서 API가 바뀔 때 수정할 코드를 한 곳으로 모았다.

깨끗한 경계

  • 경계에 위치하는 코드는 깔끔하게 분리한다.
  • 기대치를 정의하는 테스트 케이스도 작성한다.
  • 외부 패키지에 의존하는 것보다 우리 코드에 의존하는 편이 훨씬 좋다.
  • 외부 패키지를 호출하는 코드를 가능한 줄인다.
    • 새로운 클래스로 경계를 감싸거나 어댑터 패턴을 사용해서 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환한다.

📌 9장 단위 테스트

TDD 법칙 세 가지

  • 첫째 법칙 : 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
  • 둘째 법칙 : 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  • 셋째 법칙 : 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.

깨끗한 테스트 코드 유지하기

  • 지저분한 테스트 코드가 있는 것보다 테스트가 없는 것이 낫다.
  • 테스트는 유연성, 유지보수성, 재사용성을 제공한다.

깨끗한 테스트 코드

  • 중복되는 코드, 자질구레한 사항이 너무 많을 경우 표현력 저하
  • BUILD-OPERATE_CHECK 패턴
    • BUILD : 테스트 자료를 만든다.
    • OPERATE : 테스트 자료를 조작한다.
    • CHECK : 조작한 결과가 올바른지 확인한다.

도메인에 특화된 테스트 언어

  • DSL(도메인 특화 언어) 으로 테스트 코드를 구현하는 기법
  • 보통 시스템 조작 API를 사용하지만, 대신에 API 위에 함수와 유틸리티를 구현한 후 그 함수와 유틸리티를 사용한다.
    • 테스트 코드 작성과 읽기가 쉬워진다.
  • 이중 표준
    • 실제 환경에서는 안되지만 테스트 환경에서는 문제 없는 방식
    • ex. StringBuffer는 효율적이지만 테스트 환경은 자원이 제한적일 가능성이 낮다.

테스트 당 assert 하나

  • given-when-then 이라는 관례를 사용해서, 테스트 코드를 읽기 쉽게 작성
  • 테스트를 분리하게 되면 중복 코드가 생기게 된다.
  • 방법1 : TEMPLATE METHOD(템플릿 메소드) 패턴을 사용하면 중복 제거 가능
    • given/when 부분을 부모 클래스에 두고, then 부분을 자식 클래스에 둔다.
  • 방법2 : 독자적인 테스트 클래스를 만들어서, @Before 함수에 given/when 부분을 넣고 @Test 함수에 then 부분을 넣는 방법

테스트 당 개념 하나

F.I.R.S.T

  • 깨끗한 테스트를 위한 다섯가지 규칙
  • Fast(빠르게) : 테스트는 빨리 돌아야 한다.
  • Independent(독립적으로) : 각 테스트는 서로 의존하면 안된다.
  • Repeatable(반복가능한) : 어떤 환경에서도 테스트는 반복이 가능해야 한다.
  • Self-Validating(자가검증하는) : 테스트는 bool 값으로 결과를 내야 한다.
  • Timely(적시에) : 단위 테스트는 테스트를 하려는 실제 코드를 구현하기 직전에 구현한다.

📌 10장 클래스

클래스 체계

  • 변수 목록
    • static public 변수
    • static private 변수
    • private instance 변수
    • public 변수가 필요한 경우는 거의 없다.
  • 공개 함수
  • 비공개 함수 : 자신을 호출하는 공개 함수 직후
  • 캡슐화
    • 변수와 유틸리티 함수는 가능한 공개하지 않는다.
    • 테스트 코드에 접근을 허용할 수 있도록 protected 으로 선언하는 경우가 있다.
    • 하지만 비공개 상태를 유지할 방법을 생각한 후, 캡슐화를 풀어주는 것은 마지막 수단으로 한다.

클래스는 작아야 한다!

  • 클래스 이름에 해당 클래스 책임을 기술한다.
  • 클래스 설명은 if, and, or, but 등을 제외한다. 그리고 25단어 내외로 가능해야 한다.
  • SRP 단일 책임 원칙(Single Responsibility Principle)
    • 클래스는 책임이 하나여야 한다.

응집도 Cohesion

  • 클래스는 인스턴스 변수의 수가 작아야 한다.
  • 각 클래스 메서드는 클래스 인스턴스 변수를 하나 이상 사용해야 한다.
  • 변수를 많이 사용할수록 메소드와 클래스는 응집도가 더 높다.
  • 응집도를 유지하면 작은 클래스 여럿이 나온다

변경으로부터 격리

  • 구체적인 클래스 : 상세한 코드를 포함
  • 추상 클래스 : 개념만 포함
  • 인터페이스와 추상 클래스를 사용해서 구현이 미치는 영향을 격리한다.
  • 시스템의 결합도를 낮추면 유연성과 재사용성을 높일 수 있다.

📌 4장 주석

  • 주석 사용 이유 : 코드로 의도를 표현하지 못해, 실패를 만회하기 위해 사용
    • 코드는 변화하기 때문에, 주석이 점점 코드에서 분리될 수 있다. 주석을 가능한 줄이도록 노력한다.

주석은 나쁜 코드를 보완하지 못한다

코드로 의도를 표현하라

// 직원에게 복지 혜택을 받을 자격이 있는지 검사한다.
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65)){}

// 코드로 의도 표현
if (employee.isEligibleForFullBenefits())

좋은 주석

  • 법적인 주석 : 저작권 정보, 소유권 정보
  • 정보를 제공하는 주석 : ex. 정규 표현식이 시각과 날짜를 뜻하는 것을 명한다.
// kk:mm:ss EEE, IWI dd, yyyy 형식이다. Pattern timeMatcher = Pattern.compile(
:"*\\d W,*\\w,*\\d "*\)\;d
  • 의도를 설명하는 주석
  • 의미를 명료하게 밝히는 주석
  • 결과를 경고하는 주석 : ex. 특정 케이스(여유 시간이 충분하지 않은 경우)를 꺼야 하는 이유를 설명
    • 굳이 주석을 사용하지 않아도 @Ignore 속성으로 테스트 케이스를 끌 수 있다.
  • TODO 주석 : 앞으로 할 일
  • 중요성을 강조하는 주석
  • 공개 API에서 Javadocs
  • 나쁜 주석
    • 대다수 주석은 나쁜 주석
    • 주절거리는 주석
    • 같은 이야기를 중복하는 주석
    • 오해할 여지가 있는 주석
    • 의무적으로 다는 주석
    • 이력을 기록하는 주석
    • 있으나 마나 한 주석
    • 무서운 잡음
    • 함수나 변수로 표현할 수 있으면 주석을 달지 마라
    • 위치를 표현하는 주석
    • 닫는 괄호에 다는 주
    • 주석으로 처리한 코드
    • HTML 주석 : 읽기 어렵다.
    • 전역 정보 : 주석은 근처에 있는 코드만 기술
    • 너무 많은 정보
    • 모호한 관계
    • 함수 헤더
    • 비공개 코드에서 Javadocs

📌 5장 형식 맞추기

형식을 맞추는 목적

  • 코드 형식은 의사소토의 일환

적절한 행 길이를 유지하라

  • 대부분 200줄의 코드인 파일로도 커다란 시스템 구축이 가능하다.

신문 기사처럼 작성하라

  • 위에서 아래로 내려갈수록 의도를 세세하게 묘사한다.

개념은 빈 행으로 분리하라

  • 빈 행 : 새로운 개념을 시작하는 시각적 단서

세로 밀집도

  • 서로 밀집한 코드 행은 세로로 가까이 놓아야 한다.
public class ReporterConfig {
/**
* 리포터 리스너의 클래스 이름
*/
private String m_className;
/**
* 리포터 리스너의 속성
*/
private List<Property> m_properties = new ArrayList<Property>(); public void addProperty(Property property) {
m_properties. add (property);
}

// 변경 후
public class ReporterConfig {
  private String m_className;
  private List<Property> m_properties = new ArrayList<Property>();
  public void addProperty(Property property) { m_properties. add(property);
  } }

수직 거리

  • 서로 밀접한 개념은 세로로 가까이 위치하도록 한다.
  • 멀리 떨어져 있으면 소스 파일과 클래스를 여기저기 뒤져야하는 불편함이 생긴다.
  • 변수 선언 : 변수를 사용하는 위치에 최대한 가까이 선언
  • 인스턴스 변수 : 클래스 맨 처음에 선언
  • 종속 함수 : 종속되는 2개의 함수는 세로로 가까이 배치한다.
    • 가능한 호출하는 함수를 호출되는 함수보다 먼저 배치한다.
  • 개념의 유사성 : 개념적인 친화도가 높을수록 코드를 가까이 배치한다.
  • 세로 순서 : 가장 중요한 개념을 먼저 표현한다.
  • 가로 형식 맞추기 : 하나의 행의 가로 길이는 짧을수록 좋다. 최대 120자가 넘지 않도록 한다.
  • 가로 공백과 밀집도 : 공백을 사용해서 밀접한 개념과 느슨한 개념을 표현한다.
  • 들여쓰기 : scope(범위)를 표현하기 위해 코드를 들여쓴다.

팀 규칙

  • 어디에 괄호를 넣을지
  • 들여쓰기는 몇 자리로 할지
  • 클래스, 변수, 메서드 이름 규칙

📌 6장 객체와 자료구조

자료 추상화

  • 변수를 의존하지 않게 만들기 위해 private 사용
  • get(조회), 설정(set)은 왜 public 인지?
  • 변수를 private 으로 선언해도, 조회와 설정을 공개하면 구현을 외부로 노출하게 된다.
    • 구현을 감추기 위해서는 추상화 필요. 추상 인터페이스 제공

자료/객체 비대칭

  • 객체 : 추상화 뒤로 자료를 숨기고 자료를 다루는 함수만 공개
  • 자료구조 : 자료를 그대로 공개하고 별다른 함수는 제공하지 않는다.

디미터 법칙

  • 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙
  • 객체는 자료를 숨기고, 함수를 공개한다.
  • 기차 충돌 : 여러 함수를 이어서 호출한 것은 이어진 기차로 보여진다.
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

// 기차 충돌 개선
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String outputDir = scratchDir.getAbsolutePath();
  • 하지만, 위의 ctxt, Options, ScratchDir가 객체면 디미터 법칙을 위반한다.
  • 아래처럼 구현하면 디미터 법칙을 거론할 필요가 없다.
final String outputDir = ctxt.options.scratchDir.absolutePath;

자료 전달 객체

  • 자료 구조체, 자료 전달 객체 DTO(Data Transfer Object) : 공개 변수만 있고, 함수가 없는 클래스
  • 데이터 베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체
  • 빈(Bean) : 구조 좀 더 일반적인 형태
    • private 변수를 조회/설정 함수로 조작한다.
  • 활성 레코드 : DTO 의 특수한 형태
    • save, find 와 같은 탐색 함수도 제공한다.
    • 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한다.
    • 활성 레코드는 자료 구조로 취급한다.
    • 비즈니스 규칙을 담고, 내부 자료를 숨기는 객체는 따로 생선한다.

결론

  • 객체는 동작을 공개하고 자료를 숨긴다.
  • 어떤 시스템을 구현할 때, 새로운 자료 타입을 추가하는 유연성이 필요하면 객체
  • 새로운 동작을 추가하는 유연성이 필요하면 자료 구조와 절차적인 코드가 적합하다.

📌 1장 깨끗한 코드

깨끗한 코드란?

  • 우아하고 효율적인 코드 : 의존성을 줄여야 유지보수가 쉬워지고, 오류를 처리하고, 성능을 최적으로 유지해야 한다.
    • 오류 처리 : 메모리 누수, 경쟁 상태(race condition)
  • 단순하고 직접적인, 가독성이 좋은 코드
  • 가독성이 좋고, 다른 사람이 고치기 쉬운 코드 : 각 의존성을 명확하게 정의한다.
  • 주의 깊게 작성한 코드
  • 켄트 백 코드 규칙
    • 모든 테스트를 통과한다.
    • 중복이 없다.
    • 시스템 내 모든 설계 아이디어를 표현한다.
    • 클래스, 메소드, 함수 등을 최대한 줄인다.
  • 읽으면서 짐작한 대로 돌아가는 코드

📌 2장 의미 있는 이름

의도를 분명히 밝혀라

  • 변수(혹은 함수나 클래스) 의 존재 이유, 수행 기능, 사용 방법을 주석 없이 이름으로 의도를 드러낸다.
  • 아래의 코드는 하는 일을 짐작하기 어렵다.
    • theList에 무엇이 들었는지?
    • theList에서 0번째 값이 어째서 중요한지?
    • 값 4는 무엇을 의미하는지?
    • 함수가 반환하는 리스트 list1은 어떻게 사용하는지?
public List<int[]> getThem() {
List<int[]> listl = new ArrayList<int[]>();
 for (int[] x : theList) if (x[0] = 4)
listl.add(x); return listl;
}
  • 이름 변경후
public List<Cell> getFlaggedCells() {
List<Cell> flaggedCells = new ArrayList<Cell>(); for (Cell cell : gameBoard)
if (cell.isFlagged()) flaggedCells.add(cell);
return flaggedCells; }

그릇된 정보를 피하라

  • 의미가 있는 단어를 다른 의미로 사용하지 않는다.
    • ex) hp, aix, sco
  • 실제 List가 아니면, accountList라고 명명하지 않는다.
    • accountGroup, bunchOfAccounts, Accounts라 명명한다.

의미 있게 구분하라

  • 컴파일러를 통과해도 연속된 숫자를 덧붙이거나 불용어(noise word) 를 추가하는 방식은 적절하지 않다.
    • ex) a1, a2, a3, ... , aN -> source와 destination와 같이 정보를 제공할 수 있는 이름을 사용한다.
public static void copyChars(char al[], char a2[]) { for (int i = 0; i < al.length; i++) {
a2[i] = al[i]; }
}

발음하기 쉬운 이름을 사용하라

검색하기 쉬운 이름을 사용하라

  • 숫자 7을 사용하게 되면, 7이 들어가는 모든 이름들이 검색된다.
  • MAX_CLASSES_PER_STUDENT 와 같이 검색하기 쉬운 이름을 사용한다.

인코딩을 피하라

자신의 기억력을 자랑하지 마라

클래스 이름

  • 클래스 이름과 객체 이름 : 명사, 명사구
  • 적절한 예시 : Customer, WikiPage, Account, AddressParser
  • 부적절한 예시 : Manager, Processor, Data, Info와 같은 이름, 동사

메소드 이름

  • 메소드 이름 : 동사, 동사구
  • 적절한 예시 : postPayment, deletePage, save
  • 접근자(Accessor), 변경자(Mutator), 조건자(Predicate)는 javabean 표준에 따라 값 앞에 get, set, is를 붙인다.

기발한 이름은 피하라

한 개념에 한 단어만 사용하라

  • 추상적인 개념 하나에 단어 하나를 선택한다.
  • 똑같은 기능의 메소드를 클래스마다 fetch, retrieve, get으로 제각각 부르면 혼란스럽다.

말장난을 하지 마라

  • 한 단어를 두 가지 목적으로 사용하지 마라
  • ex) 기존에 add를 사용한 메소드가 두 개를 더하거나 이어서 새로운 값을 만드는 기능이라 가정했을 경우
    • 값 하나만 추가하는 메소드의 이름은 add보다 insert나 append를 사용하는 것이 적당하다.

해법 영역에서 가져온 이름을 사용하라

  • 익숙한 기술 개념에는 기술 이름이 적합한 선택

문제 영역에서 가져온 이름을 사용하라

  • 적절한 용어가 없으면 문제 영역에서 이름을 가져온다.

의미 있는 맥락을 추가하라

  • state -> addState

불필요한 맥락을 없애라

  • 고급 휘발유 충전소(Gas Station Deluxe) 라는 애플리케이션을 만들었을 때, 모든 클래스 이름을 GSD로 시작하는 것은 바람직하지 않다.

📌 3장 함수

작게 만들어라

  • 블록과 들여쓰기
    • 함수에서 들여쓰기 수준은 1단이나 2단을 넘으면 안된다.

한 가지만 해라

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.

함수 당 추상화 수준은 하나로

  • 위에서 아래로 코드 읽기 : 내려가기 규칙
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
  • switch 문은 작게 만들기 어렵다.

서술적인 이름을 사용하라

  • ex) testableHtml -> SetupTeardownlncluder 함수가 하는 일을 좀 더 잘 표현할 수 있다.
  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.

함수 인수

  • 함수에서 이상적인 인수 개수 0개(무항)
  • 3개 이상은 피하는 것이 좋다.
  • 플래그 인수
    • 함수로 부울 값을 넘기는 것은 좋지 않다.
    • ex) render(boolean isSuite) 보다는 renderForSuite(), renderForSingleTest()이라는 함수로 나눠야한다.
  • 출력 인수 : 일반적으로 출력 인수는 피한다.

명령과 조회를 분리하라

오류 코드보다 예외를 사용하라

  • try/catch 블록 뽑아내기
    • try/catch 블록은 정상 동작과 오류 처리 동작을 뒤섞기 때문에, 별도 함수로 뽑아낸다.
    • 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하기 수정하기 쉬워진다.
    • 오류 처리도 한 가지 작업이다.

반복하지 마라

  • 알고리즘이 여러 함수에서 반복되지 않도록 작성한다.
  • 객체지향 프로그래밍은 코드를 부모 클래스에 몰아 중복을 없앤다.

구조적 프로그래밍

  • 루프 안에서 break, continue, goto를 사용하지 않는다.
  • 함수를 작게 만드려면, return, break, continue를 사용해도 된다.

+ Recent posts