Spring Container
📌 Spring으로 구성된 애플리케이션에서 객체(Bean)를 생성, 관리, 소멸하는 역할을 담당한다. 애플리케이션 시작 시, 설정 파일이나 Annotation을 읽어 Bean을 생성하고 주입하는 모든 과정을 컨트롤한다. 심지어는 의존성마저 주입한다.
- 총괄주방장 = shef 라고 보면 편하다.
- Spring Container를 사용하면 인터페이스에만 의존하는 설계가 가능해진다.
- OCP, DIP 준수
Spring Container의 종류
- BeanFactory
- Spring Container의 최상위 인터페이스
- Spring Bean을 관리하고 조회한다.
- ApplicationContext
- BeanFactory의 확장된 형태(implements) -> 진화된 버전
- Application 개발에 필요한 다양한 기능을 추가적으로 제공한다.
- 국제화, 환경변수 분리, 이벤트, 리소스 조회
일반적으로 ApplicationContext를 사용하기 때문에 ApplicationContext를 Spring Container라 표현한다. |
Spring Bean
📌 Spring 컨테이너가 관리하는 객체를 의미한다. 자바 객체 자체는 특별하지 않지만, Spring이 이 객체를 관리하는 순간부터 Bean이 된다. Spring은 Bean을 생성, 초기화, 의존성 주입 등을 통해 관리한다.
- 모든 객체가 Bean인게 아니라 spring container가 관리하는 객체를 bean이라 하는 것
- Bean은 new 키워드 대신 사용하는 것이다.
- Spring Container가 제어한다.
Spring Bean의 역할
- Chef인 Spring Container가 요리할 음식에 사용될 재료(Bean)
- 요리(Application)의 핵심을 이루는 재료(Bean)
- Spring Bean의 특징
- Spring 컨테이너에 의해 생성되고 관리된다.
- 기본적으로 Singleton( 애플리케이션 전역에서 단 하나의 인스턴스만 생성하도록 보장하는 디자인 패턴 )으로 설정된다.
- 의존성 주입(DI)을 통해 다른 객체들과 의존 관계를 맺을 수 있다.
- 생성, 초기화, 사용, 소멸의 생명주기를 가진다.
Bean 등록 방법
- XML, Java Annotation, Java 설정파일 등을 통해 Bean으로 등록할 수 있다.
- XML
<beans>
<!-- myBean이라는 이름의 Bean 정의 -->
<bean id="myBean" class="com.example.MyBean" />
</beans>
--------------------------------------------------------------------
public class MyApp {
public static void main(String[] args) {
// Spring 컨테이너에서 Bean을 가져옴
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
MyService myService = context.getBean("myService", MyService.class);
myService.doSomething();
}
}
- Annotation
- @ComponentScan
- 개발자가 일일이 빈(Bean)을 설정하지 않아도, 지정한 패키지에서 필요한 클래스들을 자동으로 찾아서 애플리케이션 컨텍스트에 빈(Bean) 등록해 줍니다.
// 이 클래스를 Bean으로 등록
// @Controller, @Service, @Repository
@Component
public class MyService {
public void doSomething() {
System.out.println("Spring Bean 으로 동작");
}
}
------------------------------------
@Component
public class MyApp {
private final MyService myService;
@Autowired // 의존성 자동 주입
public MyApp(MyService myService) {
this.myService = myService;
}
public void run() {
myService.doSomething();
}
}
---------------------------------------------
// com.example 패키지를 스캔하여 Bean 등록
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyApp app = context.getBean(MyApp.class);
app.run();
}
}
Java 설정파일
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
------------------------------------
public class MyApp {
public static void main(String[] args) {
// Spring 컨테이너에서 Bean을 가져옴
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = context.getBean(MyService.class);
myService.doSomething();
}
}
IOC(제어의 역전, Inversion Of Control)
📌 객체의 생성과 관리 권한을 개발자가 아닌 Spring 컨테이너가 담당하는 것을 말한다. 기본적으로 개발자가 객체를 직접 생성하고 관리했지만, Spring에서는 컨테이너가 객체 생성, 주입, 소멸을 관리한다.
- 요리사(개발자)는 필요한 재료를 직접 준비하지 않고, Chef가 알아서 필요한 재료(Bean)을 관리하고 요리사에게 가져다준다.
IoC 개념
- 객체의 생성 및 생명주기 관리를 개발자가 직접 하는 것이 아니라 컨테이너가 담당한다.
- 객체 간의 결합도를 낮춰 유연한 코드가 된다.
DI(의존성 주입, Dependency Injection)
📌 Spring이 객체 간의 의존성을 자동으로 주입해주는 것을 의미한다. 한 객체가 다른 객체를 사용할 때, 해당 객체를 직접 생성하지 않고 Spring이 주입해주는 방식이다. IOC를 구현하는 방식 중 하나이다.
- 셰프가 요리를 만들 때 필요한 재료(Bean)를 자동으로 요리사에게 가져다주는 과정
- 요리사(개발자)는 재료를 찾을 필요 없이, Chef가 알아서 제공해준다.
의존성 = 추상화가 아닌 이유
의존성이란, 한 객체가 다른 객체를 사용하거나 그 객체의 기능에 의존하는 관계를 말합니다. 그리고 추상화는 의존성을 설계하는 중요한 방식 중 하나입니다. 하지만 의존성이 항상 추상화와 동일한 개념은 아닙니다. 의존성과 추상화의 관계를 설명하겠습니다.
- 의존성은 설계의 결과, 추상화는 설계의 방법:
- 의존성은 클래스 간의 관계를 나타냅니다.
- 추상화는 의존성을 관리하거나 줄이기 위한 설계 기법입니다.
- 구체적인 의존성도 존재:
- 의존성이 항상 추상화된 타입에만 연결되지 않습니다.
- 구체 클래스에 대한 의존성도 의존성의 한 형태입니다.
IOC, DI 주입 개발자, SPRING 비교
// Service 인터페이스
public interface MyService {
void doSomething();
}
// Repository 인터페이스
public interface MyRepository {
void queryDatabase();
}
// Service 구현체
public class MyServiceImpl implements MyService {
private MyRepository myRepository;
// 의존성 주입
public MyServiceImpl(MyRepository myRepository) {
this.myRepository = myRepository;
}
@Override
public void doSomething() {
System.out.println("서비스 작업 실행");
myRepository.queryDatabase();
}
}
// Repository 구현체
public class MyRepositoryImpl implements MyRepository {
@Override
public void queryDatabase() {
System.out.println("데이터베이스 쿼리 실행");
}
}
public class MyApp {
public static void main(String[] args) {
MyRepository repo = new MyRepositoryImpl();
// MyRepository repo2 = new MyRepositoryImplV2();
MyService myService = new MyServiceImpl(repo);
// MyService myService2 = new MyServiceImpl(repo2);
myService.doSomething();
}
}
// 새로운 Repository 구현체
public class MyRepositoryImplV2 implements MyRepository {
@Override
public void queryDatabase() {
System.out.println("데이터베이스 쿼리 실행 V2");
}
}
---------------------------------
// Service 구현체
@Service
public class MyIocService implements MyService {
private final MyRepository myRepository;
// 생성자 주입(DI 적용)
@Autowired: 스프링이 자동으로 MyRepository 타입의 빈(Bean)을 찾아 생성자에 주입하도록 지시합니다.
public MyIocService(MyRepository myRepository) {
this.myRepository = myRepository;
}
@Override
public void doSomething() {
System.out.println("IOC 서비스 작업 실행");
myRepository.queryDatabase();
}
}
// Repository 구현체
@Repository
public class MyIocRepository implements MyRepository {
@Override
public void queryDatabase() {
// 데이터베이스와 상호작용
System.out.println("IOC 데이터베이스 쿼리 실행");
}
}
// Spring Container 관리(IoC 적용)
@ComponentScan(basePackages = "com.example")
: @ComponentScan으로 지정된 com.example 패키지를 스캔하여 빈으로 등록할 클래스들을 검색합니다.
public class MyIocApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MyIocApp.class);
// Service 빈을 가져와서 실행
MyService service = context.getBean(MyService.class);
service.doSomething();
}
}
// 새로운 Repository 구현체
@Repository
public class MyIocRepositoryV2 implements MyRepository {
@Override
public void queryDatabase() {
// 데이터베이스와 상호작용
System.out.println("IOC 데이터베이스 쿼리 실행 V2");
}
}
- 구현 코드가 변경되어도 클라이언트의 코드에는 영향이 없다.
- 다른 구현체를 구현하여 Bean으로 등록하면 자유롭게 변경이 가능하다.
- 위 예시 코드는 @Repository 로 등록된 빈이 중복되어 충돌이 발생한다.
- 의존성 주입(DI), 제어의 역전(IOC)을 통해 객체 간의 결합도를 낮추고 유연한 설계가 가능해진다.
IOC/DI
- IoC는 객체의 제어권을 개발자가 아닌 Spring 컨테이너에게 넘기는 개념으로, Spring이 객체 생성과 관리를 담당한다.
- DI는 Spring이 객체 간의 의존성을 자동으로 주입해주는 기법이다.
- 의존관계 주입은 객체 간의 결합도를 낮추고 코드의 유연성과 테스트 가능성을 높여준다.
Singleton Pattern
📌 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴이다.
public class MainApp {
public static void main(String[] args) {
// 첫 번째 싱글톤 인스턴스 요청, 구현클래스.getInstance();
Singleton instance1 = SingletonImpl.getInstance();
instance1.showMessage(); // 인스턴스 주소값 출력
// 두 번째 싱글톤 인스턴스 요청, 구현클래스.getInstance();
Singleton instance2 = SingletonImpl.getInstance();
instance2.showMessage(); // 인스턴스 주소값 출력
// 다른 구현체로 바꾸려면 DIP, OCP 위반
Singleton instance3 = SingletonImplV2.getInstance();
instance3.showMessage();
}
}
- 위처럼 인스턴스가 클래스마다 1개씩만 배치된다.
- 다만 보면 알겠지만, 클래스의 개수가 많아지는 것 같은 느낌이 들 것이다.
싱글톤 패턴의 문제점
- 싱글톤 패턴을 구현하기 위한 코드의 양이 많다.
- 구현 클래스에 의존해야 한다.(DIP, OCP 위반)
- 유연성이 떨어져서 안티패턴으로 불리기도 한다.
Spring의 싱글톤 컨테이너
- Spring은 Web Application을 만들 때 주로 사용된다.
- Spring Container는 싱글톤 패턴의 문제점들을 해결하면서 객체를 싱글톤으로 관리한다.
- Spring Bean은 싱글톤으로 관리되는 객체이다.
- Spring이 Bean을 등록하는 방법은 기본적으로 싱글톤 이다. 하지만, 요청할 때 마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.
Spring Bean이 싱글톤을 사용하는 이유
스프링에서 Bean을 기본적으로 싱글톤으로 사용하는 이유는 싱글톤 패턴의 장점을 최대한 활용하면서도, 패턴 자체의 단점들을 해결하기 때문입니다. 이를 단계적으로 설명하겠습니다.
싱글톤 패턴의 장점
- 리소스 절약:
- 객체를 한 번만 생성하고 재사용하므로 메모리와 리소스를 절약할 수 있습니다.
- 전역 접근 가능:
- 애플리케이션 전체에서 동일한 인스턴스를 사용할 수 있어 상태 관리나 공유 자원 관리가 용이합니다.
싱글톤 패턴의 단점과 스프링의 해결책
1. 구현이 복잡하다 (코드가 많다)
- 문제점:
- 싱글톤 패턴을 구현하려면 추가적인 코드(정적 변수, 동기화 처리 등)가 필요합니다.
- 이를 잘못 구현하면 멀티스레드 환경에서 안전하지 않을 수 있습니다.
- 스프링의 해결:
- 스프링 컨테이너가 싱글톤 관리를 대신합니다.
- 개발자는 객체 생성 방식을 신경 쓸 필요 없이, @Component, @Service, @Repository 등의 애너테이션만 추가하면 스프링이 자동으로 관리합니다.
2. 구현 클래스에 의존 (DIP, OCP 위반)
- 문제점:
- 싱글톤 패턴은 클래스 자체에서 객체를 관리하기 때문에 구체 클래스에 의존하게 됩니다.
- 이는 **의존성 역전 원칙(DIP)**과 **개방-폐쇄 원칙(OCP)**을 위반할 가능성을 높입니다.
- 스프링의 해결:
- 스프링은 **DI(Dependency Injection)**를 사용하여 의존성을 주입합니다.
- Bean을 인터페이스 기반 설계로 사용할 수 있게 하여 DIP를 준수합니다.
- 개발자는 객체 생성 방식을 몰라도 컨테이너가 관리하는 객체를 사용할 수 있으므로 OCP를 준수합니다.
3. 테스트 어려움
- 문제점:
- 싱글톤 객체는 전역 상태를 공유하기 때문에 테스트에서 객체의 상태를 초기화하거나 Mock 객체로 대체하기 어렵습니다.
- 스프링의 해결:
- 스프링은 DI를 통해 의존성을 주입하므로, 테스트 환경에서 Mock 객체를 쉽게 주입할 수 있습니다.
- 빈의 스코프를 싱글톤 이외로 설정(예: 프로토타입 스코프)할 수도 있습니다.
4. 유연성 부족 (안티패턴)
- 문제점:
- 전역 상태를 공유하는 싱글톤 객체는 설계의 유연성을 저하시켜 애플리케이션의 확장과 변경에 제약을 줄 수 있습니다.
- 스프링의 해결:
- 스프링은 필요에 따라 Bean의 **스코프(scope)**를 변경할 수 있습니다:
- 싱글톤(Singleton): 기본 설정, 모든 요청에서 동일한 인스턴스를 공유.
- 프로토타입(Prototype): 요청 시마다 새로운 인스턴스 생성.
- 요청(Request): HTTP 요청마다 새로운 인스턴스 생성.
- 세션(Session): HTTP 세션마다 새로운 인스턴스 생성.
- 웹 소켓(WebSocket): 웹소켓 연결마다 새로운 인스턴스 생성.
- 스프링은 필요에 따라 Bean의 **스코프(scope)**를 변경할 수 있습니다:
스프링이 싱글톤을 사용하는 이유
스프링은 싱글톤 패턴의 단점을 해결하면서도, 다음과 같은 이유로 싱글톤을 기본으로 사용합니다:
- 효율성:
- 애플리케이션에서 동일한 빈을 여러 번 생성하지 않고, 한 번 생성된 객체를 재사용하여 리소스를 절약합니다.
- 글로벌 상태 관리:
- 데이터베이스 연결, 캐시, 설정 정보 등 전역적으로 공유할 필요가 있는 객체를 관리하기 적합합니다.
- 유연성:
- 스프링은 개발자가 직접 싱글톤 패턴을 구현하지 않아도, 컨테이너가 이를 관리합니다.
- 필요에 따라 스코프를 변경하거나 테스트 시 Mock 객체로 대체할 수 있습니다.
- 단순화된 코드:
- 개발자는 @Component, @Service 등 애너테이션만으로 빈을 정의하고 사용할 수 있습니다. 싱글톤 관리 로직을 작성할 필요가 없습니다.
결론
스프링은 싱글톤 패턴의 장점을 최대한 활용하면서, **DI(의존성 주입)**와 유연한 스코프 관리를 통해 단점들을 극복했습니다. 이로 인해 스프링의 Bean은 싱글톤을 기본으로 사용하면서도 효율적이고 확장 가능한 구조를 제공합니다.
Singleton Pattern의 주의
📌 객체의 인스턴스를 하나만 생성하여 공유하는 싱글톤 패턴의 객체는 상태를 유지(stateful)하면 안된다.
- 지역 변수라면 변수여도 된다. 하지만 요청마다 값이 변경되거나 외부에서 변경 가능한 데이터가 싱글톤 객체에 저장되면 문제가 생길 수 있다.
- 즉 공유 객체( 여러 사용자나 스레드가 동시에 접근하여 값을 읽거나 수정할 수 있는 데이터 )는 안된다.
상태 유지(stateful)의 문제점
- 데이터의 불일치나 동시성 문제가 발생할 수 있다.
- 코드예시
public class StatefulSingleton {
private static StatefulSingleton instance;
// 상태를 나타내는 필드
private int value;
// private 생성자
private StatefulSingleton() {}
// 싱글톤 인스턴스를 반환하는 메서드
public static StatefulSingleton getInstance() {
if (instance == null) {
instance = new StatefulSingleton();
}
return instance;
}
// 상태 변경 메서드
public void setValue(int value) {
this.value = value;
}
// 상태를 반환하는 메서드
public int getValue() {
return this.value;
}
}
public class MainApp {
public static void main(String[] args) {
// 클라이언트 1: 싱글톤 인스턴스를 가져와서 상태를 설정
StatefulSingleton client1 = StatefulSingleton.getInstance();
client1.setValue(42);
System.out.println("클라이언트 1이 설정한 값: " + client1.getValue());
// 클라이언트 2: 동일한 싱글톤 인스턴스를 사용해 상태를 변경
StatefulSingleton client2 = StatefulSingleton.getInstance();
client2.setValue(100);
System.out.println("클라이언트 2가 설정한 값: " + client2.getValue());
// 클라이언트 1이 다시 값을 확인
System.out.println("클라이언트 1이 다시 확인한 값: " + client1.getValue());
}
}
클라이언트 1이 설정한 값: 42 클라이언트 2가 설정한 값: 100 클라이언트 1이 다시 확인한 값: 100 |
- value 필드는 공유되는 필드인데, 특정 클라이언트가 값을 변경한다.
- Spring Bean은 항상 무상태(stateless)로 설계를 해야한다. 아주 중요!
- 특정 클라이언트에 의존적인 필드가 있거나 변경할 수 있으면 안된다.
'Back-End (Web) > Spring' 카테고리의 다른 글
[Spring] 의존관계 주입 (0) | 2024.12.26 |
---|---|
[Spring] Bean 등록 (0) | 2024.12.25 |
[Spring] Layered Architecture (2) | 2024.12.20 |
[Spring] Server에서 Client로 Data를 전달하는 방법 (0) | 2024.12.19 |
[Spring] HTTP Message Body & TEXT (0) | 2024.12.18 |