Spring 핵심원리 기본편 요약정리

쿠우·2022년 12월 16일
0

설계

회원 도메인 협력 '관계' :기획자들도 볼 수 있는 도식
회원 '클래스 다이어그램' : 클래스 들만 나타낸 개발자들의 도식
회원 '객체 다이어그램' : 동적인 흐름을 알 수 있는 다이어그램

위 3개로 설계를 구분

Spring이 왜 필요했는지 설명했음

-> 로직을 짜고 SOLID 원칙에 맞춰서 코드를 짜다가 복잡함을 발견하고
-> 스프링 프레임워크로 SOLID 원칙을 지킬 수 있다는 설명

SOLID원칙은 왜 중요한가?

-> 인터페이스를 통한 객체지향적 코드를 이용해서 구현과 역할을 분리시킴
-> 덕분에 비즈니스로직에 대해서 변경사항이나 추가 기능을 구현 할 때 소요되는 노력과 시간이 줄어든다.

스프링 컨테이너에 @Configuration과 @Bean을 통해 빈을 등록하고 조회해보는 과정 연습했음

(굳이 사용은 안하는 기능이지만 기본적인 내용이기도 하고 가끔 자바애플리케이션에서 스프링 컨테이너를 생성해서 써야 할 일이 있고 부모 타입으로 빈조회가 어디까지 가능한지 )
0. 모든 빈 출력하는 법
1. 애플리케이션 빈 (내가 등록한 빈) 출력하는 방법
2. 빈 이름으로 조회 , 이름없이 타입으로 조회하기
3. 구체 타입으로 조회(변경시 유연성이 떨어진다.)
4. 타입이 둘 이상 중복되면 중복오류 발생
5. 타입이 같으면 이름을 명시해주면 된다는 것
상속관계 조회시
6. 부모타입으로 조회시 자식이 둘 이상 있으면 중복오류 발생
7. 부모타입으로 조회시 자식 둘 이상 있을 때 빈이름으로 지정하면 된다는 것

빈 팩토리와 애플리케이션 컨택스트

( beanFactory 상속>> ApplicationContext 구현>> AnnotationConfigApplicationContext 등 )

BeanFactory

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

ApplicationContext

  • 메시지소스를 활용한 국제화 기능
    ->예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수(현업에서는 환경이 구분되어있음 로컬, 테스트 서버 등등)
    ->로컬, 개발, 운영등을 구분해서 처리
  • 애플리케이션 이벤트
    ->이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회
    ->파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

XML을 통한 ApplicationContext 설정

  • 최근에 Spring boot를 사용하며 XML대신 anotation을 통한 자바코드를 설정을 주로 함
    (과거에 클래식한 Spring에서 bean파일을 등록 해줬을 때 , https://velog.io/@koo9b9h/93%EC%9D%BC%EC%B0%A8-Spring-DI 에서 xml을 통해 어떻게 빈을 등록했는지 비교하면서 보면 좋다. )

  • 빈 설정에 대해서 스프링 컨테이너는 BeanDefinition이라는 추상화를 통해
    자바코드형식과 xml형식 등 다양한 형식으로 역할과 구현을 나눠놓았다.

ApplicatonContext에 대해서 작동 방식 확인했음. (살짝 이해안감/어려움)

  • 코드를 뒤적뒤적 해봤을 때 ApplicatonContext인터페이스를 AnnotationConfigApplicationContext 클래스에서 구현 생성자 안에서 AnnotatedBeanDefinitionReader를 사용한다.
    AnnotatedBeanDefinitionReader클래스는 매우복잡.. BeanDefinitionRegistry인터페이스를 이용해서 BeanDefinition 정보를 조정한다. -> https://blog.woniper.net/336 스프링 컨테이너에 대해서 정리 잘된 고마운 블로그
  • AnnotationConfigApplicationContext 는 AnnotatedBeanDefinitionReader 를 사용해서 AppConfig.class 를 읽고 BeanDefinition 을 생성한다.

BeanDefinition 메타 정보(데이터에 대한 데이터)에 대해서 확인했음.

웹 앱과 싱글톤

싱글톤이 SOLID원칙을 위배하는 안티패턴인데 스프링으로 어떻게 단점들을 다 제거했는지 확인 -> 싱글톤 컨테이너(스프링 컨테이너)로 싱글톤 객체를 생성하고 관리한다.

싱글톤 방식의 주의점

  • 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다. 무상태(stateless)로 설계해야 한다!
  • 가급적 읽기만 가능해야 한다.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다 절대!! 필드가 있으면 안된다. 공유된 값에 올바르게 작동 안하기 때문에

@Configuration 에 의해 어떻게 스프링 컨테이너에 싱글톤으로 조작 되는지 알아봤다.

  • 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용
  • CGLIB라는 바이트코드 조작 라이브러리를 사용
  • AppConfig클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것 그 임의의 다른 클래스가 바로 싱글톤이 보장(내부에 로직에 의해서)

@Configuration 없이 @Bean으로만 등록하면 싱글톤 보장이 안된다.

자동으로 스캔해서 빈등록하는 법과 자동으로 의존관계 주입해주는 법

@ComponentScan과 @Component로 빈등록이 가능하고 생성자에 @Autowired를 붙여주면 등록되어있는 빈에서 알맞게 찾아 의존관계를 주입해준다.

빈 이름 등록

  • 빈 이름 기본 전략은 첫글자만 소문자로 바뀐 클래스명으로 등록
  • 빈 이름 직접 지정은 @Component("지정이름") 이런식으로 이름 부여

탐색 위치와 기본스캔 대상 지정

  • basePackages = {"hello.지정패키지", "hello.지정패키지2"} 여러 시작위치 가능
  • basePackageClasses 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다
  • 개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다. 최근 스프링 부트도 이 방법을 기본으로 제공한다.

스프링 부트를 사용하면 따로 @ComponentScan 안써도 된다.

  • @SpringBootApplication 안에 들어있고 해당 프로젝트를 스캔하도록 설정됭어있음.

컴포넌트 스캔 기본 대상

  • 아래 어노테이션 모두 @Component를 포함하고 있다.
    @Controlller : 스프링 MVC 컨트롤러에서 사용
    @Service : 스프링 비즈니스 로직에서 사용
    @Repository : 스프링 데이터 접근 계층에서 사용
    @Configuration : 스프링 설정 정보에서 사용

필터

includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyExcludeComponent.class)
)
이런식으로 사용

  • @Component 면 충분하기 때문에, includeFilters 를 사용할 일은 거의 없다. excludeFilters
    는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다
    스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고, 선호

중복 등록과 충돌

  1. 자동 빈 등록 vs 자동 빈 등록
  • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.
  • ConflictingBeanDefinitionException 예외 발생
  1. 수동 빈 등록 vs 자동 빈 등록
  • 수동 빈 등록이 우선권을 가진다.(수동 빈이 자동 빈을 오버라이딩 해버린다.)
  • 수동 빈 등록시 남는 로그 Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
  • 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는 여러 설정들이 꼬여서 이런 결과가
    만들어지는 경우가 대부분이다! 그러면 정말 잡기 어려운 버그가 만들어진다. 항상 잡기 어려운 버그는 애매한 버그다.
    최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.

의존관계 자동주입

의존관계 주입 방법 4가지

####생성자 주입

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

setter 주입

  • setter에 주입하기로 선택하면 생성자 주입은 안써도 된다.
  • 선택, 변경 가능성이 있는 의존관계에 사용

필드 주입

  • 안티패턴이다
  • 코드가 간결해서 많은 개발자들을 유혹하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는
    치명적인 단점이 있다.
  • 변경하려면 setter를 만들어줘야하는데 그럼 setter를 만들어서 주입하면 되기 떄문.
  • 애플리케이션의 실제 코드와 관계 없는 테스트 코드에서는 사용해도 된다.
  • 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용할 때 사용해도 된다.

일반 메서드 주입

  • 거의 사용하지 않음(생성자와 setter선에서 다 해결)

옵션처리

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • @Nullable : 주입할 대상이 없으면 null로 입력된다
  • Optional<타입> : 자동 주입할 대상이 없으면 Optional.empty 가 입력된다.

왜 생성자 주입을 선택해야하는지!!

  • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려
    대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다.
  • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어두어야 한다. 누군가 실수로 변경할 수 도 있고, 변경하면 안되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다
  • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이
    설정되지 않는 오류를 컴파일 시점에 막아준다.

lombok 을 사용하라

  • 기존에 내 블로그에 적혀있는 @~~ArgsConstructor 랑 등등 참고

역할에 의존해야하는데 그럼 2개 이상의 빈이 검색되어 오류가 나는 경우가 있다.

  • 하위타입을 주입하면 DIP 위배로 역할 의존을 위배한다.
  • 해결 방법1: @Autowired 의 기능 중에 필드의 이름(파라미터 이름)을 따라 빈을 찾는기능을 사용한다
  • 해결 방법2: @Quilifier 추가 구분자를 통해 정보를 추가해준다.
  • 해결 방법 3: @Primary 를 통해 우선순위를 지정한다.

@Quilifier는 새로운 어노테이션을 만들어서 적용시키는 것이 유지보수에 더 좋다.

@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}

역할의존시 여러개 빈을 다 검색하는 방법으로는 Map과 List로 받는 방법이 있음

  • Map이나 List에 역할에 따른 구현된 하위타입들을 다 담아놓고 사용자 비즈니스 로직에 맞춰 .get() 하여 알맞게 동작하는 메서드를 만들어 사용하기도 한다.

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

업무 로직 빈 : 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는
리포지토리등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나,
공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

  • 편리한 자동 기능을 기본으로 사용하자 (관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담) (업무 로직 빈)
  • 기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미치기 때문에 기술 지원 빈을 주로 수동 빈 등록 해준다.
    (애플리케이션에 광범위하게 영향을 미치는 기술 지원 객체는 수동 빈으로 등록해서 딱! 설정 정보에 바로
    나타나게 하는 것이 유지보수 하기 좋다.) (스프링과 스프링 부트가 자동으로 등록하는 수 많은 빈들은 예외)
  • List, Map으로 담길 수 있는 여러 빈으로 구현 되어있는 경우 자동 등록을 사용시 파악하려면 여러 코드를 찾아봐야 한다 따라서 이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어두는게 좋다

빈 생명주기 콜백

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

  • 스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 사용 -> 소멸전 콜백 -> 스프링
    종료
  • 초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출
  • 소멸전 콜백: 빈이 소멸되기 직전에 호출

*참고 : 객체의 생성자 사용은 객체 내부의 값을 세팅하는 가벼운 작업만 수행하고 , 무거운 작업들 즉 값을 이용해서 외부 커넥션등을 연결하는 작업은 초기화 작업으로 이뤄주는게 책임에 맞게 분할되어 유지보수관점에서 좋다.

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원

인터페이스(InitializingBean, DisposableBean)

  • afterPropertiesSet() , destroy()등 메소드에 오버라이드를 통하여 동작을 구현하면된다.
  • 단점 : 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존
  • 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법 , 지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다.

설정 정보에 초기화 메서드, 종료 메서드 지정

  • 설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를
    지정할 수 있다.
  • 생성과 종료 메서드 추론 (@Bean의 destroyMethod 는 기본값이 (inferred) (추론)으로 등록되어 있다.)

@PostConstruct, @PreDestroy 애노테이션 지원

  • 주로 쓰는 최신 스프링에서 가장 권장하는 방법이다.
  • 패키지를 잘 보면 javax.annotation.PostConstruct으로 스프링에 종속적인 기술이 아니라 JSR-250
    라는 자바 표준이다. 스프링이 아닌 다른 컨테이너에서도 동작
  • 유일한 단점 : 외부 라이브러리에는 적용하지 못한다는 것 외부 라이브러리를 초기화, 종료 해야 하면
    @Bean에서 설정하는 기능 사용하자

빈 스코프

빈 스코프 : 빈이 존재할 수 있는 범위

스프링은 다음과 같은 다양한 스코프를 지원

프로토 타입 스프링 빈

싱글톤 빈과의 차이점

싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만, 프로토타입 스코프의 빈은 스프링
컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.

프로토 타입 빈

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

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

프르토 타입 빈과 싱글톤 빈 함께 사용 시(싱글톤 빈 내부에 프로토타입 빈을 사용할 때)

  • 문제점 : 싱글톤 빈이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다 따라서 싱글톤 빈 처럼 계속 같은 주소를 가지고 요청에 응한다. 스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다 즉, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제

  • 스프링에 의존하는 해결법 : 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
    (의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다. )
    싱글톤 빈이 프로토 타입을 부를 때마다 컨테이너에 요청하면 싱글톤 빈이 컨테이너에 종속적인 코드가 되어버려 단위 테스트가 어려워진다. 스프링에 존재하는 DL 서비스를 제공클래스 ObjectProvider을 사용하자 getObject()을 호출하면 컨테이너를 통해 해당 빈 찾아 반환한다.

  • 스프링에 의존하지 않는 해결법 : javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다 단, javax.inject:javax.inject:1 라이브러리를 gradle에 추가해줘야 한다.

ObjectProvider , JSR330 Provider , 프로토타입빈 거의 사용하지 않는다. 나중에 깊이 있게 할 때 공부하면 좋을듯.
실무에서 만약(정말 그럴일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야 한다. 다른 기능들도 자바 표준과 스프링이 제공하는 기능이겹칠때가 많이 있다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.

###웹 스코프

  • request 스코프를 위해서 provider로 각 요청에 맞게끔 구현해줬다.
  • 위와 같은 방법이 하나 더 있는데 @Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를
    생성한다. 주입시에 가짜 프록시 객체가 주입된다.
    (가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.)

정리

  • 프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수
    있다. (싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용)
  • 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지
    지연처리 한다는 점이다.
  • 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI
    컨테이너가 가진 큰 강점
profile
일단 흐자

1개의 댓글

comment-user-thumbnail
2022년 12월 18일

월드컵 보느라 잠깐 쉬었다 16강 갔어 ㅠㅠ.. 다시 빡공해야해..

답글 달기