스프링 스터디, #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)으로 기본설정을 변경할 수 있도록 하였다.
항상 코딩할때 코드가 길어지고 깔금하지 않더라고 명확성을 유지하는게 나은 선택이다. 안그러면 잡기힘든 버그가 될 가능성이 높다.
의존관계 자동 주입
의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야만 한다.
의존관계 자동 주입 방법
- 생성자
@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번 호출 보장
- 좋은 개발 습관은 제약을 잘 두자 모두 변경에 열어두면 에러/변경 추적이 힘들다. 예) 객체를 불변으로 만들면(여기서 제약은 생성할 때만 내부 값을 세팅할 수 있음) 중간에 어디서 바뀔지 걱정하지 않아도 된다.
- 수정자
@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가 있는 메서드에 의존성을 주입하는 사이클을 따른다. (생성과 의존관계가 나누어진 단계)
- 필드
@Component
public class OrderServiceImpl implements OrderService {
@Autowired
private MemberRepository memberRepository;
@Autowired
private DiscountPolicy discountPolicy;
}
- 간결하다.
- 외부에서 변경이 힘들어 테스트 하기 힘들다.
- DI 프레임워크 없이는 아무것도 못한다.
- @Configuration이나 테스트 코드와 같이 실제 어플리케이션과 관계없는 곳에만 사용 권장
- 일반 메서드
@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;
}
}
- 수정자 주입과 똑같다고 볼 수 있다.
- 생성자와 수정자 주입으로 다 해결할 수 있기 때문에 잘 사용되지 않는다.
옵션처리
- @Autowired(required=false)
Member가 스프링에서 관리되지 않는 빈이면 해당 메서드는 호출이 안됨 하지만 required=true(default)면 exception 터짐
@Autowired(required = false)
public void setNoBean(Member member) {}
- org.springframework.lang.@Nullable
Member가 스프링에서 관리되지 않는 빈이면 null값으로 들어옴
@Autowired
public void setNoBean(@Nullable Member member) {
}
- 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 등) 갯수가 많기 때문에 수동 등록이 번거롭고 빈이 많아지면 설정정보 관리자체가 부담이 되니까.
- 업무 로직 빈은 문제가 발생해도 어디서 문제가 발생했는지 명확하게 파악하기 쉽기 때문에 자동등록을 적극사용해도 된다.(이게 정확하게 어떤 뜻일까?🙄) - 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적 이긴하다.
- 업무 로직 빈 (controller,service, repository 등) 갯수가 많기 때문에 수동 등록이 번거롭고 빈이 많아지면 설정정보 관리자체가 부담이 되니까.
-
수동등록 사용
- 기술 지원 빈 (데이터베이스 연결 ,공통 로그 처리)
- 기술 지원 빈은 문제가 발생하면 어디서 발생했는지 파악하기 어려움으로 수동 등록으로 명확하게 보여줘야 한다.(이게 정확하게 어떤 뜻일까?🙄)
- 다형성을 적극 활용할 때
- List, Map과 같은 타입으로 여러객체를 자동등록 받는다면 어떤 객체들이 들어오는지 map의 key값으로 들어오는 빈의 이름은 무엇인지가 쉽게 파악하기 어렵다. 자동 등록을 하려면 최소 특정 패키지에 묶어두어야 한다.빈 생명주기 콜백
스프링 빈 이벤트 라이플 사이클(싱글톤 기준)
-
스프링 컨테이서 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료
-
생성자 의존성 주입은 빈 생성과 동시에 의존관계를 주입힌다.
-
객체 생성과 초기화 분리
외부 커넥션 연결등과 같은 무거운 동작을 수행하는 초기화 작업은 객체 생성과 분리하는게 유지보수 관점에서 좋다.(SRP) 하지만 🙄 그렇다면 항상 생성 후 초기화 메서드를 호출하고 다른 메서드를 사용하도록 디자인하는게 더 좋을까? 특히 스프링을 사용하지 않는 상황이라면 개발자가 직접 초기화 메서드를 호출하는게 번거롭지 않을까?빈 생명주기 콜백 사용방법
-
인터페이스
- InitializingBean, DisposableBean 인터페이스를 각각 상속받아 afterPropertiesSet(), destroy() 이렇게 두메서드를 오버라이딩 하면된다.
단점
- 스프링에 의존적인 인터페이스에 의존한다.
- 초기화,소멸 메서드 이름이 고정되어 변경이 불가하다.
- 외부 라이브러리 적용불가
- InitializingBean, DisposableBean 인터페이스를 각각 상속받아 afterPropertiesSet(), destroy() 이렇게 두메서드를 오버라이딩 하면된다.
-
설정 정보에 초기화 메서드, 종료 메서드 지정
- @Bean(initMethod = "init", destroyMethod = "close") 이렇게 설정정보에 초기화,소멸 메서드 이름을 직접 지정하면 된다.
- 메서드 이름이 자유롭고 스프링 코드에 의존적이지 않다. 또한 설정정보에 사용하기 때문에 외부 라이브러리에도 사용가능하다.
- destroyMethod의 경우 지정하지 않으면 스프링에서 자동 추론하여 close , shutdown 라는 이름의 메서드를 자동으로 호출해준다. (사용하지 않으려면 공백을 넣어주면 된다.)
-
@PostConstruct, @PreDestroy 애노테이션
- @PostConstruct , @PreDestroy 라는 애노테이션을 메서드 위에 달아주면 된다.
- 편리하다.
- 스프링에 종속적인 기술이 아니라 JSR-250라는 자바 표준이다.
- 외부 라이브러리에는 적용이 불가하다.
고로 최대한 @PostConstruct, @PreDestroy 애노테이션을 사용하고 외부 라이브러리 경우에만 @Bean 의 initMethod , destroyMethod를 사용하자
Author And Source
이 문제에 관하여(스프링 스터디, #3), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@wnwl1216/스프링-스터디-3저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)