Spring WebSocket & Async
검색팀 송원석
YSM 사내 교육자료
요구사항
하루치 키워드별
검색 상품수를 뽑아주세요
- 하루 약 20만 키워드
-
실제 검색을 하나하나 날려봐야 검색 카운트를 파악 가능
-
20만 키워드 검색시 예상 소요시간 : 4~5시간
아귀찮
처음엔 단순하게
1. 키워드 목록을 쭉 가져와서
2. 키워드마다 다시 서버 호출
3. 받은 result값으로 화면 갱신
//키워드마다 다시 ajax call
$.ajax("searchCount.do", {
data : {},
type : 'POST',
tryCount: 0,
retryLimit : 5,
success : function(data) {
if(data) {
// 화면갱신
}
}
});
//검색시작 버튼 클릭시
$("#start").click(function(){
$.ajax("getKeywordList.do",{
data : {
"hello" : "world"
},
success : function(result) {
totalCount = result.totalCount;
list = result.list;
//키워드 하나씩 계속 루프 돌면서 처리
search();
}
});
});
$("#search0").text(searchSummary + requestCnt);
$("#keyword0").text(++keywordSummary);
$("#click0").text(clickSummary + clickCnt);
$("#searchClick0").text(searchClickSummary.toFixed(3) );
참쉽죠?
대충 만드니 문제발생
- 나한테만 실행결과 보임
- 화면을 새로고침 하면 모든 데이터가 날라감!
- 잠깐이라도 네트워크가 끊기면 문제발생
- 브라우저 화면을 못 끔 (끄면 처음부터 다시)
- 화면에 뿌린 수십만 키워드를 복사하니 브라우저가 사망(!)
어떻게 하지?
서버에서 백그라운드로 돌아가게 바꿔볼까?
예상되는 문제점
- 긴 request로 인한 Connection Timeout
- progress를 실시간으로 보지못함 (어느정도 실행 됐나?)
- 만일 여러사람이 실행 한다면? 데이터가 꼬이지 않을까?
- Batch는 너무 잔손이 많이간다. (쉽게쉽게 갈 수 없을까?)
@Async
이렇게 바꿔봤어요
AS-IS | TO-BE | |
---|---|---|
서버 호출 | 키워드 건바이건 | 딱 한번만 호출 |
키워드목록 | 클라이언트에서 루프처리 | 서버에서 루프처리 |
진행 progress 정보 | 건바이건 자바스크립트 연산 | static하게 서버에 정보를 가지고있음 |
화면 갱신 | 건바이건 자바스크립트 화면갱신 | 주기적으로 서버를 호출해서 진행 정보 가지고옴 |
실행 종료시 | X | 엑셀로 export해서 다운로드 가능하게 처리 |
설정은 이렇게
<task:executor id="asyncExecutor" pool-size="10-100" queue-capacity="10" rejection-policy="ABORT" />
<task:annotation-driven executor="asyncExecutor" />
XML 기반
사용은 이렇게
@Async
public void existsCheckList(SearchRequestBean reqBean, HashMap<String, String> param) throws InterruptedException {
logger.info("exists shop job start");
// 잡 초기화
Job job = JobTaskUtil.startJob(CommonUtil.EXISTS_SHOP_JOB_ID);
// 키워드 목록을 가지고온다.
List<KeywordLogBean> keywordList = searchDAO.getKeywordList(param);
job.setTotalCount(keywordList.size());
reqBean.setSiteid("1");
reqBean.setMode("simple");
reqBean.setPageSize(1);
if (StringUtils.isEmpty(reqBean.getServer())) {
reqBean.setServer(Const.TEST_SERVER);
}
String opt = CommonUtil.getLastQueryType();
int progressCount = 0;
// 키워드 마다 루프를 돔
for (KeywordLogBean keyword : keywordList) {
if (!job.getIsRunning()) {
break;
}
if (StringUtils.isEmpty(opt)) {
opt = CommonUtil.getLastQueryType();
}
reqBean.setKwd(keyword.getKeyword());
String[] kql_str = kqlGenerator.makeKQLString2(opt, reqBean, true);
// 키워드별로 코난 검색엔진 호출
SearchResultBean resultList = KonanModule.searchTestList(kql_str, reqBean);
if (resultList.getTotalCount() <= 0 && StringUtils.isEmpty(reqBean.getSid())) {
resultList = KonanModule.searchTestList2(kql_str, reqBean);
}
// 카운트를 받아와서 체크
int count = resultList.getTotalCount() + resultList.getSecondTotalCount();
if (count > 0) {
HashMap<String, String> map = new HashMap<>();
map.put("keyword", keyword.getKeyword());
map.put("count", Integer.toString(count));
job.getList().add(map);
}
progressCount++;
job.setProgressCount(progressCount);
}
// 엑셀 생성 로직 이관
JobTaskUtil.completeJob(job.getJobId());
logger.info("exists shop job done");
}
method에 @Async 만 쓰면 OK
@Async사용시 http thread가 아닌 별도의 thread를 내부적으로 만들어서 사용
@Async 사용시 주의점
- method 타입은 void혹은 Future타입만 가능 (Future는 callback 처럼 실행됨)
- public method 만 가능
- async method내부에서 response, multipartfile등을 제어하지 말자. async method가 동작하는 타이밍엔 이미 request가 끝난 상태기 때문에 알 수 없는 에러발생
- 꼭 method 내부에서 try catch 걸어놓자. throws안먹힘. 에러는 꼭 method 내부에서 처리해야함.
잘 동작은 하는데...
화면갱신이 예전보다 매끄럽지 못함
건by건 -> 가끔으로 다운그레이드됨
websocket을 써볼까?
nodejs에서는 내가 금방 세팅 했었는데...
욕심의 서막;
Spring WebSocket
우선 문서를 보고 설정
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<absolute-ordering>
<name>spring_web</name>
</absolute-ordering>
</web-app>
- servlet3.0을 서버에서 지원해야함
(tomcat7.0.47+, Jetty 9.1+)
- 현재 spring application이 가장 높은 우선순위를 가지게 설정
우선 문서를 보고 설정2
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@Bean
public WebSocketHandler myHandler() {
return new MyHandler();
}
}
- @EnableWebSocket 으로 websocket을 사용하게 선언
- .withSocketJS() 설정으로 URL을 socketJS와 연동 설정
안된다...
SocketJs에서 알 수 없는 404에러 발생
STOMP 설정을 안해서 그런가?
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/app");
config.enableSimpleBroker("/topic", "/queue");
}
}
그래도 안된다...
이번엔 stomp에서 알 수 없는 404에러 발생
원인은
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<absolute-ordering>
<name>spring</name>
</absolute-ordering>
servlet-mapping의 url-pattern이 특정 확장자로만 지정
되어 있으면 오류 발생.
url-pattern을 / 로 설정해서
모든 request를 spring에서 받게 설정
그래도 안되지만 다른에러
모든 스크립트 & 이미지파일 404에러 발생
resource handler
spring 설정에서 js와 image경로를 resource handler로 설정
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/common/**").addResourceLocations("/common/");
registry.addResourceHandler("/images/**").addResourceLocations("/images/").setCachePeriod(31556926);
}
it works!
힘든 여정이었다
사용방법
JSP에선
<script src="<c:url value='/common/js/sockjs-0.3.4.js'/>"></script>
<script src="<c:url value='/common/js/stomp.js'/>"></script>
var sock = new SockJS('/RankPilot/websocket');
stompClient = Stomp.over(sock);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/jobStatus_checkSearchCount', function(obj){
if (obj) {
var jobStatus = JSON.parse(obj.body);
console.info(jobStatus);
}
});
});
JAVA에선
@Configuration
@EnableWebSocketMessageBroker
public class AppWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/websocket", "notify", "/jobStatus*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/RankPilot");
}
}
client -> server
stompClient.send('/topic/notify', {}, 'hello world');
@Controller
public class WebsocketController {
@MessageMapping("/notify")
@SendTo("/topic/notify")
public String notify(String message) {
return message;
}
}
server -> client
@Autowired
private SimpMessagingTemplate template;
public void sendWebsocketJobStatus(String jobId, Job job) {
HashMap<String, Object> returnMap = new HashMap<>();
returnMap.put("totalCount", job.getTotalCount());
returnMap.put("progressCount", job.getProgressCount());
returnMap.put("isRunning", job.getIsRunning());
returnMap.put("reportStatus", job.getReportStatus());
returnMap.put("errorMsg", job.getErrorMsg());
returnMap.put("totalTimeMsg", job.getTotalTimeMsg());
template.convertAndSend("/topic/jobStatus_" + jobId, returnMap);
}
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
<version>${spring-version}</version>
</dependency>
server -> client
var sock = new SockJS('/RankPilot/websocket');
stompClient = Stomp.over(sock);
stompClient.connect({}, function(frame) {
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/jobStatus_checkSearchCount', function(obj){
if (obj) {
var jobStatus = JSON.parse(obj.body);
$(".endCount").html(jobStatus.progressCount);
$("#endProgress").html(jobStatus.totalCount);
var percent = jobStatus.progressCount / jobStatus.totalCount * 100;
$("#progressBar").css({width: (percent + "%")});
$("#jobPercent").html(Math.floor(percent));
$("#totalTimeMsg").html("[약 " +jobStatus.totalTimeMsg + "]");
}
});
});
server -> client
특정 사용자에게만 메시지 전송의 경우
@SendToUser or
template.convertAndSendToUser 사용
stompClient.subscribe(destination, callback, { id: mysubid });
시연
Q&A
참고문헌
다음주제선정
- Basic of Javascript
- Eclipse IDE 꿀팁
- Apache Zeppelin
- 쿼리를 빠르게 하자(기초과정)
- 디버깅과 테스트
spring websocket & @Async
By wonseok
spring websocket & @Async
내부발표자료_2015.12.17
- 222