스프링 핵심 원리 - 기본편 정리

Jim·2023년 6월 24일
0

스프링

목록 보기
1/10
post-thumbnail

스프링의 진짜 핵심

  • 스프링은 자바 언어 기반의 프레임워크
  • 자바 언어의 가장 큰 특징 - 객체 지향 언어
  • 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크
  • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크

좋은 객체 지향 프로그래밍?

객체 지향 특징

  • 추상화
  • 캡슐화
  • 상속
  • 다형성

객체 지향 프로그래밍

  • 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것. 각각의 객체메시지를 주고받고, 데이터를 처리할 수 있다. (협력)
  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.

다형성

역할과구현을 분리 -> 단순, 유연, 변경 용이

  • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
  • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
  • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다.
  • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.
  • 객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기

다형성의 본질

  • 인터페이스를 구현한 객체 인스턴스를 실행 시점유연하게 변경할 수 있다.
  • 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야함
  • 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.

역할과 구현을 분리

정리

  • 유연하고, 변경이 용이
  • 확장 가능한 설계
  • 클라이언트에 영향을 주지 않는 변경 가능
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요

한계

  • 역할(인터페이스) 자체가 변하면, 클라이언트, 서버 모두에 큰 변경이 발생

스프링과 객체 지향

  • 스프링은 다형성을 극대화해서 이용할 수 있게 도와 준다.
  • 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 역활과 구현을 편리하게 다룰 수 있도록 지원.

좋은 객체 지향 설계의 5가지 원칙(SOLID)

SRP (Single Responsibility Principle) : 단일 책임 원칙
OCP (Open/Closed Principle) : 개방-폐쇄 원칙
LSP (Liskov Substitution Principle) : 리스코프 치환 원칙
ISP (Interface Segregation Principle) : 인터페이스 분리 원칙
DIP (Dependency Inversion Principle) : 의존관계 역전 원칙

SRP (단일 책임 원칙)

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 중요한 기준은 변경! 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것

OCP (개방-폐쇄 원칙)

  • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다.
  • 다형성 활용 -> 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현
public class MemberService {
	
    private MemberRepository memberRepository = new MemoryMemberRepository();
}
public class MemberService {

//	private MemberRepository memberRepository = new MemoryMemberRepository();
    private MemberRepository memberRepository = new JdbcMemberRepository();
 
}
  • 위와 같이 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
  • 분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.
    -> 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요

LSP (리스코프 치환 원칙)

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙, 인터페이스를 구현한 구현체는 믿고 사용하려면, 이 원칙이 필요
  • 단순히 컴파일에 성공하는 것을 넘어서는 이야기

ISP (인터페이스 분리 원칙)

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

DIP (의존관계 역전 원칙)

  • 프로그래머는 "추상화(인터페이스)에 의존해야지, 구체화(구현 클래스)에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다.
  • 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다! 구현체에 의존하게 되면 변경이 아주 어려워진다.
  • OCP에서 설명한 MemberService는 인터페이스에 의존하지만, 구현 클래스도 동시에 의존 -> DIP 위반

정리

  • 객체 지향의 핵심은 다형성
  • 다형성 만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
  • 다형성 만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.
  • 다형성 만으로는 OCP, DIP를 지킬 수 없다.

객체 지향 설계와 스프링

스프링 이야기에 왜 객체 지향 이야기가 나오는가?

  • 스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원
    • DI(Dependency Injection): 의존관계, 의존성 주입
    • DI 컨테이너 제공
  • 클라이언트 코드의 변경 없이 기능 확장
  • 쉽게 부품을 교체하듯이 개발

스프링이 없던 시절

  • 순수하게 자바로 OCP, DIP 원칙들을 지키면서 개발을 해보면, 결국 스프링 프레임워크를 만들게 된다. (더 정확히는 DI 컨테이너)

정리

  • 모든 설계에 역할구현을 분리하자.
  • 이상적으로는 모든 설계에 인터페이스를 부여하자.
  • 하지만 인터페이스를 도입하면 추상화라는비용이 발생
  • 기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고, 향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법

IOC, DI 그리고 컨테이너

제어의 역전 IoC(Inversion of Control)

  • 기존 프로그램은 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행했다.
  • 반면에 AppConfig가 등장한 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. 프로그램의 제어 흐름은 이제 AppConfig가 가져간다.
  • 이렇듯 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.

프레임워크 vs 라이브러리

  • 내가 작성한 코드를 제어하고, 대신 실행하면 프레임워크(JUnit)
  • 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 라이브러리

의존관계 주입 DI(Dependency Injection)

  • OrderServiceImpl은 DiscountPolicy 인터페이스에 의존. 실제 어떤 구현 객체가 사용될지는 모른다.
  • 의존관계는 1. 정적인 클래스 의존 관계와, 2. 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.

1. 정적인 클래스 의존 관계

클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있다. 그런데 이러한 클래스 의존관계 만으로는 실제 어떤 객체가 OrderServiceImpl에 주입 될지 알 수 없다.

2. 동적인 객체 인스턴스 의존 관계

애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계

  • 애플리케이션 실행 시점(런타임)에 외부에서 실제 구현 객체를 생성하고 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
  • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
  • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
  • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

IoC 컨테이너, DI 컨테이너

  • AppConfig 처럼 객체를 생성하고 관리하면서 의존관계를 연결해 주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
  • 의존관계 주입에 초점을 맞추어 최근에는 주로 DI컨테이너라 한다.
  • 또는 어셈블러, 오브젝트 팩토리 등으로 불리기도 한다.

스프링 컨테이너

  • ApplicationContext를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI를 했지만, 이제부터는 스프링 컨테이너를 통해서 사용한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용.
  • 이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 한다. 스프링 빈은 applicationContext.getBean()메서드를 사용해서 찾을 수 있다.
  • 기존에는 개발자가 직접 자바코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었다.

    코드가 약간 더 복잡해진 것 같은데, 스프링 컨테이너를 사용하면 어떤 장점이 있을까?

스프링 컨테이너 생성

//스프링 컨테이너 생성
ApplicationContext applicationContext = 
new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라 한다.
  • ApplicationContext는 인터페이스이다.
  • 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
  • AnnotationConfigApplicationContext 클래스는 ApplicationContext 인터페이스의 구현체이다.

더 정확히는 스프링 컨테이너를 부를 때 BeanFactory, ApplicationContext로 구분해서 이야기 한다. BeanFactory를 직접 사용하는 경우는 거의 없으므로 일반적으로 ApplicationContext를 스프링 컨테이너라 한다.

스프링 컨테이너 생성 과정

  1. 스프링 컨테이너 생성
  • new AnnotationConfigApplicationContext(AppConfig.class)
  • 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다.
  • 여기서는 AppConfig.class를 구성 정보로 지정했다.
  1. 스프링 빈 등록
  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록
  • 빈 이름은 메서드 이름을 사용, 직접 부여할 수 도 있다. @Bean(name="memberService2")
  • 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면, 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 설정에 따라 오류가 발생.
  1. 스프링 빈 의존관계 설정
  • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)
  • 단순히 자바 코드를 호출하는 것 같지만, 차이가 있다. 이 차이는 뒤에 싱글톤 컨테이너에서 설명

스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다. 그런데 이렇게 자바 코드로 스프링빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다. 자세한 내용은 의존관계 자동주입에서 다시 설명

스프링 컨테이너에 등록된 모든 빈 조회

class ApplicationContextInfoTest {

    AnnotationConfigApplicationContext ac = 
    	new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("모든 빈 출력하기")
    void findAllBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            Object bean = ac.getBean(beanDefinitionName);
            System.out.println("name = " + beanDefinitionName 
            + " object = " + bean);
        }
    }

    @Test
    @DisplayName("애플리케이션 빈 출력하기")
    void findApplicationBean() {
        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = 
            	ac.getBeanDefinition(beanDefinitionName);

            //Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
            //Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
           if(beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("name = " + beanDefinitionName 
                + " object = " + bean);
            }
        }
    }
}

모든 빈 출력하기

  • ac.getBeanDefinitionNames() : 스프링에 등록된 모든 빈 이름을 조회
  • ac.getBean() : 빈 이름으로 빈 객체(인스턴스) 조회

애플리케이션 빈 출력하기

  • 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력
  • 스프링이 내부에서 사용하는 빈은 getRole()로 구분할 수 있다.
    • ROLE_APPLICATION : 일반적으로 사용자가 정의한 빈
    • ROLE_INFRASTRUCTURE : 스프링이 내부에서 사용하는 빈

스프링 빈 조회 - 기본

  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 예외 발생
    NoSuchBeanDefinitionException: No bean named 'xxxxx' available
class ApplicationContextBasicFindTest {

    AnnotationConfigApplicationContext ac = 
    	new AnnotationConfigApplicationContext(AppConfig.class);

    @Test
    @DisplayName("빈 이름으로 조회")
    void findBeanByName() {
        MemberService memberService = 
        	ac.getBean("memberService", MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("이름없이 타입으로만 조회")
    void findBeanByType() {
        MemberService memberService =
        	ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("구체 타입으로 조회")
    void findBeanByName2() {
        MemberService memberService =
        	ac.getBean("memberService", MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
    }

    @Test
    @DisplayName("빈 이름으로 조회X")
    void findBeanByNameX() {
        //ac.getBean("xxxxx", MemberService.class);
        assertThrows(NoSuchBeanDefinitionException.class,
                () -> ac.getBean("xxxx", MemberService.class));
    }
}

구체 타입으로 조회하면 변경시 유연성이 떨어진다.

스프링 빈 조회 - 동일한 타입이 둘 이상

  • 타입으로 조회시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생. 이때는 빈 이름을 지정하자.
  • ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.
class ApplicationContextSameBeanFindTest {

    AnnotationConfigApplicationContext ac = 
    	new AnnotationConfigApplicationContext(SameBeanConfig.class);

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

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

    @Test
    @DisplayName("특정 타입을 모두 조회하기")
    void findAllBeanByType() {
        Map<String, MemberRepository> beansOfType = 
        	ac.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();
        }
    }
}

스프링 빈 조회 - 상속 관계

  • 부모 타입으로 조회하면, 자식 타입도 함께 조회한다.
  • Object 타입으로 조회하면, 모든 스프링 빈을 조회한다.
class ApplicationContextExtendsFindTest {

    AnnotationConfigApplicationContext ac = 
    	new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다")
    void findBeanByParentTypeDuplicate() {
        assertThrows(NoUniqueBeanDefinitionException.class,
                () -> ac.getBean(DiscountPolicy.class));
    }

    @Test
    @DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다")
    void findBeanByParentTypeBeanName() {
        DiscountPolicy rateDiscountPolicy =
        	ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
        assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("특정 하위 타입으로 조회")
    void findBeanBySubType() {
        RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
        assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기")
    void findAllBeanByParentType() {
        Map<String, DiscountPolicy> beansOfType =
        	ac.getBeansOfType(DiscountPolicy.class);
        assertThat(beansOfType.size()).isEqualTo(2);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + 
            	" value = " + beansOfType.get(key));
        }
    }

    @Test
    @DisplayName("부모 타입으로 모두 조회하기 - Object")
    void findAllBeanByObjectType() {
        Map<String, Object> beansOfType =
        	ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + 
            	" value = " + beansOfType.get(key));
        }
    }

    @Configuration
    static class TestConfig {

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

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

BeanFactory와 ApplicationContext

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 담당
  • getBean()을 제공
  • 지금까지 우리가 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 그러면 둘의 차이는?
  • 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요
  • BeanFactory외 여러 인터페이스를 상속받아 메세지소스를 활용한 국제화 기능, 환경변수, 애플리케이션 이벤트, 편리한 리소스 조회등 여러 편리한 부가기능이 있다.
  • BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
  • BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.

다양한 설정 형식 지원 - 자바 코드, XML

  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다. - 자바 코드, XML, Groovy 등등

스프링 빈 설정 메타 정보 - BeanDefinition

  • 스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까? 그 중심에는 BeanDefinition이라는 추상화가 있다.
  • 쉽게 이야기해서 역할과 구현을 개념적으로 나눈 것
    • XML을 읽어서 BeanDefinition을 만들면 된다.
    • 자바 코드를 읽어서 BeanDefinition을 만들면 된다.
    • 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 된다. 오직 BeanDefinition만 알면된다.
  • BeanDefinition을 빈 설정 메타정보라 한다.
  • @Bean <bean>당 각각 하나씩 메타 정보가 생성된다.
  • 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.

싱글톤 컨테이너

스프링이 없는 순수한 DI 컨테이너 테스트

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
    
        AppConfig appconfig = new AppConfig();
        
        //1. 조회: 호출할 때 마다 객체를 생성
        MemberService memberService1 = appconfig.memberService();

        //2. 조회: 호출할 때 마다 객체를 생성
        MemberService memberService2 = appconfig.memberService();

        //참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        //memberService1 != memberService2
        assertThat(memberService1).isNotSameAs(memberService2);
    }
}
  • 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸 -> 메모리 낭비가 심하다.
  • 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계 -> 싱글톤 패턴

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴
  • 싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.
public class SingletonService {
	
    // 1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
	
    // 2. public으로 열어서 객체 인스턴스가 필요하면 
    // 이 static 메서드를 통해서만 조회하도록 허용
    public static SingletonService getInstance() {
        return instance;
    }
	
   	// 3. 생성자를 private으로 선언해서 
    // 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {

    }

    public void logic() {
        System.out.println("싱글톤 객체 로직 호출");
    }
}

싱글톤 패턴 문제점

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으료 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다.

싱글톤 컨테이너

  • 스프링 컨테이너는 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP, OCP, 테스트, private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있다.

싱글톤 방식의 주의점

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태유지(Stateful)하게 설계하면 안된다.
  • 무상태(Stateless)로 설계해야 한다!
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파리미터, ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!

@Configuration과 싱글톤

  • 스프링 컨테이너가 각각 @Bean으 호출해서 스프링 빈을 생성한다. 그래서 memberRepository()는 다음과 같이 총 세 번 호출?
    • 스프링 컨테이너가 스프링 빈에 등록하기 위해 @Bean이 붙어있는 memberRepository() 호출
    • memberService() 로직에서 memberRepository() 호출
    • orderService() 로직에서 memberRepository() 호출
  • 결과는 한 번만 호출된다!

Configuration과 바이트코드 조작의 마법

@Test
    void configurationDeep() {
        ApplicationContext ac = 
        	new AnnotationConfigApplicationContext(AppConfig.class);
        
        //AppConfig도 스프링 빈으로 등록된다.
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
        // 출력: bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d7

    }
  • 순수한 클래스라면 다음과 같이 출력되어야 한다.
    class hello.core.AppConfig
  • 내가 만든 클래스가 아니라 CGLIB이라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!
  • 이 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 복잡한 내부 기술로 바이트 코드를 조작해서 작성되어 있다. (@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드)

@Configuration을 적용하지 않고 @Bean만 적용하면?

  • AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된다.
  • 따라서, @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다. -> memberRepository()처럼 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는다.

컴포넌트 스캔

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

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, 
        	classes = Configuration.class)
)
public class AutoAppConfig {
    
}
  • 컴포넌트 스캔을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록되기 때문에, AppConfig, TestConfig 등 앞서 만들어두었던 설정 정보도 함께 등록되고, 실행되어 버린다. 그래서 excludeFilters 사용
  • 컴포넌트 스캔은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다. 이제 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component 애노테이션을 붙여주자.
  • 이전에 AppConfig에서는 @Bean으로 직접 설정 정보를 작성했고, 의존관계도 직접 명시했다. 이제는 이런 설정 정보 자체가 없기 때문에, 의존관계 주입도 각 클래스 안에서 해결해야 한다.
  • @Autowired는 의존관계를 자동으로 주입해준다.
public class AutoAppConfigTest {

    @Test
    void basicScan() {
        ApplicationContext ac = 
        	new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}
  • 로그를 잘 보면 컴포넌트 스캔이 잘 동작하는 것을 확인할 수 있다.
ClassPathBeanDefinitionScanner - Identified candidate component class:
.. RateDiscountPolicy.class
.. MemberServiceImpl.class
.. MemoryMemberRepository.class
.. OrderServiceImpl.class
  1. @ComponentScan
  • @ComponentScan@Component가 붙은 모든 클래스를 스프링 빈으로 등록
  • 빈 이름 기본 전략: MemberServiceImpl 클래스 -> memberServiceImpl
  • 빈 이름 직접 지정: @Component("memberService2")
  1. @Autowired 의존관계 자동 주입
  • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입 -> getBean(MemberRepository.class)와 동일하다고 이해하면 된다.

탐색 위치와 기본 스캔 대상

탐색할 패키지의 시작 위치 지정

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

컴포넌트 스캔 기본 대상

컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행

  • @Component
  • @Controller : 스프링 MVC 컨트롤러로 인식
  • @Service : 스프링 비즈니스 로직에서 사용, 특별한 처리를 하지 않는다.
  • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
  • 참고로 useDefaultFilters옵션은 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.

필터

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정
  • 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

중복 등록과 충돌

컴포넌트 스캔에서 같은 빈 이름을 등록하면 ?
1. 자동 빈 등록 vs 자동 빈 등록
2. 수동 빈 등록 vs 자동 빈 등록

1. 자동 빈 등록 vs 자동 빈 등록

  • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다. ->ConflictingBeanDefinitionException 예외 발생

2. 수동 빈 등록 vs 자동 빈 등록

  • 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.
  • 수동 빈 등록, 자동 빈 등록 오류시 스프링 부트 에러
    -> Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

의존관계 자동 주입

다양한 의존관계 주입 방법

  • 생성자 주입
  • 수정자 주입 (setter 주입)
  • 필드 주입
  • 일반 메서드 주입 -> 일반적으로 잘 사용하지 않는다.

생성자 주입

  • 생성자 호출시점에 딱 한 번만 호출되는 것이 보장된다.
  • 불편, 필수 의존관계에 사용
  • 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당

수정자 주입 (setter 주입)

  • 선택, 변경 가능성이 있는 의존관계에 사용
  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법

필드 주입

  • 코드가 간결하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다
  • DI 프레임워크가 없으면 아무것도 할 수 없다.
  • 순수한 자바 테스트 코드에서는 당연히 @Autowired가 동작하지 않는다. @SpringBootTest 처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다.
  • 사용하지 말자!
    • 애플리케이션의 실제 코드와 관계 없는 테스트 코드
    • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용

당연한 이야기이지만 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 Member 같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

옵션 처리

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨 (기본값이 true 이므로 자동 주입 대상이 없으면 오류 발생)
  • org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면 null 입력
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty 입력
  • @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 생성자 자동 주입에서 특정 필드에만 사용해도 된다.
//호출 안됨 (Member는 스프링 빈이 아니다.)
@Autowired(required = false)
public void setNoBean1(Member member) {
	System.out.println("setNoBean1 = " + member);
}
//null 호출
@Autowired
public void setNoBean2(@Nullable Member member) {
	System.out.println("setNoBean2 = " + member);
}
//Optional.empty 호출
@Autowired(required = false)
public void setNoBean3(Optional<Member> member) {
	System.out.println("setNoBean3 = " + member);
}
setNoBean2 = null
setNoBean3 = Optional.empty

생성자 주입을 선택해라!

불변

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다. (불변해야 한다.)
  • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다.
    -> 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 한번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.

누락

  • 프레임워크 없이 순수한 자바 코드를 단위 테스트 하는 경우, 수정자 의존관계일때와 다르게 생성자 주입을 사용하면 주입 데이터를 누락 했을 때 컴파일 오류가 발생

final 키워드

  • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
  • 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.
  • 생성자 주입 방식이 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법
  • 기본으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여

롬복과 최신 트랜드

  • 롬복 라이브러리가 제공하는 @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
    -> 롬복이 자바의 애노테이션 프로세서라는 기능을 이용해서 컴파일 시점에 생성자 코드를 자동으로 생성. 실제 class를 열어보면 코드가 추가 되어있다.

조회 빈이 2개 이상 - 문제

  • @Autowired는 타입(Type)으로 조회한다. 따라서 마치 다음 코드와 유사하게 동작한다. (실제로는 더 많은 기능을 제공) -> ac.getBean(DiscountPolicy.class)
  • 스프링 빈 조회에서 학습했듯이 타입으로 조회하면 선택된 빈이 2개 이상일 때 문제가 발생한다.
NoUniqueBeanDefinitionException: No qualifying bean of type 
'hello.core.discount.DiscountPolicy' available: expected single matching bean 
but found 2: fixDiscountPolicy,rateDiscountPolicy
  • 이때 하위 타입으로 지정할 수 도 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다. 그리고 이름만 다르고, 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다. 스프링 빈을 수동 등록해서 문제를 해결해도 되지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.

Autowired 필드 명 매칭

  • @Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭
@Autowired
private DiscountPolicy rateDiscountPolicy
  • 필드명이 rateDiscountPolicy이므로 정상 주입된다.
  • 필드 명 매칭은 먼저 타입 매칭을 시도 하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능

Qualifier 사용

  • Qualifier는 추가 구분자를 붙여주는 방법. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
//생성자 자동 주입 예시
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
		@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
	this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}
//수정자 자동 주입 예시
@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}
  • @Qulifier로 주입할 때 @Qualifier("mainDiscountPolicy")를 못찾으면 misDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다.
// 직접 빈 등록시에도 동일하게 사용
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
	return new ...
}

Primary 사용

  • @Primary는 우선순위를 정하는 방법. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.
// rateDiscountPolicy가 우선권을 가짐
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}
// 생성자 자동 주입 예시
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
	DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}

// 수정자 자동 주입 예시
@Autowired
public DiscountPolicy setDiscountPolicy(DiscountPolicy discountPolicy) {
	this.discountPolicy = discountPolicy;
}

@Primary, @Qualifier 활용

  • 메이 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득 하는 방식. 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해주는 것은 상관 없다.
  • 우선순위 : @Primary < @Qualifier

애노테이션 직접 만들기

  • 애노테이션을 직접 만들면 컴파일시 타입 체크가 가능하다.
  • 애노테이션에는 상속이라는 개념이 없다. 이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
		@MainDiscountPolicy DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
 	this.discountPolicy = discountPolicy;
}

//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
	this.discountPolicy = discountPolicy;
}

조회한 빈이 모두 필요할 때, List, Map

  • 예를 들어서 할인 서비스를 제공하는데, 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다고 가정해보자. 스프링을 사용하면 소위 말하는 전략 패턴을 매우 간단하게 구현할 수 있다.
public class AllBeanTest {

    @Test
    void findAllBean() {
    	// new AnnotationConfigApplicationContext()를 통해 스프링 컨테이너를 생성
        // AutoAppConfig.class, DiscountService.class를 파라미터로 넘기면서 해당 클래스를 자동으로 스프링 빈으로 등록
        ApplicationContext ac = 
        	new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);
    }

    static class DiscountService {
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;

        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}
  • Map<String, DiscountPolicy> : map의 키에 스프링 빈의 이름을 넣어주고, 그 값으로 discountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • List<DiscountPolicy> : discountPolicy타입으로 조회한 모든 스프링 빈을 담아준다.
  • 만약 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입

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

  • 업루 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리등이 모두 업무 로직. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
  • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들.
  • 애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로 나타나게 하는 것이 유지보수 하기 좋다.
  • 위에서 본 조회한 빈이 모두 필요할 때, 조회하는 빈들을 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 어떤 빈들이 주입될지 파악하기 용이하다.

빈 생명주기 콜백

  • 데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면, 객체의 초기화와 종료 작업이 필요하다.
  • 스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.
  • 스프링 빈의 이벤트 라이프 사이클
    스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백(빈이 소멸되기 직전에 호출) -> 스프링 종료

인터페이스 InitializingBean, DisposableBean

  • InitializingBeanafterPropertiesSet() 메서드로 초기화를 지원
  • DisposableBeandestroy()메서드로 소멸을 지원
  • 초기화, 소멸 인터페이스 단점
    • 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존
    • 초기화, 소멸 메서드의 이름 변경 불가
    • 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

빈 등록 초기화, 소멸 메서드 지정

public class BeanLifeCycleTest {

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

    @Configuration
    static class LifeCycleConfig {

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

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
    }

    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);
    }

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

    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}
//결과
생성자 호출, url = null
NetworkClient.init
connect: http://hello-spring.dev
call: http://hello-spring.dev message = 초기화 연결 메시지
13:33:10.029 [main] DEBUG 
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing NetworkClient.close
close + http://hello-spring.dev
  • 메서드 이름을 자유롭게 줄 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.
  • @Bean의 destroyMethod는 기본값이 (inferred)(추론)으로 등록되어 있다.
  • 이 추론 기능은 close, shutdown 이라는 이름의 메서드를 자동으로 호출.
  • 라이브러리는 대부분 close, shutdown 이라는 이름의 종료 메서드를 사용하기 때문에 종료 메서드는 따로 적어주지 않아도 잘 동작한다.
  • 추론 기능을 사용하기 싫으면 destroyMethod=""

애노테이션 @PostConstruct, @PreDestroy

  • 메서드에 애노테이션 하나만 붙이면 된다.
  • javax.annotation.PostConstruct -> 스프링에 종속적인 기술이 아니라 JSR-250이라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
  • 컴포넌트 스캔과 잘 어울린다.
  • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료 해야 하면 @Bean의 기능을 사용하자.

빈 스코프

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프
  • 웹 관련 스코프
    • request: 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
    • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

프로토타입 스코프

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

    핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리 (종료 메서드 호출 X)

public class PrototypeTest {

    @Test
    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");
        }
    }
}
//실행 결과
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@13d4992d
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@302f7971
org.springframework.context.annotation.AnnotationConfigApplicationContext - 
Closing
  • 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.

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

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

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);
    }

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(1);
    }

    @Scope("singleton")
    static class ClientBean {
		
        /* 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청
        @Autowired
		private ApplicationContext ac;
		
        public int logic() {
 			PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
 			prototypeBean.addCount();
 			int count = prototypeBean.getCount();
 			return count;
		}
        */
        
        /* ObjectProvider 사용
        @Autowired
		private ObjectProvider<PrototypeBean> prototypeBeanProvider;
        
		public int logic() {
 			PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
 			prototypeBean.addCount();
 			int count = prototypeBean.getCount();
 			return count;
		}
        */
        
        @Autowired
        private Provider<PrototypeBean> prototypeBeanProvider;

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

    @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");
        }
    }
}
  • 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청 시

    • 의존관계를 외부에서 주입(DI)받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색) 이라한다.
    • 그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
    • 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 된다.
  • ObjectFactory, ObjectProvider (ObjectFactory에서 편의 기능 추가)

    • ObjectProvidergetObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
    • 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
    • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
    • ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
  • JSR-330 Provider

    • javax.inject.Provier라는 JSR-330 자바 표준 (스프링 부트 3.0jakarta.inject.Provider)
    • 스프링부트 3.0 미만 -> javax.inject:javax.inject:1,
      스프링 부트 3.0 이상 -> jakarta.inject:jakarta.inject-api:2.0.1 라이브러리를 gradle에 추가
    • Providerget()을 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
    • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.

웹 스코프

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
  • 웹 스코프는 웹 환경에서만 동작한다.
  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.

request 스코프 예제

  • spring-boot-starter-web라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해서 웹서버와 스프링을 함께 실행
  • 스프링 부트는 웹 라이브러리가 없으면 우리가 지금까지 학습한 AnnotationConfigApplicationContext을 기반으로 애플리케이션을 구동한다. 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요하므로 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션을 구동한다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
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);
    }
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    //private final ObjectProvider<MyLogger> myLoggerProvider;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString();
        //MyLogger myLogger = myLoggerProvider.getObject();

        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {

	//private final ObjectProvider<MyLogger> myLoggerProvider;
    private final MyLogger myLogger;

    public void logic(String id) {
    	//MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}
  • proxyMode = ScopedProxyMode.TARGET_CLASS 추가
    • 적용 대상이 인터페이스가 아닌 클래스면 TARTGET_CLASS
    • 적용 대상이 인터페이스면 INTERFACES
  • 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
  • 주입된 myLogger를 확인 해보자.
System.out.println("myLogger = " + myLogger.getClass());
//출력 결과
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
  • CGLIB이라는 바이트코드를 조작하는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
  • ac.getBean("myLogger", MyLogger.class)로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다.
  • 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
  • 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤 처럼 동작한다.

스프링 핵심 원리 - 기본편

profile
You never fail until you stop trying.

0개의 댓글