의존성 자동 주입, 컴포넌트 스캔

예지성준·2024년 7월 5일

스프링프레임워크

목록 보기
2/14
post-thumbnail

스프링 DI 설정 및 사용

스프링을 이용한 객체 조립과 사용

  1. DI 방식1 : 생성자 방식

  2. DI 방식2 : 세터 메서드 방식

  3. @Configuration

  4. @Bean

🔼 이전글에 있습니다! 😋

5. 두 개 이상의 설정 파일 사용하기

1) 생성자 매개변수

AnnotationConfigApplicationContext(Class<?>... componentClasses)

관리 객체가 많을때는 설정 클래스를 분리하는 것이 좋다.
설정클래스는 현재 통합된 상태

AnnotationConfigApplicationContext(String... basePackages)

매개변수에 설정클래스 패키지로 지정도 가능하다.

2) @Import

  • 설정 클래스에서 다른 설정 클래스를 포함시킬때

🔼 설정 클래스가 없다면 사용 불가

Improt애노테이션 사용

#AppCtx설정 클래스
//설정 클래스 - 스프링 컨테이너가 관리할 객체를 정의하고 설정하는 역할
@Import(value = {AppCtx2.class})
@Configuration
public class AppCtx {

    @Bean //스프링 컨테이너가 관리 해야할 객체임을 알려주는 역할(Bean)
    public Greeter greeter(){
        return new Greeter();
    }
}

value값을 명시하지 않아도 되고 설정이 1개일때는 중괄호도 생략 가능하다.
@Import(AppCtx2.class)

이후..
동작 성공 ⭕⭕

의존 자동 주입🎈

스프링의 역할은 객체 관리💨📖

1.@Autowired

  • 의존성을 주입해야되는 객체임을 알려주는 애노테이션

👩‍💻@Autowired를 설정해준 객체가 객체 컨테이너에 있다면 해당 객체를 꺼내서 의존성을 자동 주입해준다.

1) 멤버 변수 위에 정의

...
    @Autowired
    private JoinValidator validator;
    @Autowired
    private MemberDao memberDao;
 ...

2) setter메서드 위에 정의

설정 클래스 내(AppCtx2)

자동 주입 전, 위 처럼 memberDao 객체를 주입해주지 않으면 nullPointException이 발생한다.

setter메서드 위에 정의했을 경우 ! memberDao 의존성 자동 주입

public class JoinValidator implements Validator<RequestJoin> {
    private MemberDao memberDao;

    @Autowired
    public void setMemberDao(MemberDao memberDao) {
        this.memberDao = memberDao;
    }


    @Override
    public void check(RequestJoin form) {

    }
}

3) Optional 정의된 멤버 변수, 메서드의 매개변수에 있어도 주입

  • Optional 클래스: 값이 존재할 수도 있고 존재하지 않을 수도 있는 객체를 나타내기 위한 컨테이너이다.
    • NullPointException을 피하기 위해 사용됨
    • null을 직접 사용하지 않고, 값이 존재하는지 여부를 명시적으로 표현할 수 있다.

      isPresent(): 값이 존재하면 true를 반환하고, 존재하지 않으면 false를 반환합니다.

      ifPresent(Consumer): 값이 존재하면 주어진 동작을 수행합니다.

      get(): 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 NoSuchElementException을 던집니다.

      orElse(T other): 값이 존재하면 그 값을 반환하고, 그렇지 않으면 other를 반환합니다.

      orElseGet(Supplier): 값이 존재하면 그 값을 반환하고, 그렇지 않으면 Supplier에서 제공하는 값을 반환합니다.

      orElseThrow(Supplier): 값이 존재하면 그 값을 반환하고, 그렇지 않으면 Supplier가 제공하는 예외를 던집니다.

설정 클래스 내(AppCtx2)

public class InfoService {

    private MemberDao memberDao;

    @Autowired
    public void setMemberDao(Optional<MemberDao> opt) {
        this.memberDao = opt.get();
    }

    public void printList(){
        List<Member> members = memberDao.getList();
        members.forEach(System.out::println);
    }
}

참고)👩‍🏫 옛날엔 존재했던...
@Resource
@Inject

4) 자동 스캔 적용(@ComponentScan)시

  • @Autowired 미 적용 방식
  • 생성자의 매개변수로 정의하는 방식 / 기본 생성자가 정의되지 않아야 한다.💥💥

의존성이 현재는 2개짜리인 예시지만 10개일경우 이렇게 10번을 정의해야한다??
ㄴㄴ
롬복으로 편리하게 생성자 매개변수에 원하는것만 넣을 수 있는 애노테이션이 존재한다..
@RequiredArgConstructor로 생성자 매개변수에 추가 될 수 있게 넣어주면 편리하게 쓸 수 있다.

👩‍🏫@RequiredArgConstructor

  • Lombok 라이브러리에서 제공하는 기능으로, 클래스 내의 final 키워드가 붙은 필드나 @NonNull 애노테이션이 붙은 필드들을 생성자의 매개변수로 자동으로 추가해주는 역할을 한다.

-> final 키워드가 붙어 있는 필드는 반드시 초기화가 필요함 즉, 객체가 생성될때 초기값을 설정해야하고 이후에는 값을 변경할 수 없다.

애노테이션을 사용하면 final 키워드가 붙은 필드를 자동으로 생성자의 매개변수로 추가 해주게 된다. (생성자를 직접 만들 필요 x)

@NonNull 애노테이션

  • 해당 필드가 null이 아니어야 함을 명시한다.
  • @RequiredArgsConstructor 애노테이션을 사용하면 생성자의 매개변수로 추가된다.
  • @NonNull 애노테이션이 붙은 필드는 final 필드와 다르게 이후에 값을 변경할 수 있다. 즉, 생성 시점에 null이 아닌 값으로 초기화되지만 이후에 값을 변경할 필요가 있는 경우 사용한다.

2. 일치하는 빈이 없는 경우

설정 클래스 내(AppCtx2)

🔼 컨테이너에서 해당 객체를 찾을 수 없기 때문에 오류 발생💥💥

  • MemberDao가 2개인경우? (Bean에 이름이 동일할 경우)

어떤 Bean인지 직접 알려줘야한다.⬇

3. @Qualifier

  • 빈의 이름을 직접 지정

해당 애노테이션으로 이름을 바꿔줄수도 있다~

4. 빈 이름과 기본 한정자

@Bean: 메서드명

@Qualifier: 변경한 이름

클래스명(자동스캔의 경우) -> 앞 첫 문자는 소문자
ex) class Joinservice -> joinService

날짜 형식 바꾸기..📆

public class InfoService {//회원목록 조회

    private MemberDao memberDao;
    private DateTimeFormatter formatter;

    @Autowired
    @Qualifier("memberDao")
    public void setMemberDao(Optional<MemberDao> opt) {
        this.memberDao = opt.get();
    }

    @Autowired
    public void setFormatter(DateTimeFormatter formatter) {
        this.formatter = formatter;
    }


    public void printList(){
        List<Member> members = memberDao.getList();
        members.forEach(m -> {
            System.out.println(m);
            LocalDateTime regDt = m.getRegDt();
            System.out.println(formatter.format(regDt));
        });
    }
}

5. @Autowired 애노테이션의 필수 여부

@Autowired가 붙어있는 경우 해당하는 객체를 컨테이너에서 찾을 수 없으면 오류 발생

required: default값은 true임
-> 주입 받는 객체는 반드시 스프링 컨테이너에 생성되어 있어야 한다. 없을시 예외발생!!!

-> 객체가 없는 경우에 대처 하기 위해 required를 false로 바꿔주어야 한다.

  • 주입 받는 객체가 없을 경우 setter메서드를 호출하지 않는다.
  • 의존성 주입도 하지 않음

🔽

public class InfoService {//회원목록 조회

    private MemberDao memberDao;
    private DateTimeFormatter formatter;

    @Autowired
    @Qualifier("memberDao")
    public void setMemberDao(Optional<MemberDao> opt) {
        this.memberDao = opt.get();
    }

    @Autowired(required = false) //DateTimeFormatter bean이 없으면 호출 X
    public void setFormatter(DateTimeFormatter formatter) {
        System.out.println("호출!"); //호출 안됨
        this.formatter = formatter;
    }


    public void printList(){
        List<Member> members = memberDao.getList();
        members.forEach(m -> {
            System.out.println(m);
            LocalDateTime regDt = m.getRegDt();
            if(formatter != null){
                System.out.println(formatter.format(regDt));
            }

        });
    }
}
  • required: true
    없을 수 있는 주입 받는 객체 @Nullable 애노테이션을 적용해도 필수 여부 해제
    • 메서드 호출⭕, 없는 의존성에 null을 대입

-> formatter에 Null로 대입되면서 출력문이 실행 되었다.

@Autowired(required = false) 의 경우 '호출!'이 출력되지 않았음
-> 아예 메서드 자체를 무시함

컴포넌트 스캔

1. @Component

2. @ComponentScan

  • 스프링 컨테이너가 자동으로 스캔할 패키지 범위 설정
  • 지정한 패키지 범위를 기본 스캔 대상으로 스캔 -> 객체 생성

설정한 스캔 범위를 보고 컨테이너가 클래스의 애노테이션들을 확인한다. 있다면 관리 할 객체임을 알고 객체를 생성해준다.

3. 기본 스캔 대상

ComponentScan이 지정한 패키지 안에 기본 스캔대상 애노테이션이 붙어있는 클래스를 찾는다.

기본 스캔 대상 애노테이션을 자동적으로 찾아서 관리 객체로 지정! 이후 스프링 컨테이너에서 객체가 생성된다.

암기!!👩‍🏫
@Component
@Service
@Configuration
@Controller
@RestController
@ControllerAdvice
@RestControllerAdvice
@Aspect
@Repository: DAO에 주로 정의

기본 스캔 대상이 지정된 클래스 안에서 의존성을 필요로 한다면 의존성 자동 주입으로 @Autowired 애노테이션을 붙여주면 컨테이너에서 객체를 찾아서 의존성 주입을 해주게된다.

4. 컴포넌트 스캔에 따른 충돌 처리

1) 빈 이름 충돌

🔼 패키지가 다르면 다른 클래스니까 같은 클래스명을 가진다해도(bean의 이름이 같다고 해도) 충돌이 안나지 않을까?!
..
..
but! 충돌 발생함

  • 클래스명만 빈의 이름으로 고려
  • 다른 패키지에 있는 동일한 이름의 클래스가 있으면 충돌 발생
@Component("memberDao2")
public class MemberDao {
}
..
이렇게 bean의 이름을 바꿔주어야 한다.

2) 수동 등록한 빈과 충돌

MemberDao 클래스의 bean 이름은 클래스명 그대로 memberDao가 된다.

bean의 이름이 같은데 충돌이 나지 않는다❓❔⁉️

💫우선순위가 있기 때문!
보통은 자동 스캔범위 보다 수동으로 등록된 bean이 우선적으로 선택이 된다.

-> 수동으로 등록된 bean을 실행하고 자동스캔 한 bean은 실행하지 않는다

단, 수동 등록 bean이 2개라면 오류 발생함

해결법 ? bean의 이름 직접 지정해주기 @Qualifier

3) excludeFilters

  • excludeFilters 속성을 사용하면 스캔할 때 특정 대상을 자동 등록 대상에서 제외할 수 있다.

filter에 배제 할 애노테이션을 설정 할 수 있다.

🔼Filter 내부 애노테이션

👩‍💻사용법

Filter 애노테이션의 type속성의 형식을 주어서 제외 대상을 지정할 수 있다.

  • 특정 애노테이션이 붙인 타입을 컴포넌트 대상에서 제외시키기

🔖type = FilterType.ANNOTATION

@Configuration
@ComponentScan(basePackages = "member",
excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {ManualBean.class})})
//ManualBean이 포함 되어있는 대상은 자동 스캔 범위에서 제외된다.
//한개 일 경우 중괄호 생략 가능
public class AppCtx {

}
@Repository
@ManualBean
public class MemberDao { 
	...
    ...
    }
}

이 상태로 돌리면 MemberDao의존성을 필요로 하는곳에서 의존성 주입이 되지 않기 때문에 테스트 에러 발생!
제외 대상에 ManualBean이 포함되었기 때문에 자동 스캔을 하지 않았다.

  • 특정 타입이나 그 하위 타입을 컴포넌트 스캔 대상에서 제외할때 사용
  • 클래스 명으로 배제 할 의존성 두개 지정

🔖type = FilterType.ASSIGNABLE_TYPE

@Configuration
@ComponentScan(basePackages = "member",
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE,
classes = {MemberDao.class, JoinValidator.class})
)
public class AppCtx {

}
  • 정규 표현식 사용해서 member패키지 안에 Dao로 끝나는 클래스 배제 시키기

🔖type = FilterType.REGEX

@Configuration
@ComponentScan(basePackages = "member",
excludeFilters = {@ComponentScan.Filter(type = FilterType.REGEX,
pattern = "member\\\\..*Dao")})
public class AppCtx {

}

implementation'org.aspectj:aspectjweaver:1.9.22.1'

  • Ant 패턴 사용 해서 배제하기
    • AspectJ 패턴이 동작하려면 의존 대상에 aspectjweaver 모듈을 추가해야한다.
  • 정규표현식 대신 AspectJ 패턴을 사용해서 대상을 지정할 수 있다.

🔖type = FilterType.ASPECTJ

@Configuration
@ComponentScan(basePackages = "member",
excludeFilters = @ComponentScan.Filter(type = FilterType.ASPECTJ, pattern = "member..*Dao")
)
public class AppCtx {

}
  • 설정 할 필터가 두개 이상이면 @ComponentScan의 excludeFilters 속성에 배열을 사용해서 @Filter 목록을 전달하면 된다.
@Configuration
@ComponentScan(basePackages = {"spring"}, 
	excludeFilters = {
		@Filter(type = FilterType.ANNOTATION, classes = ManualBean.class),
		@Filter(type = FilterType.REGEX, pattern = "spring2\\.*")
	})

📕정리📘
스프링 컨테이너(AnnotationConfigApplicationContext)

  • 💡객체 생성
    • 스프링 설정을 참고해서 객체를 생성함
      • 설정 클래스: @Configuration 애노테이션이 붙어있는 클래스 ex) AppCtx
        @Bean애노테이션: 메서드 명 위에 -> 수동 등록 빈
        @ComponentScan("스캔범위"): 스프링 컨테이너가 생성할 객체의 클래스를 스캔할 범위
    • 스캔 범위가 설정되어 있으면 그 범위를 스캔한다.
    • 기본 스캔 대상에 해당하는 클래스이면 객체 생성

      @Component
      @Service

      <특정 기능과 관련된 애노테이션>
      @Configuration
      @Controller
      @RestController
      @ControllerAdvice
      @RestControllrAdvice
      @Aspect
      @Repository
  • 💡의존성 주입
    • @Autowired: 의존성 자동 주입
      1) 멤버 변수 위
      2) setter 메서드 위
      3) Optional 형태로 감싼 구조(값이 없을 경우를 대비)/ 멤버 변수, setter 메서드
    • @Autowired 애노테이션을 사용하지 않고 의존성 주입
      - 생성자 매개변수에 의존성을 정의한 경우(주의! -> 기본 생성자는 정의 X)
      : 객체 생성할때 의존성 주입을 강제한다.
      - 롬복 @RequiredArgsConstructor와 함께 많이 사용된다.
      ◻ 초기화가 반드시 필요한 멤버변수를 생성자 매개변수에 추가

빈 라이프 사이클과 범위

  • 스프링 컨테이너는 초기화와 종료라는 라이프 사이클을 갖는다.

1. 컨테이너 초기화 : 빈 객체의 생성, 의존 주입, 초기화

1) 컨테이너 초기화
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppContext.class);

  • AnnotationConfigApplicationContext의 생성자를 이용해서 컨텍스트 객체를 생성하는데 이 시점에 스프링 컨테이너를 초기화한다.

  • 스프링 컨테이너는 설정 클래스에서 정보를 읽어와 알맞은 빈 객체를 생성하고 각 빈을 연결(의존 주입)하는 작업을 수행한다.

2) 컨테이너에서 빈 객체를 구해서 사용

Greeter g = ctx.getBean("greeter", Greeter.class);
String msg = g.greet("스프링");
System.out.println(msg);
  • 컨테이너 초기화가 완료되면 컨테이너를 사용할 수 있다.

  • 컨테이너를 사용한다는 것은 getBean()과 같은 메서드를 이용해서 컨테이너에 보관된 빈 객체를 구한다는 것을 뜻한다.

2. 컨테이너 종료 : 빈 객체의 소멸

ctx.close();
  • 컨테이너 사용이 끝나면 컨테이너를 종료한다. 컨테이너를 종료할 때 사용하는 메서드가 close() 메서드이다.

  • close() 메서드는 AbstractApplicationContext 클래스에 정의되어 있다.

  • 스프링 컨테이너의 라이프사이클에 따라 빈 객체도 자연스럽게 생성과 소멸이라는 라이프사이클을 갖는다.

3. 빈 객체의 라이프 사이클

  • 스프링 컨테이너는 빈 객체의 라이프 사이클을 관리한다.

1) 객체 생성 -> 의존 설정 -> 초기화 -> 소멸

  • 스프링 컨테이너를 초기화할 때 스프링 컨테이너는 가장 먼저 빈 객체를 생성하고 의존을 설정한다.
  • 의존 자동 주입을 통한 의존 설정이 이 시점에 수행된다.
  • 모든 의존 설정이 완료되면 빈 객체의 초기화를 수행한다. 빈 객체를 초기화하기 위해 스프링은 빈 객체의 지정된 메서드를 호출한다.
  • 스프링 컨테이너를 종료하면 스프링 컨테이너는 빈 객체의 소멸을 처리한다. 이때에도 지정된 메서드를 호출한다.

  • 초기화: 객체가 완전히 생성되고 조립된 다음에 처리 할 작업을 정의하면 실행되는 단계

  • 소멸 -> (ctx.close()- 객체 소멸): 소멸 전에 처리할 작업을 정의하면 실행되는 단계

📢스프링 컨테이너 생성시 진행되는 부분(객체 생성 -> 의존 설정 -> 초기화)

2) InitializingBean 인터페이스

  • 빈 객체가 InitializingBean 인터페이스를 구현하면 스프링 컨테이너는 초기화 과정에서 빈 객체의 afterPropertiesSet() 메서드를 실행한다.

  • afterPropertiesSet 메서드: 초기화 단계시에 실행된다.

    • 객체가 완전히 조립되고 생성 된 후에 처리 할 작업을 정의

afterPropertiesSet 은 컨테이너 생성시 발생된다.

public class Ex02 {

    @Test
    void test1(){
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);

//        BoardService service = ctx.getBean(BoardService.class);
//        service.write();

        ctx.close();
    }
}

없어도 똑같이 실행됨

3) DisposableBean 인터페이스

  • 스프링 컨테이너는 빈 객체가 DisposableBean 인터페이스를 구현한 경우 소멸 과정에서 빈 객체의 destroy() 메서드를 실행한다.

  • destroy 메서드: 컨테이너에 있는 객체가 소멸되기 전에 실행

    • 주로 객체 소멸 전에 할 작업 ex) 자원해제 등..
@Service
public class BoardService implements InitializingBean, DisposableBean {
    public void write(){
        System.out.println("글쓰기!");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet()!");
    }

    @Override
    public void destroy() throws Exception {
        //객체 소멸 직후 실행
        System.out.println("destroy()!");
    }
}
...
#Test
public class Ex02 {

    @Test
    void test1(){
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
        
        ctx.close();
    }
}

destroy는 ctx.close() 가 호출되면 close하기 전에 호출된다.
만약에 ctx.close()가 없다면 호출되지 않음

👩‍🏫제한 조건 -> 인터페이스 추가하는 것은 직접 만든 클래스만 가능하다.

외부에서 이용하는 클래스들을 사용해야 할 경우가 더 많을텐데 그건 어떻게 해야할까?
🔽 🔽 🔽

4. 빈 객체의 초기화와 소멸 : 커스텀 메서드

  • 모든 클래스가 InitializingBean 인터페이스와 DisposableBean 인터페이스를 상속받아 구현할 수 있는 것은 아니다.
    직접 구현한 클래스가 아닌 외부에서 제공받은 클래스를 스프링 빈 객체로 설정하고 싶을 때도 있다. 이 경우 소스 코드를 받지 않았다면 두 인터페이스를 구현하도록 수정할 수 없다.

ex) 외부 라이브러리, 자바 JDK 기본 클래스, 스프링 프레임 워크 기본 클래스 등등

  • 이렇게 InitializingBean 인터페이스와 DisposableBean 인터페이스를 구현할 수 없거나 이 두 인터페이스를 사용하고 싶지 않은 경우에는 스프링 설정에서 직접 메서드를 지정할 수 있다.

  • 방법은 @Bean 태그에서 initMethod 속성과 destroyMethod 속성를 사용해서 초기화 메서드와 소멸 메서드의 이름을 지정하면 된다.

@Bean 애노테이션에 추가되어있는 설정

1) initMethod

  • InitializingBean :: afterPropertiesSet 메서드와 동일 시점에 호출된다.

2) destroyMethod

  • Disposable :: destroy 메서드와 동일 시점에 호출된다.

🔸BoardService2는 외부에서 제공 받은 클래스이고 변경 불가를 가정하고 있다.

5. 빈 객체의 생성과 관리 범위

@Scope

  • @Scope("singleton"): 정의하지 않아도 기본은 싱글톤 패턴으로 관리
  • @Scope("prototype"): 매번 조회시마다 새로운 객체를 생성
    • 스프링 객체 관리 기능은 기본적으로 싱글톤일때 정상적으로 동작하기 때문에 -> prototype: 일부 기능에 제한조건이 발생하고 실행 시점이 달라질수있다.

🔼갱신할때마다 새로운 객체 만들어짐

profile
꽁꽁 얼어붙은 한강 위로 😺

0개의 댓글