스프링(Spring) #3 - 스프링 빈에 대해 알아보자

햄햄·2023년 3월 9일
0

Spring

목록 보기
3/3

스프링 빈이란?

스프링 공식문서에서는 빈을 다음과 같이 정의한다.

애플리케이션의 뼈대를 형성하고 스프링 IoC 컨테이너에 의해 관리되는 객체를 이라고 한다. 스프링 IoC 컨테이너는 빈(bean) 객체를 인스턴스화, 조립 등의 방식으로 관리한다.

IoC 컨테이너

먼저, IoC가 무엇인지에 대해서는 이전 글을 참조할 수 있다.

일반적으로 객체를 생성할 땐 클래스의 생성자 함수를 사용한다.

Address address = new Address("High Street", 1000);
Company company = new Company(address);

그런데 만약 수십, 수백 개의 클래스가 있는 애플리케이션이라면 어떨까? 이렇게 많은 객체를 위와 같이 손수 관리하는 것은 악몽이나 다름 없을 것이다. 이때 IoC가 도움이 될 수 있다. 객체가 자체적으로 의존성을 구성하는 대신, IoC 컨테이너에서 의존성을 가져오는 것이다. 우리는 컨테이너에게 적절한 configuration 정보를 제공하기만 하면 된다.

먼저 Company 클래스에 @Component 어노테이션을 달아주자

@Component
public class Company {
    // body는 전과 같다
}

그리고 IoC 컨테이너에 빈 메타데이터를 제공하는 configuration 클래스를 정의한다.

@Configuration
@ComponentScan(basePackageClasses = Company.class)
public class Config {
    @Bean
    public Address getAddress() {
        return new Address("High Street", 1000);
    }
}

configuration 클래스는 Address 타입의 빈을 생성한다. 또한 @ComponentScan 어노테이션은 컨테이너가 Company 클래스가 포함된 패키지에서 빈을 탐색하도록 지시한다.

스프링 IoC 컨테이너가 이런 식으로 객체들을 구성할 때, 이 모든 객체들은 IoC 컨테이너가 관리하므로 스프링 빈이라고 불리는 것이다.

configuration 클래스를 정의했으므로,AnnotationConfigApplicationContext클래스를 인스턴스화 하여 컨테이너를 구축해보자.

ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);

이제 간단한 테스트를 통해 빈의 존재 여부와 속성 값을 확인할 수 있다.

Company company = context.getBean("company", Company.class);
assertEquals("High Street", company.getAddress().getStreet());
assertEquals(1000, company.getAddress().getNumber());

스프링 빈의 스코프

빈 스코프는 컨텍스트에서의 빈의 생명 주기와 가시성을 정한다. 가장 최신 버전의 스프링 프레임워크에는 6가지 유형의 스코프가 있다.

  • singleton
  • prototype
  • request
  • session
  • application
  • webscoket
    마지막에 언급된 request, session, application, websocket은 웹 인식(web-aware) 애플리케이션에만 사용할 수 있다.

Singleton 스코프

빈의 스코프를 싱글톤으로 정의하면 컨테이너는 빈의 단일 인스턴스만 생성한다. 해당 빈 이름에 대한 모든 요청은 동일한 객체를 반환하며, 이 객체는 캐시된다. 이 객체에 대한 모든 수정 사항은 빈을 참조하는 모든 곳에 반영된다. 스코프가 따로 지정되지 않을 경우 기본값으로 지정되는 스코프이다.

예시를 위해 Person 엔티티를 만들어보자.

public class Person {
    private String name;

    // standard constructor, getters and setters
}

그리고 @Scope 어노테이션을 사용하여 이 빈을 싱글톤으로 정의한다.

@Bean
@Scope("singleton")
public Person personSingleton() {
    return new Person();
}

다음과 같은 방법으로 문자열 대신 상수를 사용할 수 있다.

@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)

이제 동일한 빈을 참조하는 두 개의 객체 중 하나만 상태가 바뀌어도 두 객체는 같은 값을 가지게 된다. 둘 다 같은 빈 인스턴스를 참조하고 있기 때문이다. 테스트를 작성해보자.

private static final String NAME = "John Smith";

@Test
public void givenSingletonScope_whenSetName_thenEqualNames() {
    ApplicationContext applicationContext = 
      new ClassPathXmlApplicationContext("scopes.xml"); // 싱글톤 스코프를 정의한 xml

    Person personSingletonA = (Person) applicationContext.getBean("personSingleton");
    Person personSingletonB = (Person) applicationContext.getBean("personSingleton");

    personSingletonA.setName(NAME);
    Assert.assertEquals(NAME, personSingletonB.getName());

    ((AbstractApplicationContext) applicationContext).close();
}

Prototype 스코프

prototype 스코프인 빈은 컨테이너에서 요청될 때마다 다른 인스턴스를 반환한다. 다음과 같은 방법으로 정의할 수 있다.

@Bean
@Scope("prototype")
public Person personPrototype() {
    return new Person();
}
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

이전과 유사한 테스트를 작성해보자. 이제 두 객체는 동일한 빈 인스턴스를 참조하지 않으므로 서로 다른 상태를 갖게 된다.

private static final String NAME = "John Smith";
private static final String NAME_OTHER = "Anna Jones";

@Test
public void givenPrototypeScope_whenSetNames_thenDifferentNames() {
    ApplicationContext applicationContext = 
      new ClassPathXmlApplicationContext("scopes.xml"); // prototype 스코프를 정의한 xml

    Person personPrototypeA = (Person) applicationContext.getBean("personPrototype");
    Person personPrototypeB = (Person) applicationContext.getBean("personPrototype");

    personPrototypeA.setName(NAME);
    personPrototypeB.setName(NAME_OTHER);

    Assert.assertEquals(NAME, personPrototypeA.getName());
    Assert.assertEquals(NAME_OTHER, personPrototypeB.getName());

    ((AbstractApplicationContext) applicationContext).close();
}

Web Aware 스코프들

앞서 언급했듯 web aware 애플리케이션 컨텍스트에서만 사용할 수 있는 네 가지 스코프들이 있다. 실제로는 자주 사용하지 않는다.

Request 스코프

request 스코프는 HTTP 요청 한 개당 하나의 빈 인스턴스를 생성한다. 다음과 같의 정의할 수 있다.

@Bean
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator requestScopedBean() {
    return new HelloMessageGenerator();
}

웹 애플리케이션 컨텍스트의 인스턴스화 시점에는 요청이 없기 때문에 proxyMode 속성이 필요하다. 스프링은 의존성으로 주입할 프록시를 생성하고 요청이 들어와 필요할 때 대상 빈을 인스턴스화 한다. @ReqeustScope 어노테이션을 사용할 수도 있다.

@Bean
@RequestScope
public HelloMessageGenerator requestScopedBean() {
    return new HelloMessageGenerator();
}

Session 스코프

session 스코프는 HTTP Session 하나 당 한 개의 빈을 생성한다. 전과 비슷한 방법으로 빈을 정의할 수 있다.

@Bean
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public HelloMessageGenerator sessionScopedBean() {
    return new HelloMessageGenerator();
}
@Bean
@SessionScope
public HelloMessageGenerator sessionScopedBean() {
    return new HelloMessageGenerator();
}

Application 스코프

애플리케이션 스코프는 ServletContext의 생명주기에 따라 빈 인스턴스를 생성한다. 싱글톤 스코프와 비슷하지만 매우 중요한 차이점이 있다.
빈이 애플리케이션 스코프로 지정될 경우, 동일한 ServletContext에서 실행되는 여러 서블릿 기반 애플리케이션끼리 동일한 빈 인스턴스가 공유된다. 반면, 싱글톤 범위로 지정된 빈은 단일 애플리케이션 컨텍스트로만 범위가 한정된다.

WebSocket 스코프

웹소켓 스코프는 특정 웹소켓 세션에 대한 빈 인스턴스를 생성한다. 웹소켓 스코프로 지정된 빈은 처음 액세스될 때 WebSocket 세션 속성에 저장된다. 그런 다음 전체 웹소켓 세션 동안 해당 빈에 액세스할 때마다 동일한 빈 인스턴스가 반환된다.

스프링 빈 어노테이션

다양한 유형의 빈을 정의하는 데 사용되는 가장 일반적인 스프링 빈 어노테이션에 대해 알아보자.

컴포넌트 스캐닝

컴포넌트 스캐닝이 활성화된 경우 스프링은 패키지에서 빈을 자동으로 스캔할 수 있다. @ComponentScan은 어떤 패키지에서 어노테이션이 설정된 클래스들을 스캔할지 지정한다.

@Configuration
@ComponentScan(value = "com.baeldung.annotations")
class VehicleFactoryConfig {}
@Configuration
@ComponentScan(basePackageClasses = VehicleFactoryConfig.class)
class VehicleFactoryConfig {}

인자는 배열이기 때문에 여러개의 패키지를 입력할 수도 있다. 인자가 지정되지 않으면 @ComponentScan 어노테이션이 달린 클래스가 존재하는 패키지를 스캐닝한다.

@Component

@Component는 클래스 레벨 어노테이션이다. 컴포넌트를 스캔하는 동안 스프링 프레임워크가 @Component 어노테이션이 달린 클래스를 자동으로 감지한다.

기본적으로 해당 클래스의 빈 인스턴스는 소문자 이니셜이 포함된 클래스 이름과 동일한 이름을 갖는다. 이 어노테이션의 value(optional) 인자를 사용하여 다른 이름을 지정할 수 있다.

@Repository, @Service, @Configuration, @Controler는 모두 @Component를 감싼 어노테이션이므로 빈 네이밍이 동일하게 작동한다. 스프링 또한 컴포넌트 스캐닝을 하는 동안 자동으로 이들을 선택한다.

@Repository

DAO 혹은 Repository 클래스들은 보통 애플리케이션 내 데이터베이스 액새스 레이어를 나타나며 @Repository 어노테이션을 달아야 한다. 이 어노테이션을 사용할 때의 한 가지 장점은 자동으로 영속성 예외 번역(persistence exception translation)이 활성화된다는 것이다. 하이버네이트와 같은 영속성 프레임워크를 사용할 때 @Repository로 어노테이션된 클래스 내에서 발생하는 네이티브 예외는 스프링의 DataAccessExcption의 서브클래스로 자동 번역된다.

@Service

애플리케이션의 비즈니스 로직은 일반적으로 서비스 레이어에 있으므로 @Service 어노테이션을 사용하여 클래스가 해당 레이어에 속해 있음을 표시한다.

@Controller

@Controller는 클래스 레벨 어노테이션으로, 스프링 프레임워크에게 이 클래스가 Spring MVC에서 컨트롤러 역할을 한다는 것을 알려준다.

@Configuration

Configuration 클래스에는 @Bean 어노테이션이 달린 빈 정의 메소드가 포함될 수 있다.

@Configuration
class VehicleFactoryConfig {

    @Bean
    Engine engine() {
        return new Engine();
    }

}

스테레오타입 어노테이션과 AOP

스프링 스테레오타입 어노테이션을 사용하면 특정 스테레오타입을 가진 모든 클래스를 대상으로 하는 포인트컷을 쉽게 만들 수 있다.

DAO 레이어에서 메서드의 실행시간을 측정해야 하는 상황을 가정해보자. @Repository 스테레오 타입을 활용하여 AspectJ 어노테이션들을 활용하는 aspect를 다음과 같이 생성할 수 있다.

@Aspect
@Component
public class PerformanceAspect {
    @Pointcut("within(@org.springframework.stereotype.Repository *)")
    public void repositoryClassMethods() {};

    @Around("repositoryClassMethods()")
    public Object measureMethodExecutionTime(ProceedingJoinPoint joinPoint) 
      throws Throwable {
        long start = System.nanoTime();
        Object returnValue = joinPoint.proceed();
        long end = System.nanoTime();
        String methodName = joinPoint.getSignature().getName();
        System.out.println(
          "Execution of " + methodName + " took " + 
          TimeUnit.NANOSECONDS.toMillis(end - start) + " ms");
        return returnValue;
    }
}

이 예제에서는 @Repository 어노테이션이 달린 클래스의 모든 메서드와 일치하는 포인트컷을 만들었다. 그런 다음 @Around advice를 사용하여 해당 포인트컷을 타겟팅하고 가로챈 메서드 호출의 실행시간을 측정했다.
또한 이 접근 방식을 사용하여 로깅, 성능 관리, audit 등의 동작을 각 애플리케이션 레이어에 추가할 수 있다.

출처

What is a Spring Bean?
Quick Guide to Spring Bean Scopes
Spring Bean Annotations

profile
@Ktown4u 개발자

0개의 댓글