Spring-JSP

[Spring-JSP] 어플리케이션 백그라운드 데몬쓰레드 + ServletContextListener

Jeong Jeon
반응형

오늘은 서버 구동시 호출되는 메서드를 먼저 알아보고, 해당 기능을 통한 서버 데몬을 기록해 두려고한다.

필자는 Redis를 사용하여 서버에서 Redis를 Sub하고있는 서버 데몬을 만들었다.

이전에는 

어플리케이션 뒷단에서 작동하는 Daemon Thread로 유용하게 사용할 수 있을 아이~!

 

다음은 필요한 정보들이다.

 

1). Redis properties를 가져오기 위하여 @PropertySoure를 사용한다.

 

2). ServletContextListener + @WebListener

@WebListener 어노테이션을 사용하여 Tomcat에 Listener임을 알려주는 어노테이션인데, 해당 어노테이션을 통해 Tomcat에게 이거 리스너로 쓸거야! 라는것을 알려준 뒤, ServletContextListener interface를 implement하여 구현한다.

 

ServletContextListener에서 제공하는 contextInitialized와 contextDestroyed를 오버라이딩하여 사용한다.

 

3). Runnable interface를 통해 Thread를 생성할텐데, 이때 데몬 쓰레드로 설정하여 서버가 종료될때 자동으로 해당 Thread도 종료 될 수 있도록 설정한다.

 

4). 무한 루프 및 조건을 사용하여 어플리케이션 실행 후 지정한 동작을 지속적으로 반복, 및 요청을 기다리고있는 상태로 만든다.

=> 필자는 Redis를 Pub하는 방식으로, brpop을 통해 10초 마다 데몬의 살아있음을 표시하고, Redis에 해당하는 Key가 들어왔을 경우 필요한 메서드를 호출하여 실행하는 방식을 사용하였다.

원하는경우 필요한 조건을 통해 다양한 형태로 변환이 가능하다.

 

 

자 간단하게 이정도만 서론으로 작성하고 코드와 함께 머리속에 남겨두도록 하자..!

 

우선 기본틀을 잡아줍시다.

@WebListener
@Configuration
@PropertySource({"classpath:config/${spring.profiles.active}.redis.properties"})
public class PushDaemonWebAppListener implements ServletContextListener, Runnable{

    private Thread thread; //Thread 객체
    private boolean isShutdown = false; //Thread의 켜짐 여부를 확인하는 Boolean
    
    /*
    * Redis Client Jedis를 사용하기 위한 설정
    */
    JedisPoolConfig jedisPoolConfig; // Jedis 설정 객체
    JedisPool pool; //JedisPool 객체
    Jedis jedis; /Jedis 객체
    
    @Autowired
	private Environment env;
    
    @Override
    public void run(){
    
    }
    
    @Override
    public void contextInitialized (ServletContextEvent event) {
    	System.out.println ("== DaemonListener.contextInitialized has been called. ==");
    }
    
    @Override
    public void contextDestroyed (ServletContextEvent event) {
   		System.out.println ("== DaemonListener.contextDestroyed has been called. ==");
    }
}

 

1). 여기서 데몬을 시작하는 메서드를 만들어줄건데, 이때 데몬쓰레드로 설정해준다.

2). 서버 시작시 contextInitialized 메서드를 통해 Thread를 구동시킨다. 아래는 thread.sleep을 이용한 방법.

@WebListener
@Configuration
@PropertySource({"classpath:config/${spring.profiles.active}.redis.properties"})
public class PushDaemonWebAppListener implements ServletContextListener, Runnable{

    private Thread thread; //Thread 객체
    private boolean isShutdown = false; //Thread의 켜짐 여부를 확인하는 Boolean
    
    /*
    * Redis Client Jedis를 사용하기 위한 설정
    */
    JedisPoolConfig jedisPoolConfig; // Jedis 설정 객체
    JedisPool pool; //JedisPool 객체
    Jedis jedis; /Jedis 객체
    
    @Autowired
	private Environment env;
    
    public void startDaemon() {
        if (thread == null) {
            thread = new Thread(this, "Daemon thread for background task");
            thread.setDaemon(true); //데몬쓰레드로 서버 종료에 의존적으로 만듬.
        }
        if (!thread.isAlive()) {
            thread.start();
        }
    }
    
    
    @Override
    public void run(){
    	Thread currentThread = Thread.currentThread();
       
        while (currentThread == thread) {
            try {
                Thread.sleep(1000 * 60);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    @Override
    public void contextInitialized (ServletContextEvent event) {
    	System.out.println ("== DaemonListener.contextInitialized has been called. ==");
        startDaemon();
    }
    
    @Override
    public void contextDestroyed (ServletContextEvent event) {
   		System.out.println ("== DaemonListener.contextDestroyed has been called. ==");
    }
}

 

3). 이제 Redis의 기능을 사용하여 데몬쓰레드를 완성시켜보자.

우선 데몬쓰레드를 동작시키고 난뒤,

Redis 정보를 입력해주어 해당 데몬쓰레드가 레디스를 subscribe하게 만든다. -> 설정 + brpop or rpop

여기서 중요한 부분이 brpop으로 Redis 공식문서를 보고 활용하면 된다. 

=>리스트에 데이터가 이미 있을 경우에는 RPOP와 같다. 데이터가 없을 경우에는 timeout(초) 만큼 기다린다.
timeout이 0일때, 데이터가 입력될때까지 기다린다. 데이터가 들어오면 pop을 하고 key, data, 시간(초)를 표시한다.

 

작동 부분 코드

@WebListener
@Configuration
@PropertySource({"classpath:config/${spring.profiles.active}.redis.properties"})
public class PushDaemonWebAppListener implements ServletContextListener, Runnable{

    private Thread thread; //Thread 객체
    private boolean isShutdown = false; //Thread의 켜짐 여부를 확인하는 Boolean
    
    /*
    * Redis Client Jedis를 사용하기 위한 설정
    */
    JedisPoolConfig jedisPoolConfig; // Jedis 설정 객체
    JedisPool pool; //JedisPool 객체
    Jedis jedis; /Jedis 객체
    
    @Autowired
	private Environment env;
    
    public void startDaemon() {
        if (thread == null) {
            thread = new Thread(this, "Daemon thread for background task");
            thread.setDaemon(true); //데몬쓰레드로 서버 종료에 의존적으로 만듬.
        }
        if (!thread.isAlive()) {
            thread.start();
        }
    }
    
    
    @Override
    public void run(){
    	Thread currentThread = Thread.currentThread();
            System.out.println("######### Contruct DaemonNotify : " + Thread.currentThread().getName() + " ########");
            String hostName 				= env.getProperty("local.redis.hostName");
            String port 					= env.getProperty("local.redis.port");
            String password 				= env.getProperty("local.redis.password");
            String timeout 					= env.getProperty("local.redis.timeout");
            String myServerkey = "";
			
            //서버 여러대를 사용했을때 해당 서버에서 받아야할 대상을 구분하기 위해 만들어놓은 serverkey를 사용한다.
            try {
				myServerkey = env.getProperty("local.redis.serverdaemonkey") + Constant.serverUuid;
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				e1.printStackTrace();
			}

			//해당 데몬 정보 로그
            System.out.println("######### redis server config ########");
            System.out.println("######### host : " + hostName + " ########");
            System.out.println("######### port : " + port + " ########");
            System.out.println("######### password : " + password + " ########");
            System.out.println("######### timeout : " + timeout + " ########");
            System.out.println("######### myServerkey : " + myServerkey + " ########");

			//Jedis 설정
            jedisPoolConfig = new JedisPoolConfig();
            pool = new JedisPool(jedisPoolConfig, hostName, Integer.parseInt(port), Integer.parseInt(timeout), password);
            jedis = pool.getResource();
            //1번 DB사용
            jedis.select(1);
    		int i = 0;

			//Thread로부터 현재 스레드를 가져와서 등록된 thread가 현재 작동 중인 thread일 때에만 반복한다
     		//while(true) ~ thread.stop 등의 메모리 누수에 따른 대체 방법
    		while (currentThread == thread && !this.isShutdown) {
    			try {
	    			String resStr = null;
	    			if (i > 0) {
                    	//10초마다 brpop을 시도하여 없으면 대기
	    				List<String> res_article_mst = jedis.brpop(10, myServerkey);
	    				if (res_article_mst != null && res_article_mst.size() > 0) {
	    					resStr = res_article_mst.get(1);
	    				}
	    			} else {
	    				resStr = jedis.rpop(myServerkey);
	    			}
	    			if (resStr != null && !resStr.equals("")) {
	    				System.out.println("[ DaemonNotify : " + Thread.currentThread().getName() + "]");
						Gson gson = new Gson();
						JsonObject message = gson.fromJson(resStr, new TypeToken<JsonObject>(){}.getType());
						System.out.println("==============NATIVE MESSAGE=============");
						System.out.println("message : "+message);
						System.out.println("=========================================");
						//#####작업 영역 #####
                        //작업을 실행 후 count를 다시 0으로 복귀
						i = 0;
	    			} else {
	    				System.out.println("[ daemon DaemonNotify " + Thread.currentThread().getName() + " is alive now : "+ new Date() + "]");
	    				i++;
	    			}
    			}catch (Exception e) {
					e.printStackTrace();
				}
    		}
    }
    
   
}

 

시작 및 종료 부분 코드

1). contextInitialized ()

처음에 startDaemon(); 으로 데몬을 시작하면 다 될거라 생각했다.

하지만 Environment @autowired 해서 가져온 값들은 다 null이었다... 여기서부터 골머리..

왜그런가 생각해봤더니, Servlet Container쪽 코드를 짜놓고, Spring Context를 사용하려고 했기때문에.... @Autowired의 값이 Null로 나왔다...

 

해결방안 : AutowiredCapableBeanFactory를 통해 현재 만든 ServletListener의 의존성을 주입해주었다.

결과 : 문제없이 DI되어 값을 읽어올수있게되었다. 

 /*
    * 컨텍스트 초기화 시 데몬스레드를 실행시킨다.
    */
    @Override
    public void contextInitialized (ServletContextEvent event) {
    	System.out.println ("== DaemonListener.contextInitialized has been called. ==");
        //의존객체 주입을 위한 앱컨텍스트의 의존성을 설정
    	AutowireCapableBeanFactory autowireCapableBeanFactory = WebApplicationContextUtils.getRequiredWebApplicationContext(event.getServletContext()).getAutowireCapableBeanFactory();
    	autowireCapableBeanFactory.autowireBean(this);
        startDaemon();
    }
    
    /*
    *컨텍스트 종료시 스레드를 종료시킨다.
    */
    @Override
    public void contextDestroyed (ServletContextEvent event) {
   		System.out.println ("== DaemonListener.contextDestroyed has been called. ==");
        this.isShutdown = true; //thread 종료 boolean도 종료flag로 전환
        try{
        	//pool close
        	if(pool != null) {
        		if(!pool.isClosed()) pool.close();
        	}
			//jedis connection close
        	if(jedis != null) {
        		if(jedis.isConnected()) jedis.close();
        	}
        	jedisPoolConfig = null;
        	thread.join(); //쓰레드 join으로 현재 쓰레드가 종료되고 난뒤 다음을 실행하게 만든다.
            thread = null;
        }
        catch (InterruptedException ie)
        {
            ie.printStackTrace();
        }
    }

 

고찰 : ServletContext가 아닌 Spring ContextListener로 만들어서 해도 상관없는 코드라면, 추가로 @Autowired를 사용하여Spring Context에서 가져올것이 있다면 InitialzingBean을 사용해서 Bean초기화 시점에 넣어도 될것같다.

 

해당 코드를 InitializingBean interface를 사용해서 작성한경우 당연히 의존객체 주입 코드없이 구동가능하다.

 

언제 어떤것을 사용할지는 개발자의 판단에 따른다는것을 항상 명심하자...

반응형