AsyncJettyServer
package server;
import dto.Dtos;
import dto.HttpException;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import service.CategoryService;
import service.RiskAnalysisService;
import util.HttpUtil;
import util.JsonUtil;
import util.ThreadUtil;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
public class AsyncJettyServer {
public static void main(String[] args) throws Exception {
int port = Integer.parseInt(System.getProperty("PORT", "8080"));
int cores = Runtime.getRuntime().availableProcessors();
int minThreads = cores; // 최소 스레드는 코어 개수
int maxThreads = cores * 4; // CPU 바운드라면 ×2, I/O 바운드라면 ×4~8
QueuedThreadPool pool = new QueuedThreadPool(maxThreads, minThreads);
Server server = new Server(pool);
ServerConnector http = new ServerConnector(server);
http.setPort(port);
http.setIdleTimeout(30_000);
server.addConnector(http);
ServletContextHandler ctx = new ServletContextHandler(ServletContextHandler.SESSIONS);
ctx.setContextPath("/");
ctx.addServlet(new ServletHolder(new ApiServlet()), "/*");
server.setHandler(ctx);
server.setStopAtShutdown(true);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
server.stop();
ThreadUtil.shutdown(); // ThreadUtil 정리 추가
} catch (Exception ignore) {}
}));
server.start();
server.join();
}
public static class ApiServlet extends HttpServlet {
private interface Handler { void handle(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException; }
private final Map<String, Handler> getRoutes = new LinkedHashMap<String, Handler>();
private final Map<String, Handler> postRoutes = new LinkedHashMap<String, Handler>();
// 서비스들을 비동기로 초기화
private final AtomicReference<CategoryService> categoryService = new AtomicReference<>();
private final AtomicReference<RiskAnalysisService> riskService = new AtomicReference<>();
private volatile boolean servicesInitialized = false;
public ApiServlet() {
initializeServicesAsync();
setupRoutes();
}
/**
* 서비스들을 비동기로 초기화 (ThreadUtil 사용)
*/
private void initializeServicesAsync() {
ThreadUtil.runAsync(() -> {
try {
Path path = Paths.get("CATEGORIES.TXT").toAbsolutePath().normalize();
Path path2 = Paths.get("RISK_PATTERNS.TXT").toAbsolutePath().normalize();
// CategoryService와 RiskAnalysisService를 병렬로 초기화
var categoryFuture = ThreadUtil.supplyAsync(() -> {
try {
return new CategoryService(path.toString(), path2.toString());
} catch (Exception e) {
throw new RuntimeException("CategoryService 초기화 실패", e);
}
});
var riskFuture = ThreadUtil.supplyAsync(() -> {
try {
return new RiskAnalysisService();
} catch (Exception e) {
throw new RuntimeException("RiskAnalysisService 초기화 실패", e);
}
});
// 두 서비스 초기화 완료 대기
ThreadUtil.awaitAll(categoryFuture, riskFuture);
categoryService.set(categoryFuture.join());
riskService.set(riskFuture.join());
servicesInitialized = true;
System.out.println("모든 서비스가 성공적으로 초기화되었습니다.");
} catch (Exception e) {
System.err.println("서비스 초기화 실패: " + e.getMessage());
e.printStackTrace();
}
});
}
/**
* 서비스가 초기화될 때까지 대기
*/
private void ensureServicesInitialized() throws HttpException {
if (!servicesInitialized) {
// 최대 5초 대기
long startTime = System.currentTimeMillis();
while (!servicesInitialized && System.currentTimeMillis() - startTime < 5000) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw HttpException.internalServerError("서비스 초기화 대기 중 중단됨");
}
}
if (!servicesInitialized) {
throw HttpException.serviceUnavailable("서비스가 아직 초기화되지 않았습니다");
}
}
}
private void setupRoutes() {
// GET 라우트 등록
//getRoutes.put("/analyze", this::getAnalyze);
// POST 라우트 등록
//postRoutes.put("/echo", this::postEcho);
//postRoutes.put("/performance", this::postPerformance);
postRoutes.put("/register-transaction", this::registerTransaction);
postRoutes.put("/analyze-session", this::analyzeSession);
}
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
dispatch(getRoutes, req, resp);
}
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
dispatch(postRoutes, req, resp);
}
private void dispatch(Map<String, Handler> routes, HttpServletRequest req, HttpServletResponse resp) throws IOException {
String path = req.getPathInfo();
if (path == null || path.isEmpty()) path = "/";
Handler h = routes.get(path);
if (h == null) {
// 404 에러 직접 처리
HttpException notFound = HttpException.notFound("경로를 찾을 수 없습니다: " + path);
jsonResponse(resp, notFound.getStatusCode(), notFound.getResponseBody());
return;
}
try {
h.handle(req, resp);
} catch (HttpException e) {
// HttpException 처리 - 실제 HTTP 응답 상태 코드 설정
HttpUtil.sendHttpExceptionResponse(resp, e);
} catch (Exception e) {
e.printStackTrace();
HttpException serverError = HttpException.internalServerError("서버 내부 오류가 발생했습니다");
jsonResponse(resp, serverError.getStatusCode(), serverError.getResponseBody());
}
}
private void getAnalyze(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
//GET <http://127.0.0.1:8080/analyze?text=><분석할 텍스트>&level=<마스킹 레벨>
//level은 basic과 strict가 있어요
String text = req.getParameter("text");
String levelStr = req.getParameter("level");
if (text == null || text.isEmpty()) {
json(resp, 400, "{\\"error\\":\\"missing text\\"}");
return;
}
String risk_level = levelStr; // 위험도 결과
json(resp, 200, "{\\"risk_level\\":" + risk_level + ",\\"result\\":\\"" + "\\"}");
}
private void postEcho(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
String body = readAll(req);
json(resp, 201, body.isEmpty() ? "{}" : body);
}
private void postPerformance(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
try {
String body = readAll(req);
json(resp, 201, body.isEmpty() ? "{}" : body);
}catch (Exception e){
json(resp,400,"{}");
}
}
// curl -X POST <http://127.0.0.1:8080/register-transaction> \\
// -H "Content-Type: application/json" \\
// -d '{
// "session_id": "SESS_001",
// "transaction": "CARD_PAYMENT 150000 RESTAURANT 20240830"
// }'
private void registerTransaction(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
ensureServicesInitialized(); // 서비스 준비 확인
String body = readAll(req);
Dtos.Request request = JsonUtil.fromJson(body, Dtos.Request.class);
// CategoryService의 비동기 메서드 사용 후 동기적으로 대기
try {
categoryService.get().saveFilteredCategoryAsync(request.sessionId, request.transaction).join();
} catch (Exception e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
if (e.getCause() instanceof HttpException) {
throw (HttpException) e.getCause();
}
throw new IOException("거래 등록 실패", e);
}
Dtos.Response response = new Dtos.Response();
response.status = "registered";
json(resp, 200, JsonUtil.toJson(response));
}
private void analyzeSession(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
String body = readAll(req);
Dtos.AnalyzeSessionRequest request = JsonUtil.fromJson(body, Dtos.AnalyzeSessionRequest.class);
// 1초 내에 처리해야 함
long startTime = System.currentTimeMillis();
try {
// 세션 파일에서 거래 데이터 비동기 읽기 (ThreadUtil로 동기 메서드를 비동기화)
var transactionsFuture = ThreadUtil.supplyAsync(() -> {
try {
return categoryService.get().readSessionTransactions(request.sessionId);
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
});
List<String> transactions;
try {
transactions = transactionsFuture.join();
} catch (Exception e) {
if (e.getCause() instanceof HttpException) {
throw (HttpException) e.getCause();
}
throw new IOException("세션 데이터 읽기 실패", e);
}
if (transactions.isEmpty()) {
Dtos.AnalyzeSessionResponse response = new Dtos.AnalyzeSessionResponse();
response.results = new String[0];
jsonResponse(resp, 200, response);
return;
}
// 외부 위험도 분석 서비스들을 병렬로 호출 (ThreadUtil로 동기 메서드를 비동기화)
var riskAnalysisFuture = ThreadUtil.supplyAsync(() -> {
try {
return riskService.get().analyzeTransactionsParallel(transactions);
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
});
List<String> riskLevels;
try {
riskLevels = riskAnalysisFuture.join();
} catch (Exception e) {
if (e.getCause() instanceof HttpException) {
throw (HttpException) e.getCause();
}
throw new IOException("위험도 분석 실패", e);
}
// 처리 시간 체크
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > 1000) {
throw HttpException.requestTimeout("분석 처리 시간이 1초를 초과했습니다");
}
// 응답
Dtos.AnalyzeSessionResponse response = new Dtos.AnalyzeSessionResponse();
response.results = riskLevels.toArray(new String[0]);
jsonResponse(resp, 200, response);
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > 1000) {
throw HttpException.requestTimeout("분석 처리 시간이 1초를 초과했습니다");
}
e.printStackTrace(); // 콘솔에 에러 출력
throw e; // 에러를 다시 던져서 dispatch에서 처리하도록
}
}
private static String readAll(HttpServletRequest req) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = req.getReader();
try {
String line;
while ((line = br.readLine()) != null) sb.append(line);
} finally {
try { br.close(); } catch (IOException ignore) {}
}
return sb.toString();
}
private static void json(HttpServletResponse resp, int status, String json) throws IOException {
resp.setStatus(status);
resp.setContentType("application/json;charset=utf-8");
PrintWriter w = resp.getWriter();
w.write(json);
w.flush();
}
private void jsonResponse(HttpServletResponse resp, int status, Object data) throws IOException {
resp.setStatus(status);
resp.setContentType("application/json;charset=utf-8");
PrintWriter w = resp.getWriter();
w.write(JsonUtil.gson().toJson(data));
w.flush();
}
private static String opt(String v, String def) { return v == null ? def : v; }
private static String safe(String s) { return s == null ? "" : s.replace("\\\\", "\\\\\\\\").replace("\\"", "\\\\\\""); }
}
}
JettyServer
package server;
import dto.Dtos;
import dto.HttpException;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import service.CategoryService;
import service.RiskAnalysisService;
import util.HttpUtil;
import util.JsonUtil;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public class JettyServer {
public static void main(String[] args) throws Exception {
int port = Integer.parseInt(System.getProperty("PORT", "8080"));
int cores = Runtime.getRuntime().availableProcessors();
int minThreads = cores; // 최소 스레드는 코어 개수
int maxThreads = cores * 4; // CPU 바운드라면 ×2, I/O 바운드라면 ×4~8
QueuedThreadPool pool = new QueuedThreadPool(maxThreads, minThreads);
Server server = new Server(pool);
ServerConnector http = new ServerConnector(server);
http.setPort(port);
http.setIdleTimeout(30_000);
server.addConnector(http);
ServletContextHandler ctx = new ServletContextHandler(ServletContextHandler.SESSIONS);
ctx.setContextPath("/");
ctx.addServlet(new ServletHolder(new ApiServlet()), "/*");
server.setHandler(ctx);
server.setStopAtShutdown(true);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try { server.stop(); } catch (Exception ignore) {}
}));
server.start();
server.join();
}
public static class ApiServlet extends HttpServlet {
private interface Handler { void handle(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException; }
private final Map<String, Handler> getRoutes = new LinkedHashMap<String, Handler>();
private final Map<String, Handler> postRoutes = new LinkedHashMap<String, Handler>();
//서비스들...?
Path path = Paths.get("CATEGORIES.TXT").toAbsolutePath().normalize();
Path path2 = Paths.get("RISK_PATTERNS.TXT").toAbsolutePath().normalize();
CategoryService categoryService = new CategoryService(path.toString(), path2.toString());
public ApiServlet() {
setupRoutes();
}
private void setupRoutes() {
// GET 라우트 등록
//getRoutes.put("/analyze", this::getAnalyze);
// POST 라우트 등록
//postRoutes.put("/echo", this::postEcho);
//postRoutes.put("/performance", this::postPerformance);
postRoutes.put("/register-transaction", this::registerTransaction);
postRoutes.put("/analyze-session", this::analyzeSession);
}
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
dispatch(getRoutes, req, resp);
}
@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
dispatch(postRoutes, req, resp);
}
private void dispatch(Map<String, Handler> routes, HttpServletRequest req, HttpServletResponse resp) throws IOException {
String path = req.getPathInfo();
if (path == null || path.isEmpty()) path = "/";
Handler h = routes.get(path);
if (h == null) {
// json(resp, 404, "{\\"error\\":\\"not_found\\",\\"path\\":\\"" + safe(path) + "\\"}");
// return;
// Todo
// 404 에러 직접 처리
HttpException notFound = HttpException.notFound("경로를 찾을 수 없습니다: " + path);
jsonResponse(resp, notFound.getStatusCode(), notFound.getResponseBody());
return;
}
// try {
// h.handle(req, resp);
// } catch (Exception e) {
// json(resp, 500, "{\\"error\\":\\"server_error\\",\\"message\\":\\"" + safe(e.getMessage()) + "\\"}");
// }
// Todo
try {
h.handle(req, resp);
} catch (HttpException e) {
// HttpException 처리 - 실제 HTTP 응답 상태 코드 설정
HttpUtil.sendHttpExceptionResponse(resp, e);
} catch (Exception e) {
e.printStackTrace();
HttpException serverError = HttpException.internalServerError("서버 내부 오류가 발생했습니다");
jsonResponse(resp, serverError.getStatusCode(), serverError.getResponseBody());
}
}
private void getAnalyze(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
//GET <http://127.0.0.1:8080/analyze?text=><분석할 텍스트>&level=<마스킹 레벨>
//level은 basic과 strict가 있어요
String text = req.getParameter("text");
String levelStr = req.getParameter("level");
if (text == null || text.isEmpty()) {
json(resp, 400, "{\\"error\\":\\"missing text\\"}");
return;
}
String risk_level = levelStr; // 위험도 결과
json(resp, 200, "{\\"risk_level\\":" + risk_level + ",\\"result\\":\\"" + "\\"}");
}
private void postEcho(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
String body = readAll(req);
json(resp, 201, body.isEmpty() ? "{}" : body);
}
private void postPerformance(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
try {
String body = readAll(req);
json(resp, 201, body.isEmpty() ? "{}" : body);
}catch (Exception e){
json(resp,400,"{}");
}
}
// curl -X POST <http://127.0.0.1:8080/register-transaction> \\
// -H "Content-Type: application/json" \\
// -d '{
// "session_id": "SESS_001",
// "transaction": "CARD_PAYMENT 150000 RESTAURANT 20240830"
// }'
private void registerTransaction(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
String body = readAll(req);
Dtos.Request request = JsonUtil.fromJson(body, Dtos.Request.class);
categoryService.saveFilteredCategory(request.sessionId, request.transaction);
Dtos.Response response = JsonUtil.fromJson(body, Dtos.Response.class);
response.status = "registered";
json(resp, 200, JsonUtil.toJson(response));
}
private void analyzeSession(HttpServletRequest req, HttpServletResponse resp) throws IOException, HttpException {
String body = readAll(req);
Dtos.AnalyzeSessionRequest request = JsonUtil.fromJson(body, Dtos.AnalyzeSessionRequest.class);
// 1초 내에 처리해야 함
long startTime = System.currentTimeMillis();
try {
// 세션 파일에서 거래 데이터 읽기 (락킹 적용, 1초 타임아웃)
List<String> transactions = categoryService.readSessionTransactions(request.sessionId);
if (transactions.isEmpty()) {
Dtos.AnalyzeSessionResponse response = new Dtos.AnalyzeSessionResponse();
response.results = new String[0];
jsonResponse(resp, 200, response);
return;
}
// 외부 위험도 분석 서비스들을 병렬로 호출
RiskAnalysisService riskService = new RiskAnalysisService();
List<String> riskLevels = riskService.analyzeTransactionsParallel(transactions);
// 처리 시간 체크
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > 1000) {
throw HttpException.requestTimeout("분석 처리 시간이 1초를 초과했습니다");
}
// 응답
Dtos.AnalyzeSessionResponse response = new Dtos.AnalyzeSessionResponse();
response.results = riskLevels.toArray(new String[0]);
jsonResponse(resp, 200, response);
} catch (Exception e) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > 1000) {
throw HttpException.requestTimeout("분석 처리 시간이 1초를 초과했습니다");
}
e.printStackTrace(); // 콘솔에 에러 출력
}}
private static String readAll(HttpServletRequest req) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = req.getReader();
try {
String line;
while ((line = br.readLine()) != null) sb.append(line);
} finally {
try { br.close(); } catch (IOException ignore) {}
}
return sb.toString();
}
private static void json(HttpServletResponse resp, int status, String json) throws IOException {
resp.setStatus(status);
resp.setContentType("application/json;charset=utf-8");
PrintWriter w = resp.getWriter();
w.write(json);
w.flush();
}
private void jsonResponse(HttpServletResponse resp, int status, Object data) throws IOException {
resp.setStatus(status);
resp.setContentType("application/json;charset=utf-8");
PrintWriter w = resp.getWriter();
w.write(JsonUtil.gson().toJson(data));
w.flush();
}
private static String opt(String v, String def) { return v == null ? def : v; }
private static String safe(String s) { return s == null ? "" : s.replace("\\\\", "\\\\\\\\").replace("\\"", "\\\\\\""); }
}
}
Dtos
package dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import com.google.gson.annotations.SerializedName;
import lombok.NoArgsConstructor;
public class Dtos {
@AllArgsConstructor
public static class Request{
@SerializedName("session_id")
public String sessionId;
public String transaction;
}
@AllArgsConstructor
@NoArgsConstructor
public static class Response{
public String status;
}
@AllArgsConstructor
@NoArgsConstructor
public static class RegisterTransactionRequest {
@SerializedName("session_id")
public String sessionId;
public String transaction;
}
@NoArgsConstructor
public static class RegisterTransactionResponse {
public String status;
}
@AllArgsConstructor
@NoArgsConstructor
public static class AnalyzeSessionRequest {
@SerializedName("session_id")
public String sessionId;
}
@NoArgsConstructor
public static class AnalyzeSessionResponse {
public String[] results;
}
// 외부 위험도 분석 서비스 요청/응답
@AllArgsConstructor
@NoArgsConstructor
public static class RiskAnalysisRequest {
public String query;
}
@NoArgsConstructor
public static class RiskAnalysisResponse {
@SerializedName("risk_code")
public String riskCode;
}
// 서비스 목록 관련
@NoArgsConstructor
public static class ServicesConfig {
public Service[] services;
}
@NoArgsConstructor
public static class Service {
@SerializedName("service_name")
public String serviceName;
public String url;
@SerializedName("risk_mappings")
public RiskMapping[] riskMappings;
}
@NoArgsConstructor
public static class RiskMapping {
public String code;
public String level;
}
}
HttpException
package dto;
public class HttpException extends Exception {
private final int statusCode;
private final Object responseBody;
public HttpException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
this.responseBody = new SimpleErrorResponse(message);
}
public HttpException(int statusCode, Object responseBody) {
super(responseBody.toString());
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public HttpException(int statusCode, String message, Object responseBody) {
super(message);
this.statusCode = statusCode;
this.responseBody = responseBody;
}
public int getStatusCode() {
return statusCode;
}
public Object getResponseBody() {
return responseBody;
}
// 간단한 에러 응답 클래스
public static class SimpleErrorResponse {
public boolean success = false;
public String error;
public long timestamp;
public SimpleErrorResponse(String error) {
this.error = error;
this.timestamp = System.currentTimeMillis();
}
}
// 편의 메서드들 - 자주 사용하는 HTTP 상태 코드들
public static HttpException badRequest(String message) {
return new HttpException(400, message);
}
public static HttpException unauthorized(String message) {
return new HttpException(401, message);
}
public static HttpException forbidden(String message) {
return new HttpException(403, message);
}
public static HttpException notFound(String message) {
return new HttpException(404, message);
}
public static HttpException methodNotAllowed(String message) {
return new HttpException(405, message);
}
public static HttpException requestTimeout(String message) {
return new HttpException(408, message);
}
public static HttpException conflict(String message) {
return new HttpException(409, message);
}
public static HttpException tooManyRequests(String message) {
return new HttpException(429, message);
}
public static HttpException internalServerError(String message) {
return new HttpException(500, message);
}
public static HttpException notImplemented(String message) {
return new HttpException(501, message);
}
public static HttpException badGateway(String message) {
return new HttpException(502, message);
}
public static HttpException serviceUnavailable(String message) {
return new HttpException(503, message);
}
public static HttpException gatewayTimeout(String message) {
return new HttpException(504, message);
}
}
FileLoader
package util;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class FileLoader {
/**
* 파일을 한 줄씩 읽어서 List<String>으로 반환 (빈 줄 포함)
*/
public static List<String> readFileLines(String filePath) {
try {
return Files.readAllLines(Paths.get(filePath), StandardCharsets.UTF_8);
} catch (IOException e) {
System.err.println("파일 읽기 실패: " + filePath + " - " + e.getMessage());
return Collections.emptyList();
}
}
/**
* 파일을 한 줄씩 읽되, 빈 줄과 공백을 제거한 List<String> 반환
*/
public static List<String> readFileLinesClean(String filePath) {
return readFileLines(filePath).stream()
.map(String::trim)
.filter(line -> !line.isEmpty())
.collect(Collectors.toList());
}
/**
* 파일을 한 줄씩 읽되, 주석(#으로 시작)과 빈 줄 제거
*/
public static List<String> readFileLinesNoComments(String filePath) {
return readFileLines(filePath).stream()
.map(String::trim)
.filter(line -> !line.isEmpty() && !line.startsWith("#"))
.collect(Collectors.toList());
}
/**
* 파일을 읽어서 구분자로 분할된 Key-Value Map 반환
*/
public static Map<String, String> parseKeyValueFile(String filePath, String delimiter) {
Map<String, String> result = new HashMap<>();
List<String> lines = readFileLinesNoComments(filePath);
for (String line : lines) {
if (line.contains(delimiter)) {
String[] parts = line.split(delimiter, 2);
if (parts.length == 2) {
result.put(parts[0].trim(), parts[1].trim());
}
}
}
return result;
}
/**
* 파일을 읽어서 특정 타입의 Key-Value Map 반환 (제네릭)
*/
public static <V> Map<String, V> parseTypedKeyValueFile(String filePath, String delimiter,
Function<String, V> valueParser) {
Map<String, V> result = new HashMap<>();
List<String> lines = readFileLinesNoComments(filePath);
for (String line : lines) {
if (line.contains(delimiter)) {
String[] parts = line.split(delimiter, 2);
if (parts.length == 2) {
try {
String key = parts[0].trim();
V value = valueParser.apply(parts[1].trim());
if (value != null) {
result.put(key, value);
}
} catch (Exception e) {
System.err.println("파싱 실패한 줄 무시: " + line + " (" + e.getMessage() + ")");
}
}
}
}
return result;
}
/**
* JSON 파일을 제너릭하게 로드하는 메서드
*/
public static <T> T parseJsonFile(String filePath, Class<T> clazz) {
try {
String json = readFileToString(filePath);
return JsonUtil.gson().fromJson(json, clazz);
} catch (IOException e) {
System.err.println("JSON 파일 읽기 실패: " + filePath + " - " + e.getMessage());
return null;
}
}
/**
* JSON 문자열을 제너릭하게 파싱하는 메서드
*/
public static <T> T parseJson(String json, Class<T> clazz) {
try {
return JsonUtil.gson().fromJson(json, clazz);
} catch (Exception e) {
System.err.println("JSON 파싱 실패: " + e.getMessage());
return null;
}
}
/**
* 이미지 파일을 로드하는 메서드
*/
public static BufferedImage loadImage(String filePath) {
try {
File imageFile = new File(filePath);
if (!imageFile.exists()) {
System.err.println("이미지 파일 미존재: " + filePath);
return null;
}
BufferedImage originalImage = ImageIO.read(imageFile);
if (originalImage == null) {
System.err.println("지원되지 않는 이미지 형식: " + filePath);
return null;
}
System.out.println("이미지 로드 성공: " + filePath +
" (" + originalImage.getWidth() + "x" + originalImage.getHeight() + ")");
return originalImage;
} catch (IOException e) {
System.err.println("이미지 로드 실패: " + filePath + " - " + e.getMessage());
return null;
}
}
/**
* JSON 파일을 JsonObject로 로드하는 메서드
*/
public static JsonObject loadJsonFile(String filePath) {
try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
String content = readFileToString(filePath);
JsonParser parser = new JsonParser();
return parser.parse(content).getAsJsonObject();
} catch (IOException e) {
System.err.println("JSON 파일 읽기 실패: " + e.getMessage());
}
return null;
}
/**
* JSON 파일을 특정 클래스 타입으로 로드하는 메서드
*/
public static <T> T loadFile(String filePath, Class<T> clazz) {
try {
String json = readFileToString(filePath);
return JsonUtil.gson().fromJson(json, clazz);
} catch (IOException e) {
System.err.println("파일 읽기 실패: " + e.getMessage());
return null;
}
}
/**
* 파일 내용을 문자열로 읽어오는 메서드
*/
public static String readFileToString(String filePath) throws IOException {
return new String(Files.readAllBytes(Paths.get(filePath)), StandardCharsets.UTF_8);
}
/**
* CSV 파일을 읽어서 2차원 리스트로 반환하는 메서드
*/
public static List<List<String>> readCsvFile(String filePath) {
return readCsvFile(filePath, ",");
}
/**
* 구분자를 지정하여 CSV 파일을 읽는 메서드
*/
public static List<List<String>> readCsvFile(String filePath, String delimiter) {
try {
List<String> lines = Files.readAllLines(Paths.get(filePath), StandardCharsets.UTF_8);
return lines.stream()
.map(line -> Arrays.asList(line.split(delimiter)))
.collect(Collectors.toList());
} catch (IOException e) {
System.err.println("CSV 파일 읽기 실패: " + e.getMessage());
return Collections.emptyList();
}
}
/**
* JSON 문자열을 특정 클래스 타입으로 변환하는 메서드
*/
public static <T> T fromJson(String json, Class<T> clazz) {
return JsonUtil.gson().fromJson(json, clazz);
}
/**
* 문장을 토큰(단어)으로 분할하는 메서드
*/
public static List<String> tokenize(String sentence) {
if (sentence == null || sentence.trim().isEmpty()) {
return Collections.emptyList();
}
return Arrays.stream(sentence.trim().split("\\\\s+"))
.filter(token -> !token.isEmpty())
.collect(Collectors.toList());
}
/**
* 파일 존재 여부를 확인하는 메서드
*/
public static boolean fileExists(String filePath) {
try {
return Files.exists(Paths.get(filePath));
} catch (Exception e) {
System.err.println("파일 존재 확인 중 오류: " + filePath + " - " + e.getMessage());
return false;
}
}
/**
* 파일의 확장자를 반환하는 메서드
*/
public static String getFileExtension(String filePath) {
if (filePath == null || filePath.isEmpty()) {
return "";
}
int lastDotIndex = filePath.lastIndexOf('.');
if (lastDotIndex == -1 || lastDotIndex == filePath.length() - 1) {
return "";
}
return filePath.substring(lastDotIndex + 1).toLowerCase();
}
/**
* 파일 크기를 반환하는 메서드 (바이트 단위)
*/
public static long getFileSize(String filePath) {
try {
return Files.size(Paths.get(filePath));
} catch (IOException e) {
System.err.println("파일 크기 확인 중 오류: " + filePath + " - " + e.getMessage());
return -1;
}
}
/**
* JSON 파일에서 객체를 읽어오는 제네릭 메서드 (보너스)
* @param <T> 읽어올 객체의 타입
* @param filePath 파일 경로
* @param fileName 파일명
* @param clazz 객체의 클래스
* @return 읽어온 객체
* @throws IOException 파일 읽기 실패 시
*/
public static <T> T loadFromJsonFile(String filePath, String fileName, Class<T> clazz) throws IOException {
if (!fileName.toLowerCase().endsWith(".json")) {
fileName += ".json";
}
Path fullPath = Paths.get(filePath, fileName);
String jsonContent = Files.readString(fullPath);
return JsonUtil.gson().fromJson(jsonContent, clazz);
}
}
FileSaver
package util;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
public class FileSaver {
// Gson 인스턴스 (재사용을 위해 static으로 선언)
//private static final Gson gson = new Gson();
/**
* 객체를 JSON 파일로 저장하는 제네릭 메서드
* @param <T> 저장할 객체의 타입
* @param object 저장할 객체
* @param filePath 저장할 경로
* @param fileName 파일명 (확장자 포함/미포함 모두 가능)
* @throws IOException 파일 쓰기 실패 시
*/
public static <T> void saveToJsonFile(T object, String filePath, String fileName) throws IOException {
// 파일명에 .json 확장자가 없으면 추가
if (!fileName.toLowerCase().endsWith(".json")) {
fileName += ".json";
}
// 전체 파일 경로 생성
Path fullPath = Paths.get(filePath, fileName);
// 디렉토리가 존재하지 않으면 생성
Files.createDirectories(fullPath.getParent());
// 객체를 JSON으로 변환하여 파일에 저장
try (FileWriter writer = new FileWriter(fullPath.toFile())) {
JsonUtil.gson().toJson(object, writer);
}
}
/**
텍스트파일로 스트링을 저장~
*/
public static void saveToTextFile(String filePath, String fileName, String content) throws IOException {
// 파일명에 .txt 확장자가 없으면 추가
if (!fileName.toLowerCase().endsWith(".txt")) {
fileName += ".txt";
}
// 전체 파일 경로 생성
Path fullPath = Paths.get(filePath, fileName);
// 디렉토리가 존재하지 않으면 생성
Files.createDirectories(fullPath.getParent());
// 문자열을 파일에 저장
try (FileWriter writer = new FileWriter(fullPath.toFile())) {
writer.write(content);
}
}
/**
텍스트파일로 스트링을 저장~ 자바 11 이상 버전
*/
public static void saveToTextFileForJava11Plus(String content, String filePath, String fileName) throws IOException {
if (!fileName.toLowerCase().endsWith(".txt")) {
fileName += ".txt";
}
Path fullPath = Paths.get(filePath, fileName);
Files.createDirectories(fullPath.getParent());
// 한 줄로 간단하게!
Files.writeString(fullPath, content);
}
/**
* 객체를 JSON 파일로 저장하는 간편 메서드 (현재 디렉토리에 저장)
* @param <T> 저장할 객체의 타입
* @param object 저장할 객체
* @param fileName 파일명
* @throws IOException 파일 쓰기 실패 시
*/
public static <T> void saveToJsonFile(T object, String fileName) throws IOException {
saveToJsonFile(object, ".", fileName);
}
/**
* 텍스트 파일에 문자열을 추가하는 메서드
* @param filePath 파일 경로
* @param fileName 파일명
* @param content 추가할 내용
* @throws IOException 파일 쓰기 실패 시
*/
public static void appendToTextFile(String filePath, String fileName, String content) throws IOException {
// 파일명에 .txt 확장자가 없으면 추가
if (!fileName.toLowerCase().endsWith(".txt")) {
fileName += ".txt";
}
// 전체 파일 경로 생성
Path fullPath = Paths.get(filePath, fileName);
// 디렉토리가 존재하지 않으면 생성
Files.createDirectories(fullPath.getParent());
// 파일에 내용 추가 (try-with-resources 사용)
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fullPath.toFile(), true))) {
writer.write(content);
}
}
/**
* Java 11+ 버전용 텍스트 파일 추가 메서드
* @param filePath 파일 경로
* @param fileName 파일명
* @param content 추가할 내용
* @throws IOException 파일 쓰기 실패 시
*/
public static void appendToTextFileForJava11Plus(String filePath, String fileName, String content) throws IOException {
if (!fileName.toLowerCase().endsWith(".txt")) {
fileName += ".txt";
}
Path fullPath = Paths.get(filePath, fileName);
Files.createDirectories(fullPath.getParent());
// Files.writeString을 APPEND 모드로 사용
Files.writeString(fullPath, content,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
}
FileLockUtil
package util;
import dto.HttpException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* 간단한 파일명 기반 락킹 유틸리티
*/
public class FileLockUtil {
// 파일별 락 관리를 위한 맵
private static final ConcurrentHashMap<String, Object> fileLocks = new ConcurrentHashMap<>();
/**
* 파일에 대한 락을 획득하고 작업을 수행
* @param fileName 락을 걸 파일명
* @param operation 수행할 작업
* @param <T> 반환 타입
* @return 작업 결과
*/
public static <T> T withFileLock(String fileName, LockableOperation<T> operation) throws IOException, HttpException {
Path filePath = Paths.get(fileName);
String absolutePath = filePath.toAbsolutePath().toString();
// 파일별 동기화 객체 생성 (없으면 새로 생성)
Object lockObject = fileLocks.computeIfAbsent(absolutePath, k -> new Object());
synchronized (lockObject) {
FileChannel channel = null;
FileLock fileLock = null;
try {
// 파일 채널 생성
channel = FileChannel.open(filePath,
StandardOpenOption.CREATE,
StandardOpenOption.READ,
StandardOpenOption.WRITE);
// 논블로킹 락 시도 (즉시 실패하면 409 반환)
fileLock = channel.tryLock();
if (fileLock == null) {
throw HttpException.conflict("파일이 다른 프로세스에서 사용 중입니다: " + fileName);
}
// 락을 획득했으므로 작업 수행
return operation.execute(filePath);
} catch (IOException e) {
throw new IOException("파일 락 처리 중 오류 발생: " + e.getMessage(), e);
} finally {
// 리소스 정리
closeQuietly(fileLock, "FileLock");
closeQuietly(channel, "FileChannel");
}
}
}
/**
* 타임아웃과 함께 락 작업 수행
*/
public static <T> T withFileLockTimeout(String fileName, LockableOperation<T> operation,
long timeout, TimeUnit unit) throws IOException, HttpException {
long startTime = System.currentTimeMillis();
long timeoutMillis = unit.toMillis(timeout);
while (System.currentTimeMillis() - startTime < timeoutMillis) {
try {
return withFileLock(fileName, operation);
} catch (HttpException e) {
if (e.getStatusCode() == 409) { // Conflict - 락 획득 실패
// 잠시 대기 후 재시도
try {
Thread.sleep(Math.min(50, timeoutMillis / 20));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw HttpException.internalServerError("작업이 중단되었습니다");
}
continue;
}
throw e; // 다른 에러는 바로 던지기
}
}
// 타임아웃 발생
throw HttpException.requestTimeout("파일 락 획득 시간이 초과되었습니다 (" + timeout + " " + unit.name().toLowerCase() + ")");
}
/**
* 리소스를 조용히 닫기
*/
private static void closeQuietly(AutoCloseable resource, String resourceName) {
if (resource != null) {
try {
resource.close();
} catch (Exception e) {
System.err.println(resourceName + " 닫기 실패: " + e.getMessage());
}
}
}
/**
* 락 하에서 실행할 작업을 정의하는 함수형 인터페이스
*/
@FunctionalInterface
public interface LockableOperation<T> {
T execute(Path filePath) throws IOException, HttpException;
}
}
HttpUtil
package util;
import dto.HttpException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
public class HttpUtil {
/**
* HttpServletRequest에서 요청 본문을 문자열로 읽어오기
*/
public static String readRequestBody(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
BufferedReader br = request.getReader();
try {
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
} finally {
try {
br.close();
} catch (IOException ignore) {
// 리소스 정리 실패는 무시
}
}
return sb.toString();
}
/**
* HttpServletRequest에서 요청 본문을 특정 클래스 타입으로 파싱
*/
public static <T> T readRequestBodyAsJson(HttpServletRequest request, Class<T> clazz) throws IOException {
String body = readRequestBody(request);
return JsonUtil.fromJson(body, clazz);
}
/**
* HttpServletRequest에서 요청 본문을 특정 클래스 타입으로 파싱 (예외 던지기)
*/
public static <T> T readRequestBodyAsJsonThrows(HttpServletRequest request, Class<T> clazz)
throws IOException, com.google.gson.JsonSyntaxException {
String body = readRequestBody(request);
return JsonUtil.fromJsonThrows(body, clazz);
}
/**
* JSON 응답을 HTTP로 전송
*/
public static void sendJsonResponse(HttpServletResponse response, int statusCode, Object data)
throws IOException {
response.setStatus(statusCode);
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
try {
writer.write(JsonUtil.toJson(data));
writer.flush();
} finally {
// PrintWriter는 자동으로 닫히지만 명시적으로 닫기
writer.close();
}
}
/**
* 문자열 응답을 HTTP로 전송
*/
public static void sendTextResponse(HttpServletResponse response, int statusCode, String text)
throws IOException {
response.setStatus(statusCode);
response.setContentType("text/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
try {
writer.write(text);
writer.flush();
} finally {
writer.close();
}
}
/**
* HTML 응답을 HTTP로 전송
*/
public static void sendHtmlResponse(HttpServletResponse response, int statusCode, String html)
throws IOException {
response.setStatus(statusCode);
response.setContentType("text/html;charset=utf-8");
PrintWriter writer = response.getWriter();
try {
writer.write(html);
writer.flush();
} finally {
writer.close();
}
}
/**
* 에러 응답을 JSON 형태로 전송
*/
public static void sendErrorResponse(HttpServletResponse response, int statusCode, String message)
throws IOException {
ErrorResponse error = new ErrorResponse(message, System.currentTimeMillis());
sendJsonResponse(response, statusCode, error);
}
/**
* HttpException을 HTTP 응답으로 전송
*/
public static void sendHttpExceptionResponse(HttpServletResponse response, HttpException exception)
throws IOException {
sendJsonResponse(response, exception.getStatusCode(), exception.getResponseBody());
}
/**
* 요청 파라미터에서 값을 가져오되, 기본값 제공
*/
public static String getParameterWithDefault(HttpServletRequest request, String paramName, String defaultValue) {
String value = request.getParameter(paramName);
return (value != null && !value.trim().isEmpty()) ? value.trim() : defaultValue;
}
/**
* 요청 파라미터를 정수로 변환 (기본값 제공)
*/
public static int getIntParameter(HttpServletRequest request, String paramName, int defaultValue) {
String value = request.getParameter(paramName);
if (value == null || value.trim().isEmpty()) {
return defaultValue;
}
try {
return Integer.parseInt(value.trim());
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* 요청 파라미터를 boolean으로 변환 (기본값 제공)
*/
public static boolean getBooleanParameter(HttpServletRequest request, String paramName, boolean defaultValue) {
String value = request.getParameter(paramName);
if (value == null || value.trim().isEmpty()) {
return defaultValue;
}
return "true".equalsIgnoreCase(value.trim()) || "1".equals(value.trim());
}
/**
* 요청 헤더에서 값을 가져오되, 기본값 제공
*/
public static String getHeaderWithDefault(HttpServletRequest request, String headerName, String defaultValue) {
String value = request.getHeader(headerName);
return (value != null && !value.trim().isEmpty()) ? value.trim() : defaultValue;
}
/**
* 클라이언트 IP 주소 가져오기 (프록시 고려)
*/
public static String getClientIpAddress(HttpServletRequest request) {
String xForwardedFor = request.getHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.getHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp.trim();
}
return request.getRemoteAddr();
}
/**
* User-Agent 헤더 가져오기
*/
public static String getUserAgent(HttpServletRequest request) {
return getHeaderWithDefault(request, "User-Agent", "Unknown");
}
// 내부 클래스 - 에러 응답용
public static class ErrorResponse {
public boolean success = false;
public String error;
public long timestamp;
public ErrorResponse(String error, long timestamp) {
this.error = error;
this.timestamp = timestamp;
}
}
}
JsonUtil
package util;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
public class JsonUtil {
// Gson 인스턴스 (재사용을 위해 static으로 선언)
private static final Gson gson = new Gson(); // Gson은 thread-safe해서 멀티스레딩 환경에서도 문제없이 사용
/**
* Gson 인스턴스 반환
*/
public static Gson gson() {
return gson;
}
/**
* 객체를 JSON 문자열로 변환
*/
public static String toJson(Object obj) {
if (obj == null) {
return "null";
}
return gson.toJson(obj);
}
/**
* JSON 문자열을 특정 클래스 타입으로 변환
*/
public static <T> T fromJson(String json, Class<T> clazz) {
if (json == null || json.trim().isEmpty()) {
return null;
}
try {
return gson.fromJson(json, clazz);
} catch (JsonSyntaxException e) {
System.err.println("JSON 파싱 실패: " + e.getMessage());
return null;
}
}
/**
* JSON 문자열을 특정 클래스 타입으로 변환 (예외 던지기)
*/
public static <T> T fromJsonThrows(String json, Class<T> clazz) throws JsonSyntaxException {
if (json == null || json.trim().isEmpty()) {
throw new JsonSyntaxException("JSON 문자열이 비어있습니다");
}
return gson.fromJson(json, clazz);
}
/**
* 객체를 예쁘게 포맷된 JSON 문자열로 변환 (디버깅용)
*/
public static String toPrettyJson(Object obj) {
if (obj == null) {
return "null";
}
return gson.newBuilder().setPrettyPrinting().create().toJson(obj);
}
/**
* JSON 문자열이 유효한지 검증
*/
public static boolean isValidJson(String json) {
if (json == null || json.trim().isEmpty()) {
return false;
}
try {
gson.fromJson(json, Object.class);
return true;
} catch (JsonSyntaxException e) {
return false;
}
}
/**
* JSON 문자열을 Object로 파싱 (타입 모를 때 사용)
*/
public static Object parseJson(String json) {
if (json == null || json.trim().isEmpty()) {
return null;
}
try {
return gson.fromJson(json, Object.class);
} catch (JsonSyntaxException e) {
System.err.println("JSON 파싱 실패: " + e.getMessage());
return null;
}
}
/**
* JSON 문자열에서 특정 필드 값을 추출 (간단한 파싱)
*/
public static String extractField(String json, String fieldName) {
if (json == null || json.trim().isEmpty() || fieldName == null) {
return null;
}
// 간단한 필드 추출: "fieldName":"value" 패턴
String pattern = "\\"" + fieldName + "\\"\\\\s*:\\\\s*\\"([^\\"]+)\\"";
java.util.regex.Pattern regex = java.util.regex.Pattern.compile(pattern);
java.util.regex.Matcher matcher = regex.matcher(json);
if (matcher.find()) {
return matcher.group(1);
}
return null;
}
}
ThreadUtil
package util;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.function.Supplier;
public class ThreadUtil {
private static final ExecutorService EXECUTOR_SERVICE =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
//읽기 접근만 허용하면 더 안전할듯함...
public static ExecutorService getExecutorService(){
return EXECUTOR_SERVICE;
}
public static <T> Future<T> submit(Callable<T> task) {
return EXECUTOR_SERVICE.submit(task);
}
public static Future<?> submit(Runnable task) {
return EXECUTOR_SERVICE.submit(task);
}
/**
* 비동기 작업을 CompletableFuture로 실행
*/
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier) {
return CompletableFuture.supplyAsync(supplier, EXECUTOR_SERVICE);
}
/**
* 반환값이 없는 비동기 작업 실행
*/
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return CompletableFuture.runAsync(runnable, EXECUTOR_SERVICE);
}
/**
* 여러 CompletableFuture를 모두 완료될 때까지 대기하고 결과 수집
*/
public static <T> List<T> awaitAllAndCollect(List<CompletableFuture<T>> futures) {
// 한 번에 모든 결과를 기다리고 수집
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(ignored -> {
List<T> results = new ArrayList<>(futures.size());
for (CompletableFuture<T> future : futures) {
results.add(future.getNow(null)); // 이미 완료되었으므로 즉시 반환
}
return results;
}).join();
}
/**
* 여러 CompletableFuture를 모두 완료될 때까지 대기만 (결과 수집 안함)
*/
public static void awaitAll(CompletableFuture<?>... futures) {
CompletableFuture.allOf(futures).join();
}
/**
* 여러 CompletableFuture 중 하나라도 완료되면 반환
*/
public static <T> T awaitAny(CompletableFuture<T>... futures) {
return CompletableFuture.anyOf(futures).thenApply(result -> (T) result).join();
}
// 기존 shutdown 관련 코드
public static void shutdown() {
EXECUTOR_SERVICE.shutdown();
try {
if (!EXECUTOR_SERVICE.awaitTermination(10, TimeUnit.SECONDS)) {
EXECUTOR_SERVICE.shutdownNow();
}
} catch (InterruptedException e) {
EXECUTOR_SERVICE.shutdownNow();
}
}
static {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (!EXECUTOR_SERVICE.isShutdown()) {
shutdown();
}
}));
}
}
CategoryService
package service;
import dto.HttpException;
import util.FileLockUtil;
import util.ThreadUtil;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static util.FileLoader.readFileLines;
public class CategoryService {
public Map<String, String> categoryMap = new ConcurrentHashMap<>();
public Map<String, String> riskMap = new ConcurrentHashMap<>();
public CategoryService(String filePath) {
loadCategoriesAsync(filePath).join(); // 동기적으로 초기화 완료 대기
}
public CategoryService(String filePath, String filePath2) {
// 두 파일을 병렬로 로드
CompletableFuture<Void> categoryFuture = loadCategoriesAsync(filePath);
CompletableFuture<Void> riskFuture = loadRisksAsync(filePath2);
// 모든 로딩 작업 완료 대기
ThreadUtil.awaitAll(categoryFuture, riskFuture);
}
/**
* 비동기로 위험 키워드 로드
*/
private CompletableFuture<Void> loadRisksAsync(String filePath) {
return ThreadUtil.supplyAsync(() -> readFileLines(filePath))
.thenAccept(list -> {
for (String s : list) {
riskMap.put(s, "");
}
});
}
/**
* 비동기로 카테고리 매핑 로드
*/
private CompletableFuture<Void> loadCategoriesAsync(String filePath) {
return ThreadUtil.supplyAsync(() -> readFileLines(filePath))
.thenAccept(list -> {
for (String s : list) {
String[] split = s.split("#");
if (split.length >= 2) {
categoryMap.put(split[0], split[1]);
}
}
});
}
/**
* 비동기로 카테고리 저장 (기본 결과 파일)
*/
public CompletableFuture<Void> saveCategoryAsync(String input) {
return ThreadUtil.supplyAsync(() -> categorize(input))
.thenCompose(categorizedResult ->
ThreadUtil.supplyAsync(() -> {
try {
return FileLockUtil.withFileLock("RESULTS.TXT", (filePath) -> {
String content = categorizedResult + "\\n";
if (Files.exists(filePath)) {
Files.write(filePath, content.getBytes(),
java.nio.file.StandardOpenOption.APPEND);
} else {
Files.write(filePath, content.getBytes(),
java.nio.file.StandardOpenOption.CREATE);
}
return null;
});
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
})
);
}
/**
* 동기 버전 (기존 호환성 유지)
*/
public void saveCategory(String input) throws IOException {
try {
saveCategoryAsync(input).join();
} catch (RuntimeException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
throw e;
}
}
/**
* 비동기로 필터링된 카테고리 저장
*/
public CompletableFuture<Void> saveFilteredCategoryAsync(String input) {
return ThreadUtil.supplyAsync(() -> categorize(input))
.thenCompose(categorizedResult ->
ThreadUtil.supplyAsync(() -> {
try {
return FileLockUtil.withFileLock("FILTERED_RESULTS.TXT", (filePath) -> {
String content = categorizedResult + "\\n";
if (Files.exists(filePath)) {
Files.write(filePath, content.getBytes(),
java.nio.file.StandardOpenOption.APPEND);
} else {
Files.write(filePath, content.getBytes(),
java.nio.file.StandardOpenOption.CREATE);
}
return null;
});
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
})
);
}
/**
* 동기 버전 (기존 호환성 유지)
*/
public void saveFilteredCategory(String input) throws IOException {
try {
saveFilteredCategoryAsync(input).join();
} catch (RuntimeException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
throw e;
}
}
/**
* 세션별 파일에 거래 데이터를 비동기로 저장 (파일명 기반 락킹)
*/
public CompletableFuture<Void> saveFilteredCategoryAsync(String sessionId, String transaction) {
return ThreadUtil.supplyAsync(() -> categorize(transaction))
.thenCompose(processedTransaction ->
ThreadUtil.supplyAsync(() -> {
try {
String sessionFileName = "SESSION_" + sessionId + ".txt";
return FileLockUtil.withFileLock(sessionFileName, (filePath) -> {
String content = processedTransaction.trim() + "\\n";
if (Files.exists(filePath)) {
Files.write(filePath, content.getBytes(),
java.nio.file.StandardOpenOption.APPEND);
} else {
Files.write(filePath, content.getBytes(),
java.nio.file.StandardOpenOption.CREATE);
}
return null;
});
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
})
);
}
/**
* 세션별 파일에 거래 데이터를 저장 (동기 버전, 기존 호환성 유지)
*/
public void saveFilteredCategory(String sessionId, String transaction) throws IOException, HttpException {
try {
saveFilteredCategoryAsync(sessionId, transaction).join();
} catch (RuntimeException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
if (e.getCause() instanceof HttpException) {
throw (HttpException) e.getCause();
}
throw e;
}
}
/**
* 세션 파일에서 모든 거래 데이터를 비동기로 읽어오기
*/
public CompletableFuture<List<String>> readSessionTransactionsAsync(String sessionId) {
return ThreadUtil.supplyAsync(() -> {
try {
String sessionFileName = "SESSION_" + sessionId + ".txt";
return FileLockUtil.withFileLockTimeout(sessionFileName, (filePath) -> {
if (!Files.exists(filePath)) {
throw HttpException.notFound("세션 " + sessionId + "의 데이터가 없습니다");
}
List<String> lines = Files.readAllLines(filePath);
lines.removeIf(line -> line.trim().isEmpty());
return lines;
}, 1, TimeUnit.SECONDS);
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
});
}
/**
* 세션 파일에서 모든 거래 데이터를 읽어오기 (동기 버전, 기존 호환성 유지)
*/
public List<String> readSessionTransactions(String sessionId) throws IOException, HttpException {
try {
return readSessionTransactionsAsync(sessionId).join();
} catch (RuntimeException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
if (e.getCause() instanceof HttpException) {
throw (HttpException) e.getCause();
}
throw e;
}
}
/**
* 병렬 처리를 통한 향상된 categorize 메서드
*/
public String categorize(String input) {
String[] arr = input.split(" ");
// 작은 입력에 대해서는 기존 방식 사용
if (arr.length < 10) {
return categorizeSync(arr);
}
// 큰 입력에 대해서는 병렬 처리
return categorizeFE(arr);
}
/**
* 동기 방식의 categorize (기존 로직)
*/
private String categorizeSync(String[] arr) {
PriorityQueue<String> pq = new PriorityQueue<>(String::compareTo);
for (String word : arr) {
if (riskMap.containsKey(word)) {
continue; // 고위험 패턴 제거
}
String category = categoryMap.getOrDefault(word, "");
if (!category.isEmpty()) {
pq.add(category);
}
}
StringBuilder sb = new StringBuilder();
while (!pq.isEmpty()) {
String category = pq.poll();
sb.append(category).append(' ');
}
return sb.toString().trim();
}
/**
* 병렬 처리 방식의 categorize
*/
private String categorizeFE(String[] arr) {
// 배열을 청크로 나누어 병렬 처리
int chunkSize = Math.max(1, arr.length / ThreadUtil.getExecutorService().getClass().getDeclaredMethods().length);
List<CompletableFuture<List<String>>> futures = new java.util.ArrayList<>();
for (int i = 0; i < arr.length; i += chunkSize) {
final int start = i;
final int end = Math.min(i + chunkSize, arr.length);
CompletableFuture<List<String>> future = ThreadUtil.supplyAsync(() -> {
List<String> results = new java.util.ArrayList<>();
for (int j = start; j < end; j++) {
String word = arr[j];
if (!riskMap.containsKey(word)) {
String category = categoryMap.getOrDefault(word, "");
if (!category.isEmpty()) {
results.add(category);
}
}
}
return results;
});
futures.add(future);
}
// 모든 결과를 수집하고 정렬
List<List<String>> allResults = ThreadUtil.awaitAllAndCollect(futures);
return allResults.stream()
.flatMap(List::stream)
.sorted()
.collect(Collectors.joining(" "));
}
/**
* 여러 입력을 배치로 병렬 처리
*/
public CompletableFuture<List<String>> categorizeBatchAsync(List<String> inputs) {
List<CompletableFuture<String>> futures = inputs.stream()
.map(input -> ThreadUtil.supplyAsync(() -> categorize(input)))
.collect(Collectors.toList());
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(ignored -> ThreadUtil.awaitAllAndCollect(futures));
}
/**
* 동기 배치 처리 (기존 호환성)
*/
public List<String> categorizeBatch(List<String> inputs) {
return categorizeBatchAsync(inputs).join();
}
}
RiskAnalysisService
package service;
import dto.Dtos;
import dto.HttpException;
import util.FileLockUtil;
import util.JsonUtil;
import util.ThreadUtil;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
public class RiskAnalysisService {
private final HttpClient httpClient;
private final Map<String, String> riskCodeToLevelMap;
private final String serviceUrl;
// 설정 파일 캐시 (파일 변경 감지를 위한 타임스탬프 포함)
private static final Map<String, CachedConfig> configCache = new ConcurrentHashMap<>();
private static final long CONFIG_CACHE_TTL = 30000; // 30초 캐시
private static class CachedConfig {
final Dtos.ServicesConfig config;
final long timestamp;
CachedConfig(Dtos.ServicesConfig config, long timestamp) {
this.config = config;
this.timestamp = timestamp;
}
boolean isExpired() {
return System.currentTimeMillis() - timestamp > CONFIG_CACHE_TTL;
}
}
public RiskAnalysisService() throws IOException, HttpException {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(500))
.build();
// 설정을 비동기로 로드
CompletableFuture<Dtos.ServicesConfig> configFuture = loadServiceConfigAsync();
this.riskCodeToLevelMap = new HashMap<>();
// 설정 로드 완료 대기 및 초기화
try {
Dtos.ServicesConfig config = configFuture.get(2, TimeUnit.SECONDS);
initializeFromConfig(config);
this.serviceUrl = config.services[0].url;
} catch (Exception e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
if (e.getCause() instanceof HttpException) {
throw (HttpException) e.getCause();
}
throw new IOException("서비스 초기화 실패", e);
}
}
/**
* 비동기로 서비스 설정 로드 (파일 락킹 및 캐싱 적용)
*/
private CompletableFuture<Dtos.ServicesConfig> loadServiceConfigAsync() {
return ThreadUtil.supplyAsync(() -> {
try {
String configFileName = "SERVICES.JSON";
// 캐시된 설정 확인
CachedConfig cached = configCache.get(configFileName);
if (cached != null && !cached.isExpired()) {
return cached.config;
}
// 파일 락을 사용하여 안전하게 설정 로드
return FileLockUtil.withFileLockTimeout(configFileName, (filePath) -> {
if (!Files.exists(filePath)) {
throw HttpException.notFound("SERVICES.JSON 파일을 찾을 수 없습니다");
}
String configJson = Files.readString(filePath);
Dtos.ServicesConfig config = JsonUtil.fromJson(configJson, Dtos.ServicesConfig.class);
if (config.services == null || config.services.length == 0) {
throw HttpException.internalServerError("위험도 분석 서비스 설정을 찾을 수 없습니다");
}
// 캐시에 저장
configCache.put(configFileName, new CachedConfig(config, System.currentTimeMillis()));
return config;
}, 1, TimeUnit.SECONDS);
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
});
}
/**
* 설정으로부터 위험도 매핑 초기화
*/
private void initializeFromConfig(Dtos.ServicesConfig config) {
// 첫 번째 서비스 사용
Dtos.Service service = config.services[0];
// 위험도 코드 매핑 설정
if (service.riskMappings != null) {
for (Dtos.RiskMapping mapping : service.riskMappings) {
riskCodeToLevelMap.put(mapping.code, mapping.level);
}
}
}
/**
* 설정 재로드 (비동기)
*/
public CompletableFuture<Void> reloadConfigAsync() {
return ThreadUtil.runAsync(() -> {
// 캐시 무효화
configCache.clear();
try {
Dtos.ServicesConfig config = loadServiceConfigAsync().join();
riskCodeToLevelMap.clear();
initializeFromConfig(config);
} catch (Exception e) {
throw new RuntimeException("설정 재로드 실패", e);
}
});
}
/**
* 여러 거래를 병렬로 분석 (ThreadUtil 활용)
*/
public CompletableFuture<List<String>> analyzeTransactionsParallelAsync(List<String> transactions) {
long startTime = System.currentTimeMillis();
return ThreadUtil.supplyAsync(() -> {
// 각 거래에 대해 비동기 분석 요청 생성
List<CompletableFuture<String>> futures = transactions.stream()
.map(this::analyzeTransactionAsync)
.collect(Collectors.toList());
try {
// ThreadUtil의 awaitAllAndCollect 사용 (800ms 타임아웃은 각 작업에서 처리)
List<String> results = ThreadUtil.awaitAllAndCollect(futures);
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed > 1000) {
throw HttpException.requestTimeout("총 처리 시간이 1초를 초과했습니다");
}
return results;
} catch (Exception e) {
if (e instanceof HttpException) {
throw new RuntimeException(e);
}
throw new RuntimeException(HttpException.internalServerError("위험도 분석 처리 중 오류 발생: " + e.getMessage()));
}
});
}
/**
* 동기 버전 (기존 호환성 유지)
*/
public List<String> analyzeTransactionsParallel(List<String> transactions) throws IOException, HttpException {
try {
return analyzeTransactionsParallelAsync(transactions).join();
} catch (RuntimeException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
if (e.getCause() instanceof HttpException) {
throw (HttpException) e.getCause();
}
throw new IOException("위험도 분석 실패", e);
}
}
/**
* 단일 거래 비동기 분석
*/
private CompletableFuture<String> analyzeTransactionAsync(String transaction) {
return ThreadUtil.supplyAsync(() -> {
try {
return analyzeTransaction(transaction);
} catch (IOException | HttpException e) {
throw new RuntimeException(e);
}
}).orTimeout(500, TimeUnit.MILLISECONDS) // 개별 요청 타임아웃
.exceptionally(throwable -> {
if (throwable instanceof java.util.concurrent.TimeoutException) {
throw new RuntimeException(HttpException.requestTimeout("개별 거래 분석 시간 초과"));
}
throw new RuntimeException(throwable);
});
}
/**
* 단일 거래 분석 (동기 버전)
*/
private String analyzeTransaction(String transaction) throws IOException, HttpException {
// 외부 서비스 요청 생성
Dtos.RiskAnalysisRequest request = new Dtos.RiskAnalysisRequest();
request.query = transaction.trim();
String requestJson = JsonUtil.toJson(request);
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create(serviceUrl))
.header("Content-Type", "application/json")
.timeout(Duration.ofMillis(500))
.POST(HttpRequest.BodyPublishers.ofString(requestJson))
.build();
try {
HttpResponse<String> response = httpClient.send(httpRequest,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
throw HttpException.internalServerError(
"외부 서비스 오류: " + response.statusCode());
}
// 응답 파싱
Dtos.RiskAnalysisResponse analysisResponse = JsonUtil.fromJson(
response.body(), Dtos.RiskAnalysisResponse.class);
// 위험도 코드를 위험도 레벨로 변환
String riskLevel = riskCodeToLevelMap.getOrDefault(
analysisResponse.riskCode, "UNKNOWN");
return riskLevel;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw HttpException.internalServerError("외부 서비스 호출이 중단되었습니다");
} catch (IOException e) {
throw new IOException("외부 서비스 통신 오류: " + e.getMessage(), e);
}
}
/**
* 배치 크기를 조정하여 처리하는 고급 병렬 분석
*/
public CompletableFuture<List<String>> analyzeTransactionsBatchAsync(List<String> transactions, int batchSize) {
if (transactions.isEmpty()) {
return CompletableFuture.completedFuture(List.of());
}
return ThreadUtil.supplyAsync(() -> {
// 배치로 나누어 처리
List<CompletableFuture<List<String>>> batchFutures = new java.util.ArrayList<>();
for (int i = 0; i < transactions.size(); i += batchSize) {
int end = Math.min(i + batchSize, transactions.size());
List<String> batch = transactions.subList(i, end);
CompletableFuture<List<String>> batchFuture = analyzeTransactionsParallelAsync(batch);
batchFutures.add(batchFuture);
}
// 모든 배치 결과 수집
List<List<String>> batchResults = ThreadUtil.awaitAllAndCollect(batchFutures);
return batchResults.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
});
}
/**
* 서비스 상태 확인 (비동기)
*/
public CompletableFuture<Boolean> checkServiceHealthAsync() {
return ThreadUtil.supplyAsync(() -> {
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(serviceUrl + "/health"))
.timeout(Duration.ofSeconds(1))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
return response.statusCode() == 200;
} catch (Exception e) {
return false;
}
});
}
/**
* 설정 파일 감시 및 자동 재로드 시작
*/
public CompletableFuture<Void> startConfigWatcherAsync() {
return ThreadUtil.runAsync(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(CONFIG_CACHE_TTL);
// 캐시된 설정이 만료되었는지 확인
CachedConfig cached = configCache.get("SERVICES.JSON");
if (cached != null && cached.isExpired()) {
reloadConfigAsync().join();
System.out.println("서비스 설정이 자동으로 재로드되었습니다.");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
System.err.println("설정 감시 중 오류 발생: " + e.getMessage());
}
}
});
}
/**
* 리소스 정리
*/
public void shutdown() {
configCache.clear();
// HttpClient는 자동으로 정리됨
}
}
FILE LOCK UTIL 완전 사용 가이드
FileLockUtil
package tutorial;
import dto.HttpException;
import util.FileLockUtil;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.ArrayList;
/**
* FileLockUtil 완전 사용 가이드 - 초심자용
* 파일 동시 접근 문제를 해결하는 방법들을 예제로 설명
*/
public class FileLockUtilTutorial {
public static void main(String[] args) {
System.out.println("=== FileLockUtil 사용법 강의 시작 ===\\n");
// 1. 기본 파일 락킹 - 안전한 파일 쓰기
lesson1_BasicFileLock();
// 2. 타임아웃과 함께 사용하기
lesson2_FileLockWithTimeout();
// 3. 파일 읽기와 쓰기를 안전하게
lesson3_SafeReadAndWrite();
// 4. 동시 접근 문제 시나리오와 해결
lesson4_ConcurrentAccessProblem();
// 5. 실제 서비스에서 사용하는 패턴들
lesson5_RealWorldExamples();
System.out.println("\\n=== FileLockUtil 강의 완료 ===");
}
/**
* 레슨 1: 기본 파일 락킹 - 안전한 파일 쓰기
* withFileLock()의 기본 사용법
*/
private static void lesson1_BasicFileLock() {
System.out.println("--- 레슨 1: 기본 파일 락킹 ---");
String fileName = "user_data.txt";
try {
// withFileLock(): 파일에 락을 걸고 안전하게 작업 수행
String result = FileLockUtil.withFileLock(fileName, (filePath) -> {
// 이 블록 안에서는 다른 프로세스가 같은 파일에 접근할 수 없음
System.out.println(" [안전영역] 파일 락 획득 성공: " + fileName);
try {
// 파일에 데이터 추가
String userData = "사용자ID: user123, 접속시간: " + System.currentTimeMillis() + "\\n";
if (Files.exists(filePath)) {
// 파일이 있으면 내용 추가
Files.write(filePath, userData.getBytes(), StandardOpenOption.APPEND);
System.out.println(" [안전영역] 기존 파일에 데이터 추가");
} else {
// 파일이 없으면 새로 생성
Files.write(filePath, userData.getBytes(), StandardOpenOption.CREATE);
System.out.println(" [안전영역] 새 파일 생성 및 데이터 저장");
}
return "데이터 저장 완료";
} catch (IOException e) {
throw new RuntimeException("파일 쓰기 실패", e);
}
});
System.out.println("결과: " + result);
System.out.println("파일 락 자동 해제됨\\n");
} catch (IOException | HttpException e) {
System.err.println("파일 작업 실패: " + e.getMessage());
}
}
/**
* 레슨 2: 타임아웃과 함께 사용하기
* withFileLockTimeout()으로 무한 대기 방지
*/
private static void lesson2_FileLockWithTimeout() {
System.out.println("--- 레슨 2: 타임아웃과 함께 파일 락킹 ---");
String fileName = "config.json";
try {
// withFileLockTimeout(): 지정된 시간 내에 락 획득 실패하면 에러 발생
String config = FileLockUtil.withFileLockTimeout(fileName, (filePath) -> {
System.out.println(" [타임아웃 락] 파일 락 획득: " + fileName);
try {
if (!Files.exists(filePath)) {
// 기본 설정 파일 생성
String defaultConfig = "{\\n \\"server_port\\": 8080,\\n \\"debug_mode\\": false\\n}";
Files.write(filePath, defaultConfig.getBytes(), StandardOpenOption.CREATE);
System.out.println(" [타임아웃 락] 기본 설정 파일 생성");
return defaultConfig;
} else {
// 기존 설정 파일 읽기
String existingConfig = Files.readString(filePath);
System.out.println(" [타임아웃 락] 기존 설정 파일 읽기 완료");
return existingConfig;
}
} catch (IOException e) {
throw new RuntimeException("설정 파일 처리 실패", e);
}
}, 2, TimeUnit.SECONDS); // 2초 타임아웃
System.out.println("설정 내용:\\n" + config);
System.out.println();
} catch (IOException | HttpException e) {
if (e instanceof HttpException && ((HttpException) e).getStatusCode() == 408) {
System.err.println("타임아웃 발생: 2초 내에 파일 락을 획득하지 못했습니다");
} else {
System.err.println("설정 파일 처리 실패: " + e.getMessage());
}
}
}
/**
* 레슨 3: 파일 읽기와 쓰기를 안전하게
* 읽기-수정-쓰기 패턴의 안전한 구현
*/
private static void lesson3_SafeReadAndWrite() {
System.out.println("--- 레슨 3: 안전한 읽기-수정-쓰기 ---");
String counterFileName = "visit_counter.txt";
try {
// 방문자 수 증가 작업 (읽기 -> 증가 -> 쓰기)
Integer newCount = FileLockUtil.withFileLock(counterFileName, (filePath) -> {
System.out.println(" [카운터 락] 방문자 카운터 파일 락 획득");
try {
int currentCount = 0;
// 1단계: 현재 카운트 읽기
if (Files.exists(filePath)) {
String countStr = Files.readString(filePath).trim();
currentCount = countStr.isEmpty() ? 0 : Integer.parseInt(countStr);
System.out.println(" [카운터 락] 현재 방문자 수: " + currentCount);
} else {
System.out.println(" [카운터 락] 카운터 파일 없음, 새로 생성");
}
// 2단계: 카운트 증가
int newCount = currentCount + 1;
// 3단계: 새 카운트 저장
Files.write(filePath, String.valueOf(newCount).getBytes(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(" [카운터 락] 새 방문자 수: " + newCount + " (저장 완료)");
return newCount;
} catch (IOException e) {
throw new RuntimeException("카운터 업데이트 실패", e);
} catch (NumberFormatException e) {
throw new RuntimeException("카운터 파일 형식 오류", e);
}
});
System.out.println("방문자 카운터 업데이트 완료: " + newCount + "번째 방문자\\n");
} catch (IOException | HttpException e) {
System.err.println("방문자 카운터 업데이트 실패: " + e.getMessage());
}
}
/**
* 레슨 4: 동시 접근 문제 시나리오와 해결
* 여러 스레드가 동시에 같은 파일에 접근할 때의 문제와 해결책
*/
private static void lesson4_ConcurrentAccessProblem() {
System.out.println("--- 레슨 4: 동시 접근 문제 해결 ---");
String logFileName = "application.log";
// 시나리오: 여러 스레드가 동시에 로그 파일에 쓰기 시도
System.out.println("5개 스레드가 동시에 로그 파일에 쓰기 시도...");
ExecutorService executor = Executors.newFixedThreadPool(5);
List<CompletableFuture<String>> tasks = new ArrayList<>();
// 5개 스레드가 각각 로그를 남기려고 시도
for (int i = 1; i <= 5; i++) {
final int threadNum = i;
CompletableFuture<String> task = CompletableFuture.supplyAsync(() -> {
try {
// FileLockUtil을 사용해서 안전하게 로그 작성
return FileLockUtil.withFileLockTimeout(logFileName, (filePath) -> {
String threadName = "Thread-" + threadNum;
System.out.println(" [" + threadName + "] 로그 파일 락 획득");
try {
// 현재 시간과 스레드 정보로 로그 메시지 생성
String logMessage = String.format("[%d] %s: 작업 수행 중...\\n",
System.currentTimeMillis(), threadName);
// 파일에 로그 추가 (기존 내용 유지)
if (Files.exists(filePath)) {
Files.write(filePath, logMessage.getBytes(), StandardOpenOption.APPEND);
} else {
Files.write(filePath, logMessage.getBytes(), StandardOpenOption.CREATE);
}
System.out.println(" [" + threadName + "] 로그 작성 완료");
return threadName + " 성공";
} catch (IOException e) {
throw new RuntimeException("로그 작성 실패", e);
}
}, 1, TimeUnit.SECONDS); // 1초 타임아웃
} catch (IOException | HttpException e) {
return "Thread-" + threadNum + " 실패: " + e.getMessage();
}
}, executor);
tasks.add(task);
}
// 모든 스레드 작업 완료 대기
List<String> results = new ArrayList<>();
for (CompletableFuture<String> task : tasks) {
results.add(task.join());
}
System.out.println("동시 접근 테스트 결과:");
results.forEach(result -> System.out.println(" " + result));
executor.shutdown();
System.out.println();
}
/**
* 레슨 5: 실제 서비스에서 사용하는 패턴들
*/
private static void lesson5_RealWorldExamples() {
System.out.println("--- 레슨 5: 실제 서비스 사용 패턴 ---");
// 패턴 1: 세션 데이터 안전 저장
example1_SessionDataStorage();
// 패턴 2: 설정 파일 안전 업데이트
example2_ConfigFileUpdate();
// 패턴 3: 임시 파일을 이용한 원자적 업데이트
example3_AtomicFileUpdate();
// 패턴 4: 여러 파일을 순서대로 안전하게 업데이트
example4_MultipleFilesUpdate();
}
/**
* 실제 예제 1: 세션 데이터 안전 저장
* 웹 서비스에서 사용자 세션 데이터를 파일에 저장할 때
*/
private static void example1_SessionDataStorage() {
System.out.println(" [실제 예제 1] 세션 데이터 저장");
String sessionId = "SESS_12345";
String sessionFileName = "session_" + sessionId + ".txt";
try {
// 세션에 새로운 거래 데이터 추가
String transactionData = "CARD_PAYMENT 50000 CAFE 2024-12-01";
String result = FileLockUtil.withFileLock(sessionFileName, (filePath) -> {
System.out.println(" [세션] 세션 파일 락 획득: " + sessionFileName);
try {
// 기존 세션 데이터 읽기
List<String> existingData = new ArrayList<>();
if (Files.exists(filePath)) {
existingData = Files.readAllLines(filePath);
System.out.println(" [세션] 기존 데이터 " + existingData.size() + "건 로드");
}
// 새 거래 데이터 추가
existingData.add(transactionData);
// 전체 데이터 다시 저장
Files.write(filePath, existingData,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(" [세션] 새 거래 데이터 추가됨 (총 " + existingData.size() + "건)");
return "세션 업데이트 완료";
} catch (IOException e) {
throw new RuntimeException("세션 파일 처리 실패", e);
}
});
System.out.println(" 결과: " + result + "\\n");
} catch (IOException | HttpException e) {
System.err.println(" 세션 데이터 저장 실패: " + e.getMessage());
}
}
/**
* 실제 예제 2: 설정 파일 안전 업데이트
* 서버 운영 중에 설정을 동적으로 변경할 때
*/
private static void example2_ConfigFileUpdate() {
System.out.println(" [실제 예제 2] 설정 파일 업데이트");
String configFileName = "server_config.properties";
try {
// 서버 포트 설정을 8080에서 8090으로 변경
String result = FileLockUtil.withFileLockTimeout(configFileName, (filePath) -> {
System.out.println(" [설정] 설정 파일 락 획득");
try {
List<String> configLines = new ArrayList<>();
if (Files.exists(filePath)) {
configLines = Files.readAllLines(filePath);
System.out.println(" [설정] 기존 설정 " + configLines.size() + "줄 로드");
} else {
System.out.println(" [설정] 새 설정 파일 생성");
}
// 포트 설정 찾아서 업데이트
boolean portUpdated = false;
for (int i = 0; i < configLines.size(); i++) {
if (configLines.get(i).startsWith("server.port=")) {
configLines.set(i, "server.port=8090");
portUpdated = true;
System.out.println(" [설정] 포트 설정 업데이트: 8080 -> 8090");
break;
}
}
// 포트 설정이 없으면 새로 추가
if (!portUpdated) {
configLines.add("server.port=8090");
System.out.println(" [설정] 새 포트 설정 추가: 8090");
}
// 설정 파일 저장
Files.write(filePath, configLines,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
return "설정 업데이트 완료 (포트: 8090)";
} catch (IOException e) {
throw new RuntimeException("설정 파일 업데이트 실패", e);
}
}, 3, TimeUnit.SECONDS);
System.out.println(" 결과: " + result + "\\n");
} catch (IOException | HttpException e) {
System.err.println(" 설정 파일 업데이트 실패: " + e.getMessage());
}
}
/**
* 실제 예제 3: 임시 파일을 이용한 원자적 업데이트
* 중요한 데이터 파일을 안전하게 업데이트할 때 (중간에 실패해도 원본 보존)
*/
private static void example3_AtomicFileUpdate() {
System.out.println(" [실제 예제 3] 원자적 파일 업데이트");
String dataFileName = "important_data.json";
String tempFileName = dataFileName + ".tmp";
try {
String result = FileLockUtil.withFileLock(dataFileName, (filePath) -> {
System.out.println(" [원자적] 데이터 파일 락 획득");
try {
Path tempPath = filePath.getParent().resolve(tempFileName);
// 1단계: 새 데이터를 임시 파일에 먼저 저장
String newData = "{\\n \\"users\\": 150,\\n \\"orders\\": 320,\\n \\"updated\\": " + System.currentTimeMillis() + "\\n}";
Files.write(tempPath, newData.getBytes(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
System.out.println(" [원자적] 새 데이터를 임시 파일에 저장");
// 2단계: 임시 파일이 제대로 생성되었는지 검증
if (!Files.exists(tempPath) || Files.size(tempPath) == 0) {
throw new RuntimeException("임시 파일 생성 실패");
}
// 3단계: 기존 파일을 백업 (선택사항)
if (Files.exists(filePath)) {
Path backupPath = filePath.getParent().resolve(dataFileName + ".backup");
Files.copy(filePath, backupPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
System.out.println(" [원자적] 기존 파일 백업 생성");
}
// 4단계: 임시 파일을 실제 파일로 이동 (원자적 연산)
Files.move(tempPath, filePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
System.out.println(" [원자적] 임시 파일을 실제 파일로 이동 완료");
return "데이터 파일 원자적 업데이트 완료";
} catch (IOException e) {
// 실패 시 임시 파일 정리
try {
Path tempPath = filePath.getParent().resolve(tempFileName);
if (Files.exists(tempPath)) {
Files.delete(tempPath);
System.out.println(" [원자적] 실패로 인한 임시 파일 정리");
}
} catch (IOException cleanupError) {
System.err.println(" [원자적] 임시 파일 정리 실패: " + cleanupError.getMessage());
}
throw new RuntimeException("원자적 업데이트 실패", e);
}
});
System.out.println(" 결과: " + result + "\\n");
} catch (IOException | HttpException e) {
System.err.println(" 원자적 파일 업데이트 실패: " + e.getMessage());
}
}
/**
* 실제 예제 4: 여러 파일을 순서대로 안전하게 업데이트
* 관련된 여러 파일을 일관성 있게 업데이트할 때
*/
private static void example4_MultipleFilesUpdate() {
System.out.println(" [실제 예제 4] 여러 파일 순서적 업데이트");
String[] fileNames = {"users.txt", "orders.txt", "products.txt"};
try {
// 파일명 순서대로 락을 획득하여 데드락 방지
for (String fileName : fileNames) {
String result = FileLockUtil.withFileLockTimeout(fileName, (filePath) -> {
System.out.println(" [다중파일] " + fileName + " 락 획득");
try {
// 각 파일에 업데이트 타임스탬프 추가
String updateInfo = "업데이트 시간: " + System.currentTimeMillis() + "\\n";
if (Files.exists(filePath)) {
// 기존 파일에 추가
Files.write(filePath, updateInfo.getBytes(), StandardOpenOption.APPEND);
System.out.println(" [다중파일] " + fileName + " 기존 파일 업데이트");
} else {
// 새 파일 생성
Files.write(filePath, updateInfo.getBytes(), StandardOpenOption.CREATE);
System.out.println(" [다중파일] " + fileName + " 새 파일 생성");
}
// 짧은 대기 시간 (실제 업데이트 작업 시뮬레이션)
Thread.sleep(100);
return fileName + " 업데이트 완료";
} catch (IOException e) {
throw new RuntimeException(fileName + " 업데이트 실패", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(fileName + " 업데이트 중단", e);
}
}, 2, TimeUnit.SECONDS);
System.out.println(" 결과: " + result);
}
System.out.println(" 모든 파일 업데이트 완료!\\n");
} catch (IOException | HttpException e) {
System.err.println(" 다중 파일 업데이트 실패: " + e.getMessage());
}
}
}
ThreadUtil완전사용가이드
ThreadUtil 완전 사용 가이드
📚 ThreadUtil 클래스 구조
public class ThreadUtil {
private static final ExecutorService EXECUTOR_SERVICE =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 기본 메서드들
public static ExecutorService getExecutorService()
public static <T> Future<T> submit(Callable<T> task)
public static Future<?> submit(Runnable task)
// CompletableFuture 메서드들
public static <T> CompletableFuture<T> supplyAsync(Supplier<T> supplier)
public static CompletableFuture<Void> runAsync(Runnable runnable)
// 대기 및 수집 메서드들
public static <T> List<T> awaitAllAndCollect(List<CompletableFuture<T>> futures)
public static void awaitAll(CompletableFuture<?>... futures)
public static <T> T awaitAny(CompletableFuture<T>... futures)
// 리소스 관리
public static void shutdown()
}
🔧 주요 메서드 상세 설명
1. supplyAsync() - 값을 반환하는 비동기 작업
// 기본 사용법
CompletableFuture<String> future = ThreadUtil.supplyAsync(() -> {
// 시간이 걸리는 작업
return "결과";
});
// 실제 예시: 파일 읽기
CompletableFuture<List<String>> fileReadFuture = ThreadUtil.supplyAsync(() -> {
try {
return Files.readAllLines(Paths.get("data.txt"));
} catch (IOException e) {
throw new RuntimeException(e); // RuntimeException으로 래핑
}
});
// 결과 사용
List<String> lines = fileReadFuture.join(); // 완료까지 대기하고 결과 반환
2. runAsync() - 값을 반환하지 않는 비동기 작업
// 기본 사용법
CompletableFuture<Void> future = ThreadUtil.runAsync(() -> {
// 로깅, 파일 쓰기 등
System.out.println("백그라운드 작업 완료");
});
// 실제 예시: 로그 저장
CompletableFuture<Void> logFuture = ThreadUtil.runAsync(() -> {
try {
Files.write(Paths.get("log.txt"), "로그 메시지\\n".getBytes(),
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
logFuture.join(); // 완료까지 대기
3. awaitAll() - 여러 작업 완료 대기
// 여러 작업을 병렬 실행하고 모두 완료까지 대기
CompletableFuture<String> task1 = ThreadUtil.supplyAsync(() -> "작업1");
CompletableFuture<String> task2 = ThreadUtil.supplyAsync(() -> "작업2");
CompletableFuture<String> task3 = ThreadUtil.supplyAsync(() -> "작업3");
// 모든 작업 완료까지 대기
ThreadUtil.awaitAll(task1, task2, task3);
// 이제 모든 결과에 안전하게 접근 가능
System.out.println(task1.join() + ", " + task2.join() + ", " + task3.join());
4. awaitAllAndCollect() - 여러 작업 완료 대기 및 결과 수집
// List로 Future들을 관리할 때 사용
List<CompletableFuture<String>> futures = Arrays.asList(
ThreadUtil.supplyAsync(() -> "결과1"),
ThreadUtil.supplyAsync(() -> "결과2"),
ThreadUtil.supplyAsync(() -> "결과3")
);
// 모든 작업 완료 후 결과를 List로 수집
List<String> results = ThreadUtil.awaitAllAndCollect(futures);
// results = ["결과1", "결과2", "결과3"]
5. awaitAny() - 가장 먼저 완료되는 작업 대기
CompletableFuture<String> task1 = ThreadUtil.supplyAsync(() -> {
// 빠른 작업
return "빠른결과";
});
CompletableFuture<String> task2 = ThreadUtil.supplyAsync(() -> {
// 느린 작업
try { Thread.sleep(1000); } catch (InterruptedException e) {}
return "느린결과";
});
// 둘 중 먼저 완료되는 것의 결과 반환
String firstResult = ThreadUtil.awaitAny(task1, task2);
// firstResult = "빠른결과"
🎯 실전 사용 패턴
패턴 1: 동기 메서드를 비동기로 변환
// 기존 동기 메서드
public String syncMethod() throws IOException {
return Files.readString(Paths.get("file.txt"));
}
// ThreadUtil로 비동기화
CompletableFuture<String> asyncResult = ThreadUtil.supplyAsync(() -> {
try {
return syncMethod();
} catch (IOException e) {
throw new RuntimeException(e); // 예외 래핑
}
});
// 사용
try {
String result = asyncResult.join();
} catch (RuntimeException e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause(); // 원본 예외 복원
}
throw e;
}
패턴 2: 여러 독립 작업 병렬 실행
// 서비스 초기화 예시
CompletableFuture<DatabaseService> dbFuture = ThreadUtil.supplyAsync(() ->
new DatabaseService());
CompletableFuture<CacheService> cacheFuture = ThreadUtil.supplyAsync(() ->
new CacheService());
CompletableFuture<LogService> logFuture = ThreadUtil.supplyAsync(() ->
new LogService());
// 모든 서비스 초기화 완료 대기
ThreadUtil.awaitAll(dbFuture, cacheFuture, logFuture);
// 초기화된 서비스들 사용
DatabaseService db = dbFuture.join();
CacheService cache = cacheFuture.join();
LogService log = logFuture.join();
패턴 3: 순차적 비동기 체인
CompletableFuture<String> result = ThreadUtil.supplyAsync(() -> {
// 1단계: 데이터 로드
return loadData();
})
.thenCompose(data -> ThreadUtil.supplyAsync(() -> {
// 2단계: 데이터 처리
return processData(data);
}))
.thenCompose(processed -> ThreadUtil.supplyAsync(() -> {
// 3단계: 결과 저장
saveResult(processed);
return "완료";
}));
String finalResult = result.join();
패턴 4: 배치 처리
// 대량 데이터를 청크로 나누어 병렬 처리
List<String> largeDataList = Arrays.asList(/* 큰 데이터 */);
int chunkSize = 100;
List<CompletableFuture<List<String>>> futures = new ArrayList<>();
for (int i = 0; i < largeDataList.size(); i += chunkSize) {
final int start = i;
final int end = Math.min(i + chunkSize, largeDataList.size());
CompletableFuture<List<String>> chunkFuture = ThreadUtil.supplyAsync(() -> {
List<String> chunk = largeDataList.subList(start, end);
return processChunk(chunk); // 청크 처리
});
futures.add(chunkFuture);
}
// 모든 청크 처리 완료 후 결과 병합
List<List<String>> chunkResults = ThreadUtil.awaitAllAndCollect(futures);
List<String> finalResults = chunkResults.stream()
.flatMap(List::stream)
.collect(Collectors.toList());
패턴 5: 타임아웃 처리
CompletableFuture<String> result = ThreadUtil.supplyAsync(() -> {
// 시간이 걸릴 수 있는 작업
return longRunningTask();
});
try {
// 5초 타임아웃
String value = result.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("작업 시간 초과");
result.cancel(true); // 작업 취소
}
// 또는 orTimeout 사용 (Java 9+)
CompletableFuture<String> timeoutResult = ThreadUtil.supplyAsync(() -> {
return longRunningTask();
}).orTimeout(5, TimeUnit.SECONDS);
⚠️ 주의사항 및 Best Practices
1. 예외 처리
// 올바른 예외 처리 패턴
CompletableFuture<String> future = ThreadUtil.supplyAsync(() -> {
try {
return riskyOperation();
} catch (SpecificException e) {
throw new RuntimeException(e); // RuntimeException으로 래핑
}
});
// 사용할 때 원본 예외 복원
try {
String result = future.join();
} catch (RuntimeException e) {
if (e.getCause() instanceof SpecificException) {
throw (SpecificException) e.getCause();
}
throw e;
}
2. 리소스 관리
// 애플리케이션 종료 시 반드시 호출
ThreadUtil.shutdown();
// Spring Boot라면 @PreDestroy 사용
@PreDestroy
public void cleanup() {
ThreadUtil.shutdown();
}
3. 스레드 풀 크기 고려
// ThreadUtil은 CPU 코어 수만큼 스레드 풀 크기 설정
// I/O 집약적 작업이 많다면 별도 스레드 풀 고려 가능
// 너무 많은 동시 작업은 피하기
// CPU 코어 수 * 2 정도가 적절한 동시 작업 수
4. 데드락 방지
// 잘못된 예 - 데드락 위험
CompletableFuture<String> future1 = ThreadUtil.supplyAsync(() -> {
CompletableFuture<String> nested = ThreadUtil.supplyAsync(() -> "nested");
return nested.join(); // 데드락 위험
});
// 올바른 예 - 체이닝 사용
CompletableFuture<String> future2 = ThreadUtil.supplyAsync(() -> "first")
.thenCompose(result -> ThreadUtil.supplyAsync(() -> "second"));
🚀 성능 최적화 팁
1. 적절한 작업 분할
- 너무 작은 작업: 오버헤드 증가
- 너무 큰 작업: 병렬성 저하
- 적절한 크기: CPU 코어 수의 2-4배 정도의 작업 개수
2. I/O vs CPU 집약적 작업 구분
- I/O 집약적: 더 많은 동시 실행 가능
- CPU 집약적: CPU 코어 수 정도로 제한
3. 결과 캐싱
// 같은 작업 반복 시 캐싱 활용
private static final Map<String, CompletableFuture<String>> cache = new ConcurrentHashMap<>();
public CompletableFuture<String> getCachedResult(String key) {
return cache.computeIfAbsent(key, k ->
ThreadUtil.supplyAsync(() -> expensiveOperation(k))
);
}
이 가이드를 참고해서 ThreadUtil을 효과적으로 활용하세요!
'TIL(CS)' 카테고리의 다른 글
lombok (0) | 2025.09.03 |
---|---|
이미지 처리 (0) | 2025.09.03 |
TypeToken (0) | 2025.09.03 |
웹소켓이란? (feat.Spring,STOMP,SockJS) (1) | 2024.10.26 |
OOP의 SOLID 원칙 (2) | 2024.05.28 |