인프런 강의 김영한

46. 자동, 수동의 올바른 실무 운영 기준

편리한 자동 기능을 기본으로 사용하는게 좋다.
어떤 경우에 컴포넌트 스캔과 자동 주입을 사용하고 어떤 경우에 설정 정보를 통해서 수동으로 빈을 등록하고 의존관계도 수동으로 주입해야 할까?

결론은 스프링이 나오고 시간이 갈 수록 점점 자동을 선호하는 추세다.
스프링은 @Component 뿐만 아니라 @Controller , @Service , @Repository 처럼 계층에 맞추어
일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다.

거기에 더해서 최근 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고 스프링 부트의 다양한 스프링 빈들도 조건이 맞으면 자동으로 등록하도록 설계했다.

설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이지만
개발자 입장에서 스프링 빈을 하나 등록할 때 @Component 만 넣어주면 끝나는 일을 @Configuration 설정 정보에 가서 @Bean 을 적고 객체를 생성하고 주입할 대상을 일일이 적어주는 과정은 상당히 번거롭다.

또 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다.

그리고 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.

수동 빈 등록은 언제 사용하면 좋을까?
애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
업무 로직 빈
웹을 지원하는 컨트롤러 핵심 비즈니스 로직이 있는 서비스 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직이다.
보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.

기술 지원 빈
기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다.
데이터베이스 연결이나 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

업무 로직은 숫자도 매우 많고 한번 개발해야 하면 컨트롤러 서비스 리포지토리 처럼 어느정도 유사한 패턴이 있다.
이런 경우 자동 기능을 적극 사용하는 것이 좋다.
보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽다.

기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다.

업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 드러나지만
기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다.

그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 좋다.
애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.

비즈니스 로직 중에서 다형성을 적극 활용할 때 수동 bean을 활용하는게 유리할때가 있다.
의존관계 자동 주입
조회한 빈이 모두 필요할 때 List, Map을 다시 보자.

DiscountService 가 의존관계 자동 주입으로 Map<String, DiscountPolicy> 에 주입을 받는 상황을 생각해보자.
여기에 어떤 빈들이 주입될 지 각 빈들의 이름은 무엇일지 코드만 보고 한번에 쉽게 파악하기 어려울 수 밖에없다

자동 등록을 사용하고 있기 때문에 파악하려면 여러 코드를 찾아봐야 한다.
이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 좋다
핵심은 딱 보고 이해가 되는 상황을 만들어줘야한다.

이 부분을 별도의 설정 정보로 만들고 수동으로 등록하면 다음과 같다

@Configuration
public class DiscountPolicyConfig {

    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }
    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}

이 설정 정보만 봐도 한눈에 빈의 이름은 물론이고 어떤 빈들이 주입될지 파악할 수 있다.
그래도 빈 자동 등록을 사용하고 싶으면 파악하기 좋게 DiscountPolicy 의 구현 빈들만 따로 모아서 특정 패키지에 모아두자.

참고로 스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외다.
이런 부분들은 스프링 자체를 잘이해하고 스프링의 의도대로 잘 사용하는게 중요하다.
스프링 부트의 경우 DataSource 같은 데이터베이스 연결에 사용하는 기술 지원 로직까지 내부에서 자동으로 등록하는데
이런 부분은 메뉴얼을 잘 참고해서 스프링 부트가 의도한 대로 편리하게 사용하면 된다.
반면에 스프링 부트가 아니라 내가 직접 기술 지원 객체를 스프링 빈으로 등록한다면 수동으로 등록해서 명확하게 드러내는 것이 좋다.

정리
편리한 자동 기능을 기본으로 사용하자
직접 등록하는 기술 지원 객체는 수동 등록하자
다형성을 적극 활용하는 비즈니스 로직은 수동 등록을 고민해보자

빈 생명주기 콜백

47. 빈(Bean) 생명주기 콜백 시작

빈 생명주기 콜백 시작
데이터베이스 커넥션 풀이나 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고
애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.

db연결시 rs.close con.close 등등등

스프링을 통해 이러한 초기화 작업과 종료 작업을 어떻게 진행하는지 예제로 알아보고자 한다.
간단하게 외부 네트워크에 미리 연결하는 객체를 하나 생성한다고 가정해보자.

실제로 네트워크에 연결하는 것은 아니고 단순히 문자만 출력해보자.

다음 NetworkClient예제는 애플리케이션 시작 시점에 connect() 를 호출해서 연결을 맺어두어야 하고
애플리케이션이 종료되면 disConnect() 를 호출해서 연결을 끊어야 한다.

public class NextWorkClinet {
    private String url;

    public NextWorkClinet() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메시지");
    }

    public void setUrl(String url) {
        this.url = url;
    }

    // 서비스 시작시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

    public void call(String message) {
        System.out.println("call: " + url + " message = " + message);
    }

    // 서비스 종료시 호출
    public void disconnect() {
        System.out.println("close: " + url);
    }
}

class BeanLifeCycleTest {
    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NextWorkClinet client = ac.getBean(NextWorkClinet.class);
        ac.close(); // 스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }

    @Configuration
    static class LifeCycleConfig {

        @Bean
        public NextWorkClinet networkClient() {
            NextWorkClinet networkClient = new NextWorkClinet();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

생성자 부분을 보면 url 정보 없이 connect가 호출되는 것을 확인할 수 있다.

스프링 빈은 간단하게 다음과 같은 라이프사이클을 가진다.
객체 생성 -> 의존관계 주입

스프링 빈은 객체를 생성하고 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다.
따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다.

그런데 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있을까
스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메소드를 통해서
초기화 시점을 알려주는 다양한 기능을 제공한다.

또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다.
따라서 안전하게 종료 작업을 진행할 수 있다.

스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 ->의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료

초기화 콜백: 빈이 생성되고 빈의 의존관계 주입이 완료된 후 호출
소멸전 콜백: 빈이 소멸되기 직전에 호출

스프링은 다양한 방식으로 생명주기 콜백을 지원한다.

참고할점 객체의 생성과 초기화를 분리하자.
생성자는 필수 정보(파라미터)를 받고 메모리를 할당해서 객체를 생성하는 책임을 가진다.
반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는등 무거운 동작을 수행한다.

따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과
초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다.

물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한번에 다 처리하는게 더 나을 수 있다.

참고할점
싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에
스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다.

싱글톤 처럼 컨테이너의 시작과 종료까지 생존하는 빈도 있지만
생명주기가 짧은 빈들도 있는데 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸전 콜백이 일어난다.

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.
인터페이스(InitializingBean, DisposableBean)
설정 정보에 초기화 메소드, 종료 메소드 지정
@PostConstruct, @PreDestroy 어노테이션 지원

48. 인터페이스 InitializingBean, DisposableBean

InitializingBean은 afterPropertiesSet메소드를 가지고있는데
의존관계 주입이 끝나면을 의미한다.

DisposableBean는 destroy()메소드를 가지고 있는데 끝날때 실행이된다.

public class NextWorkClinet implements InitializingBean, DisposableBean{
    public NextWorkClinet() {
        System.out.println("생성자 호출, url = " + url);
    }
    //.....
    @Override
    public void afterPropertiesSet() throws Exception {
        connect();
        call("초기화 연결메시지");
    }

    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}

출력 결과를 보면 초기화 메서드가 주입 완료 후에 적절하게 호출 된 것을 확인할 수 있다.
그리고 스프링 컨테이너의 종료가 호출되자 소멸 메서드가 호출 된 것도 확인할 수 있다.

초기화, 소멸 인터페이스 단점
이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
초기화, 소멸 메서드의 이름을 변경할 수 없다.

내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

참고할점
인터페이스를 사용하는 초기화 종료 방법은 스프링 초창기에 나온 방법들이고
지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다!!

49. 빈 등록 초기화, 소멸 메서드

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화 소멸 메서드를 지정할 수 있다

public class NextWorkClinet {

    //....
    public void init(){
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결메시지");
    }

    public void destroy() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

class BeanLifeCycleTest {
    @Test
    public void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NextWorkClinet client = ac.getBean(NextWorkClinet.class);
        ac.close(); // 스프링 컨테이너를 종료, ConfigurableApplicationContext 필요
    }

    @Configuration
    static class LifeCycleConfig {

        @Bean(initMethod = "init", destroyMethod = "close")
        public NextWorkClinet networkClient() {
            NextWorkClinet networkClient = new NextWorkClinet();
            networkClient.setUrl("http://hello-spring.dev");
            return networkClient;
        }
    }
}

설정 정보 사용 특징
메서드 이름을 자유롭게 줄 수 있다.
스프링 빈이 스프링 코드에 의존하지 않는다.
코드가 아니라 설정 정보(config)를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화 종료 메서드를 적용할 수 있다

종료 메서드 추론
@Bean을 사용할때만 특별하게 발생한다.
@Bean의 destroyMethod 속성에는 아주 특별한 기능이 있다.
라이브러리는 대부분 close shutdown 이라는 이름의 종료 메서드를 사용한다.

@Bean의 destroyMethod 는 기본값이 (inferred) (추론)으로 등록되어 있다.

이 추론 기능은 close, shutdown 라는 이름의 메서드를 자동으로 호출해준다.
이름 그대로 종료 메서드를 추론해서 호출해준다.

따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
추론 기능을 사용하기 싫으면 destroyMethod="" 처럼 빈 공백을 지정하면 된다.

50. 어노테이션 @PostConstruct, @PreDestroy

결론부터 말하자면 이방법을 사용하면된다.
@PostConstruct 생성된 이후에 @PreDestroy끝나기 전에

@PostConstruct
public void init() {
    System.out.println("NetworkClient.init");
    connect();
    call("초기화 연결메시지");
}

@PreDestroy
public void close() {
    System.out.println("NetworkClient.close");
    disconnect();
}

@PostConstruct, @PreDestroy 애노테이션 특징

최신 스프링에서 가장 권장하는 방법이다.
애노테이션 하나만 붙이면 되므로 매우 편리하다.
패키지가 jakarta.annotation인데 자바 자체에서 지원하는 것이다.
스프링에 종속적인 기술이 아니라 JSR-250 라는 자바 표준이다.
따라서 스프링이 아닌 다른 컨테이너에서도 동작한다. 컴포넌트 스캔과 잘 어울린다.

유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다.
외부 라이브러리를 초기화 종료 해야 하면 @Bean의 기능을 사용하자.

정리
@PostConstruct, @PreDestroy 애노테이션을 사용하자
코드를 고칠 수 없는 외부 라이브러리를 초기화 종료해야 하면 @Bean 의 initMethod , destroyMethod 를 사용하자

빈 스코프

51. 빈 스코프란?

지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지된다고 학습했다.
이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다.
스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다

스프링은 다음과 같은 다양한 스코프를 지원한다.
싱글톤:
기본 스코프 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

프로토타입:
스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.

웹 관련 스코프
request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.

다음과 같이 스코프를 설정할 수 있다.

@Scope("prototype")

싱글톤 프로토타입 request정도만 이해하면된다.

52. 프로토타입 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.
반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.

싱글톤 스코프
1.싱글톤 스코프의 빈을 스프링 컨테이너에 요청한다.
2.스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
3.이후에 스프링 컨테이너에 같은 요청이 와도 같은 객체 인스턴스의 스프링 빈을 반환한다

프로토타입 스코프
1.프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
2.스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고 필요한 의존관계를 주입한다
새로운 빈 생성 + DI
3.스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
4.이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.

정리
여기서 핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고 의존관계 주입 초기화까지만 처리한다는 것이다.
클라이언트에 빈을 반환하고 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다.
프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다.
그래서 @PreDestroy 같은 종료메서드가 호출되지 않는다

public class SingletonTest {
    @Test
    public void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        assertThat(singletonBean1).isSameAs(singletonBean2);
        ac.close(); // 종료
    }

    @Scope("singleton")
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }
}

빈 초기화 메서드를 한번만 실행하고
같은 인스턴스의 빈을 조회하고
한번만 종료 메서드를 정상 호출 된 것을 확인할 수 있다.

한번생성하고 같은 객체를 주기 때문이다.

public class PrototypeTest {
    @Test
    public void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close(); // 종료
    }

    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만
프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고 초기화 메서드도 실행된다.
프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고 초기화도 2번 실행된 것을 확인할 수 있다.

싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만,
프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하고 더는 관리하지 않는다.
따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestroy 같은 종료 메서드가 전혀 실행되지 않는다

프로토타입 빈의 특징 정리
스프링 컨테이너에 요청할 때 마다 새로 생성된다.
스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입 그리고 초기화까지만 관여한다.
종료 메서드가 호출되지 않는다.
그래서 프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다.
종료 메서드에 대한 호출도 클라이언트가 직접 해야한다

prototypeBean1.destroy();
prototypeBean2.destroy();

53. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

대부분 싱글톤을 그대로 사용하고 어쩔때 한번씩 프로토타입을 사용하는데 여기서 문제가 발생한다.

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다.
하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.

프로토타입 빈 직접 요청

스프링 컨테이너에 프로토타입 빈 직접 요청1
1.클라이언트A는 스프링 컨테이너에 프로토타입 빈을 요청한다.

2.스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x01)한다. 해당 빈의 count 필드 값은 0이다.

3.클라이언트는 조회한 프로토타입 빈에 addCount() 를 호출하면서 count 필드를 +1 한다.

결과적으로 프로토타입 빈(x01)의 count는 1이 된다

스프링 컨테이너에 프로토타입 빈 직접 요청2
1.클라이언트B는 스프링 컨테이너에 프로토타입 빈을 요청한다.

2.스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x02)한다. 해당 빈의 count 필드 값은 0이다.

3.클라이언트는 조회한 프로토타입 빈에 addCount() 를 호출하면서 count 필드를 +1 한다.
결과적으로 프로토타입 빈(x02)의 count는 1이 된다.

public class SingletonWithPrototypeTest1 {
    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init " + this);
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

싱글톤 빈에서 프로토타입 빈 사용
clientBean 이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예를 보자.

싱글톤에서 프로토타입 빈 사용1
clientBean 은 싱글톤이므로 보통 스프링 컨테이너 생성 시점에 함께 생성되고 의존관계 주입도 발생한다.
1.clientBean 은 의존관계 자동 주입을 사용한다.
주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.

2.스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 에 반환한다. 프로토타입 빈의 count 필드 값은 0이다.
이제 clientBean 은 프로토타입 빈을 내부 필드에 보관한다. (정확히는 참조값을 보관한다.)

클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받는다.
싱글톤이므로 항상 같은 clientBean 이 반환된다.

3.클라이언트 A는 clientBean.logic() 을 호출한다.

4.clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
count값이 1이 된다

싱글톤에서 프로토타입 빈 사용3
클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받는다.
싱글톤이므로 항상 같은 clientBean 이 반환된다.

여기서 중요한 점이 있는데 clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.
주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지 사용 할 때마다 새로 생성되는 것이 아니다

5.클라이언트 B는 clientBean.logic() 을 호출한다.

6.clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.
원래 count 값이 1이었으므로 2가 된다

스프링은 일반적으로 싱글톤 빈을 사용하므로 싱글톤 빈이 프로토타입 빈을 사용하게 된다.
그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 프로토타입 빈이 새로 생성되기는 하지만
싱글톤 빈과 함께 계속 유지되는 것이 문제다.

아마 원하는 것이 이런 것은 아닐 것이다.
프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라 사용할때 마다 새로 생성해서 사용하는 것을 원할 것이다.

참고할점 여러 빈에서 같은 프로토타입 빈을 주입 받으면 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.
예를 들어서 clientA, clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.

clientA prototypeBean@x01
clientB prototypeBean@x02

물론 사용할 때 마다 새로 생성되는 것은 아니다

54. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

싱글톤 빈과 프로토타입 빈을 함께 사용할 때 어떻게 하면 사용할 때 마다
항상 새로운 프로토타입 빈을 생성할 수 있을까?

54.1 ObjectProvider

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다

static class ClientBean {
    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

getObject()를 실행하면 그때서야 스프링컨테이너에서 PrototypeBean를 찾아줘서 전달한다.
그래소 필요할때마다 요청하는 것을 실행하는것이다.

실행해보면 ac.getBean()을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다.

그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.
필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱 DL 정도의 기능만 제공하는무언가가 있으면 된다.

ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.
참고로 과거에는 ObjectFactory 가 있었는데 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어졌다

특징
ObjectFactory:
기능이 단순, 별도의 라이브러리 필요 없음 스프링에 의존한다.
ObjectProvider:
ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없다. 스프링에 의존한다.

54.2 JSR-330 Provider

마지막 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.
자바표준으로 스프링에 의존하지 않는 것이다.
단점은 라이브러리에 추가해줘야한다.

이 방법을 사용하려면 다음 라이브러리를 gradle에 추가해야 한다.
스프링부트 3.0 미만
javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.
스프링부트 3.0 이상
jakarta.inject:jakarta.inject-api:2.0.1 라이브러리를 gradle에 추가해야 한다.

메소드만 getObject가 아니라 get이 된다.

static class ClientBean {
    //@Autowired
    //private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.get();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}

실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.
(DL)자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
장점은 심플하다 이고 단점도 심플하다이다.

특징
get() 메서드 하나로 기능이 매우 단순하다.
별도의 라이브러리가 필요하다.
자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

정리
그러면 프로토타입 빈을 언제 사용할까?
매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
그런데 실무에서 웹 애플리케이션을 개발해보면 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
ObjectProvider , JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

A가 B를의존하고 B가 A를 의존할때가 가끔 있다. 서로 순환적으로 필요하면 이런걸 사용할 수 있다.
그런데 거의 사용할일 없고 다른데 이런게 들어갈때도 있다. 알아두기만 하자.

참고할점
실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지
아니면 스프링이 제공하는 ObjectProvider를 사용할 것인지 고민이 될 것이다.

ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하다.
만약 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야한다.

스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다.
대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에 특별히 다른 컨테이너를 사용할 일이 없다면 스프링이 제공하는 기능을 사용하면 된다.

JPA처럼 자바 표준이 이긴 것도 있다.
그런데 대부분 자바표준이 불편하고 스프링기능이 편해서 넘어가지 않는 경우도 많다.
컨테이너 기술은 스프링이 사실상 표준이라고 할 수 있다.
스프링 기능이 편리하면 대부분 그냥 이것을 사용하고 기능이 비슷하면 자바표준을 사용하는 경우도 있다.
본인의 판단이 필요한 영역인듯 하다.

55. 웹 스코프

지금까지 싱글톤과 프로토타입 스코프를 학습했다.
싱글톤은 스프링 컨테이너의 시작과 끝까지 함께하는 매우 긴 스코프이고
프로토타입은 생성과 의존관계 주입 그리고 초기화까지만 진행하는 특별한 스코프이다.

이번에는 웹 스코프에 대해서 알아보자

55.1 웹 스코프의 특징

웹 스코프는 웹 환경에서만 동작한다.
웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.
따라서 종료 메서드가 호출된다.

55.2 웹 스코프 종류

request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.

session: HTTP Session과 동일한 생명주기를 가지는 스코프

application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프

websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

request 스코프를 예제를 볼것이지만 나머지도 범위만 다르지 동작 방식은 비슷하다.

56. request 스코프 예제 만들기

웹 환경 추가
웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가해줘야한다.

동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴때 사용하기 딱 좋은것이 바로 request 스코프이다.

다음과 같이 로그가 남도록 request 스코프를 활용해서 추가 기능을 개발해보자.

[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close

기대하는 공통 포멧: [UUID][requestURL] {message}
UUID를 사용해서 HTTP 요청을 구분하자.
requestURL 정보도 추가로 넣어서 어떤 URL을 요청해서 남은 로그인지 확인하자.

@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "]" + "[" + requestURL + "] " +
                message);
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}

로그를 출력하기 위한 MyLogger 클래스이다.
@Scope(value = "request") 를 사용해서 request 스코프로 지정했다.
이제 이 빈은 HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 소멸된다.

이 빈이 생성되는 시점에 자동으로 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장해둔다.
이 빈은 HTTP 요청 당 하나씩 생성되므로 uuid를 저장해두면 다른 HTTP 요청과 구분할 수 있다.
이 빈이 소멸되는 시점에 @PreDestroy 를 사용해서 종료 메시지를 남긴다.
requestURL 은 이 빈이 생성되는 시점에는 알 수 없으므로 외부에서 setter로 입력 받는다

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

로거가 잘 작동하는지 확인하는 테스트용 컨트롤러다.
여기서 HttpServletRequest를 통해서 요청 URL을 받았다.
requestURL 값 http://localhost:8080/log-demo
이렇게 받은 requestURL 값을 myLogger에 저장해둔다.
myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.
컨트롤러에서 controller test라는 로그를 남긴다.

비즈니스 로직이 있는 서비스 계층에서도 로그를 출력해보자.
여기서 중요한점이 있다.
request scope를 사용하지 않고 파라미터로 이 모든 정보를 서비스 계층에 넘긴다면 파라미터가 많아서 지저분해진다.
더 문제는 requestURL 같은 웹과 관련된 정보가 웹과 관련없는 서비스 계층까지 넘어가게 된다.
웹과 관련된 부분은 컨트롤러까지만 사용해야 한다.
서비스 계층은 웹 기술에 종속되지 않고 가급적 순수하게 유지하는 것이 유지보수 관점에서 좋다.
request scope의 MyLogger 덕분에 이런 부분을 파라미터로 넘기지 않고
MyLogger의 멤버변수에 저장해서 코드와 계층을 깔끔하게 유지할 수 있다

스프링 애플리케이션을 실행 시키면 오류가 발생한다.
메시지 마지막에 싱글톤이라는 단어가 나오고 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만
request 스코프 빈은 아직 생성되지 않는다.
이 빈은 실제 고객의 요청이 와야 생성할 수 있다

2023.05.20

지금까지 우리는 스프링의 핵심 원리와 핵심 기능에 대해서 깊이있게 학습했다.
스프링이 왜 만들어졌고, 왜 필요한지, 그리고 객체 지향 설계와 스프링이 왜 땔 수 없는 관계인지 이해했다.
스프링의 핵심 원리와 핵심 컨셉을 제대로 학습했기 때문에 스프링 웹 MVC, 스프링 데이터 접근 기술,
스프링 부트를 포함해서 스프링의 핵심 기술을 활용하는 수 많은 스프링 기술들을 배우고 사용할 때도,
단순한 기능 사용을 넘어서 깊이있는 이해가 가능할 것이다.

'기초단계 > SPRING' 카테고리의 다른 글

2023.05.24 Spring  (0) 2023.05.24
2023.05.22 Spring  (0) 2023.05.22
2023.05.17 Spring  (0) 2023.05.19
2023.05.16 Spring  (0) 2023.05.16
2023.05.13 Spring  (0) 2023.05.16

+ Recent posts