본문 바로가기

WINK-(Web & App)/Spring Boot 스터디

[2025 1학기 스프링부트 스터디] 이종윤 #4주차& 5주차

반응형

섹션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를 활용해볼 예정입니다.

감사합니다.

반응형