From 71ed61a397f29c652703cffb2393c5b9c38528e5 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 26 Jun 2025 22:00:43 +0900 Subject: [PATCH 01/17] =?UTF-8?q?build:=20shadow=20plugin=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20fat-jar=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/build.gradle b/app/build.gradle index 0358188..e4b195b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ plugins { + id 'com.github.johnrengelman.shadow' version '7.1.2' id 'application' } @@ -13,3 +14,8 @@ dependencies { implementation 'ch.qos.logback:logback-classic:1.5.13' } +shadowJar { + archiveBaseName = 'elephant-app' + archiveVersion = '0.0.1' + archiveClassifier = '' +} \ No newline at end of file From 9a9a0981297420d2668d54756f3ea25556651792 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 26 Jun 2025 22:05:01 +0900 Subject: [PATCH 02/17] =?UTF-8?q?refactor:=20=EB=94=94=ED=8F=B4=ED=8A=B8?= =?UTF-8?q?=20=EA=B2=BD=EB=A1=9C=20=EC=84=A4=EC=A0=95(index.html)=20?= =?UTF-8?q?=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- trunk/src/main/java/trunk/servlet/DefaultServlet.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/trunk/src/main/java/trunk/servlet/DefaultServlet.java b/trunk/src/main/java/trunk/servlet/DefaultServlet.java index 6c1eb6b..a10cc0e 100644 --- a/trunk/src/main/java/trunk/servlet/DefaultServlet.java +++ b/trunk/src/main/java/trunk/servlet/DefaultServlet.java @@ -1,5 +1,7 @@ package trunk.servlet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import trunk.container.ServletContextAware; import trunk.container.StandardContext; import trunk.http11.request.HttpRequest; @@ -19,6 +21,7 @@ * @author jungbin97 */ public class DefaultServlet extends HttpServlet implements ServletContextAware { + private static final Logger log = LoggerFactory.getLogger(DefaultServlet.class); private StandardContext context; @Override @@ -29,8 +32,11 @@ public void setServletContext(StandardContext context) { @Override public void service(HttpRequest request, HttpResponse response) throws IOException { String requestPath = request.getStartLine().getRequestUri(); - String realPathStr = context.getRealPath(requestPath); + if ("/".equals(requestPath)) { + requestPath = "/index.html"; + } + String realPathStr = context.getRealPath(requestPath); if (realPathStr == null) { sendNotFound(response); return; From 131bf065f5316bfa6eece26b8e82bcbd0269c542 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 26 Jun 2025 22:06:23 +0900 Subject: [PATCH 03/17] =?UTF-8?q?refactor:=20=EC=9B=B9=20=EC=95=A0?= =?UTF-8?q?=ED=94=8C=EB=A6=AC=EC=BC=80=EC=9D=B4=EC=85=98=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8D=EC=8A=A4=ED=8A=B8=20=EC=84=A4=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20JAR=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EC=A7=80=EC=9B=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/elephant/WebServerLauncher.java | 107 +++++++++++++++--- .../java/trunk/container/StandardContext.java | 21 +++- 2 files changed, 105 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/example/elephant/WebServerLauncher.java b/app/src/main/java/com/example/elephant/WebServerLauncher.java index c67a3b8..2611468 100644 --- a/app/src/main/java/com/example/elephant/WebServerLauncher.java +++ b/app/src/main/java/com/example/elephant/WebServerLauncher.java @@ -8,9 +8,16 @@ 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 웹 서버의 실제 부팅 로직을 담당하는 클래스입니다. @@ -19,8 +26,10 @@ 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() { try { @@ -43,43 +52,105 @@ 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 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."); })); diff --git a/trunk/src/main/java/trunk/container/StandardContext.java b/trunk/src/main/java/trunk/container/StandardContext.java index 83c7b69..d5f0b45 100644 --- a/trunk/src/main/java/trunk/container/StandardContext.java +++ b/trunk/src/main/java/trunk/container/StandardContext.java @@ -4,7 +4,8 @@ import org.slf4j.LoggerFactory; import trunk.servlet.Servlet; -import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.*; /** @@ -100,19 +101,29 @@ public void setDocBase(String docBase) { this.docBase = docBase; } + + /** + * 이 컨텍스트의 문서 루트 디렉토리(docBase)를 반환합니다. + * @return 문서 루트 디렉토리의 경로, 또는 설정되지 않은 경우 null + */ + public String getDocBase() { + return docBase; + } + /** * 주어진 경로에 대한 실제 파일 시스템 경로를 반환합니다. *

* 이 메서드는 보안을 위해 상위 디렉토리로 이동하는 것을 방지할 수 있습니다. * * @param path 요청된 리소스의 경로 - * @return 해당 경로의 절대 파일 시스템 경로, 또는 docBase가 설정되지 않았거나 path가 null인 경우 null + * @return 해당 경로의 실제 파일 시스템 경로, 또는 유효하지 않은 경우 null */ public String getRealPath(String path) { - if (docBase == null || path == null) { + if (path.contains("..")) { return null; } - File file = new File(docBase, path); - return file.getAbsolutePath(); + Path resolvedPath = Paths.get(this.docBase, path).normalize(); + return resolvedPath.toString(); } + } From 07820935191313562d5879ab95ece0551cc559ce Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 26 Jun 2025 22:07:31 +0900 Subject: [PATCH 04/17] =?UTF-8?q?refactor:=20=EA=B0=9C=EC=84=A0=EB=90=9C?= =?UTF-8?q?=20web.xml=20=ED=8C=8C=EC=8B=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/trunk/container/ContextConfig.java | 110 ++++++++++++------ 1 file changed, 72 insertions(+), 38 deletions(-) diff --git a/trunk/src/main/java/trunk/container/ContextConfig.java b/trunk/src/main/java/trunk/container/ContextConfig.java index 02797f1..1f8fe68 100644 --- a/trunk/src/main/java/trunk/container/ContextConfig.java +++ b/trunk/src/main/java/trunk/container/ContextConfig.java @@ -1,5 +1,7 @@ package trunk.container; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; @@ -10,8 +12,12 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import java.io.File; +import java.io.FileInputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -26,6 +32,8 @@ * @see StandardContext */ public class ContextConfig { + private static final Logger log = LoggerFactory.getLogger(ContextConfig.class); + private static final String SERVLET = "servlet"; private static final String SERVLET_NAME = "servlet-name"; private static final String SERVLET_CLASS = "servlet-class"; @@ -33,69 +41,95 @@ public class ContextConfig { private static final String URL_PATTERN = "url-pattern"; private static final String SERVLET_MAPPING = "servlet-mapping"; - private final StandardContext standardContext; + private final StandardContext context; /** * 지정된 {@link StandardContext}를 설정하는 생성자. * 이 {@code ContextConfig}는 파싱 결과를 이 컨텍스트에 등록합니다. * - * @param standardContext 서블릿 정보를 등록할 컨텍스트 + * @param context 서블릿 정보를 등록할 컨텍스트 */ - public ContextConfig(StandardContext standardContext) { - this.standardContext = standardContext; + public ContextConfig(StandardContext context) { + this.context = context; } /** * 지정된 경로의 web.xml 파일을 파싱하고, 그 내용을 {@link StandardContext}에 적용합니다. * - * @param path web.xml 파일의 전체 경로 * @throws RuntimeException 파일 파싱 또는 서블릿 클래스 로딩 중 심각한 오류가 발생했을 경우 */ - public void parseWebXml(String path) { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + public void parseWebXml() { + Path webXmlPath = Paths.get(context.getDocBase(), "WEB-INF", "web.xml"); - try { + if (!Files.exists(webXmlPath)) { + log.info("web.xml not found at path: {}. Skipping servlet configuration.", webXmlPath); + return; + } + try (InputStream is = new FileInputStream(webXmlPath.toFile())) { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(new File(path)); + Document document = builder.parse(is); document.getDocumentElement().normalize(); - Map > servletNameToClass = new HashMap<>(); - NodeList servletNodes = document.getElementsByTagName(SERVLET); + // 태그들을 먼저 파싱하여 서블릿 이름과 클래스 정보를 매핑 + Map > servletInfoMap = parseServletDefinitions(document); + + // 태그들을 파싱하여 URL 패턴과 서블릿 클래스를 매핑 + mapServletUrls(document, servletInfoMap); + + } catch (ParserConfigurationException | SAXException | IOException | ClassNotFoundException e) { + throw new RuntimeException("Failed to parse web.xml", e); + } - for (int i = 0; i < servletNodes.getLength(); i++) { - Element servletElement = (Element) servletNodes.item(i); - String servletName = servletElement.getElementsByTagName(SERVLET_NAME).item(0).getTextContent(); - String servletClass = servletElement.getElementsByTagName(SERVLET_CLASS).item(0).getTextContent(); + } - // Default value lazy loading - String loadOnStartUp = "-1"; - NodeList loadOnStartupNode = servletElement.getElementsByTagName(LOAD_ON_STARTUP); - if (loadOnStartupNode.getLength() > 0) { - loadOnStartUp = loadOnStartupNode.item(0).getTextContent(); - } + private void mapServletUrls(Document document, Map> servletInfoMap) throws ClassNotFoundException { + NodeList mappingNodes = document.getElementsByTagName(SERVLET_MAPPING); - servletNameToClass.put(servletName, List.of(servletClass, loadOnStartUp)); - } + for (int i = 0; i < mappingNodes.getLength(); i++) { + Element mappingElement = (Element) mappingNodes.item(i); + String servletName = getElementTextContent(mappingElement, SERVLET_NAME); + String urlPattern = getElementTextContent(mappingElement, URL_PATTERN); - NodeList mappingNodes = document.getElementsByTagName(SERVLET_MAPPING); - for (int i = 0; i < mappingNodes.getLength(); i++) { - Element mappingElement = (Element) mappingNodes.item(i); - String servletName = mappingElement.getElementsByTagName(SERVLET_NAME).item(0).getTextContent(); - String urlPattern = mappingElement.getElementsByTagName(URL_PATTERN).item(0).getTextContent(); - String servletClass = servletNameToClass.get(servletName).get(0); - int loadOnStartUp = Integer.parseInt(servletNameToClass.get(servletName).get(1)); + List servletInfo = servletInfoMap.get(servletName); - Class clazz = Class.forName(servletClass); + String servletClass = servletInfo.get(0); + int loadOnStartup = Integer.parseInt(servletInfo.get(1)); - if (HttpServlet.class.isAssignableFrom(clazz)) { - Class servletClazz = clazz.asSubclass(Servlet.class); - standardContext.addChild(urlPattern, servletClazz, loadOnStartUp); - } + Class clazz = Class.forName(servletClass); + if (HttpServlet.class.isAssignableFrom(clazz)) { + Class servletClazz = clazz.asSubclass(Servlet.class); + context.addChild(urlPattern, servletClazz, loadOnStartup); } + } + } - } catch (ParserConfigurationException | SAXException | IOException | ClassNotFoundException e) { - throw new RuntimeException("Failed to parse web.xml", e); + private Map> parseServletDefinitions(Document document) { + Map> servletInfoMap = new HashMap<>(); + NodeList servletNodes = document.getElementsByTagName(SERVLET); + + for (int i = 0; i < servletNodes.getLength(); i++) { + Element servletElement = (Element) servletNodes.item(i); + String servletName = getElementTextContent(servletElement, SERVLET_NAME); + String servletClass = getElementTextContent(servletElement, SERVLET_CLASS); + String loadOnStartUp = getElementTextContent(servletElement, LOAD_ON_STARTUP, "-1"); + + servletInfoMap.put(servletName, List.of(servletClass, loadOnStartUp)); } + return servletInfoMap; + } + + private String getElementTextContent(Element element, String tagName) { + NodeList nodeList = element.getElementsByTagName(tagName); + if (nodeList.getLength() > 0) { + return nodeList.item(0).getTextContent().trim(); + } + return null; + } + + private String getElementTextContent(Element element, String tagName, String defaultValue) { + String content = getElementTextContent(element, tagName); + return (content == null || content.isEmpty()) ? defaultValue : content; } } From 7d8291de1a08f0e43cc5a3a296661740a994d450 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Fri, 27 Jun 2025 01:13:40 +0900 Subject: [PATCH 05/17] =?UTF-8?q?test:=20contextConfig=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../trunk/container/ContextConfigTest.java | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/trunk/src/test/java/trunk/container/ContextConfigTest.java b/trunk/src/test/java/trunk/container/ContextConfigTest.java index bd0a53c..2f8f9d6 100644 --- a/trunk/src/test/java/trunk/container/ContextConfigTest.java +++ b/trunk/src/test/java/trunk/container/ContextConfigTest.java @@ -1,5 +1,6 @@ package trunk.container; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -9,23 +10,36 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; class ContextConfigTest { @TempDir Path tempDir; + private StandardContext mockContext; + private Path webInfDir; + + @BeforeEach + void setUp() throws IOException { + mockContext = mock(StandardContext.class); + + when(mockContext.getDocBase()).thenReturn(tempDir.toString()); + + webInfDir = tempDir.resolve("WEB-INF"); + Files.createDirectories(webInfDir); + } + @Test @DisplayName("web.xml을 정상 파싱하여 서블릿을 등록한다.") void parseXmlSuccess() throws IOException { // given - File xmlFile = tempDir.resolve("web.xml").toFile(); + File xmlFile = webInfDir.resolve("web.xml").toFile(); try (FileWriter fileWriter = new FileWriter(xmlFile)) { fileWriter.write( """ @@ -43,11 +57,10 @@ void parseXmlSuccess() throws IOException { """); } - StandardContext mockContext = mock(StandardContext.class); ContextConfig contextConfig = new ContextConfig(mockContext); // when - contextConfig.parseWebXml(xmlFile.getPath()); + contextConfig.parseWebXml(); // then ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(String.class); @@ -65,7 +78,7 @@ void parseXmlSuccess() throws IOException { @DisplayName("web.xml에 정상적인 서블릿 클래스가 없을 경우 예외를 발생시킨다.") void parseXmlinavlidClass() throws Exception { // given - File xmlFile = tempDir.resolve("web.xml").toFile(); + File xmlFile = webInfDir.resolve("web.xml").toFile(); try (FileWriter fileWriter = new FileWriter(xmlFile)) { fileWriter.write( """ @@ -83,12 +96,24 @@ void parseXmlinavlidClass() throws Exception { """); } - StandardContext mockContext = mock(StandardContext.class); ContextConfig config = new ContextConfig(mockContext); // then - assertThatThrownBy(() -> config.parseWebXml(xmlFile.getPath())) + assertThatThrownBy(() -> config.parseWebXml()) .isInstanceOf(RuntimeException.class) .hasMessageContaining("Failed to parse web.xml"); } + + @Test + @DisplayName("web.xml 파일이 존재하지않아도 오류 없이 정상적으로 종료된다.") + void webXmlNotFound() { + // given + ContextConfig config = new ContextConfig(mockContext); + + // when + config.parseWebXml(); + + // then + verify(mockContext, never()).addChild(anyString(), any(), anyInt()); + } } From 4aa6e5a886d8edb12ecbc557cead199a19d28472 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 02:49:40 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20=EC=95=84=EC=8A=A4=ED=82=A4?= =?UTF-8?q?=20=EC=95=84=ED=8A=B8=20=EC=B6=9C=EB=A0=A5=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/elephant/WebServerLauncher.java | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/example/elephant/WebServerLauncher.java b/app/src/main/java/com/example/elephant/WebServerLauncher.java index 2611468..f586ce5 100644 --- a/app/src/main/java/com/example/elephant/WebServerLauncher.java +++ b/app/src/main/java/com/example/elephant/WebServerLauncher.java @@ -157,16 +157,18 @@ private void setupShutdownHook(StandardContext context) { } private void logAsciiArt() { - log.info(""" - - _____ _ _ _ \s - | ___| | | | | | | \s - | |__ | | ___ _ __ | |__ __ _ _ __ | |_\s - | __| | | / _ \\ | '_ \\ | '_ \\ / _` | | '_ \\ | __| - | |___ | | | __/ | |_) | | | | | | (_| | | | | | | |_\s - \\____/ |_| \\___| | .__/ |_| |_| \\__,_| |_| |_| \\__| - | | \s - |_| \s - """); + System.out.println( + """ + + _____ _ _ _ \s + | ___| | | | | | | \s + | |__ | | ___ _ __ | |__ __ _ _ __ | |_\s + | __| | | / _ \\ | '_ \\ | '_ \\ / _` | | '_ \\ | __| + | |___ | | | __/ | |_) | | | | | | (_| | | | | | | |_\s + \\____/ |_| \\___| | .__/ |_| |_| \\__,_| |_| |_| \\__| + | | \s + |_| \s + """ + ); } } From 9ce6b0a391b64e83dd1a438d44df3f5bf6f20726 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 02:50:43 +0900 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=EC=9B=B0=EC=BB=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=9E=84=EC=8B=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/resources/webapp/boa-docs.html | 137 ++++++ app/src/main/resources/webapp/favicon.ico | Bin 1150 -> 0 bytes app/src/main/resources/webapp/index.html | 389 ++++++++---------- app/src/main/resources/webapp/trunk-docs.html | 201 +++++++++ 4 files changed, 512 insertions(+), 215 deletions(-) create mode 100644 app/src/main/resources/webapp/boa-docs.html delete mode 100644 app/src/main/resources/webapp/favicon.ico create mode 100644 app/src/main/resources/webapp/trunk-docs.html diff --git a/app/src/main/resources/webapp/boa-docs.html b/app/src/main/resources/webapp/boa-docs.html new file mode 100644 index 0000000..2c417ff --- /dev/null +++ b/app/src/main/resources/webapp/boa-docs.html @@ -0,0 +1,137 @@ + + + + + + Boa Documentation | Elephant Project + + + + + + + +

+ + + + +
+
+

Boa

+

The Application Framework

+
+ +
+

개요 (Overview)

+

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

+
+ +
+

핵심 원리 (Core Principles)

+ +

IoC (Inversion of Control) & DI (Dependency Injection)

+

객체의 생성과 생명주기 관리를 개발자가 아닌 프레임워크(컨테이너)가 담당하는 '제어의 역전' 원리를 구현합니다. 이를 통해 컴포넌트 간의 결합도를 낮추고 테스트하기 쉬운 코드를 작성할 수 있습니다.

+
    +
  • BeanFactory: 빈(Bean)의 생성과 조립을 담당하는 IoC 컨테이너의 심장입니다.
  • +
  • BeanDefinition: 빈의 메타데이터(클래스 타입, 스코프, 의존성 등)를 담고 있는 설계도입니다.
  • +
  • Annotation-based Configuration: @Component, @Autowired 등의 어노테이션을 통해 자동으로 빈을 등록하고 의존성을 주입합니다.
  • +
+ +

AOP (Aspect-Oriented Programming)

+

로깅, 트랜잭션, 보안 등 여러 객체에 공통적으로 나타나는 부가 기능(횡단 관심사)을 핵심 비즈니스 로직으로부터 분리하여 모듈화합니다.

+
    +
  • Dynamic Proxy: JDK Dynamic Proxy와 CGLIB를 이용하여 런타임에 프록시 객체를 생성, 부가 기능을 주입합니다.
  • +
  • Advisor: 하나의 어드바이스(Advice, 부가 기능)와 하나의 포인트컷(Pointcut, 적용 대상)을 묶은 모듈입니다.
  • +
  • Auto-proxying: 빈 후처리기(BeanPostProcessor)를 통해 컨테이너의 빈들 중 포인트컷에 해당하는 빈을 자동으로 프록시 객체로 교체합니다.
  • +
+
+ +
+

핵심 코드 예시 (Code Snippets)

+

BeanFactory의 빈 생성 로직 일부

+

BeanFactory는 BeanDefinition을 기반으로 빈 인스턴스를 생성하고 의존성을 주입하는 복잡한 과정을 책임집니다.

+
+

+  // 구현 중
+
+
+
+ + +
+ +
+ + diff --git a/app/src/main/resources/webapp/favicon.ico b/app/src/main/resources/webapp/favicon.ico deleted file mode 100644 index 5cd1446a1258f1cde9a2e40388b8df9a68124dd2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1150 zcmeH{&r1SP5XWE8AK~V?HzUn3=;PWPqy8MyMSomnNMHkPOUV}$og@s+`yjl!e5MICguVAqGC3(0tf3wUM;}rnsBAIA-o#c;Cz@$9zxL` zt=`mD6YrfCWd~3i0!EMBh}FtiW_Pg2N5B+ - - - - - SLiPP Java Web Programming - - - - - - - - - + -
-
-
- -
-
-
- -
- -
+ +
+
+ + + + + + + +
-
-
+

+ Elephant Project +

+

추상화의 이면의 본질을 탐험하는 여정, A Deep Dive into WAS & Framework Internals.

+ - - + +
+

Project Philosophy

+

+ 우리는 이미 잘 구축된 거대한 시스템 위에서 개발하고 있습니다. 그러나 그 시스템을 블랙박스로 취급한 채 사용하는 것은, 문제가 발생했을 때 제대로 대응할 수 없다는 의미이기도 합니다. + 이 프로젝트는 그런 무지에서 벗어나, 직접 HTTP를 파싱하고, I/O 처리 및 스레드 풀을 구성하며, 서블릿 컨테이너와 MVC 프레임워크의 동작 원리를 구현합니다. + 시스템의 핵심 아키텍처가 어떻게 구성되고 작동하는지를, 코드 수준에서 정확히 이해하는 것이 목표입니다. +

+
+ + +
- - + +
+

Trunk

+

The Web Application Server

+

코끼리의 코(trunk)는 물건을 들어 올리고, 냄새를 맡고, 소통하며, 생존에 필요한 거의 모든 상호작용을 처리합니다. + Trunk는 HTTP 요청을 수신하고 응답을 전달하는 전 과정을 처리하는 Java 기반 웹 애플리케이션 서버입니다.

+
    +
  • + + NIO-based Event Loop & Thread Pool +
  • +
  • + + HTTP/1.1 Request/Response Parser +
  • +
  • + + Servlet Container & Lifecycle +
  • +
  • + + Session Management & Cookies +
  • +
- -
- - - - ---> + - - - - - + + +
+

Boa

+

The Application Framework

+

보아뱀이 먹이를 감싸듯, Boa는 객체들을 느슨하게 연결하면서도 강력하게 제어합니다. Spring의 core와 MVC 구조를 구현하여 'Spring이 왜 그렇게 설계되었는가'에 대해 탐구합니다.

+
    +
  • + + IoC Container & DI +
  • +
  • + + Annotation-based Bean Configuration +
  • +
  • + + AOP with Dynamic Proxy +
  • +
  • + + DispatcherServlet based MVC Structure +
  • +
+
+
+ + + + + + \ No newline at end of file diff --git a/app/src/main/resources/webapp/trunk-docs.html b/app/src/main/resources/webapp/trunk-docs.html new file mode 100644 index 0000000..4f04b4c --- /dev/null +++ b/app/src/main/resources/webapp/trunk-docs.html @@ -0,0 +1,201 @@ + + + + + + Trunk Documentation | Elephant Project + + + + + + + +
+ + + + +
+
+

Trunk

+

The Web Application Server

+
+ +
+

개요 (Overview)

+

Trunk는 Elephant Project의 가장 근간이 되는 웹 애플리케이션 서버(WAS) 모듈입니다. + 외부의 HTTP 요청을 받아 내부의 서블릿 컨테이너까지 전달하는 모든 과정을 순수 Java로 구현합니다. + 이는 Tomcat의 핵심 아키텍처 구조를 모방하여, 저수준 I/O 처리부터 서블릿 생명주기 관리에 대한 깊은 이해를 목표로 합니다.

+
+ +
+

아키텍처 (Architecture)

+

Trunk는 크게 요청을 받아들이고 연결을 관리하는 Connector와, 전달된 요청을 실제 비즈니스 로직(서블릿)으로 연결하는 Container 두 부분으로 나뉩니다.

+ +

Connector: The I/O Handler

+

Connector는 클라이언트와의 통신을 담당합니다. Java NIO(Non-blocking I/O)를 기반으로 하여 적은 수의 스레드로 다수의 동시 연결을 효율적으로 처리합니다.

+
+
+

Endpoint & Acceptor

+

+ 네트워크 연결을 수락하고 관리하는 최상위 컴포넌트입니다. Acceptor 스레드가 클라이언트의 새로운 연결 요청(accept)을 받아 소켓을 생성합니다. +

+
+
+

Poller

+

+ 다수의 소켓 채널을 Selector에 등록하고, I/O 이벤트를 감지하는 이벤트 루프를 실행합니다. +

+
+
+

SocketProcessor

+

+ 실제 I/O 작업(read/write)과 HTTP 파싱을 수행하며, 워커 스레드 풀에서 비동기적으로 실행됩니다. +

+
+
+ +

Container: The Servlet Engine

+

+ Container는 Connector로부터 전달받은 요청을 처리하는 서블릿 엔진입니다. + 서블릿 표준 명세를 구현하고 서블릿의 생명주기를 관리하며, URL 요청을 가장 적절한 서블릿으로 매핑하는 역할을 수행합니다. +

+
+
+

ContextConfig

+

+ 웹 애플리케이션의 배포 서술자인 webapp/WEB-INF/web.xml 파일을 파싱하여, 서블릿 정보를 읽어들이고 Context를 동적으로 설정하는 역할을 담당합니다. +

+
+
+

StandardContext

+

+ 하나의 웹 애플리케이션(webapp)에 해당되는 최상위 컨테이너입니다. web.xml에 명시된 load-on-startup값에 따라 서버 시작 시점에 서블릿을 미리 로딩(Eager Loading)하거나, 첫 요청 시 로딩(Lazy Loading)하는 전략을 관리합니다. +

+
+
+

StandardWrapper

+

+ 개별 서블릿 인스턴스를 감싸는 래퍼 객체입니다. 서블릿의 init(), service(), destroy() 메서드를 호출하며 생명주기를 직접 제어합니다. 또한, 서블릿이 ServletContextAware 인터페이스를 구현한 경우, Context 객체를 서블릿에 주입(DI)하여 컨테이너의 기능을 사용할 수 있도록 지원합니다. +

+
+
+

Mapper

+

+ 요청 URI와 서블릿을 매핑하는 컴포넌트입니다. 서블릿 표준 명세와 동일하게, 아래와 같은 우선순위 규칙에 따라 가장 적합한 서블릿을 찾아냅니다. +

    +
  • 정확한 경로 매칭 (Exact Match)
  • +
  • 가장 긴 접두사 매칭 (Prefix Match)
  • +
  • 확장자 매칭 (Extension Match)
  • +
  • 기본 매칭 (Default Match)
  • +
+
+
+
+ +
+

핵심 코드 예시 (Code Snippets)

+

Poller의 이벤트 루프

+

Poller는 `Selector`를 이용해 I/O 이벤트를 감지하는 핵심적인 역할을 수행합니다. 아래는 이벤트 루프의 일부입니다.

+
+

+public void run() {
+    while (true) {
+        try {
+            if (selector.select(1000) > 0) {
+                Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
+                while (keys.hasNext()) {
+                    SelectionKey key = keys.next();
+                    // ... 이벤트 타입에 따라 처리 로직 분기 ...
+                    if (key.isReadable()) {
+                        // 워커 스레드에 작업 위임
+                    }
+                    keys.remove();
+                }
+            }
+        } catch (IOException e) {
+            // ... 예외 처리 ...
+        }
+    }
+}
+
+
+
+ + +
+
+ + From 4e1392ef543494474dcc91cd48179c39f37d24af Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 03:17:49 +0900 Subject: [PATCH 08/17] =?UTF-8?q?feat:=20=EC=84=9C=EB=B2=84=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20=ED=8F=AC=ED=8A=B8=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/elephant/WebServer.java | 16 +++++++++++++++- .../com/example/elephant/WebServerLauncher.java | 7 +++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/example/elephant/WebServer.java b/app/src/main/java/com/example/elephant/WebServer.java index 229a0c0..331a09b 100644 --- a/app/src/main/java/com/example/elephant/WebServer.java +++ b/app/src/main/java/com/example/elephant/WebServer.java @@ -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); } } \ No newline at end of file diff --git a/app/src/main/java/com/example/elephant/WebServerLauncher.java b/app/src/main/java/com/example/elephant/WebServerLauncher.java index f586ce5..87d1acf 100644 --- a/app/src/main/java/com/example/elephant/WebServerLauncher.java +++ b/app/src/main/java/com/example/elephant/WebServerLauncher.java @@ -25,23 +25,22 @@ */ 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); From 0e277d70ae6c7dce80edbde5aba7f7f61b93bf38 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 03:18:51 +0900 Subject: [PATCH 09/17] =?UTF-8?q?fix:=20=EC=88=98=EC=A0=95=EB=90=9C=20JAR?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=EB=AA=85=EB=A0=B9=EC=96=B4=EC=97=90=20?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=20=EB=B2=88=ED=98=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 49f0c1e..dbdbb52 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -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 @@ -61,5 +61,5 @@ jobs: # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 # 실행 로그를 nohup.out 파일에 기록 - nohup sudo java -jar elephant-app-*.jar > nohup.out 2>&1 & + nohup sudo java -jar elephant-app-*.jar 80 > nohup.out 2>&1 & From a788a64f63e1c8bfc5a2c838c539dbbe6af06bf4 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 03:42:09 +0900 Subject: [PATCH 10/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index dbdbb52..8ec3ddc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -57,7 +57,7 @@ jobs: pkill -f 'elephant-app.*.jar' || true # 위에서 파일 복사한 디렉토리로 이동 - cd ~/elephant-project/ + cd ~/elephant-project/app/build/libs/ # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 # 실행 로그를 nohup.out 파일에 기록 From a0a47d3c1ddaeac2117333d4c853670165cb4d23 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 03:50:37 +0900 Subject: [PATCH 11/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 8ec3ddc..3eae185 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -61,5 +61,5 @@ jobs: # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 # 실행 로그를 nohup.out 파일에 기록 - nohup sudo java -jar elephant-app-*.jar 80 > nohup.out 2>&1 & + nohup java -jar elephant-app-*.jar 80 > nohup.out 2>&1 & From feef6c494efec5b32d57084fa7050158db00be08 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 04:01:54 +0900 Subject: [PATCH 12/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3eae185..a8eadee 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -61,5 +61,5 @@ jobs: # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 # 실행 로그를 nohup.out 파일에 기록 - nohup java -jar elephant-app-*.jar 80 > nohup.out 2>&1 & + nohup java -jar elephant-app-*.jar 8082 > nohup.out 2>&1 & From 31fb5519b398d3783f1faf71ba977bf83b9d6e57 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 04:04:39 +0900 Subject: [PATCH 13/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a8eadee..2cdf5f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,7 +54,7 @@ jobs: key: ${{ secrets.GCP_SSH_PRIVATE_KEY }} port: 22 script: | - pkill -f 'elephant-app.*.jar' || true + sudo pkill -f 'elephant-app.*.jar' || true # 위에서 파일 복사한 디렉토리로 이동 cd ~/elephant-project/app/build/libs/ From c312cf43a861c87ea8edfe1dde4c51d9314f6c4f Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 04:11:01 +0900 Subject: [PATCH 14/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2cdf5f4..19fdf2f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -62,4 +62,69 @@ jobs: # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 # 실행 로그를 nohup.out 파일에 기록 nohup java -jar elephant-app-*.jar 8082 > nohup.out 2>&1 & +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle + +name: Elephant Project CI/CD + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + # 코드 체크 아웃 + - name: Checkout + uses: actions/checkout@v4 + # JDK 17 설정 + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + # Gradle 권한 부여 + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + # Gradle shadowJar로 빌드 + - name: Build with Gradle Shadow + run: ./gradlew shadowJar + # SCP를 통해 GCP 인스턴스에 JAR 파일 전송 + - name: SCP to GCP + uses: appleboy/scp-action@master + with: + host: ${{secrets.GCP_HOST}} + username: ${{ secrets.GCP_USERNAME }} + key: ${{ secrets.GCP_SSH_PRIVATE_KEY }} + port: 22 + source: "app/build/libs/elephant-app-*.jar" + target: "~/elephant-project" + # GCP에서 배포 스크립트 실행 + - name: Deploy to GCP + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.GCP_HOST }} + username: ${{ secrets.GCP_USERNAME }} + key: ${{ secrets.GCP_SSH_PRIVATE_KEY }} + port: 22 + script: | + sudo pkill -f 'elephant-app.*.jar' || true + + # 위에서 파일 복사한 디렉토리로 이동 + cd ~/elephant-project/app/build/libs/ + + # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 + # 실행 로그를 nohup.out 파일에 기록 + nohup sudo java -jar elephant-app-*.jar 80 < /dev/null > nohup.out 2>&1 & + From d1aa8c72ee13f61ebb0db917d1e375e1e9f16f27 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 04:13:28 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2cdf5f4..2a23cdb 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -61,5 +61,5 @@ jobs: # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 # 실행 로그를 nohup.out 파일에 기록 - nohup java -jar elephant-app-*.jar 8082 > nohup.out 2>&1 & + nohup sudo java -jar elephant-app-*.jar 80 < /dev/null > nohup.out 2>&1 & From 09ab2156e1a2f95ec4a47da90845c8ad8c4b13ae Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 04:17:41 +0900 Subject: [PATCH 16/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2a23cdb..3915fe2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -60,6 +60,6 @@ jobs: cd ~/elephant-project/app/build/libs/ # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 - # 실행 로그를 nohup.out 파일에 기록 + # 실행 로그를 nohup.out 파일에 기록 nohup sudo java -jar elephant-app-*.jar 80 < /dev/null > nohup.out 2>&1 & From 6e0eeef734fcf8c428a1a29078f036fadc3544c4 Mon Sep 17 00:00:00 2001 From: jungbin97 Date: Thu, 10 Jul 2025 04:31:42 +0900 Subject: [PATCH 17/17] =?UTF-8?q?fix:=20=EC=8B=A4=ED=96=89=20=EC=8A=A4?= =?UTF-8?q?=ED=81=AC=EB=A6=BD=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3915fe2..9e6f579 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,12 +54,4 @@ jobs: key: ${{ secrets.GCP_SSH_PRIVATE_KEY }} port: 22 script: | - sudo pkill -f 'elephant-app.*.jar' || true - - # 위에서 파일 복사한 디렉토리로 이동 - cd ~/elephant-project/app/build/libs/ - - # nohup으로 새로운 JAR 파일을 백그라운드에서 실행 - # 실행 로그를 nohup.out 파일에 기록 - nohup sudo java -jar elephant-app-*.jar 80 < /dev/null > nohup.out 2>&1 & - + ~/deploy.sh