[Spring Core] Spring Core 핵심 개념

2025. 2. 15. 21:23·Spring
728x90
반응형

IoC, DI, 그리고 컨테이너

IoC(제어의 역전)

  • 기존에는 클라이언트 코드가 직접 구현체를 생성하고 실행 흐름을 제어했지만, IoC는 이 흐름 제어를 외부(AppConfig 등)가 담당하게 한다.
  • 클라이언트는 인터페이스에만 의존하여, 어떤 구현체가 주입될지 모르게 됨.
  • Framework vs. Library:
    • 프레임워크는 애플리케이션의 흐름을 제어하고 실행함.
    • 라이브러리는 개발자가 직접 흐름을 제어함.DI(의존관계 주입)
  • 정적인 클래스 의존관계: 코드 상에서 클래스 간의 관계를 확인할 수 있음(예: 클래스 다이어그램)
  • 동적인 객체 인스턴스 의존관계: 런타임 시 실제 객체 인스턴스가 외부에서 생성되어 클라이언트에 주입됨
  • DI를 통해 클라이언트 코드를 수정하지 않고도 주입될 객체를 변경할 수 있음

IoC/DI 컨테이너

  • AppConfig와 같이 객체를 생성, 관리하며 의존관계를 연결해주는 역할을 한다.
  • 애플리케이션의 흐름과 객체 간의 실제 의존관계를 결정하여, 코드의 유연성과 확장성을 높인다.

Spring Container / Spring Bean

스프링 컨테이너(ApplicationContext)

package com.study.springcore;

import com.study.springcore.member.Grade;
import com.study.springcore.member.Member;
import com.study.springcore.member.MemberService;
import com.study.springcore.member.MemberServiceImpl;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MemberApp {
    public static void main(String[] args) {
//        AppConfig appConfig = new AppConfig();
//        MemberService memberService = appConfig.memberService();

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);
        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        Member findMember = memberService.findMember(1L);
        System.out.println("new Member = " + member);
        System.out.println("findMember = " + findMember);
    }
}
// 기존과 동일한 결과 출력
  • 스프링 컨테이너는 애플리케이션의 객체(스프링 빈)를 생성, 관리, 그리고 의존관계를 주입하는 역할을 한다.
  • AnnotationConfigApplicationContext와 같이 자바 기반 설정(AppConfig 클래스)을 사용하여 컨테이너를 생성할 수 있다.

스프링 빈 등록과 조회

  • AppConfig에 @Configuration과 @Bean 어노테이션을 사용하면, 해당 메소드들이 반환하는 객체들이 스프링 빈으로 등록된다.
  • 등록된 빈은 기본적으로 메소드 이름(예: "memberService")을 이름으로 사용하며, applicationContext.getBean()을 통해 조회할 수 있다.

의존관계 주입(DI)와 컨테이너 역할

  • 스프링 컨테이너는 설정 정보를 기반으로 각 빈 간의 의존관계를 동적으로 연결(주입)한다.
  • 이는 기존에 직접 AppConfig를 통해 객체를 생성하고 주입하던 방식과 동일한 결과를 내지만, 관리와 확장이 더욱 용이해진다.

스프링 컨테이너 생성 과정

  1. 스프링 컨테이너 생성
    • AppConfig.class 등을 통해 구성 정보를 지정해주어야 함
  2. 스프링 빈 등록
    • 스프링 컨테이너는 설정 클래스 정보(구성 정보)를 사용해서 스프링 빈을 등록
  3. 스프링 빈 의존관계 설정 - 준비
  4. 스프링 빈 의존관계 설정 - 완료
    • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI) = 동적인 의존관계 연결
    • 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다. (뒤 싱글톤 컨테이너 부분 참고)
    • 스프링 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있지만, 사실 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다. (의존관계 자동 주입 부분 참고)

장점

  • 동적 의존관계 연결: 런타임에 실제 객체 인스턴스가 주입되므로, 클라이언트 코드를 수정하지 않고도 구현체를 쉽게 변경할 수 있다.
  • 관심사의 분리: 객체 생성과 비즈니스 로직이 분리되어 코드의 유지보수성과 확장성이 향상된다.

스프링 컨테이너와 빈 조회

  • 등록된 빈 조회
    • ac.getBeanDefinitionNames()로 컨테이너에 등록된 모든 빈 이름을 확인하고, 각 빈의 역할(일반 사용자 정의 vs. 스프링 내부용)을 BeanDefinition.getRole()로 알 수 있음.
  • 빈 조회 방법
    • '이름+타입' 또는 '타입'만으로 빈을 조회할 수 있으며, 대상 빈이 없으면 예외 발생
    • 동일 타입의 빈이 여러 개 있을 경우, 빈 이름이나 getBeansOfType()을 사용해 모두 조회 가능
    • 상속 관계에서도 부모 타입으로 조회하면 자식 빈도 함께 검색됨

동일한 타입이 둘 이상 있는 스프링 빈 조회 예시

@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.")
void findBeanByTypeDuplicate() {
    AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    assertThrows(NoUniqueBeanDefinitionException.class, () -> ac2.getBean(MemberRepository.class));
}

@Test
@DisplayName("타입으로 조회 시 같은 타입이 둘 이상 있으면, 빈 이름을 지정하면 된다.")
void findBeanByNameWhenTypeDuplication() {
    AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    MemberRepository memberRepository = ac2.getBean("memberRepository1", MemberRepository.class);
    assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}

@Test
@DisplayName("특정 타입을 모두 조회하기.")
void findAllBeanByType() {
    AnnotationConfigApplicationContext ac2 = new AnnotationConfigApplicationContext(SameBeanConfig.class);
    Map<String, MemberRepository> beansOfType = ac2.getBeansOfType(MemberRepository.class);
    for (String key : beansOfType.keySet()) {
        System.out.println("key = " + key + " value = " + beansOfType.get(key));
    }
    System.out.println("beansOfType = " + beansOfType);
    assertThat(beansOfType.size()).isEqualTo(2);
}

@Configuration
static class SameBeanConfig {
    @Bean
    public MemberRepository memberRepository1() {
        return new MemoryMemberRepository();
    }

    @Bean
    public MemberRepository memberRepository2() {
        return new MemoryMemberRepository();
    }
}

BeanFactory vs. ApplicationContext

  • BeanFactory
    • 스프링 빈을 관리·검색하는 최상위 인터페이스
    • 스프링 빈을 관리하고 조회하는 역할 담당
    • getBean() 제공
  • ApplicationContext
    • BeanFactory의 기능을 모두 상속 받고 이외에도 국제화, 환경변수, 이벤트 발행, 리소스 조회 등 부가 기능을 제공하므로 실제로 많이 사용됨
    • 부가 기능
      • MessageSource -> 메시지 소스를 활용한 국제화 기능
      • EnvironmentCapable -> 환경변수 기능
      • ApplicationEventPublisher -> 애플리케이션 이벤트를 발행하고 구독하는 모델 지원
      • ResourceLoader -> 편리한 리소스 조회 기능

차피 ApplicationContext가 모든 기능을 갖고 있으므로, BeanFactory를 직접 사용할 일은 거의 없음. BeanFactory나 ApplicationContext를 스프링 컨테이너라고 함.

빈 설정 메타 정보와 다양한 설정 방식

  • 모든 빈은 결국 BeanDefinition이라는 빈 설정 메타 정보로 등록되며, 이는 자바 설정(@Configuration, @Bean)이나 XML 설정을 통해 생성됨
    • 즉, 역할과 구현을 개념적으로 나눈 것으로 어떤 설정 형식이든 결국 BeanDefinition을 만들도록 하고, ApplicationContext는 이 BeanDefinition만 알면 됨
  • 최근에는 애노테이션 기반 자바 설정이 주로 사용되고, XML은 레거시 환경에서 활용됨.
  • BeanDefinition 살펴보기
    • BeanClassName: 생성할 빈의 클래스 명(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음.)
    • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
    • factoryMethodName: 빈을 생성할 팩토리 메소드 지정, 예) memberService
    • Scope: 싱글톤(기본값)
    • LazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때까지 최대한 생성을 지연처리 하는지 여부
    • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메소드 명
    • DestroyMethodname: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메소드 명
    • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)

실무에서 BeanDefinition을 직접 정의하거나 사용할 일은 거의 없으니 굳이 외우진말자


싱글톤 컨테이너와 싱글톤 패턴

  • 싱글톤 패턴: 객체를 단 한 번만 생성해 공유하는 디자인 패턴으로 메모리를 절약할 수는 있지만, 다음과 같은 단점이 있음..
    • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
    • 의존관계 상 클라이언트가 구체 클래스에 의존한다. → DIP를 위반한다.
    • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
    • 테스트하기 어렵다.
    • 내부 속성을 변경하거나 초기화하기 어렵다.
    • private 생성자로 자식 클래스를 만들기 어렵다.
    • 결론적으로 유연성이 떨어진다. → DI 적용하기 어려워짐..
    • 안티패턴으로 불리기도 한다.
  • 스프링 컨테이너는 기본적으로 싱글톤 방식으로 빈을 관리하여, 동일한 빈 인스턴스를 재사용한다.
    • 따로 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리해주며, 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지한다
    • @Configuration이 붙은 클래스는 CGLIB를 통해 바이트코드 조작으로 싱글톤 보장이 이루어짐.
    • 만약 @Configuration 없이 @Bean만 사용하면 싱글톤 보장이 깨질 수 있음.

싱글톤 방식은 여러 클라이언트가 같은 객체 인스턴스를 공유하기 때문에, 싱글톤 객체는 반드시 stateless로 설계해야 한다. (동시성 문제 예방)


컴포넌트 스캔과 의존관계 자동 주입

컴포넌트 스캔

  • 이전까지 AppConfig에서 @Bean을 통해 직접 빈을 등록하여 스프링 빈을 나열했지만, 이러한 설정 정보(AppConfig.class) 없이도 자동으로 스프링 빈을 등록하는 '컴포넌트 스캔'이라는 기능 제공
  • 컴포넌트 스캔: @Component(및 @Controller, @Service, @Repository, @Configuration)가 붙은 클래스를 자동으로 스캔하여 빈으로 등록
    • 이때 스프링 빈의 기본 이름은 '클래스명'을 사용하되, 맨 앞글자만 소문자를 사용 (물론 직접 지정 가능)
  • 이렇게 애노테이션 방식으로 스프링 빈을 등록하면, 의존관계를 명시해줄 수가 없게 되는데, 이러한 의존관계 주입 역시 클래스 안에서 @Autowired 애노테이션으로 해결 -> 의존관계 자동 주입
    • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입
    • 이때 기본 조회 전략은 '타입'이 같은 빈을 찾아서 수행(= getBean(타입클래스))
  • 컴포넌트 스캔을 사용하려면 @ComponentScan을 붙여주어야 하지만, @SpringBootApplication 애노테이션 안에 이미 포함되어 있어서 따로 명시해주지 않아도 사용 가능하다.
    • 물론, 컴포넌트 스캔 위치를 커스텀하고 싶다면 변경이 필요하다.

컴포넌트 스캔 탐색 위치

모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

@ComponentScan(
        basePackages = "hello.core",
)
  • basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
    • basePackages = { “hello.core”, “hello.service” } 이렇게 여러 시작위치를 지정할 수도 있다.
  • basePackageClass: 지정한 클래스의 패키지를 탐색 시작 위로 지정한다.
  • 만약 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
    • => 별도의 설정을 하지 않을 경우, @SpringBootApplication가 있는 패키지가 기본 스캔 시작 위치

컴포넌트 스캔 필터

  • includeFilteres: 컴포넌트 스캔 대상을 추가로 지정한다.
  • excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정한다.
  • FilterType 옵션
    • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
      • ex) org.example.SomeAnnotation
    • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
      • ex) org.example.SomeClass
    • ASPECTJ: AspectJ 패턴 사용
      • ex) org.example.*Service*
    • REGEX: 정규 표현식
      • ex) org\.example\.Default.*
    • CUSTOM: TypeFilter이라는 인터페이스를 구현해서 처리
      • ex) org.example.MyTypeFilter
@Configuration
@ComponentScan(
        includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
        excludeFilters = {
                        @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
                        @Filter(type = FilterType.ASSEIGNABLE_TYPE, classes = BeanA.class)
                }
)
static class ComponentFilterAppConfig {

}

의존관계 자동 주입(@Autowired)

  • 생성자, setter, 필드, 일반 메소드 주입 방식이 있으며, 생성자 주입이 불변성 보장과 누락 방지 때문에 권장됨.
  • 타입이 여러 개일 경우, 필드명 매칭, @Qualifier 또는 @Primary를 통해 해결할 수 있음.
    • 동일한 타입의 여러 빈을 사용해야 하는 경우, @Primary를 메인 비즈니스 로직을 담는 빈에 사용하고, @Qualifier를 서브 비즈니스 로직을 담는 빈에 사용하는 형태가 일반적이다.
    • @Primary보다 @Qualifier가 더 우선순위가 높다.
    • 필요 시 커스텀 애노테이션을 만들어 보다 명시적으로 구분할 수 있음.

빈 컬렉션 주입과 실무 운영 기준

  • 모든 빈 주입: 특정 타입의 빈을 모두 List나 Map으로 주입받아, 전략 패턴 등 동적 빈 선택에 활용 가능.
  • 자동(@ComponentScan + @Autowired) vs. 수동(@Configuration + @Bean) 빈 등록
    • 업무 로직(많은 빈)은 컴포넌트 스캔과 자동 주입을 사용하여 관리 편의성을 높이고,
    • 기술 지원 로직(중요, 변경이 적은 빈)은 수동 등록(AppConfig 등)으로 명확하게 관리하는 것이 좋음.

빈 생명주기 콜백(Bean Lifecycle Callback)

  • 목적: 외부 리소스(예: 데이터베이스 커넥션, 네트워크 소켓)와 같이 애플리케이션 시작 시 연결하고 종료 시 정리해야 하는 작업을 위해, 빈의 초기화와 종료 시점을 관리

라이프사이클 단계

  1. 컨테이너 생성
  2. 빈 생성: 생성자 주입을 통해 인스턴스가 만들어짐.
  3. 의존관계 주입: setter나 필드 주입을 통해 의존성이 할당됨.
  4. 초기화 콜백: 빈이 완전히 준비된 후 호출되어 외부 연결이나 초기 작업을 수행함.
  5. 사용: 빈이 실제 비즈니스 로직에서 활용됨.
  6. 소멸 전 콜백: 컨테이너 종료 전 호출되어 리소스를 정리함.
  7. 컨테이너 종료

콜백을 구현하는 3가지 방법

  • 인터페이스 구현 (InitializingBean, DisposableBean)
    • 빈 클래스가 해당 스프링 전용 인터페이스(InitializingBean, DisposableBean)를 구현하여 afterPropertiesSet()와 destroy() 메서드를 오버라이드해 초기화와 소멸 콜백을 받음
    • 단점: 스프링에 종속되며, 메서드 이름 변경이 불가능하고 외부 라이브러리에는 적용하기 어려움
  • 빈 등록 시 initMethod, destroyMethod 지정
    • @Bean 애노테이션에서 initMethod와 destroyMethod 속성을 사용하여 초기화 및 종료 메서드를 지정할 수 있음
    • 장점: 스프링 종속성을 제거할 수 있으며, 외부 라이브러리에도 적용할 수 있고, 메서드 이름을 자유롭게 설정 가능
    • 또한, 종료 메서드에 대해서는 close나 shutdown 등의 메서드 이름을 자동으로 추론하는 기능도 제공
  • JSR-250 애노테이션 (@PostConstruct, @PreDestroy)
    • 가장 최신이자 권장되는 방법으로, 해당 애노테이션을 메서드에 붙이면 의존관계 주입 후 초기화(@PostConstruct)와 소멸 전(@PreDestroy) 시 자동으로 호출
    • 장점: 코드가 간결하고 자바 표준(JSR-250)을 따르므로 스프링 외의 컨테이너에서도 사용 가능
    • 단점: 외부 라이브러리에는 적용할 수 없으므로, 그런 경우에는 initMethod/destroyMethod를 사용해야 함

빈 스코프 (Bean Scope)

기본 개념

  • 빈 스코프는 빈이 컨테이너 내에서 얼마나 오래, 어디까지 관리되는지를 결정함
  • 기본 스코프는 싱글톤이며, 컨테이너 생성 시 초기화되고 종료 시까지 동일한 인스턴스를 사용

주요 스코프 종류

  • 싱글톤(Singleton): 컨테이너 시작과 종료까지 하나의 인스턴스만 유지
  • 프로토타입(Prototype): 스프링 컨테이너가 프로토타입 빈을 생성하고 의존관계 주입, 초기화까지만 관여하며, '요청할 때마다' 새로운 인스턴스를 반환
    • 생성 시마다 새로운 인스턴스가 만들어지며, 컨테이너는 소멸 콜백(종료 메서드)을 호출하지 않음
    • 프로토타입 빈은 클라이언트가 직접 관리해야 함
  • 웹 스코프
    • request: 각 HTTP 요청마다 새 빈 인스턴스가 생성되고, 요청 종료 시 소멸
    • session: HTTP 세션 생명주기와 동일하게 관리됨
    • application: 서블릿 컨텍스트의 생명주기와 동일
    • websocket: 웹소켓 연결 생명주기에 맞춰 관리됨

프로토타입 빈과 싱글톤 빈의 결합 문제

  • 만약 싱글톤 빈에 프로토타입 빈을 의존관계 주입하면, 싱글톤 빈이 생성될 때 한 번만 프로토타입 빈이 생성되어 주입됨
  • 따라서 여러 번 호출해도 항상 같은 프로토타입 인스턴스가 사용되어 의도한 "매번 새로 생성"하는 효과를 얻지 못함
  • 해결 방법 (의존관계 조회, DL):
    • ApplicationContext 직접 조회
      • 싱글톤 빈 내부에서 필요할 때마다 ac.getBean(PrototypeBean.class)를 호출하여 새 인스턴스를 얻는 방법
    • ObjectFactory / ObjectProvider 사용
      • 스프링이 제공하는 DL(Dependency Lookup) 기능으로, ObjectProvider의 getObject()를 호출하면 매번 새로운 프로토타입 빈을 생성하여 반환
    • JSR-330 Provider 사용
      • jakarta.inject.Provider를 이용하면, provider.get() 호출 시마다 새로운 프로토타입 빈을 생성 가능
      • 별도의 라이브러리가 필요
      • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용 가능
  • 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하자
    • => 여기서도 굳이 DL을 위한 편의 기능을 많이 제공해주는 ObjectProvider가 아니라 별도의 라이브러리를 필요로 하는 JSR-330 Provider를 사용할 필요는 없다.
728x90
반응형

'Spring' 카테고리의 다른 글

[Spring] Spring Transaction 핵심 요약  (0) 2025.04.10
[Spring] 스프링의 데이터 접근 예외 추상화와 JdbcTemplate  (0) 2025.04.07
[Spring Core] 스프링을 사용하는 이유?  (0) 2025.02.15
'Spring' 카테고리의 다른 글
  • [Spring] Spring Transaction 핵심 요약
  • [Spring] 스프링의 데이터 접근 예외 추상화와 JdbcTemplate
  • [Spring Core] 스프링을 사용하는 이유?
mxruhxn
mxruhxn
소소하게 개발 공부 기록하기
    반응형
    250x250
  • mxruhxn
    maruhxn
    mxruhxn
  • 전체
    오늘
    어제
    • 분류 전체보기 (150)
      • Java (21)
      • Spring (4)
      • Database (13)
      • Operating Syste.. (1)
      • Computer Archit.. (0)
      • Network (24)
      • Data Structure (6)
      • Algorithm (11)
      • Data Infra (7)
      • DevOps (12)
      • ETC (27)
      • Project (21)
      • Book (1)
      • Look Back (1)
  • 블로그 메뉴

    • 링크

      • Github
    • 공지사항

    • 인기 글

    • 태그

    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.0
    mxruhxn
    [Spring Core] Spring Core 핵심 개념
    상단으로

    티스토리툴바