반응형

웹 개발을 진행하다보면 수많은 취약점공격을 대비하면서 개발을 진행해야 합니다.

개인정보를 탈취하거나 해킹, 계정 도용, 잘못된 수정이나 악의적인 페이지로 강제 이동하는 현상 등등 많은 이슈들을 사용자가 겪을 수 있거나 고객사쪽 데이터에도 큰 문제를 일으킬 수 있기 때문입니다.😱

 

이를 방어하기 위해 회사 동료분들과 공통으로 사용할 필터나 인터셉터등을 작성하여 공격을 방어해보았지만, 새로운 기술이나 방법을 통해 다양한 방식의 공격에서 취약해지는 현상을 발견했고, 이를 보완하고자 추가 개발을 해야할지 다른 라이브러리르 적용해볼지 찾아보다가 시간상의 어려움으로 네이버의 lucy필터라는걸 알게되어 이번에 적용을 하게 되었습니다. 네이버는 기존에 lucy-xss-filter라는 라이브러리를 제공하였는데, 해당 라이브러리도 취약한 부분이 발생하여 해결책으로 만든 라이브러리가 lucy-xss-servlet-filter입니다.

 

특징으로는 모든 xss공격으로 의심되는 패턴을 제거하는게 아니라 필터링(치환)이되어 컨트롤러에 도착했을때는, 다른값으로 이미 변경이되어 있습니다.

 

네이버측에서도 이러한 문제가 발생하여 기존에 잘 동작하던 소스가 문제가 생길수 있으므로, 새로운 프로젝트에 적용하는걸 추천하고 있습니다.

 

또한 직접 적용해보니 json형태의 문제와 multipart는 필터링이 안되어 전송되는 현상등이 발생하여 이러한 부분들을 해결한 포스팅은 추후 작성되면 하단에 링크를 걸어두도록 하겠습니다.

 

▼▼▼아래 글은 spring에서 lucy-xss-servlet-filter적용하는 방법을 포스팅했습니다.

https://myhappyman.tistory.com/253

 

XSS - Spring lucy servlet filter 적용하기

적용은 차근차근 따라오면 크게 어렵지 않게 적용이 가능합니다. maven을 통해 적용합니다. 프로젝트에 lucy적용하기 pom.xml com.navercorp.lucy lucy-xss-servlet 2.0.1 다음은 필터를 프로젝트에 설정해주어

myhappyman.tistory.com

 

▼▼▼아래 링크는 네이버 lucy 깃허브 url입니다.

https://github.com/naver/lucy-xss-servlet-filter

 

GitHub - naver/lucy-xss-servlet-filter

Contribute to naver/lucy-xss-servlet-filter development by creating an account on GitHub.

github.com

 

반응형
반응형

form태그 내부에 input태그가 단일로 존재하면 엔터 입력시 submit동작이 이루어지는걸 알고 계셨나요?

 

input, button태그 하나씩 구성된 form태그를 만들고 이벤트를 작성중이였습니다.

button태그 타입도 submit이 아닌 button으로 지정하였고, js에 key event도 작성하지 않았는데, input에 글을 입력하고 엔터키를 누르면 자동으로 submit동작이 발생하였습니다.(저는 ajax를 통해 데이터를 처리하고 싶었습니다!)

 

다음은 문제가 발생한 코드 예제입니다.

<form name="agencyAddForm" action="" autocomplete="off">
    <div class="form-group m-t-10">
        <label class="control-label col-md-2 col-sm-2 col-xs-2">회사명</label>
        <div class="col-md-8 col-sm-8 col-xs-8">
            <input type="text" id="agencySearchName" class="form-control">
        </div>
        <div class="col-md-2 col-sm-2 col-xs-2">
            <button type="button" id="agencySearchBtn" class="btn btn-success btn-sm">찾기</button>
        </div>
    </div>
</form>

(부트스트랩을 사용하였기에 class가 많습니다.)

다른 내용은 볼 필요가 없고 form태그 내부를 보시면 input태그는 단일 한개가 존재하고 있습니다.

form내부에 input태그가 한개만 존재하면 자동으로 submit동작이 발생한다고 합니다.

 

해결방법

- onsubmit="return false;" 속성 추가하기

<form name="agencyAddForm" action="" autocomplete="off" onsubmit="return false;">
	...
</form>

이를 방지하기 위해 form태그 내부에 onsubmit이벤트를 추가하였습니다.

 

그외 방법으로 불필요한 input text태그를 숨기거나, hidden태그등을 넣어도 되지만, submit동작을 사용하진 않을거라 막아두었습니다.

 

 

ps. 신입때도 form태그 내부에 button태그에 type을 지정을 안하고 기본 동작인 submit동작이 되어서 고생했던 기억이 있는데, 역시 기초가 중요한것 같습니다!!!

1. form태그에 input이 하나만 있으면 엔터입력시 자동으로 submit이 동작한다.

2. form태그에 button이 있고 type지정을 안하면 submit이 동작한다.

 

2가지만 기억하시면 시간낭비가 줄어드실거 같습니다😙

반응형
반응형

개발 도중이나 실운영시에 문제가 발생하면 항상 제일 먼저 확인하는것이 로그파일부터 찾게 됩니다. 오류에 대한 흔적이나 로직상 개발자가 남겨둔 정보가 있기 때문인데, Log4j2 설정에 대해 정리를 해봅니다.

 

XML 위치

먼저 log4j2.xml 파일의 위치는 WEB-INF/classes하위에 위치시킵니다.

(개발 구조에서는 resources 밑에 위치합니다.)

 

파일 내부 구조

xml내부에는 <Configration> 최상위에 위치하고 <Logger>, <Appender>가 존재합니다.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
  </Appenders>
  <Loggers>
    <Root level="ERROR">
      <AppenderRef ref="Console"/>
    </Root>
  </Loggers>
</Configuration>

 

Logger 설정

Logger부분에서는 어플리케이션의 단위별, 수행 단계를 설정합니다.

name, level, additivity 속성들로 이루어져 있습니다.

<Logger name="egovframework" level="DEBUG" additivity="false">
    <AppenderRef ref="console" />
</Logger>

🟧name: 로거의 이름(java.sql, jdbc.sqltiming...)

🟧level: 로그의 레벨(DEBUG, INFO, ERROR, OFF...)

🟧additivity: 중복로깅여부 (true, false)

 

Appender 설정

로그가 출력되는 위치를 설정합니다.

이클립스등에서 콘솔창에 로그가 출력되는 양식이라거나 로그를 파일로 출력하는 위치, 구조등을 설정합니다.

<Console>, <File>, <RollingFile> 등이 주로 사용됩니다.

구조는 아래와 같이 작성됩니다.

 <Appenders>
   <Console name="console" target="SYSTEM_OUT">
	<PatternLayout pattern="%d %5p [%c] %m%n" />
   </Console>
   <RollingFile name="file" fileName="./logs/${date:yyyy}/${date:MM}/dailyLog.log">
   </RollingFile>
 </Appenders>

RollingFile의 경우 FileAppender의 개선된 형태로 특정 크기이상의 파일로 커지면 기존 파일을 백업파일로 변경하고 다시 로깅을 시작합니다.

 

실제로 나중에 파일을 생성하는 RollingFile쪽을 좀 더 살펴 보겠습니다.

🟧name: 로거의 이름입니다. 추후 AddenderRef와 같은 태그에서 참조를 할때 해당 name을 사용합니다.

🟧fileName: 로그 파일의 생성 경로 및 파일 이름을 지정합니다.

(C:/test/dailyout.log로 지정하면 c:/text 디렉토리 내부에 dailyout.log형태로 생성됩니다.)

🟧filePattern: 파일의 생성 패턴입니다. 서버 일자가 변경되거나 특정 크기가 커지는 등 변화가 필요할때 사용되는 옵션입니다. 이를통해 로그 파일들을 구분합니다.

     🔸${date:yyyy}: 금일 년도만 가져옵니다 (ex) 2019, 2020, 2021)

     🔸${date:MM}: 금일 월을 가져옵니다. (ex) 01, 02, 03, 04 ... 11, 12)

     🔸%d{yyyyMMdd}: 금일 일자 조합

 

 

 

 

 

✅아래는 실제로 사용한 Log4j2.xml 구조입니다.

<RollingFile>의 fileNamefilePattern부분에 로그 위치만 지정하시고 각 로거별 레벨을 수정해서 사용하면 됩니다.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
		<Console name="console" target="SYSTEM_OUT">
		    <PatternLayout pattern="%d %5p [%c] %m%n" />
		</Console>
		<RollingFile name="file" fileName="로그 디렉토리 위치/${date:yyyy}/${date:MM}/dailyLog.log"
								filePattern="로그 디렉토리 위치/${date:yyyy}/${date:MM}/dailyLog_%d{yyyyMMdd}.log">
			<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
			<Policies>
				<TimeBasedTriggeringPolicy />
			</Policies>
		</RollingFile>
    </Appenders>
    <Loggers>
		<Logger name="java.sql" level="OFF" additivity="false">  <!--OFF    DEBUG   ERROR   INFO  -->
			<AppenderRef ref="console" />
		</Logger>
		<Logger name="egovframework" level="DEBUG" additivity="false">
			<AppenderRef ref="console" />
		</Logger>
		<Logger name="jdbc.sqltiming" level="OFF" additivity="false">
			<AppenderRef ref="console" />
		</Logger>
		<Logger name="org.springframework" level="INFO" additivity="false">
			<AppenderRef ref="console" />
		</Logger>
		<Root level="ERROR">
			<AppenderRef ref="console" />
			<AppenderRef ref="file"/>
		</Root>
    </Loggers>
</Configuration>

 

반응형
반응형

5+@ 년전쯤 개발된 소스를 추가 개발하게 되었는데, 레거시 버전들로 이루어져 있었고 고객사에서도 버전업이 가능한지에 대한 문의가 와서 버전업을 진행해보았습니다.

하나씩 버전을 변경하고 구동하다보니 아래와 같은 에러들이 발생했는데🤣, pom.xml부분을 차근차근 수정해서 구동하는데 성공하였습니다.

 

ASM ClassReader failed to parse class file - probably ~....

 

버전업을 하면서 발생한 오류들...2

 

구성 버전

 

먼저 pom.xml을 열어서 수정을 진행합니다.

- 스프링 버전을 4.3.22.RELEASE로 올려줍니다.

<properties>
	<spring.maven.artifact.version>4.3.22.RELEASE</spring.maven.artifact.version>
	<egovframework.rte.version>3.9.0</egovframework.rte.version>
	<spring.security.version>3.2.4.RELEASE</spring.security.version>
	<poi.version>3.12</poi.version>
</properties>

비교를 위한 기존 버전

(스프링 시큐리티부분은 딱히 사용하고 있지 않아 그대로 두었다.)

 

 

- Java11로 올리면서 의존성 오류가 발생하여 maven을 추가하였습니다.

<!-- 자바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 이슈 -->

 

- 자바와 톰캣 버전을 변경합니다.

<pluginManagement>
	<plugins>
		<plugin>
			<groupId>org.apache.tomcat.maven</groupId>
			<artifactId>tomcat8.5-maven-plugin</artifactId>
			<version>2.2</version>
			<configuration>
				<port>80</port>
				<path>/</path>
				<systemProperties>
					<JAVA_OPTS>-Xms256m -Xmx768m -XX:MaxPermSize=256m</JAVA_OPTS>
				</systemProperties>
			</configuration>
		</plugin>
		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-compiler-plugin</artifactId>
			<configuration>
				<source>1.11</source>
				<target>1.11</target>
				<encoding>UTF-8</encoding>
			</configuration>
		</plugin>
	</plugins>
</pluginManagement>

톰캣 : tomcat7.0-maven-plugin -> tomcat8.5-maven-plugin

자바 : 1.7 -> 1.11

 

이후 maven update -> clean 이후 구동해봅니다.

 

 

 

반응형
반응형

카카오 주소🏠 찾기 API

 

https://postcode.map.daum.net/guide

 

Daum 우편번호 서비스

우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.

postcode.map.daum.net

 

해당 공식 홈페이지에 접속해보면 어렵지 않게 연동을 해 볼 수 있습니다.

요즘 많은 사이트들의 주소 입력창을 보면 아래 캡처와 같은 형태를 자주 접할 수 있었는데, 많은 사이트에서 활용 중인것 같습니다.🙆‍♂️

카카오 주소찾기 양식

 

카카오는 카카오 발송, 지도 API 등 다양한 서비스를 제공하는데 사용을 위해 키 발급, 일일 사용량 제한 또는 용도에 따른 유료처리 등 이슈가 존재합니다.

하지만, 주소 API는 조금 다르게 제공이 됩니다. 👀

아래는 제공되는 형태와 제한에 대한 내용입니다.

https://postcode.map.daum.net/guide

덕분에 연동방법만 익히면 굉장히 쉽게 사용할 수 있습니다. (뭐 거의 준비물이 외부망이 되는 환경이면 끝이네요! 😎)

 

사용법은 아래와 같습니다.

<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
    new daum.Postcode({
        oncomplete: function(data) {
            // 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분입니다.
            // 예제를 참고하여 다양한 활용법을 확인해 보세요.
        }
    }).open();
</script>

 

카카오 주소찾기 적용 예제🐱‍🏍

<!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>Kakao API</title>
</head>
<body>
    <table>
        <tr>
            <th>이름</th>
            <td><input type="text" name="user_name"></td>
        </tr>
        <tr>
            <th>주소</th>
            <td><input type="text" id="address_kakao" name="address" readonly /></td>
        </tr>
        <tr>
            <th>상세 주소</th>
            <td><input type="text" name="address_detail" /></td>
        </tr>
    </table>
</body>
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
<script>
window.onload = function(){
    document.getElementById("address_kakao").addEventListener("click", function(){ //주소입력칸을 클릭하면
        //카카오 지도 발생
        new daum.Postcode({
            oncomplete: function(data) { //선택시 입력값 세팅
                document.getElementById("address_kakao").value = data.address; // 주소 넣기
                document.querySelector("input[name=address_detail]").focus(); //상세입력 포커싱
            }
        }).open();
    });
}
</script>
</html>

테스트 예시.gif

 

반응형
반응형

구동중인 프로젝트의 특정 경로에서 데이터를 가져와서 추가 처리가 필요한 상태였는데, 서버의 구동중인 절대 경로를 가져오기 위해 아래의 구문을 사용해보니 deprecated처리가 되어있습니다.🙄

@RequestMapping(value = "/report.do",method = RequestMethod.POST)
public @ResponseBody HashMap<Object, Object> report(@RequestParam HashMap<Object, Object> param,
													HttpServletRequest request) {
	String absolutePath = request.getRealPath(request.getContextPath()); //deprecated
	return param;
}

 

문서를 찾아가보니 ServletContext.getRealPath로 대체한다고 되어있습니다.😃

 

request.getSession().getServletContext().getRealPath("/");

 

아래처럼 변경된 문법을 통해 절대 경로를 처리할 수 있습니다.

@RequestMapping(value = "/report.do",method = RequestMethod.POST)
public @ResponseBody HashMap<Object, Object> report(@RequestParam HashMap<Object, Object> param,
														HttpServletRequest request) {
	String absolutePath = request.getSession().getServletContext().getRealPath("/");
	return param;
}
반응형
반응형

비밀번호등을 받기 위해 숫자나 특정 단어 하나씩 입력하고 다음칸으로 이동하는 형태의 UI를 많이 접할 수 있습니다.

해당 포스팅에서는 하나의 Input태그마다 입력을 받으면 다음칸으로 이동하고 숫자만 받는 형태를 작성해보겠습니다.

 

단순하게 maxlength와 number값 타입을 지정하더라도 영문 'e' 또는 한글 자음 모음등이 입력되는 현상😓을 볼 수 있는데, 정규식과 사용할 수 있는 이벤트등을 활용하여 처리하였습니다.😋

 

좀 더 좋은 깔끔한 방식이 있다면 댓글 부탁드립니다!🙆‍♀️

<input type="tel" maxlength="1" min="0" max="9" onlyNumber>
$(function(){
    $(document).on("keypress keyup keydown", "input[onlyNumber]", function(e){
        console.log(e.which);
        if(/[a-z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/g.test(this.value)){ //한글 막기
            e.preventDefault();
            this.value = "";
        }else if (e.which != 8 && e.which != 0 //영문 e막기
            && e.which < 48 || e.which > 57    //숫자키만 받기
            && e.which < 96 || e.which > 105){ //텐키 받기
            e.preventDefault();
            this.value = "";
        }else if (this.value.length >= this.maxLength){ //1자리 이상 입력되면 다음 input으로 이동시키기
            this.value = this.value.slice(0, this.maxLength);
            if($(this).next("input").length > 0){
                $(this).next().focus();
            }else{
                $(this).blur();
            }
        }
    });    
});

태그 input에 type을 tel로 한 이유는 모바일 환경에서 넘버패드로 먼저 동작하도록 하기 위해 처리하였고, input태그 내부에 onlyNumber라는 속성이 있으면 동일하게 동작하도록 처리하였습니다.

 

마지막 else if문에서 길이를 체크하고 maxlength값만큼 커지거나 같아지면 다음 input이 있는지 체크 후 자동으로 포커싱하는 스크립트입니다.

 

 

동작테스트는 아래 codePen⌨을 활용해주세요!

See the Pen Get one number by one by myhappyman (@myhappyman) on CodePen.

 

 

 

 

반응형
반응형

보통 스케줄러를 구성할때 크론탭으로 일정시간을 하드코딩으로 넣거나 프로퍼티 또는 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 박스에 크론탭형태로 데이터를 넣고 시간 변경을 하면 변경된 크론 형태로 스케줄러가 동작하고, 중지를 누르면 기존에 동작하던 스케줄러가 멈추도록 구성하였다.

반응형