본문 바로가기

회고록(TIL&WIL)

TIL 2023.03.20 JPA, WebSocket

JPA

properties

# DB Connection
spring.datasource.dbcp2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.99.100:3306/mydb
spring.datasource.username=scott
spring.datasource.password=tiger

# jpa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create-drop

# logging slf4j
logging.level.web=info
logging.level.com.bit.boot06=debug
spring.jpa.hibernate.ddl-auto=?
create : 기존테이블 삭제 후 다시 생성
create-drop: create와 같으나 종료시점에 테이블 DROP
update: 변경분만 반영
validate: 엔티티와 테이블이 정상 매핑되었는지만 확인
none: 사용하지 않음

Entity

import ...;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Getter
@Entity
@Table(name = "dept03")
public class Dept03 {
	@Id // pk
//	@GeneratedValue(strategy = GenerationType.AUTO) // auto_increment
	private long deptno;
//	@Column(name = "domainname", columnDefinition = "varchar(8) default '제목없음'", nullable = false)
	@Column(columnDefinition = "varchar(8)")
	private String dname;
//	@Column(name = "location", columnDefinition = "text not null")
	private String loc;
	
	
	public ResponseDeptVo getEntity() {
		return ResponseDeptVo.builder()
				.deptno(deptno)
				.dname(dname)
				.loc(loc).build();
	}
	
}

DTO - Entity의 영속성을 지키기 위해서 Request, Response DTO를 정의해서 사용한다.

import ...;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class RequestDeptVo {
	private long deptno;
	private String dname;
	private String loc;
}

Repository

JpaRepository를 상속받기만 해도 기본적인 CRUD와 관련된 메서드들이 구현되어있으며 바로 사용 할 수 있다.

import java.util.List;

import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import com.bit.boot06.domain.entity.Dept03;

public interface DeptRepo extends JpaRepository<Dept03, Long>{
	
	List<Dept03> findAll(Sort sort);
	
	List<Dept03> findAllByOrderByDeptnoDesc();

	List<Dept03> findAllByOrderByDnameDesc();
	
	Dept03 findByDname(String string);

	List<Dept03> findByDname(String dname, Sort sort);

	Dept03 findByDnameContaining(String dname);

	@Query(value = "select * from dept03 a", nativeQuery = true)
	List<Dept03> myall();
}

Entity Test (Junit 5 jupiter)

import ...;

@SpringBootTest
public class DeptRepoTest {
	@Autowired
	private DeptRepo deptRepo;
	
	@Test
	public void test() {
		Dept03 entity = Dept03.builder()
				.deptno(1111)
				.dname("teset")
				.loc("test")
				.build();
		deptRepo.save(entity);
	}

}

Service

import ...;

@Service
@AllArgsConstructor
public class DeptService {
	
	private final DeptRepo deptRepo;
	
	ResponseDeptVo insertOne(RequestDeptVo bean) {
		Dept03 entity = Dept03.builder()
				.deptno(bean.getDeptno())
				.dname(bean.getDname())
				.loc(bean.getLoc())
				.build();
		return deptRepo.save(entity).getEntity();
	}
	
	ResponseDeptVo selectOne(long deptno) {
//		return deptRepo.findById(deptno).get().getEntity();
//		return deptRepo.findByDname("tester1").getEntity();
		return deptRepo.findByDnameContaining("1").getEntity();
	}
	
	ResponseDeptVo updateOne(RequestDeptVo bean) {
		Optional<Dept03> op = deptRepo.findById(bean.getDeptno());
		if(op.isEmpty()) throw new RuntimeException("존재하지 않음");
		return deptRepo.save(Dept03.builder()
				.deptno(op.get().getDeptno())
				.dname(bean.getDname())
				.loc(bean.getLoc())
				.build()).getEntity();
		
	}
	
	void deleteOne(long deptno) {
		Optional<Dept03> op = deptRepo.findById(deptno);
		if(op.isEmpty()) throw new RuntimeException("존재하지 않음");
//		deptRepo.deleteById(deptno);
		deptRepo.delete(op.get());
	}
	
	List<ResponseDeptVo> selectAll() {
		List<ResponseDeptVo> list = new ArrayList<>();
		Sort sort = Sort.by("deptno").descending();
//		deptRepo.findAllOrderByIdDesc().forEach(bean->list.add(bean.getEntity()));
//		for(Dept03 bean : deptRepo.findAllByOrderByDeptnoDesc()) {
		for(Dept03 bean : deptRepo.findAll(sort)) {
//			list.add(bean.getEntity());
			list.add(ResponseDeptVo.builder()
					.deptno(bean.getDeptno())
					.dname(bean.getDname())
					.loc(bean.getLoc())
					.build());
		}
		return list;
	}
	
	List<ResponseDeptVo> selectAll(int pageNum, int limit){
		List<ResponseDeptVo> list = new ArrayList<>();
		Sort sort = Sort.by("deptno").descending();
		Pageable page = PageRequest.of(pageNum, limit, sort);
//		Pageable page = Pageable.ofSize(limit).withPage(pageNum);
		deptRepo.findAll(page).forEach(ele->list.add(ele.getEntity()));;
		return list;
	}
	
	List<ResponseDeptVo> myall() {
		List<ResponseDeptVo> list = new ArrayList<>();
		deptRepo.myall().forEach(bean->list.add(bean.getEntity()));
		return list;
	}
	
}

Service Test (Junit 5 jupiter)

import ...;

@SpringBootTest
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class DeptServiceTest {
	@Autowired
	DeptService deptService;
	RequestDeptVo target ;
	
	@BeforeEach
	public void before() {
		target = RequestDeptVo.builder()
				.deptno(7777)
				.dname("tester7")
				.loc("test")
				.build();
	}
	
	@Test
	@Transactional
	@Commit
	@Order(1)
	void test1InsertOne() {
//		assertNotNull(deptService);
		
		ResponseDeptVo result = deptService.insertOne(RequestDeptVo.builder()
				.deptno(target.getDeptno())
				.dname(target.getDname())
				.loc(target.getLoc())
				.build());
		log.debug(result.toString());
		assertEquals(target.getDeptno(), result.getDeptno());
		assertEquals(target.getDname(), result.getDname());
		assertEquals(target.getLoc(), result.getLoc());
	}
	
	@Test
	@Order(2)
	public void test2SelectOne() {
		ResponseDeptVo result = deptService.selectOne(target.getDeptno());
		log.debug(result.toString());
		assertEquals(target.getDeptno(), result.getDeptno());
		assertEquals(target.getDname(), result.getDname());
		assertEquals(target.getLoc(), result.getLoc());
	}
	
	@Test
	@Order(3)
	public void test3UpdateOne() {
		target.setDname("한글");
		ResponseDeptVo result = deptService.updateOne(target);
		log.debug(result.toString());
		assertEquals(target.getDeptno(), result.getDeptno());
		assertEquals(target.getDname(), result.getDname());
		assertEquals(target.getLoc(), result.getLoc());
	}
	
	@Test
	@Order(4)
	public void test4DeleteOne() {
		deptService.deleteOne(target.getDeptno());
	}
	
	@Test
	@Order(5)
	public void test5SelectAll() {
		for(ResponseDeptVo bean : deptService.selectAll()) {
			System.out.println(bean.toString());
		}
	}
	
	@Test
	public void test6SelectPaging() {
		for(ResponseDeptVo bean : deptService.selectAll(0,5)) {
			System.out.println(bean);
		}
	}
	
	@Test
	public void test7MyAll() {
		for(ResponseDeptVo bean : deptService.myall()) {
			System.out.println(bean);
		}
	}
	
}

WebSocket

EchoHandler

import ...;

public class EchoHandler extends TextWebSocketHandler {
	private static Map<String, WebSocketSession> list = new HashMap<>();
	
	public static Map<String, WebSocketSession> getList() {
		return list;
	}
	@Override
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		System.out.println("종료...");
		list.remove(session.getAttributes().get("sid"));
	}
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		System.out.println("접속됨...");
		System.out.println("websocket session ID:" + session.getId());
//		for(Entry<String,Object> entry :session.getAttributes().entrySet()) {
//			System.out.println(entry.getKey() + ":" + entry.getValue());
//		}
		list.put((String)session.getAttributes().get("sid"), session);
	
	}
	@Override
	public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
		System.out.println("메세지수신... " + message);
		for(WebSocketSession sock : list.values()) {
			sock.sendMessage(message);
		}
	}
}

Controller

package com.bit.boot07;

import java.io.IOException;
import java.util.Map;

import javax.servlet.http.HttpSession;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;

import com.bit.boot07.config.EchoHandler;

import lombok.AllArgsConstructor;

@Controller
@AllArgsConstructor
public class RootController {
	
	final WebSocketHandler webSocketHandler;
	
	@GetMapping("/ex01")
	public String ex01(HttpSession session, String id) throws Exception {
		session.setAttribute("user", "admin");
		session.setAttribute("sid", session.getId());
		
		
		return "ex01";
	}
	
	@GetMapping("/ex02")
	public String ex02(HttpSession session) throws IOException {
		System.out.println("Controller session ID:"+session.getId());
		System.out.println(webSocketHandler);
		
		Map<String, WebSocketSession> list = ((EchoHandler)webSocketHandler).getList();
		list.get(session.getId()).sendMessage(new TextMessage("서버에서 보낸 메세지"));
		return "ex02";
	}
	
	@GetMapping("/ex03")
	public String ex03(HttpSession session) {
		session.invalidate();
		return "ex03";
	}
}

View - index, ex01 등 전부 동일

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<script src="https://code.jquery.com/jquery-3.6.4.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
	</head>
	<body>
		<h1>Index Page</h1>
		<div>
			<input/>
			<button>전송</button>
		</div>
				
		<ul></ul>
		
		<script type="text/javascript">
			// WebSocket 연결 생성
			const socket = new WebSocket('ws://localhost:8080/echo');
			
			// 연결이 열리면
			socket.addEventListener('open', function (event) {
			    socket.send('Hello Server!');
			    console.log(socket, event);
			});
			// 연결이 닫히면
			socket.addEventListener('close', function(e){
				setTimeout(function() {
					// 재접속
					socket = new WebSocket('ws://localhost:8080/echo')
				}, 1000);
			})
			
			// 메시지 수신
			socket.addEventListener('message', function (event) {
			    console.log('Message from server ', event.data);
			    $('<li/>').text(event.data).appendTo('ul');
			});
			
			$(function() {
				$('button').click(function(){
					let msg = $('input').val();
					socket.send(msg);
					$('input').val('');
				});
			});
			
		</script>
	</body>
</html>

SocketConfig

import ...;

@Configuration
@EnableWebSocket
public class SocketConfig implements WebSocketConfigurer{
	@Bean
	WebSocketHandler getWebSocketHandler() {
		return new EchoHandler();
	}
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		WebSocketHandlerRegistration reg = registry.addHandler(getWebSocketHandler(), "/echo");
		reg.addInterceptors(new HttpSessionHandshakeInterceptor());
	}
}

Sockjs

EchoHandler는 위에서 작성한 것 그대로 재활용

WebSocketConfig

package com.bit.boot08.config;

import org.springframework.context.annotation.Bean;
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;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
	
	@Bean
	EchoHandler getEchoHandler() {
		return new EchoHandler();
	}
	@Override
	public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
		registry.addHandler(getEchoHandler(), "/echo")
				.withSockJS();
	}
}

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
		<script src="https://code.jquery.com/jquery-3.6.4.min.js" integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
	</head>
	<body>
		<h1>index page</h1>
		
		<script type="text/javascript">
			 var sock = new SockJS('/echo');
			 sock.onopen = function() {
			     console.log('open');
			     sock.send('test');
			 };
			
			 sock.onmessage = function(e) {
			     console.log(e); // data, timestamp
			     //sock.close();
			 };
			
			 sock.onclose = function() {
			     console.log('close');
			 };
		</script>		
	</body>
</html>

Spring Websocket

HelloMessage & Greeting

package com.bit.boot08;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class HelloMessage {
	private String cmd;
	private String name;
}
---------------------------------
package com.bit.boot08;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Greeting {
	private String content;
}

GreetingController

package com.bit.boot08;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;

@Controller
public class GreetingController {
	@MessageMapping("/hello")
	@SendTo("/topic/greetings")
	public Greeting greeting(HelloMessage message) throws Exception {
//	public Greeting greeting(String message) throws Exception {
		Thread.sleep(1000); // simulated delay
		return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
//		return new Greeting("Hello, " + message + "!");
	}
}

WebSocketConfig

package com.bit.boot08.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    config.enableSimpleBroker("/topic");
    config.setApplicationDestinationPrefixes("/app");
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/gs-guide-websocket").withSockJS();
  }
}

index.html

<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8">
	<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script>
	<script src="https://code.jquery.com/jquery-3.6.4.min.js"
		integrity="sha256-oP6HI9z1XaZNBrJURtCoUT5SUnxFr8s3BzRl+cbzUq8=" crossorigin="anonymous"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"
		integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g=="
		crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</head>

<body>
	<h1>index page</h1>
	<noscript>
		<h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being
			enabled. Please enable
			Javascript and reload this page!</h2>
	</noscript>
	<div id="main-content" class="container">
		<div class="row">
			<div class="col-md-6">
				<form class="form-inline">
					<div class="form-group">
						<label for="connect">WebSocket connection:</label>
						<button id="connect" class="btn btn-default" type="submit">Connect</button>
						<button id="disconnect" class="btn btn-default" type="submit" disabled="disabled">Disconnect
						</button>
					</div>
				</form>
			</div>
			<div class="col-md-6">
				<form class="form-inline">
					<div class="form-group">
						<label for="name">What is your name?</label>
						<input type="text" id="name" class="form-control" placeholder="Your name here...">
					</div>
					<button id="send" class="btn btn-default" type="submit">Send</button>
				</form>
			</div>
		</div>
		<div class="row">
			<div class="col-md-12">
				<table id="conversation" class="table table-striped">
					<thead>
						<tr>
							<th>Greetings</th>
						</tr>
					</thead>
					<tbody id="greetings">
					</tbody>
				</table>
			</div>
		</div>
	</div>
	<script type="text/javascript">
		var stompClient = null;

		function setConnected(connected) {
			$("#connect").prop("disabled", connected);
			$("#disconnect").prop("disabled", !connected);
			if (connected) {
				$("#conversation").show();
			}
			else {
				$("#conversation").hide();
			}
			$("#greetings").html("");
		}
		// 접속
		function connect() {
			var socket = new SockJS('/gs-guide-websocket');
			stompClient = Stomp.over(socket);
			stompClient.connect({}, function (frame) {
				setConnected(true);
				console.log('Connected: ' + frame);
				// 수신 Listener Event - 큐스택에 쌓여있는 데이터를 불러오도록 정기적으로 새로고침
				stompClient.subscribe('/topic/greetings', function (greeting) {
					showGreeting(JSON.parse(greeting.body).content);
				});
			});
		}
		
		// 접속종료
		function disconnect() {
			if (stompClient !== null) {
				stompClient.disconnect();
			}
			setConnected(false);
			console.log("Disconnected");
		}
		
		// 메세지 보내기
		function sendName() {
			stompClient.send("/app/hello", {}, JSON.stringify({cmd:'귓속말', name: $("#name").val()}));
			//stompClient.send("/app/hello", {}, JSON.stringify({'name': $("#name").val()}));
		}

		function showGreeting(message) {
			$("#greetings").append("<tr><td>" + message + "</td></tr>");
		}

		$(function () {
			$("form").on('submit', function (e) {
				e.preventDefault();
			});
			$("#connect").click(function () {connect();});
			$("#disconnect").click(function () {disconnect();});
			$("#send").click(function () {sendName();});
		});
	</script>
</body>

</html>