멍두의 개발새발
[Spring] 스프링 빈 왜 쓰세요 ? 본문
"스프링 빈 왜 쓰세요?" 하면 어떤 것이 떠오르는가?
대표적으로 의존성 주입, 싱글톤이 떠오를 것이다.
좀 더 구체적으로 말해보자면
1. 의존성 주입을 IoC컨테이너가 해주어서 객체지향적이다.
2. 싱글톤으로 요청마다 객체가 생성되지 않아 객체 생성과 메모리 비용이 줄어 성능이 향상된다.
일 것이다.
그렇다면 과연 진짜 그럴까?
한번 진짜 그런지 직접 두눈으로 확인해보자.
1. 의존성 주입을 IoC컨테이너가 해주어서 객체지향적이다.
🤔 그러면 개발자가 DI 해주면 객체지향적이지 않다는 건가?
실제로 해보자.
예시 : RacingGame 도메인
- RacingController → RacingService, DriverServcie를 의존
- 각 Service → Repository를 의존
개발자가 직접 DI를 해준다.
여기까지는 나쁘지 않은 것 같다.
상황 1 : 의존성 추가
- Car 도메인 도입
- DriverService, RacingService → CarRepository를 추가적으로 의존
DriverService와 RacingService에가서 CarRepositroy 의존을 추가한 뒤,
DI코드로 가서 의존성을 변경해주었다.
벌써 불편해지기 시작했다.
상황 2 : 구현체 변경
- Jdbc → Jpa로 마이그레이션
JPA를 구현한 뒤 DI 코드로 가서 Repository의 구현체를 전부 변경해주어야한다.
개발자가 직접 DI를 관리하면, 의존 관계가 바뀔 때마다 객체를 사용하는 코드와, 객체를 생성하고 연결해주는 설정 코드를 모두 개발자가 직접 관리해야 하는 부담이 생긴다.
문제는 여기서 끝이 아니다.
상황 3 : 테스트코드
- Repository 에 Fake 객체를 주입하고 싶은 상황
클래스들이 의존하는 하위 클래스 계층 구조를 알아야한다.
또한, 필요한 의존성을 일일이 구성해야한다.
이제 여기서 의존관계의 변경이 일어난다면 도메인 코드 변경 → DI 설정 코드 변경 → 해당 클래스를 사용하는 모든 테스트 코드 변경이라는 끔찍한 연쇄 작업이 발생한다.
지금은 고작 6개의 클래스밖에 없는 구조인데, 만약 클래스가 몇백개라면?
.... 의존관계를 변경하고..DI 클래스를 찾아가고..몇 백줄의 코드를 찾아서 의존관계 주입 코드를 수정해주고..테스트코드를 수정해주고..
정말 끔찍하지 않을 수 없다.
스프링은 이러한 불편함을 IoC컨테이너로 해결해준다.
스프링 컨테이너가 구성하는 책임을 짊어져서 개발자는 사용하는 책임에만 집중할 수 있는 것이다. 이렇게 함으로서, 우리는 객체지향에서 가장 기본인 코드의 응집도를 높히고, 클래스 간의 결합도를 낮출 수 있다.
의존성 주입을 IoC컨테이너가 해주기 때문에, 개발자는 더 편리하게 비지니스 로직에 집중하며 객체지향적인 코드를 작성할 수 있다.
2. 싱글톤으로 요청마다 객체가 생성되지 않아 객체 생성과 메모리 비용이 줄어 성능이 향상된다.
흔히들 빈이 싱글톤이기 때문에, 요청마다 객체가 생성되지 않는다고 말한다.
요청이 100번 들어오면, 컨트롤러와 서비스, 레포지토리 .. 등 하위 계층들이 각 100개씩 만들어져서 문제가 될 것이라고 한다.
🤔 그렇다면 요청이 처음 들어오는 곳이자, 클래스의 가장 첫번째 계층인 컨트롤러만 빈으로 만든다면 ?
컨트롤러 아래 계층들은 빈으로 만들지 않아도 되는거 아닐까?
컨트롤러가 싱글톤이라면 그 아래 생성되는 서비스, 레포지토리, ETC 클래스들의 인스턴스도 하나일 것이다.
한번 확인해볼까?
컨트롤러만 빈으로 등록한 뒤, 서비스는 일반 객체로 생성하였다.
요청을 보낼 때마다 인스턴스 주소를 출력하도록 했다.
요청마다 동일한 인스턴스 주소를 출력하는 것을 볼 수 있다.
그렇다면 정말 컨트롤러만 빈으로 등록하면 되는 것일까?
조금만 더 고민해본다면 이는 틀린말이라는 것을 알 수 있다.
우리가 간과한 부분은 실제 객체는 보통 이런식으로 N:N의 구조를 가진다는 점이다.
이 구조가 만약 기존 스프링처럼 싱글톤이었다면 7개만 생성이 된다.
하지만 controller를 제외하고 싱글톤이 아니라면 객체는 12개가 생성될 것이다.
계층 구조가 더 복잡해진다면 이러한 문제는 더욱더 심화된다.
따라서 빈들이 싱글톤이라, 객체 생성과 메모리 비용이 줄어 성능이 향상된다 라는 부분은 이제 동의할 수 있을 것 같다.
그렇다면 또 한번 반박할 수 있다.
🤔 그럼 개발자가 클래스들을 싱글톤으로 만들면 빈 안써도되는거아님!!??
그러나 개발자가 직접 싱글톤 패턴으로 개발한다면 문제가 많다.
1. 상속 불가
생성자가 private이기 때문에 상속이 불가능하다.
다형성을 활용하기 어려워 객체지향 설계의 유연성을 해친다.
2. 테스트 어려움
싱글톤인 경우, JVM 종료까지 같은 인스턴스가 유지된다.
이렇게 된다면 테스트 메서드 마다 독립적인 객체를 사용할 수 없고, 테스트의 순서에 따라 테스트 결과가 달라질 수 있다.
3. 개발자가 매번 싱글톤으로 개발해야함
가장 큰 문제라고 생각한다.
클래스가 추가될 때 마다 매번 개발자가 싱글톤으로 개발해야 한다. 이 과정에서 비지니스 로직과는 관련 없는 코드를 반복적으로 작성하게 된다.
개발자가 작성한 싱글톤 코드를 완전히 신뢰할 수 없다는 것또한 매우 큰 문제이다.
✋ 그렇다면 여기서 잠깐!
스프링 컨테이너는 이런 싱글톤을 어떻게 제공하는걸까?
스프링 컨테이너에서의 싱글톤과 일반적인 싱글톤 패턴은 매우 다르다.
싱글톤 패턴에서의 싱글톤은 애플리케이션 생명주기내에서 (자바라면 JVM 시작부터 종료까지) 단 하나의 인스턴스만 존재한다는 것을 의미한다.
스프링 컨테이너에서의 싱글톤 빈은 스프링 컨테이너에서 동일한 아이디(이름)를 가진 빈이 하나만 존재한다는 것을 의미한다.
만약 스프링 빈이 일반적인 싱글톤 패턴이였다면, RacingController는 어디서 참조해도 항상 동일한 인스턴스를 반환해야할 것이다.
하지만 위의 예시를 보면 알 수 있듯이, 둘의 인스턴스는 다르다는 것을 알 수 있다.
스프링이 이를 어떻게 구현했을지 예시와 함께 스프링 내부 코드를 살펴보자.
Configuration으로 MyBean클래스의 myBean1, myBean2로 빈을 등록하는 상황이다.
1. @SpringBootApplicaiton을 실행시키면 어노테이션 내부에 @ComponantScan이 동작한다.
2. @Configuration, @Bean을 스캔하여 BeanDefinition(빈 설정 메타데이터로 클래스 이름, 생성방식, 의존성, 스코프 등의 정보를 가지고 있음)을 생성한다.
3. 이때 BeanDefinitionRegistry에 Map<String, BeanDefinition> 으로 저장한다. key는 BeanName으로 저장된다.
즉, 지금의 예시로는
"myBean1", MyBean1관련 BeanDefinition
"myBean2", MyBean2관련 BeanDefinition
이 등록된다.
아직 실제 객체는 생성되지 않은 상태이다.
4. 컨텍스트 초기화 과정(ApplicationContext.refresh())에서 BeanDefinition을 사용하여 실제 객체가 생성된다.
DefaultSingletonBeanRegistry 내부의 Map<String, Object> 형태의 싱글톤 캐시에 저장한다. key는 beanName으로 저장된다.
5. 이후, 의존성 주입하거나 getBean()을 할 때, 이 캐시에서 beanName으로 이미 생성된 객체를 찾아 반환한다.
스프링은 싱글톤 패턴을 사용한다기보다, beanName으로 instance를 캐싱하고 사용한다. 라고 볼 수 있다.
이렇게 함으로서, 기존의 싱글톤 패턴의 단점들을 해결했다.
1. 상속 불가
-> 클래스에 제약 없이 작성할 수 있다. 상속, 구현이 모두 자유롭다.
2. 테스트 어려움
-> 테스트마다 새로운 ApplicationContext를 생성하여 테스트 가능 (ex DirtiesContext)
3. 개발자가 매번 싱글톤으로 개발해야함
-> 개발자는 비지니스 로직에만 집중한다. 싱글톤 관리의 책임은 SpringContainer 가 전담한다.
다시 원래 이야기로 돌아오자면, (싱글톤으로 요청마다 객체가 생성되지 않아 객체 생성과 메모리 비용이 줄어 성능이 향상된다.를 검증하고 있었다.)
스프링 컨테이너가 스프링 빈을 ApplicationContext에서 유일한 인스턴스로 관리해주기 때문에, 객체 생성과 메모리 비용이 줄어 성능이 향상된다. 또한, 기존의 싱글톤 패턴의 근본적인 문제들인 상속 불가, 테스트 어려움, 싱글톤 코드 반복적 작성 등을 해결해주었다.
라고 말할 수 있겠다.
결론
우리는 결국 싱글톤, 의존관계 주입, 객체 생성 등 애플리케이션의 규모가 커질수록 유지보수를 위해 필수적이지만 비지니스 로직과는 거리가 있는 반복적인 작업들을 스프링에게 믿고 맡긴다. 자연스럽게 개발자는 가장 중요한 비지니스로직에 집중하면서 좋은 애플리케이션을 쉽게 개발할 수 있게 되는 것이다,
지금껏 별 생각 없이 사용하던 Bean 이 어떤 목적과 철학을 가지고 있는지 알아보았다.
기술을 사용할 땐 왜 그 기술이 존재하고, 어떻게 동작하는지 아는 것이 중요하다고 생각한다.
모든 것에 의문을 던지자! 아자자 화이팅
'Programming > Spring' 카테고리의 다른 글
[JPA] @NotNull VS @Column(nullable = false) (1) | 2025.05.26 |
---|---|
[JPA] Entity에서 @Table, @Entity, @Column의 name을 정의해주어야할까? (2) | 2025.05.26 |
[JPA] Entity에서 사용되는 Annotation 정리 (@Table, @Entity, @Column, @Enumerated, @Temporal .. ) (2) | 2025.05.26 |
[스프링] DTO에 @NoArgsConstructor와 @Gettter이 필요한 이유 (3) | 2024.07.30 |
[스프링] - findByIN절, jdbc batchUpdate (bulk Insert)로 쿼리 개선 (0) | 2024.05.29 |