it 서적 독후감

Java 병렬프로그래밍 chap 1

데일리코딩 2025. 2. 24. 22:57

1. 서론 (왜 이 책을 읽게 되었는가)

 

스레드에 대해 김영한님의 강의를 보고 포스팅을 작성하며 여러 번 내용을 확인했지만,

결국 스레드의 전반적인 큰 틀과 사용법 정도만 익힌 느낌을 받았다.

 

실무에서 사용하는 데 큰 문제가 없다고 생각했지만,

제 개인적인 부족한 견해일 수도 있다는 생각에

좀 더 깊이 파고들고자 이펙티브 자바의 저자 조슈아 블로크가 추천한 **“자바 병렬프로그래밍”**이라는 책을 읽기 시작했다.

사실, IT 업계에서 유명한 모 기업에 근무하셨던 개발자 분이 추천해 주신 책이다.

 

그분의 말씀에 따르면, 이 책을 통해 내가 작성하는 코드가 Thread Safe인지 아닌지 판단할 기준을 마련해 주었다고 한다.

 

또한, 자바는 태생부터 병렬 프로그래밍을 염두에 둔 언어이다.

그 증거로, 자바의 옛 컬렉션 중 Vector 컬렉션을 보면 동기화 처리가 많이 되어 있는데,

(Vector는 현재 사용을 권장하지 않는 클래스라고 소개되어 있다.)

이와 같이 자바는 1.5 버전부터 동기화를 지원하는 java.util.concurrent 패키지를 도입하여,

많은 Thread Safe 컬렉션, 유틸리티, 스레드 풀 등을 제공하고 있다.

이러한 자바의 히스토리와 어떤 코드가 Thread Safe한 코드인지, 현재는 어떻게 동기화를 하는 것이 좋은 방법인지 알아보고자 한다.

 

개발 경력이 쌓일수록 기초적인 지식이 더 중요하다는 것을 절실히 느끼고 있는 지금,

이 책은 많은 도움이 될 것이라고 생각한다.

 

단, 이 글은 스레드를 처음 접하는 분들에게는 다소 이해하기 어려운 부분이 있을 수 있다.

만약 스레드를 처음 입문한다면, 김영한님의 자바 고급편 스레드 강의를 보시거나

[The Java Programming Language] 책을 추천한다.


2. 왜 스레드를 사용하는가

 

스레드를 제대로 사용한다면, 개발 및 유지보수 비용을 줄이고 복잡한 애플리케이션의 성능을 향상시킬 수 있다.

GUI 애플리케이션: 사용자 인터페이스의 반응 속도를 빠르게 할 수 있다.

서버 애플리케이션: 자원 활용도와 처리율을 높이는 데 유용하다.

JVM 구현 단순화: 스레드를 활용하면 JVM을 더 단순하게 구현할 수 있도록 도와준다.

 

추상적인 이야기보다 스레드를 활용한 예제를 보면 이해가 더 쉽다.

예를 들어, 단순한 비동기 처리의 경우를 들어보자.

 

여러 클라이언트 프로그램에서 소켓 연결을 받는 서버 애플리케이션에서는,

각 연결마다 스레드를 할당하여 I/O 작업을 수행하면 개발 작업이 쉬워진다.

데이터가 없을 때 소켓에서 읽으려고 하면, 애플리케이션은 추가 데이터가 들어오기 전까지 read 연산에서 대기하게 된다.

 

만약 스레드가 하나뿐이라면, 해당 요청에서 작업이 멈출 뿐만 아니라 다른 모든 요청도 처리하지 못하고 정지하게 된다.

이런 문제를 피하기 위해, 단일 스레드 서버 프로그램에서는 복잡하고 실수하기 쉬운 넌블로킹 I/O 기능을 사용해야 하지만,

각 요청을 별도의 스레드에서 처리하면 대기 상태에 들어가도 다른 스레드가 요청을 처리하는 데 별다른 영향을 주지 않는다.

 


3. 스레드 안정성

 

스레드 안정성을 판단하기 어렵지만 구글에 검색하면 다음과 같이 나온다.

더보기

여러 프로그램 스레드에서 스레드 간에 원치 않는 상호 작용 없이 호출할 수 있는...

호출하는 측에서 다른 작업을 하지 않고도 여러 스레드에서 동시에 호출할 수 있는...

이런 정의를 보자면 안정성에 대한 개념이 헷갈린다. 

이번장에는 스레드 안정성을 위한 대원칙 기준을 제시한다

1. 해당 상태 변수를 스레드간에 공유하지 않는다

2. 해당 상태 변수를 변경할 수 없도록 만들거나

3. 해당 상태 변수에 접근할 땐 언제나 동기화를 사용한다. 

 

결국 안전한 클래스를 설계할땐, 바람직한 객체 지향 기법이 왕도다. 캡슐화와 불변 객체를 잘 활용하고, 불변 조건을 명확하게 기술해야한다. 

 

그렇다면 스레드안정성에 대한 기준에 대해 생각해보자

도대체 어떤 기준으로 해당 코드가 스레드 안정성을 갖춘다고 볼 수 있을까?

더보기

여러 스레드가 특정 클래스에 접근할 때, 실행 환경이 해당 스레드들의 실행을 어떻게 스케줄하든 어디에 끼워넣든, 호출하는 쪽에서

추가적인 동기화나 다른 조율 없이도 정확하게 동작하면 스레드 안정성을 갖추었다고 볼 수 있다.

 


4. 상태 없는 서블릿

 

이제 이론적으로 어떻게 스레드 안정성을 갖춰야 하는지 대원칙과 어떤 기준으로 해당 코드가 스레드 안정성을 갖추었다고 이야기 해보았다.

 

스레드 안정성을 갖추기 위해서는 직접 스레드를 생성하는 경우보다 서블릿 프레임웍 같은 수단을 사용하는 경우가 꽤 많다.

여기서 간단한 예제로 서블릿 기반 인수분해 서비스를 만들고, 스레드 안정성을 유지하면서 하나 하나 기능을 추가해보겠다.

 

public class StatelessFactorizer implements Servlet {
	public void service (ServletRequest req, ServletResponse res) {
    	BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        encodeIntoResponse(res, factors);
    }
}
// 상태없는 서블릿

 

 

StatelessFactorizer 클래스는 상태를 가지고 있지않다, 즉 선언한 변수가 없고 다른 클래스의 변수도 참조하지도 않는다.

특정 계산을 위한 일시적인 상태는 스레드 의 스택에 저장되는 지역 변수에만 저장하고, 실행하는 해당 스레드에서만 접근할 수 있다.

따라서 StatelessFactorizer에 접근하는 다른 스레드의 결과에 영향을 줄 수 없다. 두 스레드가 상태를 공유하지 않기 때문에 사실상 서로 다른 인스턴스에 접근하는 것과 같다. 상태 없는 객체에 접근하는 스레드가 어떤 일을 하든 다른 스레드가 수행하는 동작의 정확성에 영향을 끼질 수없기 때문에 상태 없는 객체는 항상 스레드 안정하다. 

 

public class UnsafeCountingFactorizer implements Servlet {

	private Long count;
    
        public Long getCoount() {
            return count;
        }
        
	public void service (ServletRequest req, ServletResponse res) {
    	BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        ++count;
        encodeIntoResponse(res, factors);
    }
}
// 안정하지 않는 카운팅Factorizer

 

 

왜 UnsafeCountingFactorizer 클래스는 스레드 안전하지 않을까?

 

UnsafeCountingFactorizer 클래스는 count라는 Long 타입의 상태 변수를 추가하면서 스레드 안전성을 확보하지 못하는 문제가 발생한다.

 

문제점: ++count 연산은 원자적(Atomic)이지 않다

++count 연산은 단순한 한 줄짜리 코드처럼 보이지만, 실제로는 다음과 같은 3단계의 연산이 포함된다.

1. count 변수의 현재 값을 읽는다.

2. 읽은 값에 +1을 더한다.

3. 새로운 값을 count 변수에 다시 저장한다.

 

이 과정에서 여러 스레드가 동시에 count 값을 변경하려 하면 데이터가 꼬일 위험이 있다.

 

스레드 간섭 문제 예제

 

예를 들어, count = 5인 상태에서 A 스레드와 B 스레드가 동시에 service() 메서드를 호출한다고 가정하자.

A 스레드가 count의 값을 읽음 (count = 5)

B 스레드도 거의 동시에 count 값을 읽음 (count = 5)

A 스레드가 count + 1을 수행하고 count = 6으로 저장

B 스레드가 count + 1을 수행하고 또 count = 6으로 저장 (A의 변경 사항이 반영되지 않음)

 

이 경우, count의 값은 원래 5 → 6 → 7이 되어야 하지만,

A와 B 스레드가 각각 5를 읽고 +1을 적용했기 때문에 6으로 덮어써지는 현상이 발생한다.

 

해결 방법

 

 synchronized 키워드 사용

public synchronized void service(ServletRequest req, ServletResponse res) {
    BigInteger i = extractFromRequest(req);
    BigInteger[] factors = factor(i);
    ++count;  // 동기화된 상태에서 실행됨
    encodeIntoResponse(res, factors);
}

 

synchronized 키워드를 메서드에 붙이면 객체(this) 자체의 모니터 락을 사용한다

즉, 한번에 하나의 스레드만 service() 메서드를 실행할 수 있다, 만약 다른 스레드가 service()를 실행하려 하면, 기존 스레드가 락

해제할 때 까지 대기해야한다.

 

 AtomicLong 사용 (권장)

import java.util.concurrent.atomic.AtomicLong;

public class SafeCountingFactorizer implements Servlet {
    private final AtomicLong count = new AtomicLong(0);

    public long getCount() {
        return count.get();
    }

    public void service(ServletRequest req, ServletResponse res) {
        BigInteger i = extractFromRequest(req);
        BigInteger[] factors = factor(i);
        count.incrementAndGet();  // 원자적 연산
        encodeIntoResponse(res, factors);
    }
}

 

AtomicLongincrementAndGet()원자적(Atomic) 연산을 수행하여 동기화 없이도 안전한 증가 연산을 보장한다.

자바 1.7부터 도입한 import java.util.concurrent 패키지는 기존 synchronized 키워드 사용보다 성능을 높이고 

스레드 안정성 까지 확보한 패키지다.