tinystruct 개발 패턴
tinystruct Java 프레임워크로 모듈을 구축하기 위한 아키텍처 및 구현 패턴 – CLI와 HTTP를 동등하게 취급하며 main() 메서드가 필요 없고 최소한의 설정만으로 동작하는 경량 고성능 프레임워크입니다.
핵심 원칙
CLI와 HTTP는 동등한 시민입니다. @Action으로 어노테이션된 모든 메서드는 이상적으로 수정 없이 터미널과 웹 브라우저 모두에서 실행 가능해야 합니다. 이 '듀얼 모드' 기능은 tinystruct의 핵심 설계 철학입니다.
활성화 시점
사용 시기
AbstractApplication을 확장하여 새Application모듈을 생성할 때@Action을 사용하여 라우트와 명령줄 액션을 정의할 때Context를 통해 요청별 상태를 처리할 때- 네이티브
Builder및Builders컴포넌트를 사용하여 JSON 직렬화를 수행할 때 AbstractDataPOJO를 통해 데이터베이스 지속성 작업을 할 때generate명령을 사용하여 데이터베이스 테이블에서 POJO를 생성할 때- 실시간 푸시를 위해 Server-Sent Events (SSE)를 구현할 때
- 멀티파트 데이터를 통해 파일 업로드를 처리할 때
URLRequest및HTTPHandler로 아웃바운드 HTTP 요청을 만들 때application.properties에서 데이터베이스 연결 또는 시스템 설정을 구성할 때- 라우팅 충돌(Actions) 또는 CLI 인수 파싱을 디버깅할 때
작동 방식
tinystruct 프레임워크는 @Action으로 어노테이션된 모든 메서드를 터미널과 웹 환경 모두에서 라우팅 가능한 엔드포인트로 취급합니다. 애플리케이션은 init()과 같은 핵심 라이프사이클 훅과 요청 Context에 대한 액세스를 제공하는 AbstractApplication을 확장하여 생성됩니다.
라우팅은 ActionRegistry에 의해 처리되며, 자동으로 경로 세그먼트를 메서드 인수에 매핑하고 의존성을 주입합니다. 데이터 전용 서비스의 경우, 제로 의존성 풋프린트를 유지하기 위해 네이티브 Builder 및 Builders 컴포넌트를 JSON 직렬화에 사용해야 합니다. 데이터베이스 계층은 외부 ORM 라이브러리 없이 CRUD 작업을 위해 XML 매핑 파일과 쌍을 이루는 AbstractData POJO를 사용합니다.
예제
기본 애플리케이션 (MyService)
public class MyService extends AbstractApplication {
@Override
public void init() {
this.setTemplateRequired(false); // 데이터/API 앱의 경우 .view 조회 비활성화
}
@Override public String version() { return "1.0.0"; }
@Action("greet")
public String greet() {
return "Hello from tinystruct!";
}
// 경로 파라미터: GET /?q=greet/James 또는 bin/dispatcher greet/James
@Action("greet")
public String greet(String name) {
return "Hello, " + name + "!";
}
}
HTTP 모드 구분 (login)
@Action(value = "login", mode = Mode.HTTP_POST)
public String doLogin(Request<?, ?> request) throws ApplicationException {
request.getSession().setAttribute("userId", "42");
return "Logged in";
}
네이티브 JSON 데이터 처리 (Builder + Builders)
import org.tinystruct.data.component.Builder;
import org.tinystruct.data.component.Builders;
@Action("api/data")
public String getData() throws ApplicationException {
Builders dataList = new Builders();
Builder item = new Builder();
item.put("id", 1);
item.put("name", "James");
dataList.add(item);
Builder response = new Builder();
response.put("status", "success");
response.put("data", dataList);
return response.toString(); // {"status":"success","data":[{"id":1,"name":"James"}]}
}
SSE (Server-Sent Events)
import org.tinystruct.http.SSEPushManager;
@Action("sse/connect")
public String connect() {
return "{\"type\":\"connect\",\"message\":\"Connected to SSE\"}";
}
// 특정 클라이언트로 푸시
String sessionId = getContext().getId();
Builder msg = new Builder();
msg.put("text", "Hello, user!");
SSEPushManager.getInstance().push(sessionId, msg);
// 모든 클라이언트에게 브로드캐스트
SSEPushManager.getInstance().broadcast(msg);
파일 업로드
import org.tinystruct.data.FileEntity;
@Action(value = "upload", mode = Mode.HTTP_POST)
public String upload(Request<?, ?> request) throws ApplicationException {
List<FileEntity> files = request.getAttachments();
if (files != null) {
for (FileEntity file : files) {
System.out.println("Uploaded: " + file.getFilename());
}
}
return "Upload OK";
}
MCP 서버 및 도구 통합
tinystruct는 SDK 버전 1.7.26 부터 MCP(Model Context Protocol)에 대한 네이티브 지원을 제공합니다.
MCP API (예: org.tinystruct.mcp.MCPTool, org.tinystruct.mcp.MCPServer, org.tinystruct.mcp.MCPException)는 코어 의존성에 직접 포함되어 있습니다.
<dependency>
<groupId>org.tinystruct</groupId>
<artifactId>tinystruct</artifactId>
<version>1.7.26</version>
</dependency>
보안 경고 (프롬프트 인젝션): 도구 반환 값은 AI 모델의 컨텍스트 윈도우에 직접 다시 공급됩니다. 도구의 반환 문자열에 호출자가 제공한 모든 인수를 포함시키기 전에 반드시 검증하고 삭제해야 합니다. 입력을 삭제하지 않으면 공격자가 모델의 동작을 재정의하는 적대적 명령(프롬프트 인젝션)을 주입할 수 있습니다. 항상 길이, 문자 집합 및 null 여부를 검증하십시오.
MCP 도구를 생성하려면:
org.tinystruct.mcp.MCPTool을 확장합니다.@Action으로 작업을 어노테이션하고arguments배열 내에서@Argument를 사용하여 매개변수를 선언합니다.@Argument의 키와 일치하는 명시적 메서드 인수로 매개변수를 받습니다. 도구 인수에getContext().getAttribute(...)를 사용하지 마십시오.
import org.tinystruct.mcp.MCPTool;
import org.tinystruct.mcp.MCPException;
import org.tinystruct.system.annotation.Action;
import org.tinystruct.system.annotation.Argument;
public class MyCustomTool extends MCPTool {
public MyCustomTool() {
super("custom", "A custom tool for demonstrating MCP");
}
@Action(
value = "custom/hello",
description = "Say hello to someone",
arguments = {
@Argument(key = "name", description = "The name to greet", type = "string", optional = false)
}
)
public String hello(String name) throws MCPException {
// 보안: 프롬프트 인젝션 취약점을 방지하기 위해
// 모델에 반환하기 전에 도구 입력을 검증/삭제합니다.
if (name == null || name.length() > 50 || !name.matches("^[a-zA-Z0-9 ]+$")) {
throw new MCPException("Invalid name provided");
}
return "Hello, " + name + "!";
}
}
MCP 서버를 배포하려면:
org.tinystruct.mcp.MCPServer를 확장합니다.init()을 재정의하고this.registerTool()을 사용하여 도구를 등록합니다. 프레임워크가 자동으로@Action메서드를 스캔하고 매핑합니다.
import org.tinystruct.mcp.MCPServer;
public class MyMCPServer extends MCPServer {
@Override
public void init() {
super.init();
this.registerTool(new MyCustomTool());
}
@Override
public String version() {
return "1.0.0";
}
}
디스패처를 통해 서버를 실행합니다.
bin/dispatcher start --import org.tinystruct.system.HttpServer --import com.example.MyMCPServer
설정
설정은 src/main/resources/application.properties에서 관리됩니다.
# Database
driver=org.h2.Driver
database.url=jdbc:h2:~/mydb
database.user=sa
database.password=
# Server
default.home.page=hello
server.port=8080
# Locale
default.language=en_US
# Session (Redis for clustered environments)
# default.session.repository=org.tinystruct.http.RedisSessionRepository
# redis.host=127.0.0.1
# redis.port=6379
애플리케이션에서 설정 값에 액세스합니다.
String port = this.getConfiguration("server.port");
레드 플래그 및 안티패턴
| 증상 | 올바른 패턴 |
|---|---|
com.google.gson 또는 com.fasterxml.jackson 임포트 | org.tinystruct.data.component.Builder / Builders 사용 |
JSON 배열에 List<Builder> 사용 | Builders를 사용하여 제네릭 타입 소거 문제 방지 |
ApplicationRuntimeException: template not found | API 전용 앱의 경우 init()에서 setTemplateRequired(false) 호출 |
private 메서드에 @Action 어노테이션 | 프레임워크가 등록하려면 액션은 public이어야 함 |
앱에서 main(String[] args) 하드코딩 | 모든 모듈의 진입점으로 bin/dispatcher 사용 |
수동 ActionRegistry 등록 | 자동 발견을 위해 @Action 어노테이션 선호 |
| 런타임에 액션을 찾을 수 없음 | --import를 통해 클래스를 임포트했는지 또는 application.properties에 나열되었는지 확인 |
| CLI 인수가 보이지 않음 | --key value로 전달하고 getContext().getAttribute("--key")로 액세스 |
| 동일한 경로에 두 메서드, 잘못된 메서드가 실행됨 | 명확성을 위해 명시적 mode(예: HTTP_GET vs HTTP_POST) 설정 |
모범 사례
- 세분화된 애플리케이션: 로직을 하나의 모놀리식 클래스 대신 더 작고 집중된 애플리케이션으로 나눕니다.
init()에서 설정: 생성자보다는init()을 설정(구성, DB)에 활용합니다.setAction()을 호출하지 말고@Action어노테이션을 사용하십시오.- 모드 인식:
@Action의Mode매개변수를 사용하여 중요한 작업을CLI전용 또는 특정 HTTP 메서드로 제한합니다. - 매개변수보다 Context: 선택적 CLI 플래그의 경우 메서드 시그니처에 매개변수를 추가하는 대신
getContext().getAttribute("--flag")를 사용합니다. - 비동기 이벤트: 이벤트에 의해 트리거되는 무거운 작업의 경우 이벤트 핸들러 내에서
CompletableFuture.runAsync()를 사용합니다.
기술 참조
자세한 가이드는 references/ 디렉토리에서 확인할 수 있습니다.
- 아키텍처 및 설정 — 추상화, 패키지 맵, 속성
- 라우팅 및 @Action — 어노테이션 세부 사항, 모드, 매개변수
- 데이터 처리 — Builder, Builders, JSON 직렬화 및 파싱
- 데이터베이스 지속성 — AbstractData POJO, CRUD, 매핑 XML, POJO 생성
- 시스템 및 사용법 — Context, 세션, SSE, 파일 업로드, 이벤트, 네트워킹
- 테스트 패턴 — JUnit 5 단위 및 HTTP 통합 테스트
참조 소스 파일 (내부)
src/main/java/org/tinystruct/AbstractApplication.java— 라이프사이클 훅이 있는 코어 기본 클래스src/main/java/org/tinystruct/system/annotation/Action.java— 어노테이션 및 모드src/main/java/org/tinystruct/application/ActionRegistry.java— 라우팅 엔진src/main/java/org/tinystruct/data/component/Builder.java— JSON 객체 직렬화기src/main/java/org/tinystruct/data/component/Builders.java— JSON 배열 직렬화기src/main/java/org/tinystruct/data/component/AbstractData.java— CRUD가 있는 기본 POJO 클래스src/main/java/org/tinystruct/data/Mapping.java— 매핑 XML 파서src/main/java/org/tinystruct/data/tools/MySQLGenerator.java— POJO 생성기 참조src/main/java/org/tinystruct/data/component/FieldType.java— SQL-Java 타입 매핑src/main/java/org/tinystruct/data/component/Condition.java— 유창한 SQL 쿼리 빌더src/main/java/org/tinystruct/http/SSEPushManager.java— SSE 연결 관리src/test/java/org/tinystruct/application/ActionRegistryTest.java— 레지스트리 테스트 예제src/test/java/org/tinystruct/system/HttpServerHttpModeTest.java— HTTP 통합 테스트 패턴