diff --git "a/11\354\236\245/\353\254\270\355\235\254\354\203\201.md" "b/11\354\236\245/\353\254\270\355\235\254\354\203\201.md" new file mode 100644 index 0000000..d62ed58 --- /dev/null +++ "b/11\354\236\245/\353\254\270\355\235\254\354\203\201.md" @@ -0,0 +1,249 @@ +# 11장 시스템 +- 복잡성은 죽음이다. 개발자에게서 생기를 앗아가며, 제품을 계획하고 제작하고 테스트하기 어렵게 만든다 + +### 도시를 세운다면? + +- 혼자서 직접 모든 것을 관리할 수 없다 +- 각 분야를 관리하는 팀이 있어야 한다 +- 또한 적절한 추상화와 모듈화가 필요하다 + +### 시스템 제작과 시스템 사용을 분리하라 + +- 우선 제작은 사용과 다르다 +- 시작 단계는 모든 애플리케이션이 풀어야 할 관심사다 + +12 + +1. 이것이 초기화 지연 혹은 계산 지연이라는 기법이다 +- 장점으로는 실제로 필요할 때까지 객체를 생성하지 않으므로 불필요한 부하가 걸리지 않는다 +- 따라서 애플리케이션을 시작하는 시간이 그만큼 빨라진다 + +2. 어떤 경우에도 null 포인터를 반환하지 않는다 +- getService 메서드가 MyServiceImpl과 생성자 인수에 명시적으로 의존한다 + +3. 단위 테스트에서 getService 메서드를 호출하기 전에 적절한 테스트 전용 객체 +- 일반 런타임 로직에다 객체 생성 로직을 섞어놓은 탓에 모든 실행 경로도 테스트해야 한다 +- 즉, SRP를 깬다 + +→ 체계적이고 탄탄한 시스템을 만들고 싶다면 흔히 쓰는 좀스럽고 손쉬운 기법으로 모듈성을 깨서는 절대로 안 된다 + +
+ +**Main 분리** + +- 시스템 생성과 시스템 사용을 분리하는 한 가지 방법으로 +- 생성과 관련한 코드는 모두 main이나 main이 호출하는 모듈로 옮기고 +- 나머지 시스템은 모든 객체가 생성되었고 모든 의존성이 연결되었다고 가정 + +22 + +- 애플리케이션은 main이나 객체가 생성되는 과정을 전혀 모른다 + +
+ +**팩토리** + +- 객체가 생성되는 시점을 애플리케이션이 결정할 필요도 생긴다 +- 주문처리 시스템에서 애플리케이션은 LineItem 인스턴스를 생성해 Order에 추가한다 +- 이때는, ABSTRACT FACTORY 패턴을 사용한다 +- LineItem을 생성하는 시점은 애플리케이션이 결정하지만 LineItem 을 생성하는 코드는 애플리케이션이 모른다 + +33 + +- 모든 의존성이 main에서 OrderProcessing 애플리케이션으로 향한다 +- 즉, OrderProcessing 애플리케이션은 LineItem이 생성되는 구체적인 방법을 모른다 + +
+ +**의존성 주입** + +- 의존성 주입은 IOC 기법을 의존성 관리에 적용한 메커니즘이다 +- IOC에서는 한 객체가 맡은 보조 책임을 새로운 객체에게 전적으로 떠넘긴다 +- 새로운 객체는 넘겨받은 책임만 맡으므로 SRP를 지키게 된다 + +44 + +- 호출하는 객체는 실제로 반환되는 객체의 유형을 제어하지 않는다 +- 진정한 의존성 주입은 여기서 한 걸음 더 나간다 +- 클래스가 의존성을 해결하려 시도하지 않는다 +- 클래스는 완전히 수동적이다 +- 대신에 의존성을 주입하는 방법으로 설정자 메서드나 생성자 인수를 제공한다 + +```java +public class OrderService { + private final PaymentService payment; + + public OrderService(PaymentService payment) { + this.payment = payment; + } + + public void process() { + payment.pay(); // 어떤 구현체인지 모름 + } +} +``` + +- **OrderService는 KakaoPay가 들어왔는지 모르고**, 그저 PaymentService라는 인터페이스만 쓴다 +- 그냥 필요하다고 선언하고, 외부에서 주입 받기만 한다 + +- 스프링 프레임워크는 가장 널리 알려진 자바 DI 컨테이너를 제공한다 +- 대다수 DI 컨테이너는 필요할 때까지는 객체를 생성하지 않고, 대부분은 계산 지연이나 비슷한 최적화에 쓸 수 있도록 팩토리를 호출하거나 프록시를 생성하는 방법을 제공한다 + +### 확장 + +- ‘처음부터 올바르게’ 시스템을 만들 수 있다는 믿음은 미신이다 +- 대신에 우리는 오늘 주어진 사용자 스토리에 맞춰 시스템을 구현해야 한다 +- TDD와 리팩토링으로 얻어지는 깨끗한 코드는 코드 수준에서 시스템을 조장하고 확장하기 쉽게 만든다 +- 단순한 아키텍처를 복잡한 아키텍처로 조금씩 키울 수 없다는 현실은 정확하다 + +
+ +**횡단 관심사** + +- 영속성과 같은 관심사는 애플리케이션의 자연스러운 객체 경계를 넘나드는 경향이 있다 +- 원론적으로는 모듈화되고 캡슐화된 방식으로 영속성 방식을 구상할 수 있다 +- 하지만 현실적으로는 영속성 방식을 구현한 코드가 온갖 객체로 흩어진다 +- 여기서 횡단 관심사란 용어가 나온다 +- AOP에서 관점이라는 모듈 구성 개념은 “특정 관심사를 지원하려면 시스템에서 특정 지점들이 동작하는 방식을 일관성 있게 바꿔야 한다 + +
+ +### 자바 프록시 + +- JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다 +- 코드 ‘양’과 크기는 프록시의 두 가지 단점이다 +- 또한 시스템 단위로 실행 ‘지점’을 명시하는 메커니즘도 제공하지 않는다 + +
+ +### 순수 자바 AOP 프레임워크 + +- 순수 자바 관점을 구현하는 스프링 AOP, JBoss AOP 등과 같은 여러 자바 프레임워크는 내부적으로 프록시를 사용한다 +- 스프링은 비즈니스 로직을 POJO로 구현한다 +- POJO는 순수하게 도메인에 초점을 맞추며, 엔터프라이즈 프레임워크에 의존하지 않는다 +- 따라서 테스트가 개념적으로 더 쉽고 간단하다 +- 영속성, 트랜잭션, 보안, 캐시, 장애조치 등과 같은 횡단 관심사가 포함된다 +- 프레임워크는 사용자가 모르게 프록시나 바이트코드 라이브러리를 사용해 이를 구현한다 + +55 + +- 클라이언트는 Bank 객체에서 getAccounts()를 호출한다고 믿지만 실제로는 Bank POJO의 기본 동작을 확장한 중첩 DECORATOR 객체 집합의 가장 외곽과 통신한다 +- 필요하다면 트랜잭션, 캐싱 등에도 DECORATOR를 추가할 수 있다 + +
+ +### AspectJ 관점 + +- 관심사를 관점으로 분리하는 가장 강력한 도구는 AspectJ언어다 +- 스프링 AOP나 JBoss AOP가 제공하는 순수 자바 방식은 관점이 필요한 상황 중 80~90%에 충분하다 +- AspectJ는 풍부한 도구 집합을 제공하지만, 새 도구를 사용하고 새 언어 문법과 사용법을 익혀야 한다는 단점이 있다 + +
+ +**Spring AOP VS AspectJ** + +| 구분 | **Spring AOP** | **AspectJ** | +| --- | --- | --- | +| **적용 방식** | **프록시 기반 (런타임)** | **컴파일/바이트코드 조작 (컴파일 타임 또는 로드 타임)** | +| **지원 대상** | **메서드 단위** (public만) | **모든 지점 (생성자, 필드 접근, static 등)** | +| **사용 편의** | 쉬움, 스프링 설정으로 바로 적용 | 복잡, 별도 컴파일러 or 로드타임 위버 필요 | +| **통합성** | 스프링 생태계에 맞춤형 | 독립적이고 강력하지만 셋업 번거로움 | +| **대표 어노테이션** | `@Aspect`, `@Around`, `@Before` 등 | `@Aspect`, `@Pointcut`, `@Before`, `@After` 등 동일 | + +
+ +✅ Spring AOP 특징 (스프링 기본 내장) + +- **JDK 동적 프록시** 또는 **CGLIB** 이용해서 메서드 호출을 “가로채는 방식” +- **`@Transactional`, `@Async`, `@Scheduled` 다 Spring AOP 기반** +- **단점**: + - 인터페이스 or 클래스 프록시만 가능 + - **private, static, 생성자, 필드 접근은 못 건드림** + +
+ +✅ AspectJ 특징 (정통 AOP) + +- **바이트코드 조작** → 실행 전에 **아예 클래스 내부 코드에 끼워 넣음** +- **Pointcut 대상이 무한함**: + - 필드 접근, 생성자 호출, 정적 메서드 등도 전부 지원 +- **AspectJ 컴파일러(ajc)** 또는 **LTW(Load Time Weaving)** 필요 + +
+ +📌 Spring AOP 불가능한 경우 + +```java +private void sendEmail() { + // 스프링 AOP로는 여기 못 끼어듬 +} +``` + +
+ +📌 AspectJ는 가능 + +```java +@Around("execution(private void sendEmail())") +public void aroundSendEmail(...) { + ... +} +``` + +
+ +정리하자면, + +| 질문 | 대답 | +| --- | --- | +| **스프링에서 기본으로 쓰는 AOP는?** | Spring AOP (프록시 기반) | +| **필드나 private 메서드도 감싸야 하면?** | AspectJ 써야 한다 | +| **AspectJ가 더 강력한가?** | YES. 하지만 셋업이 귀찮고 무겁다 | +| **Spring AOP는 어디서 주로 쓰나?** | 트랜잭션, 로깅, 캐시, 보안 같은 횡단 관심사 적용할 때 | +| **둘 다 `@Aspect` 쓰는 거야?** | YES. 어노테이션은 같고, 동작 방식이 다르다 | + +
+ +### 테스트 주도 시스템 아키텍처 구축 + +- 애플리케이션 도메인 논리를 POJO로 작성할 수 있다면, +- 코드 수준에서 아키텍처 관심사를 분리할 수 있다면, +- 테스트 주도 아키텍처 구축이 가능해진다 +- ‘아주 단순하면서도’ 멋지게 분리된 아키텍처로 소프트웨어 프로젝트를 진행해 재빨리 출시한 후 조금씩 확장해 나가도 괜찮다 +- 이는 설계가 최대한 분리되어 각 추상화 수준과 범위에서 코드가 적당히 단순한 경우 가능하다 +- 설계가 아주 멋진 API 조차도 정말 필요하지 않으면 과유불급이다 + +
+ +### 의사 결정을 최적화하라 + +- 모듈을 나누고 관심사를 분리하면 지엽적인 관리와 결정이 가능해진다 +- 관심사를 모듈로 분리한 POJO 시스템은 기민함을 제공한다 +- 이 덕에 최선의 시점에 최적의 결정을 내리기가 쉬워진다 +- 또한 결정의 복잡성도 줄어든다 + +
+ +### 명백한 가치가 있을 때 표준을 현명하게 사용하라 + +- 아주 과장되게 포장된 표준에 집착하는 바람에 고개 가치가 뒷전으로 밀려난 사례가 많다 +- 표준을 사용하면 아이디어와 컴포넌트를 재사용하기 쉽고, 적절한 경험을 가진 사람을 구하기 쉬우며, 좋은 아이디어를 캡슐화하기 쉽고, 컴포넌트를 엮기 쉽다 +- 하지만 표준을 만드는 시간이 너무 오래 걸려 업계가 기다리지 못하며, 원래 표준을 제정한 목적을 잊어버리기도 한다 + +
+ +### 시스템은 도메인 특화 언어가 필요하다 + +- DSL(Domain-Specific Language)는 간단한 스크립트 언어나 표준 언어로 구현한 API를 가리킨다 +- 좋은 DSL은 도메인 개념과 그 개념을 구현한 코드 사이에 존재하는 ‘의사소통 간극’을 줄여준다 +- 효과적으로 사용한다면 DSL은 추상화 수준을 코드 관용구나 디자인 패턴 이상으로 끌어올린다 +- 즉, 고차원부터 저차원 세부사항까지 모든 추상화 수준과 모든 도메인을 POJO로 표현할 수 있다 + +
+ +### 결론 + +- 시스템 역시 깨끗해야 한다 +- 깨끗하지 못한 아키텍처는 도메인 논리를 흐리며 기민성을 떨어뜨린다 +- 모든 추상화 단계에서 의도는 명확히 표현해야 한다 +- 시스템을 설계하든 개별 모듈을 설계하든, 실제로 돌아가는 가장 단순나 수단을 사용해야 한다 diff --git "a/12\354\236\245/\353\254\270\355\235\254\354\203\201.md" "b/12\354\236\245/\353\254\270\355\235\254\354\203\201.md" new file mode 100644 index 0000000..be38840 --- /dev/null +++ "b/12\354\236\245/\353\254\270\355\235\254\354\203\201.md" @@ -0,0 +1,84 @@ +- **창발(創發)** = “스스로 드러난다”, “밑에서부터 자연스럽게 생겨난다” +- 영어로는 **Emergence** +- **작은 요소들이 상호작용하면서 전체적인 구조나 질서가 자연스럽게 나타나는 현상**을 뜻한다 + +
+ +### 창발적 설계로 깔끔한 코드를 구현하자 + +- 대다수는 켄트 벡이 제시한 단순한 설계 규칙 네 가지가 소프트웨어 설계 품질을 크게 높여준다고 믿는다 +- 켄트 벡은 다음 규칙을 따르면 설계는 ‘단순하다’고 말한다 +1. 모든 테스트를 실행한다 +2. 중복을 없앤다 +3. 프로그래머 의도를 표현헌다 +4. 클래스와 메서드 수를 최소로 줄인다 + +
+ +### 단순한 설계 규칙 1 : 모든 테스트를 실행하라 + +- 시스템이 의도한 대로 돌아가는지 검증할 간단한 방법이 없다면, 문서 작성을 위해 투자한 노력에 대한 가치는 인정받기 어렵다 +- 검증이 불가능한 시스템은 절대 출시하면 안 된다 +- 테스트가 가능한 시스템을 만들려고 애쓰면 설계 품질이 더불어 높아진다 +- 크기가 작고 목적 하나만 수행하는 클래스가 나온다 +- 다만 결합도가 높으면 테스트 케이스를 작성하기가 여렵다 +- 테스트 케이스를 많이 작성할수록 개발자는 DIP와 같은 원칙을 적용하고 DI, 인터페이스, 추상화 같은 도구를 사용해 결합도를 낮춘다 +- 따라서 설계 품질은 더욱 높아진다 + +
+ +### 단순한 설계 규칙 2~4 : 리팩터링 + +- 테스트 케이스를 모두 작성했다면 이제 코드와 클래스를 정리해도 괜찮다 +- 테스트 케이스가 있으면 코드를 정리하면서 시스템이 깨질까 걱정할 필요가 없다 +- 리팩터링 단계에서는 응집도를 노이고, 결합도를 낮추고, 관심사를 분리하고, 시스템 관심사를 모듈로 나누고, 함수와 클래스 크기를 줄이고, 더 나은 이름을 선택하는 등 다양한 기법을 동원한다 + +
+ +### 중복을 없애라 + +- 중복은 추가 작업, 추가 위험, 불필요한 복잡도를 뜻한다 +- 깔끔한 시스템을 만들려면 단 몇 줄이라도 중복을 제거하겠다는 의지가 필요하다 + +image + + +- 아주 적은 양이지만 공통적인 코드를 새 메서드로 뽑고 보니 클래스가 SRP를 위반한다 +- 이런 ‘소규모 재사용’은 시스템 복잡도를 극적으로 줄여준다 + +
+ +**템플릿 메서드 패턴** + +- 고차원 중복을 제거할 목적으로 자주 사용하는 기법이다 + +image + +- 최소 법정 일수를 계산하는 코드만 제외하고 거의 동일했기에 위와 같이 중복을 제거할 수 있다 + +
+ +### 표현하라 + +- 엉망인 코드는 나중에 코드를 유지보수할 사람이 코드를 짜는 사람만큼이나 문제를 깊이 이해할 가능성은 희박히다 +- 프로젝트 비용 중 대다수는 장기적인 유지보수에 들어간다 +- 코드는 개발자의 의도를 분명히 표현해야 한다 +- 그래야 결함이 줄어들고 유지보수 비용이 적게 든다 + +1. 좋은 이름을 선택한다 +2. 함수와 클래스 크기를 가능한 줄인다 +3. 표준 명칭을 사용한다 + - 클래스가 COMMAND 나 VISITOR 와 같은 표준 패턴을 사용해 구현된다면 클래스 이름에 패턴 이름을 넣어준다 +4. 단위 테스트 케이스를 꼼꼼히 작성한다 + +→ 나중에 코드를 읽을 사람은 바로 자신일 가능성이 높다는 사실을 명심해라 + +
+ +### 클래스와 메서드 수를 최소로 줄여라 + +- 때로는 무의미하고 독단적인 정책 탓에 클래스 수와 메서드 수가 늘어나기도 한다 +- 예로는 클래스마다 무조건 인터페이스를 생성하라고 요구하는 구현 표준, +- 그리고 자료 클래스와 동작 클래스는 무조건 분리해야 한다고 주장하는 것들이 있다 +- 가능한 독단적인 견해는 멀리하고 실용적인 방식을 택한다 +- 결국 클래스와 함수 수를 줄이는 작업도 중요하지만, 테스트 케이스를 만들고 중복을 제거하고 의도를 표현하는 작업이 더 중요하다 diff --git "a/13\354\236\245/\353\254\270\355\235\254\354\203\201.md" "b/13\354\236\245/\353\254\270\355\235\254\354\203\201.md" new file mode 100644 index 0000000..b456596 --- /dev/null +++ "b/13\354\236\245/\353\254\270\355\235\254\354\203\201.md" @@ -0,0 +1,303 @@ +### 동시성이 필요한 이유? + +- 동시성은 결합(coupling)을 없애는 전략이다 +- 즉, 무엇과 언제를 분리하는 전략이다 +- 스레드가 하나인 프로그램은 무엇과 언제가 서로 밀접하다 +- 무엇과 언제를 분리하면 애플리케이션 구조와 효율이 극적으로 나아진다 +- 서블릿을 예로 보면, 웹 요청이 들어올 때마다 웹 서버는 비동기식으로 서블릿을 실행한다 +- 원칙적으로 각 서블릿 스레드는 다른 서블릿 스레드와 무관하게 자신만의 세상에서 돌아간다 + +
+ +- 구조적 개선만을 위해 동시성을 채택하는 건 아니다 +- 응답 시간과 작업 처리량(throughput) 개선이라는 요구사항으로 인해 직접적인 동시성 구현이 불가피하다 +- 많은 사용자를 동시에 처리하면 시스템 응답 시간을 높일 수 있다 + +
+ +**미신과 오해** + +- 동시성은 항상 성능을 높여준다 + - → 동시성은 때로 성능을 높여준다 + - 대기 시간이 아주 길어 여러 스레드가 프로세서를 공유할 수 있거나, 여러 프로세서가 동시에 처리할 독립적인 계산이 충분히 많은 경우에만 성능이 높아진다 + +--- + +### 💡 상황: 외부 요청을 기다려야 하는 작업 (I/O 대기 중심) + +```java +public class EmailSender { + public void send() { + // SMTP 서버에 요청 → 대기 2초 + Thread.sleep(2000); // 대기 시간 + } +} +``` + +- 만약 **100명이 동시에 이메일 보내야 하는데**, +- 한 쓰레드로 처리하면 100 × 2초 = **200초** +- 하지만 **10개 쓰레드로 병렬 처리**하면 20초 안에 끝남 + +✅ **결과**: I/O 대기가 많을 때 동시성 적용 → CPU 놀지 않음 → **성능 향상** + +### 💡 상황: 계산량 많은 CPU 작업 + 공유 자원 접근 + +```java +public class Calculator { + private int total = 0; + + public synchronized void add() { + for (int i = 0; i < 1_000_000; i++) { + total++; + } + } +} + +``` + +- 여러 스레드가 동시에 `add()`를 호출하면? + - **synchronized** 때문에 **락 경쟁 발생** + - CPU는 계산보다 락 기다리느라 낭비 +- 결과적으로 **단일 스레드보다 느려질 수도 있음** + +❌ **결과**: 락 경합, 컨텍스트 스위칭 비용 → **성능 하락** + +--- + +- 동시성을 구현해도 설계는 변하지 않는다 + - 무엇과 언제를 분리하면 시스템 구조가 크게 달라진다 +- 웹 또는 EJB 컨테이너를 사용하면 동시성을 이해할 필요가 없다 + - 실제로 컨테이너가 어떻게 동작하는지 + - 어떻게 동시 수정, 데드락 등과 같은 문제를 피할 수 있는지를 알아야만 한다 + +
+ +**타당한 생각** + +- 동시성은 다소 부하를 유발한다 +- 동시성은 복잡하다 +- 일반적으로 동시성 버그는 재현하기 어렵다 +- 동시성을 구현하려면 흔히 근본적인 설계 전략을 재고해야 한다 + +
+ +### 난관 + +image + +- 두 스레드가 같은 변수를 동시에 참조하면 세 번째와 같이 놀라운 결과가 발생한다 +- 두 스레드가 자바 코드 한 줄을 거쳐가는 경로는 수없이 많은데, 그 중에서 일부 경로가 잘못된 결과를 내놓기 때문이다 +- 정확히는 JIT 컴파일러가 바이트 코드를 처리하는 방식과 자바 메모리 모델이 원자로 간주하는 최소 단위를 알아야 한다 +- 물론 대다수 경로는 올바른 결과를 내놓지만, 잘못된 결과를 내놓는 일부 경로가 있다 + +
+ +### 동시성 방어 원칙 + +1. **단일 책임 원칙** +- 동시성 관련 코드는 다른 코드와 분리해야 한다 + +image + +
+ +2. **따름 정리 : 자료 범위를 제한하라** +- 공유 객체를 사용하는 코드 내 임계영역(critical section)을 synchronized 키워드로 보호하라고 권장한다 +- 공유 자료를 수정하는 위치가 많을수록 다음 가능성도 커진다 +- 보호할 임계영역을 빼먹는다 +- 모든 임계영역을 올바르게 보호했는지 확인하느라 똑같은 노력과 수고를 반복한다 + +→ 권장사항 : 자료를 캡슐화하라. 공유 자료를 최대한 줄여라 + +
+ +3. **따름 정리 : 자료 사본을 사용하라** +- 공유 자료를 줄이려면 처음부터 공유하지 않는 방법이 제일 좋다 +- 객체를 복사해 읽기 전용으로 사용하는 방법과 +- 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본에서 결과를 가져오는 방법도 가능하다 +- 사본으로 동기화를 피할 수 있다면 내부 잠금을 없애 절약한 수행 시간이 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다 + +
+ +4. **따름 정리 : 스레드는 가능한 독립적으로 구현하라** +- 다른 스레드와 자료를 공유하지 않는 스레드를 구현하자 +- 각 스레드는 클라이언트 요청 하나를 처리하며, 모든 정보는 비공유 출처에서 가져오며 로컬 변수에 저장한다 + +→ 독자적인 스레드로, 가능하면 다른 프로세서에서, 돌려도 괜찮도록 자료를 독립적인 단위로 분할하라 + +
+ +### 라이브러리를 이해하라 + +**스레드 환경에 안전한 컬렉션** + +- ConcurrentHashMap은 거의 모든 상황에서 HashMap보다 빠르다 +- 동시 읽기/쓰기를 지원하며, 자주 사용하는 복합 연산을 다중 스레드 상에서 안전하게 만든 메서드로 제공한다 + +→ 언어가 제공하는 클래스를 검토하라. 자바에서는 java.util.concurrent, concurrent.atomic, concurrent.locks를 익혀라 + +
+ +### 실행 모델을 이해하라 + +**생산자 - 소비자** + +- 생산자 스레드와 소비자 스레드가 사용하는 대기열은 한정된 자원이다 +- 대기열을 올바로 사용하고자 생산자 스레드와 소비자 스레드는 서로에게 시그널을 보낸다 +- 잘못하면 동시에 시그널을 보낼 수도 있다 + +
+ +**읽기 - 쓰기** + +- 처리율을 강조하면 기아(starvation) 현상이 생기거나 오래된 정보가 쌓인다 +- 대게는 쓰기 쓰레드가 버퍼를 오랫동안 점유하는 바람에 여러 읽기 스레드가 버퍼를 기다리느라 처리율이 떨어진다 +- 양쪽 균형을 잡으면서 동시 갱신 문제를 피하는 해법이 필요하다 + +
+ +**식사하는 철학자들** + +- 철학자를 스레드로 포크를 자원으로 바꿔 생각해보자 +- 주의해서 설계하지 않으면 데드락, 라이브락, 처리율 저하, 효율성 저하 등을 겪는다 + +→ 위에서 설명한 기본 알고리즘과 각 해법을 이해하라 + +
+ +### 동기화하는 메서드 사이에 존재하는 의존성을 이해하라 + +- 동기화하는 메서드 사이에 의존성이 존재하면 동시성 코드에 찾아내기 어려운 버그가 생긴다 + +→ 공유 객체 하나에는 메서드 하나만 사용하라 + +
+ +1. 클라이언트에서 잠금 - 첫번째 메서드를 호출하기전 클라이언트에서 잠근다. 끝날때까지 +2. 서버에서 잠금 - 서버를 잠그고 모든 메서드 호출 후 해제하는 메서드 구현 +3. 연결 서버 - 잠금을 수행하는 중간 단계를 생성한다 + +
+ +### 동기화하는 부분을 작게 만들어라 + +- 락은 스레드를 지연시키고 부하를 가중시킨다 +- 필요 이상으로 임계영역 크기를 키우면 스레드 간에 경쟁이 늘어나고 프로그램 성능이 떨어진다 + +
+ +### 올바른 종료 코드는 구현하기 어렵다 + +- 깔끔하게 종료하는 코드는 올바로 구현하기 어렵다 +- 흔히 발생하는 문제가 데드락으로, 스레드가 절대 오지 않을 시그널을 기다린다 +- 자식 스레드가 생산자/소비자 관계라면, 생산자에서 메시지를 기다리는 소비자 스레드는 차단(blocked) 상태에 있으므로 종료하라는 시그널을 못 받는다 + +--- + +✅ 문제 발생 + +```java +BlockingQueue queue = new LinkedBlockingQueue<>(); + +// 소비자 스레드 +Thread consumer = new Thread(() -> { + try { + while (true) { + String msg = queue.take(); // 🚨 여기서 블로킹됨 + if (msg.equals("STOP")) break; // 종료 신호 + System.out.println("Consumed: " + msg); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } +}); + +// 생산자 스레드 +Thread producer = new Thread(() -> { + // 아무것도 넣지 않음 +}); + +consumer.start(); +producer.start(); + +``` + +위 코드에서 `queue.take()`는 큐가 비어 있으면 **무한 대기(block)** 한다 + +`"STOP"` 같은 종료 신호가 큐에 **들어오지 않으면**, + +**consumer 스레드는 끝나지 못하고 영원히 대기 상태에 빠짐.** + +--- + +→ 종료 코드를 개발 초기부터 고민하고 동작하게 초기부터 구현하라. 생각보다 오래 걸린다. 생각보다 어려우므로 이미 나온 알고리즘을 검토하라 + +
+ +### 스레드 코드 테스트하기 + +- 문제를 노출하는 테스트 케이스를 작성하라 +- 프로그램 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라 +- 테스트가 실패하면 원인을 추적하라 +- 다시 돌렸더니 통과하더라는 이유로 그냥 넘어가면 절대로 안 된다 + +아래 지침들로 여러가지를 고려해보자 + +
+ +1. 말이 안되는 실패는 잠정적인 스레드 문제로 취급하라 +- 다중 스레드 코드는 때때로 ‘말이 안 되는’ 오류를 일으킨다 +- 스레드 코드에 잡입한 버그는 수천, 아니 수백만 번에 한 번씩 드러나기도 한다 + +2. 다중 스레드를 고려하지 않은 순차 코드부터 제대로 돌게 만들자 +- 스레드 환경 밖에서 코드가 제대로 도는지 반드시 확인한다 +- 스레드 환경 밖에서 생기는 버그와 스레드 환경에서 생기는 버그를 동시에 디버깅하지 마라 +- 먼저 스레드 환경 밖에서 코드를 올바로 돌려라 + +3. 다중 스레드를 쓰는 코드 부분을 다양한 환경에 쉽게 끼워 넣을 수 있게 스레드 코드를 구현하라 +- 스레드 코드를 실제 환경이나 테스트 환경에서 둘러본다 +- 빨리, 천천히, 다양한 속도로 돌려본다 + +4. 다중 스레드를 쓰는 코드 부분을 상황에 맞게 조율할 수 있게 작성하라 +- 프로그램 처리율과 효율에 따라 스스로 스레드 개수를 조율하는 코드도 고민한다 + +5. 프로세서 수보다 많은 스레드를 돌려보라 +6. 다른 플랫폼에서 돌려보라 +7. 코드에 보조 코드를 넣어 돌려라. 강제로 실패를 일으키게 해보라 +- 스레드 버그가 산발적이고 우발적이고 재현이 어려운 이유는 코드가 실행되는 수천 가지 경로 중에 아주 소수만 실패하기 때문이다 +- Object.wait(), Object.sleep(), Object.yield(), Object.priority 등과 같은 메서드를 추가해 코드를 다양한 순서로 실행한다 + +
+ +### 보조 코드 추가 + +**직접 구현하기** + +- 보조 코드를 삽입할 적정 위치를 직접 찾아야 한다 +- 어떤 함수를 어디서 호출해야 적당할까 +- 배포 환경에 보조 코드를 그대로 남겨두면 프로그램 성능이 떨어진다 +- 무작위적이다. 오류가 드러날지도 모르고 드러나지 않을지도 모른다 +- 배포 환경이 아니라 테스트 환경에서 보조 코드를 실행할 방법이 필요하다 + +
+ +**자동화** + +- AOF, CGLIB, ASM 등과 같은 도구를 사용한다 + +image + +- jiggle은 무작위로 sleep나 yield를 호출한다 +- 코드를 흔드는 이유는 스레드를 매번 다른 순서로 실행하기 위해서다 +- 이는 오류가 드러날 확률을 크게 높여준다 + +
+ +### 결론 + +- 다중 스레드 코드를 작성한다면 각별히 깨끗하게 코드를 짜야 한다 +- POJO를 사용해 스레드를 아는 코드와 스레드를 모르는 코드를 분리한다 +- 동시성 오류를 일으키는 잠정적인 원인을 철저히 이해한다 +- 특정 라이브러리 기능이 기본 알고리즘과 유사한 어떤 문제를 어떻게 해결하는지 파악한다 +- 보호할 코드 영역을 찾아내는 방법과 특정 코드 영역을 잠그는 방법을 이해한다 +- 스레드 코드는 출시하기 전까지 최대한 오랫동안 돌려봐야 한다