스프링 스터디, #3

박주진·2021년 9월 1일
0

스프링 스터디

목록 보기
3/3

아래 내용은 김영한 님의 스프링 핵심 원리 기본편의 내용에 기반한다.

컨포넌트 스캔

컴포넌트 스캔이 왜 필요하지?

스프링 빈을 등록할때 자바 코드의 @Bean이나 XML의 등을 통해서 설정 정보에 직접 등록을 하는 방식은 규모가 커지면 다음과 같은 문제가 있다.

  • 귀찮다.
  • 누락 문제 발생가능성 있음.
  • 설정정보가 너무 커질 수 있음.

하지만 컴포넌트 스캔 방식으로 하면 의존관계 자동 주입을 위해 @Autowired를 사용해야 한다.

  • 설정정보로 등록
  @Configuration
public class AppConfig {
 
  @Bean
 public MemberService memberService() {
 return new MemberServiceImpl(memberRepository());
 }
 
 @Bean
 public MemberRepository memberRepository() {
 return new MemoryMemberRepository();
 }
 
}
  • 컴포넌트 스캔 방식
    @Component를 붙혀주면 자동 스캔의 대상이 된다.
@Configuration
@ComponentScan
public class AutoAppConfig {
}
  
  
@Component
public class MemberServiceImpl implements MemberService {

  private final MemberRepository memberRepository;
  
  @Autowired
  public MemberServiceImpl(MemberRepository memberRepository) {
  this.memberRepository = memberRepository;
  }
}
  
@Component
public class MemberRepositoryImpl implements MemberRepository {}  

이렇게 설정하면 component scan을 통해 스프링이 @Component가 붙은 모든 클래스를 빈으로 등록한다. 그리고 @autowired가 있으면 스프링은 컨테이너에서 등록된 빈 중에서 타입으로(기본조회 전략) 조회하여 주입한다.

component scan 탐색위치

모든 자바 클래스를 스캔하면 시간이 오래걸릴 수 있기 때문에 필요한 위치에서 사용해야 한다.

  • 기본
    @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치. 보통은 스프링 부트 @SpringBootApplication에 @ComponentScan이 포함되어 있기때 해당 클래스 패키지가 시작위치다.
  • 커스텀 설정
    basePackages를 통해 스캔이 시작될 패키지를 지정가능하다. 또는 basePackageClasses를 통해 지정한 클래스의 패키지를 탐색 시작 위치로 지정 가능하다.
  @ComponentScan(basePackages = "hello.core",}

권장하는 방법

  • component scan은 메인 설정 정보로써 프로젝트를 대표하는 정보임으로 프로젝트 시작 루트 위치에 두는 것이 좋다.

컴포넌트 스캔 대상 에노테이션

  • @Component

  • @Controller (MVC 컨트롤러로 인식)

  • @Service (핵심 비즈니스 로직 위치 인식)

  • @Repository (스프링 데이터 접근 계층으로 인식)

  • @configuration (스프링 설정 정보로 인식)
    사실상 @Component만 인식이 되는것이고 다른 애노테이션은 내부에 @Component를 포함하고 있다.

    참고로 에노테이션은 상속의 개념이 없어 특정 애노테이션이 다른 에노테이션을 포함하고 있다는 것을 인식하는건 스프링의 지원하는 기능이다.

ComponentScan 필터

includeFilters,excludeFilters 옵션을 사용해 자동 등록할 빈을 추가 또는 제외 시킬 수 있다.

  @ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
MyExcludeComponent.class)
)

필터 타입

  • ANNOTATION (기본값이며 애노테이션을 인식해서 동작)
  • ASSIGNABLE (지정한 타임과 자식타입만 인식해서 동작)
  • ASPECTJ (AspectJ 패턴을 사용해 인식해서 동작)
  • REGEX (정규표현식을 사용해 인식해서 동작)
  • CUSTOM (TypeFilter라는 인터페이스를 직접 구현해서 처리)

중복등록과 충돌

  • 자동 등록 and 자동 등록 -> ConflictingBeanDefinitionException 예외 발생

  • 수동 등록 and 자동 등록 -> 수동 등록 우선순위

  • 최근 스프링 부트는 수동 등록 and 자동 등록에도 오류가 나도록 기본값을 변경하였고 프로퍼티 값(spring.main.allow-bean-definition-overriding)으로 기본설정을 변경할 수 있도록 하였다.

    항상 코딩할때 코드가 길어지고 깔금하지 않더라고 명확성을 유지하는게 나은 선택이다. 안그러면 잡기힘든 버그가 될 가능성이 높다.

의존관계 자동 주입

의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야만 한다.

의존관계 자동 주입 방법

  1. 생성자
  @Component
public class OrderServiceImpl implements OrderService {
  
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;

  @Autowired
  public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy
  discountPolicy) {
  this.memberRepository = memberRepository;
  this.discountPolicy = discountPolicy;
  }
}

- 불변,필수 의존관계에 사용, 1번 호출 보장
- 좋은 개발 습관은 제약을 잘 두자 모두 변경에 열어두면 에러/변경 추적이 힘들다. 예) 객체를 불변으로 만들면(여기서 제약은 생성할 때만 내부 값을 세팅할 수 있음) 중간에 어디서 바뀔지 걱정하지 않아도 된다.

  1. 수정자
  @Component
public class OrderServiceImpl implements OrderService {
  
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
  
  @Autowired
  public void setMemberRepository(MemberRepository memberRepository) {
  this.memberRepository = memberRepository;
  }
  @Autowired
  public void setDiscountPolicy(DiscountPolicy discountPolicy) {
  this.discountPolicy = discountPolicy;
  }
}

- 자바빈 프로퍼티 규약(getXxx,setXxx)의 수정자 메서드 방식을 사용
- 선택 변경 가능서이 있는 의존관계에만 사용
- @Autowired(required = false)로 지정해야 주입할 대상이 없어도 작동한다.
- 스프링에서 빈을 모두 생성후 @autowired가 있는 메서드에 의존성을 주입하는 사이클을 따른다. (생성과 의존관계가 나누어진 단계)

  1. 필드
@Component
public class OrderServiceImpl implements OrderService {
  
  @Autowired
  private MemberRepository memberRepository;
  @Autowired
  private DiscountPolicy discountPolicy;
}

- 간결하다.
- 외부에서 변경이 힘들어 테스트 하기 힘들다.
- DI 프레임워크 없이는 아무것도 못한다.
- @Configuration이나 테스트 코드와 같이 실제 어플리케이션과 관계없는 곳에만 사용 권장

  1. 일반 메서드
  @Component
public class OrderServiceImpl implements OrderService {
  
private MemberRepository memberRepository;
private DiscountPolicy discountPolicy;
  
  @Autowired
  public void init(MemberRepository memberRepository, DiscountPolicy
  discountPolicy) {
  this.memberRepository = memberRepository;
  this.discountPolicy = discountPolicy;
  }
}
  

- 수정자 주입과 똑같다고 볼 수 있다.
- 생성자와 수정자 주입으로 다 해결할 수 있기 때문에 잘 사용되지 않는다.

옵션처리

  1. @Autowired(required=false)
Member가 스프링에서 관리되지 않는 빈이면 해당 메서드는 호출이 안됨 하지만 required=true(default)면 exception 터짐
@Autowired(required = false) 
public void setNoBean(Member member) {}
  1. org.springframework.lang.@Nullable
 Member가 스프링에서 관리되지 않는 빈이면 null값으로 들어옴
@Autowired
public void setNoBean(@Nullable Member member) {

}
  1. Optional<>
Member가 스프링에서 관리 여부에 따라 적절한 optional값으로 들어옴

  @Autowired
public void setNoBean(Optional<Member> member)) {
}

생성자 주입을 선택하라!

  • 불변으로 객체를 설계할 수 있다.
  • 객체 사용시 필요한 의존성 누락을 방지 할 수 있다.
  • final keyword를 사용해서 필수 의존성에 대한 컴파일 오류를 생성 시킬수 있다.

롬복과 최신 트렌드

@RequiredArgsConstructor를 사용 (final keyword가 붙은 필드들에 대한 생성자를 자동으로 생성해준다.)

조회 빈2 개 이상일때 자동주입 방법

  • @Autowired 필드 명 매칭 (필드 명 매칭은 먼저 타입 매칭을 시도 하고 그 결과에 여러 빈이 있을 때 추가로 동작)
  • @Qualifier (@Qualifier끼리 매칭 -> 없으면 빈 이름 매칭 -> NoSuchBeanDefinitionException )
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
  
  
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}  
  • @Primary (@Primary가 무조건 우선권을 가진다.)
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}
  
  
@Autowired
  // RateDiscountPolicy가 주입된다.
public OrderServiceImpl(MemberRepository memberRepository,DiscountPolicy discountPolicy ) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}  

@Qualifier가 우선순위가 @Primary보다 높다.

  • List, Map을 활용하면 모두 받을 수 있다. (전략 패턴 사용시 유용)
@Component
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}
  

class DiscountService {  
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
  
  @Autowired
  public OrderServiceImpl(Map<String, DiscountPolicy> policyMap,List<DiscountPolicy> policies ) {
  this.policyMap = policyMap;
  this.policies = policies;
 } 
}

Map에서 key값이 String이고 빈이름으로 자동으로 등록되다면 불편하지 않을까? enum처럼 확실한 값으로 지정할려면 수동 등록으로 하면 될까?

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

  • 자동등록 사용

    • 업무 로직 빈 (controller,service, repository 등) 갯수가 많기 때문에 수동 등록이 번거롭고 빈이 많아지면 설정정보 관리자체가 부담이 되니까.
      - 업무 로직 빈은 문제가 발생해도 어디서 문제가 발생했는지 명확하게 파악하기 쉽기 때문에 자동등록을 적극사용해도 된다.(이게 정확하게 어떤 뜻일까?🙄)
    • 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적 이긴하다.
  • 수동등록 사용
    - 기술 지원 빈 (데이터베이스 연결 ,공통 로그 처리)
    - 기술 지원 빈은 문제가 발생하면 어디서 발생했는지 파악하기 어려움으로 수동 등록으로 명확하게 보여줘야 한다.(이게 정확하게 어떤 뜻일까?🙄)
    - 다형성을 적극 활용할 때
    - List, Map과 같은 타입으로 여러객체를 자동등록 받는다면 어떤 객체들이 들어오는지 map의 key값으로 들어오는 빈의 이름은 무엇인지가 쉽게 파악하기 어렵다. 자동 등록을 하려면 최소 특정 패키지에 묶어두어야 한다.

    빈 생명주기 콜백

    스프링 빈 이벤트 라이플 사이클(싱글톤 기준)

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

  • 생성자 의존성 주입은 빈 생성과 동시에 의존관계를 주입힌다.

  • 객체 생성과 초기화 분리
    외부 커넥션 연결등과 같은 무거운 동작을 수행하는 초기화 작업은 객체 생성과 분리하는게 유지보수 관점에서 좋다.(SRP) 하지만 🙄 그렇다면 항상 생성 후 초기화 메서드를 호출하고 다른 메서드를 사용하도록 디자인하는게 더 좋을까? 특히 스프링을 사용하지 않는 상황이라면 개발자가 직접 초기화 메서드를 호출하는게 번거롭지 않을까?

    빈 생명주기 콜백 사용방법

  • 인터페이스

    • InitializingBean, DisposableBean 인터페이스를 각각 상속받아 afterPropertiesSet(), destroy() 이렇게 두메서드를 오버라이딩 하면된다.
      단점
      - 스프링에 의존적인 인터페이스에 의존한다.
      - 초기화,소멸 메서드 이름이 고정되어 변경이 불가하다.
      - 외부 라이브러리 적용불가
  • 설정 정보에 초기화 메서드, 종료 메서드 지정

    • @Bean(initMethod = "init", destroyMethod = "close") 이렇게 설정정보에 초기화,소멸 메서드 이름을 직접 지정하면 된다.
    • 메서드 이름이 자유롭고 스프링 코드에 의존적이지 않다. 또한 설정정보에 사용하기 때문에 외부 라이브러리에도 사용가능하다.
    • destroyMethod의 경우 지정하지 않으면 스프링에서 자동 추론하여 close , shutdown 라는 이름의 메서드를 자동으로 호출해준다. (사용하지 않으려면 공백을 넣어주면 된다.)
  • @PostConstruct, @PreDestroy 애노테이션

    • @PostConstruct , @PreDestroy 라는 애노테이션을 메서드 위에 달아주면 된다.
    • 편리하다.
    • 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다.
    • 외부 라이브러리에는 적용이 불가하다.

    고로 최대한 @PostConstruct, @PreDestroy 애노테이션을 사용하고 외부 라이브러리 경우에만 @Bean 의 initMethod , destroyMethod를 사용하자

0개의 댓글