📌 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장 창발성
📌창발적 설계로 깔끔한 코드를 구현하자
- 단순한 설계 규칙
- 모든 테스트를 실행해라
- 테스트 케이스를 많이 작성할수록 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. 데드락
- 부모 스레드가 자식 스레드를 여러 개 만들고, 모두 끝나면 자원을 해제하고 종료하는 시스템
- 자식 스레드 하나가 데드락에 걸리면 부모 스레드는 계속 기다리게되고, 프로그램은 종료할 수 없다.
스레드 코드 테스트하기
- 테스트가 정확성을 보장하지는 않지만, 위험도를 낮출 수 있다.
- 문제를 노출하는 테스트 케이스를 작성하라
- 구체적인 지침
- 말이 안되는 실패는 잠정적인 스레드 문제로 취급하라
- 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자
- 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드르 ㄹ구현하라
- 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라
- 프로세서 수보다 많은 스레드를 돌려보라
- 다른 플랫폼에서 돌려보라
- 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라
- 직접 구현하기
- 자동화
14장 점진적인 개선
📌점진적으로 개선하다
- 테스트 주도 개발(Test-Driven Development)
- 언제 어느 때라도 시스템이 돌아가야 한다.
- 내용이 변경되어도 시스템 변경 전과 똑같이 돌아가야 한다.
- 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다.
- 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 쉬워진다.
'ETC > 클린코드' 카테고리의 다른 글
[클린코드] 7~10장 정리 : 오류처리, 경계, 단위 테스트, 클래스 (0) | 2022.12.22 |
---|---|
[클린코드] 4~6장 정리 : 주석, 형식 맞추기, 객체와 자료구조 (0) | 2022.12.22 |
[클린코드] 1~3장 정리 : 깨끗한 코드, 의미있는 이름, 함수 (0) | 2022.12.22 |