지나공 : 지식을 나누는 공간

Spring 핵심 원리 기본편 2 본문

Tech/Spring

Spring 핵심 원리 기본편 2

해리리_ 2023. 5. 7. 22:53

개요


지난 시간에는 Spring이 객체지향원칙을 지켜주는 방법의 도입부. 즉 그냥 단순하게 개발하면 어디서 SOLID 원칙이 여겨지는지 알아보고 스프링 컨테이너를 사용하면 그걸 어떻게 해결해주는지를 알아봤다. 그중에서도 ApplicationContext(스프링컨테이너)를 활용해서 싱글톤 빈을 관리하는 것까지 봤는데, 이번에는 그 활용 자체를 좀 더 편하게 하도록 스프링이 뭘 제공해주는지 보고 그 외 여러가지 스프링 기능들도 볼 예정이다. 영한님 강의 중 핵심원리 기본편의 마무리 글이다. 얼른 끝내고 MVC로 가자!

 

아래 포스팅에 이어지는 내용이다.

https://eocoding.tistory.com/120

 

Spring 핵심 원리 기본편

Spring 핵심 원리를 오고가면서 쓰윽 들었다. 그래서 그간의 테스트 코드들을 살펴보면서 아주 퀵하게 정리해보려 한다. 이미 너무나 유명한 영한님 강의!ㅎㅎ 좀 오글거리는 소제목들이 있지만

eocoding.tistory.com

 

ComponetScan, 빈 자동 등록


이전에는 AppConfig라는 설정정보를 만든 뒤 @Bean 애노테이션을 달아서 직접 빈을 등록했는데, 빈을 자동으로 스캔할 수 있도록 컴포넌트 스캔을 사용할 수 있다. 강의에서는 컴포넌트 스캔을 활용할 설정정보를 AutoAppConfig.class로 만든다.

 

@ComponentScan은 직접 빈 등록을 코드로 하지 않아도 @Component가 붙은 클래스들을 스캔해서 빈으로 등록한다. 근데 @Configuration 안에는 @Component 어노테이션이 포함되어 있어서 컴포넌트 스캔의 대상이 되고, 강의에서는 기존에 만든 설정정보 클래스 코드들을 유지할 것이기 때문에 얘네들이 같이 컴포넌트 스캔의 대상이 되지 않게 excludeFilters를 설정한다.

 

@Component 와 @Bean


@Component와 @Bean은 둘다 빈으로 등록하는 애노테이션인데 둘의 차이는 코드에서 볼 수 있다.

빈과 컴포넌트

@Bean은 메소드랑 애노테이션 단위에 붙일 수 있다. @Component는 Type이라고 하니까 클래스나 인터페이스나 enum 같은 데에 붙일 수 있다. 사용처를 따지자면 @Bean은 외부 라이브러리나 개발자가 본인이 직접 정의하지 않은 객체를 유연하게 어딘가 넘기기 위해 반환값으로 해서 빈으로 등록하는 경우에 사용한다. @Component는 개발자가 직접 정의한 클래스 자체를 빈으로 등록할 때 사용한다.

 

수동빈과 자동빈의 우선순위


다시 돌아와서 ComponentScan을 보자. 컴포넌트 스캔을 통해 자동 빈 등록을 할 수도 있지만 지난번에 한 것처럼 수동으로 빈을 등록할 수도 있다. 이걸 둘 다 설정해놓으면 누가 우선권을 가질까? 스프링부트 설정을 어떻게 하느냐에 따라 여기서 에러를 내줄 수도 있고 수동빈이 자동빈을 오버라이딩 해버릴 수도 있다.

Componet로 자동빈등록도 하고 직접 수동빈에도 동일한 빈을 등록해보자.
친절하게 액션까지 설명해주는 스프링.ㅎㅎ

빈을 하나만 남게 하든가, 아님 spring.main.allow-bean-definition-overriding=true로 설정해서 수동빈이 우선권을 가지도록 하든가(수동빈이 자동빈을 오버라이딩하게) 둘 중 하나를 결정하라고 알려준다.

 

의존관계 자동 주입 4가지


  • 생성자 주입
  • 수정자 주입, setter
  • 필드 주입
  • 일반 메서드 주입

생성자 주입은 생성자 호출 시점에 딱 1번만 호출되는 것이 보장되는 반면 다른 것들은 그걸 보장할 수 없다. 그래서 생성자 주입을 권한다. 생성자 주입은 생성자 위에 Autowired를 붙이면 된다. 수정자 주입도 setter 함수 만들어서 그 위에 Autowried 어노테이션 붙이면 된다. 필드 주입은 그냥 객체 내에 선언된 필드들 위에 Autowired 붙이는 것이다. 텍스트로만 말해서 좀 그렇긴 한데 ㅋㅋㅋ 굳이 사진 넣을 정도로 중요한 내용은 아니라서 스킵한다. 생성자 주입만 아래 보면 되겠다.

 

참고로 Autowired는 스프링에서 제공되는 스프링 라이프사이클을 타는 어노테이션이기 때문에 순수한 JUnit 테스트에서는 동작하지 않고 @SpringBootTest와 같은 어노테이션을 통해서 스프링 컨테이너를 테스트에 통합해야 한다. 그리고 당연히 빈에서만 동작한다.

 

여기서 중요한 내용! 해당 클래스가 스프링 빈이고 생성자가 1개 뿐이라면 @Autowired를 생략해도 의존관계 자동 주입이 이뤄진다.

 

밑에서 언급할 빈 생명주기와 관련있는데 생성자 주입은 특이한 점이 있다. 본래 스프링 빈의 이벤트 사이클은 아래의 순서대로 이루어진다.

  1. 스프링 컨테이너 생성
  2. 스프링 빈 생성
  3. 의존관계 주입
  4. 초기화 콜백 : 빈이 생성되고 나서 빈의 의존관계 주입까지 완료된 이후에 호출되는 함수
  5. 빈 사용
  6. 소멸전 콜백 : 빈이 소멸되기 직전에 호출되는 함수
  7. 스프링 종료

그런데 생성자 주입에서는 객체(빈)의 생성과 의존관계 주입이 동시에 일어난다. 수정자 주입이나 필드 주입은 객체의 생성 단계가 끝나고 의존관계 주입이 되는 식으로, 라이프사이클이 나뉘어져 있다.

 

그리고 생성자 주입에서는 final을 잘 활용할 수 있다. 생성자에 누락된 필드가 있을 때 컴파일 시점에 오류를 내 준다.

 

동일한 타입의 빈이 여러개일 때 발생하는 문제


스프링 컨테이너에서 빈을 조회할 때는 타입을 가지고 식별한다. 우리 예제에는 DiscountPolicy 라는 interface가 있고 이를 구현한 FixDiscountPolicy (고정된 값을 할인하는 정책)과 RateDiscountPolicy(비율로 할인하는 정책)가 있는데, DiscountPolicy 타입으로 선언한 필드에 대해 의존관계 주입을 하게 하면 스프링에서는 ac.getBean(DiscountPolicy.class) 와 거의 유사하게 코드가 동작한다.

 

생성자 주입의 특성과 lombok을 활용한 코드.

위 코드는 생성자가 딱 1개 뿐이라서 Autowired 어노테이션을 생략하고, 그 다음 그 생성자마저 lombok의 RequiredArgsConstructor로 숨긴 상태다. 위 코드는 생성자를 통해 의존관계 자동 주입이 이뤄지고, 여기서 DiscountPolicy에 대한 의존관계 주입을 하기 위해 스프링 컨테이너에서 빈을 찾을 것이다. 타입으로 찾을 것이다. ac.getBean(DiscountPolicy.class)와 유사하게 동작할 것이다.

 

두 DiscountPolicy의 구현체를 모두 빈으로 등록했다.

그런데 여기서 만약 위처럼 FixDiscount, RateDiscount 모두를 빈으로 선언하게 되면 어떻게 될까? 얘네 둘다 상위 타입이 DiscountPolicy라서, 상위 타입으로 빈을 조회할 때 두 빈이 모두 인식된다. 빈을 타입으로 조회하기 때문에 다수의 빈이 같은 상위 타입을 가지면 어떤 빈이 실제 원하는 빈인지 알 수가 없다. 진짠지 테스트를 해보자.

 

AutoAppConfig를 스프링컨테이너의 설정정보로 등록하여 컴포넌트 스캔 기능을 활성화 시키는 테스트 코드가 필요하다. 

AutoAppConfig를 설정 정보로 등록한 스프링 컨테이너 생성

위 테스트를 실행해보면 아래와 같이 빈이 2개라는 에러가 발생한다. 역시 친절하게 무슨 클래스인지도 알려준다.

 

여러 개의 빈 중에서 특정 빈으로 의존관계가 주입되게 하는 방법


이때 가장 우선순위가 높은 스프링 빈이 뭔지 설정해줘야 하는데 그 방법에는 세 가지가 있다.

  1. @Autowired + 필드명 조합
  2. @Qualifier
  3. @Primary

1 방법은 아래와 같이 생성자 내 파라미터명만 바꾸면 된다. OrderServiceImpl이 가진 필드명은 그대로 두고, 생성자가 입력받는 그 파라미터의 이름으로 빈을 조회하니까 거기에 구체적인 빈 이름을 세팅해줄 수 있다.

 

궁금해서 롬복 유지하고 필드명 바꿔봤는데 그것도 된다. 왜 되는지 궁금해서 바이트코드로 이뤄진 클래스파일도 열어봤다. 어차피 인텔리제이가 사람이 읽을 수 있게 바이트 코드를 예쁘게 보여주니까 우리는 해석할 수 있다!

RequiredArgsConstructor 유지하고 필드명만 바꿔도 동작됨.
이건 바이트 코드 열어본 것.

롬복이 생성자를 만들 때 필드명을 참고해서 만드니까 생성자 파라미터명도 내가 세팅한 필드명을 따라갔다. 혹시나 @Autowired도 바이트코드에 생겨있으려나 했는데 그건 아니었다. 생성자가 하나라 생략해도 의존관계 주입이 되니까 아무튼 주입은 됐다.

 

2번 Qualifier는 빈을 등록할 때랑 해당 빈에 대해 의존관계를 주입할 때 두 코드 모두에 어노테이션을 작성해야 한다.

Qualifier 사용

 

3번 Primary는 빈을 등록할 때에 그냥 @Primary를 붙여주면 알아서 동작한다. 의존관계 주입하는 부분에서까지 뭔가 어노테이션을 쓰는 작업은 안해도 된다. Qualifier는 해당 빈에 대해 의존관계를 주입하는 모든 코드에 어노테이션을 써야하지만 primary는 그렇지 않다. 스프링은 항상 수동으로 뭔가 힘들게 작성한 코드에 대해서 우선권을 더 주나보다. 둘다 쓰면 Primary보다 조금 더 귀찮은 Qualifier가 우선권을 가진다고 한다.

 

이제 빈 생명주기, 콜백 그리고 스코프가 남았다.

728x90
Comments