
TIL: Java SpringBoot 대 Asp.Net REST API 성능
TIL: Java Spring 부트와 Asp.Net REST API 성능 비교
오늘 저는 Java SpringBoot 및 Asp.Net 성능에 대해 배웠습니다(#til).
곧 출시될 .Net 7 릴리스와 최소 API를 테스트하고 있었는데 속도에 놀랐습니다. 그래서 Java 18 SpringBoot 프로젝트와 비교하기로 결정했습니다.
결과는 Asp.Net 7 솔루션이 Java 18의 SpringBoot 애플리케이션보다 거의 2배 빠릅니다!
결과는 다음과 같습니다.
Asp.Net 7 Minimal API는 SpringBoot Java 18 애플리케이션의 경우 30초당 2,152,070개 요청에 비해 30초 동안 4,084,163개의 요청을 전달합니다!
자바 18 스프링부트
$ k6 run --vus 16 --duration 30s config.json
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: config.json
output: -
scenarios: (100.00%) 1 scenario, 16 max VUs, 1m0s max duration (incl. graceful stop):
* default: 16 looping VUs for 30s (gracefulStop: 30s)
running (0m30.0s), 00/16 VUs, 2152070 complete and 0 interrupted iterations
default ✓ [======================================] 16 VUs 30s
data_received..................: 373 MB 12 MB/s
data_sent......................: 392 MB 13 MB/s
http_req_blocked...............: avg=1.61µs min=441ns med=942ns max=2.39ms p(90)=1.53µs p(95)=1.75µs
http_req_connecting............: avg=308ns min=0s med=0s max=1.55ms p(90)=0s p(95)=0s
http_req_duration..............: avg=180.65µs min=114.4µs med=170.42µs max=4.11ms p(90)=202.21µs p(95)=216.99µs
{ expected_response:true }...: avg=180.65µs min=114.4µs med=170.42µs max=4.11ms p(90)=202.21µs p(95)=216.99µs
http_req_failed................: 0.00% ✓ 0 ✗ 2152070
http_req_receiving.............: avg=22.51µs min=4.59µs med=20.95µs max=3.95ms p(90)=32.44µs p(95)=39.5µs
http_req_sending...............: avg=6.3µs min=2.63µs med=5.61µs max=3.03ms p(90)=8.87µs p(95)=9.59µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=151.83µs min=92.24µs med=142.39µs max=3.43ms p(90)=172.73µs p(95)=187.11µs
http_reqs......................: 2152070 71733.676043/s
iteration_duration.............: avg=218.91µs min=138.9µs med=207.65µs max=4.54ms p(90)=244.87µs p(95)=263.15µs
iterations.....................: 2152070 71733.676043/s
vus............................: 16 min=16 max=16
vus_max........................: 16 min=16 max=16
Asp.Net 7
$ k6 run --vus 16 --duration 30s config.json
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: config.json
output: -
scenarios: (100.00%) 1 scenario, 16 max VUs, 1m0s max duration (incl. graceful stop):
* default: 16 looping VUs for 30s (gracefulStop: 30s)
running (0m30.0s), 00/16 VUs, 4084163 complete and 0 interrupted iterations
default ✓ [======================================] 16 VUs 30s
data_received..................: 845 MB 28 MB/s
data_sent......................: 743 MB 25 MB/s
http_req_blocked...............: avg=1.06µs min=431ns med=972ns max=2.48ms p(90)=1.41µs p(95)=1.62µs
http_req_connecting............: avg=0ns min=0s med=0s max=66.6µs p(90)=0s p(95)=0s
http_req_duration..............: avg=74.37µs min=30.59µs med=67.84µs max=7.33ms p(90)=91.37µs p(95)=106.97µs
{ expected_response:true }...: avg=74.37µs min=30.59µs med=67.84µs max=7.33ms p(90)=91.37µs p(95)=106.97µs
http_req_failed................: 0.00% ✓ 0 ✗ 4084163
http_req_receiving.............: avg=13.64µs min=4.43µs med=12.72µs max=7.05ms p(90)=17.75µs p(95)=19.44µs
http_req_sending...............: avg=6.36µs min=2.77µs med=5.96µs max=5.83ms p(90)=8.24µs p(95)=8.96µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=54.35µs min=19.55µs med=48.45µs max=5.34ms p(90)=70.1µs p(95)=83.96µs
http_reqs......................: 4084163 136135.973692/s
iteration_duration.............: avg=113.11µs min=56.58µs med=105.71µs max=7.9ms p(90)=133.41µs p(95)=153.52µs
iterations.....................: 4084163 136135.973692/s
vus............................: 16 min=16 max=16
vus_max........................: 16 min=16 max=16
세부
목표는 JSON 본문을 가져와 역직렬화하고 값을 수정하고 직렬화된 JSON 응답을 반환하는 간단한 HTTP POST API를 테스트하는 것이었습니다.
요청 및 응답 개체는 동일하지만 다른 클래스로 표시됩니다. 데이터 수정은 단순히 문자열을 연결하고 정수 값에 추가하는 것입니다.
Asp.Net 프로젝트의 경우 .Net 7의 미리보기 7이 사용되었으며 Java의 경우 Oracle OpenJDK 18.0.2.1과 결합된 종속성으로 spring-boot-starter-web
및 lombok
가 있는 새로운 SpringBoot 프로젝트가 사용되었습니다.
코드는 다음과 같습니다.
자바 18 스프링 부트
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>RestApiDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>RestApiDemo</name>
<description>RestApiDemo</description>
<properties>
<java.version>18</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
RestApiDemoApplication.java
package com.example.restapidemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RestApiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RestApiDemoApplication.class, args);
}
}
입력 모델.자바
package com.example.restapidemo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public final class InputModel {
String FirstName;
String LastName;
Integer Age;
}
출력모델.자바
package com.example.restapidemo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@AllArgsConstructor
public final class OutputModel {
String FirstName;
String LastName;
Integer Age;
}
데모컨트롤러.자바
package com.example.restapidemo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@PostMapping("/get")
@ResponseBody
public OutputModel GetData(@RequestBody InputModel input) {
return new OutputModel(input.FirstName + "x", input.LastName + "x", input.Age + 10);
}
}
Asp.Net 7
프로그램.cs
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/get", ([FromBody] InputModel input) => new OutputModel
{
FirstName = input.FirstName + "x",
LastName = input.LastName + "x",
Age = input.Age + 10
}
);
app.Run();
internal sealed class InputModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
internal sealed class OutputModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
K6.io config.json
import http from 'k6/http';
export default function () {
const url = 'http://localhost:8080/get';
const payload = JSON.stringify({
FirstName: 'John',
LastName: 'Doe',
Age: 33
});
const params = {
headers: {
'Content-Type': 'application/json',
},
};
http.post(url, payload, params);
}
응답 유효성을 위해 CURL로 먼저 테스트:
자바 18 스프링부트
$ curl -X POST http://localhost:8080/get -H "content-type: application/json" -d "{\"FirstName\":\"John\",\"LastName\":\"Doe\",\"Age\":33}"
{"firstName":"Johnx","lastName":"Doex","age":43}%
Asp.Net 7
$ curl -X POST http://localhost:8080/get -H "content-type: application/json" -d "{\"FirstName\":\"John\",\"LastName\":\"Doe\",\"Age\":33}"
{"firstName":"Johnx","lastName":"Doex","age":43}%
.Net 7이 있는 Kestrel 서버는 Java 18 기반 SpringBoot 애플리케이션이 있는 기본 Tomcat 서버와 비교하여 간단한 API를 처리하는 꽤 괜찮은 작업을 수행하는 것 같습니다.
ORM 프레임워크나 로깅을 사용하기로 결정하기 전까지는 베어 언어가 매우 유사하기 때문에 간단한 REST API를 구현하기로 결정할 때 누군가 이것을 사용하는 것을 고려할 수 있습니다.
$ k6 run --vus 16 --duration 30s config.json
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: config.json
output: -
scenarios: (100.00%) 1 scenario, 16 max VUs, 1m0s max duration (incl. graceful stop):
* default: 16 looping VUs for 30s (gracefulStop: 30s)
running (0m30.0s), 00/16 VUs, 2152070 complete and 0 interrupted iterations
default ✓ [======================================] 16 VUs 30s
data_received..................: 373 MB 12 MB/s
data_sent......................: 392 MB 13 MB/s
http_req_blocked...............: avg=1.61µs min=441ns med=942ns max=2.39ms p(90)=1.53µs p(95)=1.75µs
http_req_connecting............: avg=308ns min=0s med=0s max=1.55ms p(90)=0s p(95)=0s
http_req_duration..............: avg=180.65µs min=114.4µs med=170.42µs max=4.11ms p(90)=202.21µs p(95)=216.99µs
{ expected_response:true }...: avg=180.65µs min=114.4µs med=170.42µs max=4.11ms p(90)=202.21µs p(95)=216.99µs
http_req_failed................: 0.00% ✓ 0 ✗ 2152070
http_req_receiving.............: avg=22.51µs min=4.59µs med=20.95µs max=3.95ms p(90)=32.44µs p(95)=39.5µs
http_req_sending...............: avg=6.3µs min=2.63µs med=5.61µs max=3.03ms p(90)=8.87µs p(95)=9.59µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=151.83µs min=92.24µs med=142.39µs max=3.43ms p(90)=172.73µs p(95)=187.11µs
http_reqs......................: 2152070 71733.676043/s
iteration_duration.............: avg=218.91µs min=138.9µs med=207.65µs max=4.54ms p(90)=244.87µs p(95)=263.15µs
iterations.....................: 2152070 71733.676043/s
vus............................: 16 min=16 max=16
vus_max........................: 16 min=16 max=16
$ k6 run --vus 16 --duration 30s config.json
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: config.json
output: -
scenarios: (100.00%) 1 scenario, 16 max VUs, 1m0s max duration (incl. graceful stop):
* default: 16 looping VUs for 30s (gracefulStop: 30s)
running (0m30.0s), 00/16 VUs, 4084163 complete and 0 interrupted iterations
default ✓ [======================================] 16 VUs 30s
data_received..................: 845 MB 28 MB/s
data_sent......................: 743 MB 25 MB/s
http_req_blocked...............: avg=1.06µs min=431ns med=972ns max=2.48ms p(90)=1.41µs p(95)=1.62µs
http_req_connecting............: avg=0ns min=0s med=0s max=66.6µs p(90)=0s p(95)=0s
http_req_duration..............: avg=74.37µs min=30.59µs med=67.84µs max=7.33ms p(90)=91.37µs p(95)=106.97µs
{ expected_response:true }...: avg=74.37µs min=30.59µs med=67.84µs max=7.33ms p(90)=91.37µs p(95)=106.97µs
http_req_failed................: 0.00% ✓ 0 ✗ 4084163
http_req_receiving.............: avg=13.64µs min=4.43µs med=12.72µs max=7.05ms p(90)=17.75µs p(95)=19.44µs
http_req_sending...............: avg=6.36µs min=2.77µs med=5.96µs max=5.83ms p(90)=8.24µs p(95)=8.96µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=54.35µs min=19.55µs med=48.45µs max=5.34ms p(90)=70.1µs p(95)=83.96µs
http_reqs......................: 4084163 136135.973692/s
iteration_duration.............: avg=113.11µs min=56.58µs med=105.71µs max=7.9ms p(90)=133.41µs p(95)=153.52µs
iterations.....................: 4084163 136135.973692/s
vus............................: 16 min=16 max=16
vus_max........................: 16 min=16 max=16
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>RestApiDemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>RestApiDemo</name>
<description>RestApiDemo</description>
<properties>
<java.version>18</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
package com.example.restapidemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RestApiDemoApplication {
public static void main(String[] args) {
SpringApplication.run(RestApiDemoApplication.class, args);
}
}
package com.example.restapidemo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor
public final class InputModel {
String FirstName;
String LastName;
Integer Age;
}
package com.example.restapidemo;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
@AllArgsConstructor
public final class OutputModel {
String FirstName;
String LastName;
Integer Age;
}
package com.example.restapidemo;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoController {
@PostMapping("/get")
@ResponseBody
public OutputModel GetData(@RequestBody InputModel input) {
return new OutputModel(input.FirstName + "x", input.LastName + "x", input.Age + 10);
}
}
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapPost("/get", ([FromBody] InputModel input) => new OutputModel
{
FirstName = input.FirstName + "x",
LastName = input.LastName + "x",
Age = input.Age + 10
}
);
app.Run();
internal sealed class InputModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
internal sealed class OutputModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
import http from 'k6/http';
export default function () {
const url = 'http://localhost:8080/get';
const payload = JSON.stringify({
FirstName: 'John',
LastName: 'Doe',
Age: 33
});
const params = {
headers: {
'Content-Type': 'application/json',
},
};
http.post(url, payload, params);
}
$ curl -X POST http://localhost:8080/get -H "content-type: application/json" -d "{\"FirstName\":\"John\",\"LastName\":\"Doe\",\"Age\":33}"
{"firstName":"Johnx","lastName":"Doex","age":43}%
$ curl -X POST http://localhost:8080/get -H "content-type: application/json" -d "{\"FirstName\":\"John\",\"LastName\":\"Doe\",\"Age\":33}"
{"firstName":"Johnx","lastName":"Doex","age":43}%