본문 바로가기
spring

스프링 기본편 복습 - 2

by 오우지 2022. 1. 28.

빈에 등록해야 할 것들이 많아지면 귀찮고 누락 할 가능성이 커진다. 따라서 스프링은 자동으로 스프링 빈을 등록하는 컴포넌트 스캔을 제공하고, 의존관계 자동 주입을 해주는 @Autowired도 제공한다. 

 

컴포넌트 스캔을 사용하기 위해서 @ComponentScan를 붙여주면 된다.

 

 

탐색 위치와 기본 스캔 대상

 

탐색할 패키지의 시작 위치 지정을 할 수 있지만 관례적으로 패키지 위치를 지정하지 않고 설정 정보 클래스의 위치를 프로젝트 최상단에 둔다. 지정을 하려면

basePackages: 탐색할 패키지의 시작 위치를 지정

basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위로 지정

디폴트는 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치

 

 

컴포넌트 스캔 기본 대상

@Component뿐만 아니라 다음 내용들도 대상에 포함한다.

@Controller, @Service, @Repository, @Configuration(스프링 설정 정보)

 

컴포넌트 스캔의 용도 뿐만 아니라 부가 기능을 수행한다.

@Controller : 스프링 MVC 컨트롤러로 인식

@Repository: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 반환해준다.

@Configuration: 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리

@Service: 특별한 처리는 없이 비즈니스 계층을 인식하는데 도움이 된다.

 

 

필터

스프링에서는 컴포넌트 스캔 시 추가할 대상과 제외할 대상을 필터를 이용해서 구분할 수 있다.

includeFilters, excludeFilters

컴포넌트 스캔에서 추가할 대상, 제외할 대상을 서술한다고 보면 된다.

 

 

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class));
    }

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

    }
}

 

 

beanA에 MyIncludeComponent를 적용, beanB에 MyExcludeComponent를 적용 한 후에 위의 코드를 실행시켜보면 beanB는 Exclude시켰기 때문에 해당하는 에러가 나오는 것을 알 수 있다.

 

FilterType은 5가지 옵션이 있는데

ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.

ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.

ASPECTJ: AspectJ패턴 사용

REGEX: 정규 표현식

CUSTOM: TypeFilter라는 인터페이스를 구현해서 처리

 

중복 등록과 충돌

1. 자동 빈 등록 vs 자동 빈 등록

컴포넌트 스캔을 통해 자동 빈 등록이 되는데 스프링은 ConflictingBeanDefinitionException이 발생한다.

 

2. 수동 빈 등록 vs 자동 빈 등록

수동 빈이 자동 빈을 오버라이드 해버린다. 그러면 잡기 어려운 버그가 만들어진다. 따라서, 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 동시에 일어나면 에러를 뽑는다.

 

 

 

 

의존관계 자동 주입

다양한 의존관계 주입 방법

1. 생성자 주입

2. 수정자 주입

3. 필드 주입

4. 일반 메서드 주입

 

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개 있으면 @Autowired를 생략해도 자동 주입 된다.

 

2. 수정자 주입

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다.

선택, 변경의 가능성이 있는 의존관계에 사용한다.

@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;
    }
}

 

 

3. 필드 주입

필드에 바로 주입하는 것이다.

코드가 간결해지긴 하지만 외부에서 변경이 불가능해서 테스트 하기 힘들다는 단점이 있다.

DI 프레임워크가 없다면 아무것도 할 수 없다. 가급적 쓰지 않는게 좋다.

@Autowired private MemberRepository memberRepository

 

4. 일반 메서드 주입

일반 메서드를 통해서 주입 받을 수 있다.

한번에 여러 필드를 주입 받을 수 있다.

일반적으로 잘 사용하지 않는다.

 

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.

그런데 @Autowired만 사용하면 required가 true로 자동 설정되어 오류가 발생한다.

public class AutowiredTest {

    @Test
    void AutowiredOption(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestBean.class);
    }

    static class TestBean{

        //컨테이너에 의해 관리되지 않는 객체 주입
        @Autowired(required = false)
        public void setNoBean1(Member noBean1){
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired
        public void setNoBean2(@Nullable Member noBean2){
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired
        public void setNoBean3(Optional<Member> noBean3){
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

결과

noBean3 = Optional.empty
noBean2 = null

 

결론:

생성자 주입을 선택해야 한다. 

 

 

 

 

조회한 빈이 모두 필요할 때, List, Map

의도적으로 해당 타입의 빈이 모두 필요한 경우가 있다. 예를 들면 고객이 할인의 종류(rate, fix)를 선택할 수 있을 때.

스프링을 사용하면 전략 패턴을 매우 간단하게 구현할 수 있다.

public class AllBeanTest {

    @Test
    void findAllBean(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, DiscountService.class);

        DiscountService discountService = ac.getBean(DiscountService.class);
        Member member = new Member(1L, "userA", Grade.VIP);
        int discountPrice = discountService.discount(member, 10000, "fixDiscountPolicy");

        assertThat(discountService).isInstanceOf(DiscountService.class);
        assertThat(discountPrice).isEqualTo(1000);

        int rateDiscountPrice = discountService.discount(member, 20000, "rateDiscountPolicy");
        assertThat(rateDiscountPrice).isEqualTo(2000);
    }

    static class DiscountService{
        private final Map<String, DiscountPolicy> policyMap;
        private final List<DiscountPolicy> policies;

        @Autowired
        public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
            this.policyMap = policyMap;
            this.policies = policies;
            System.out.println("policyMap = " + policyMap);
            System.out.println("policies = " + policies);
        }

        public int discount(Member member, int price, String discountCode) {
            DiscountPolicy discountPolicy = policyMap.get(discountCode);
            return discountPolicy.discount(member, price);
        }
    }
}

 

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

스프링이 나오고 시간이 갈 수록 자동은 선호하고, 스프링은 Layed pattern에 맞춰서 로직을 자동 스캔할 수 있게 지원한다.

기본은 자동 빈 등록에 Primary를 사용하고 다형성을 적극 활용할 때 수동 빈 등록을 사용하면 좋다. 또한, 이 때는 특정 패키지에 함께 묶어두면 빠르게 이해가 가능하다.

수동 빈 등록은 업무를 업무 로직과 기술 지원 로직으로 나누었을 때 기술 지원 빈(AOP)를 처리할 때 사용하면 좋다.

 

 

빈 생명주기 콜백

스프링은 크게 3가지로 빈 생명주기 콜백을 지원한다.

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

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

3. @PostConstruct, @PreDestroy 애노테이션 지정

 

 

데이터베이스 커넥션 풀이나 네트워크 소켓처럼 시작 시점에 연결해두고 종료 시점에 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.

 

스프링 빈의 이벤트 라이프 사이클:

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

*초기화 콜백: 빈이 생성되고, 빈의 의존관계 주입이 완료된 후 호출

*소멸전 콜백: 빈이 소멸되기 직전에 호출

 

*참고: 객체의 생성과 초기화를 분리해야 한다.

 

 

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

    public NetworkClient(){
        System.out.println("생성자 호출, url = " + url);

    }

    public void setUrl(String url){
        this.url = url;
    }

    public void connet(){
        System.out.println("connet = " + url);
    }

    public void call(String message){
        System.out.println("call = " + url + " message = " + message);
    }

    //서비스 종료시 호출
    public void disconnect(){
        System.out.println("close = " + url);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        connet();
        call("초기화 연결 메시지");
    }

    @Override
    public void destroy() throws Exception {
        disconnect();
    }
}

위에서는 스프링에서 지원하는 초기화, 소멸 메서드를 사용했는데, 이 인터페이스는

1. 스프링 전용 인터페이스로 해당 코드가 스프링 전용 인터페이스에만 의존한다. 

2. 초기화, 소멸 메서드의 이름을 변경할 수 없다.

3. 내가 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

 

인터페이스를 사용하는 초기화, 종료는 스프링 초창기고 지금은 사용하지 않는다.

 

빈 등록 초기화, 소멸 메서드 지정

public class NetworkClient {

    private String url;

    public NetworkClient(){
        System.out.println("생성자 호출, url = " + url);

    }

    public void setUrl(String url){
        this.url = url;
    }

    public void connet(){
        System.out.println("connet = " + url);
    }

    public void call(String message){
        System.out.println("call = " + url + " message = " + message);
    }

    //서비스 종료시 호출
    public void disconnect(){
        System.out.println("close = " + url);
    }

    public void init() {
        System.out.println("NetworkClient.init");
        connet();
        call("초기화 연결 메시지");
    }

    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}

 

public class BeanLifeCycleTest {

    @Test
    public void lifeCycleTest(){
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig{

        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient(){
            NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("httpL//hello-spring.dev");
            return networkClient;
        }
    }
}

설정정보를 사용한 코드를 다음과 같이 적어주면 초기화, 소멸 메서드를 사용할 수 있다.

1. 메서드 이름을 자유롭게 줄 수 있다.

2. 스프링 빈이 스프링 코드에 의존하지 않는다.

3. 코드가 아니라 설정 정보를 사용하기 떄문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 메서드를 적용할 수 있다.

 

종료 메서드 추론

@Bean의 destoyMethod는 추론 기능이 있는데 close, shutdown이라는 종료 메서드를 이용한다. 그래서 스프링 빈으로 등록하면 종료메서드를 따로 적어주지 않아도 잘 동작하고, 추론 기능을 사용하기 싫으면 destroyMethod=""처럼 빈 공백을 지정하면 된다.

 

 

@PostConstruct, @PreDestroy 애노테이션

1. 최신 스프링에서 가장 권장하는 방법

2. 애노테이션 하나만 붙이면 되므로 편리하다.

3. javax, 자바 표준으로 다른 컨테이너에서도 잘 동작한다.

4. 컴포넌트 스캔과 잘 어울린다.

5. 외부 라이브러리에는 적용하지 못한다.

 

 

 

 

 

빈의 스코프

빈의 스코프는 다양하다. 우리가 일반적으로 사용하는 스코프는 싱글톤 스코프다.

1. 싱글톤 스코프: 기본 스코프로 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프

2. 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계까지만 관리

*웹 관련

3. request: 웹 요청이 들어오고 나갈때까지 유지되는 스코프

4. session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프

5. application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

프로토타입 빈과 싱글톤 빈을 함께 사용할 때 생기는 문제점

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께 사용할 때는 의도한대로 잘 동작하지 않으므로 주의해야 한다.

 

예를 들어 한 객체 안에서 생성될 때 마다 addCount를 하는 메서드가 있다면 프로토타입 빈은 매번 새로 생성하므로 계속 1만 반환할 것 같지만 생성시점에 이미 주입된 프로토타입 빈이 반환되기 때문에 증가하는 수가 반환한다.

 

@Autowired
private ApplicationContext ac;

public int logic() {
    PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

가장 쉬운 로직은 다음과 같이 ac.getBean()을 통해서 항상 새로운 프로토타입 빈을 생성하는 것이다. 이는 DI가 아니라 Dependency Lookup(DL)이라고 불린다. 그런데 이런 주입은 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.

 

ObjectFactory, ObjectProvider

지정한 빈을 DL해주는 ObjectProvider가 있는데 기존의 get만 지원해주던 ObjectFactory에 기능을 추가해서 만든 ObjectProvider가 존재한다. ObjectProvider는 옵션, 스트림 처리 등 편의기능이 있다. 하지만 둘 다 스프링에 의존한다는 문제가 있다.

 

JSR-330 Provider

마지막으로 JSR-330을 이용하는 것인데 javax.inject:javax.inject:1을 라이브러리에 추가해줘야 한다.

get() 메서드 하나로 기능이 매우 단순하다. 자바 표준이라 스프링 외부에서도 사용 가능하다. 하지만 라이브러리가 필요하다.

 

결론:

실무에서 대부분 싱글톤으로 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다. 현재 상황에 따라 맞는 것을 사용하면 된다.

 

 

request 스코프 예제 만들기

 

원래 로그를 남기기 위해선 인터셉터를 사용하는 것이 낫지만 여기서는 Controller에 간단하게 로그를 남겨 보려고 한다.

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;

@Component
@Scope(value = "request")
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message){
        System.out.println("[" + uuid + "]" + " [" + requestURL + "] " + message);
    }

    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }

    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}

다음과 같이 로거를 만들고

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    //스프링 컨테이너가 뜨는 시점에 http request가 들어오지 않기 때문에 에러가 뜬다.
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "ok";
    }
}

컨트롤러에 DI를 해준다. 여기서 에러가 뜨게 되는데 MyLogger는 request 스코프를 가지고 있기 때문에 스프링 컨테이너가 생성되는 시점에 DI를 해줄 MyLogger가 존재하지 않기 때문에 아무것도 들어가지 않는다.

 

첫번째 해결 방법은 ObjectProvider를 사용하는 것이다.

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    //스프링 컨테이너가 뜨는 시점에 http request가 들어오지 않기 때문에 에러가 뜬다. 
    private final ObjectProvider<MyLogger> myLoggerProvider;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        MyLogger myLogger = myLoggerProvider.getObject();
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "ok";
    }
}

 

두번째 방법은 프록시를 사용하는 것이다.

MyLogger의 Scope에 proxyMode = ScopedProxyMode.TARGET_CLASS를 넣음으로써 프록시를 사용할 수 있다.

이렇게 하면 가짜 프록시를 만들어서 주입시켜 준다.

 

결록적으로 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이고 이게 다형성과 DI 컨테이너가 가진 강점이다.

'spring' 카테고리의 다른 글

MVC-2 message, validation  (0) 2022.02.11
HTTP 웹 기본지식 - 1  (0) 2022.02.04
스프링 기본편 복습 - 1  (0) 2022.01.23
JPA @Embedded, @Lob, 생성자  (0) 2022.01.15
Optional, JPA N + 1, fetch  (0) 2021.11.26