1. Java String으 불변성
1. 불변 객체란?
객체가 생성된 이후 내부 상태를 변경할 수 없는 객체
String str = "Hello";
str = str + "World";
위 코드를 보면 기존 String 객체에 "World"가 추가된 것 같지만 실제로 기존 객체가 변경된 것은 아니다.Hello World라는 새로운 객체가 만들어지고 str이 이 새로운 객체를 참조하게 된다.
즉, 한 번 생성된 Hello 객체 자체의 값은 바뀌지 않는다.
2. String을 불변 객체로 설계한 이유
String은 파일 경로, URL, DB 연결 정보, 클래스 이름, 사용자 정보, 네트워크 주소 등 Java 프로그램의 다양한 영역에서 사용된다.
또한 String Pool 을 통해 동일한 문자열 객체를 여러 곳에서 공유할 수 있다.
따라서 한 곳에서 String 의 값을 변경하면 같은 객체를 참조하는 다른 코드까지 영향을 줄 수 있기 때문에 자바는 String을 불변 객체로 만들어 이러한 문제를 방지한다.
3. String 불변성 장점
| 장점 | 이유 |
| 보안 | 전달된 문자열 객체의 값이 임의로 변경되는 것을 방지 |
| 동시성 | 상태가 변경되지 않아 여러 스레드에서 안전하게 공유 가능 -> Thread-Safe |
| Hash Key 안정성 | 객체의 값과 hashCode()가 변경되지 않음 |
| 해시 캐싱 | 한 번 계산한 해시값을 재사용할 수 있음 |
| String Pool | 동일 객체를 여러 곳에서 안전하게 공유 가능 |
2. Spring 싱글톤 빈 과 Thread Safety
1. Spring의 싱글톤 빈
스프링 빈은 별도의 스코프를 지정하지 않으면 기본적으로 싱글톤 스코프로 관리된다.
@Service
public class OrderService{
}
스프링 컨테이너는 일반적으로 빈의 인스턴스를 하나 생성하고 여러 요청에서 같은 객체를 공유한다.
Request A ─┐
Request B ─┼──> OrderService Bean
Request C ─┘
웹 서버에서는 여러 요청이 서로 다른 스레드에서 동시에 처리될 수 있어 하나의 객체를 공유한다면 동시성 문제가 발생할 것처럼 보이지만 싱글톤 자체가 Thread-Safe하기 때문이 아니라 빈은 Stateless하게 설계하기 때문에 대부분 안전하게 사용할 수 있다.
Thread-Safe 란?
여러 스레드가 동시에 같은 객체나 자원에 접근하더라고 데이터가 꼬이거나 예상하지 못한 결과가 발생하지 않는 상태
즉, 여러 스레드가 동시에 접근하더라도 데이터의 일관성과 올바른 실행 결과가 보장되는 것.
2. Stateless한 싱글톤 빈
주문 서비스가 있다고 가정해보자.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public Order findOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow();
return order;
}
}
orderId 와 order 는 메서드의 지역 변수이다.
각 요청을 처리하는 스레드는 자신의 Stack 영역을 가지고 있으며 메서드 지역 변수는 각 스레드의 Stack에서 관리된다.

두 스레드가 동일한 OrderService 객체를 사용하더라도 요청별 데이터가 Service 필드에 저장되지 않아 두 요청이 서로 영향을 주고받지 않는다.
3. 싱글톤 빈에 변경 가능한 필드를 둔다면?
반대로 요청 데이터를 필드에 저장한다고 가정해보자.
@Service
public class OrderService {
private Long currentOrderId;
public void process(Long orderId) {
this.currentOrderId = orderId;
// 주문 처리
System.out.println(currentOrderId);
}
}
currentOrderId 는 OrderService 객체의 필드이므로 모든 스레드가 공유한다.

다음과 같은 상황이 발생할 수 있다.

Thread A는 자신이 저장한 1 을 기대했지만 중간에 Thread B 가 2로 변경했기 때문에 잘못된 값을 사용하게 된다.
이렇게 여러 스레드가 공유하는 상태에 동시에 접근하면서 실행 결과가 실행 순서에 따라 달라지는 문제를 Race Condition 이라고 한다.
4. final 필드는 괜찮은가?
스프링 서비스에서는 다음과 같은 코드를 매우 자주 사용한다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
}
orderRepository 역시 필드인데 왜 문제가 되지 않을까?
여기서 중요한 점은 필드가 존재하느냐가 아니라 공유되는 변경 가능한 상태가 존재하느냐이다.
final 은 orderRepository가 참조하는 객체를 다른 객체로 재할당하지 못하게 한다.
그리고 일반적인 스프링의 Repository나 Service 역시 요청별 데이터를 자신의 필드에 저장하지 않는 Stateless 한 구조로 설계한다.
5. 지역변수와 인스턴스 필드의 차이
싱글톤 빈의 Tread Safety를 이해할 때 핵심은 공유되는 상태인지의 여부이다.
| 구분 | 여러 스레드 간 공유 | 일반적인 안전성 |
| 메서드 지역 변수 | X | 안전 |
| 메서드 파라미터 | X | 안전 |
| 불변 객체 | 공유 가능 | 안전 |
| 변경 가능한 인스턴스 필드 | O | 주의 필요 |
| static 변경 가능 필드 | O | 주의 필요 |
따라서 스프링의 싱글톤 빈에서는 요청별 상태를 인스턴스 필드에 저장하지 않고 메서드 파라미터와 지역 변수를 중심으로 처리하는 것이 중요하다.
'CS & Algorithm > CS' 카테고리의 다른 글
| [Plus] Let’s Encrypt 인증서를 적용하면 HTTPS는 어떻게 동작할까? (0) | 2026.08.30 |
|---|---|
| [CS] 가상 메모리와 HTTP vs. HTTPS (0) | 2026.08.30 |
| [CS] 데이터베이스 트랜잭션 격리 수준과 스택 & 큐 (0) | 2026.08.24 |
| [CS] 컨텍스트 스위칭과 DNS (0) | 2026.08.24 |
| [CS] Java의 예외와 스프링의 트랜잭션 (0) | 2026.08.23 |