스프링 프레임워크는 필요한 클래스를 의존주입(Dependency Injection)하여 사용한다. 이때 주입하여 생성된 객체는 싱글톤으로 생성되어 스프링 컨테이너에서 한번 생성하여 관리한다.
- 싱글톤 Bean은 스프링 컨테이너에서 한번 생성된다. (컨테이너가 사라질 때 Bean도 제거된다)
- 생성된 하나의 인스턴스는 Single-Beans-Cache에 저장되고, 해당 Bean에 대한 요청과 참조가 있으면 캐시된 객체를 반환한다.
- 즉, 하나만 생성되기 때문에 동일한 것을 참조한다. 기본적으로 모든 Bean은 Scope이 명시적으로 지정되지 않는다면, Singleton이다. 단, ProtoType으로 빈이 생성된다면, 다수의 객체로써 존재하게 된다
이슈상황 코드 확인
controller
package com.test.controller;
import com.test.service.TestService;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
public class TestController {
private final TestService testService;
@Autowired
public TestController(TestService testService) {
this.testService = testService;
}
@GetMapping(value = "/test")
public void test() {
testService.getCurrentNumber1();
testService.getCurrentNumber2();
testService.getCurrentNumber3();
}
}
service
package com.test.service;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import com.test.mapper.TestMapper;
@Service
public class TestService {
private final String myNumber = 0;
private final TestMapper testMapper;
@Autowired
public TestService(TestMapper testMapper) {
this.testMapper = testMapper;
}
// 첫번째 메소드
public getCurrentNumber1() {
// 첫번째 값등록 및 호출
this.myNumber = 30000;
System.out.println(this.myNumber);
// 두번째 값등록 및 호출
this.myNumber = 25000;
System.out.println(this.myNumber);
// 세번째 값등록 및 호출
this.myNumber = 20000;
System.out.println(this.myNumber);
}
// 두번째 메소드
public getCurrentNumber2() {
// 첫번째 값등록 및 호출
this.myNumber = 40000;
System.out.println(this.myNumber);
// 두번째 값등록 및 호출
this.myNumber = 35000;
System.out.println(this.myNumber);
// 세번째 값등록 및 호출
this.myNumber = 33000;
System.out.println(this.myNumber);
}
// 세번째 메소드
public getCurrentNumber3() {
// 첫번째 값등록 및 호출
this.myNumber = 50000;
System.out.println(this.myNumber);
// 두번째 값등록 및 호출
this.myNumber = 48000;
System.out.println(this.myNumber);
// 세번째 값등록 및 호출
this.myNumber = 45000;
System.out.println(this.myNumber);
}
}
Mapper와 같은 Class들은 인스턴스 변수에 선언하여 @Autowired를 통해 Bean에 등록된 Class를 주입하여 사용한다. (메모리주소값 1개를 지니며, 하나만 생성해서 사용)
그렇자면, 인스턴스 변수에 int나 String Class같은 타입을 사용한다면 어떨까?
이 값들이 변경없이 공통적으로 사용하는 값이라면, 위와같이 선언하여 사용해도 문제가 없을 것이다. 하지만 값이 수정된다면? 1번 프로세스에서 값이 수정된다면, 2번 프로세스에서도 해당 값을 참조(동일한 메모리주소)하기 때문에 영향을 받을 것이다.
물론 1번프로세스가 정상 실행되고 각각 2번, 3번.. 순차적으로 실행된다면 문제없이 값이 잘 리턴될 것이다.
하지만, 동시에 프로세스가 호출이 된다고 한다면..?
1번프로세스 30,000 → 25,000 → 20,000 리턴: 20,000
2번프로세스 40,000 → 35,000 → 33,000 리턴: 33,000
3번프로세스 50,000 → 48,000 → 45,000 리턴: 45,000
로 예상할 수 있다.
- 1번째 실행결과
1번프로세스 30,000 → 25,000 → 20,000 리턴: 33,000
2번프로세스 40,000 → 35,000 → 33,000 리턴: 33,000
3번프로세스 50,000 → 48,000 → 45,000 리턴: 40,000
- 2번째 실행결과
1번프로세스 30,000 → 25,000 → 20,000 리턴: 20,000
2번프로세스 40,000 → 35,000 → 33,000 리턴: 33,000
3번프로세스 50,000 → 48,000 → 45,000 리턴: 33,000
- 3번째 실행결과
1번프로세스 30,000 → 25,000 → 20,000 리턴: 35,000
2번프로세스 40,000 → 35,000 → 33,000 리턴: 20,000
3번프로세스 50,000 → 48,000 → 45,000 리턴: 45,000
위와같은 결과가 도출된다. (결과는 그때그때 달라짐)
원인 및 해결방법
- 원인: myNumber라는 인스턴스변수를 사용한 결과 위와같은 문제점이 발생하였다. 최초 빌드 컴파일 진행시, 한번 객체를 생성하고난 뒤에 초기화가 진행되지 않는다. 하나의 인스턴스변수를 공유하기 때문에, 조회하는 시점에 다른 프로세스에 간섭이 일어날 수 있는 것이다.
- 해결방법
1] 빈을 싱글톤 타입이 아닌 프로토 타입으로 생성하도록 변경
=> 대용량 트래픽의 프로젝트에서 이방법을 사용한다면, 프로토 타입으로 빈생성은 메모리 릭 발생 가능성이 존재하므로, 적절하지 않다.
2] 인스턴스변수를 지역변수로 변경 => 변경
=> 응답 데이터 DTO가 new 객체생성(in Heap Memory)을 통해 생성되며, 메소드가 종료되면 GC에 의해 자동으로 메모리에서 제거됨. 독립적인 메모리 주소값을 참조하므로, 타프로세스의 간섭이 없음.
참고자료
- https://developyo.tistory.com/159
- https://gmlwjd9405.github.io/2018/11/10/spring-beans.html
'SpringFramework > Spring' 카테고리의 다른 글
Spring - Mybatis 샵(#)과 달러($)의 차이 (0) | 2022.03.10 |
---|---|
Spring - Mybatis FrameWork 여러 스키마 적용하기 (0) | 2022.02.08 |
Spring - Redis 연동 (0) | 2021.10.20 |
Spring - Validation (0) | 2021.10.20 |
Spring - ErrorController (0) | 2021.10.15 |