Name: 테스트 계획에 대한 이름을 지정하는 필드입니다. 기본값은 "Thread Group"입니다.
Comments: 해당 Thread Group에 대한 설명이나 주석을 추가할 수 있는 필드입니다. 테스트 계획을 설명하는 데 유용합니다.
Action to be taken after a Sampler error:
Continue: 오류가 발생해도 테스트를 계속 진행
Start Next Thread Loop: 현재 스레드의 남은 작업을 중지하고 다음 루프를 시작
Stop Thread: 현재 스레드를 중지
Stop Test: 전체 테스트를 중지
Thread Properties:
Number of Threads (users): 테스트에 사용할 가상 사용자의 수를 설정 (100명으로 설정)
Ramp-up period (seconds): 모든 스레드를 시작하는 데 걸리는 시간을 설정 -> 0.01초마다 1명씩 총 100명이 스레드 실행
Loop Count: 각 스레드가 샘플러를 실행할 횟수를 설정합니다. "Infinite" 옵션을 선택하면 무한 반복됩니다.
Same user on each iteration: 체크하면 각 스레드가 반복할 때 동일한 사용자로 동작합니다.
Number of Threads (users): 100명의 가상 사용자를 생성하여 테스트를 수행합니다.
Ramp-up period (seconds): 1초 동안 모든 스레드를 시작합니다.
Loop Count: 각 스레드가 테스트 샘플러를 한 번 실행합니다.
Same user on each iteration: 동일한 스레드가 동일한 사용자로 반복 동작합니다.
주요 내용은 100명의 사용자가 0.01초마다 테스트를 진행할 예정이다.
홈페이지 접속하기
Thread Group - add - Sampler - HTTP Request 클릭
Path - 테스트할 페이지의 URL 입력 Server Name or IP - IP 입력
테스트 결과 확인하기
Thread Group - Add - Listener - 원하는 report 클릭 (여기선 View Result Tree 및 플러그인 3 Basic Graph의 Active Thread Over time을 사용)
View Result Tree - 테스트 상세 내역을 확인가능
Active Thread Over time - 전체 테스트 시간 확인가
로그인 진행하기
동일한 HTTP Request를 생성하며 HTTP Request를 POST로 변경, Body Data를 json형태로 입력 가능하다.
401에러가 나타날경우
Add - Config Element - Http Header Mnager를 다음과 같이 설정해준다.
CSV의 경우
만일 데이터를 동적으로 할당하고 싶은경우
Add - Config Element -CSV Data Set Config 클릭
filename은 자신이 사용할 CSV파일을 지정해준다
login은 다음과 같이 "${value}"의 형태로 사용한다
만약 에러가 나타날경우추가적인 설정을 진행해줘야 한다. 에러에 대한 자세한 내용은 해당부분에서 확인 가능하다
1. DB랑 통신이 되어있는가?
2. 스프링 시큐리티 설정으로 인해 접근이 안되는가?
3. 로그인 정보가 올바른가?
view Results Tree에서 테스트 결과 확인이 가능하다
WEBSOCKET 테스트
해당 설정은 Websocket의 설정이지만 내 방식은 STOMP를 활용하기에 조금 다른 형태로 진행한다.
처음 테스트를 진행했을때 계속 에러가 나왔는데 확인해보니 STOMP의 경우 URL이 변계속 연결부분이 바뀌는 문제가 있었다 (개발자도구에서 확인 가능)
자신이 만든 WebSocketConfig 에서 Endpoint 입력할것
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/example/stomp-endpoint")
.setAllowedOrigins("*")
.withSockJS();
}
${counter}: 랜덤한 index값
Jmeter - ADD - ConfigElemnet - Counter 클릭
111 부터 1씩 올리는 형태로 설정
${__RandomString(8,abcdefghijklmnopqrstuvwxyz,sessionld)}: 각 연결에 대해 고유한 임의의 문자열을 생성한다 (8글자를 사용하며 랜덤으로 사용한다)
/websocket: WebSocket 프로토콜을 사용하여 연결을 설정
전체 depth는 다음과 같다.
결과
100개의 스레드가 10초간 실행되도록 하였을때 10초 부분까지 점진적으로 증가하다 테스트가 스레드가 종료되는 10초 이후부터 감소하는것을 확인할수 있다.
논리적인 의미의 소켓은 컴퓨터 네트워크를 경유하는 프로세스간 통신의 종착점을 의미하며 서로 네트워크를 이용해 데이터를 송수신할경우 최종적으로 거치게되는 endpoint를 뜻한다.
클라이언트
socket() 함수를 호출하여 클라이언트 소켓을 생성
connect() 함수를 호출하여 서버 소켓에 연결을 요청 - 서버의 IP 주소와 포트 번호를 사용하여 연결을 시도
send() 함수를 사용하여 데이터를 서버로 전송, recv() 함수를 사용하여 서버로부터 데이터를 수신
통신이 완료되면 close() 함수를 호출하여 소켓을 닫고 연결을 종료
서버
socket() 함수를 호출하여 서버 소켓을 생성
bind() 함수를 사용하여 생성된 소켓을 특정 IP 주소와 포트 번호에 바인딩 ->서버가 특정 네트워크 인터페이스와 포트에서 요청을 수신할 수 있도록 설정
listen() 함수를 호출하여 소켓을 수신 대기 상태로 변경
accept() 함수를 사용하여 클라이언트의 연결 요청을 수락
send() 함수를 사용하여 데이터를 클라이언트로 전송, recv() 함수를 통해 클라이언트로부터 데이터를 수신
통신이 완료되면 close() 함수를 호출하여 소켓을 닫고 연결을 종료
역할과 위치
소켓은 OSI 7계층 중 실제 유저들과 상호작용하는응용 계층(application layer)과 데이터를 송수신하는 전송 계층(transport layer)사이에 위치하며, 응용 프로그램이 데이터를 송수신하기 위해 TCP/IP를 사용하는 인터페이스 역할을 담당한다.
한 프로세스는 여러 개의 소켓을 사용할 수 있으며 소켓 번호를 통해 각 소켓을 구분하며 하나의 호스트에서 여러 호스트와 동시에 통신할 수 있습니다.
또한 통신을 통해 전달되는 모든 데이터 포맷은 5-tuple 이라는 규격에 맞추어 진행되며 여기에 필요로 하는것은 크게 IP 주소와 포트 번호를 필요로한다.
IP 주소: 데이터를 송신할 호스트를 식별.
포트 번호: 호스트 내의 특정 프로세스를 식별.
TCP: 데이터 전송 전에 연결을 설정하여 신뢰성 있는 통신을 제공합니다.
5-tuple
프로토콜 (Protocol)
호스트 IP 주소 (source IP address)
호스트 port 번호 (source port nunber)
목적지 IP 주소 (destination IP address)
목적지 port 번호 (destination port number)
server-client 구조
TCP/UDP 위에서 동작하므로 당연하게 server-client 통신 구조를 갖춘다.
처음에 데이터를 보내는 쪽이 client가 되고, 받는 쪽이 server가 된다. 이후에는 서로가 데이터를 송수신할 수 있다. (자세한 설명은 아래)
양방향 통신
socket은 한 쪽에서 데이터를 보내고 반대 편에서 이를 수신한 뒤 연결이 끊어지는게 아니라 양 쪽에서 실시간으로 데이터를 송수신할 수 있다. 따라서 실시간 스트리밍이나 채팅에 주로 유용하게 사용된다.
프로그래밍 언어나 운영체제에 종속적
socket은 TCP/IP 표준이 아니라 네트워크 프로그래밍 인터페이스다. 따라서 운영체제마다 사용법이 약간씩 다르며, 그 안에서 또 프로그래밍 언어마다 소켓 api 를 구현한 라이브러리가 다 다르다.
JAVA의 경우
import java.io.*;
import java.net.*;
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
while (true) {
Socket clientSocket = serverSocket.accept();
OutputStream output = clientSocket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true);
writer.println("Welcome to the server!");
clientSocket.close();
}
}
}
Node의 경우
const net = require('net');
const server = net.createServer((socket) => {
console.log('Connection from', socket.remoteAddress, 'has been established.');
socket.write('Welcome to the server!\n');
socket.end();
});
server.listen(8080, () => {
console.log('Server is listening on port 8080...');
});
HTTP가 웹에서 데이터를 교환하기 위한 프로토콜이라고 하였다.
해당 방식은 클라이언트가 요청을 보내면 서버가 해당 요청에 따른 응답을 반환하지만 HTTP의 특성인 무연결성으로 인하여 신규 데이터가 등록되더라도 사용자가 이를 보기 위해선 항상 새로운 요청을 보내야 등록된 데이터를 가져올수 있었다. (매 요청마다 새로운 연결을 생헝하고 응답후 연결을 끊는 방식)
그렇다면 실시간으로 채팅을 구현하기 위해선 어떤 방식을 활용해야 핳까?
웹소켓
기존 HTTP의 무연결성을 해결하여 서버와 브라우저간 양방향 통신이 가능하게 해주는 기술로 한번 연결이 설정되면 끊어질때까지 서버와 클라이언트 모두 데이터를 전송할수 있게된다
저지연: 데이터를 즉각적으로 전송할 수 있다
HTTP 호환성: 웹소켓 연결은 HTTP 요청을 통해 시작되며, ws 또는 보안 연결인 wss 프로토콜을 사용
효율적인 자원 사용: 한 번의 연결 설정으로 지속적인 데이터 교환을 할 수 있어 자원 사용이 효율적입니다. (반복적인 handshake 생략)
동작방식
연결초기화
HTTP 요청을 사용해 서버에 연결하며 Upgrade: websocket 헤더를 포함해 해당 요청이 웹소켓임을 알리며 이에 맞도록 http요청을 웹소켓 연결로 업그레이드
HTTP GET 요청에 Upgrade: websocket, Connection: Upgrade 포함되며 해당 헤더는 웹소켓 프로토콜의 업그레이드를 의미한다.
서버는 HTTP/1.1 101 Switching Protocols 상태 코드와 함께 웹소켓으로 업그레이드 요청을 수락한다는 의미의 Upgrade: websocket 및 Connection: Upgrade 헤더를 포함시킵니다.
핸드셰이크 응답
서버가 요청을 수락하면 http 응답에 Upgrade: websocket 헤더를 포함해 웹소켓 프로토콜로 업그레이드 후 클라이언트에 다시 전달 (TCP 기반 프로토콜)
데이터 교환(연결된 상태)
연결이 될경우 클라이언트와 서버는 네트워크 지연 없이 실시간으로 메세지를 교환할수 있으며 메세지는 텍스트 또는 바이너리 형태가 될수있다.
연결 종료
어느 한쪽이든 연결 종료가 가능하며 종료과정이 합의된 프로토콜에 따라 이뤄진다.
웹소켓 (WebSocket): 웹소켓은 OSI 모델의 응용 계층에 속하며 웹 애플리케이션 실시간 통신을 가능하게 하는 프로토콜이다.
HTTP를 기반으로 초기 연결을 수립한 뒤 웹소켓 프로토콜로 업그레이드하여 데이터를 교환합니다. 웹소켓 프로토콜은 HTTP와 같은 다른 응용 계층 프로토콜 위에서 동작하면서도, 그 자체로 응용 계층의 기능을 수행합니다.
package com.study.config;
import java.util.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.DefaultContentTypeResolver;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic/"); // 클라이언트가 구독할 수 있는 주소의 prefix
registry.setApplicationDestinationPrefixes("/app"); // 메시지를 보낼 때 사용할 prefix
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
System.out.println("초기화 : "+ registry);
registry.addEndpoint("/stomp-endpoint").withSockJS();
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
// TODO Auto-generated method stub
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// TODO Auto-generated method stub
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
// TODO Auto-generated method stub
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
// TODO Auto-generated method stub
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
// TODO Auto-generated method stub
}
@Override
public boolean configureMessageConverters(List<MessageConverter> converters) {
DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
resolver.setDefaultMimeType(MimeTypeUtils.APPLICATION_JSON);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setObjectMapper(new ObjectMapper());
converter.setContentTypeResolver(resolver);
converters.add(converter);
return false; // 기본 컨버터를 추가하지 않음
}
}
jsp코드
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WebSocket Chatting</title>
</head>
<body>
<div>
<button type="button" id="connect" onclick="openSocket();">대화방 참여</button>
<button type="button" id="disconnect" onclick="closeSocket();">대회방 나가기</button>
<br/><br/><br/>
메세지 입력 :
<input type="text" id="sender" value="${sessionScope.id}" style="display: none;">
<input type="text" id="messageinput">
<button type="button" onclick="send();">메세지 전송</button>
<button type="button" onclick="javascript:clearText();">대화내용 지우기</button>
</div>
<div id="messages">
</div>
<script src="https://cdn.jsdelivr.net/npm/sockjs-client/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs/lib/stomp.min.js"></script>
<script type="text/javascript">
var socket = new SockJS('/assetmanager/stomp-endpoint');
var stompClient = Stomp.over(socket);
var messages = document.getElementById("messages");
function openSocket() {
if (stompClient !== null && stompClient.connected) {
writeResponse("WebSocket is already opened.");
return;
}
var socket = new SockJS("/assetmanager/stomp-endpoint");
stompClient = Stomp.over(socket);
stompClient.connect({}, function(frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function(greeting) {
console.log('Received: ', JSON.parse(greeting.body).content);
showGreeting(JSON.parse(greeting.body).content);
});
},
function(error) { // 연결 실패 콜백
console.log('연결 안됨: ' + error);
}
);
console.log("연결프로세스종료");
}
function setConnected(connected) {
document.getElementById("connect").disabled = connected;
document.getElementById("disconnect").disabled = !connected;
if (connected) {
messages.style.display = "";
} else {
messages.style.display = "none";
}
}
function send() {
var name = document.getElementById("messageinput").value;
stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
}
function closeSocket() {
if (stompClient !== null) {
stompClient.disconnect();
}
setConnected(false);
console.log("Disconnected");
}
function showGreeting(message) {
var messages = document.getElementById("messages");
messages.innerHTML += "<br/>" + message;
}
function clearText() {
var messages = document.getElementById("messages");
messages.innerHTML = "";
}
</script>
</body>
</html>
controller
@MessageMapping("/hello") // 클라이언트에서 보낸 메시지를 받을 경로
@SendTo("/topic/greetings") // 메시지를 다시 발행할 경로
public GreetingVO greeting(HelloMessageVO message) throws Exception {
Date now = new Date();
System.out.println(now);
return new GreetingVO(HtmlUtils.htmlEscape(message.getName()));
}
public class CallByValueExample {
public static void addTen(int number) {
number = number + 10; // 매개변수의 값 변경
System.out.println("Inside addTen: " + number);
}
public static void main(String[] args) {
int a = 5;
addTen(a); // a의 값은 여전히 5
System.out.println("In main after addTen: " + a);
}
}
정의 - Call by value 방식에서는 함수에 인자를 전달할 때, 인자의 실제 값(value)이 복사되어 함수의 매개변수로 전달됩니다.
동작 방식
함수가 호출될 때, 인자의 값이 새로운 메모리 위치에 복사되며 함수 내에서 매개변수의 값을 변경해도, 원래 인자의 값은 불변.
특징
함수 내에서 매개변수의 값을 변경해도 원본 데이터에는 영향을 미치지 않아 부수 효과(side effect)가 없다
큰 데이터를 복사할 때 성능 저하가 발생우려
함수 안에서 인자 값이 변경되더라도, 외부 변수 값은 변경되지 않는다.
addTen(a);를 하여도 기존의 a는 변경되지 않는다
Call by Reference
public class CallByValueOfReferenceExample {
static class Number {
int value;
Number(int value) {
this.value = value;
}
}
public static void addTen(Number number) {
number.value += 10; // 객체의 상태 변경
System.out.println("Inside addTen: " + number.value);
number = new Number(30); // 매개변수에 새로운 객체 할당
System.out.println("Inside addTen after new assignment: " + number.value);
}
public static void main(String[] args) {
Number myNumber = new Number(5);
addTen(myNumber); // myNumber 객체의 상태가 변경됨
System.out.println("In main after addTen: " + myNumber.value); // new Number(30) 할당은 영향을 미치지 않음
}
}
정의: 함수에 인자를 전달할 때, 인자의 메모리 주소(reference)가 전달됩니다. 즉, 함수는 원본 인자를 직접 참조하게 됩니다.
동작 방식: 함수가 호출될 때, 인자의 메모리 주소가 함수의 매개변수로 전달됩니다. 함수 내에서 매개변수를 통해 원본 데이터를 직접 변경할 수 있습니다.
특징: 함수 내에서의 변경이 원본 데이터에 직접 반영되므로, 원본 데이터를 쉽게 수정할 수 있습니다. 그러나 이로 인해 부수 효과가 발생할 수 있으며, 원본 데이터를 실수로 변경할 위험이 있습니다.
addTen(myNumber);를 진행할경우 myNumber의 값이 변경된다.
자바의 경우 call by value 방식을 사용한다.
원본 데이터를 변경할 위험이 줄어들며
하단의 코드는 Call By value를 실제 코드를 비교한 방법이며
callme 함수를 호출한 하여도 원본 객체인 valueTest1의 값이 변경되지 않음을 확인할수있다.
구글에 "고양이" 를 검색했을때 버튼이 나타나며 이것을 클릭했을때 현재 마우스 클릭위치를 기준으로 고양이 발바닥이 나타나는 이스터에그가 존재한다.
이것을 만들어볼거다...
기초 흐름
1. 사용할 img를 정한다
2. 클릭을 했을때 특정한 구역(box 등)에 나타난다
3. 랜덤하게 회전된 모습으로 출력한다.
조건
1. 이미지는 겹쳐지도록 만들어야한다.
2. 사이즈는 200,200으로 잡았다.
3. 구역의 사이즈는 동적페이지로 구성해야한다 (화면이 커져도 작아져도 그에 맞춰지도록)
enjoy.jsp
<section name="form-section">
<form id="fileUploadForm" enctype="multipart/form-data">
<input type="file" name="file" id="file" onchange="previewImage(this);" />
<img id="preview" style="max-width: 200px; max-height: 200px;">
<button type="button" onclick="uploadFile()">Upload</button>
</form>
</section>
//이미지가 나올 컨테이너
<figure id="img-section"></figure>
<button id="loadImage">버튼을 클릭하세요</button>
<button id="clear">삭제하기</button>
<script>
let uploadedImageUrl = null;
//업로드 이미지 미리보기
function previewImage(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
uploadedImageUrl = e.target.result; // 업로드된 이미지의 URL 저장
document.getElementById('preview').src = uploadedImageUrl;
}
reader.readAsDataURL(input.files[0]);
}
}
//이미지 랜덤 배치하기
document.getElementById('loadImage').addEventListener('click', function() {
if (uploadedImageUrl) {
const imgSection = document.getElementById('img-section');
const img = document.createElement('img');
img.src = uploadedImageUrl;
// 이미지의 크기를 200x200 픽셀로 설정
img.style.width = '200px';
img.style.height = '200px';
img.style.position = 'absolute';
//양 끝 위치에 배치시 잘릴 우려가 있어 이미지 크기만큼 제외
const maxTop = imgSection.clientHeight - 200;
const maxLeft = imgSection.clientWidth - 200;
img.style.top = Math.floor(Math.random() * maxTop) + 'px';
img.style.left = Math.floor(Math.random() * maxLeft) + 'px';
// 이미지에 랜덤한 회전 각도 적용
const rotationDegree = Math.floor(Math.random() * 360); // 0도에서 359도 사이의 랜덤한 값
//ES6 이전 버전이라 이렇게 작성
//ES6 이후는 밑의 주석처리 코드로 작성
//img.style.transform = `rotate(${rotationDegree}deg)`;
img.style.transform = 'rotate(' + rotationDegree + 'deg)';
let scale = 0;
let opacity = 0;
const scaleStep = 0.05;
const opacityStep = 0.05;
function animate() {
scale += scaleStep;
opacity += opacityStep;
if (scale < 1) {
img.style.transform = `rotate(${rotationDegree}deg) scale(${scale})`;
img.style.opacity = opacity;
requestAnimationFrame(animate);
} else {
img.style.transform = `rotate(${rotationDegree}deg) scale(1)`;
img.style.opacity = 1;
}
}
requestAnimationFrame(animate);
imgSection.appendChild(img);
} else {
alert('먼저 이미지를 업로드해주세요.');
}
});
//삭제 애니메이션
document.getElementById('clear').addEventListener('click', function() {
var imgSection = document.getElementById('img-section');
// 높이와 투명도를 0으로 변경
imgSection.style.height = '0px';
imgSection.style.opacity = '0';
// 전환 효과가 완료되면 내용 삭제
imgSection.addEventListener('transitionend', function() {
imgSection.innerHTML = '';
imgSection.style.height = '';
imgSection.style.opacity = '1';
}, { once: true });
});
</script>
CSS
#img-section {
width: 100vw;
height: 100vh; /* 뷰포트 높이의 80%로 설정 */
justify-content: center; /* 가로 중앙 정렬 */
align-items: center;
margin: 10px;
position: relative; /* 이미지들이 이 요소 내에서 절대 위치를 가짐 */
overflow: hidden; /* 내부 요소가 밖으로 넘치지 않도록 함 */
transition: height 0.5s ease, opacity 0.5s ease; /* 높이와 투명도 변화에 대한 전환 효과 적용 */
}