반응형

깃허브 링크 : https://github.com/minseokLim/wanted-job-search

 

필자는 잠시 휴식 기간을 가지고 이제 다시 구직 활동 중이다. 원티드를 주로 이용하는데, 필터 조건이 너무 마음에 들지 않았다. 나는 Spring + Java or Kotlin 기반의 개발을 해왔고, 다음 회사에서 프레임워크와 언어를 바꾸고 싶은 마음이 아직은 없었다. 근데 원티드에서 제공하는 필터링 기능을 이용해서 검색을 하면, 프론트엔드, PHP, Node.js 공고가 섞여서 나왔다....-_- 그 밖에도 온갖 걸려야할 것들을 일일히 누르면서 거르고 있자니... 너무 단순 반복 작업이고 개발자스럽지 않았다. 그래서 Python selenium을 이용하여 이를 자동화해보기로 했다!!

참고로 필자는 파이썬에 대해 거의 아는 게 없다. 그래서 파이썬 개발자가 보기엔 코드가 매우 우스워 보일 수도 있다;;

그리고 원티드에서 UI를 개편하거나 화면 구조를 살짝 틀어도 제대로 동작하지 않을 수 있다^^;;;

 

사용법은 다음과 같다.

 

1. 필요한 라이브러리 설치 : 터미널에서 다음 명령어를 쳐서 필요한 라이브러리를 다운 받는다. 크롬 기반으로 동작하므로 크롬은 반드시 설치되어 있어야 한다.

python -m pip install --upgrade pip
pip install selenium
pip install webdriver-manager

 

2. 깃의 소스 코드를 클론 받는다.

3. 소스 코드 상의 상수 4개를 자신의 상황에 맞게 변경한다.

  • FILTERED_RECRUITMENT_URL : 원티드에서 제공하는 필터링 기능을 사용한 이후의 URL이다. 필자의 경우, 경력 3년, 개발 > 서버개발자, 자바개발자를 선택하였다.
  • FILTERING_POSITION_REGEX : 필터링할 채용 공고 제목의 정규식이다. 이 정규식에 매칭되는 채용 공고들은 대상에서 제외된다.
  • FILTERING_COMPANY_REGEX : 필터링할 회사명의 정규식이다. 이 정규식에 매칭되는 회사명들은 대상에서 제외된다. 필자의 경우 바로 직전 회사와 면접을 본지 몇 달 지나지 않은 우아한형제들을 제외시켰다.
  • JOB_DESCRIPTION_REGEX : 채용 상세 페이지에서 JD(Job Description)에 반드시 포함되었으면 하는 내용에 대한 정규식이다. 이 정규식에 매칭되는 채용 공고들만 북마크가 된다.

4. 실행 : 터미널에서 아래와 같이 실행한다. 첫번째 인자는 자신의 이메일 주소이고, 두번재 인자는 비밀번호이다.

python main.py test@test.com testpassword

 

5. 기다린다. 꽤 오래걸린다. 각자의 네트워크 상황에 따라 조금씩 다르겠지만 필자의 경우 대략 1시간이 소요되었다...^^;;

 

이제 필터링된 채용 공고들이 나의 북마크에 추가되었다. 근데 북마크된 것만 500건이 넘는구나...-_- 1700건 -> 500건으로 줄었으니 잘한 일이라고 봐야할까...?;; 좀 더 손을 봐야하겠지만, 아무튼 셀레니움을 사용해보는 재밌는 경험이었다.

 

 

※ 참고 : https://greeksharifa.github.io/references/2020/10/30/python-selenium-usage

반응형
반응형

1. 빈 스코프란?

우리가 일반적으로 빈(Bean)이라고 생각하는 것은, 스프링 컨테이너가 시작될 때 함께 생성되고, 스프링 컨테이너가 종료될 때까지 유지되는 것이다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프란 빈이 존재할 수 있는 범위를 뜻한다.

 

스프링은 다음과 같은 다양한 스코프를 지원한다.

1) 싱글톤 : 디폴트 스코프. 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.

2) 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존 관계 주입까지만 관여하고, 더는 관리하지 않는 매우 짧은 범위의 스코프이다. 따라서 종료 메서드는 호출되지 않는다.

3) 웹 관련 스코프

  • request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프이다.
  • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
  • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.

빈 스코프는 아래와 같이 지정할 수 있다.

@Scope("prototype")
@Component
public class NetworkClient {
}

@Configuration
class LifeCycleConfig {
    @Scope("prototype")
    @Bean
    public NetworkClient networkClient() {
        final NetworkClient networkClient = new NetworkClient();
        return networkClient;
    }
}

 

2. 프로토타입 스코프

싱글톤 스코프의 빈을 조회하면, 스프링 컨테이너는 항상 같은 인스턴스를 반환한다. 반면 프로토타입 스코프의 경우, 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다. 또한 프로토타입 스코프의 빈의 경우, 생성된 이후에는 스프링 컨테이너에 의해 관리되지 않는다는 점도 싱글톤 스코프와의 차이점이다. 따라서 종료 메서드는 호출되지 않는다.

아래 코드를 보면, 프로토타입 스코프 빈의 경우 빈을 조회할 때마다 빈의 생성과 의존 관계 주입, 초기화가 이뤄진다는 것을 확인할 수 있고, 스프링 컨테이너가 종료될 때도 destroy 메서드는 호출되지 않는 것을 볼 수 있다.

class PrototypeTest {
    @Test
    void prototypeBeanFind() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        final PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        final PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close();
    }

    @Scope("prototype")
    static class PrototypeBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }
}

 

3. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

앞서 말했듯이 프로토타입 스코프의 빈은 해당 타입의 빈이 스프링 컨테이너에 요청될 때마다 생성된다. 헌데 싱글톤 스코프의 빈이 프로토타입의 빈을 주입 받는다면, 싱글톤 스코프의 빈이 생성되고 의존 관계가 주입되는 시점에만 프로토타입 빈이 조회될 것이고, 이후에는 계속 같은 빈이 사용될 것이다. 대부분의 경우, 개발자는 이를 의도하지 않았을 것이다. 애초에 매번 새로운 객체가 필요하기 때문에 프로토 타입으로 선언했을 것이기 때문이다.

따라서 아래와 같은 테스트 코드를 실행한다면, 테스트는 실패할 것이다.

class SingletonWithPrototypeTest1 {
    @Test
    void singletonClientUserPrototype() {
        final AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.class);
        final ClientBean clientBean1 = ac.getBean(ClientBean.class);
        final int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        final ClientBean clientBean2 = ac.getBean(ClientBean.class);
        final int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(1);

        ac.close();
    }

    @Scope("singleton")
    static class ClientBean {
        private final PrototypeBean prototypeBean; // 생성 시점에 주입

        public ClientBean(final PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public int logic() {
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init " + this);
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

 

4. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

가장 간단한 방법은, 프로토타입의 의존 관계를 주입받지 않고 ApplicationContext에 매번 빈을 아래와 같이 요청하는 것이다. 하지만 이렇게 되면, 스프링에 종속적인 코드가 되고 단위테스트도 어려워 진다.

@Scope("singleton")
static class ClientBean {
    @Autowired
    private ApplicationContext ac;

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

 

1) ObjectFactory, ObjectProvider

ObjectProvider는 지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup) 서비스를 제공한다. 원래는 ObjectFactory만 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다. 아래와 같이 사용할 수 있다.

@Scope("singleton")
static class ClientBean {
    private final ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public ClientBean(final ObjectProvider<PrototypeBean> prototypeBeanProvider) {
        this.prototypeBeanProvider = prototypeBeanProvider;
    }

    public int logic() {
        final PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

 

특징

1) 스프링이 제공하는 기능을 사용하긴 하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.

2) 딱 필요한 DL(Dependency Lookup) 기능만 제공한다.

 

2) JSR-330 Provider

이 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다. 이 방법을 사용하기 위해선 javax.inject:javax.inject:1 라이브러리를 gradle에 별도로 추가해야 한다. 아래와 같이 사용할 수 있다.

@Scope("singleton")
static class ClientBean {
    private final Provider<PrototypeBean> prototypeBeanProvider;

    ClientBean(final Provider<PrototypeBean> prototypeBeanProvider) {
        this.prototypeBeanProvider = prototypeBeanProvider;
    }

    public int logic() {
        final PrototypeBean prototypeBean = prototypeBeanProvider.get();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

 

특징

1) 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

2) 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기 쉬워진다.

3) 딱 필요한 DL(Dependency Lookup) 기능만 제공한다.

4) 별도의 라이브러리가 필요하다.

 

5. 웹 스코프

웹 스코프는 웹 환경에서만 동작한다. 웹 스코프는 프로토타입과는 다르게, 스프링 컨테이너가 해당 스코프의 종료 시점까지 관리를 한다. 따라서 종료 메서드가 호출된다.

 

웹 스코프 종류

1) request : HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.

2) session : HTTP session 과 동일한 생명 주기를 가지는 스코프

3) application : 서블릿 컨텍스트과 동일한 생명 주기를 가지는 스코프

4) websocket : 웹 소켓과 동일한 생명 주기를 가지는 스코프

 

6. request 스코프 예제 만들기

 request 스코프의 경우, HTTP 요청이 들어왔을 때마다 빈이 생성되고 요청이 끝날 때까지 유지가 된다. 즉 요청이 들어왔을 때 생성이 되기 때문에, 일반적인 생성자/수정자 주입 방식 등으로 의존 관계를 주입하려고 하면 에러가 발생한다.

 

1) ObjectProvider를 사용

아래에서 하나의 HTTP 요청에서는 같은 빈 인스턴스를 공유한다는 것을 확인할 수 있다.

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

    private String uuid;
    private String requestUrl;

    public void setRequestUrl(final 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 created : " + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean closed  : " + this);
    }
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;

    @RequestMapping("/log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) throws InterruptedException {
        final String requestURL = request.getRequestURL().toString();
        final MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestUrl(requestURL);
        myLogger.log("controller test");
        Thread.sleep(1000);
        logDemoService.logic("testId");

        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerProvider;

    public void logic(final String id) {
        final MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " + id);
    }
}

 

2) 프록시 사용

proxyMode를 사용하면 마치 싱글톤 스코프 빈을 주입받는 것처럼 코드를 간결하게 만들 수 있다. 적용 대상이 클래스라면 proxyMode = ScopedProxyMode.TARGET_CLASS, 인터페이스라면 proxyMode = ScopedProxyMode.INTERFACES 를 선택하면 된다.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {

    private String uuid;
    private String requestUrl;

    public void setRequestUrl(final 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 created : " + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean closed  : " + this);
    }
}
@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("/log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) throws InterruptedException {
        final String requestURL = request.getRequestURL().toString();
        myLogger.setRequestUrl(requestURL);
        myLogger.log("controller test");

        System.out.println("myLogger = " + myLogger);

        Thread.sleep(1000);
        logDemoService.logic("testId");

        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

    public void logic(final String id) {
        myLogger.log("service id = " + id);
    }
}

 

프록시 모드를 사용하면, 스프링은 CGLIB이라는 라이브러리를 이용하여, 내 클래스를 상속 받은 프록시 객체를 만들어서 주입한다. 따라서 스프링 컨테이너가 생성되고 빈의 의존 관계를 주입할 때, 프록시 객체가 주입되는 것이다.

가짜 프록시 객체는, HTTP 요청이 들어왔을 때 내부에서 진짜 빈을 요청하는 위임 로직을 가지고 있다. 프록시 객체는 원본 클래스를 상속해서 만들어졌기 때문에, 이 객체를 사용하는 클라이언트 입장에서는 원본인지 아닌지 모르게 동일하게 사용할 수 있다.

 

주의점

1) 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 주의해서 사용해야 한다.

2) 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자. 무분별하게 사용하면 유지보수하기 어려워진다.

 

반응형
반응형

1. 빈 생명주기 콜백

데이터베이스 커넥션 풀이나, 네트워크 소켓과 같은 것들은 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료할 필요가 있다. 아래의 NetworkClient라는 클래스에서 connect()를 통해 미리 연결을 하고, disconnect()를 통해 연결을 종료한다고 가정해보자.

public class NetworkClient {

    private String url;

    public NetworkClient() {
        System.out.println("생성자 호출, url = " + url);
        connect();
        call("초기화 연결 메세지");
    }

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

    // 서비스 시작 시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

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

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

 

NetworkClient를 통해 아래와 같은 테스트 코드를 작성하여 실행해보면, url이 모두 null이 나오는 걸 확인할 수 있다. 이는 사실 당연한 결과이다. setUrl을 통해 url을 지정하기 때문에, 객체 생성 시점에는 url이 null이기 때문이다.

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

    @Configuration
    static class LifeCycleConfig {
        @Bean
        public NetworkClient networkClient() {
            final NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello.dev");
            return networkClient;
        }
    }
}

 

스프링 빈은 다음과 같은 순서로 생성이 된다.

스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존 관계 주입

 

스프링 빈은 객체가 생성되고 의존 관계가 모두 주입이 된 이후에야 필요한 데이터를 사용할 준비가 완료된다. 따라서 초기화 작업은 의존 관계 주입이 모두 끝난 이후에 호출되어야 한다. 그런데 개발자는 의존 관계 주입이 끝난 시점을 어떻게 알 수 있을까?

스프링은 의존관계 주입이 완료되면, 스프링 빈에게 콜백 메서드를 통해 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링 컨테이너가 종료되기 직전에 대한 소멸 콜백 기능도 제공한다. 따라서 개발자는 이를 이용하여 초기화와 소멸을 구현할 수 있다. 이를 포함한 스프링 빈의 라이프 사이클은 다음과 같다

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

 

* 참고 : 객체의 생성과 초기화를 분리하자.

사실 생성자를 통한 의존 관계 주입을 이용하면, 스프링 빈이 생성되면서 의존 관계도 같이 주입이 되기 때문에, 이 때 초기화를 할 수도 있다. 근데 만약 초기화 작업에 외부 커넥션 연결 등과 같은 무거운 동작이 포함되어 있다면, 생성자가 너무 많은 책임을 가지게 된다. 이는 SRP에 위배되고, 이보다는 객체의 생성과 초기화 부분을 명확하게 분리하는 것이 유지 보수 관점에서 좋다. 물론 내부 값들만 약간 변경하는 단순한 초기화의 경우, 생성자에서 한 번에 처리하는 게 나을 수도 있다.

 

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

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

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

3) @PostConstruct, @PreDestroy 애노테이션 지원

 

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

아래와 같이 NetworkClient 클래스가 InitializingBean, DisposableBean 인터페이스를 구현하도록 한 후, afterPropertiesSet()에서 초기화를, destroy()에서 소멸 로직을 넣어주면 된다.

public class NetworkClient implements InitializingBean, DisposableBean {

    private String url;

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

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

    // 서비스 시작 시 호출
    public void connect() {
        System.out.println("connect: " + 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() {
        System.out.println("NetworkClient.afterPropertiesSet");
        connect();
        call("초기화 연결 메세지");
    }

    @Override
    public void destroy() {
        System.out.println("NetworkClient.destroy");
        disconnect();
    }
}
class BeanLifeCycleTest {
    @Test
    void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        final NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig {
        @Bean
        public NetworkClient networkClient() {
            final NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello.dev");
            return networkClient;
        }
    }
}

 

인터페이스 방식의 단점

1) 이 인터페이스들은 스프링 전용 인터페이스이다. 해당 코드가 스프링 전용 인터페이스에 의존하게 된다. 물론 스프링으로 개발하면서 아예 스프링에 의존적이지 않을 수는 없겠지만 인터페이스까지 의존하는 건 좀 부담된다.

2) 초기화, 종료 메서드 명을 변경할 수 없다.

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

 

따라서 이 방법들은 지금은 거의 사용되지 않는다고 한다.

 

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

설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")와 같이 초기화, 종료 메서드를 지정해줄 수 있다.

public class NetworkClient {

    private String url;

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

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

    // 서비스 시작 시 호출
    public void connect() {
        System.out.println("connect: " + 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");
        connect();
        call("초기화 연결 메세지");
    }

    public void close() {
        System.out.println("NetworkClient.close");
        disconnect();
    }
}
class BeanLifeCycleTest {
    @Test
    void lifeCycleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(LifeCycleConfig.class);
        final NetworkClient client = ac.getBean(NetworkClient.class);
        ac.close();
    }

    @Configuration
    static class LifeCycleConfig {
        @Bean(initMethod = "init", destroyMethod = "close")
        public NetworkClient networkClient() {
            final NetworkClient networkClient = new NetworkClient();
            networkClient.setUrl("http://hello.dev");
            return networkClient;
        }
    }
}

 

설정 정보 방식의 특징

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

2) 스프링 빈 클래스가 스프링 코드에 의존하지 않는다.

3) 코드가 아닌 설정 정보를 이용하기 때문에, 코드를 고칠 수 없는 외부 라이브러리에도 적용이 가능하다.

 

종료 메서드 추론

@Bean의 destroyMethod 속성에는 특별한 기능이 있는데, 바로 종료 메서드 추론 기능이다. 라이브러리는 대부분 close()나 shutdown()을 종료 메서드로서 가진다. 따라서 특별히 종료 메서드를 지정해주지 않으면, 알아서 close나 shutdown이라는 메서드를 자동으로 호출해준다. 참고로 destroyMethod의 디폴트 값은 (inferred)로 등록되어 있다.

 

4. @PostConstruct, @PreDestroy 애노테이션 지원

초기화 메서드와 종료 메서드 앞에 각각 @PostConstruct, @PreDestroy를 붙여주면 되는 아주 간단한 방법이다.

public class NetworkClient {

    private String url;

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

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

    // 서비스 시작 시 호출
    public void connect() {
        System.out.println("connect: " + url);
    }

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

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

    @PostConstruct
    public void init() {
        System.out.println("NetworkClient.init");
        connect();
        call("초기화 연결 메세지");
    }

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

 

애노테이션 방식의 특징

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

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

3) 패키지를 보면, 'javax.annotation.PostConstruct'이다. 이는 스프링에 종속적인 기술이 아니라 JSR-250이라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 잘 동작한다.

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

5) 유일한 단점은 외부 라이브러리에는 적용할 수 없다는 것이다. 따라서 외부 라이브러리의 경우, @Bean의 기능을 사용하자.

 

5. 정리

  • @PostConstruct, @PreDestroy 애노테이션을 사용하자
  • 코드를 고칠 수 없는 외부 라이브러리에 대한 경우에는 @Bean의 initMethod, destroyMethod를 사용하자.
반응형
반응형

너무 잘 정리해놓은 페이지가 있어서 기록용으로 남겨놓는다.

https://antennagom.com/1020

 

노션 페이지 자신의 도메인으로 연결하는 방법 (2021년 11월 기준)

노션 페이지를 자신의 도메인으로 연결하는 방법 - 에버노트 대신 노션으로 갈아타서 주요 노트앱으로 사용하시는 분들이 많은데요. 에버노트와는 다른 노션만의 장점이나 기능이 있죠. 메모장

antennagom.com

 

반응형
반응형

1. 다양한 의존관계 주입 방법

 의존 관계 주입에는 크게 4가지 방법이 있다.

1) 생성자 주입

 생성자를 통해서 의존 관계를 주입하는 방법이다. 이것은 생성자 호출 시점에 딱 한 번만 주입되는 것이 보장 된다. 따라서 이는 불변 의존 관계를 보장할 수 있다(단, setter가 없어야 한다.). 생성자에 파라미터가 넘어와야하기 때문에 필수 의존 관계에도 사용된다.

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired // 생성자가 1개일 때는 Autowired 생략 가능
    public MemberServiceImpl(final MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

2) 수정자 주입(setter 주입)

 setter를 통해 의존 관계를 주입하는 방법이다. 의존 관계를 런타임에 선택하거나 변경해야하는 상황일 때 사용된다.

@Component
public class MemberServiceImpl implements MemberService {

    private MemberRepository memberRepository;

    @Autowired
    public void setMemberRepository(final MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

 

3) 필드 주입

 필드에 그대로 의존 관계를 주입하는 방법이다. 이전에는 많이 사용되던 방법이나, 스프링 없이 테스트 코드를 작성할 때 의존 관계를 설정해줄 수 없다는 단점이 있다. 그래도 요즘은 안티 패턴으로 여겨진다. 단 테스트 코드에서는 사용해도 된다.

@Component
public class MemberServiceImpl implements MemberService {

    @Autowired
    private final MemberRepository memberRepository;
}

 

4) 일반 메서드 주입

 일반 메서드를 통해서 의존 관계를 주입하는 방법이다. 일반적으로는 거의 사용되지 않는다.

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Autowired
    public void init(MemoryMemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

2. 옵션 처리

 주입할 스프링 빈이 없어도 동작해야 할 때가 있다. 그런데 @Autowired만 사용하면, required 옵션의 기본값이 true이기 때문에 오류가 발생한다. 자동으로 의존 관계를 주입할 대상을 옵션으로 처리할 수 있는 방법은 다음과 같다.

 

 1) @Autowired(required = false) : 자동 주입할 대상이 없으면, 수정자 메서드가 호출되지 않음

 2) org.springframework.lang.@Nullable : 자동 주입할 대상이 없으면, null이 입력된다.

 3) Optional<> : 자동 주입할 대상이 없으면, Optional.empty()가 입력된다.

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

    static class TestBean {
        @Autowired(required = false) // Member 타입의 빈이 없을 경우, 아예 호출이 안됨
        public void setNoBean1(Member noBean1) { 
            System.out.println("noBean1 = " + noBean1);
        }

        @Autowired // Member 타입의 빈이 없을 경우, null이 들어감
        public void setNoBean2(@Nullable Member noBean2) {
            System.out.println("noBean2 = " + noBean2);
        }

        @Autowired // Member 타입의 빈이 없을 경우, Optional.empty()가 들어감
        public void setNoBean3(@Nullable Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }
    }
}

 

3. 생성자 주입을 선택해라!

 과거에는 setter 주입과 필드 주입도 많이 사용하였지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 그 이유는 다음과 같다.

 

1) 불변

  • 대부분의 의존 관계 주입은 한 번 일어나면, 애플리케이션 종료 시점까지 의존 관계를 변경할 일이 없다. 오히려 대부분의 의존 관계는 애플리케이션 종료 전까지 변하면 안된다.
  • 수정자 주입을 사용하면, setXxx 메서드를 public으로 열어둬야하는데, 누군가 실수로 변경할 수도 있기 때문에 이는 좋은 설계가 아니다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로, 이후에 호출될 일은 없다. 따라서 불변하게 설계할 수 있다.

2) 누락

 만약 아래와 같이 수정자 주입을 사용한다면, 테스트 코드를 작성할 때 의존 관계를 누락시킬 가능성이 있다. 하지만 생성자를 통한 주입을 사용하여 기본 생성자를 제공하지 않는다면, 의존 관계를 누락시킬 수가 없다.

@Component
public class OrderServiceImpl implements OrderService {

    private MemberRepository memberRepository;
    private DiscountPolicy discountPolicy;

    @Override
    public Order createOrder(final Long memberId, final String itemName, final int itemPrice) {
        final Member member = memberRepository.findById(memberId);
        final int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }

    @Autowired
    public void setMemberRepository(final MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(final DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
}
class OrderServiceImplTest {

    @Test
    void createOrder() {
        OrderServiceImpl orderService = new OrderServiceImpl();
        orderService.createOrder(1L, "itemA", 10000);
    }
}

3) final 키워드

 생성자 주입을 사용하면, 필드에 final 키워드를 사용할 수 있다. 이를 통해 의존 관계가 바뀌지 않음을 보장할 수 있고, 혹시라도 값이 설정되지 않는 오류를 컴파일 시점에 막아줄 수 있다.

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

4) 정리

 항상 생성자 주입을 선택하자. 그리고 가끔 옵션이 필요하면 수정자 주입을 사용하자. 필드 주입은 사용하지 않는 게 좋다.

 

4. 같은 타입의 빈이 2개 이상일 때

 @Autowired는 타입에 의해 빈을 조회한다. 근데 만약 아래와 같이 타입의 빈이 2개 이상일 때는 어떻게 해야할까?

하위 타입으로 지정해서 빈을 주입할 수도 있겠지만 이는 DIP를 위배하고 유연성이 떨어진다. 또한 완전히 같은 타입의 2개 이상의 빈이 있는 경우도 있을 것이다.

@Component
public class FixDiscountPolicy implements DiscountPolicy {
}

@Component
public class RateDiscountPolicy implements DiscountPolicy {
}

 

해결 방법은 다음과 같다.

 

1) @Autowired 필드 명 매칭

 @Autowired는 타입 매칭을 시도하고, 이때 여러 개의 빈이 있을 경우, 필드 명, 파라미터 명으로 빈 이름을 추가 매칭한다. 아래와 같이 필드명 혹은 파라미터 명을 빈 이름과 같게해줄 경우, 그 빈을 주입한다.

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(final MemberRepository memberRepository, final DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }
}

 

 

2) @Qualifier 사용

@Qualifier 는 추가 구분자를 붙여주는 방법이다. 빈 이름이 변경되지는 않는다.

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
}

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
}

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(final MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") final DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
}

 

3) @Primary 사용

@Primary는 우선 순위를 정하는 방법이다. @Autowired에 의해 여러 빈이 매칭이 되면, @Primary 를 갖는 빈이 우선권을 갖는다.

@Component
public class FixDiscountPolicy implements DiscountPolicy {
}

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
}

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

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

 

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

 같은 타입의 빈이 모두 필요할 때는 아래와 같이 List 또는 Map으로 모든 빈들을 주입받을 수 있다. Map의 경우, key는 빈의 이름이다.

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

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

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

 

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

 그러면 어떤 경우에 컴포넌트 스캔을 활용한 자동 주입을 사용하고, 어떤 경우에 별도의 설정 정보를 통해 수동으로 빈을 등록하고, 의존 관계도 수동으로 주입해야 할까?

 스프링이 나오고 시간이 갈 수록 점점 자동을 선호하는 추세다. 수동으로 설정 정보를 관리하면 매번 @Bean을 적고, 객체를 생성하고, 주입할 대상을 적어줘야하는 번거로운 작업을 해줘야한다. 그렇다면 언제 수동 빈 등록을 사용하는 것이 좋을까?

 애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.

  • 업무 로직 빈 : 웹을 지원하는 컨트롤러, 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다. 이 로직들은 숫자도 매우 많고 대부분 유사한 패턴으로 개발된다. 따라서 자동 기능을 적극 사용하는 것이 좋다.
  • 기술 지원 빈 : 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리와 같이 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들에 대한 것이다. 이들은 업무 로직에 비해 그 수가 매우 적고, 애플리케이션 전반에 걸쳐 광범위하게 영향을 미치는 경우가 많다. 따라서 이런 기술 지원 로직 빈들의 경우, 가급적 수동으로 빈을 등록해서 명확하게 드러내는 것이 좋다.

 업무 로직 빈이라고 하더라도, 다형성을 적극 활용하는 경우 수동으로 빈을 등록하고 관리하는 것이 더 유리할 수도 있다. 아래와 같이 관리하면, 한 눈에 관련 빈들을 확인할 수 있기 때문이다.

@Configuration
public class DiscountConfig {
    @Bean
    public DiscountPolicy rateDiscountPolicy() {
        return new RateDiscountPolicy();
    }

    @Bean
    public DiscountPolicy fixDiscountPolicy() {
        return new FixDiscountPolicy();
    }
}

 

반응형
반응형

1. 컴포넌트 스캔

 이전에 스프링 빈을 등록할 때는, 자바 코드의 @Bean이나 XML의 <bean> 등을 통해 설정 정보에 등록할 스프링 빈들을 나열해야했다. 만약 이렇게 등록해야할 스프링 빈의 갯수가 많아진다면 매우 번거로운 작업이 될 것이며, 누락되는 일도 생길 것이다. 그래서 스프링은 별도의 설정 정보 없이 자동으로 스프링 빈을 등록해주는 컴포넌트 스캔이라는 기능을 제공한다. 컴포넌트 스캔은 @Component 어노테이션이 붙은 클래스를 찾아 스프링 컨테이너에 빈으로 등록해주는 기능을 말한다.

 이때, 설정 정보에서 일일히 해주던 의존 관계 주입은, @Autowired라는 어노테이션을 통해 스프링에 위임하게 된다. 스프링 컨테이너에 해당 타입의 빈이 존재한다면, 이를 주입해주는 것이다.

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

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

    @Override
    public void join(final Member member) {
        memberRepository.save(member);
    }
}
@Configuration
@ComponentScan
public class AutoAppConfig {
}
class AutoAppConfigTest {
    @Test
    void basicScan() {
        final AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        final MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

2. 컴포넌트 스캔 탐색 위치

 컴포넌트 스캔을 할 패키지를 아래와 같이 지정할 수 있다. 특별히 지정하지 않는다면, 디폴트는 @ComponentScan 어노테이션이 붙은 설정 정보 클래스가 있는 패키지가 탐색할 패키지의 시작 위치가 된다.

 일반적으로는 특별히 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이 좋다. 프로젝트의 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문이다. 참고로 스프링 이니셜라이저를 통해 스프링 부트 프로젝트를 생성하면, @SpringBootApplication이 붙은 클래스가 프로젝트 최상단에 생성되는데, 이 어노테이션 내에는 @ComponentScan이 포함되어 있다.

@Configuration
@ComponentScan(basePackages = "hello.core")
public class AutoAppConfig {
}

 

3. 컴포넌트 스캔 기본 대상

  • @Component
  • @Controller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 서비스 레이어에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용

@Component를 제외한 나머지 어노테이션들의 소스 코드를 보면, @Component 어노테이션을 포함하고 있는 것을 확인할 수 있다.

 

* 참고 : 사실 어노테이션에 상속 관계라는 것은 없다. 따라서 어떤 어노테이션이 다른 어노테이션을 포함한다고 해서 그 어노테이션을 인식할 수 있는 것은, 자바 언어가 지원하는 기능은 아니다. 이것은 스프링이 지원하는 기능이다.

 

 위에서 컴포넌트 스캔의 대상이 되었던 어노테이션들은 이 용도 외에도 다른 스프링의 부가 기능을 수행한다.

  • @Controller : 스프링 MVC 컨트롤러로 인식
  • @Repository : 스프링 데이터 접근 계층으로 인식. 데이터 계층에서 발생하는 예외를 스프링 예외로 변환해준다. 왜냐하면, 만약 사용하는 데이터 구현체가 변경되었을 때 예외 종류까지 변경된다면, 이전에 처리해놓았던 예외 처리 또한 모두 변경되어야하기 때문이다. 이를 스프링 예외로 한 번 감싸줌으로써, 구현체에 종속적이지 않을 수 있도록 해준다. 
  • @Configuration : 스프링 설정 정보로 인식. 스프링 빈이 싱글톤으로 유지될 수 있도록 추가적인 처리를 한다.
  • @Service : 사실 특별한 처리를 하는 것은 없다. 다만 개발자들이 여기가 비즈니스 계층이라는 것을 인지하는데 도움을 준다.

 

4. 중복 등록과 충돌

 이름이 같은 빈들이 등록되면 어떻게 될까? 다음 두 가지 상황이 있을 수 있다.

1) 컴포넌트 스캔에 의한 등록 vs 컴포넌트 스캔에 의한 등록 : ConflictingBeanDefinitionException 예외가 발생한다.

@Component("discountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
}

@Component("discountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
}

 

2) @Bean에 의한 등록 vs 컴포넌트 스캔에 의한 등록 : 이 경우에는 @Bean에 의한 등록이 우선권을 가진다. 즉, @Bean에 의한 빈이 컴포넌트 스캔에 의한 빈을 오버라이딩한다. 물론 개발자가 이를 의도적으로 의도했다면 상관이 없지만, 현실은 여러 설정들이 꼬여서 이런 결과가 만들어지는 경우가 대부분이다. 이럴 경우 정말 잡기 어려운 버그가 만들어진다. 그래서 최근 스프링 부트에서는 이 경우에도 빈 등록에 충돌이 일어나면 오류가 발생하도록 기본 값을 변경하였다. 만약 오버라이딩을 하도록 변경하고 싶다면, spring.main.allow-bean-definition-overriding=true 로 설정하면 된다.

@Configuration
@ComponentScan
public class AutoAppConfig {
    @Bean(name = "memoryMemberRepository")
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}

class AutoAppConfigTest {
    @Test
    void basicScan() {
        final AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        final MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

반응형
반응형

1. 웹 애플리케이션과 싱글톤

 웹 애플리케이션은 보통 여러 고객이 동시에 요청을 한다. 이전에 만들었던 스프링을 사용하지 않는 순수한 DI 컨테이너인 AppConfig은 요청을 할 때마다 객체를 새로 생성한다.

@Test
void pureContainer() {
    AppConfig appConfig = new AppConfig();

    final MemberService memberService1 = appConfig.memberService();
    final MemberService memberService2 = appConfig.memberService();

    assertThat(memberService1).isNotSameAs(memberService2);
}
public class AppConfig {
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }
}

 

 해결 방안은, 해당 객체가 딱 1개만 생성되고 이를 공유하도록 설계하면 된다. 이것이 싱글톤 패턴이다.

 

2. 싱글톤 패턴

  싱글톤 패턴이란, 하나의 JVM 안에서 한 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다. 인스턴스가 2개 이상 생성되지 못하도록 막는 방법은 다음과 같다.

 1) static 영역에 객체를 미리 하나 생성해 놓는다.

 2) 이 객체를 사용하려면 getInstance() 메서드를 통해서만 조회할 수 있다. 이 메서드는 항상 같은 객체를 반환한다.

 3) 생성자를 private으로 만들어서 추가적인 객체가 생성되는 것을 막는다. 

public class SingletonService {

    private static final SingletonService instance = new SingletonService();

    private SingletonService() {
    }

    public static SingletonService getInstance() {
        return instance;
    }
}

 

 싱글톤 패턴을 적용하면, 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 문제점들을 가지고 있다.

 1) 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.

 2) 클라이언트가 구체 클래스에 의존한다. -> DIP 위반

 3) 클라이언트가 구체 클래스에 의존하기 때문에 OCP도 위반할 가능성이 높다.

 4) 테스트하기 어렵다. 

 5) 내부 속성을 변경하거나 초기화하기 어렵다.

 6) private 생성자로 인해 자식 클래스를 만들기 어렵다.

 7) 결론적으로 유연성이 떨어진다.

 8) 때문에 안티 패턴으로 불리기도 한다.

 

3. 싱글톤 컨테이너

 스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다. 스프링의 빈(Bean)들이 모두 싱글톤으로 관리된다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다. 스프링 컨테이너의 이러한 기능 덕분에 싱글톤 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다. 아래의 테스트 코드를 보면, ApplicationContext에서 빈을 여러번 호출하더라도 동일한 객체가 호출됨을 알 수 있다.

@Test
void springContainer() {
    final ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    final MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    final MemberService memberService2 = ac.getBean("memberService", MemberService.class);

    assertThat(memberService1).isSameAs(memberService2);
}

 

* 참고 : 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.(Bean Scope)

 

4. 싱글톤 방식의 주의점

 객체를 하나만 생성해서 공유하는 싱글톤 방식은, 여러 클라이언트가 하나의 객체를 공유하기 때문에, 객체 상태를 유지(stateful)하게 설계하면 안된다. 즉, 무상태(stateless)로 설계해야 한다. 무상태로 설계하는 방법은 다음과 같다.

 1) 특정 클라이언트에 의존적인 필드가 있어선 안된다.

 2) 특정 클라이언트가 값을 변경할 수 있는 필드가 있어선 안된다.

 3) 가급적 읽기만 가능해야 한다.

 4) 필드를 사용하는 대신, 자바에서 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

 

5. @Configuration과 싱글톤

 아래의 코드를 보면, MemberService 빈이 생성될 때 memberRepository() 메서드가 호출되어 new MemoryMemberRepository()가 호출된다. 마찬가지로 OrderService 빈이 생성될 때도 memberRepository() 메서드가 호출되어 new MemoryMemberRepository()가 호출된다. 결과적으로 MemoryMemberRepository 객체가 2개 생성되어 싱글톤이 깨진 것처럼 보인다. 

@Configuration
public class AppConfig {
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    
    @Bean
    public DiscountPolicy discountPolicy() {
        System.out.println("AppConfig.discountPolicy");
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService() {
        System.out.println("AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        System.out.println("AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

 

 하지만 막상 아래의 테스트 코드를 돌려 보면, 싱글톤이 보장된다는 것을 알 수 있다. 그리고 콘솔창을 확인하면, memberRepository() 메서드는 한 번만 호출되었다는 것을 확인할 수 있다.

class ConfigurationSingletonTest {
    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        final MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        final OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        final MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        assertThat(memberRepository).isSameAs(orderService.getMemberRepository());
        assertThat(memberRepository).isSameAs(memberService.getMemberRepository());
    }
}

 

 

6. @Configuration과 바이트코드 조작의 마법

 AppConfig의 자바 코드를 보면, memberRepository() 메서드는 분명 3번 호출되는 것이 맞다. 하지만 스프링은 싱글톤을 보장해야하기 때문에, 바이트코드를 조작하는 라이브러리를 사용한다. 아래의 테스트 코드를 실행해보자. 만약 AppConfig이 순수한 클래스라면 AppConfig의 클래스명만 나와야 한다. 하지만 결과를 보면 그렇지 않고 xxxCGLIB가 붙은 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서, AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스의 객체를 빈으로 등록한 것이다.

class ConfigurationSingletonTest {
    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        final AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
    }
}

 

 AppConfig@CGLIB의 예상코드는 아래와 같다. @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면, 존재하는 빈을 반환하고, 없으면 기존 로직을 호출해서 스프링 컨테이너에 등록 후, 반환하는 코드가 동적으로 만들어진 것이다. 이를 통해 스프링은 싱글톤을 보장할 수 있다.

@Bean
public MemberRepository memberRepository() {
	if (memberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
    	return 스프링 컨테이너에서 찾아서 반환;
    } else {
    	기존 로직을 호출해서 MemoryMemberRepository객체를 생성하고 스프링 컨테이너에 등록
        return 반환;
    }
}

 

7. @Configuration을 빼면 어떻게 될까?

 만약 AppConfig의 @Configuration을 빼면 어떻게 될까? AppConfig의 @Configuration을 주석 처리하고 아래 테스트 코드를 다시 실행해보면, Bean 등록은 이전과 동일하게 되지만 싱글톤이 보장이 되지 않는다는 것을 확인할 수 있다.(memberRepository 메서드가 3번 호출되고 있음)

class ConfigurationSingletonTest {
    @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        final AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass());
    }
}

 

 따라서 스프링 설정 정보에 대한 클래스에는 @Configuration를 사용하도록 한다.

반응형
반응형

1. 스프링 컨테이너

1) 스프링 컨테이너 생성

 아래와 같이 스프링 컨테이너를 생성할 수 있다. ApplicationContext를 스프링 컨테이너라고 한다. 이는 인터페이스인데, 이는 다양한 구현 클래스가 있다는 것을 의미한다. 아래에는 어노테이션 기반인 AnnotationConfigApplicationContext 클래스를 사용하였으나 XML 기반의 컨테이너를 사용할 수도 있다.

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

 

2) 스프링 컨테이너 생성 과정

 1) 스프링 컨테이너 생성 

 스프링 컨테이너 내에는 빈(Bean) 저장소가 있다. 빈 저장소는 빈 이름을 Key로 가지고 빈 객체를 값으로 가지는 저장소이다.

 2) 스프링 빈 등록

 스프링 컨테이너는 AppConfig과 같은 구성 정보를 바탕으로 빈을 생성하여 빈 저장소를 구성한다.

 3) 스프링 빈 의존 관계 설정

 스프링 컨테이너는 구성 정보를 참고해서 의존 관계를 주입(DI)한다.

 

2. 스프링 빈 조회

 스프링 컨테이너에서 부모 타입으로 빈을 조회하면, 자식 타입도 함께 조회된다. 따라서 만약 모든 자바 객체의 부모인 Object 타입으로 조회를 한다면, 모든 스프링 빈이 조회될 것이다.

class ApplicationContextExtendsFindTest {
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    @Test
    void findAllBeanByObjectType() {
        final Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
        for (String key : beansOfType.keySet()) {
            System.out.println("key = " + key + " value = " + beansOfType.get(key));
        }
    }

    @Configuration
    static class TestConfig {
    }
}

 

3. BeanFactory와 ApplicationContext

1) BeanFactory

 BeanFactory는 스프링 컨테이너의 최상위 인터페이스이다. 스프링 빈을 관리하고 조회하는 역할을 담당한다.

2) ApplicationContext

 BeanFactory의 기능을 모두 상속받아서 제공하는 인터페이스이다. ApplicationContext는 빈 관련 기능 이 외에도 다양한 부가 기능을 제공한다. 

 1) 메시지 소스를 활용한 국제화 기능(MessageSource)

 2) 환경 변수(EnvironmentCapable) : 로컬, 개발, 운영 등을 구분해서 처리

 3) 애플리케이션 이벤트(ApplicationEventPublisher) : 이벤트를 발행하고 구독하는 모델을 지원

 4) 편리한 리소스 조회(ResourceLoader) : 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

 

BeanFactory를 직접 사용할 일은 거의 없다. 부가 기능이 포함된 ApplicationContext를 사용한다.

 

4. 스프링 빈 설정 메타 정보 - BeanDefinition

 앞서 말했듯이 ApplicationContext는 인터페이스이고 이를 구현한 다양한 클래스가 있다. 예를 들어 어노테이션 기반으로 구성 정보를 제공하는 AnnotationConfigApplicationContext가 있고, xml로 구성 정보를 제공하는 GenericXmlApplicationContext가 있다. 이렇듯 스프링이 다양한 설정 형식을 지원하는 것을 가능하게 해주는데에는 BeanDefinition이라는 추상화가 있다. 스프링 컨테이너는 자바 코드인지 xml인지 몰라도 된다. 오직 BeanDefinition만 알면 된다. 스프링 컨테이너는 BeanDefinition 정보를 기반으로 빈을 생성한다. 이는 역할과 구현을 분리해야한다는 객체 지향적 설계 원칙을 따른 것이다.

1) BeanDefinition 정보

  • BeanClassName : 생성할 빈의 클래스 명(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName : 팩토리 역할의 빈을 사용할 경우 이름, 예) appConfig
  • factoryMethodName : 빈을 생성할 때 팩토리 메서드 이름, 예) memberService
  • Scope : 싱글톤이 기본값
  • lazyInit : 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 떄까지 최대한 생성을 지연 처리하는지의 여부
  • InitMethodName : 빈을 생성하고, 의존 관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName : 빈의 생명 주기가 끝나고 제거되기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties : 의존 관계 주입에서 사용된다. (자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)

 

반응형

+ Recent posts