본문 바로가기
Spring

[Spring] Bean의 스코프

by dbjh 2019. 12. 29.
반응형

이번 글에서는 Bean의 스코프에 대해서 알아보도록 하자.


Bean의 스코프란, 
Bean이 생성되는 방식을 가르키는데 한번 생성한 후 계속 사용할지, 필요할때마다 생성해서 사용할지를 말하는 것이다.
기본적으로 Spring에서 Bean의 스코프는 Singleton이 기본 설정 값이다.
추가설정을 하지 않고 Bean으로 등록되면 Singleton 형태로 등록이된다.

1. Singlton Scope - 한번만 생성해서 계속 재활용 하는 방식

실제로 추가적인 설정없이 @Autowired 만으로 Bean으로 등록된 객체의 주소 값이 동일한 확인하여 보자.
객체를 비교하기위해 필요한 클래스 2개와 두 클래스가 Bean으로 등록된 후에 사용할 수 있게 Runner 클래스를 만들도록 하자.

디렉토리 및 파일 구조

 

Single.java
package com.bean.example.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class Single {

    @Autowired
    private Proto proto;
    
    // proto 필드 사용을 위한 getter
    public Proto getProto() {
        return proto;
    }
}

 

Proto.java
package com.bean.example.bean;

import org.springframework.stereotype.Component;

@Component
public class Proto {
}

 

BeanRunner.java
package com.bean.example.bean;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class BeanRunner implements ApplicationRunner {

    @Autowired
    Single single;

    @Autowired
    Proto proto;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        // 참조하는 주소값이 같다면..
        if (single.getProto() == proto) {
            System.out.println("두 변수가 참조하는 객체는 같습니다.");
            // 빨간 글씨로 강조하기 위해 err 필드 사용
            System.err.println(single.getProto() + " = " +proto);
        }
    }
}

위와 같이 코드를 작성하고, 프로젝트를 실행해도록 해보자. Single 클래스의 proto 필드에 주입된 객체와 BeanRunner 클래스에 proto 객체가 같다는 것을 확인 할 수 있다.

같은 객체를 참조하는 결과

 

간단하게 그림으로 보면 아래와 같은 구조이다. Application Context가 관리하는 Bean들의 구조를 확인 할 수 있다.

Singleton Scope 구조

위와 같이 일반적으로 Spring에서 사용하는 Bean들은 Singleton으로 하나만 생성된다. 하지만 상황에 따라서 다른 객체가 계속 생성되어야할 경우도 있을 것이다. Singleton 이외에 Proto, Request Session, Web Socket, Thread 등의 Scope을 사용할 수도 있다. 거의 대부분의 경우에 Singleton scope를 사용하겠지만, 추가적으로 Proto Scope에 대해 알아보도록 하자.


2. Proto Scope - 필요할때 마다 새로운 객체를 생성하는 방식

proto scope을 사용하기 위해서는 @Scope 어노테이션을 이용하여 scope를 지정해주면된다. Proto scope로 사용하고 싶은 클래스 위에 명시해주도록하자. 여기서는 Proto 클래스를 이용할 것이다. 
@Scope을 명시해주고 문자열로 "prototype"을 입력해주면 끝이다. 그런후에, Proto 클래스를 Bean으로 가져오도록 Runner 클래스에서 ApplicationContext의 getBean 메소드를 호출하도록 하자.

Proto.java
// 코드중략

@Component @Scope("prototype")
public class Proto {
}

 

BeanRunner.java
// 코드중략

@Component
public class BeanRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext ctx;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
    }
}

결과를 실행해 보면 아래와 같이 모두 다른 객체를 참조하고 있는 것을 확인할 수 있다.

모두 다른 객체를 참조함

위의 Bean 구조를 그림으로 살펴보자.

Proto Scope 구조

그림과 같이 ctx 필드가 모든 Bean들을 관리하는 ApplicationContext 객체를 참조하고, ApplicationContext의 getBean메소드를 호출할때마다 새로운 Proto 클래스의 Bean을 생성하는 구조이다. 그래서 각각 다른 객체를 참조하는 것이다.


3. 서로다른 스코프를 가진 Bean들이 Has-a 관계일 때 구조

has-a 관계란 말그대로 class가 다른 class를 필드에 담아서 가지고 있는 것을 말한다.

3.1 Proto 스코프의 Bean이 Singleton 스코프의 Bean을 가지고 있는 경우

아래의 그림과 같이 Proto 스코프 Bean이 Singleton 스코프 Bean을 가지고 있는 형태라 스코프의 특성을
유지한다. 그렇기 때문에 문제가 되는 상황이 아니다.

Proto 클래스의 코드를 아래와 같이 수정하고, BeanRunner 클래스의 출력을 확인해보자.

Proto.java
// 코드중략
@Component @Scope("prototype")
public class Proto {

    @Autowired
    private Single single;

    public Single getSingle() {
        return single;
    }
}

 

BeanRunner.java
// 코드중략

@Component
public class BeanRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext ctx;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        // proto 빈 주입 - 각각 다른 객체가 주입됨
        Proto proto1 = ctx.getBean(Proto.class);
        Proto proto2 = ctx.getBean(Proto.class);
        Proto proto3 = ctx.getBean(Proto.class);

        // 주임된 proto bean 주소 출력 => 모두다름
        System.out.println("주임된 proto bean 주소 출력 => 모두다름");
        System.out.println(proto1);
        System.out.println(proto2);
        System.out.println(proto3);

        System.out.println();

        // proto bean이 가지고 있는 single bean 주소 출력 => 모두같음
        System.out.println("proto bean이 가지고 있는 single bean 주소 출력 => 모두같음");
        System.out.println(proto1.getSingle());
        System.out.println(proto2.getSingle());
        System.out.println(proto3.getSingle());

    }
}

코드를 위와 같이 수정하고 실행해보면 주석에 적혀있는 상황대로 주소 값이 출력된다.

3.1 상황 출력 결과

 

위의 상황은 그림으로 나타내면 아래와 같고 순서를 정리하자면,

1. BeanRunner 클래스의 ctx 필드에  ApplicationContext Bean을 주입받는다.
2. BeanRunner 클래스의 run 메소드에서 ctx.getBean() 메소드를 3번 호출하여 Proto Bean을 로컬 필드proto1, proto2, proto3에 각각 저장한다. * 로컬필드 : 메소드 내부의 변수
3. proto1, proto2, proto3의 주소가 모두 다른지 확인한다.
4. proto1, proto2, proto3의 single 필드에 주입된 Single Bean의 주소가 모두 같은지 확인한다.

상황을 정리하면 Proto Bean을 생성하면 새로운 객체가 생성되고, 생성된 객체가 참조하는 Single Bean은 모두 같은 객체이다.

Proto 스코프 Bean이 Singleton 스코프 Bean을 가지고 있는 경우

 

하지만, Singleton 스코프의 Bean이 Proto 스코프의 Bean을 가지고 있는 상황이라면 얘기가 달라진다.
한번 Bean으로 등록되면 바뀌지 않는 Singleton 스코프 Bean의 특성 때문에 해당 필드가 참조하고 있는 주소값도 바뀌지 않는다.

3.2 Singleton 스코프의 Bean이 Proto 스코프의 Bean을 가지고 있는 경우

기존의 코드를 사용하고, 주소 값을 출력하기 위해 BeanRunner 클래스의 코드만 수정하도록하자.

BeanRunner.java
// 코드중략

@Component
public class BeanRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext ctx;

    @Override
    public void run(ApplicationArguments args) throws Exception {

        System.out.println(ctx.getBean(Single.class));
        System.out.println(ctx.getBean(Single.class));
        System.out.println(ctx.getBean(Single.class));

        // single bean이 가지고 있는 proto bean 주소 출력 => 모두같음
        System.out.println("single bean이 가지고 있는 proto bean 주소 출력 => 모두같음");
        System.out.println(ctx.getBean(Single.class).getProto());
        System.out.println(ctx.getBean(Single.class).getProto());
        System.out.println(ctx.getBean(Single.class).getProto());

    }
}

위처럼 코드를 수정하여 실행해보면 예상과 같이 Single Bean은 하나의 객체를 계속 이용하고, Single Bean의 proto 필드가 참조하는 Proto 클래스는 계속 새로운 객체가 생성되어야 하는 것이 맞다고 생각할 것이다. 코드를 실행 시켜서 결과를 확인해보자.

3.2 상황 출력 결과

예상과 다르게 Single Bean이 가지고 있는 Proto Bean의 주소가 모두 같다. 그 이유는 위에서도 말했듯이, 
한번 Bean으로 등록되면 바뀌지 않는 Singleton 스코프 Bean의 특성 때문에 해당 필드가 참조하고 있는 주소값도 바뀌지 않기 때문이다.

위의 상황을 그림으로 나타내면 아래와 같다.(ApplicationContext의 getBean() 메소드 호출과정 생략)

Singleton 스코프 Bean이 Proto 스코프 Bean을 가지고 있는 경우

위 그림과 같이 한번, Singleton 스코프 Bean의 필드로 주입된 Proto 스코프 Bean은 바뀌지 않고 고정이 된다.
이런경우를 대비해서 Proto 스코프의 특성을 유지할 수 있게 해주는 방법이 있다.


4. Proto 스코프의 특성을 유지하는 방법

Bean을 사용할때 마다 새로운 객체가 생성되어야 하는 Proto 스코프의 특성을 유지시키는 방법, 두가지를 알아보자.

4.1 Bean으로 등록할 클래스에 어노테이션을 명시하는 방식

Proto.java
//코드중략

// proxy 클래스 모드로 설정하여 Proto를 상속받은 Proxy 클래스를 생성하도록하자
@Component @Scope(value = "prototype" , proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Proto {
}

위와같이 @Scope 어노테이션의 속성을 이용하여 해당 클래스를 Proxy가 감싸는 구조가 되도록 설정한다.
이렇게 되면 Proto Bean 주입을 해야할 상황에 Proto 클래스를 상속받은 Proxy 클래스가 생성되면서 Proxy Bean이 주입이된다. 

BeanRunner 클래스의 코드를 그대로 두고 실행하여 보자.

Single Bean에 주입된 Proto 타입의 Bean이 모두 다른 주소값을 가짐

그림으로 나타내면 아래와 같다.(ApplicationContext의 getBean() 메소드 호출과정 생략)

Single Bean에 주입된 Proxy Bean 구조

 

4.2 Bean을 주입할 필드의 타입을 변경하는 방식

Single.java
// 코드중략

@Component
public class Single {

    @Autowired
    private ObjectProvider<Proto> proto;

    // proto 필드 사용을 위한 getter
    public Proto getProto() {
        return proto.getIfAvailable();
    }
}

 

Proto.java
// 코드중략

@Component @Scope("prototype")
public class Proto {
}

 

위의 코드도 4.1 Bean으로 등록할 클래스에 어노테이션을 명시하는 방법과 동일한 구조를 갖는다. 하지만 Spring의 코드 를 적용시키는 방법이라서 별로 추천하지는 않는다.

Proxy 패턴에 관련해서는 이후에 AOP 관련 글에서 정리하도록 하겠다. 


어쨌든, Singleton Scope가 아니면 거의 쓸일이 없다는 것을 알아두도록하자. 하지만 분명히 써야할 일이 생길 수 있다.
그리고 Singleton Scope의 경우 멀티스레드의 상황에서 해당 필드의 값이 바뀌는 상황이 생길 수 있기 때문에 Thread Safe하게 코드를 작성하는 것을 잊지 않도록하자.

 

내용 출처

반응형

댓글