Eclipse -No tests found with test runner JUnit 5

Junit 4로 로 테스트를 진행하는데 계속

No tests found with test runner JUnit 5 로 에러가 발생했다.

이럴때 해결하는 방법은

run configurations > JUnit > Test > 아래 Test runner 설정 > JUnit4로 변경을 해주면된다.

그리고 JUnit4 에서는 @Test 실행하려는 메소드의 접근 지정자는 꼭 public 이어야한다.

아래의 예제 대로 실행을 하면 Before, test,After 가 순차적으로 진행이 되어야한다.

public class BeforeAfter {

    @Before
    public void SetUp(){
        System.out.println("Before");
    }

    @Test
    void transformation() {
        System.out.println("test");
    }
    @After
    public void after(){
        System.out.println("After");
    }

}

하지만 test 밖에 출력이 되지 않았다. 왜 그런걸까 찾아보니 JUnit5 에서는 @Before, @After 가 @BeforeEach, @AfterEach 로 설정을 해야한다.

JUnit5 의 어노테이션으로 아래의 코드로 다시 실행을 하니 다시 실행이 된다.

public class BeforeAfter {

    @BeforeEach
    public void SetUp(){
        System.out.println("Before");
    }

    @Test
    void transformation() {
        System.out.println("test");
    }
    @AfterEach
    public void after(){
        System.out.println("After");
    }

}

SpringBoot에서 JUnit이 몇버전인지 알고 싶을때 아래로 확인하고 변경하면 된다.

생각없이 그냥 springInitializer에서 다운을 받으면 아래와 같은 설정이 있는데, springboot-2.2.X 이후부터는 Junit5로 설정이 되어있는 것 같다.

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.2-Release-Notes

인프런 백기선님의 SpringMVC 강의를 보고 정리한 내용입니다.
DispatcherServlet

이전의 글에서 SpringWebMVC에서는 FrontController로 DispatcherServlet 이라는 것구현해놓았다고 했다.

mvc context hierarchy

ServletWebApplicationContext 에서 Controller 설정만 가지고 와서 특정 url에 따른 처리를 해주고

Root WebApplicationContext 에는 Service, Repository 는 모든곳에서 사용할 수 있게 구분을 할 수 있게 하는 구조로 되어있다.

DispatcherServlet 사용하기

위와 같이 Controller는 ServletWebApplicationContext에 등록하고, Service,Repository의 설정은 Root WebApplication에 등록을 해주기

web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  </context-param>

  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>me.morris.AppConfig</param-value>
  </context-param>

  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>app</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>me.morris.WebConfig</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>app</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>

</web-app>

AppConfig.java : Controller를 제외한 ApplicationContext

WebConfig.java : Controller를 포함한 ApplicationContext

AppConfig.java

package me.morris;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;

@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(Controller.class))
public class AppConfig {

}

WebConfig.java

package me.morris;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;

@Configuration
@ComponentScan(useDefaultFilters = false, includeFilters = @ComponentScan.Filter(Controller.class))
public class WebConfig {

}

HelloController.java

package me.morris;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @Autowired
    HelloService helloService;
    @GetMapping("/hello")
    public String hello(){
        return "Hello, "+ helloService.getName();
    }
}

HelloService.java

package me.morris;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
    public String getName(){
        return "morrisKim";
    }
}

이렇게 RootWebApplicationContext와 Servlet WebapplicattionContext에 나눠서 설정을 할 수 있다.


간단하게 하면 Root WebApplicationContext에 Controller와 Service, Repository 등을 전부 등록하여 사용이 가능하다.

web.xml

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <servlet>
    <servlet-name>app</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>me.morris.WebConfig</param-value>
    </init-param>
  </servlet>

  <servlet-mapping>
    <servlet-name>app</servlet-name>
    <url-pattern>/app/*</url-pattern>
  </servlet-mapping>

</web-app>

WebConfig.java : ComponentScan을 전체로 등록함

package me.morris;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;

@Configuration
@ComponentScan
public class WebConfig {

}

IOC Container 연동

인프런 Spring WEB MVC 강의를 듣고 정리 한 내용입니다.

SpringIOC 를 Servlet에 연동을 하려고한다.

maven 의 pom.xml 의존성을 추가함

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>5.1.3.RELEASE</version>
</dependency>

web.xml 변경

  • 이전 서블릿에서 사용했던 servlet listener (ServletContextListener 구현체) 대신에 Spring에서 지원하는 ContextLoaderListener로 변경 해줌

변경 전

<listener>
  <listener-class>me.morris.MyListener</listener-class>
</listener>

spring에서 지원하는 org.springframework.web.context.ContextLoaderListener

: 스프링 IOC 컨테이너 applicationcontext 를 servlet의 생명주기에 맞춰서 servletcontext에 등록해주고 , 종료할때 applicationcontext를 제거하는 역할을 함

변경 후

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

applicationcontext의 위치, 파일 을 파라미터로 지정해줘야함

contextClass 등록

ContextLoader를 통해 ApplicationContext를 등록 해줘야한다.

web.xml

contextClass : applicationContext를 어떤 방법으로 등록을 할것인가?(Java Configure, Xml Configure ...)

<context-param>
  <param-name>contextClass</param-name>
  <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>me.morris.AppConfig</param-value>
</context-param>

me.morris.AppConfig.java

package me.morris;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class AppConfig {

}

HelloService.java 서비스 빈으로 등록

package me.morris;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
    public String getName(){
        return "morrisKim";
    }
}

web.xml에 아래와 같이 오류가 난다고 한다면??아래의 순서대로 태그를 나열하면 해결된다.

The content of element type "web-app" must match "(icon?,display-name?,description?,distributable?,context-param*,filter*,filter-mapping*,listener*,servlet*,servlet-mapping*,session-config?,mime-mapping*,welcome-file-list?,error-page*,taglib*,resource-env-ref*,resource-ref*,security-constraint*,login-config?,security-role*,env-entry*,ejb-ref*,ejb-local-ref*)".

Web ApplicationContext 불러오기

아래를 보면 ContextLoaderListener.class가 initwebApplicationContext를 통하여 context를 지정해준다.

99 servletContext.setAttribute(applicationContextName, context);

servletContext에 등록이 되는 것을 알 수 있다.

그 이후에는 이전에 만들었던 HelloServlet.java 클래스로 이동해서 doGet메서드에 서 ApplicationContext를 불러 와보자.

servletContext에 WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 라는 이름으로 ApplicationContext가 등록이 되어있으므로, 아래와 같이 불러와서 BeanFactory에서 HelloService.class 를 불러와서 사용이 가능하다.

HelloServlet.java

...
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

  ApplicationContext context = (ApplicationContext) getServletContext().getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
  HelloService helloService = context.getBean(HelloService.class);

  System.out.println("doGet");
  resp.getWriter().println("" +
                           "<html>\n" +
                           "<body>\n" +
                           "<h2>Hello Servlet!" +helloService.getName()+"</h2>\n" +
                           "</body>\n" +
                           "</html>\n");
}
...

이로써 IOC 컨테이너를 Servlet에서 사용 할 수 있는 방법을 알 아 볼 수 있었다.

url 하나당 서블릿 하나씩 만들어 나간다면, 설정이 계속 추가가 되며, 여러 서블릿에서 공통적으로 쓰는 것들을 어떻게 해결을 할수 있을까?

그래서 나온것이 FrontController

FrontController의 역할을 하는 하나의 Servlet을 설정해주고 url을 구분하는 방법을 나타냄

SpringWebMVC에서는FrontController로 DispatcherServlet 이라는 것구현해놓았다.

Servlet 애플리케이션 개발

웹 MVC 시작하기

처음 프로젝트 시작을 maven-archetype-webapp 으로 시작

Pom.xml에 servlet-api 디펜던시 추가

<dependency>
    <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>4.0.1</version>
  <scope>provided</scope>
</dependency>

provided 는 개발할때만 사용되고, 빌드 될때는 빠진다. 이유는 tomcat 에서 구현이 될것이다.

Servlet 생성

web.xml 파일

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>
  <display-name>Archetype Created Web Application</display-name>

  <servlet>
    <servlet-name>hello</servlet-name>
    <servlet-class>me.morris.HelloServlet</servlet-class>
  </servlet>


  <servlet-mapping>
    <servlet-name>hello</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>

</web-app>

me.morris 패키지 HelloServlet.java

package me.morris;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HelloServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        System.out.println("init");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("doGet");
        resp.getWriter().println("" +
                "<html>\n" +
                "<body>\n" +
                "<h2>Hello Servlet!</h2>\n" +
                "</body>\n" +
                "</html>\n");

    }

    @Override
    public void destroy() {
        System.out.println("Destroy");
    }
}

톰캣을 사용하여 실행 하면 처리가 됨

Init 메소드는 처음에 요청시에만 출력

Destroy는 tomcat servlet콘테이너가 종료 될때 실행

Servlet Listener와 Filter

Servlet Listener :

  • 웹 어플리케이션에서 발생하는 주요 이벤트를 감지하고, 각 이벤트에 특별한 작업이 필요한 경우에 사용 할 수 있다.
    • 서블릿 컨택스트 수준의 이벤트
      • 컨텍스트 라이프사이클 이벤트
      • 컨텍스트 애트리뷰트 변경 이벤트
    • 세션 수준의 이벤트
      • 세션 라이프 사이클 이벤트
      • 세션 애트리뷰트 변경 이벤트

Servlet Container가 시작될때, DatabaseConnection을 미리 만들어놓고, 다양한 servlet에서 생성할 수 있다.

Servlet attribute에 DatabaseConnection을 미리 만들어 놓을 수 있다.

구현

아래는 Servlet Context의 라이프사이클 변경에 대한 이벤트 를 감지 한것

MyListener.java 추가 하기

public class MyListener implements ServletContextListener {
    //Servlet Container의 라이프 사이클의 이벤트를 감지
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("contextInitialized ");
        sce.getServletContext().setAttribute("name","morriskim");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("contextDestroyed");
    }
}

web.xml 에 listener 태그 추가

<listener>
  <listener-class>me.morris.MyListener</listener-class>
</listener>

결과 :

[2020-03-01 06:39:52,389] Artifact SpringWebMVC:war exploded: Artifact is being deployed, please wait...
contextInitialized <-- 여기 추가된것을 알수 있다.
[2020-03-01 06:39:52,781] Artifact SpringWebMVC:war exploded: Artifact is deployed successfully
[2020-03-01 06:39:52,782] Artifact SpringWebMVC:war exploded: Deploy took 393 milliseconds
01-Mar-2020 18:40:02.244 정보 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [/Users/daeyunkim/Documents/03.util/apache-tomcat-8.5.42/webapps/manager]
01-Mar-2020 18:40:02.281 정보 [localhost-startStop-1] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [/Users/daeyunkim/Documents/03.util/apache-tomcat-8.5.42/webapps/manager] has finished in [37] ms
init
doGet

Servlet Filter :

  • 들어온 요청을 서블릿으로 보내고, 또 서블릿이 작성한 응답을 클라이언트로 보내기 전에 특별한 처리가 필요한 경우에 사용 할 수 있다.
  • 체인 형태의 구조

어떠한 요청이 들어왔을때, 요청, 응답에 있어서 전에 처리를 하고 싶은 경우, 여러개의 서블릿에 추가적으로 할 수 있다.

구현

MyFilter.java 추가

  • 주의 해야할점 : javax.servlet의 Filter를 implements 해야됨
    doFilter의 파라미터 들의 값을 보면 ServletRequest request, ServletResponse response, FilterChain chain 을 들어간것을 볼수 있다.
    여기에서는 Filterchain이 request,response를 연결 해줘야한다.

    package me.morris;

    import javax.servlet.*;
    import java.io.IOException;

    public class MyFilter implements Filter {

      @Override
      public void init(FilterConfig filterConfig) throws ServletException {
          System.out.println("Filter init");
      }
    
      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
          System.out.println("Filter ");
          chain.doFilter(request,response);
      }
    
      @Override
      public void destroy() {
          System.out.println("Filter destroy");
      }

    }

web.xml 에 Filter 설정 추가

,

의 안의 속성에는 servlet-name과 url-pattern이 있는데,

하나의 주소 이외에 다양한 곳에 필터를 사용할때 url-pattern을 사용한다.

  <filter>
    <filter-name>myfilter</filter-name>
    <filter-class>me.morris.MyFilter</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>myfilter</filter-name>
    <servlet-name>hello</servlet-name>
  </filter-mapping>

Filter를 추가한 실행 동작 순서

[2020-03-01 06:49:03,499] Artifact SpringWebMVC:war exploded: Artifact is being deployed, please wait...
contextInitialized 
Filter init
[2020-03-01 06:49:03,887] Artifact SpringWebMVC:war exploded: Artifact is deployed successfully
[2020-03-01 06:49:03,887] Artifact SpringWebMVC:war exploded: Deploy took 388 milliseconds
init
Filter 
doGet
01-Mar-2020 18:
  1. contextInitialized : 서블릿 리스너에서 contextInitinalize가 실행
  2. Filter init : Filter 를 구현한 클래스에서 init 메서드 실행
  3. FilterMapping된 Servlet 클래스 호출
  4. init : Servlet클래스 init 메서드 실행
  5. Filter : Filter 클래스에서의 doFilter 메서드 실행
  6. doGet : Servlet 메서드의 doGet 메서드 실행

으로 적용된다.

Servlet Container ->(request)-> Filter -> Servlet ->(response)-> Servlet Container

해결하고 싶은 것

아래와 같은 내용을 Spring @RequestBody 에 값을 전달하고싶다.

{
    "zooName":"MorrisZoo",
    "animals":[
        {"type":"cat", "name":"yammi", "likesCream":true,"lives":0},
        {"type":"dog", "name":"Fancy","barkVolume":4}
    ]
}

animals는 Animal 타입을 상속 받은 dog, cat 의 객체로 받아와야한다.

구현하기
Jackson Polymorphic Type Handling Annotations 을 참고
Jackson 라이브러리는 다음에 한번에 정리를 하겠습니다.

AnimalController.java

@RestController
public class AnimalController {

    @PostMapping(value="/zooinfo")
    public void jsonTypeCheck(@RequestBody Zoo zoo){
        String zooName = zoo.zooName;
        System.out.println("동물원 이름 : "+zooName);
        zoo.animals.forEach((value)->value.println());
    }
}

Zoo.java : @RequestBody에 받아올 zoo 객체

package com.jackson.jacksontutorial;

import java.util.List;

public class Zoo {
    public String zooName;
    public List<Animal> animals;
}

Animal.java` : Dog, Cat의 상위 클래스

package com.jackson.jacksontutorial;

import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = Dog.class, name = "dog"),
        @JsonSubTypes.Type(value = Cat.class, name = "cat")
})
public abstract class Animal {
    public String name;

    public void println( ){
        System.out.println("Animal Type : "+name);
    };

}

Cat.java

package com.jackson.jacksontutorial;

import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonTypeName;


//@JsonTypeName("cat")
@JsonRootName("cat")
public class Cat extends Animal {
    boolean likesCream;
    public int lives;

    @Override
    public String toString() {
        return "Cat{" +
                "likesCream=" + likesCream +
                ", lives=" + lives +
                '}';
    }

    @Override
    public void println() {
        String AnimalType = super.name;
        System.out.println("name : "+name+","+this.toString());
    }
}

Dog.java

package com.jackson.jacksontutorial;

import com.fasterxml.jackson.annotation.JsonRootName;
import com.fasterxml.jackson.annotation.JsonTypeName;

//@JsonTypeName("dog")
@JsonRootName("dog")
public class Dog extends Animal {
    public double barkVolume;

    @Override
    public String toString() {
        return "Dog{" +
                "barkVolume=" + barkVolume +
                '}';
    }
    @Override
    public void println() {
        String name = super.name;
        System.out.println("name : "+name+","+this.toString());
    }
}

결과

요청

post- @RequestBody

{
    "zooName":"MorrisZoo",
    "animals":[
        {"type":"cat", "name":"yammi", "likesCream":true,"lives":0},
        {"type":"dog", "name":"Fancy","barkVolume":4}
    ]
}

SpringApplication

2020-02-28 16:51:14.376  INFO 17268 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 5 ms
동물원 이름 : MorrisZoo
name : yammi,Cat{likesCream=false, lives=0}
name : Fancy,Dog{barkVolume=4.0}

ClassPath Scanning

특정 classpath 이하에 있는 관리할 컴포넌트(@Component)들을 등록을 하기 위해 스캔함

@Component : Annotation Type으로 런타임에 동작함. classpath scanning을 통해 자동적으로 검색됨.

@Repository, @Service, @Controller

@ComponentScan(basePackages = "kr.co.fastcampus.cli") 을 해주면 xml에 별도로 설정을 안해줘도 알아서 추가가 됨.

예시

@Configuration
@ComponentScan(basePackages = "kr.co.fastcampus.cli")
public class AppConfig {
    @Bean
    public A a1(){
        return new A();
    }
    @Bean
      @Scope("singleton")
    public B b1(){
        return new B();
    }
}

@Component
class A {}

@Component
class B{}

아래와 같이 AnnotationConfigApplicationContext()를 사용해서 사용이 가능하다.

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

AnnotationConfigApplicationContext API 를 살펴보면 아래와 같이 파라미터를 넣어주면 사용이 가능하다.

Constructor and Description
AnnotationConfigApplicationContext()Create a new AnnotationConfigApplicationContext that needs to be populated through register(java.lang.Class...) calls and then manually refreshed.
AnnotationConfigApplicationContext(Class... componentClasses)Create a new AnnotationConfigApplicationContext, deriving bean definitions from the given component classes and automatically refreshing the context.
AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory)Create a new AnnotationConfigApplicationContext with the given DefaultListableBeanFactory.
AnnotationConfigApplicationContext(String... basePackages)Create a new AnnotationConfigApplicationContext, scanning for components in the given packages, registering bean definitions for those components, and automatically refreshing the context.

Filter

ComponentScan에서 특정 클래스를 빼고 싶을 때, (pattern, class, 등 의 다양함 )

아래처럼 filter를 사용해서 execludeFilters를 사용하여 가능하다.

@Slf4j
@ComponentScan(basePackageClasses = Main.class , excludeFilters = @ComponentScan.Filter (type= FilterType.REGEX,pattern="kr.co.fastcampus.cli.B"))
public class Main {
    static Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String []args) {

        //Java Anotation을 사용한 주입
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        B b = context.getBean(B.class);
        log.info("b : "+b);
        context.close();
    }
}

ComponentScan 성능 향상

ComponentScan은 Java Generic을 사용하여 JVM에서 어노테이션이 붙어있는지 아닌지 결정하여 판단을 하게 되는데, 만약 어노테이션이 너무 많이 사용되게 되면 ComponentScan이 오래 걸릴 수 있다.

그래서 indexer를 만들어 놓음

pom.xml에 추가

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.3.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

빌드를 하게 되면 META-INF/spring.components 가 만들어짐.

Bean Scope

빈의 범위

객체를 만들때 컨테이너가 객체를 한번만 호출하는지, 객체를 호출받을때마다 생성하는지를 정의

singleton, prototype을 주로 많이 쓰임

Singleton : 객체를 한번만 생성 (동일성을 보장)

<bean id="A" class="kr.co.fastcampus.cli.A" scope="singleton"></bean>

prototype : 객체를 계속 새로 만듬

<bean id="A" class="kr.co.fastcampus.cli.A" scope="prototype"></bean>

Scope Description
singleton (Default) Scopes a single bean definition to a single object instance for each Spring IoC container.
prototype Scopes a single bean definition to any number of object instances.
request Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring ApplicationContext.
session Scopes a single bean definition to the lifecycle of an HTTP Session. Only valid in the context of a web-aware Spring ApplicationContext.
application Scopes a single bean definition to the lifecycle of a ServletContext. Only valid in the context of a web-aware Spring ApplicationContext.
websocket Scopes a single bean definition to the lifecycle of a WebSocket. Only valid in the context of a web-aware Spring ApplicationContext.

동일성과 동등성

동일성 : 객체 주소가 같다.

동등성 : 객체의 값이 같다.

동일성

@Test
public void testIdentity(){
  //동일성 (identity) : 객체 주소 가 같다. (object1==object2==object3) 는 동일한 주소 값을 가질 수 있다.
  //동등성 (eqauls) : 객체의 값이 같다. (object1.equals(object2) ==true )

  A a1 = new A();
  A a2 = new A();
  Assert.assertFalse(a1==a2); //a1과 a2의 주소값이 같은지 ? 다르다

  A a3 = a1;
  Assert.assertTrue(a3==a1);

}
class A{

}

동등성

Object 의 equals 는 아래와 같이 동일성 체크를 한다.

public boolean equals(Object obj) {
            return (this == obj);
}



@Test
public void testEquals(){

  A a1 = new A(10,"Hello world");
  A a2 = new A(10,"Hello world");
  A a3 = new A(5,"Hello world");


  Assert.assertTrue(a1.equals(a2));
  Assert.assertFalse(a1.equals(a3));

}

코드

class A{
    private int a1 ;
    private String a2;

    public A(int a1, String a2) {
        this.a1 = a1;
        this.a2 = a2;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof A)) return false;
        A a = (A) o;
        return a1 == a.a1 &&
                Objects.equals(a2, a.a2);
    }

    @Override
    public int hashCode() {
        return Objects.hash(a1,a2);
    }
}

lombok을 응용하여 사용하면 아래와 같이 쉽게 만들 수 있다.

@EqualsAndHashCode
@AllArgsConstructor
class A{
    private int a1 ;
    private String a2;
}

TestCode

https://junit.org/junit5/docs/current/user-guide/#running-tests

https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-scopes

fastcampus Java SpringCore 강좌를들으면서 정리를 하였습니다.

ClassPath Scanning

특정 classpath 이하에 있는 관리할 컴포넌트(@Component)들을 등록을 하기 위해 스캔함

@Component : Annotation Type으로 런타임에 동작함. classpath scanning을 통해 자동적으로 검색됨.

@Repository, @Service, @Controller

@ComponentScan(basePackages = "kr.co.fastcampus.cli") 을 해주면 xml에 별도로 설정을 안해줘도 알아서 추가가 됨.

예시

@Configuration
@ComponentScan(basePackages = "kr.co.fastcampus.cli")
public class AppConfig {
    @Bean
    public A a1(){
        return new A();
    }
    @Bean
      @Scope("singleton")
    public B b1(){
        return new B();
    }
}

@Component
class A {}

@Component
class B{}

아래와 같이 AnnotationConfigApplicationContext()를 사용해서 사용이 가능하다.

AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

AnnotationConfigApplicationContext API 를 살펴보면 아래와 같이 파라미터를 넣어주면 사용이 가능하다.

Constructor and Description
AnnotationConfigApplicationContext()Create a new AnnotationConfigApplicationContext that needs to be populated through register(java.lang.Class...) calls and then manually refreshed.
AnnotationConfigApplicationContext(Class... componentClasses)Create a new AnnotationConfigApplicationContext, deriving bean definitions from the given component classes and automatically refreshing the context.
AnnotationConfigApplicationContext(DefaultListableBeanFactory beanFactory)Create a new AnnotationConfigApplicationContext with the given DefaultListableBeanFactory.
AnnotationConfigApplicationContext(String... basePackages)Create a new AnnotationConfigApplicationContext, scanning for components in the given packages, registering bean definitions for those components, and automatically refreshing the context.

Filter

ComponentScan에서 특정 클래스를 빼고 싶을 때, (pattern, class, 등 의 다양함 )

아래처럼 filter를 사용해서 execludeFilters를 사용하여 가능하다.

@Slf4j
@ComponentScan(basePackageClasses = Main.class , excludeFilters = @ComponentScan.Filter (type= FilterType.REGEX,pattern="kr.co.fastcampus.cli.B"))
public class Main {
    static Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String []args) {

        //Java Anotation을 사용한 주입
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Main.class);
        B b = context.getBean(B.class);
        log.info("b : "+b);
        context.close();
    }
}

ComponentScan 성능 향상

ComponentScan은 Java Generic을 사용하여 JVM에서 어노테이션이 붙어있는지 아닌지 결정하여 판단을 하게 되는데, 만약 어노테이션이 너무 많이 사용되게 되면 ComponentScan이 오래 걸릴 수 있다.

그래서 indexer를 만들어 놓음

pom.xml에 추가

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.3.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

빌드를 하게 되면 META-INF/spring.components 가 만들어짐.

'BackEnd > Spring' 카테고리의 다른 글

ClassPath Scanning and Managed Components  (0) 2020.02.22
Bean Scope  (0) 2020.02.11
Customizing the Nature of a Bean  (0) 2020.02.01
DI (Dependency Injection)  (0) 2020.02.01
Spring-Core  (0) 2020.01.19

fastcampus java 온라인 강의를 들으면서 정리한 내용입니다.

Customizing the Nature of a Bean

  • Lifecycle Callbacks
  • ApplicationContextAware and BeanNameAware

LifeCycle

객체들을 관리 , 객체를 만들때(new), 객체를 없앨 때(destroy)

new라는 객체를 만들기 전

특정 빈에 대해서 자원 없앨때 close에 대한 함수

  • InitializingBean
  • DisposableBean
  • xml -init method / destroy

실행

ApplicationContext context = new ClassPathXmlApplicationContext("dao.xml");

ConnectionFactory connectionFactory = context.getBean("connectionFactory",ConnectionFactory.class);
log.info("result : "+connectionFactory.getConnection());
  1. Initializing Bean (Interface)

    bean이 생성 될때 미리 생성됨 : 컨테이너가 빈에 필요한 모든 속성을 설정 한 후 빈 초기화 작업을 수행

    InitializingBean 인터페이스는 afterPropertiesSet 하나의 메서드를 가진다.

    void afterPropertiesSet() throws Exception;

예제 dao.xml

<bean id="connectionFactory" class="kr.co.fastcampus.cli.ConnectionFactory" >
  <constructor-arg name="driverClass" value="org.h2.Driver"/>
  <constructor-arg name="url" value="jdbc:h2:mem:test"/>
  <constructor-arg name="user" value="sa"/>
  <constructor-arg name="password" value=""/>
</bean>

<bean id = "connection" class="java.sql.Connection"
      factory-bean = "connectionFactory"
      factory-method="createConnection"
      />

<bean id="dao" class="kr.co.fastcampus.cli.Dao">
  <constructor-arg ref="connection"></constructor-arg>
</bean>

ConnectionFactory.java

public class ConnectionFactory implements InitializingBean {
    private String driverClass;
    private String url;
    private String user;
    private String password;
    //bean을 만들지면서 Connection을 미리 생성 하기
    @Getter
    private Connection connection=null;

    public ConnectionFactory(String driverClass,String url, String user, String password) {
        this.driverClass = driverClass;
        this.url = url;
        this.user = user;
        this.password = password;
    }

    public Connection createConnection() throws SQLException {
        try {
            Class.forName("org.h2.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        String url = "jdbc:h2:mem:test;MODE=MySQL;";
        return DriverManager.getConnection(this.url,this.user,this.password);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        this.connection = createConnection();
    }
}
  1. Initializing Bean (xml)

    init-method 에 는 빈 팩토리를 불러올 때 실행될 메서드이름을 지정해서 사용할 수 있다.

    dao.xml

    <bean id="connectionFactory" class="kr.co.fastcampus.cli.ConnectionFactory" init-method="init" >

    ConnectionFactory.java

    public class ConnectionFactory {
        private String driverClass;
        private String url;
        private String user;
        private String password;
        //bean을 만들지면서 Connection을 미리 생성 하기
        @Getter
        private Connection connection=null;
    
        public ConnectionFactory( String driverClass,String url, String user, String password) {
            this.driverClass = driverClass;
            this.url = url;
            this.user = user;
            this.password = password;
        }
        //bean 메서드(xml)의 init의 메서드를 지정해주면 bean이 초기화될때 생성된다.
        public void init() throws Exception {
            this.connection = createConnection();
        }
    
        public Connection createConnection() throws SQLException {
            try {
                Class.forName("org.h2.Driver");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
            String url = "jdbc:h2:mem:test;MODE=MySQL;";
            return DriverManager.getConnection(this.url,this.user,this.password);
        }
    }

###DisposableBean

Bean을 포함하는 컨테이너가 소멸 될 때 Bean이 콜백을 얻을 수 있습니다.

1.Disposable Bean (Interface)

DisposableBean 인터페이스는 destroy 메서드를 가진다.

void destroy() throws Exception;

ConnectionFactory.java

@Slf4j
public class ConnectionFactory implements DisposableBean {
    private String driverClass;
    private String url;
    private String user;
    private String password;
    //bean을 만들지면서 Connection을 미리 생성 하기
    @Getter
    private Connection connection=null;

    public ConnectionFactory( String driverClass,String url, String user, String password) {
        this.driverClass = driverClass;
        this.url = url;
        this.user = user;
        this.password = password;
    }
    //bean 메서드(xml)의 init의 메서드를 지정해주면 bean이 초기화될때 생성된다.
    public void init() throws Exception {
        this.connection = createConnection();
    }

    public Connection createConnection() throws SQLException {
        try {
            Class.forName("org.h2.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        String url = "jdbc:h2:mem:test;MODE=MySQL;";
        return DriverManager.getConnection(this.url,this.user,this.password);
    }

    @Override
    public void destroy() throws Exception {
        log.info("destory connection");
        if(this.connection!=null){
            this.connection.close();
        }
    }
}

이렇게 해주고 위에서 getbean을 사용해서 사용했지만, 여기서는 ApplicationContext 보다 ApplicationContext클래스를 상속받고 있는 ConfigurableApplicationContext 를 사용해서 테스트를 진행해야 한다.

ConfigurableApplicationContextApplicationContext, Lifecycle, Closeable 를 상속 받고 있다.

테스트를 위한 실행

ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("dao.xml");

ConnectionFactory connectionFactory = context.getBean("connectionFactory",ConnectionFactory.class);
log.info("result : "+connectionFactory.getConnection());
context.close();

결과

00:58:37.149 [main] INFO  kr.co.fastcampus.cli.Main - result : conn0: url=jdbc:h2:mem:test user=SA
00:58:37.151 [main] INFO  k.c.fastcampus.cli.ConnectionFactory - destory connection

2.DisposableBean Bean (xml)

위의 initializing Bean을 xml 로 설정한것과 같다.destroy-method 속성을 추가 해주면 된다.

<bean id="connectionFactory" class="kr.co.fastcampus.cli.ConnectionFactory" init-method="init" destroy-method="destroy">
@Slf4j
public class ConnectionFactory {
    private String driverClass;
    private String url;
    private String user;
    private String password;
    //bean을 만들지면서 Connection을 미리 생성 하기
    @Getter
    private Connection connection=null;

    public ConnectionFactory( String driverClass,String url, String user, String password) {
        this.driverClass = driverClass;
        this.url = url;
        this.user = user;
        this.password = password;
    }
    //bean 메서드(xml)의 init의 메서드를 지정해주면 bean이 초기화될때 생성된다.
    public void init() throws Exception {
        this.connection = createConnection();
    }

    public Connection createConnection() throws SQLException {
        try {
            Class.forName("org.h2.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        String url = "jdbc:h2:mem:test;MODE=MySQL;";
        return DriverManager.getConnection(this.url,this.user,this.password);
    }

    public void destroy() throws Exception {
        log.info("destory connection");
        if(this.connection!=null){
            this.connection.close();
        }
    }
}

결과는 똑같이 나온다.

01:08:23.145 [main] INFO  kr.co.fastcampus.cli.Main - result : conn0: url=jdbc:h2:mem:test user=SA
01:08:23.148 [main] INFO  k.c.fastcampus.cli.ConnectionFactory - destory connection

##Default init, destroy

default-init-method , default-destroy-method 를 beans에 추가 해주면 기본적으로 bean에서 특정 메서드로 사용이 가능하다.

<beans default-init-method="init"
       default-destroy-method="destroy"
       xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

LifeCycle

Lifecycle 인터페이스

  • void start();

  • void stop();

  • boolean isRunning();

context가 실행 중인지 아닌지 lifeCycle의 상태를 조회 가능하다.

ConfigurableApplicationContext context = new ClassPathXmlApplicationContext("dao.xml");
Lifecycle lifecycle = context.getBean(Lifecycle.class);
log.info("lifeCycle is Running ? "+lifecycle.isRunning());
context.close();
log.info("lifeCycle is Running ? "+lifecycle.isRunning());

결과

01:37:25.064 [main] INFO  kr.co.fastcampus.cli.Main - lifeCycle is Running ? true
01:37:25.069 [main] INFO  k.c.fastcampus.cli.ConnectionFactory - destory connection
01:37:25.070 [main] INFO  kr.co.fastcampus.cli.Main - lifeCycle is Running ? false

###ApplicationContextAware

특정 빈에서 application Context를 사용 하고 싶을때 사용

public interface ApplicationContextAware {
    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans-factory-nature

'BackEnd > Spring' 카테고리의 다른 글

Bean Scope  (0) 2020.02.11
ClassPath Scanning and Managed Components  (0) 2020.02.08
DI (Dependency Injection)  (0) 2020.02.01
Spring-Core  (0) 2020.01.19
SpringCore - IoC Container(1)  (0) 2020.01.19

+ Recent posts