반응형

보통 스케줄러를 구성할때 크론탭으로 일정시간을 하드코딩으로 넣거나 프로퍼티 또는 db에서 가져와서 세팅하는 형태로 많이 구현을 하는데, 요구사항으로 입력에 따라 운용중인 시스템에서 스케줄러 동작 시간이 변경되도록 구현이 되어야 했다.

 

자바8 이상 기준으로 작성되었습니다.

 

ThreadPoolTaskScheduler 클래스를 통해 구현을 진행했다.

동적 스케줄러 구성하기

DynamicChangeScheduler.java

import java.time.LocalDateTime;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

@Component
public class DynamicChangeScheduler {
	private ThreadPoolTaskScheduler scheduler;
	private String cron = "*/2 * * * * *";

	public void startScheduler() {
		scheduler = new ThreadPoolTaskScheduler();
		scheduler.initialize();
		// scheduler setting 
		scheduler.schedule(getRunnable(), getTrigger());
	}

	public void changeCronSet(String cron) {
		this.cron = cron;
	}

	public void stopScheduler() {
		scheduler.shutdown();
	}

	private Runnable getRunnable() {
		// do something
		return () -> {
			System.out.println(LocalDateTime.now().toString());
		};
	}

	private Trigger getTrigger() {
		// cronSetting
		return new CronTrigger(cron);
	}

	@PostConstruct
	public void init() {
		startScheduler();
	}

	@PreDestroy
	public void destroy() {
		stopScheduler();
	}
}

일반적으로 @Scheduled 어노테이션으로 크론탭이나 주기시간을 입력하여 동작하도록 처리하는데, ThreadPoolTaskScheduler를 통해 스케줄러를 생성하고 run을 시키는 형태로 구현하였다.

프로젝트가 구동하자마자 동작을 시키기 위해 postContruct 어노테이션 메소드에 startScheduler메소드 실행하여 스케줄러가 바로 동작하도록 하였다.

 

getRunnable 메소드에 정의된 실행 동작으로 인해 처음엔 2초마다 현재 시간을 찍어대기 시작한다.

 

view페이지에서 입력이 들어오면 시간을 바꾸도록 하였다.

AdminController.java

import java.util.HashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import egovframework.srok.sheduler.DynamicChangeScheduler;

@Controller
public class AdminController {
	
	@Autowired
	DynamicChangeScheduler ps;
	
	@RequestMapping(value="/setting.do")
	public ModelAndView setting() throws Exception{
		ModelAndView mv = new ModelAndView();
		mv.setViewName("admin/setting");
		return mv;
	}
	
	@RequestMapping(value="/updateScheduler.do")
	public @ResponseBody HashMap<Object, Object> updateScheduler(@RequestParam  HashMap<Object, Object> params) throws Exception{
		ps.stopScheduler();
		Thread.sleep(1000);
		ps.changeCronSet((String) params.get("cron"));
		ps.startScheduler();
		
		HashMap<Object, Object> res = new HashMap<Object, Object>();
		res.put("res", "success");
		return res;
	}
	
	@RequestMapping(value="/pauseScheduler.do")
	public @ResponseBody HashMap<Object, Object> pauseScheduler(@RequestParam  HashMap<Object, Object> params) throws Exception{
		ps.stopScheduler();
		HashMap<Object, Object> res = new HashMap<Object, Object>();
		res.put("res", "success");
		return res;
	}
}

 

admin/setting.jsp

<%@ page language="java" contentType="text/html; charset=EUC-KR"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>스케줄러 테스트</title>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
</head>
<body>
    <h1>스케줄러 동작 시간 변경 테스트</h1>
    <input type="text" id="cronTab">
    <br/>
    <a href="javascript:void(0);" id="changeTest">스케줄러 동작 시간 변경</a>
    <a href="javascript:void(0);" id="pauseTest">스케줄러 중지</a>
</body>
<script>
$(function(){
    $(document).on("click", "#changeTest", function(){
		$.ajax({
	        url: 'updateScheduler.do',
	        type: 'POST',
	        data: {cron: $("#cronTab").val()},
	        success: function (data) {
	    		if(data.res == "success"){
	    		    alert("스케줄러 동작 시간이 변경되었습니다.");
	    		    location.reload();
	    		}
	        },
	        error: function (error) {
	    		console.log(error)
	        }
	    });
    }).on("click", "#pauseTest", function(){
		$.ajax({
	        url: 'pauseScheduler.do',
	        type: 'POST',
	        data: '',
	        success: function (data) {
	    		if(data.res == "success"){
	    		    alert("스케줄러가 멈췄습니다.");
	    		    location.reload();
	    		}
	        },
	        error: function (error) {
	    		console.log(error)
	        }
	    });
	
    });
});
</script>
</html>

input 박스에 크론탭형태로 데이터를 넣고 시간 변경을 하면 변경된 크론 형태로 스케줄러가 동작하고, 중지를 누르면 기존에 동작하던 스케줄러가 멈추도록 구성하였다.

반응형
반응형

새로운 전자정부 프레임워크를 설치하고 구동하면서 과거 소스가 돌아가지 않는 현상이 발생했다.

자바 버전도 맞춰주고 오류날만한것도 다 잡고 Maven - Udate Project... 처리까지 했지만 계속해서 오류가 발생했는데, 원인은 자바11 버전으로 기본 세팅이 되어있으면서 발생하는 오류였고 pom.xml에 아래 내용을 추가하고 해결되었다.

 

 

 

자바11 pom.xml 오류 수정

pom.xml

<!-- 자바11 이슈 -->
<dependency>
	<groupId>javax.xml.bind</groupId>
	<artifactId>jaxb-api</artifactId>
	<version>2.3.1</version>
</dependency>
<dependency>
	<groupId>com.sun.xml.bind</groupId>
	<artifactId>jaxb-core</artifactId>
	<version>2.3.0.1</version>
</dependency>
<dependency>
	<groupId>com.sun.xml.bind</groupId>
	<artifactId>jaxb-impl</artifactId>
	<version>2.3.1</version>
</dependency>
<!-- 자바11 이슈 -->

 

 

 

Maven 저장소 위치변경

사실 이렇게하고 구동하면 또 되어야겠지만, maven 저장소인 .m2 - repogitory가 꼬인경우도 있다.

이미 너무 많은 프로젝트들끼리 같은 저장소를 바라보고 있어서 해당 전자정부프레임워크는 다른곳을 바라보도록 했다.

일반적으로 아래 경로를 바라보고 있을텐데,

C:\Users\사용자명\.m2\repository

 

.m2 repository_egov3.10(원하는 디렉토리 명으로 만든다)디렉토리에 를 추가하고 settings.xml을 만든다.


settings.xml

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
    
	<localRepository>C:\Users\사용자\.m2\repository_egov3.10</localRepository>
	<interactiveMode>true</interactiveMode>
	<offline>false</offline>
</settings>

 

다 만들었으면 이클립스로 돌아가서 Window - Preferences

Maven - User Settings로 가서 User Settings에 방금 만든 xml을 연결한다.

 

그럼 Maven이 새로 받기 시작한다.

 

 

 

톰캣 재연결

다 끝났으면 기존 Tomcat은 삭제하고 꼭 다시 생성해서 연결해주고, 아래 설정을 추가한다.

프로젝트 우클릭 - Buil Path - Configure Build Path...

 

추가하면 Apach Tomcat 버전이 뜬다.

반응형
반응형

서버 특정 경로에 이미지파일이 존재할때 해당 파일의 경로나 파일등의 정보를 노출하지 않고 바이너리 데이터를 통해 이미지를 표현하고 싶었습니다.

특히나 개인정보가 담긴 이미지 등의 데이터는 더욱 민감할 수 있는데 이런 형태의 인증 처리가 없을 경우 최악으로 인가되지 않은 특정 클라이언트가 url/경로/파일명 형태로 마구잡이로 접근하여 개인정보를 가져갈 수 있는 위험이 있다.

 

해당 위치 접근을 바로 할 수 없도록 막았고 가져오기 위해 인증된 계정만 서버에서 데이터를 전달하도록 처리하였다.

 

Controller.java

@Controller
public class AdminController {
	
	private final Logger logger = Logger.getLogger(AdminController.class);
    
	@RequestMapping(value={"preView"}, method=RequestMethod.POST)
	private @ResponseBody Map<String, Object> preView(@RequestParam Map<String, Object> params, final HttpServletRequest request) throws Exception {
		String fileName = (String) params.get("fileName");
		String filePath = request.getSession().getServletContext().getRealPath("/") + "upload/";
		File file = new File(filePath + fileName);
		if(file.exists()) {
			params.put("exist", true);
			params.put("blob", FileUtils.readFileToByteArray(file));
		}else {
			params.put("exist", false);
		}
		return params;
	}
}

요청한 파라미터의 파일 데이터를 기준으로 존재하면 apache.commons 의 FileUtils 를 사용하여 File 데이터를 바이트로 변환했습니다.

 

request.js

function preview(file){
  $.ajax({
    type:"POST",
    url:"preView",
    data:fileName="+file,
    dataType: "json",
    success: function(args){
      if(args.exist){
      	$("#img").attr("src", "data:image/png;base64," + res.blob);
      }else{
      	alert("파일이 존재하지 않습니다.");
      }
    },
    error: function(error){
    	console.log(error);
    	alert("오류가 발생했습니다.");
    }
  });
}

서버 특정 경로에 존재하는 파일명.확장자 형태로 요청하면 서버에서 존재하는지 확인 후 byteArray 형태로 데이터를 전달해주면 base64를 통해 데이터를 이진데이터로 읽어들여서 이미지를 표출해준다. 다만 매번 이미지를 가져올때 마다 서버에서 통신이 발생하고 이진데이터로 노출하다보니 속도가 미세하게 느려지는게 느껴진다.

반응형
반응형

부트에서 스케줄러 사용하기

기존 Legacy 프로젝트를 구성할 때는 quartz를 통해  servlet.xml에 cron설정을 하여 주로 사용하여 왔는데, 부트에서는 추가 라이브러리 없이 쉽게 사용이 가능합니다.

 

 

기존 메인 설정은 그대로 사용하셔도 됩니다. 스케줄러 사용을 위해 어노테이션 @EnableScheduling를 추가합니다.

 

TestSpringBootApplication.java

@EnableScheduling  //스케줄링
@SpringBootApplication
public class TestSpringBootApplication {

	public static void main(String[] args) {
    		SpringApplication app = new SpringApplication(SrokSpringBootApplication.class);		
    		app.setBannerMode(Mode.CONSOLE);
    		app.run(args);
	}
}

설정을 하였으면, 스케줄러가 동작할 Class를 작성해보겠습니다.

 

 

 

SchedulerTest.java - cron버전

import java.time.LocalDateTime;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class SchedulerTest {
	
	/**
	 * cron탭을 사용한 스케줄러
	 */
	@Scheduled(cron="0 14 * * * ?")
	public void cronJobSch() {
		System.out.println("Sheduler time(cron) : " + LocalDateTime.now());
	}
}

cron 탭을 사용하여 동작 처리를 하였습니다.

예제의 cron은 매 14분 0초마다 동작하는 스케줄러입니다.

 

 

 

SchedulerTest.java - fixedDelay버전

@Component
public class SchedulerTest {

	/**
	 * 이전 수행 종료된 시간으로부터 입력 시간마다 호출됨
	 * 수행이 끝나고 1초뒤에 수행
	 */
	@Scheduled(fixedDelay = 1000)
	public void oneSecActionSch() {
		System.out.println("Sheduler time(fixedDelay) : " + LocalDateTime.now());
	}
}

이번엔 fixedDelay 속성으로 처리하였습니다. fixedDelay는 해당 스케줄러의 동작이 종료되고 입력한 n초뒤에 동작합니다. ex) 1000 = 1초

 

 

 

SchedulerTest.java - fixedRate버전

@Component
public class SchedulerTest {
	
	/**
	 * 수행이 시작된 시간으로부터 입력 시간마다 호출됨
	 * 수행이 시작되고 1초뒤 수행
	 */
	@Scheduled(fixedRate = 1000)
	public void rateActionSch() {
		System.out.println("Sheduler time(fixedRate) : " + LocalDateTime.now());
	}
}

마지막으로 fixedRate 방식입니다. fixedDelay와 비슷하지만 종료시간이 기준이 아닌 시작시간이 기준으로 해당 스케줄러가 동작하고 n초 뒤에 동작합니다.

 

 

 

반응형
반응형

SpringBoot 프로젝트를 war로 배포하고 명령어를 통해 프로젝트를 정상적으로 구동하였는데, 특정 페이지 이동시 해당 Exception 에러가 발생하는것을 확인 할 수 있었다.

 

jsp가 아닌 thymeleaf 템플릿을 적용하여 사용중이였는데, 이클립스에서 구동할때는 전혀 문제가 되지 않았지만, war로 배포후 구동시 절대경로 /preFix/view.html 형태로 적용하면 발생하는 에러로 html을 찾지 못해서 발생한 에러다.

 

컨트롤러에서 절대경로에서 상대경로 형태로 변경 후 정상 구동이 되었다.

 

view 경로를 변경하였다.

 

반응형
반응형

SpringBoot에서 프로젝트를 진행하던 도중 JUnit 테스트를 위해 Run As - JUnit Test 진행하였는데, 아래와 같은 에러가 발생하였다.

 

 

No tests found with test runner 'JUnit 5'

 

 

발생 원인으로 JUnit5의 설정을 처리한 소스를 구동하여서 발생한 원인으로 프로젝트의 사용하는 JUnit 버전을 확인해봐야한다. 필자는 JUnit4로 설정되어 있었다.

 

Properties - Java Build Path - Libraries - Classpath(JUnit4) - JUnit5로 변경 후 Apply and Close

 

1. 프로젝트 우클릭 후 Properties 메뉴에 접근한다.

 

2. Java Build Path - Libraries 항목에서 JUnit 버전을 5로 변경한다.

 

3. JUnit5로 변경하고 Apply and Close 후 구동하면 정상동작하는 것을 볼 수 있다.

반응형
반응형

application.properties

SpringBoot를 구동시 동작할 포트 정보, DB정보 등 여러가지 세팅 관련된 데이터나 별도의 경로 옵션등의 값을 프로퍼티에 넣어두고 사용됩니다.

기본 프로퍼티 위치는 /src/main/resources 아래에 존재하며 구동시 자동으로 감지되므로 별도로 프로퍼티의 위치를 명시적으로 등록하거나 경로를 입력할 필요가 없습니다.

 

 

프로퍼티를 외부에서 주입하려는 이유

부트 프로젝트를 빌드하여 배포하게 되면 추후 변경사항이 발생하여 변경하고자 할때 war안에 존재하다보니 명령어를 통해 교체해주거나 새로 빌드하여 배포해야 하는 번거로움이 존재합니다. 이러한 점을 보안하고자 애초에 배포시 프로퍼티 위치 자체를 war 밖으로 빼내면 설정파일을 열어서 간단하게 값을 변경 후 저장하고 부트 프로젝트만 구동시키면 되기에 외부에 존재하는 프로퍼티를 적용하는 방법을 찾아보았습니다.

 

 

프로퍼티 구동 순서

아래는 부트 구동시 프로퍼티를 찾는 순서입니다.

  1. 명령 줄 인수.
  2. Java System 속성 ( System.getProperties()).
  3. OS 환경 변수.
  4. @PropertySource@Configuration수업 에 대한 주석 .
  5. 패키지 된 jar 외부의 애플리케이션 속성 ( application.properties YAML 및 프로필 변형 포함).
  6. jar 내부에 패키지 된 애플리케이션 속성 ( application.properties YAML 및 프로필 변형 포함).
  7. 기본 속성 (을 사용하여 지정됨 SpringApplication.setDefaultProperties).

일반적으로 부트 구동시 사용되는 방법은 6번 내부에 패키지된 프로퍼티를 가져오는것입니다.

6번 구동방식

 

여기서 우리는 외부에 존재하는 프로퍼티 파일을 읽도록 처리해보겠습니다.

 

 

외부 프로퍼티 적용하기

먼저 테스트 환경 방식입니다.

- Spring Boot war 배포파일로 작성(내부 톰캣)

디렉토리 구성 및 파일 구성

 

정상적으로 property디렉토리 프로퍼티 파일을 읽으면 포트가 8100으로 구동됩니다.

 

1. 구동시 명령 옵션으로 처리하기

  java -jar app.war  

일반적인 부트 구동 명령어인데 여기서 옵션을 추가하여 property 디렉토리 내부의 application.properties를 로드하여 구동하겠습니다.

--spring.config.loaction 옵션을 통해 classpath에 존재하는 프로퍼티 또는 file을 통해 존재하는 파일을 명시해줄 수 있습니다.

--spring.config.name 옵션도 존재하는데, 해당 옵션을 통해 application.properties가 아닌 다른 프로퍼티를 읽도록 처리도 가능합니다.

 

적용하기 예제

절대경로를 통하여 file 위치를 명시하였습니다.

  java -jar app.war --spring.config.location=file:C:/test/property/application.properties  

정상적으로 8100으로 구동된 모습

 

2. jar 외부에 존재하는 프로퍼티를 읽기

사실 1번보다 간단한 방법인데 배포하는 디렉토리에 프로퍼티를 같이 위치하도록 하면 됩니다.

그럼 부트 구동 동작원리상 내부보다 외부를 먼저 보기때문에 자동으로 등록됩니다.

 

이번엔 명령줄에 별다른 옵션없이 구동하였지만 정상적으로 8100포트로 동작하는 모습을 볼 수 있습니다.

 

 

 

이 외에도 다른 설정 방법이 있지만 여기까지만 알아보도록 하겠습니다.

반응형
반응형

스프링 프로젝트를 구성하여 개발하다보면 사용자가 로그인이 되었는지 접근이 가능한 사용자인지 보안토큰이나 불필요한 파라미터나 파일등을 전송중인지 여러가지를 앞단에서 먼저 체크하고 통과시키거나 멈추게하는 기능을 처리 할 수 있는데, 필터 또는 인터셉터를 사용하여 처리가 가능합니다.

 

 

이 중에 컨트롤러에 도착하기전 먼저 계정의 세션정보나 접근레벨등을 처리하기 위한 인터셉터 적용법을 알아볼건데, 가로채다라는 뜻이 있듯이 컨트롤러에 도착하기전 데이터를 가로채서 요청한 데이터의 정보를 확인하여 해당 요청의 진행 유무를 판단할수있습니다.

 

 

먼저 스프링에서 요청이 발생했을때의 Life Cycle을 보시면 아래와 같습니다.

 

Filter와 Interceptor의 차이로 Dispatcher Servlet의 앞단에서 처리하냐, 뒷단에서 처리하냐의 차이가 존재합니다.

그림을 보시면 아시겠지만 Filter는 Servlet의 앞단에서 처리하며, Interceptor는 뒷단에서 처리합니다.

 

그럼 이제 적용방법과 사용법을 알아보겠습니다.

 

 

Interceptor 적용하기

구성환경

- STS4

- Java8

 

 

Interceptor 구성

먼저 interface HandlerInterceptor를 상속받아서 Interceptor를 구성합니다.

* sts4 기준으로 java8을 사용하며 HandlerInterceptor를 사용합니다. 기존에 상속받아서 사용하던 추상클래스HandlerInterceptorAdapter는 "Deprecated." 처리되었습니다.

public class AdminInterceptor implements HandlerInterceptor {
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		return true;
	}
	
	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
	}
	
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, @Nullable Exception arg3) throws Exception {
	}
}

-preHandler : 컨트롤러에 도착하기전에 동작하는 메소드로 return값이 true이면 진행, false이면 멈춥니다.

-postHandler : 컨트롤러에 도착하여 view가 랜더링되기 전에 동작합니다.

-afterCompletion: view가 정상적으로 랜더링된 후에 마지막에 실행됩니다.

 

 

WebWebMvcConfigurer를 통한 Interceptor 설정

* sts4 java8 WebMvcConfigurer를 사용합니다. 기존에 상속받아서 사용하던 추상클래스 WebMvcConfigurerAdapter는 "Deprecated." 처리되었습니다.

@Configuration
public class ServerConfigure implements WebMvcConfigurer{
	private static final List<String> URL_PATTERNS = Arrays.asList("/async/*", "/board", "/user");  //인터셉터가 동작 해야 될 요청 주소 mapping 목록
	
	//인터셉터 주소 세팅
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(new AdminInterceptor()).addPathPatterns(URL_PATTERNS);
	}
}

SpringLegacy에서는 servlet-context.xml에서 맵핑할 정보와 어떤 인터셉터를 적용할건지 처리를 하곤 했는데, 부트에서는 java를 통해 설정을 진행합니다.

 

addIntercepotrs를 통해 사용할 Interceptor를 등록하고. 패턴을 등록해줍니다.

-addPathPatterns : 해당 메소드는 동작해야할 url패턴을 설정합니다.

-excludePathPatterns: 해당 메소드는 적용한 인터셉터에서 제외할 url패턴을 설정합니다.

 

 

이상으로 springboot에서 Interceptor 적용법을 알아보았습니다.

 

반응형