부자 되기 위한 블로그, 머니킹

스프링 기초

어떤 프레임워크이든 간에 그 프레임워카의 진행 흐름이나 기초 구성을 아는 것이 중요하다. 스프링 또한 많은 기능들을 제공하지만 핵심적인 구조에 대해서 알 필요가 있다. 이번 기회에 스프링에 대해 제대로 파악하고자 인프런에서 김영한 님의 스프링 기초 수강을 들었다. 오늘은 그 내용을 정리한 포스팅을 연재하려고 한다.

 

 

 

객체 지향 설계와 스프링

 

객체 지향적 설계

  • SPR(Single Responsibility Principle) : 단일 책임 원칙
    • 한 클래스는 하나의 책임만 가져야 한다.
  • OCP(Open Closed Principle) : 개방 폐쇄 원칙
    • 소프트웨어 요소는 확장에는 열려 있으나 변경에는 닫혀 있어야 한다
  • LSP(Liskov Substitution Principle) : 리스코프 치환 원칙
    • 서브 타입은 언제나 자신의 기반타입으로 교체할 수 있어야 한다.
  • ISP(Interface Segregation Principle) : 인터페이스 분리 원칙
    • 클라이언트는 자신이 사용하지 않는 메소드에 의존 관계를 맺으면 안된다.
  • DIP(Dependency Inversion Principle) : 의존 역전 원칙
    • 프로그래머는 “추상화에 의존해야지, 구체화에 의존하면 안된다.” 의존성 주입은 이 원칙을 따르는 방법 중 하나다.

 

 

 

 

스프링 핵심 원리 이해

 

스프링 객체 설계

  • interface 에 역할을 설계하고 이를 상속받아 구현체에 기능을 작성하자
  • 객체가 다른 객체의 기능이 필요할 때 의존 관계가 발생한다.
    • 하지만 OCP 원리에 의해 해당 기능이 변경될 때에도 의존 관계 대상 객체만 변경해야 한다.
    • 이를 위해 관심사를 분리하여 구현 객체를 생성하고 연결 책임을 지닌 별도의 설정 클래스가 필요하다.
    • Config Class를 만들어 위의 내용처럼 구성할 수 있지만 자동으로 이런 일을 해주는 클래스는 없을까?

 

 

IOC,DI, 컨테이너

  • 제어의 역전 IOC
    • 프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에 관리하는 것을 말한다.
  • 프레임워크 vs 라이브러리
    • 프레임워크가 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크가 맞다. (JUnit)
    • 반면에 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리다.
  • 의존관계 주입
    • 정적 클래스 의존관계 : 코드만 보고 의존관계 쉽게 파악
    • 동적 클래스 의존관계 : 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결됨
      • 실행 시점에 외부에서 실제 구현 객체를 생성하고 의존관계가 연결되는 것은 의존관계 주입이라고 함
  • IOC,DI 컨테이너
    • 객체를 생성하고 관리하면서 의존관계 연결해주는 것을 말한다.

 

 

스프링에서의 의존관계 주입

  • ApplicationContext 를 스프링 컨테이너라 한다.
  • 스프링 컨테이너는 @Configuration 이 붙은 AppConfig 를 설정(구성) 정보로 사용한다. 여기서 @Bean 이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다 이렇게 스프링 컨테이너에
    등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 ApplicationContext.getBean() 메서드를 사용해서 찾을 수 있다

 

 

 

스프링 컨테이너와 스프링 빈

스프링 컨테이너의 생성 과정

  1. 스프링 컨테이너 생성
    1. new AnnotationConfigApplicationContext(AppConfig.class)
  2. 스프링 빈 등록
    1. 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다. (앞에만 소문자로)
  3. 스프링 빈 의존관계 설정 - 준비
  4. 스프링 빈 의존관계 설정 - 완료

 

 

스프링 컨테이너 함수 (AnnotationConfigApplicationContext)

AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(AppConfig.class);

String[] beanDefinitionNames = ac.getBeanDefinitionNames();

  for (String name : beanDefinitionNames) {
      //System.out.println("name = " + name);
      //Object bean = ac.getBean(name);
      //System.out.println("bean = " + bean);
      BeanDefinition beanDefinition = ac.getBeanDefinition(name);
      if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
          System.out.println("name = " + name);
          Object bean2 = ac.getBean(name);
          System.out.println("bean2 = " + bean2);
      }
  }

@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() {
        MemberServiceImpl memberService = ac.getBean("memberService",
MemberServiceImpl.class);
        assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
} 

@Test
@DisplayName("빈 이름으로 조회X") void findBeanByNameX() {
            //ac.getBean("xxxxx", MemberService.class);
            Assertions.assertThrows(NoSuchBeanDefinitionException.class, () ->
    ac.getBean("xxxxx", MemberService.class));
} }
  • AnnotationConfigApplicationContext([Configure class]) : 스프링 컨테이너 객체
  • ac.getBean() : 빈 조회
  • ac.getBeanDefinition() : bean 등록 정보 반환, AppConfig.class 같은 클래스의 설정 정보를 반환한다고 보면 됨
  • ac.getDefinitionNames : bean 이름들 반환

 

 

 

등록 빈 두개 이상인 문제

추상화를 통해 구현체를 2개 이상 만들고 추상화 클래스 타입로 bean을 찾을 때 오류가 발생한다.

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

물론 위와 같이 getBeansOfType을 통해 해당 클래스 타입 빈을 모두 반환할 수 있다.

 

 

BeanFactory, ApplicationContext

  • BeanFactory
    • 스프링 컨테이너의 최상위 인터페이스로 getBean을 제공한다.
  • ApplicationContext
    • BeanFactory 기능을 모두 상속받고 애플리케이션 개발에 빈을 관리/조회에 필요한 부가적인 기능을 제공한다.

 

 

ApplicationContext 제공 부가 기능

  • 메시지 소스를 활용한 국제화 기능
    • 한국어,영어 등 header lang 정보에 따라 다양한 언어 지원
  • 환경 변수
    • 로컬,개발,운영 등을 구분해서 처리해줌
  • 애플리케이션 이벤트
    • 이벤트를 발생하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회
    • 파일, 클래스패스, 외부 등에서 리소스 편리하게 조회

AnnotationConfigApplicationContext는 클래스 타입 형식의 설정 정보를 받아들일 수 있고ApplicationContext를 상속받은 다른 객체들은 다양한 형식의 설정 정보를 받아들일 수 있다.

 

 

 

BeanDefinition 정보

  • BeanClassName: 생성할 빈의 클래스 명(자바 설정 처럼 팩토리 역할의 빈을 사용하면 없음
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정, 예) memberService
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때 까지 최대한
    생성을 지연처리 하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용한다. (자바 설정 처럼 팩토리
    역할의 빈을 사용하면 없음)

 

 

 

싱글톤 컨테이너

 

빈에 등록된 클래스 인스턴스는 딱 1개만 생성되는 것이 보장되는 디자인 패턴이다. 이를 위해 스프링 컨테이너는 default 값으로 싱글톤으로 객체 인스턴스를 관리한다.

 

싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다!

  • 의존적 필드 x
  • 특정 클라이언트가 값을 변경할 수 있는 필드 x
  • 읽기만 가능
  • 필드 대신 자바에서 공유되지 않는 지역 변수, 파라미터, ThreadLocal 사용해야함

 

 

어떻게 스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리할까?

 

CGLIB라는 바이트 조작 라이브러리를 사용해 등록한 클래스를 상속받은 가짜 클래스를 스프링 빈으로 등록한다.

 

 

 

컴포넌트 스캔

일일히 Configuration 어노테이션을 통해 수동으로 Bean등록시 설정 정보가 엄청나게 많아진다. 이 점 때문에 스프링은 컴포넌트 스캔이라는 기능을 제공한다. 의존관계 주입 또한 @Autowired 어노테이션을 통해 기능을 제공한다.

 

컴포넌트 스캔 기본 대상

컴포넌트 스캔은 @Component 뿐만 아니라 다음과 내용도 추가로 대상에 포함한다.

  • @Component : 컴포넌트 스캔에서 사용
  • @Controlller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용

 

 

@Configuration
  @ComponentScan(
          includeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyIncludeComponent.class),
          excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyExcludeComponent.class)
  )
  static class ComponentFilterAppConfig {
  }

이렇게 Configuration 에서 includeFilters, excludeFilters를 통해 빈 등록에 제외할 수 있다.

  • FilterType 5가지 옵션.
    • 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

 

 

자동 빈 등록 vs 수동 빈 등록

수동 빈 등록이 우선권을 가진다

 

 

의존관계 자동 주입

다양한 의존관계 주입 방법이 있지만 생성자 주입을 선택하라 이 떄 RequiredArgsConstructor 롬복 어노테이션을 사용하면 자동으로 final 붙은 필드들을 모아 생성자를 만들어주기에 이를 사용하자

 

 

 

조회 빈이 2개 이상인 경우

조회 대상 빈이 2개 이상일 때 해결 방법

  • @Autowired 필드 명 매칭 (필드명을 구현체 이름으로 바꿈)
@Autowired
private DiscountPolicy rateDiscountPolicy
  • @Qualifier @Qualifier끼리 매칭 빈 이름 매칭
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
  • @Primary 사용
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

 

 

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

해당 타입의 스프링 빈이 모두 필요할 때는 List Map을 활용할 수 있다.

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

DiscountService는 Map으로 모든 DiscountPolicy 를 주입받는다. 이때 fixDiscountPolicy , rateDiscountPolicy 가 주입된다.

discount () 메서드는 discountCode로 "fixDiscountPolicy"가 넘어오면 map에서 fixDiscountPolicy 스프링 빈을 찾아서 실행한다. 물론 “rateDiscountPolicy”가 넘어오면 rateDiscountPolicy 스프링 빈을 찾아서 실행한다.

 

 

수동 빈 사용 vs 자동빈 사용

  • 수동빈 사용 : 기술 지원 빈에 사용, 공통 AOP 처리나 데이터베이스 연결, 공통 로그처리 등
  • 자동빈 사용 : 웹의 컨트롤러, 비지니스로직, 데이터계층 로직 처리 등을 할 때 자동빈 등록

애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로
나타나게 하는 것이 유지보수 하기 좋다

 

 

 

 

빈 생명주기 콜백

스프링 빈의 이벤트 라이프사이클

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

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

 

 

@PostConstructor, @PreDestory

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

@PreDestroy
public void close() {
    System.out.println("NetworkClient.close");
    disConnect();
}
  • PostConstructor : 빈생성/의존관계 주입이 끝나고 초기화 콜백 때 호출
  • PreDestory : 빈이 소멸전 콜백 때 호출

 

빈스코프

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

 

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

Scope Annotation을 통해 쉽게 프로토타입 스코프로 선언할 수 있다

 

 

 

프로토타입과 싱글톤이 같이 사용해야 하는 경우

싱글톤에서 프로토타입에 대한 의존 주입이 있는 경우나 같이 사용해야 하는 경우는 직접 프로토타입을 받으면 된다.

  • ac.getBean()
  • Provider

 

 

Provider 사용

@Autowired
  private ObjectProvider<PrototypeBean> prototypeBeanProvider;
  public int logic() {
      PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}

 

 

 

 

웹 스코프

웹 스코프를 의존관계에 주입 받을 경우 문제점 : 해당 빈은 실제 고객의 요청이 와야(request) 생성할 수 있다. 그렇다면 테스트 용도 등으로 request 요청이 안와도 생성하는 방법은 없을까?

 

 

 

스코프와 프록시

@Component
  @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
  public class MyLogger {
  }
  • 적용 대상이 클래스면 TARGET_CLASS
  • 적용 대상이 인터페이스면 INTERFACE

이렇게 하면 적용 대상(MyLogger) 이를 상속받은 가짜 프록시 객체를 생성한다. 의존관계 주입도 가짜 프록시 객체가 주입된다.