반응형

웹을 하다보면 자주 접하는 중복 로그인 방지 기능 요청사항이 들어오는데, HttpSessionListener를 통해 관리할 수 있습니다. 해당 객체는 Session이 생성되거나 제거될때 발생하는 이벤트를 제공하므로 등록만 해주면 세션을 통제할 수 있습니다.

 

HttpSessionListener

 

@WebListener
public class SessionConfig implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent hse) {
		//세션 생성시
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent hse) {
		//세션 삭제시
    }
}

HttpSessionListener는 EventListener를 상속받아 구현되어 있고 sessionCreated와 sessionDestroyed를 상속받습니다.

 

메소드명 그대로 sessionCreated는 세션이 생성될 때 동작하며, sessionDestroyed는 세션이 삭제될 때 생성됩니다.

 

우리는 해당 리스너에서 전역으로 처리할 세션 컬렉션을 통해 관리를 할 예정입니다.

private static final Map<String, HttpSession> sessions = new ConcurrentHashMap<>();

ConcurrentHashMap을 사용하여 세션을 처리합니다.

ConcurrentHashMap는 일반 HashMap과는 다르게 key, value값으로 Null을 허용하지 않는 컬렉션입니다.

 

다음은 해당 리스너를 사용한 예제입니다.

 

사용 예제

 

SessionConfig.java

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;

@WebListener
public class SessionConfig implements HttpSessionListener {

	private static final Map<String, HttpSession> sessions = new ConcurrentHashMap<>();
	
	//중복로그인 지우기
	public synchronized static String getSessionidCheck(String type, String compareId){
		String result = "";
		for( String key : sessions.keySet() ){
			HttpSession hs = sessions.get(key);
			if(hs != null &&  hs.getAttribute(type) != null && hs.getAttribute(type).toString().equals(compareId) ){
				result =  key.toString();
			}
		}
		removeSessionForDoubleLogin(result);
		return result;
	}
	
	private static void removeSessionForDoubleLogin(String userId){    	
		System.out.println("remove userId : " + userId);
		if(userId != null && userId.length() > 0){
			sessions.get(userId).invalidate();
			sessions.remove(userId);    		
		}
	}
	
	@Override
	public void sessionCreated(HttpSessionEvent se) {
		System.out.println(se);
	    sessions.put(se.getSession().getId(), se.getSession());
	}
	
	@Override
	public void sessionDestroyed(HttpSessionEvent se) {
		if(sessions.get(se.getSession().getId()) != null){
			sessions.get(se.getSession().getId()).invalidate();
			sessions.remove(se.getSession().getId());	
		}
	}
}

SessionConfig는 @WebListener 어노테이션을 통해 리스너임을 명명합니다.

 

어노테이션 방식이 아닌 설정파일에 추가할 경우 web.xml에 listener태그를 통해 추가해주시면 됩니다.

@WebListener가 아닌 web.xml설정하기

<listener>
	<listener-class>패키지경로.SessionConfig</listener-class>
</listener>

getSessionCheck메소드에 synchronized키워드가 보이실 텐데, 모르는분들을 위하여 간단하게 설명하면 멀티쓰레드로 인한 동시 접근을 막아 처리의 순서를 보장하기위해 해당 키워드를 처리하였습니다. (톰캣은 접속하는 세션이 늘어날 때마다 쓰레드가 증가됩니다. 기본 개수는 200개였던것으로 기억합니다...)

 

 

login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Session Test</title>
</head>
<body>
	<form action="/login.do" method="POST">
		id : <input type="text" id="id" name="id">
		<input type="submit" value="login">
	</form>
</body>
</html>

간단하게 ID만 받을 form태그입니다.

 

 

LoginController.java

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
public class LoginController {
	
	@RequestMapping(value = "/login.do", method = RequestMethod.POST)
	public String login(HttpServletRequest request, HttpSession session, RedirectAttributes rttr) throws Exception {
		String id = request.getParameter("id");
		if(id != null){
			String userId = SessionConfig.getSessionidCheck("login_id", id);
			System.out.println(id + " : " +userId);
			session.setMaxInactiveInterval(60 * 60);
			session.setAttribute("login_id", id);
			return "redirect:/home.do";
		}
		return "redirect:/main.do";
	}
	
	@RequestMapping(value = "/main.do")
	public String index(HttpSession session) throws Exception {
		return "login";
	}
	
	@RequestMapping(value = "/home.do")
	public String home(HttpSession session) throws Exception {
		return "home";
	}
}

메인 페이지, 로그인 성공시 home.jsp로 처리할 매핑 함수와 로그인 처리를 할 함수를 구성한 로그인 컨트롤러입니다.

 

마지막으로 인터셉터를 구성 후 테스트를 진행해보겠습니다.

 

 

LoginInterceptor.java

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class LoginInterceptor extends HandlerInterceptorAdapter {
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		final HttpSession session = request.getSession();
		String path = request.getRequestURI();
		if(path.contains("/main.do") || path.contains("/login.do")) { //접근 경로가 main.do인 경우에인 interceptor 체크 예외
			return true;
		}else if (session.getAttribute("login_id") == null) {  //세션 로그인이 없으면 리다이렉트 처리
			response.sendRedirect("/main.do");
			return false;
		}
		
		return true;
	}
}

main.do, login.do를 제외하고 세션의 login_id값이 존재하는지 체크 후 존재하지 않는다면 기본 페이지로 리다이렉트 처리를 해주는 인터셉터입니다.

인터셉터 사용을 위해 설정을 추가합니다.

 

 

servlet-context.xml

<mvc:interceptors>
	<mvc:interceptor>
		<mvc:mapping path="/*.do" />
		<beans:bean class="com.test.httpSessions.LoginInterceptor" />
	</mvc:interceptor>
</mvc:interceptors>

 

 

결과 테스트

 

테스트를 위해 파이어폭스에서 main.do를 접근하였습니다.

 

다음은 IE에서 추가로 접근하였습니다.

해당 서버에 접근하는 순간 세션이 생기면서 SessionConfig.sessionCreated에 sysout을 찍어놓은 부분이 동작하여 주소값이 출력되는 모습을 확인할 수 있습니다.

 

이제 로그인 테스트를 해보겠습니다. DB는 제외되었고 당연히 정상 입력이라고 가정하고 테스트를 진행합니다.

admin을 입력하여 로그인합니다.
정상 로그인되어 home.jsp가 출력됩니다.

 

 

다음은 IE에서 admin으로 로그인해보겠습니다.

정상 접속

 

역시 정상적으로 접속 되었고 파이어폭스에서 새로고침을 해보면 로그인 페이지로 이동된 것을 볼 수 있습니다.

기존에 접속되어있던 admin 중복 세션을 제거하였기 때문입니다.

/home.do는 세션을 체크하는 인터셉터에 의하여 초기페이지로 이동된 모습을 확인 할 수있습니다.

기존 로그인된 세션이 제거되어 로그인페이지로 변경

 

 

짤버전 추가...

중복로그인 체크

 

 

* 세션 중복제거를 하면서 헷갈렸던 부분은 SessionConfig의 sessionCreated메소드였는데, 당연히 session.setAttribute의 동작이 이루어지면 리스너에 의해서 이부분이 매번 동작되는것으로 착각하여 초기에 많은 시간의 뻘짓을 하였습니다. 죄 없는 이클립스가 캐시가 먹었다부터 해서 브라우저가 고장난건지 여러 의문을 품었지만 역시 컴퓨터는 거짓말을 하지 않습니다... 세션이 클라이언트가 해당 서버에 접근하는 순간 생성되는것이며, login_id의 관리는 별개로 컬렉션에서 관리하는것이므로 두 개념을 헷갈려서는 안됩니다...

반응형
반응형

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

원인 분석

서비스에서 구성하는 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에서 데이터를 가져와 영화정보를 파싱하는 예제를 작성해보았습니다.

반응형
반응형

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형태로 가져옵니다.

 

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

반응형
반응형

최근 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

반응형
반응형

 

1장부터 보실분들은 아래 URL을 참고해주세요!

https://myhappyman.tistory.com/100?category=873296

 

SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-1(단순 채팅, 메시지 보내기)

이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에서 만들어보겠습니다. 간단하게 프로젝트를 생성부터 채팅방 생성 및 채팅하는 과정까지 만들어보겠습니다. 소켓통신을 사용한 채팅프로그램 만들기 스프링..

myhappyman.tistory.com

 

4장(방 구성하여 채팅하기)에 이어서 마지막으로 파일전송을 다뤄보겠습니다.

https://myhappyman.tistory.com/103?category=873296

 

SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-4(채팅방 만들기2)

1장부터 확인해보실분들은 아래 url을 확인해주세요. https://myhappyman.tistory.com/100 SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-1 이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에..

myhappyman.tistory.com

 

이번 장은 간단하게 파일을 전송할 수 있다정도만 봐주시면 될 것같습니다.

실제로 사용해야 한다면 제가 제공하는 예제의 형태로 구현하지 않을 것 같기 때문입니다.

 

 

웹소켓으로 파일 전송하기

BinaryMessage

처음에 웹 소켓을 시작하면서 서버단을 구성할 때, 구현체부분인 SockHandler부분을 구성할때 TextWebSocketHandler을 상속받고 메시지 타입에 따라 handleBinaryMessage 또는 handleTextMessage가 실행된다고 했는데, 지금까지 JSON형태의 String메시지만 전송하다보니 바이너리메시지는 다룰일이 없었습니다. 이번 장에서는 바이너리메시지를 사용하여 파일을 받고 서버에 저장도 하고 채팅방에 전송된 이미지를 표출하는부분까지 구성해보겠습니다.

 

 

Server단

@Override
public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
}

SocketHandler에 추가할 메소드입니다. BinaryMessage의 데이터가 들어오면 해당 메소드가 실행됩니다.

 

 

SocketHandler.java

package com.psw.chating.handler;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
public class SocketHandler extends TextWebSocketHandler {
	
	List<HashMap<String, Object>> rls = new ArrayList<>(); //웹소켓 세션을 담아둘 리스트 ---roomListSessions
	private static final String FILE_UPLOAD_PATH = "C:/test/websocket/";
	static int fileUploadIdx = 0;
	static String fileUploadSession = "";
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload(); //JSON형태의 String메시지를 받는다.
		JSONObject obj = jsonToObjectParser(msg); //JSON데이터를 JSONObject로 파싱한다.
		
		String rN = (String) obj.get("roomNumber"); //방의 번호를 받는다.
		String msgType = (String) obj.get("type"); //메시지의 타입을 확인한다.
		HashMap<String, Object> temp = new HashMap<String, Object>();
		if(rls.size() > 0) {
			for(int i=0; i<rls.size(); i++) {
				String roomNumber = (String) rls.get(i).get("roomNumber"); //세션리스트의 저장된 방번호를 가져와서
				if(roomNumber.equals(rN)) { //같은값의 방이 존재한다면
					temp = rls.get(i); //해당 방번호의 세션리스트의 존재하는 모든 object값을 가져온다.
					fileUploadIdx = i;
					fileUploadSession = (String) obj.get("sessionId");
					break;
				}
			}
			if(!msgType.equals("fileUpload")) { //메시지의 타입이 파일업로드가 아닐때만 전송한다.
				//해당 방의 세션들만 찾아서 메시지를 발송해준다.
				for(String k : temp.keySet()) { 
					if(k.equals("roomNumber")) { //다만 방번호일 경우에는 건너뛴다.
						continue;
					}
					
					WebSocketSession wss = (WebSocketSession) temp.get(k);
					if(wss != null) {
						try {
							wss.sendMessage(new TextMessage(obj.toJSONString()));
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
				}
			}
		}
	}
	
	@Override
	public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
		//바이너리 메시지 발송
		ByteBuffer byteBuffer = message.getPayload();
		String fileName = "temp.jpg";
		File dir = new File(FILE_UPLOAD_PATH);
		if(!dir.exists()) {
			dir.mkdirs();
		}
		
		File file = new File(FILE_UPLOAD_PATH, fileName);
		FileOutputStream out = null;
		FileChannel outChannel = null;
		try {
			byteBuffer.flip(); //byteBuffer를 읽기 위해 세팅
			out = new FileOutputStream(file, true); //생성을 위해 OutputStream을 연다.
			outChannel = out.getChannel(); //채널을 열고
			byteBuffer.compact(); //파일을 복사한다.
			outChannel.write(byteBuffer); //파일을 쓴다.
		}catch(Exception e) {
			e.printStackTrace();
		}finally {
			try {
				if(out != null) {
					out.close();
				}
				if(outChannel != null) {
					outChannel.close();
				}
			}catch (IOException e) {
				e.printStackTrace();
			}
		}
		
		byteBuffer.position(0); //파일을 저장하면서 position값이 변경되었으므로 0으로 초기화한다.
		//파일쓰기가 끝나면 이미지를 발송한다.
		HashMap<String, Object> temp = rls.get(fileUploadIdx);
		for(String k : temp.keySet()) {
			if(k.equals("roomNumber")) {
				continue;
			}
			WebSocketSession wss = (WebSocketSession) temp.get(k);
			try {
				wss.sendMessage(new BinaryMessage(byteBuffer)); //초기화된 버퍼를 발송한다.
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		boolean flag = false;
		String url = session.getUri().toString();
		String roomNumber = url.split("/chating/")[1];
		int idx = rls.size(); //방의 사이즈를 조사한다.
		if(rls.size() > 0) {
			for(int i=0; i<rls.size(); i++) {
				String rN = (String) rls.get(i).get("roomNumber");
				if(rN.equals(roomNumber)) {
					flag = true;
					idx = i;
					break;
				}
			}
		}
		
		if(flag) { //존재하는 방이라면 세션만 추가한다.
			HashMap<String, Object> map = rls.get(idx);
			map.put(session.getId(), session);
		}else { //최초 생성하는 방이라면 방번호와 세션을 추가한다.
			HashMap<String, Object> map = new HashMap<String, Object>();
			map.put("roomNumber", roomNumber);
			map.put(session.getId(), session);
			rls.add(map);
		}
		
		//세션등록이 끝나면 발급받은 세션ID값의 메시지를 발송한다.
		JSONObject obj = new JSONObject();
		obj.put("type", "getId");
		obj.put("sessionId", session.getId());
		session.sendMessage(new TextMessage(obj.toJSONString()));
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
		if(rls.size() > 0) { //소켓이 종료되면 해당 세션값들을 찾아서 지운다.
			for(int i=0; i<rls.size(); i++) {
				rls.get(i).remove(session.getId());
			}
		}
		super.afterConnectionClosed(session, status);
	}
	
	private static JSONObject jsonToObjectParser(String jsonStr) {
		JSONParser parser = new JSONParser();
		JSONObject obj = null;
		try {
			obj = (JSONObject) parser.parse(jsonStr);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		return obj;
	}
}

BhandleBinaryMessage 메소드가 추가되었고, 매개변수 BinaryMessage의 데이터를 ByteBuffer로 받아서 파일을 저장하고, 현재 방에 존재하는 세션에게만 ByteBuffer데이터를 전송하는 예제입니다.

이번에 소켓 파일전송을 하면서 ByteBuffer를 처음 써봤는데, 생소해서 조금 구성하는데 어려움이 있었습니다. 로직에 문제가 있다면 지적해주시고 수정하도록 하겠습니다.

 

 

Client단

chat.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<meta charset="UTF-8">
	<title>Chating</title>
	<style>
		*{
			margin:0;
			padding:0;
		}
		.container{
			width: 500px;
			margin: 0 auto;
			padding: 25px
		}
		.container h1{
			text-align: left;
			padding: 5px 5px 5px 15px;
			color: #FFBB00;
			border-left: 3px solid #FFBB00;
			margin-bottom: 20px;
		}
		.chating{
			background-color: #000;
			width: 500px;
			height: 500px;
			overflow: auto;
		}
		.chating .me{
			color: #F6F6F6;
			text-align: right;
		}
		.chating .others{
			color: #FFE400;
			text-align: left;
		}
		input{
			width: 330px;
			height: 25px;
		}
		#yourMsg{
			display: none;
		}
		.msgImg{
			width: 200px;
			height: 125px;
		}
		.clearBoth{
			clear: both;
		}
		.img{
			float: right;
		}
	</style>
</head>

<script type="text/javascript">
	var ws;

	function wsOpen(){
		//웹소켓 전송시 현재 방의 번호를 넘겨서 보낸다.
		ws = new WebSocket("ws://" + location.host + "/chating/"+$("#roomNumber").val());
		wsEvt();
	}
		
	function wsEvt() {
		ws.onopen = function(data){
			//소켓이 열리면 동작
		}
		
		ws.onmessage = function(data) {
			//메시지를 받으면 동작
			var msg = data.data;
			if(msg != null && msg.type != ''){
				//파일 업로드가 아닌 경우 메시지를 뿌려준다.
				var d = JSON.parse(msg);
				if(d.type == "getId"){
					var si = d.sessionId != null ? d.sessionId : "";
					if(si != ''){
						$("#sessionId").val(si); 
					}
				}else if(d.type == "message"){
					if(d.sessionId == $("#sessionId").val()){
						$("#chating").append("<p class='me'>나 :" + d.msg + "</p>");	
					}else{
						$("#chating").append("<p class='others'>" + d.userName + " :" + d.msg + "</p>");
					}
						
				}else{
					console.warn("unknown type!")
				}
			}else{
				//파일 업로드한 경우 업로드한 파일을 채팅방에 뿌려준다.
				var url = URL.createObjectURL(new Blob([msg]));
				$("#chating").append("<div class='img'><img class='msgImg' src="+url+"></div><div class='clearBoth'></div>");
			}
		}

		document.addEventListener("keypress", function(e){
			if(e.keyCode == 13){ //enter press
				send();
			}
		});
	}

	function chatName(){
		var userName = $("#userName").val();
		if(userName == null || userName.trim() == ""){
			alert("사용자 이름을 입력해주세요.");
			$("#userName").focus();
		}else{
			wsOpen();
			$("#yourName").hide();
			$("#yourMsg").show();
		}
	}

	function send() {
		var option ={
			type: "message",
			roomNumber: $("#roomNumber").val(),
			sessionId : $("#sessionId").val(),
			userName : $("#userName").val(),
			msg : $("#chatting").val()
		}
		ws.send(JSON.stringify(option))
		$('#chatting').val("");
	}

	function fileSend(){
		var file = document.querySelector("#fileUpload").files[0];
		var fileReader = new FileReader();
		fileReader.onload = function() {
			var param = {
				type: "fileUpload",
				file: file,
				roomNumber: $("#roomNumber").val(),
				sessionId : $("#sessionId").val(),
				msg : $("#chatting").val(),
				userName : $("#userName").val()
			}
			ws.send(JSON.stringify(param)); //파일 보내기전 메시지를 보내서 파일을 보냄을 명시한다.

		    arrayBuffer = this.result;
			ws.send(arrayBuffer); //파일 소켓 전송
		};
		fileReader.readAsArrayBuffer(file);
	}
</script>
<body>
	<div id="container" class="container">
		<h1>${roomName}의 채팅방</h1>
		<input type="hidden" id="sessionId" value="">
		<input type="hidden" id="roomNumber" value="${roomNumber}">
		
		<div id="chating" class="chating">
		</div>
		
		<div id="yourName">
			<table class="inputTable">
				<tr>
					<th>사용자명</th>
					<th><input type="text" name="userName" id="userName"></th>
					<th><button onclick="chatName()" id="startBtn">이름 등록</button></th>
				</tr>
			</table>
		</div>
		<div id="yourMsg">
			<table class="inputTable">
				<tr>
					<th>메시지</th>
					<th><input id="chatting" placeholder="보내실 메시지를 입력하세요."></th>
					<th><button onclick="send()" id="sendBtn">보내기</button></th>
				</tr>
				<tr>
					<th>파일업로드</th>
					<th><input type="file" id="fileUpload"></th>
					<th><button onclick="fileSend()" id="sendFileBtn">파일올리기</button></th>
				</tr>
			</table>
		</div>
	</div>
</body>
</html>

파일전송을 할때, 이 파일은 어떤세션에서 넘겼고 어떤파일명을 넘겼는지 등에 대해 정보를 남길 수가 없어서 메시지를 한번 보내서 이제 이런형태의 메시지가 올것이라고 명시를 하고 파일을 넘겼습니다.

여러개의 채팅방이 구성되고 동시에 여러 소켓통신이 발생한다면 해당 로직은 정상적으로 동작하지 않고 다른방으로 전송될수도 있는 위험이 존재합니다.

오로지 파일 전송의 예제를 위해 구성되었음을 알려드립니다.

 

파일 올리기 버튼을 누르면 fileSend함수가 실행되면서 FileReader객체를 통해 파일을 버퍼로 읽고 전송처리해줍니다.

서버에서 파일을 전달받으면 다시 클라이언트로 byteBuffer데이터를 넘겨주는데 Blob객체를 통해 이미지로 파싱하여 채팅방에 표출합니다.

 

파일 업로드 제한, 용량 제한 이런부분은 처리하지 않았습니다.

 

동작  화면

파일이 서버로 전송되었고 특정 경로에 저장하였습니다.

 

채팅방에도 정상적으로 이미지가 출력되는걸 볼 수 있습니다.

반응형
반응형

1장부터 확인해보실분들은 아래 url을 확인해주세요.

 

https://myhappyman.tistory.com/100

 

SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-1

이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에서 만들어보겠습니다. 간단하게 프로젝트를 생성부터 채팅방 생성 및 채팅하는 과정까지 만들어보겠습니다. 소켓통신을 사용한 채팅프로그램 만들기 스프링..

myhappyman.tistory.com

 

방을 만들었고, 채팅방으로 넘어가는 로직까지 구성하였지만 여전히 채팅시 모든 소켓의 데이터가 서로에게 전송될 겁니다. 이제 방정보를 기준으로 데이터를 저장하여 구분하고 방별로 채팅이 되는 프로그램으로 업그레이드 해보겠습니다.

 

server단

구현체를 등록하였던 url정보를 방번호에 따라 구분될 수 있도록 변경해줍니다.

webSocketConfig.java

package com.psw.chating.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import com.psw.chating.handler.SocketHandler;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{

	@Autowired
	SocketHandler socketHandler;
	
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(socketHandler, "/chating/{roomNumber}");
	}
}

url에서 chating/ 이후 들어오는 {roomNumber}값은 앞으로 방을 구분하는 값이 될 예정입니다.

 

 

SocketHandler.java

package com.psw.chating.handler;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
public class SocketHandler extends TextWebSocketHandler {
	
	//HashMap<String, WebSocketSession> sessionMap = new HashMap<>(); //웹소켓 세션을 담아둘 맵
	List<HashMap<String, Object>> rls = new ArrayList<>(); //웹소켓 세션을 담아둘 리스트 ---roomListSessions
	
	@Override
	public void handleTextMessage(WebSocketSession session, TextMessage message) {
		//메시지 발송
		String msg = message.getPayload();
		JSONObject obj = jsonToObjectParser(msg);
		
		String rN = (String) obj.get("roomNumber");
		HashMap<String, Object> temp = new HashMap<String, Object>();
		if(rls.size() > 0) {
			for(int i=0; i<rls.size(); i++) {
				String roomNumber = (String) rls.get(i).get("roomNumber"); //세션리스트의 저장된 방번호를 가져와서
				if(roomNumber.equals(rN)) { //같은값의 방이 존재한다면
					temp = rls.get(i); //해당 방번호의 세션리스트의 존재하는 모든 object값을 가져온다.
					break;
				}
			}
			
			//해당 방의 세션들만 찾아서 메시지를 발송해준다.
			for(String k : temp.keySet()) { 
				if(k.equals("roomNumber")) { //다만 방번호일 경우에는 건너뛴다.
					continue;
				}
				
				WebSocketSession wss = (WebSocketSession) temp.get(k);
				if(wss != null) {
					try {
						wss.sendMessage(new TextMessage(obj.toJSONString()));
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		//소켓 연결
		super.afterConnectionEstablished(session);
		boolean flag = false;
		String url = session.getUri().toString();
		System.out.println(url);
		String roomNumber = url.split("/chating/")[1];
		int idx = rls.size(); //방의 사이즈를 조사한다.
		if(rls.size() > 0) {
			for(int i=0; i<rls.size(); i++) {
				String rN = (String) rls.get(i).get("roomNumber");
				if(rN.equals(roomNumber)) {
					flag = true;
					idx = i;
					break;
				}
			}
		}
		
		if(flag) { //존재하는 방이라면 세션만 추가한다.
			HashMap<String, Object> map = rls.get(idx);
			map.put(session.getId(), session);
		}else { //최초 생성하는 방이라면 방번호와 세션을 추가한다.
			HashMap<String, Object> map = new HashMap<String, Object>();
			map.put("roomNumber", roomNumber);
			map.put(session.getId(), session);
			rls.add(map);
		}
		
		//세션등록이 끝나면 발급받은 세션ID값의 메시지를 발송한다.
		JSONObject obj = new JSONObject();
		obj.put("type", "getId");
		obj.put("sessionId", session.getId());
		session.sendMessage(new TextMessage(obj.toJSONString()));
	}
	
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		//소켓 종료
		if(rls.size() > 0) { //소켓이 종료되면 해당 세션값들을 찾아서 지운다.
			for(int i=0; i<rls.size(); i++) {
				rls.get(i).remove(session.getId());
			}
		}
		super.afterConnectionClosed(session, status);
	}
	
	private static JSONObject jsonToObjectParser(String jsonStr) {
		JSONParser parser = new JSONParser();
		JSONObject obj = null;
		try {
			obj = (JSONObject) parser.parse(jsonStr);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		return obj;
	}
}

구현체 부분의 handler입니다. 텍스트로 설명은 한계가 있어서 주석을 달아두었습니다.

변경 된 부분을 나열하겠습니다.

1. 세션을 관리하던 map객체에서 list, hashmap형태로 변경되었습니다. hashmap의 value자료형도 WebSocketSession에서 Object형으로 변경되었습니다.

2. 세션을 저장할때, 현재 접근한 방의 정보가 있는지 체크하고 존재하지 않으면 방의 번호를 입력 후 세션들을 담아주는 로직으로 변경되었습니다.

3. 마찬가지로 종료시에도 list컬랙션을 순회하면서 해당 키값의 세션들을 삭제하도록 변경되었습니다.

4. 메시지를 발송하는 handleTextMessage메소드에서는 현재의 방번호를 가져오고 방정보+세션정보를 관리하는 rls리스트 컬랙션에서 데이터를 조회한 후에 해당 Hashmap을 임시 맵에 파싱하여 roomNumber의 키값을 제외한 모든 세션키값들을 웹소켓을 통해 메시지를 보내줍니다.

 

말로 정리가 어려운데, 결국 4번에서 하는 처리로 인해 방구분을 하고 해당 방에 존재하는 session값들에게만 메시지를 발송하여 구분이 됩니다.

 

 

client단

chat.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<meta charset="UTF-8">
	<title>Chating</title>
	<style>
		*{
			margin:0;
			padding:0;
		}
		.container{
			width: 500px;
			margin: 0 auto;
			padding: 25px
		}
		.container h1{
			text-align: left;
			padding: 5px 5px 5px 15px;
			color: #FFBB00;
			border-left: 3px solid #FFBB00;
			margin-bottom: 20px;
		}
		.chating{
			background-color: #000;
			width: 500px;
			height: 500px;
			overflow: auto;
		}
		.chating .me{
			color: #F6F6F6;
			text-align: right;
		}
		.chating .others{
			color: #FFE400;
			text-align: left;
		}
		input{
			width: 330px;
			height: 25px;
		}
		#yourMsg{
			display: none;
		}
	</style>
</head>

<script type="text/javascript">
	var ws;

	function wsOpen(){
		//웹소켓 전송시 현재 방의 번호를 넘겨서 보낸다.
		ws = new WebSocket("ws://" + location.host + "/chating/"+$("#roomNumber").val());
		wsEvt();
	}
		
	function wsEvt() {
		ws.onopen = function(data){
			//소켓이 열리면 동작
		}
		
		ws.onmessage = function(data) {
			//메시지를 받으면 동작
			var msg = data.data;
			if(msg != null && msg.trim() != ''){
				var d = JSON.parse(msg);
				if(d.type == "getId"){
					var si = d.sessionId != null ? d.sessionId : "";
					if(si != ''){
						$("#sessionId").val(si); 
					}
				}else if(d.type == "message"){
					if(d.sessionId == $("#sessionId").val()){
						$("#chating").append("<p class='me'>나 :" + d.msg + "</p>");	
					}else{
						$("#chating").append("<p class='others'>" + d.userName + " :" + d.msg + "</p>");
					}
						
				}else{
					console.warn("unknown type!")
				}
			}
		}

		document.addEventListener("keypress", function(e){
			if(e.keyCode == 13){ //enter press
				send();
			}
		});
	}

	function chatName(){
		var userName = $("#userName").val();
		if(userName == null || userName.trim() == ""){
			alert("사용자 이름을 입력해주세요.");
			$("#userName").focus();
		}else{
			wsOpen();
			$("#yourName").hide();
			$("#yourMsg").show();
		}
	}

	function send() {
		var option ={
			type: "message",
			roomNumber: $("#roomNumber").val(),
			sessionId : $("#sessionId").val(),
			userName : $("#userName").val(),
			msg : $("#chatting").val()
		}
		ws.send(JSON.stringify(option))
		$('#chatting').val("");
	}
</script>
<body>
	<div id="container" class="container">
		<h1>${roomName}의 채팅방</h1>
		<input type="hidden" id="sessionId" value="">
		<input type="hidden" id="roomNumber" value="${roomNumber}">
		
		<div id="chating" class="chating">
		</div>
		
		<div id="yourName">
			<table class="inputTable">
				<tr>
					<th>사용자명</th>
					<th><input type="text" name="userName" id="userName"></th>
					<th><button onclick="chatName()" id="startBtn">이름 등록</button></th>
				</tr>
			</table>
		</div>
		<div id="yourMsg">
			<table class="inputTable">
				<tr>
					<th>메시지</th>
					<th><input id="chatting" placeholder="보내실 메시지를 입력하세요."></th>
					<th><button onclick="send()" id="sendBtn">보내기</button></th>
				</tr>
			</table>
		</div>
	</div>
</body>
</html>

마찬가지로 변경된 점을 나열하겠습니다.

1. 방의 번호값을 모델에서 저장한 값을 jstl을 통해 파싱합니다. (id=roomNumber)

2. 접속한 방의 이름을 모델에서 저장한 값을 가져와서 채팅방 이름을 추가해줍니다. ${roomName}

3. 메시지를 보내는 send함수에 roomNumber 키 값이 추가되었습니다.

방의 번호를 보내줌으로써 소켓서버는 어느방에서 메시지를 보냈는지 구분합니다.

 

 

동작 화면

채팅방 구현

화면의 짤이 작아서 잘 안보이지만 방1, 방1에 접근한 1번, 2번 브라우저만 서로 메시지가 오고

방3은 혼자 채팅되는 모습을 볼 수 있습니다.

 

방3는 메시지가 안온다.

반응형
반응형

1장부터 차례대로 진행하고 싶은 분들은 아래 url을 이용해주세요.

 

https://myhappyman.tistory.com/100?category=873296

 

SpringBoot - 스프링부트에서 채팅프로그램(소켓통신) 만들기-1

이번엔 소켓통신을 통하여 채팅프로그램을 스프링부트에서 만들어보겠습니다. 간단하게 프로젝트를 생성부터 채팅방 생성 및 채팅하는 과정까지 만들어보겠습니다. 소켓통신을 사용한 채팅프로그램 만들기 스프링..

myhappyman.tistory.com

 

2장에 이어서 3장입니다. 이번엔 채팅방을 개설하고 채팅방별로 채팅을 구분해보겠습니다.

 

먼저 채팅방을 구성하기 위해 서버단에 뷰페이지 작성과 방을 관리하는 리스트배열 VO객체를 생성하고 제어해보겠습니다. 이번장에서는 소켓관련된 내용이 없을 예정이며, 다음 장에서 방을 생성 후 채팅방에서 접속한 방의 정보를 통해 소켓 데이터를 구분 처리하도록 하겠습니다.

 

Server단

VO 객체 생성하기

Room.java

package com.psw.chating.vo;

public class Room {
	int roomNumber;
	String roomName;
	
	public int getRoomNumber() {
		return roomNumber;
	}
	public void setRoomNumber(int roomNumber) {
		this.roomNumber = roomNumber;
	}
	public String getRoomName() {
		return roomName;
	}
	public void setRoomName(String roomName) {
		this.roomName = roomName;
	}
	
	@Override
	public String toString() {
		return "Room [roomNumber=" + roomNumber + ", roomName=" + roomName + "]";
	}	
}

방의 정보를 담아둘 Room객체를 생성하였습니다.

 

 

뷰페이지 작성 및 제어하기

MainController.java

package com.psw.chating.controller;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

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 com.psw.chating.vo.Room;

@Controller
public class MainController {
	
	List<Room> roomList = new ArrayList<Room>();
	static int roomNumber = 0;
	
	@RequestMapping("/chat")
	public ModelAndView chat() {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("chat");
		return mv;
	}
	
	/**
	 * 방 페이지
	 * @return
	 */
	@RequestMapping("/room")
	public ModelAndView room() {
		ModelAndView mv = new ModelAndView();
		mv.setViewName("room");
		return mv;
	}
	
	/**
	 * 방 생성하기
	 * @param params
	 * @return
	 */
	@RequestMapping("/createRoom")
	public @ResponseBody List<Room> createRoom(@RequestParam HashMap<Object, Object> params){
		String roomName = (String) params.get("roomName");
		if(roomName != null && !roomName.trim().equals("")) {
			Room room = new Room();
			room.setRoomNumber(++roomNumber);
			room.setRoomName(roomName);
			roomList.add(room);
		}
		return roomList;
	}
	
	/**
	 * 방 정보가져오기
	 * @param params
	 * @return
	 */
	@RequestMapping("/getRoom")
	public @ResponseBody List<Room> getRoom(@RequestParam HashMap<Object, Object> params){
		return roomList;
	}
	
	/**
	 * 채팅방
	 * @return
	 */
	@RequestMapping("/moveChating")
	public ModelAndView chating(@RequestParam HashMap<Object, Object> params) {
		ModelAndView mv = new ModelAndView();
		int roomNumber = Integer.parseInt((String) params.get("roomNumber"));
		
		List<Room> new_list = roomList.stream().filter(o->o.getRoomNumber()==roomNumber).collect(Collectors.toList());
		if(new_list != null && new_list.size() > 0) {
			mv.addObject("roomName", params.get("roomName"));
			mv.addObject("roomNumber", params.get("roomNumber"));
			mv.setViewName("chat");
		}else {
			mv.setViewName("room");
		}
		return mv;
	}
}

방으로 접근시킬 뷰페이지 room()메소드와 방을 생성하는 createRoom()메소드 방정보를 가져오는 getRoom()메소드를 작성하였습니다.

 

현재 프로젝트는 DB에 데이터를 담아놓거나, 파일등에 저장하는게 아니므로 방의 정보를 담아둘 List<Room>컬랙션을 생성하였고, 메소드에 따라 방을 생성하거나 방의 정보를 가져오도록 처리하였습니다.

나중에 찾아볼 room.jsp에서는 접속버튼에 따라 채팅방을 이동시킬 예정인데, moveChating()메소드에서는 전달받은 파라미터값으로 방이 생성되었는지 필터함수로 체크후 존재하는 방이면 해당방으로 이동시켜주고, 강제로 url을 바꾼 값이면 이동하지 못하도록 처리하였습니다.

 

 

Client단

방을 생성하는 jsp 구성

room.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
<meta charset="UTF-8">
	<title>Room</title>
	<style>
		*{
			margin:0;
			padding:0;
		}
		.container{
			width: 500px;
			margin: 0 auto;
			padding: 25px
		}
		.container h1{
			text-align: left;
			padding: 5px 5px 5px 15px;
			color: #FFBB00;
			border-left: 3px solid #FFBB00;
			margin-bottom: 20px;
		}
		.roomContainer{
			background-color: #F6F6F6;
			width: 500px;
			height: 500px;
			overflow: auto;
		}
		.roomList{
			border: none;
		}
		.roomList th{
			border: 1px solid #FFBB00;
			background-color: #fff;
			color: #FFBB00;
		}
		.roomList td{
			border: 1px solid #FFBB00;
			background-color: #fff;
			text-align: left;
			color: #FFBB00;
		}
		.roomList .num{
			width: 75px;
			text-align: center;
		}
		.roomList .room{
			width: 350px;
		}
		.roomList .go{
			width: 71px;
			text-align: center;
		}
		button{
			background-color: #FFBB00;
			font-size: 14px;
			color: #000;
			border: 1px solid #000;
			border-radius: 5px;
			padding: 3px;
			margin: 3px;
		}
		.inputTable th{
			padding: 5px;
		}
		.inputTable input{
			width: 330px;
			height: 25px;
		}
	</style>
</head>

<script type="text/javascript">
	var ws;
	window.onload = function(){
		getRoom();
		createRoom();
	}

	function getRoom(){
		commonAjax('/getRoom', "", 'post', function(result){
			createChatingRoom(result);
		});
	}
	
	function createRoom(){
		$("#createRoom").click(function(){
			var msg = {	roomName : $('#roomName').val()	};

			commonAjax('/createRoom', msg, 'post', function(result){
				createChatingRoom(result);
			});

			$("#roomName").val("");
		});
	}

	function goRoom(number, name){
		location.href="/moveChating?roomName="+name+"&"+"roomNumber="+number;
	}

	function createChatingRoom(res){
		if(res != null){
			var tag = "<tr><th class='num'>순서</th><th class='room'>방 이름</th><th class='go'></th></tr>";
			res.forEach(function(d, idx){
				var rn = d.roomName.trim();
				var roomNumber = d.roomNumber;
				tag += "<tr>"+
							"<td class='num'>"+(idx+1)+"</td>"+
							"<td class='room'>"+ rn +"</td>"+
							"<td class='go'><button type='button' onclick='goRoom(\""+roomNumber+"\", \""+rn+"\")'>참여</button></td>" +
						"</tr>";	
			});
			$("#roomList").empty().append(tag);
		}
	}

	function commonAjax(url, parameter, type, calbak, contentType){
		$.ajax({
			url: url,
			data: parameter,
			type: type,
			contentType : contentType!=null?contentType:'application/x-www-form-urlencoded; charset=UTF-8',
			success: function (res) {
				calbak(res);
			},
			error : function(err){
				console.log('error');
				calbak(err);
			}
		});
	}
</script>
<body>
	<div class="container">
		<h1>채팅방</h1>
		<div id="roomContainer" class="roomContainer">
			<table id="roomList" class="roomList"></table>
		</div>
		<div>
			<table class="inputTable">
				<tr>
					<th>방 제목</th>
					<th><input type="text" name="roomName" id="roomName"></th>
					<th><button id="createRoom">방 만들기</button></th>
				</tr>
			</table>
		</div>
	</div>
</body>
</html>

최초 페이지 접근시 비동기 ajax를 통해 방의 정보를 가져오고 접속요청한 버튼에 따라 서버에서 검증후 결과에 따라 페이지가 이동되는 jsp입니다.

 

채팅방

 

소켓관련된 jsp가 아니므로 자세한 설명은 생략하겠습니다.

다음장에서는 작성한 room.jsp의 데이터를 가지고 방정보를 저장 후 소켓통신시 해당 데이터들을 활용하여 구분처리하도록 하겠습니다.

반응형