소프트웨어 시스템은 (애플리케이션 객체를 제작하고 의존성을 서로 연결하는) 준비 과정과 (준비 과정 이후에 이어지는) 런타임 로직을 분리해야 한다.
초기화 지연(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장 창발성
창발적 설계로 깔끔한 코드를 구현하자
단순한 설계 규칙
모든 테스트를 실행해라
테스트 케이스를 많이 작성할수록 DIP와 같은 원칙을 적용하고 DI, 인터페이스, 추상화 같은 도구를 사용해 결합도를 낮춘다.
중복을 없앤다.
템플릿 메소드 패턴(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() {
// 유럽연합 최소 법정 일수를 사용한다. }
}
}
프로그래머 의도를 표현한다.
좋은 이름 선택
함수와 클래스 크기 줄이기
표준 명칭 사용
단위 테스트 작성
클래스와 메소드 수를 최소로 줄인다.
📌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. 데드락
부모 스레드가 자식 스레드를 여러 개 만들고, 모두 끝나면 자원을 해제하고 종료하는 시스템
자식 스레드 하나가 데드락에 걸리면 부모 스레드는 계속 기다리게되고, 프로그램은 종료할 수 없다.
스레드 코드 테스트하기
테스트가 정확성을 보장하지는 않지만, 위험도를 낮출 수 있다.
문제를 노출하는 테스트 케이스를 작성하라
구체적인 지침
말이 안되는 실패는 잠정적인 스레드 문제로 취급하라
다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드르 ㄹ구현하라