개발자의 길

[Spring] 김영한의 스프링 핵심 원리 - 기본편 정리

토아드 2023. 7. 16. 12:50
반응형

* 개인 복습을 위한 게시글입니다.

 

- IoC 컨테이너/ DI 컨테이너

 객체를 생성하고 관리하면서 의존성을 주입하는 역할을 하는 인스턴스를 말한다. 여러 인터페이스가 존재하고 그 인터페이스에 어떤 구현객체를 생성해서 다른 인터페이스에서 사용하게 할지를 정하는 역할을 하는데, 이녀석이 존재함으로써 구현 객체에서 자신이 사용하는 인터페이스에 어떤 구현객체를 선택해야할지를 몰라도 된다.

 기존에는 InterfaceA 를 구현하는 ImplementA가 InterfaceB 를 사용한다면, Init() 같은 함수나 생성자로 자신이 사용하는 구현객체를 선택하는 역할도 가지고 있었다면, 최근에는 DI 컨테이너 DiContainer 가 getAImplement, getBImplement 같은 함수를 제공함으로써 InterfaceA interfaceA = DiContainer.getAImplement()

InterfaceB interfaceB = DiContainer.getBImplement()

와 같은 식으로 구현객체를 선택하는 역할을 DiContainer 가 가지고 있게되는 것이다.

 

- 스프링 컨테이너 : 스프링에서의 DI 컨테이너

 스프링에서는 ApplicationContext 라는 인터페이스를 구현하는 스프링 컨테이너들이 있는데, 어노테이션을 이용해서 스프링 빈 구성 정보를 읽는 AnnotationConfigApplicationContext 와  XML 파일로 표현된 스프링 빈 구성 정보를 읽는 GenericXmlApplicationContext 가 있다. 여기서 스프링 빈 이라는 녀석은 스프링이 관리하도록 컨테이너에 등록되어 있는 객체들을 말한다.

 AnnotationConfigApplicationContext 같은 경우 @Configuration 이라는 어노테이션이 붙은 클래스를 생성자 인자로 받아서 스프링 빈 구성 정보를 읽어들인다. 아래 코드에서는 AppConfig 라는 클래스가 구성 정보를 표현하는 클래스인데, AnnotationConfigApplicationContext 는 해당 클래스의 @Bean 이 붙은 메서드를 모두 자동으로 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 스프링 컨테이너는 아래와 같이 생성하며, 스프링 컨테이너 객체 생성과 동시에 스프링 빈을 생성해 스프링 컨테이너에 등록한다.

 

 ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
 ApplicationContext applicationContext2 = new GenericXmlApplicationContext("appConfig.xml");

ApplicationContext.getBean("BeanName", BeanClass.class) : 스프링에 등록된 Bean 을 가져오는 함수. ApplicationContext.getBean(BeanClass.class) 와 같이도 사용 가능하다. 타입으로 조회할때 중요한 점은 자식 타입도 같이 조회한다는 것이다. 만약에 해당 클래스를 상속한 자식 클래스가 두개 이상 등록되어 있다면, 부모 타입으로 조회시 중복 오류가 난다.

 

ApplicationContext.getBeanDefinitionNames() : 스프링에 등록된 모든 Bean 이름을 String[] 으로 리턴하는 함수. 

 

ApplicationContext.getBeanDefinition("BeanName") : 스프링 빈 이름을 받아서 BeanDefinition 를 리턴한다. 

 

BeanDefinition : 위의 ApllicationContext 를 구현하는 컨테이너들이 스프링 빈들을 등록할때 생성하는 클래스이다. 하나의 빈을 표현하고 해당 빈의 생명주기와 관련된 행동들을 저장하고 있다. 실제로 우리가 사용할 일은 없다.

 

BeanDefinition.getRole() : 스프링 빈의 역할( 사용자가 등록하였느냐 : ROLE_APPLICATION, 스프링 내부적으로 자동 등록되었느냐 : ROLE_INFRASTRUCTURE ) 을 가져온다.

 

- BeanFactory

  스프링 컨테이너의 최상위 컨테이너다.

- ApplicationContext

  BeanFactory 와 여러 부가기능들을 상속받아서, 빈 관리기능과 부가 기능들을 제공한다. 사실 거의 해당 컨테이너를 사용한다고 보면 된다. 

 

 

- 싱글톤 컨테이너 

스프링 컨테이너는 @Bean 으로 등록되어 있는 메서드를 스프링 빈으로 등록하는데, @Bean 으로 등록하는 함수는 new 를 통해서 하나의 스프링 빈으로 등록될 객체를 새로 생성하게 된다. 그럼 이 스프링 빈들을 호출하는 함수를 사용할때마다 생성하냐? 하면 또 아니다.

@Configuration 으로 등록된 컨테이너는 이녀석 또한 Bean 으로 등록되는데, 이때 바이트코드 수준에서 싱글톤을 구현하는 빈으로써 동작하게 된다. 이로써 해당 컨테이너는 @Bean 으로 등록된 스프링 빈들을 초기에 하나만 생성하서 등록하고, 이후에 함수가 호출될 때마다 관리하고 있던 스프링 빈들을 리턴하는 방식인 싱글톤 패턴으로 관리하게 된다.

 @Configuration 을 등록하지 않으면 @Bean 을 통해 스프링 빈에 등록은 되지만, 의존관계 주입 등에서 스프링 빈을 등록하는 함수를 직접 호출하면 새로운 객체를 생성하게 되어서 문제가 되긴 한다.

 

 

- 컴포넌트 스캔

 위에서 DI 컨테이너에 대해 배웠는데, 실제 서비스에서 하나의 클래스나 파일에 모든 구성 정보를 표현한다는 건 정리도 안되고 매우 방대해 지기 때문에 보기가 어려울 수 있다. 그래서 스프링에서는 자동으로 스프링 빈으로 등록되어야 할 클래스들을 탐색해서 등록해주는 기능이 있는데 이것이 컴포넌트 스캔이다.

 사용방법은 간단한데, 아래와 같이 @Configuration 어노테이션을 붙일 클래스에 @ComponentScan 어노테이션을 붙여주면, 해당 녀석이 자동으로 컴포넌트 스캔을 통해 자동으로 스프링 빈 객체를 생성해 준다.

// 컴포넌트 스캔을 하면 @Configuration 이 붙은 설정 정보도 스프링 빈으로 등록되어 버리기 때문에, 아래의 excludeFilters 를 이용해 
// Configuration 어노테이션이 붙은 클래스는 컴포넌트 스캔을 하지 않도록 설정해준다.

@Configuration
@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoConfig{
}

그럼 이제 컴포넌트 스캔을 통해 어떤 클래스를 스프링 빈으로 등록할 것인지 어떻게 표시를 할까? 아래와 같이 스프링 빈으로 등록할 클래스에 @Component 를 붙여주면 된다.

 

@Component
public class ImportMembershipPaymentService implements MembershiPaymentService {}

 

여기서 이제 스프링 빈으로 등록될 객체들의 의존 관계는 어떻게 표현할까 라는 의문이 생길 수 있는데, 컴포넌트 스캔에서는 @Autowired 를 이용한다.

 

@Component
public class ImportMembershipPaymentServiceImpl implements MembershipService {
	private final ImportService importService;
    private final MembershipRepository membershipRepository;
    
    @Autowired
    public ImportMembershipPaymentServiceImpl(ImportService importService, MembershipRepository membershipRepository) {
    	this.importService = importService;
        this.membershipRepository = membershipRepository;
    }

이렇게 선언만 해준다면 스프링에서 알아서 의존관계를 읽고 객체들을 생성해서 의존성 주입을 시켜준다고 한다!

어떤 녀석을 의존성 주입을 해줄지는 @Autowired 에 선언된 생성자의 인자에 정의된 클래스 타입을 보고 정한다고 한다.

 

컴포넌트 스캔에서의 스프링 빈 이름은?

컴포넌트 스캔에서는 이름을 지정하지 않으면 @Component 가 붙은 클래스 이름의 맨 앞글자로 소문자로 수정하여 사용하는데, 이름을 지정하고 싶다면 아래와 같이 하면 된다.

@ComponentScan("membershipServiceName")
public class ImportMembershipService implements MembershipService {
}

 

탐색할 패키지 지정

컴포넌트 스캔은 모든 파일을 다 탐색하기 때문에 시간이 오래 걸린다. 그래서 스캔할 패키지만 지정해 줄수 있는데 방법은 다음과 같다.

@CompnentScan(
	basePackages = { "app.core" }
)

default 값은 @CompnentScan 이 붙은 클래스의 패키지 위치가 시작 위치가 된다. 김영한 님의 개인적인 취향은 프로젝트 최상단에 설정 정보 클래스를 두는 것이라고 한다.

 

@Component 어노테이션의 종류

@Compnent 어노테이션을 상속하는것과 같은 (스프링이 제공해주는 기능임) 어노테이션이 있는데 그 종류와 기능은 아래와 같다.

@Controller : 스프링 MVC 컨트롤러로 인식한다

@Repository : 데이터 접근 계층으로 인식하며, 해당 클래스가 발생시키는 데이터 계층의 예외를 스프링 계층의 예외로 변환해준다

@Configuration : 위에서 설명했던 설정 정보 클래스를 의미한다. 스프링 빈이 싱글톤으로 유지하도록 추가 처리를 해준다..

@Service : 별도의 처리를 하지 않는다.

 

의존관계 자동 주입

 의존관계 자동주입은 생성자 주입, 수정자 주입, 필드 주입, 일반 메서드 주입 등이 있다. 강의 내용의 결론부터 말하자면 생성자 주입을 쓰는것이 가장 안전하다는 것이다. 수정자 주입 같은경우 setter 메서드를 public 으로 열어둬야 하기에 실수의 가능성과 변경의 가능성이 존재하고, 필드 주입같은 경우는 변수에 의존관계를 수정 불가능하게 박아 버리기에 테스트가 불가능해 지는 문제가 있다. 일반 메서드 주입은 init 같은 함수를 생성하여 하는 방법인데 잘 안쓰인다고 한다. 

 생성자 주입을 사용하면 멤버변수에 final 키워드를 쓸 수 있는데 이는 실수가 났을 때 컴파일 에러 수준에서 디버깅을 할 수 있기 때문에 매우 편리하다. 

 

옵션 처리

의존관계 자동 주입시 주입할 스프링 빈이 없어도 동작해야 할 때가 있는데, 이는 스프링 빈에서 지원하는 옵션 기능을 통해 처리가 가능하다

@Autowired(required = false)
public void setNoBean1(Member member){
	System.out.println(member)
	// 주입할 빈이 없으면 호출되지 않음
}

@Autowired
public void setNoBean2(@Nullable Member member){
	System.out.println(member)
    // 주입할 빈이 없으면 null 을 주입함
}

@Autowired
public void setNoBean3(Optional<Member> member){
	System.out.println(member)
	// 주입할 빈이 없으면 Optional.empty 를 주입함
}

 

롬복

생성자에 의존성 자동 주입 기능을 사용하지 않고 생성자 선언도 없이 의존성 자동 주입이 가능한데, 롬복이라는 라이브러리를 사용하는 것이다. 사용법은 다음과 같다.

@Component
@RequiredArgsConstructor
public class MemberServiceImpl implements MemberInterface {
	private final MemberRepository memberRepository
    private final MemberPolicy memberPolicy
}

/// 위와 같이 어노테이션을 사용해주면 롬복이 자동으로 의존성 자동주입을 해주는 생성자를 만들어준다고 한다.

 

조회 타입이 2개 이상이면?

 

Autowired 는 기본적으로 조회할 빈의 타입으로 조회하기 때문에 하위 타입이 2개인 경우에는 에러가 생길 수 있는데, 이런 경우 주입할 하위 타입을 별도로 설정해 줄 수도 있고, List 나 Map 등을 이용해서 여러 타입을 주입하는 방식도 가능하다. 

 

@Autowired 의 기본 빈 조회 방식은 타입 조회 -> 필드 명이 빈 이름이랑 같은지 조회 -> 파라미터 명이 빈 이름이랑 같은지 조회

 

@Qualifier 사용

@Component
@Qualifier("mainDiscountPolicy")
public class CouponDiscountPolicy implements DiscountPolicy {}

@Component
@Qualifier("promotionDiscountPolicy")
public class PromotionDiscountPolicy implements DiscountPolicy {}


@Autowired
public MembershipPaymentImpl(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
	this.discountPolicy = discountPolicy; // 이제 CouponDiscountPolicy 를 우선적으로 넣어준다. 
}

 

@Primary 사용

@Component
@Primary
public class CouponDiscountPolicy implements DiscountPolicy {}

@Component
public class PromotionDiscountPolicy implements DiscountPolicy {}


@Autowired
public MembershipPaymentImpl(DiscountPolicy discountPolicy) {
	this.discountPolicy = discountPolicy; // 이제 CouponDiscountPolicy 를 우선적으로 넣어준다. 
}

 

@Primary 보다는 @Qualifier 가 우선권이 더 높다고 한다. (더 좁고 명확한 범위가 우선권을 가진다!)

 

스프링 빈에 다형성을 적용

 

특정 타입의 스프링 빈이 모두 다 필요한 경우도 있다. 아래 코드를 보자.

 

public class DiscountService {
	private final Map<String, DiscountPolicy> policyMap;
    private final List<DiscountPolicy> policies;
    
    
    // Map 주입 : 키로 스프링 빈의 이름을 넣어주고, DiscountPolicy 타입에 해당하는 모든 빈을 넣어줌
    // List 주입 : DiscountPolicy 타입에 해당하는 모든 빈을 넣어준다.
    public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
    	this.policyMap = policyMap;
        this.policies = policies;
    }
    
    public int discount(Member member, int price, String discountCode) {
    	DiscountPolicy discountPolicy = this.policyMap.get(discountCode);
        return discountPolicy.discount(member, price);
    }
}

 

다형성을 사용했을 경우의 주의점

 위와 같이 다형성을 적극 활용한 경우에 자동 빈 등록이 된다면 어떤 빈이 등록될지 알기 어렵기 때문에 다형성이 적용된 빈의 경우에는 하나의 패키지에 정리해 주는편이 좋다. 

 

수동 빈 등록으로 다형성을 관리하는 경우

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

자동으로 다형성이 적용된 빈을 등록하고 싶다면 하나의 패키지에 몰아서 구현 빈을 선언하는 방식으로 관리하자!

 

 

수동 빈 등록과 자동 빈 등록

@Configuration 과 @Bean 을 사용해서 수동으로 빈을 지정해주는 방법과, @Component 를 써서 자동 빈 등록을 하는 방법을 위에서 소개하였는데, 실무에서 어떤 것을 사용하는게 좋을까?

 강의에서는 비즈니스 로직등이 들어가는 매우 개수가 많은 '업무 지원 빈' 같은 경우에는 자동 빈 등록으로 관리하고, 코어 기술 등 여러 범위에 걸쳐서 영향을 주는 '기술 지원 빈' 같은 경우에는 수동 빈 등록을 이용해서 명확하게 관리하는게 좋다고 한다. 

 

 

- 빈 생명주기 콜백

스프링이 시작되고 종료될 때 빈 생성자 및 소멸자 호출 이외에 빈에서 네트워크 커넥션 등을 초기화/종료 하는 작업 등이 필요할 수 있는데, 스프링에서는 이를 3가지 방법으로 지원한다.

 

3가지 방법을 소개하기 전에 스프링의 초기화 및 종료 시퀀스를 알아보자.

 

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

 

위 시퀀스에서 우리가 지금 알아보는 단계가 '초기화 콜백' 과 '종료 콜백' 이다.

 

1. InitializingBean, DisposableBean 인터페이스 구현

InitializingBean 인터페이스의 public void afterPropertiesSet 메서드를 구현하거나, DisposableBean 인터페이스의 public void destroy() 메서드를 구현하여 해당 빈을 구현하는 빈이 시작되고 종료될때 초기화/종료 작업을 할수 있도록 한다. 하지만 이 방법은 스프링의 인터페이스를 의존하고, 외부 라이브러리에 사용할 수 없다는 점 때문에 잘 사용되지 않는다고 한다.

2. 스프링 빈 설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화 콜백과 종료 콜백을 지정할 수 있다.

@Configuration
public class NetworkConfig{
	// 아래와 같이 어노테이션에 선언해 주면 해당 스프링 빈의 의존성 주입을 한 뒤에 해당 빈의 init 메서드를 호출하고. 
    // 스프링 종료 전에 해당 빈의 close 메서드를 호출한다.
	@Bean(initMethod="init", destroyMethod="close")
    public NetworkClient networkClient() {
    	NetworkClient networkClient = new NetworkClient();
        networkClient.setUrl("http://hello-spring.com");
        return networkClient;
   			
    }
}

 * 여기서 destroyMethod 의 특별한 기능이 있는데, 설정값을 정해주지 않으면 기본적으로 close 또는 shutdown 이라는 메서드를 자동으로 종료 콜백으로 지정해준다. 따라서 스프링 빈으로 등록되면 해당 빈의 close 또는 shutdown을 자동으로 호출한다.

 

 

3. @PostConstruct, @PreDestroy 에노테이션 적용

초기화 콜백, 종료 콜백으로 등록할 스프링 빈의 메서드에 위 에노테이션을 적용시켜주는 방법인데, 위 에노테이션은 자바 표준 에노테이션이기 때문에 빈으로 설정할 객체가 스프링 프레임워크에 의존하지 않게 된다. 외부 라이브러리에 초기화콜백/종료 콜백을 지정할 게 아니라면 이 방법을 쓰는것이 좋다!

 

 

-빈 스코프

현재까지는 모두 스프링의 시작과 종료에 맞춰서 한번만 생성되는 싱글톤 빈들을 사용했는데, 스프링에서는 다양한 스코프를 지원하고 있다. 그 종류로는 싱글톤, 프로토타입, 웹 관련 스코프 등이 있다.

 

싱글톤

싱글톤은 여태까지 우리가 썼던 것과 같이 가장 넓은 범위의 스코프이다

@Scope("singleton")
public class SingletonBean{

}

 

프로토타입

프로토타입 스코프는 빈을 요청할 때마다 생성되는 스코프로써, 스프링이 생성과 의존관계 주입까지만 관여하고 그 외에는 관여하지 않는다. 그러므로 위에서 배운 @PreDestroy 등의 에노테이션을 적용시킨 메서드는 호출되지 않는다.

@Scope("prototype")
public class PrototypeBean{

}

 

싱글톤 빈에서 프로토타입 빈을 의존성 주입으로 사용하고 있을때는 싱글톤 빈이 스프링에 등록되면서 프로토타입 빈을 한번 의존성 주입 당하는데, 이때 프로토타입 빈을 한번 가져올때 새로 생성해서 가져오는데 이 이후로는 이미 주입된 프로토타입 빈을 계속 사용하게 된다.

우리는 프로토타입 빈을 사용 할 때마다 새로 생성하는게 목적일 텐데, 이렇게 되면 하나의 프로토타입 빈을 계속해서 사용하게 되는 의도하던 바와 다른 일이 발생하게 된다. 이는 3가지 방법으로 해결할 수 있는데 그 방법은 아래와 같다.

 

1. 싱글톤 빈에서 필요할 때마다 스프링 컨테이너에 요청하도록 하는 방법

스프링 컨테이너에 의존적이게 되고 테스트가 어려워 지므로 추천하지 않음. 게다가 스프링 컨테이너를 싱글톤 빈에서 가지고 있어야 해서 좀 과다함.

 

2. ObjectFactory, ObjectProvider 사용

사실 1과 비슷한 방법이긴 한데, 스프링 컨테이너의 메서드를 호출하는 것이 아니고 스프링 컨테이너에 요청하는 작업을 하는 클래스로 래핑 해주는 작업이다 

@Autowired
ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic(){
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    return prototypeBean.doSomething();
}

위의 getObject 내에서 스프링 컨테이너를 통해 프로토타입 빈을 가져오는 작업을 하게 된다.

 

3. JSR-330 Provider 사용

JSR-330 자바 표준 라이브러리인데 라이브러리 별도 설치가 필요하다. 이용방법은 ObjectProvider 와 거의 같다.

@Autowired
Provider<PrototypeBean> provider;

public int logic(){
	PrototypeBean prototypeBean = provider.get();
    return prototypeBean.doSomething();
}

프로토타입빈을 스프링 외의 코드에서도 사용해야 된다면 JSR-330 Provider 를 사용하고, 그게 아니라면 여러 편의기능을 같이 제공해주고 라이브러리 추가도 필요없는 ObjectProvider 를 추천한다.

 

 

웹 스코프

웹 환경에서만 사용되는 스코프로써, 스프링이 해당 스코프의 종료 시점까지 관리하게 되는데 웹 서비스에 최적화된 스코프라고 할 수 있다. 웹 스코프는 아래와 같은 4가지가 있다.

 

request : HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프

session : HTTP session 하나와 동일한 생명주기를 가지는 스코프

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

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

 

웹 스코프와 Provider

웹 스코프 같은경우 웹 요청이 들어와야지 생성될 수 있는 특징이 있기 때문에, 스프링이 시작될때 의존성 주입을 하려고하면 에러가 난다. 예를 들어 싱글톤 빈이 웹 스코프의 빈을 의존성 주입을 받으려고 한다면 스프링 시작시점에는 HTTP 요청이 온 request 스코프가 아니기 때문에 해당 빈을 생성할 수 없다는 에러가 발생한다. 이때 위의 Provider 들을 의존성 주입을 받도록 한다면 웹 request 가 들어온 시점에 스프링 컨테이너에 웹 스코프 빈을 요청할 수 있으니 참고하자.

@Component
@RequiredArgsConstructor
public class WebService {
    ObjectProvider<WebScopeBean> webScopdeBeanProvider;
    
    public void logic(){
	    WebScopeBean webScopeBean = webScopdeBeanProvider.getObject();
	    return webScopeBean.doSomething();
    }
}

 

웹 스코프와 프록시

 위에서 Provider 를 활용한 웹 스코프의 의존성 주입 문제를 소개했는데, 스프링에서 제공하는 proxyMode 라는걸 활용하면 더 간단하게 해결이 가능하다

@Component
@Scope(value= "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebScopeBean{

}

// 이제 위 Bean 을 스프링 시작 시점에 의존성 주입 받을 수 있고, 해당 Bean 사용하는 시점에 진짜 Bean 을 찾아서 로직을 수행하게 해준다.

 

반응형