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

+ Recent posts