Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 2 additions & 10 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
steps:
# 코드 체크 아웃
- name: Checkout
- uses: actions/checkout@v4
uses: actions/checkout@v4
# JDK 17 설정
- name: Set up JDK 17
uses: actions/setup-java@v4
Expand Down Expand Up @@ -54,12 +54,4 @@ jobs:
key: ${{ secrets.GCP_SSH_PRIVATE_KEY }}
port: 22
script: |
pkill -f 'elephant-app.*.jar' || true

# 위에서 파일 복사한 디렉토리로 이동
cd ~/elephant-project/

# nohup으로 새로운 JAR 파일을 백그라운드에서 실행
# 실행 로그를 nohup.out 파일에 기록
nohup sudo java -jar elephant-app-*.jar > nohup.out 2>&1 &

~/deploy.sh
6 changes: 6 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
plugins {
id 'com.github.johnrengelman.shadow' version '7.1.2'
id 'application'
}

Expand All @@ -13,3 +14,8 @@ dependencies {
implementation 'ch.qos.logback:logback-classic:1.5.13'
}

shadowJar {
archiveBaseName = 'elephant-app'
archiveVersion = '0.0.1'
archiveClassifier = ''
}
16 changes: 15 additions & 1 deletion app/src/main/java/com/example/elephant/WebServer.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
package com.example.elephant;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class WebServer {
private static final Logger log = LoggerFactory.getLogger(WebServer.class);
private static final int DEFAULT_PORT = 8080;

public static void main(String[] args) {
int port = DEFAULT_PORT;
if (args.length > 0) {
try {
port = Integer.parseInt(args[0]);
} catch (NumberFormatException e) {
log.error("Invalid port number specified: '{}'. Using default port {}.", args[0], DEFAULT_PORT);
}
}
WebServerLauncher launcher = new WebServerLauncher();
launcher.start();
launcher.start(port);
}
}
138 changes: 105 additions & 33 deletions app/src/main/java/com/example/elephant/WebServerLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,31 +8,39 @@
import trunk.container.ContextConfig;
import trunk.container.StandardContext;

import java.io.File;
import java.net.URISyntaxException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Comparator;
import java.util.jar.JarFile;
import java.util.stream.Stream;

/**
* Elephant 웹 서버의 실제 부팅 로직을 담당하는 클래스입니다.
* 서버의 컴포넌트를 생성, 설정, 조립하고 생명주기를 관리합니다.
*/
public class WebServerLauncher {
private static final Logger log = LoggerFactory.getLogger(WebServerLauncher.class);
private static final int DEFAULT_PORT = 8080;
private static final String WEBAPP_RESOURCE_PATH = "/webapp";

private Connector connector;
private Path tempDocBase;

public void start() {
public void start(int port) {
try {
StandardContext context = configureContext();
ProtocolHandler handler = new Http11NioProtocol(DEFAULT_PORT);
ProtocolHandler handler = new Http11NioProtocol(port);

this.connector = new Connector(handler, context);
this.connector.init();
this.connector.start();

logAsciiArt();
log.info("Web Application Server started successfully on port {}.", DEFAULT_PORT);
log.info("Web Application Server started successfully on port {}.", port);

setupShutdownHook(context);

Expand All @@ -43,59 +51,123 @@ public void start() {
}
}

private StandardContext configureContext() throws URISyntaxException {
StandardContext context = new StandardContext();

// webapp 디렉토리의 실제 경로 찾아 Context의 dcosBase 설정
URL webappUrl = this.getClass().getClassLoader().getResource("webapp");
if (webappUrl == null) {
throw new IllegalStateException("Could not find webapp directory in classpath!");
/**
* 웹 애플리케이션 컨텍스트를 설정하고, web.xml 파일을 파싱하여 서블릿 정보를 등록합니다.
* 실행 환경을 감지하고, 웹 애플리케이션의 문서 루트 경로를 설정합니다.
* @return 설정된 {@link StandardContext} 인스턴스
* @throws Exception
*/
private StandardContext configureContext() throws Exception {
URL resourceUrl = getClass().getResource(WEBAPP_RESOURCE_PATH);
if (resourceUrl == null) {
throw new IllegalStateException("Cannot find webapp resource: " + WEBAPP_RESOURCE_PATH);
}
String webappPath = new File(webappUrl.toURI()).getAbsolutePath();
context.setDocBase(webappPath);

// web.xml 파일 경로 찾아 파싱
File webXmlFile = new File(webappPath, "WEB-INF/web.xml");
if (!webXmlFile.exists()) {
throw new IllegalStateException("Could not find 'web.xml' in 'webapp/WEB-INF/'.");
Path docBasePath;
String protocol = resourceUrl.getProtocol();

if ("jar".equals(protocol)) {
// JAR 환경: unpackWARs 전략 실행
log.info("Execution environment: JAR. Unpacking webapp resources...");
this.tempDocBase = unpackResourcesToTempDir(resourceUrl);
docBasePath = this.tempDocBase;
log.info("Webapp resources unpacked to temporary directory: {}", docBasePath);
} else if ("file".equals(protocol)) {
// 파일 시스템 환경: 직접 경로 사용
log.info("Execution environment: File System. Using direct path.");
docBasePath = Path.of(resourceUrl.toURI());
} else {
throw new IllegalStateException("Unsupported protocol for webapp resource: " + protocol);
}

StandardContext context = new StandardContext();
context.setDocBase(docBasePath.toAbsolutePath().toString());
// web.xml 파일 경로 찾아 파싱
ContextConfig contextConfig = new ContextConfig(context);
contextConfig.parseWebXml(webXmlFile.getAbsolutePath());
contextConfig.parseWebXml();

// 서블릿 즉시 로딩
context.loadOnStartup();

return context;
}

private Path unpackResourcesToTempDir(URL jarUrl) throws IOException {
Path tempDir = Files.createTempDirectory("elephant-webapp-");
JarURLConnection connection = (JarURLConnection) jarUrl.openConnection();
connection.setUseCaches(false);
try (JarFile jarFile = connection.getJarFile()) {
String prefix = connection.getEntryName() + "/";
jarFile.stream()
.filter(entry -> entry.getName().startsWith(prefix) && !entry.isDirectory())
.forEach(entry -> {
String relativePath = entry.getName().substring(prefix.length());
Path targetPath = tempDir.resolve(relativePath);
try {
Files.createDirectories(targetPath.getParent());
try (InputStream is = jarFile.getInputStream(entry)) {
Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new RuntimeException("Failed to unpack resource: " + entry.getName(), e);
}
});
}

return tempDir;
}

private void deleteDirectoryRecursively(Path path) throws IOException {
try (Stream<Path> walk = Files.walk(path)) {
walk.sorted(Comparator.reverseOrder())
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
log.error("Unable to delete path: {}", p, e);
}
});
}
}

/**
* 애플리케이션 종료 시 안전하게 서버 자원을 해제하기 위한 종료 Hook을 등록합니다.
*/
private void setupShutdownHook(StandardContext context) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("Shutting down the web application server...");
try {
log.info("Shutting down the web application server...");
context.destroyAll();
connector.stop();
} catch (Exception e) {
log.error("Error stopping Web Application Server", e);
} finally {
if (tempDocBase != null) {
try {
log.info("Deleting temporary docBase directory: {}", tempDocBase);
deleteDirectoryRecursively(tempDocBase);
log.info("Temporary directory deleted successfully.");
} catch (IOException e) {
log.error("Failed to delete temporary directory: {}", tempDocBase, e);
}
}
}
log.info("Server has been shut down gracefully.");
}));
}

private void logAsciiArt() {
log.info("""

_____ _ _ _ \s
| ___| | | | | | | \s
| |__ | | ___ _ __ | |__ __ _ _ __ | |_\s
| __| | | / _ \\ | '_ \\ | '_ \\ / _` | | '_ \\ | __|
| |___ | | | __/ | |_) | | | | | | (_| | | | | | | |_\s
\\____/ |_| \\___| | .__/ |_| |_| \\__,_| |_| |_| \\__|
| | \s
|_| \s
""");
System.out.println(
"""

_____ _ _ _ \s
| ___| | | | | | | \s
| |__ | | ___ _ __ | |__ __ _ _ __ | |_\s
| __| | | / _ \\ | '_ \\ | '_ \\ / _` | | '_ \\ | __|
| |___ | | | __/ | |_) | | | | | | (_| | | | | | | |_\s
\\____/ |_| \\___| | .__/ |_| |_| \\__,_| |_| |_| \\__|
| | \s
|_| \s
"""
);
}
}
137 changes: 137 additions & 0 deletions app/src/main/resources/webapp/boa-docs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Boa Documentation | Elephant Project</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;700&family=Noto+Sans+KR:wght@300;400;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Noto Sans KR', sans-serif; background-color: #0D1117; color: #C9D1D9; }
.font-mono { font-family: 'IBM Plex Mono', monospace; }
.docs-container { display: flex; }
.sidebar { width: 280px; flex-shrink: 0; }
.main-content { flex-grow: 1; }
.sidebar a {
display: block;
padding: 10px 16px;
color: #8B949E;
border-left: 3px solid transparent;
transition: all 0.2s ease;
}
.sidebar a:hover {
background-color: #161B22;
color: #C9D1D9;
}
.sidebar a.active {
color: #58A6FF;
border-left-color: #58A6FF;
font-weight: 700;
}
.content-section h2 {
font-size: 1.875rem;
font-weight: 700;
padding-bottom: 0.5rem;
border-bottom: 1px solid #30363D;
margin-bottom: 1.5rem;
margin-top: 2.5rem;
}
.content-section h3 {
font-size: 1.5rem;
font-weight: 700;
margin-top: 2rem;
margin-bottom: 1rem;
}
.code-block {
background-color: #161B22;
border: 1px solid #30363D;
border-radius: 6px;
padding: 1rem;
overflow-x: auto;
font-family: 'IBM Plex Mono', monospace;
font-size: 0.875rem;
}
.inline-code {
background-color: rgba(110, 118, 129, 0.4);
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
border-radius: 6px;
font-family: 'IBM Plex Mono', monospace;
}
</style>
</head>
<body class="antialiased">
<div class="docs-container max-w-screen-xl mx-auto">
<!-- 사이드바 -->
<aside class="sidebar h-screen sticky top-0 py-10 pr-4">
<h1 class="text-xl font-bold px-4 mb-4">Elephant Project</h1>
<nav>
<a href="index.html">소개 (Introduction)</a>
<a href="trunk-docs.html">Trunk (WAS)</a>
<a href="boa-docs.html" class="active">Boa (Framework)</a>
</nav>
</aside>

<!-- 메인 컨텐츠 -->
<main class="main-content py-10 px-4 md:px-8">
<div class="content-section">
<h1 class="text-5xl font-bold mb-4">Boa</h1>
<p class="text-xl text-gray-400">The Application Framework</p>
</div>

<div class="content-section">
<h2>개요 (Overview)</h2>
<p>Boa는 애플리케이션 개발의 복잡도를 낮추고, 객체지향적인 설계를 유도하기 위한 프레임워크 모듈입니다. Spring Framework의 핵심 철학인 IoC/DI, AOP와 MVC 모듈을 직접 구현하면서, '프레임워크는 왜 그렇게 설계되었는가'에 대한 근본적인 답을 찾고자 합니다. Boa는 Trunk 위에서 동작하며, 개발자가 비즈니스 로직에만 집중할 수 있는 환경을 제공합니다.</p>
</div>

<div class="content-section">
<h2>핵심 원리 (Core Principles)</h2>

<h3>IoC (Inversion of Control) & DI (Dependency Injection)</h3>
<p>객체의 생성과 생명주기 관리를 개발자가 아닌 프레임워크(컨테이너)가 담당하는 '제어의 역전' 원리를 구현합니다. 이를 통해 컴포넌트 간의 결합도를 낮추고 테스트하기 쉬운 코드를 작성할 수 있습니다.</p>
<ul class="list-disc list-inside mt-4 space-y-2">
<li><b>BeanFactory:</b> 빈(Bean)의 생성과 조립을 담당하는 IoC 컨테이너의 심장입니다.</li>
<li><b>BeanDefinition:</b> 빈의 메타데이터(클래스 타입, 스코프, 의존성 등)를 담고 있는 설계도입니다.</li>
<li><b>Annotation-based Configuration:</b> <span class="inline-code">@Component</span>, <span class="inline-code">@Autowired</span> 등의 어노테이션을 통해 자동으로 빈을 등록하고 의존성을 주입합니다.</li>
</ul>

<h3>AOP (Aspect-Oriented Programming)</h3>
<p>로깅, 트랜잭션, 보안 등 여러 객체에 공통적으로 나타나는 부가 기능(횡단 관심사)을 핵심 비즈니스 로직으로부터 분리하여 모듈화합니다.</p>
<ul class="list-disc list-inside mt-4 space-y-2">
<li><b>Dynamic Proxy:</b> JDK Dynamic Proxy와 CGLIB를 이용하여 런타임에 프록시 객체를 생성, 부가 기능을 주입합니다.</li>
<li><b>Advisor:</b> 하나의 어드바이스(Advice, 부가 기능)와 하나의 포인트컷(Pointcut, 적용 대상)을 묶은 모듈입니다.</li>
<li><b>Auto-proxying:</b> 빈 후처리기(BeanPostProcessor)를 통해 컨테이너의 빈들 중 포인트컷에 해당하는 빈을 자동으로 프록시 객체로 교체합니다.</li>
</ul>
</div>

<div class="content-section">
<h2>핵심 코드 예시 (Code Snippets)</h2>
<h3>BeanFactory의 빈 생성 로직 일부</h3>
<p>BeanFactory는 BeanDefinition을 기반으로 빈 인스턴스를 생성하고 의존성을 주입하는 복잡한 과정을 책임집니다.</p>
<div class="code-block mt-4">
<pre><code>
// 구현 중
</code></pre>
</div>
</div>
<!-- 푸터 섹션 -->
<footer class="text-center mt-16 md:mt-24 pt-8 border-t border-gray-700 fade-in fade-in-delay-4">
<p class="text-gray-400 mb-4">이 사이트는 코끼리 프로젝트 위에서 동작하며, 그 코드는 아래에서 확인하실 수 있습니다.</p>
<div class="flex justify-center space-x-6">
<a href="https://github.com/jungbin97/elephant-project" class="text-blue-400 hover:text-blue-300 transition-colors duration-300 font-mono text-lg">
GitHub Repository
</a>
<a href="https://ego2-1.tistory.com/" class="text-green-400 hover:text-green-300 transition-colors duration-300 font-mono text-lg">
Tech Blog
</a>
</div>
<p class="mt-8 text-sm text-gray-500">&copy; 2025. Elephant Project. Released under the <a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer" class="text-blue-400 hover:underline">MIT License</a>.</p>
</footer>
</main>

</div>
</body>
</html>
Binary file removed app/src/main/resources/webapp/favicon.ico
Binary file not shown.
Loading