Java Quarkus로 REST API 구축하기: 고성능, 반응형 및 클라우드 네이티브 가이드
현대 엔터프라이즈 소프트웨어 공학에서 RESTful API 구축은 백엔드 시스템 개발의 핵심입니다. 수십 년 동안 Java 개발자들은 Spring Boot나 Jakarta EE와 같은 전통적인 프레임워크에 의존해 왔습니다.
이 문제를 해결하기 위해 등장한 것이 바로 Quarkus입니다. OpenJDK HotSpot 및 GraalVM에 맞춰 특별히 설계된 Supersonic Subatomic Java 프레임워크입니다.
1. Java REST API 구축에 Quarkus를 선택하는 이유 A. RESTEasy Reactive 패러다임 B. 설정이 필요 없는 Dev Services & 라이브 코딩 2. Quarkus REST API 아키텍처 3. 프로젝트 초기화 및 의존성 설정 mvn io.quarkus.platform:quarkus-maven-plugin:3.15.0:create \ -DprojectGroupId=com.ghaznix.api \ -DprojectArtifactId=quarkus-product-api \ -DclassName="com.ghaznix.api.ProductResource" \ -Dpath="/api/products" \ -Dextensions="resteasy-reactive-jackson,hibernate-orm-panache,jdbc-postgresql,hibernate-validator,smallrye-openapi" 4. 애플리케이션 속성 설정 # Application Configuration quarkus.application.name=quarkus-product-api quarkus.http.port=8080 # Database Configuration (Dev Services will override in dev mode automatically) quarkus.datasource.db-kind=postgresql quarkus.datasource.username=postgres quarkus.datasource.password=secret quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/product_db # Hibernate ORM Configuration quarkus.hibernate-orm.database.generation=drop-and-create quarkus.hibernate-orm.log.sql=true # Swagger UI & OpenAPI Path quarkus.swagger-ui.always-include=true quarkus.swagger-ui.path=/swagger-ui 5. 도메인 모델 및 Panache Active Record package com.ghaznix.api; import io.quarkus.hibernate.orm.panache.PanacheEntity; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.util.List; @Entity @Table(name = "products") public class Product extends PanacheEntity { @NotBlank(message = "Product name cannot be blank") @Column(nullable = false) public String name; @Column(length = 1000) public String description; @NotNull(message = "Price is required") @Min(value = 0, message = "Price must be non-negative") @Column(nullable = false) public BigDecimal price; @Column(nullable = false) public Integer stockQuantity; public String category; // Static Finder Utility Methods powered by Panache public static List<Product> findByCategory(String category) { return list("category", category); } public static List<Product> findAvailableProducts() { return list("stockQuantity > 0"); } } 6. 프로덕션급 REST 엔드포인트 구축 package com.ghaznix.api; import io.quarkus.panache.common.Page; import io.quarkus.panache.common.Sort; import jakarta.transaction.Transactional; import jakarta.validation.Valid; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.tags.Tag; import java.net.URI; import java.util.List; @Path("/api/products") @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) @Tag(name = "Product REST API", description = "Operations for managing catalog products") public class ProductResource { @GET @Operation(summary = "Get all products with pagination") @APIResponse(responseCode = "200", description = "List of products retrieved successfully") public List<Product> getAllProducts( @QueryParam("page") @DefaultValue("0") int pageIndex, @QueryParam("size") @DefaultValue("10") int pageSize, @QueryParam("sort") @DefaultValue("name") String sortBy) { return Product.findAll(Sort.by(sortBy)) .page(Page.of(pageIndex, pageSize)) .list(); } @GET @Path("/{id}") @Operation(summary = "Get product by ID") @APIResponse(responseCode = "200", description = "Product details found") @APIResponse(responseCode = "404", description = "Product not found") public Product getProductById(@PathParam("id") Long id) { Product product = Product.findById(id); if (product == null) { throw new NotFoundException("Product with ID " + id + " does not exist."); } return product; } @POST @Transactional @Operation(summary = "Create a new product") @APIResponse(responseCode = "201", description = "Product created successfully") @APIResponse(responseCode = "400", description = "Invalid request payload") public Response createProduct(@Valid Product product) { product.persist(); return Response.created(URI.create("/api/products/" + product.id)) .entity(product) .build(); } @PUT @Path("/{id}") @Transactional @Operation(summary = "Update an existing product") @APIResponse(responseCode = "200", description = "Product updated") @APIResponse(responseCode = "404", description = "Product ID not found") public Product updateProduct(@PathParam("id") Long id, @Valid Product updatedProduct) { Product entity = Product.findById(id); if (entity == null) { throw new NotFoundException("Product with ID " + id + " not found."); } entity.name = updatedProduct.name; entity.description = updatedProduct.description; entity.price = updatedProduct.price; entity.stockQuantity = updatedProduct.stockQuantity; entity.category = updatedProduct.category; return entity; } @DELETE @Path("/{id}") @Transactional @Operation(summary = "Delete a product by ID") @APIResponse(responseCode = "204", description = "Product deleted successfully") @APIResponse(responseCode = "404", description = "Product ID not found") public Response deleteProduct(@PathParam("id") Long id) { boolean deleted = Product.deleteById(id); if (!deleted) { throw new NotFoundException("Product with ID " + id + " not found."); } return Response.noContent().build(); } } 7. 반응형 REST 엔드포인트 vs 블로킹 실행 import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Multi; @GET @Path("/reactive/{id}") public Uni<Product> getProductReactive(@PathParam("id") Long id) { return Product.<Product>findByIdAsync(id) .onItem().ifNull().failWith(new NotFoundException("Product not found")); } @GET @Path("/stream") @Produces(MediaType.SERVER_SENT_EVENTS) public Multi<Product> streamProducts() { return Product.streamAll(); } 8. 전역 예외 처리 및 에러 표준화 package com.ghaznix.api.exception; import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; import java.time.Instant; import java.util.HashMap; import java.util.Map; @Provider public class GlobalExceptionMapper implements ExceptionMapper<Exception> { @Override public Response toResponse(Exception exception) { Map<String, Object> errorPayload = new HashMap<>(); errorPayload.put("timestamp", Instant.now().toString()); if (exception instanceof NotFoundException) { errorPayload.put("status", 404); errorPayload.put("error", "Not Found"); errorPayload.put("message", exception.getMessage()); return Response.status(Response.Status.NOT_FOUND) .entity(errorPayload) .type(MediaType.APPLICATION_JSON) .build(); } errorPayload.put("status", 500); errorPayload.put("error", "Internal Server Error"); errorPayload.put("message", exception.getMessage()); return Response.status(Response.Status.INTERNAL_SERVER_ERROR) .entity(errorPayload) .type(MediaType.APPLICATION_JSON) .build(); } } 9. @QuarkusTest 및 REST Assured를 활용한 자동화 테스트 package com.ghaznix.api; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.*; @QuarkusTest public class ProductResourceTest { @Test public void testGetAllProductsEndpoint() { given() .when().get("/api/products") .then() .statusCode(200) .contentType(ContentType.JSON); } @Test public void testCreateAndGetProduct() { Product product = new Product(); product.name = "Gaming Laptop"; product.description = "High-performance GPU laptop"; product.price = new java.math.BigDecimal("1499.99"); product.stockQuantity = 15; product.category = "Electronics"; // Test POST Endpoint Integer createdId = given() .contentType(ContentType.JSON) .body(product) .when().post("/api/products") .then() .statusCode(201) .body("name", equalTo("Gaming Laptop")) .extract().path("id"); // Test GET Endpoint by ID given() .pathParam("id", createdId) .when().get("/api/products/{id}") .then() .statusCode(200) .body("category", equalTo("Electronics")); } } ./mvnw test 10. 패키징 및 GraalVM 네이티브 실행 파일 컴파일 ./mvnw quarkus:dev ./mvnw package java -jar target/quarkus-app/quarkus-run.jar ./mvnw package -Dnative -Dquarkus.native.container-build=true ./target/quarkus-product-api-1.0.0-runner 2026-09-21 22:30:00,014 INFO [io.quarkus] (main) quarkus-product-api 1.0.0-runner native (powered by Quarkus 3.15.0) started in 0.018s. Listening on: http://0.0.0.0:8080 11. 성능 벤치마크: Quarkus vs Spring Boot 벤치마크 지표 Spring Boot (JVM) Quarkus (OpenJDK JVM) Quarkus (GraalVM Native) 성능 향상 부팅 시간 (콜드 스타트) 3.82 초 1.18 초 0.015 초 254배 빨라짐 메모리 점유율 (RSS) 184 MB 108 MB 28 MB 84% 메모리 절감 12. 결론 및 베스트 프랙티스 Ghaznix Blog.
Java
Quarkus
REST API
마이크로서비스
GraalVM
반응형 프로그래밍
Hibernate Panache
클라우드 네이티브
소프트웨어 아키텍처