singleton, prototype 처럼 @Scope를 통해 request 객체를 지정해줄 수 있다.

 

request 객체는 Http 요청이 발생하면 생성되는 객체이다.

각 요청마다 하나씩 생성되기 때문에 수많은 요청이 들어와도 request 객체를 통해 전부 구분할 수 있다.

 

하지만 코드를 짤 때 아무런 조치도 취하지 않고 @Scope(value = "request") 처리만 해놓으면 서버가 제대로 동작하지 않는다.

 

말했듯이 request 객체는 Http 요청이 발생할 때 생성되는 객체인데 서버를 막 띄운 상태에서는 어떠한 Http 요청도 들어오지 않은 상태이기 때문이다.

 

즉, 생성된 request 객체는 없는데 request 객체에 의존성을 가진 Controller, Service 등의 클래스들이 컴포넌트 스캔의 대상이 되어 DI를 실행할 때 문제가 생기는 것이다.

 

이를 두 가지 방식으로 해결할 수 있다.

 

 

1. ObjectProvider<T>

request 객체에 의존성을 띈 모든 코드에 request 객체 대신 ObjectProvider를 두고 request 객체 사용 로직에서는 provider.getObject() 로 객체를 받아서 사용하는 것이다.

 

 

2. Proxy (이걸 더 많이 사용하는 듯)

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)

(타겟의 형태에 따라서 TARGET_CLASS or INTERFACES 선택)

 

이렇게 하면 Http 요청 발생 여부와 관계 없이 request 클래스를 상속받은 가짜 프록시 클래스의 객체를(CGLIB) 생성하여 request 객체에 의존성을 가진 다른 빈에 대해서 DI를 실시한다.

 

request 객체를 사용하는 로직에서는 호출 시점에 진짜 request 객체를 찾아서 사용한다.

(진짜 request 객체를 찾아내는 로직은 가짜 프록시 객체 내부에서 진행된다.)

 

Proxy를 사용하면 어노테이션 설정 변경만으로 request 객체를 마치 싱글톤 객체를 사용하는 것처럼 사용할 수 있다.

(하지만 진짜 reqeust 객체는 Http 요청마다 생성되는 것이기 때문에 실제로는 여러 개가 존재)

 

 

 

 

@Scope의 무분별한 사용을 경계해야 한다.

유지보수에 큰 장애가 될 수 있으니 꼭 필요한 곳에서만 사용하자

@Scope를 통해 빈의 스코프를 지정해줄 수 있다.

(디폴트는 싱글톤 빈이다.)

 

프로토타입.

@Scope("prototype") 으로 지정해준다.

싱글톤처럼 하나의 객체를 계속 사용하는 방식이 아닌 호출때마다 매번 객체를 생성한다.

@PostConstruct 메서드는 스프링이 실행해주지만 그 이후의 관리는 전적으로 사용자의 몫으로 남겨진다.

그래서 @PreDestroy 메서드는 사용자가 명시적으로 호출해줘야 한다.

 

싱글톤과 프로토타입을 함께 사용하면 문제가 발생할 수 있다.

싱글톤에서 프로토타입에 대한 의존성을 갖고 있을 경우.

싱글톤 객체는 한 번만 생성되고 그 이후부터는 쭉 스프링 컨테이너에 저장되어있을뿐 새로 만들어지지 않는다.

당연히 그 안에 있는 프로토타입 빈도 의존성 주입 당시에 한 번만 생성되어 그대로 해당 객체가 유지된다. 

이는 접근할 때마다 새로운 객체를 생성한다는 프로토타입의 원래 목적에 반하는 동작이다.

(여러 종류의 싱글톤에서 한 프로토타입을 의존할 경우에는 각각 다른 프로토타입 빈이 싱글톤에 등록된다. 그 이후로 쭉 유지되는건 똑같지만)

 

이러한 문제점을 ObjectProvider<T>를 사용하여 해결할 수 있다.

private ObjectProvider<PrototypeBean> prototypeBeanProvider;
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();

기존 방식처럼 생성자를 통해 DI를 실행하면 프로토타입이 프로토타입일 수 없는 문제가 발생했다.

 

ObjectProvider를 사용하여 해당 프로토타입 빈에 대한 프로바이더를 만들 수 있고, 이를 통해 getObject()로 프로토타입 빈에 접근한다.

 

이때, 미리 만들어져있는 빈을 제공하는 것이 아니라 getObject() 가 호출될 때 새 프로토타입 빈을 생성하여 반환해준다.

(getObject() 하려는 빈이 프로토타입 빈일 때만임. 사실 Provider는 프로토타입을 위한 기능이 아니라 DL을 위한 기능임)

 

즉, 호출할 때마다 매번 생성된다. 프로토타입으로서의 올바른 동작법이다. 이를 DL (Dependency Lookup) 이라고 한다.

(물론 ObjectProvider에 대한 DI는 따로 해줘야 한다.)

 

 

 

사실 프로토타입을 사용하는 일은 거의 없고 대부분이 싱글톤으로 해결된다고 한다.

스프링에서 빈 생명주기 콜백을 지원하는 방법은 3가지가 있다.

 

1. 인터페이스(InitializingBean, DisposableBean)  

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;
    public NetworkClient() {
        ...
    }

    // 서비스 시작시 호출
    public void connect() {
        ...
    }

    public void call() {
        ...
    }

    // 서비스 종료시 호출
    public void disconnect() {
        ...
    }

    @Override  // 의존성 주입 이후에 실행 (InitializingBean)
    public void afterPropertiesSet() throws Exception {
        connect();
        call();
    }

    @Override  // 빈 소멸 전 호출 (DisposableBean)
    public void destroy() throws Exception {
        disconnect();
    }
}

 관련 로직은 직접 구현해야 한다

 

-> 요즘은 별로 사용하지 않는 방식이다.

 

 

 

 

2. 설정 정보에 초기화 메서드, 종료 메서드 지정

public class NetworkClient {

    private String url;
    public NetworkClient() {
        ...
    }

    // 서비스 시작시 호출
    public void connect() {
        ...
    }

    public void call() {
        ...
    }

    // 서비스 종료시 호출
    public void disconnect() {
        ...
    }

    public void init() {
        connect();
        call();
    }

    public void close() {
        disconnect();
    }
}

.
.
.

@Configuration
static class LifeCycleConfig {

    // networkClient.init(), networkClient.close() 를 초기화, 소멸자 메서드로 사용한다 
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
    	NetworkClient networkClient = new NetworkClient();
        return networkClient;
    }
}

 

@Bean() 에서 속성을 줄 수 있다.

@Component에 적용시키니까 컴파일 에러가 발생한다. 아마 자동 등록에서는 쓸 수 없는 방식인 듯?

 

코드에 직접적으로 변화를 주는 것이 아니라 설정 정보(@Configuration)를 사용하는 것이기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있게 된다.

 

 

 

3. @PostConstruct, @PreDestory (이걸 쓰면 된다)

public class NetworkClient {

    private String url;
    public NetworkClient() {
        ...
    }

    // 서비스 시작시 호출
    public void connect() {
        ...
    }

    public void call() {
        ...
    }

    // 서비스 종료시 호출
    public void disconnect() {
        ...
    }

    @PostConstruct
    public void init() {
        connect();
        call();
    }

    @PreDestroy
    public void close() {
        disconnect();
    }
}

 

해당 메서드들에 직접적으로 어노테이션을 달아주면 된다.

(클래스에 직접 적용된 코드이니 당연히 @Componenet를 써도 잘 돌아간다)

 

스프링에서 직접 권장하는 방식이므로 그냥 이걸 사용하자.

 

코드를 직접적으로 추가해야 하기 때문에 외부 라이브러리에는 적용이 불가능하다. 외부 라이브러리에 대해서는 2번 방식(@Bean(initMethod = "init", destroyMethod = "close"))을 사용해야 한다.

@Autowired 자동 주입은 객체의 '타입'으로 조회한 빈을 주입한다.

@Autowired
private DiscountPolicy discountPolicy
.
.
@Component
public class FixDiscountPolicy implements DiscountPolicy {
}
.
.
@Component
public class RateDiscountPolicy implements DiscountPolicy {
}

바람직한 설계는 인터페이스에 의존하는 것이다.

위와 같은 코드에서 discountPolicy에 주입할 빈들을 조회하면 해당 인터페이스를 구현한 모든 구현체들의 빈이 조회될 것이다.

그럼 그 여러 가지 빈들 중 어느것을 주입해야할까?

 

 

세 가지 방법이 존재한다.

 

1. 필드명 매칭

조회된 빈들 중에서 자동주입이 되어야 하는 필드 변수의 이름과 같은 이름의 빈이 있다면 이 빈을 우선적으로 등록한다.

@Component
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired  // 파라미터 변수명 rateDiscountPolicy
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }
}

타입은 DiscountPolicy 이지만 생성자의 파라미터 이름은 rateDiscountPolicy 이다.

조회된 빈들(fixDiscountPolicy, rateDiscountPolicy) 중 필드명과 이름이 같은 빈인 rateDiscountPolicy 가 우선적으로 주입되게 된다.

(생성자 주입이 아닌 필드 주입을 사용하는 경우에도 동일하게 적용된다.)

 

 

2. @Qualifier

public class BlahBlah {
    private final DiscountPolicy discountPolicy
    
    @Autowired
    public BlahBlah(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}
.
.
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
}
.
.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
}

@Qualifier는 의존성 주입에 있어 추가적인 구분 방법일 뿐 빈 이름을 변경하는 것은 아니라는 것에 주의하자

(@Qualifier 역시 필드 주입에서도 사용 가능하다)

 

만약 @Qualifier("mainDiscountPolicy") 가 일치하는 빈을 찾지 못한다면 차선책으로 빈 이름이 mainDiscountPolicy인 빈을 찾는다.

(이 기능은 굳이 사용되게 하지 않도록 하는 것이 좋다)

 

@Qualifier 는 ComponentScan 이 아닌 @Bean 직접 등록할 때에도 달아넣을 수 있다.

 

(추가)

Lombok의 @RequiredArgsConstructor 을 사용해서 생성자를 만들 경우 @Qualifier를 사용하지 못한다.

@RequiredArgsConstructor 는 어노테이션까지 포함해 생성자를 만들 수 없다.

www.inflearn.com/questions/71872

위 링크에 @RequiredArgsConstructor과 @Qualifier를 같이 사용할 수 있는 방법이 나와있지만 귀찮으니 그냥 같이 쓰지는 말자

 

 

3. @Primary  (자주 사용함, 편하지만 한계점이 있다)

그냥 우선순위를 주고 싶은 빈의 클래스에 @Primary 를 달아주면 된다.

@Autowired
private DiscountPolicy discountPolicy
.
.
@Component
public class FixDiscountPolicy implements DiscountPolicy {
}
.
.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
}

 

 

만약 @Primary와 @Qualifier가 겹치는 상황이 발생한다면?

-> @Qualifier가 우선된다.

 

스프링은 항상 포괄적인 개념보다는 상세한 개념을 우선시 한다.

(수동등록 > 자동등록 처럼)

여러 가지 문제들에 의해 필드주입, setter주입은 사용을 지양하며 반드시 생성자 주입을 사용하는게 좋다.

 

생성자 주입은 필드변수에 final키워드 사용 가능, 테스트 코드에서 컴파일에러를 통해 실수한 부분을 바로 알 수 있음 등의 장점이 있다.

 

하지만 생성자 코드를 그때그때 전부 입력하는 것은 좀 귀찮을 수도 있다.

 

Lombok 라이브러리를 통해 이를 해결할 수 있다.

 

Lombok 라이브러리는 start.spring.io 에서 프로젝트 생성 시 Dependencies에서 검색하여 추가할 수 있다.

이 라이브러리를 사용하기 위해선

file -> settings -> build, execution, deployment -> compiler -> annotation processors 에서 enable annotation processing을 체크해주어야 한다.

(file -> settings -> plugins 에서 lombok 검색하여 설치는 반드시 해줘야 함)

 

Lombok 라이브러리를 사용하면 getter, setter, 생성자 등을 직접 만들지 않아도 된다.

 

클래스에 @Getter, @Setter를 다는 것만으로 라이브러리가 만들어주는 getter, setter를 사용할 수 있으며,

 

@RequiredArgsConstructor 를 통해 final이 붙은 필드변수들을 받는 생성자를 알아서 만들어준다

 

@Component
@RequiredArgsConstructor  // final이 붙은 필드변수들을 받는 생성자를 알아서 만들어준다
public class OrderServiceImpl implements OrderService{

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

}

위 코드에서 눈에 보이는 생성자는 없지만 @RequiredArgsConstructor 에 의해 memberRepository, discountPolicy 두 객체를 받아서 주입해주는 생성자가 존재하게 된다.

 

 

생성자 주입의 이점과 코드의 간편화를 동시에 챙길 수 있다.

 

 

 

 

 

출처 : www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

1. 생성자 주입 (가장 많이 사용)

생성자에 @Autowired 를 사용해 주입한다.

클래스가 ComponentScan 범위에 들어오면 빈이 생성되면서 생성자가 호출된다. 이때 @Autowired를 통해 DI가 이루어진다.

생성자가 하나만 있을 경우 @Autowired는 생략이 가능하다.

생성자는 한 번만 호출되기 때문에 불변, 필수 의존성 주입에 사용하는 방식이다. (객체가 절대로 바뀔 일이 없고 반드시 null이 아니어야 하는 의존성)

 

 

2. setter 주입

setter에 @Autowired 를 사용해 주입한다.

setter는 계속해서 호출이 가능하기 때문에 선택, 변경의 가능성이 있는 의존성 주입에 사용하는 방식이다.

필수값인 객체는 생성자 주입을 사용하고 필수가 아닌 객체는 setter 주입을 따로 사용하는 방법도 있다.

 

 

3. 필드 주입 (그냥 쓰지 말자)

그냥 필드 변수 선언 시에 필드측에 @Autowired로 박아버린다.

생성자, setter 주입 방식은 테스트 코드에서 순수 자바코드로도 생성자 주입이 가능하지만 필드 주입은 오로지 스프링 컨테이너를 통해서만 의존성 주입이 가능함.

스프링 컨테이너를 띄우지 않고 순수 자바코드로 단위 테스트를 실행할 때 의존성을 주입할 수 있는 방법이 없어져 곤란해질 수 있다.

(규모가 큰 프로젝트에서 스프링 컨테이너를 띄우고 테스트하는 것은 속도가 느리기 때문에 순수 자바 코드만 사용하는 단위테스트를 사용하는 경우도 많다. 필드 주입은 이 상황에서 의존성을 주입할 수 없게 된다.)

 

굳이 필드주입을 사용하고 순수 자바 코드 테스트를 진행하려면 별도의 setter를 만들어서 의존성을 주입해주어야 한다.

(근데 이럴거면 그냥 생성자 주입 or setter 주입을 쓰고 말지..)

 

단, 애플리케이션 설계와 전혀 관계가 없는 테스트 코드 내에서는 필드 주입으로 빠르게 의존성을 주입하고 사용하는 게 더 좋을 수도 있다.

 

 

4. 일반 메서드 주입

그냥 일반 메서드에 @Autowired 달고 주입받는 방식

거의 쓰지 않는다. 쓰지 말자.

 

 

 

 

(옵션 처리 설정)

@Autowired로 의존성 주입을 실시할 때 자동주입할 객체가 없는 경우가 있을 수도 있다.

이때 아무런 조치를 취하지 않은 채로 그냥 @Autowired만 사용한다면 에러가 발생한다.

주입할 빈이 없어도 문제없이 동작하게 하기 위해선 다음과 같은 처리들을 해줄 수 있다.

 

1. required

@Autowired(required = false) 로 옵션 지정을 해준다.

required의 디폴트 값은 true이다.

false로 지정될 경우, 주입할 빈이 없으면 해당 메서드를 실행하지 않고 바로 넘어간다.

 

2. @Nullable

method(@Nullable Member member)

파라미터 앞에 @Nullable을 추가할 수 있다. 들어올 빈이 없어도 메서드 호출은 이루어지지만 null이 주입된다.

 

3. Optional

method(Optional<Member> member)

자바8의 Optional을 사용하는 방법이다. (Optional.empty 주입됨)

 

 

 

 

 

 

 

출처 : www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

@Component("등록될 빈 이름")을 통해 빈의 이름을 직접 설정할 수 있다. (디폴트는 클래스명에서 앞글자만 소문자로 바꾼 이름)

만약 각각 다른 클래스에 똑같은 이름으로 설정해 놓는다면?

ex) 둘 다 service로 이름 설정 됨

@Component("service")
public class MemberServiceImpl implements MemberService {
}
.
.
@Component("service")
public class OrderServiceImpl implements OrderService {
}

-> ConflictingBeanDefinitionException 예외 발생

 

 

 

위처럼 자동vs자동이 아닌 수동vs자동인 경우 수동빈이 우선권을 가진다. 자동빈을 수동빈이 덮어버림

 

-> 이를 의도하여 사용할 수도 있겠지만 현실은 의도적이기 보다는 실수로 이런 현상을 많이 초래한다. 에러가 나는 상황인데 에러가 나지 않으니 잡기도 어렵다. 그래서 최근 스프링 부트에서 수동 빈 등록과 자동 빈 등록이 충돌하면 오류가 발생하도록 기본값을 변경하였다.

 

 

@ComponentScan 을 통해 빈 등록을 위한 컴포넌트 스캔을 조작할 수 있다.

 

basePackages 옵션을 통해 컴포넌트 스캔을 시작할 패키지의 위치를 직접 설정할 수 있으나 디폴트는 일단 @Component 가 있는 클래스가 속한 패키지를 시작으로 그 하위를 모두 스캔한다.

 

일반적이고 바람직한 방식은 스캔 시작 패키지를 따로 지정하기 보다는 설정 정보 클래스(AppConfig)를 프로젝트 최상단에 두는 것이다.

 

프로젝트의 구조가 다음과 같다면,

com.hello

com.hello.service

com.hello.repository

AppConfig를 com.hello 하위에 만들고 @ComponentScan을 다는 것이다. 그럼 service, repository 모든 패키지를 스캔할 수 있다.

 

컴포넌트 스캔을 시작하면 스캔 범위에 해당하는 모든 클래스를 검사하여 @Component 가 붙은 모든 클래스의 빈을 등록한다. 

(@Configuration, @Controller, @Service, @Repository 등은 모두 내부에 @Component를 포함하고 있다.)

 

스프링 부트를 사용할 때는 @SpringBootApplication 을 프로젝트 시작 루트 위치에 두는 것이 일반적이다.

(@SpringBootApplication은 내부적으로 @Component를 포함한다.)

 

 

 

또한 굳이 AppConfig 내부에서 @Bean을 통해 의존성 주입을 수동으로 해주지 않아도 @Autowired를 통해 자동으로 의존성 주입을 해줄 수 있다.

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
}

이렇게 생성자에 @Autowired를 달아 의존성 생성자 주입을 자동으로 해줄 수 있다.

(@Autowired 를 찾으면 해당 함수의 파라미터의 타입으로 주입할 빈을 찾는다.)

(필드주입에도 @Autowired 사용 가능함)

 

이때, 만약 등록할 빈의 클래스에 @Component가 붙어있지 않거나 스캔의 범위에서 벗어나있어 빈 등록이 되지 않았을 경우 에러가 발생한다.

 

또한, 해당 빈의 타입으로 여러 가지 빈이 존재할 수 있는데 이 경우에는 @Qualifier 를 통해 어떤 타입의 빈을 주입할 것인지 명시할 수 있다.

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(@Qualifier("memory") MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    
}

.
.
.

@Repository("memory")
public class MemoryMemberRepository implements MemberRepository {
    ...
}

의존성 주입에서 우선순위를 두고 싶은 빈의 이름을 @Qualifier에 담아 어노테이션을 달아놓으면 해당 빈을 우선적으로 찾아 주입한다.

(codeung.tistory.com/116 에서 좀 더 자세히 설명)

 

 

 

 

출처 : www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

+ Recent posts