섹션5. 스트링빈과 의존관계
1. 웹 애플리케이션과 싱글톤
웹 애플리케이션에서는 사용자 요청이 많습니다
사용자 요청마다 객체를 생성하면 다음과 같은 문제가 발생합니다
- 메모리 낭비
- GC 부담 증가
- 성능 저하
그래서 우리는 "객체 하나만 만들어서 계속 재사용할 수는 없을까?"라는 질문을 하게 되고,
이때 등장하는 해결책이 바로 싱글톤 패턴입니다
2. 싱글톤 패턴이란?
싱글톤은 클래스 당 인스턴스를 오직 하나만 생성하고, 모든 요청에서 이 하나의 객체를 재사용하도록 하는 디자인 패턴입니다.
public class SingletonService {
// 1. static으로 클래스 레벨에 인스턴스 하나만 생성
private static final SingletonService instance = new SingletonService();
// 2. 생성자는 private으로 외부에서 new로 호출 불가
private SingletonService() {
System.out.println("SingletonService 생성자 호출");
}
// 3. 외부에서 객체를 가져가는 유일한 방법 - static 메서드
public static SingletonService getInstance() {
return instance;
}
public void logic() {
System.out.println("싱글톤 객체의 로직 호출");
}
}
public class SingletonTest {
public static void main(String[] args) {
SingletonService s1 = SingletonService.getInstance();
SingletonService s2 = SingletonService.getInstance();
System.out.println(s1 == s2); // true (같은 객체)
}
}
<전형적인 싱글톤 코드 예시>
SingletonService.getInstance()를 호출하면 항상 같은 인스턴스를 반환합니다.
== 연산자를 통해 객체 주소값이 같은지 확인해보면 true가 나옵니다.
즉, 객체를 여러 번 요청해도 실제로는 한 번만 생성된 인스턴스를 계속 재사용하는 싱글톤 코드입니다.
싱글톤 패턴 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. ➜ DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
- private 생성자로 자식 클래스를 만들기 어렵다.
- 결론적으로 유연성이 떨어진다.
- 안티패턴으로 불리기도 한다.
그래서 우리가 원하는 건...
“이렇게 객체를 하나만 만들되, 관리도 자동으로 해주는 프레임워크 없을까?”
-> 바로 '스프링컨테이너'가 그 역할을 대신해줍니다.
3. 스프링컨테이너
스프링은 기본적으로 모든 빈(Bean)을 싱글톤으로 관리합니다.
개발자는 객체를 직접 싱글톤으로 만들지 않아도 되고, 스프링이 대신 관리해줍니다.
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
겉으로 보기엔 memberService()와 memberRepository()가 각각 호출될 때마다 새로운 객체를 생성할 것 같지만...
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberService m1 = ac.getBean("memberService", MemberService.class);
MemberService m2 = ac.getBean("memberService", MemberService.class);
System.out.println(m1 == m2); // true
두 객체는 완전히 같은 인스턴스입니다.
스프링은 @Bean 메서드가 여러 번 호출되더라도 내부적으로 싱글톤으로 관리되도록 처리합니다.
4. 싱글톤 방식의 주의점
싱글톤 패턴은 하나의 객체를 여러 곳에서 공유하기 때문에, 절대 상태(state)를 가지면 안 됩니다.
<문제예시-상태 저장 방식>
public class StatefulService {
private int price; // 상태를 저장하는 필드
public void order(String name, int price) {
System.out.println(name + " 주문 금액 = " + price);
this.price = price; // 문제 발생
}
public int getPrice() {
return price;
}
}
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
StatefulService service1 = ac.getBean(StatefulService.class);
StatefulService service2 = ac.getBean(StatefulService.class);
// 사용자 A가 1만원 주문
service1.order("userA", 10000);
// 사용자 B가 2만원 주문
service2.order("userB", 20000);
// 사용자 A의 주문 금액을 조회하면?
System.out.println(service1.getPrice()); // 20000 -> 잘못된 결과
- StatefulService는 싱글톤 객체이기 때문에 하나만 존재합니다.
- price라는 필드는 공유된 객체 안에 있으므로, 모든 사용자가 같은 필드를 사용합니다.
- 결국 사용자 A의 데이터가 사용자 B의 요청으로 덮어씌워지는 현상이 발생합니다.
=> 이처럼 공유 객체 안에 상태를 저장하면 사용자 간의 데이터가 꼬이는 위험이 생깁니다.
public class StatelessService {
public int order(String name, int price) {
System.out.println(name + " 주문 금액 = " + price);
return price; // 내부 상태를 저장하지 않음
}
}
객체가 상태를 저장하지 않도록 설계하자!
즉, "무상태 서비스"로 만들어야 합니다.
5. @Configuration과 바이트코드 조작
스프링은 내부적으로 CGLIB라는 바이트코드 조작 라이브러리를 사용해
@Configuration이 붙은 클래스를 프록시 객체로 바꿉니다.
이 프록시는 메서드를 호출할 때, 내부적으로 이미 스프링 컨테이너에 등록된 빈이 있는지 확인한 후,
있다면 기존 빈을 반환하고, 없으면 새로 만들어 등록합니다.
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository());
}
}
memberService()와 orderService() 모두 memberRepository()를 호출합니다.
일반적으로 생각하면 memberRepository()는 2번 호출되므로, 객체가 2개 생성되어야 합니다.
-> 하지만 실제로는 객체가 1개만 생성되어 두 곳에서 공유됩니다.
6. 컴포넌트 스캔
이전 섹션에서는 @Configuration과 @Bean을 이용해 스프링 빈을 수동으로 등록했지만,
실무에서는 이렇게 일일이 등록하지 않습니다. 너무 번거롭고, 클래스가 많아지면 관리가 어렵기 때문입니다.
그래서 스프링은 개발자가 @Component만 붙여두면,
자동으로 객체를 생성하고 빈으로 등록해주는 컴포넌트 스캔기능을 제공합니다.
@Component
public class MemberServiceImpl implements MemberService {
...
}
이렇게 @Component 어노테이션만 붙이면,
스프링은 이 클래스를 스캔해서 자동으로 스프링 빈으로 등록해줍니다.
그리고 의존 관계가 필요한 곳에는 @Autowired를 붙여서 스프링이 자동으로 주입해줍니다.
7. 탐색위치와 기본대상
@ComponentScan은 설정 클래스가 있는 패키지를 기준으로 하위 패키지를 모두 탐색합니다.
보통 @SpringBootApplication이 붙은 메인 클래스가 최상단 패키지에 위치하며,
자동으로 컴포넌트 스캔이 동작하므로 별도 설정 없이도 작동하는 이유가 여기에 있습니다.
또한 필요하다면 스캔 대상을 조절할 수도 있습니다.
- includeFilters: 추가적으로 포함할 대상 지정
- excludeFilters: 제외할 대상 지정
@ComponentScan(
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
섹션6. 회원 관리 예제
실습목표
- 스프링 MVC 구조를 이해하고 컨트롤러/서비스/리포지토리 계층을 분리한다.
- Thymeleaf를 활용한 웹 화면을 구성한다.
- 회원 등록/조회 기능을 직접 구현해본다.



이번 실습을 통해 자동 빈 등록(@ComponentScan), @Autowired를 활용한 DI, HTML 폼 처리 방식 등을 더 확실히 이해할 수 있었습니다.
다음 포스트에서는 DB를 활용해볼 예정입니다.
감사합니다.
'WINK-(Web & App) > Spring Boot 스터디' 카테고리의 다른 글
[2025 1학기 스프링부트 스터디] 여민호 #5주차 (0) | 2025.05.13 |
---|---|
[2025 1학기 스프링 부트 스터디] 남윤찬 #5주차 (0) | 2025.05.13 |
[2025 1학기 스프링부트 스터디] 김민서 #5주차 (0) | 2025.05.11 |
[2025 1학기 스프링부트 스터디] 고윤정 #5주차 (1) | 2025.05.11 |
[2025 1학기 스프링 부트 스터디] 정다은 #5주차 (0) | 2025.05.11 |