반응형

체크박스나 라디오등을 제어하다보면 jQuery에 의존해서 많이 사용하게 되는데 순수 javascript만의 힘으로 제어하는 방법을 알아보겠습니다.

 

생각보다 사용법이 jQuery와 많이 다르지 않으며 비슷하기에 어렵지 않을 것입니다.

무엇보다도 추가적으로 라이브러리(jQuery)가 필요하지 않기에 빠르기도하고 간단하게 데이터를 확인할 수 있습니다.

 

 

체크박스의 체크 유무

먼저 체크박스값에 체크가 되어있는지 확인해보겠습니다.

.checked 를 통해 체크 유무를 검사할 수 있습니다.

 

html

<input type="checkbox" name="test1" value="test1">TEST1

 

js

var chk = document.querySelector("input[name=test1]").checked;
console.log(chk);

 

결과

체크가 해제되어있으면 false
체크되어 있다면 true

 


체크된 체크박스의 값 가져오기

이번에는 취미생활에 체크된 데이터의 value를 가져와 보겠습니다.

 

hobby.html

<form>
    <input type="checkbox" name="hobby" value="music">음악감상
    <input type="checkbox" name="hobby" value="game">게임
    <input type="checkbox" name="hobby" value="travel">여행
    <input type="checkbox" name="hobby" value="soccer">축구
    <input type="checkbox" name="hobby" value="reading">독서
    <input type="checkbox" name="hobby" value="cooking">요리
</form>

 

hobby.js

var chkList = document.querySelectorAll("input[name=hobby]:checked");
chkList.forEach(function (ch) {
    console.log(ch.value);
});

 

결과

querySelectorAll을 활용하여 name속성이 hobby인 데이터 중에 :checked속성이 있는 데이터만 추출하여 chkList에 담아두었습니다.

chkList를 forEach메소드를 통해 각각 요소마다 접근하여 value값을 console.log를 통해 출력하는 예제입니다.

 

해당부분을 특정 배열에 push하여 담아도 되고, 문자열에 붙여서 처리하면 될 것 같습니다.


모든 체크박스 선택하기

게시판이나 여러 형태를 작성하다보면 특정값이 모두 선택되게 해야하는 경우가 있습니다.

change 이벤트를 추가하여 제어해보겠습니다.

 

See the Pen vanillaJS CheckBox by myhappyman (@myhappyman) on CodePen.

결과

전체선택

hobbyAll의 요소를 선택하여 change 이벤트가 발생하게 되면

name값이 hobby인 데이터를 모두 찾아 속성값을 전체선택된 데이터값으로 변경처리를 해줍니다.

반응형
반응형

스프링 부트 프로젝트를 진행하다가 발생한 에러이다.

원인 분석

서비스에서 구성하는 dao객체를 autowired 어노테이션이 아닌 생성자를 활용하여 주입하는 방식으로 구성하였는데, 예를들면 아래와 같이 변경하였다.

 

기존

public class DataService{
    @Autowired
    private DataDao datadao;
}

 

변경방식

@AllArgsConstructor
public class DataService{
	@NotNull private DataDao datadao;
}

 

Lombok을 활용하여 Bean으로 등록된 dao 사용하기 위해 생성자 방식을 통해 처리하였다.

 

소스를 전달받고 구동하였으나 해당방식으로 처리한 부분만 계속해서 NullPointException이 발생하였고, 결과값이 Null인지부터 연결정보가 잘못 되었는지 모두 체크하였으나 옆 개발자분 PC와 소스부분은 틀린점이 하나도 없었다.

 

 

확인해 본 결과 Lombok이 정상적으로 동작하지 않는것을 발견하였다.

 

해결 방법

해결방법은 아래와 같다.

1-1. maven으로 받아진 lombok.jar를 구동한다.

프로젝트 내 Maven Dependencies에서 lombok을 찾는다.

 

1-2. 또는 cmd창을 열어 위치를 찾아가서 명령어를 입력한다.

cmd에서 처리

 

2. 그럼 구동된 lombok.jar파일이 보인다.

구동하고 일정시간 기다리면 구동중인 IDE를 검색하는데 해당 IDE를 Install/Update를 클릭한다.

 

 

3. 정상적으로 설치가 완료되면 설치된 IDE가 열러있다면 종료 후 재시작을 하고 서비스를 올려보면 정상 동작하는 모습을 볼 수 있다.

 

반응형
반응형

WebFlux에서 몽고 DB 연동하는 법을 알아 보겠습니다.

 

Webflux에서 몽고 DB 연동하기

1. 프로젝트 생성하기

프로젝트 생성

 

디펜던시도 선택해줍니다.

Lombok, 반응형 스프링용으로 나온 Reactive Mongo, Reactive Web까지 선택하였습니다.

 

그리고 Finish를 해줍니다.

 

2. 뷰 설정

뷰 페이지에서 사용할 thymeleaf가 빠졌네요. 추가된 pom.xml 입니다.

<dependencies>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
	</dependency>
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-webflux</artifactId>
	</dependency>

	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<optional>true</optional>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
		<exclusions>
			<exclusion>
				<groupId>org.junit.vintage</groupId>
				<artifactId>junit-vintage-engine</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
	<dependency>
		<groupId>io.projectreactor</groupId>
		<artifactId>reactor-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

 

WebConfig.java

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;

@EnableWebFlux
@Configuration
public class WebConfig implements ApplicationContextAware, WebFluxConfigurer {
	
	ApplicationContext context;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = context;
	}
	
	@Bean
	public ITemplateResolver thymeleafTemplateResolver() {
	    final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
	    resolver.setApplicationContext(this.context);
	    resolver.setPrefix("classpath:templates/");
	    resolver.setSuffix(".html");
	    resolver.setTemplateMode(TemplateMode.HTML);
	    resolver.setCacheable(false);
	    resolver.setCheckExistence(false);
	    return resolver;
	}
	
	@Bean
	public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() {
	    SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine();
	    templateEngine.setTemplateResolver(thymeleafTemplateResolver());
	    return templateEngine;
	}
	
	@Bean
	public ThymeleafReactiveViewResolver thymeleafReactiveViewResolver() {
	    ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver();
	    viewResolver.setTemplateEngine(thymeleafTemplateEngine());
	    return viewResolver;
	}
	
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
	    registry.viewResolver(thymeleafReactiveViewResolver());
	}
}

view 설정을 해줍니다.

resources/templates 하위에 있는 html파일을 읽도록 처리할 예정입니다.

 

3. document 설정

Lombok을 활용한 document를 설정합니다.

JPA로 만들기 때문에 매핑할 Movie 클래스를 정의하였습니다.

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@Document(collection="list")
public class Movie {
	@Id
	private String id;
	private String title;
	private String director;
	private String since;
	private int audienceCnt;
	
	public Movie(String title, String director, String since, int audienceCnt) {
		super();
		this.title = title;
		this.director = director;
		this.since = since;
		this.audienceCnt = audienceCnt;
	}
}

 

몽고 DB 데이터는 아래와 같습니다.

 

 

 

4. repository 생성하기

MovieRepository.java

import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;

import com.psw.movie.document.Movie;

@Repository
public interface MovieRepository extends ReactiveMongoRepository<Movie, String>{
}

ReactiveMongoRepository를 상속받아 구현합니다.

 

 

5. 서비스 생성

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import com.psw.movie.document.Movie;
import com.psw.movie.repository.MovieRepository;

import reactor.core.publisher.Flux;

@Service
public class MovieService {

	private final ApplicationEventPublisher publisher;
	private final MovieRepository movieRepository;
	
	public MovieService(ApplicationEventPublisher publisher, MovieRepository movieRepository) {
		this.publisher = publisher;
		this.movieRepository = movieRepository;
	}
	
	public Flux<Movie> allMovies(){
		return movieRepository.findAll();
	}
}

생성한 repository에서 데이터를 가공하거나 변경할 서비스를 생성합니다.

해당 포스팅에서는 다른 기능없이 전체를 가져오는 findAll()메소드를 통해 전부 가져올 예정입니다.

 

6. handler 작성

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import com.psw.movie.document.Movie;
import com.psw.movie.service.MovieService;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
public class MovieHandler {
	
	private final MovieService movieService;
	
	public MovieHandler(MovieService service) {
		this.movieService = service;
	}
	
	final static MediaType TEXT_HTML = MediaType.TEXT_HTML;
	final static MediaType APPLICATION_JSON = MediaType.APPLICATION_JSON;
	
	//movie.html로 변환
	public Mono<ServerResponse> movie(ServerRequest request){
		return ServerResponse.ok().contentType(TEXT_HTML).render("movie");
	}
	
	//요청이오면 JSON형태로 데이터 파싱
	public Mono<ServerResponse> getMovieList(ServerRequest request){
		Flux<Movie> list = this.movieService.allMovies();
		return ServerResponse.ok().contentType(APPLICATION_JSON).body(list, Movie.class);
	}
}

서비스를 사용하여 모든 영화리스트를 가져오는 핸들러를 작성합니다.

 

 

7. 핸들러를 연결할 라우터를 작성합니다.

요청에 따라 파싱해줄 라우터입니다.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import com.psw.movie.handler.MovieHandler;

@Configuration
public class MovieRouter {
	
	final static MediaType TEXT_HTML = MediaType.TEXT_HTML;
	final static MediaType APPLICATION_JSON = MediaType.APPLICATION_JSON;
	
	@Bean
	public RouterFunction<ServerResponse> index(MovieHandler movieHandler){
		return RouterFunctions.route(
				RequestPredicates.GET("/").and(RequestPredicates.accept(TEXT_HTML)), movieHandler::movie)
				.andRoute(RequestPredicates.GET("/movie").and(RequestPredicates.accept(TEXT_HTML)), movieHandler::movie);
	}
	
	@Bean
	public RouterFunction<ServerResponse> getMoiveList(MovieHandler movieHandler){
		return RouterFunctions.route(
					RequestPredicates
						.POST("/getMovieList")
						.and(RequestPredicates.accept(APPLICATION_JSON)), 
						movieHandler::getMovieList);
	}
	
	@Bean
	public RouterFunction<ServerResponse> getMoiveListSearch(MovieHandler movieHandler){
		return RouterFunctions.route(
					RequestPredicates
						.POST("/getMovieList/{req}")
						.and(RequestPredicates.accept(APPLICATION_JSON)), 
						movieHandler::getMovieList);
	}
}

 

 

8. html소스 작성

마지막으로 뷰페이지 html소스를 작성하고 부트 프로젝트를 구동합니다.

movie.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>Movie</title>
		<style>
	    	*{padding:0; margin:0;}
	    	table tr th{background-color: #FFBB00;}
	    	table tr th, td{border: 1px solid black; text-align: center; width: 200px;padding: 5px;}
	    	.inputMovieData tr td{padding: 5px;}
	    </style>
	</head>
	<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
	<body>
		<h1>WebFlux Asynchronous</h1>
		<br/><br/><br/>
		<h1>Movie List</h1>
			<table id="MovieTable">
				<tr>
					<th>제목</th>
					<th>감독</th>
					<th>연도</th>
					<th>관객수</th>
				</tr>
			</table>			
	</body>
	<script>
	$(document).ready(function(){
	    init();
	});
	
	function init(){
		reqAjaxPost("/getMovieList", "", fnSuccess);
	}
	
	function fnSuccess(res){
		if(res != null){
			var tag = "<tr><th>제목</th><th>감독</th><th>연도</th><th>관객수</th>	</tr>";
			res.forEach(function(d){
				tag += "<tr>" +
							"<td>" + d.title + "</td>" +
							"<td>" + d.director + "</td>" +
							"<td>" + d.since + "</td>" +
							"<td>" + d.audienceCnt + "</td>" +
						"</tr>";
			});
			
			$("#MovieTable").empty().append(tag);
		}
	}
	
	function reqAjaxPost(url, param, fnSuccess){
		$.ajax({
			url: url,
			type: "POST",
			data: param,
			dataType: "JSON",
			success: function(args){
				fnSuccess(args);
			},
			error: function(args){
				alert("에러가 발생했습니다.");
			}
		});
	}
	</script>
</html>

 

 

완성된 소스의 구조

프로젝트 구조

 

 

스프링 부트 프로젝트를 구동합니다.

로컬 몽고DB에서 데이터를 가져와 영화정보를 파싱하는 예제를 작성해보았습니다.

반응형
반응형

branch 정보를 보기 위해 git branch입력을 해보면 로컬 저장소에 등록된 브랜치 리스트만 출력이 됩니다.

 

원격저장소에서 데이터를 가져오는 방법을 알아보겠습니다.

 

원격저장소에서 브랜치 정보 가져오기

원격 저장소 업데이트

먼저 원격 저장소를 업데이트를 해줍니다.

git remote update

새로운 저장소 정보를 가져왔다!

 

 

원격 저장소의 브랜치 정보 확인하기

다음으로 브랜치 정보를 확인합니다.

-r 옵션은 저장소의 브랜치 리스트만 -a옵션은 모든 브랜치 리스트를 확인합니다.(로컬 + 원격)

git branch -a  //원격 + 로컬
git branch -r  //원격

원격 저장소의 브랜치 정보들

 

 

원격 저장소에서 브랜치 가져오기

-t 옵션과 checkout을 사용하여 브랜치를 가져옵니다.

git checkout -t [브랜치명]

git checkout -t [원격 저장소 브랜치명]
//ex
git checkout -t origin/rts_list

반응형
반응형

https://myhappyman.tistory.com/109

 

Spring WebFlux - WebFlux 알아보기... index.html연동

최근 Spring에서 반응형 코딩이 가능하다는 이야기를 들었고 팀원끼리 학습을 해보기로 하여 rx.js를 통해 컨셉을 이해해보고 비동기, 동기~ 블로킹이니 단어나 개념들부터 차근차근 잡고 학습을 진행해보았습니다..

myhappyman.tistory.com

해당 글에서는 페이지에 접근할 때 Thymeleaf를 활용하여 해당 태그에 데이터를 파싱하는 예제를 확인하였습니다.

 

이번에는 ajax를 통해 비동기로 해당 페이지에 데이터를 파싱하는 예제를 확인해보겠습니다.

 

ajax를 통한 Flux데이터 파싱하기

1. 먼저 스프링부트 프로젝트를 생성합니다.

 

생성할 구조는 아래와 같습니다.

프로젝트 구조

 

2. 디펜던시 설정을 합니다.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

* webflux, thymeleaf 사용을 위해 적용

 

3. 프로젝트의 뷰 설정을 합니다.

WebConfig.java

package com.psw.movie.config;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;

@EnableWebFlux
@Configuration
public class WebConfig implements ApplicationContextAware, WebFluxConfigurer {
	
	ApplicationContext context;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = context;
	}
	
	@Bean
	public ITemplateResolver thymeleafTemplateResolver() {
	    final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
	    resolver.setApplicationContext(this.context);
	    resolver.setPrefix("classpath:templates/");
	    resolver.setSuffix(".html");
	    resolver.setTemplateMode(TemplateMode.HTML);
	    resolver.setCacheable(false);
	    resolver.setCheckExistence(false);
	    return resolver;
	}
	
	@Bean
	public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() {
	    SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine();
	    templateEngine.setTemplateResolver(thymeleafTemplateResolver());
	    return templateEngine;
	}
	
	@Bean
	public ThymeleafReactiveViewResolver thymeleafReactiveViewResolver() {
	    ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver();
	    viewResolver.setTemplateEngine(thymeleafTemplateEngine());
	    return viewResolver;
	}
	
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
	    registry.viewResolver(thymeleafReactiveViewResolver());
	}
}

 

4. 요청 페이지로 핸들링 및 데이터 파싱을 도와줄 handler를 생성합니다.

MovieHandler.java

package com.psw.movie.handler;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import com.psw.movie.vo.Movie;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Component
public class MovieHandler {
	
	final static MediaType TEXT_HTML = MediaType.TEXT_HTML;
	final static MediaType APPLICATION_JSON = MediaType.APPLICATION_JSON;
	
	//movie.html로 변환
	public Mono<ServerResponse> movie(ServerRequest request){
		return ServerResponse.ok().contentType(TEXT_HTML).render("movie");
	}
	
	//요청이오면 JSON형태로 데이터 파싱
	public Mono<ServerResponse> getMovieList(ServerRequest request){
		Flux<Movie> list = Flux.just(
				new Movie("괴물", "봉준호", "2006", 13019740)
				, new Movie("아바타", "제임스 카메론", "2009", 13338863)
				, new Movie("인터스텔라", "크리스토퍼 놀란", "2014", 10309432)
				, new Movie("어벤져스: 인피티니워", "루소형제", "2018", 11212710)
				, new Movie("기생충", "봉준호", "2019", 10281306)
				);
		return ServerResponse.ok().contentType(APPLICATION_JSON).body(list, Movie.class);
	}
}

movie메소드에서는 movie.html페이지로 랜더를 해주기 때문에 MediaType.TEXT_HTML 형태로 파싱해주고

getMovieList메소드는 Flux객체에 Movie객체형태를 담아 JSON형태로 파싱을 해줄 예정입니다.

 

5. 요청에 따른 데이터 처리를 해줄 router를 구성합니다.

기존 mvc패턴의 controller라고 생각하시면 됩니다.

MovieRouter.java

package com.psw.movie.router;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import com.psw.movie.handler.MovieHandler;

@Configuration
public class MovieRouter {
	
	final static MediaType TEXT_HTML = MediaType.TEXT_HTML;
	final static MediaType APPLICATION_JSON = MediaType.APPLICATION_JSON;
	
	@Bean
	public RouterFunction<ServerResponse> index(MovieHandler movieHandler){
		return RouterFunctions.route(
				RequestPredicates.GET("/").and(RequestPredicates.accept(TEXT_HTML)), movieHandler::movie)
				.andRoute(RequestPredicates.GET("/movie").and(RequestPredicates.accept(TEXT_HTML)), movieHandler::movie);
	}
	
	@Bean
	public RouterFunction<ServerResponse> getMoiveList(MovieHandler movieHandler){
		return RouterFunctions.route(
					RequestPredicates
						.POST("/getMovieList")
						.and(RequestPredicates.accept(APPLICATION_JSON)), 
						movieHandler::getMovieList);
	}
}

/, /moive url로 접근하게 되면 movie.html페이지로 연결해주며

getMovieList를 POST형식으로 요청을 하게 되면 handler에서 작성한 리스트 데이터가 JSON형태로 응답해줍니다.

 

6. Movie객체를 생성할 VO를 생성합니다.

Moive.java

package com.psw.movie.vo;

public class Movie {
	String title;
	String director;
	String since;
	int audienceCnt;
	
	public Movie(String title, String director, String since, int audienceCnt) {
		super();
		this.title = title;
		this.director = director;
		this.since = since;
		this.audienceCnt = audienceCnt;
	}

	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	public String getDirector() {
		return director;
	}
	public void setDirector(String director) {
		this.director = director;
	}
	public String getSince() {
		return since;
	}
	public void setSince(String since) {
		this.since = since;
	}
	public int getAudienceCnt() {
		return audienceCnt;
	}
	public void setAudienceCnt(int audienceCnt) {
		this.audienceCnt = audienceCnt;
	}
}

 

7. resources아래 templates디렉토리를 생성하고 뷰 페이지로 사용할 movie.html를 생성합니다.

templates/movie.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>Movie</title>
		<style>
	    	*{padding:0; margin:0;}
	    	table tr th{background-color: #FFBB00;}
	    	table tr th, td{border: 1px solid black; text-align: center; width: 200px;}
	    </style>
	</head>
	<script src="https://code.jquery.com/jquery-3.4.1.js"></script>
	<body>
		<h1>WebFlux Asynchronous</h1>
		<br/><br/><br/>
		<h1>Movie List</h1>
			<table id="MovieTable">
				<tr>
					<th>제목</th>
					<th>감독</th>
					<th>연도</th>
					<th>관객수</th>
				</tr>
			</table>
	</body>
	<script>
	$(document).ready(function(){
	    init();
	});
	
	function init(){
		$.ajax({
			url: "/getMovieList",
			type: "POST",
			data: "",
			dataType: "JSON",
			success: function(args){
				fnSuccess(args);
			},
			error: function(args){
				alert("에러가 발생했습니다.");
			}
		});
	}
	
	function fnSuccess(res){
		if(res != null){
			var tag = "";
			res.forEach(function(d){
				tag += "<tr>" +
							"<td>" + d.title + "</td>" +
							"<td>" + d.director + "</td>" +
							"<td>" + d.since + "</td>" +
							"<td>" + d.audienceCnt + "</td>" +
						"</tr>";
			});
			
			$("#MovieTable").append(tag);
		}
	}
	</script>
</html>

 

8. 마무리

부트 프로젝트를 동작시키고 정상적으로 작동하는지 확인합니다.

영화 리스트 파싱

localhost:8080 또는 localhost:8080/movie로 접근하면 페이지에 접근이 됩니다.

html페이지에서는 jquery때문에 페이지가 로드가 된 후에 ajax를 통해 POST방식의 getMoiveList으로 요청을 진행합니다. 그럼 만들어놓은 라우터로 접근이 되고 핸들러(mvc의 서비스역할)의 getMovieList의 데이터를 JSON형태로 가져옵니다.

 

이후는 자바스크립트 단에서 원하는 형태로 파싱을 하면 해당 페이지처럼 데이터를 확인할 수 있습니다.

반응형
반응형

자바는 자주 쓰이는 형식의 함수형 인터페이스를 미리 정의해놓았다. 가능하면 매번 새롭게 정의하지 말고 해당 패키지의 인터페이스를 활용하는 것이 좋다고 하여 정리해본다.(메서드 이름의 통일화, 재사용, 유지보수 이점 등)

함수형 인터페이스

메소드

설명

java.land.Runnable

void run()

매개변수, 반환 모두 없음

Supplier<T>

T get()

매개변수는 없고 반환만 있음

Consumer<T>

void accept(T t)

Supplier와 반대

매개변수만 있고 반환은 없음

Function<T, R>

R apply(T t)

일반적인 함수. 하나의 매개변수를 받아서 결과를 반환

Predicate<T>

boolean test(T t)

조건식을 표현하는데 사용.

매개변수 1, 반환 타입은 boolean

 

매개변수가 2개인 경우 접두사 Bi를 붙인다.

함수형 인터페이스

메소드

설명

BiConsumer<T, U>

void accept(T t, U u)

두 개의 매개변수만 있고, 반환 값이 없음

BiFunction<T, U, R>

R apply(T t, U u)

일반적인 함수. 두개의 매개변수를 받아서 결과를 반환

BiPredicate<T>

boolean test(T t, U u)

조건식을 표현하는데 사용.

매개변수 2, 반환 타입은 boolean

 

* 매개변수가 3개이상인 경우엔 직접 만들어야 한다.

 

반응형
반응형

최근 Spring에서 반응형 코딩이 가능하다는 이야기를 들었고 팀원끼리 학습을 해보기로 하여 rx.js를 통해 컨셉을 이해해보고 비동기, 동기~ 블로킹이니 단어나 개념들부터 차근차근 잡고 학습을 진행해보았습니다. 여전히 개념은 잡히지 않고 단순 html파싱조차도 헤매고 있는 상황이지만 기록과 공유를 위해 일단 학습한 내용부분까지 적어보고자 합니다.

 

 

일단 가장 헷갈렸던 부분으로 Blocking, Non-Blocking / Synchronous, Asynchronous 용어였습니다.

2:2 매트릭스 형태의 그림을 가장 많이 발견 할 수 있는데, 간단하게 정리해보겠습니다.

 

용어정리

Blocking - Synchronous

블로킹이면서 싱크로나이즈인 경우는 일반적인 동기식 프로그램 scanf() 메소드, file.read(), jdbc연결의 select 등등이 있을것입니다.

 

Non-Blocking - Asynchronous

논블로킹방식이면서 비동기 방식으로 콜백을 던져주고 완료되면 콜백에게 이벤트가 끝났다고 알리는 방식으로 입력과 출력간에 기다리는 상황이 없기때문에, 자원을 효율적으로 사용할 수 있습니다. 사용 예로는 Node.js, ajax, 이번에 다뤄볼 WebFlux가 있습니다.

 

Non-Blocking - Synchronous

논블로킹이면서 동기식으로 폴링(Polling)방식으로 불리우며, 특정 시간마다 데이터가 준비되었는지 상태를 확인하는 방식입니다. 계속 확인을 하다가 완료가 되면 종료됩니다.

 

Blocking - Asynchronous

굳이 다루지 않겠습니다... 

 

 

 

Spring MVC와 Spring Webflux의 차이

Spring MVC 패턴은 HTTP요청이 들어오면 큐를 통해 들어오는데 Thread pool이 수용할 수 있는 수의 요청까지만 동시적으로 작업이 처리되고, 그 이상의 수가 들어오면 큐에서 대기를 합니다. 즉 한개의 요청은 한개의 Thread를 사용합니다.

Thread를 생성하는 비용이 크기때문에 미리 생성해뒀다가 재사용하면서 효율적으로 사용하며, 서버의 성능에 맞게 제한이 됩니다. (톰캣은 기본 Thread가 200개) 여기서 문제는 이 Thread pool의 개수가 넘어간 경우인데, 큐에 쌓이기 시작하면 처리속도가 급격하게 느려지면서 많은 지연시간이 발생됩니다.

이러한 문제를 타파하고자 만들어진 컨셉은 Webflux이며 Event만 담당하는 적은 수의 Thread로 비동기식 처리를 하여 효율적인 프로그램을 생성합니다. 하지만 제한점이 역시 존재하는데 중간에 블로킹이 존재하면 해당 방식은 안하느니만 못하게 됩니다. core가 2개짜리인 cpu에서는 Thread가 2개가 생성되는데, 블로킹이 걸리는 작업이 존재한다면 100개의 요청이 들어온 경우 98개의 요청은 마냥 기다리고 있어야 하기 때문입니다. 일반 MVC패턴이라면 200개가 처리하므로 더 효율적일거 같네요...

 

아직 논블로킹 방식의 지원하는 라이브러리가 많지는 않은 상황이고 NoSql인 몽고DB나, redis가 지원중이고 RDB는 아직 사용은 안해봤지만 MS-Sql, Postgresql정도가 지원한다고 합니다.

 

 

간단 사용 예제 index.html 띄워보기

1. 먼저 boot 프로젝트부터 생성해야 합니다.

 

2. webflux사용을 위해 dependency 추가를 합니다.

pom.xml

<dependencies>
	<dependency>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-webflux</artifactId>
	</dependency>
	
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-thymeleaf</artifactId>
	</dependency>

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
		<exclusions>
			<exclusion>
				<groupId>org.junit.vintage</groupId>
				<artifactId>junit-vintage-engine</artifactId>
			</exclusion>
		</exclusions>
	</dependency>
</dependencies>

 

 

3. 생성할 구조 트리는 아래 캡처와 같습니다.

WebConfig : webflux사용을 위한 설정들을 진행할 파일입니다.

WebFluxHandler : index.html을 반환하는 hanlder를 작성합니다.

WebFluxRouter : 요청을 해당 핸들러로 라우팅하는 라우터입니다.

WebFluxVO : 데이터를 담을 객체 vo입니다.

index.html : view 처리를 할 html입니다.

 

4. WebFlux 및 view페이지 설정하기

WebConfig.java

package com.psw.webflux.config;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.config.ViewResolverRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.thymeleaf.spring5.ISpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.SpringWebFluxTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.reactive.ThymeleafReactiveViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;

@EnableWebFlux
@Configuration
public class WebConfig implements ApplicationContextAware, WebFluxConfigurer {
	
	ApplicationContext context;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		this.context = context;
	}
	
	@Bean
	public ITemplateResolver thymeleafTemplateResolver() {
	    final SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
	    resolver.setApplicationContext(this.context);
	    resolver.setPrefix("classpath:templates/");
	    resolver.setSuffix(".html");
	    resolver.setTemplateMode(TemplateMode.HTML);
	    resolver.setCacheable(false);
	    resolver.setCheckExistence(false);
	    return resolver;
	}
	
	@Bean
	public ISpringWebFluxTemplateEngine thymeleafTemplateEngine() {
	    SpringWebFluxTemplateEngine templateEngine = new SpringWebFluxTemplateEngine();
	    templateEngine.setTemplateResolver(thymeleafTemplateResolver());
	    return templateEngine;
	}
	
	@Bean
	public ThymeleafReactiveViewResolver thymeleafReactiveViewResolver() {
	    ThymeleafReactiveViewResolver viewResolver = new ThymeleafReactiveViewResolver();
	    viewResolver.setTemplateEngine(thymeleafTemplateEngine());
	    return viewResolver;
	}
	
	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
	    registry.viewResolver(thymeleafReactiveViewResolver());
	}
}

boot프로젝트에서 WebFlux사용을 위해 @EnableWebFlux어노테이션을 설정합니다.

또한 뷰처리를 위해 Thymeleaf 설정도 bean등록을 해줍니다.(TemplateResolver, TemplateEngine, ViewResolver)

TemplateResolver에서 templates아래를 보도록 설정하였으니 resources아래에 templates디렉토리와 index.html을 생성합니다.

 

5. view 페이지 생성

index.html

 

index.html

<!DOCTYPE html>
<html>
	<head>
	    <meta charset="UTF-8"/>
	    <title>WebFlux</title>
	</head>
	<body>
		<h1>WebFlux!!!!!!!!!!!!!!!!!</h1>
		<h3>
			<p>name : <b th:text="${data.name}"> </b></p>
			<p>age : <b th:text="${data.age}"> </b></p>
		</h3>
	</body>
</html>	

 

6. 데이터를 담을 VO를 작성합니다.

package com.psw.webflux.vo;

public class WebFluxVO {
	String name;
	int age;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	
	public WebFluxVO(String name, int age) {
		this.name = name;
		this.age = age;
	}
}

 

7. index.html을 반환하는 handler를 작성합니다.

WebFluxHandler.java

package com.psw.webflux.handler;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;

import com.psw.webflux.vo.WebFluxVO;

import reactor.core.publisher.Mono;

@Component
public class WebFluxHandler {
	public Mono<ServerResponse> index(ServerRequest request){
		final Map<String, WebFluxVO> data = new HashMap<>();
		data.put("data", new WebFluxVO("", 0));
		return ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("index", data);
	}
	
	public Mono<ServerResponse> hello(ServerRequest request){
		final Map<String, WebFluxVO> data = new HashMap<>();
		data.put("data", new WebFluxVO("psw", 32));
		return ServerResponse.ok().contentType(MediaType.TEXT_HTML).render("index", data);
	}
}

return타입을 보면 Mono라는 클래스형이면서 ServerResponse를 상속받는 객체만 처리하도록 되어 있습니다.

여기서는 단순하게 Mono는 0~1개의 단일 데이터, Flux는 0~다중 데이터로 알고 넘어 가면 될 것 같습니다.

 

8. 작성한 handler를 요청에 따라 연결해줄 router를 작성합니다.

WebFluxRouter.java

package com.psw.webflux.router;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;

import com.psw.webflux.handler.WebFluxHandler;


@Configuration
public class WebFluxRouter {
	@Bean
	public RouterFunction<ServerResponse> index1(WebFluxHandler webFluxHandler){
		return RouterFunctions.route(RequestPredicates.GET("/")
				.and(RequestPredicates.accept(MediaType.TEXT_HTML)),
				webFluxHandler::index);
	}
	
	@Bean
	public RouterFunction<ServerResponse> index2(WebFluxHandler webFluxHandler){
		return RouterFunctions.route(RequestPredicates.GET("/index")
				.and(RequestPredicates.accept(MediaType.TEXT_HTML)),
				webFluxHandler::index);
	}
	
	@Bean
	public RouterFunction<ServerResponse> hello(WebFluxHandler webFluxHandler){
		return RouterFunctions.route(RequestPredicates.GET("/hello")
				.and(RequestPredicates.accept(MediaType.TEXT_HTML)),
				webFluxHandler::hello);
	}
}

 

9. 작성이 완료되면 Spring boot App을 구동합니다.

localhost:8080으로 접근하면 아래처럼 페이지를 확인할 수 있습니다.

localhost:8080

 

 

 

router에서 hello인 경우에도 매핑처리를 하였는데, 이번엔 hello주소까지 접근해보겠습니다.

localhost:8080/hello

map객체에서 name, age값이 파싱되어 출력되는걸 볼 수 있습니다.

 

 

 

 

참조 사이트 :

https://phrase.com/blog/posts/a-step-by-step-guide-to-i18n-in-spring-webflux/

https://alwayspr.tistory.com/44

https://homoefficio.github.io/2017/02/19/Blocking-NonBlocking-Synchronous-Asynchronous/

https://docs.spring.io/spring/docs/5.0.0.M5/spring-framework-reference/html/web-reactive.html

https://www.programcreek.com/java-api-examples/?api=org.springframework.web.reactive.function.server.ServerResponse

반응형
반응형

 Rx JS를 통해 github로 user정보를 요청하고 응답받는 예제입니다.

 

https://api.github.com/search/users?q="검색할git ID" 

해당 URL을 통해 JSON형태의 메시지를 전달받고 메시지를 출력하는 예제를 진행해보겠습니다.

 

 

 

Github유저의 ID를 검색하고 파싱하기

rx.js

import { fromEvent, Observable } from 'rxjs';
import {
    map,
    debounceTime,
    filter,
    distinctUntilChanged,
    partition,
    tap,
    switchMap,
    retry,
    finalize,
    share
} from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';

let $layer, $loading;
window.onload = function() {
    $layer = document.getElementById('suggestLayer');
    $loading = document.getElementById('loading');
};

function drawLayer(target, items) {
    target.innerHTML = items
        .map(user => {
            return `<li class="user">
                        <img src="${user.avatar_url}" width="50px" height="50px" />
                        <p><a href="${user.html_url}" target="_blank">${user.login}</a></p>
                    </li>`;
        })
        .join('');
}

function showLoading() {
    return new Observable(observer => {
        const subscription = observable.subscribe({
            next() {
                $loading.style.display = 'block';
            },
            complete() {}
        });
    });
}
function hideLoading() {
    return new Observable(observer => {
        const subscription = observable.subscribe({
            next() {
                $loading.style.display = 'none';
            },
            complete() {}
        });
    });
}

const keyup$ = fromEvent(document, 'keyup').pipe(
    debounceTime(300),
    filter(f => f.target.id === 'searchId'),
    map(e => e.target.value),
    distinctUntilChanged(),
    tap(v => console.log('from keyup$', v)),
    share()
);

let [user$, reset$] = keyup$.pipe(partition(query => query.trim().length > 0));

user$ = user$.pipe(
    tap(showLoading),
    switchMap(query =>
        ajax.getJSON(`https://api.github.com/search/users?q=${query}`)
    ),
    tap(hideLoading),
    retry(2),
    finalize(hideLoading)
);
user$.subscribe({
    next: v => drawLayer($layer, v.items),
    error: e => {
        console.log(e);
    }
});
reset$
    .pipe(
        tap(v => ($layer.innerHTML = '')),
        tap(v => console.log('from reset$', v))
    )
    .subscribe();

 

 

 

index.html

<html>
    <link rel="stylesheet" href="./src/style.css" />
    <script src="main_bundle.js"></script>
    <h1>RxJS GitHub User Map</h1>

    <p>Please enter the name of the user you want to search for...</p>
    <div class="autocomplete">
        <input
            type="text"
            id="searchId"
            placeholder="Please enter your ID..."
        />
        <ul id="suggestLayer"></ul>

        <div id="loading">
            <i class="fas fa-spinner fa-pulse"></i>
        </div>
    </div>
</html>

 

 

style.css

ul {
    list-style-type: none !important;
}
li {
    display: block;
}
.autocomplete {
    position: relative;
    width: 300px;
}
#searchId {
    width: 100%;
    height: 50px;
    line-height: 50px;
    font-size: 20px;
}
#suggestLayer {
    position: absolute;
    color: #666;
    padding: 0;
    margin: 0;
    width: 100%;
}
#suggestLayer li {
    border: 1px solid #bec8d8;
}
.user img {
    position: relative;
    float: left;
    margin-right: 10px;
}
.user p {
    line-height: 50px;
    margin: 0;
    padding: 0;
}
#loading {
    position: absolute;
    z-index: 2;
    top: 2px;
    right: 0;
    display: none;
}

 

 

package.json

{
    "name": "rxjsproj",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "scripts": {
        "publish": "webpack && webpack-dev-server --output-public=/dev/",
        "watch": "webpack --watch",
        "build": "webpack",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC",
    "devDependencies": {
        "@fortawesome/fontawesome-free": "^5.4.0",
        "@babel/core": "^7.8.7",
        "@babel/preset-env": "^7.8.7",
        "babel-loader": "^8.0.6",
        "esm": "^3.2.25",
        "rxjs": "^6.5.4",
        "webpack": "^4.42.0",
        "webpack-cli": "^3.3.11",
        "webpack-dev-server": "^3.10.3"
    },
    "dependencies": {
        "react-hot-loader": "^4.12.20"
    }
}

 

 

 

webpack.config.js

var path = require('path');

module.exports = {
    entry: {
        app: './src/rx.js'
    },
    output: {
        path: path.resolve(__dirname, 'dev'),
        filename: 'main_bundle.js'
    },
    mode: 'development',
    module: {
        rules: [
            {
                test: /\.(js)$/,
                include: path.resolve(__dirname, 'src'),
                loader: 'babel-loader',
                query: {
                    presets: ['@babel/preset-env']
                }
            }
        ]
    }
};

 

 

결과

동작 gif

 

 

참고사이트 : https://uxicode.tistory.com/m/402?category=456750

 

RxJs study - 자동완성 UI

 

uxicode.tistory.com

 

반응형