tinystruct 개발 패턴
tinystruct Java 프레임워크로 모듈을 구축하기 위한 아키텍처 및 구현 패턴입니다. tinystruct는 CLI와 HTTP를 동등하게 취급하며, main() 메서드가 필요 없고 최소한의 설정만으로 동작하는 경량 고성능 프레임워크입니다.
핵심 원칙
CLI와 HTTP는 동등한 시민입니다. @Action으로 어노테이션된 모든 메서드는 수정 없이 터미널과 웹 브라우저 모두에서 실행 가능해야 합니다. 이러한 "듀얼 모드" 기능이 tinystruct의 핵심 설계 철학입니다.
활성화 시점
사용해야 할 때
AbstractApplication을 확장하여 새로운Application모듈을 생성할 때.@Action을 사용하여 라우트와 커맨드라인 액션을 정의할 때.Context를 통해 요청별 상태를 처리할 때.- 네이티브
Builder및Builders컴포넌트를 사용하여 JSON 직렬화를 수행할 때. AbstractDataPOJO를 통해 데이터베이스 영속성을 작업할 때.generate명령을 사용하여 데이터베이스 테이블에서 POJO를 생성할 때.- 실시간 푸시를 위해 서버-전송 이벤트(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 (서버-전송 이벤트)
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**부터 Model Context Protocol (MCP)을 기본 지원합니다.
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) 호출 |
@Action으로 private 메서드 어노테이션 | 액션은 프레임워크에 등록되려면 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-to-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 통합 테스트 패턴