WASλ₯Ό μ§μ ꡬννλ©° ꡬ쑰μ μ±λ₯μ λΆμνκ³ κ°μ ν μμ§λμ΄λ§ μ€μ¬ νλ‘μ νΈ
λ³μνΉλ‘λ‘ νΈμ€ν νμ¬μμ μμ€ν μμ§λμ΄λ‘ 근무νλ©° μλ² νλμ¨μ΄λ μννΈμ¨μ΄λ₯Ό μ²μ μ νκ² λλ©΄μ μλ² κ°λ°μ κ΄μ¬μ΄ μκ²Όμ΅λλ€. μ 무 μ€ νλλ Apache, Nginx, Tomcatμ μ€μΉμ μ΄μμ νμμ΅λλ€. κ·Έλ¬λ μ΄λ¬ν μμ λ§μΌλ‘λ λμ μ리λ₯Ό μ΄ν΄νκΈ° μ΄λ ΅λ€κ³ λκΌκΈ°μ μ§μ WAS(Web Application Server)λ₯Ό Javaλ‘ κ΅¬ννκ³ μ§μμ μΌλ‘ ꡬ쑰μ μ±λ₯μ κ°μ ν΄μμ΅λλ€.
sequenceDiagram
actor Developer
participant WAS as WebAppServer
participant Srv as Server
participant WS as WebService
participant HPH as HttpProtocolHandler
participant AEP as AbstractEndpoint
participant ACC as Acceptor
participant TP as ThreadPoolExecutor
participant PROC as AbstractHttpProcessor
participant ADP as InternalAdapter
participant API as HttpApiHandler
actor Client
%% 1. Startup
Developer->>WAS: start()
WAS->>Srv: start()
Srv->>WS: init() β HTTP APIs
Srv->>HPH: init() β Endpoint
HPH->>AEP: bind & configure
AEP->>ACC: start() β acceptor
AEP->>TP: start() β executor
%% 2. Accept
Client->>ACC: TCP connect
ACC->>AEP: socket
AEP->>TP: submit task
%% 3. Request Processing
TP->>PROC: process(socket)
loop Persistent Connection
PROC->>PROC: parse request
PROC->>ADP: service(request, response)
ADP->>API: dispatch
API-->>ADP: result
ADP-->>PROC: commit
PROC->>PROC: serialize response
PROC->>Client: send response
PROC->>PROC: recycle
end
%% 4. Close
PROC->>Client: close
%% 5. Shutdown
Developer->>WAS: stop()
WAS->>Srv: stop()
Srv->>HPH: stop()
HPH->>AEP: stop()
AEP->>ACC: close acceptor
AEP->>TP: shutdown executor
Srv->>WS: destroy() β HTTP APIs
Srv->>HPH: destroy()
HPH->>AEP: unbind resources
classDiagram
direction TB
namespace lifecycle {
class Lifecycle {
<<interface>>
+init()
+start()
+stop()
+destroy()
}
class AbstractLifecycle {
<<abstract>>
-state
-lifecycles
#initInternal()*
#startInternal()*
#stopInternal()*
#destroyInternal()*
}
}
namespace bootstrap {
class WebAppServer {
+start()
+stop()
}
class Server
class WebService {
+addService(path, handler) WebService
}
}
namespace net {
class AbstractEndpoint {
<<abstract>>
#int port
#String hostName
#Executor executor
#Deque~AbstractSocketTask~ socketTaskPool
+setHandler(Handler)
#bind()*
#createSocketTask(AbstractSocketWrapper)* AbstractSocketTask
}
class BioEndpoint
class Acceptor
class AbstractSocketWrapper {
<<abstract>>
+read()
+write()
+close()
}
class BioSocketWrapper
class AbstractSocketTask {
<<abstract>>
+reset(AbstractSocketWrapper)
+run()
}
class BioSocketTask
}
namespace internal {
class HttpProtocolHandler
class AbstractHttpProcessor {
<<abstract>>
#Request request
#Response response
#Adapter adapter
+process(AbstractSocketWrapper) SocketState
}
class Http11Processor {
+service(AbstractSocketWrapper) SocketState
+recycle()
}
class internal_Request["internal.Request"]
class internal_Response["internal.Response"]
}
namespace connector {
class Adapter {
<<interface>>
+service(internal.Request, internal.Response)
}
class InternalAdapter {
-ensureRequestFacade(internal.Request)
-ensureResponseFacade(internal.Response)
}
class connector_Request["connector.Request"]
class connector_Response["connector.Response"]
}
namespace external {
class ExternalRequest {
<<interface>>
}
class ExternalResponse {
<<interface>>
}
}
namespace api_handler {
class HttpApiHandler {
<<interface>>
+init(WebServiceConfig)
+service(ExternalRequest, ExternalResponse)
+destroy()
}
class AbstractHttpApiHandler {
<<abstract>>
#doGet(req, res)
#doPost(req, res)
#doPut(req, res)
#doPatch(req, res)
#doDelete(req, res)
}
}
%% lifecycle
Lifecycle <|.. AbstractLifecycle
AbstractLifecycle <|-- Server
AbstractLifecycle <|-- WebService
AbstractLifecycle <|-- HttpProtocolHandler
AbstractLifecycle <|-- AbstractEndpoint
%% bootstrap
WebAppServer --> Server : creates & starts
Server --> WebService : manages lifecycle
Server --> HttpProtocolHandler : manages lifecycle
%% net
AbstractEndpoint <|-- BioEndpoint
AbstractSocketWrapper <|-- BioSocketWrapper
AbstractSocketTask <|-- BioSocketTask
BioEndpoint --> Acceptor : creates & runs
BioEndpoint --> BioSocketWrapper : wraps socket
BioEndpoint --> BioSocketTask : creates
AbstractSocketTask --> AbstractSocketWrapper : uses
%% internal
HttpProtocolHandler --> AbstractEndpoint : owns & manages lifecycle
AbstractHttpProcessor <|-- Http11Processor
Http11Processor --> InternalAdapter : delegates service
AbstractHttpProcessor --> internal_Request
AbstractHttpProcessor --> internal_Response
AbstractHttpProcessor --> AbstractSocketWrapper : reads/writes
%% connector
Adapter <|.. InternalAdapter
InternalAdapter --> WebService : routes by path
%% ν΅μ¬ κ³μΈ΅ μ°κ²°
InternalAdapter --> connector_Request : ensures facade
InternalAdapter --> connector_Response : ensures facade
%% external μ°κ²°
connector_Request ..|> ExternalRequest
connector_Response ..|> ExternalResponse
%% api
HttpApiHandler <|.. AbstractHttpApiHandler
WebService --> HttpApiHandler : dispatches
Dochi WASλ₯Ό κ°λ°νλ©΄μ λ°μν μ±λ₯ λ³λͺ©μ κ³μΈ‘νκ³ , μμΈμ λΆμν λ€ κ΅¬μ‘°μ μΌλ‘ κ°μ νμ΅λλ€. μ΄μ λν μμΈν μ€ν κ³Όμ , λ³λͺ© λΆμ, ꡬν λ΄μ©, μ±λ₯ λΉκ΅λ λ€μ μν€ λ¬Έμμμ νμΈν μ μμ΅λλ€.
μμΈ μν€ λ¬Έμ: Dochi WAS Wiki
- λ¬Έμ νμ: λΆν ν μ€νΈμ P99 Latency λκ³ μΌμ μ£ΌκΈ°λ‘ κΈμ¦/κΈκ° ν¨ν΄ λ°μ
- λ³λͺ© κ³μΈ‘: λ¬Έμ λ°μ μμ μ λ‘κ·Έλ‘ μμ μ΄ νμ μ£ΌκΈ°μ μΌλ‘ λ°λ Έλ€κ° νκΊΌλ²μ λΉμμ§λ κ²μ νμΈ
- μμΈ λΆμ: λμ μμ μκ° μ€λ λνμ ν¬κΈ°λ³΄λ€ μ»Έμλ μ€λ λ ν λ³λͺ© λ°μ
- λͺ¨λ μ€λ λκ° λμμ λΈλ‘νΉ λλ μκ°μ΄ λ°μ
- μ€λ λ νμ΄ μΌμμ μΌλ‘ λ©μΆ€
- κ·Έ μκ° λμ νμ μμ² μμ μ΄ κ³μ μμ¬μ P99 κΈμ¦
- μμ²μ΄ μμ λκ³ μ€λ λλ€μ΄ κΉ¨μ΄λλ©΄μ νκΊΌλ²μ μ²λ¦¬νμ¬ P99 κΈλ½
- ν΄κ²° λ°©μ: λμ μ μμ λΉλ‘νλ λμ μΌλ‘ νμ₯κ°λ₯ν μ컀 μ€λ λ ν νμ
- κ°μ μ±κ³Ό: P99 Latency κΈμ¦/κΈκ° ν¨ν΄ μνμ μ½ 4.81λ°° κ°μ λ° λ©λͺ¨λ¦¬ ν¨μ¨ν
- μμΈ κ³Όμ : 1. μ€λ λ ν μ¦κ° μ€ν κΈ°λ° νμ₯ μ€λ λ ν ꡬν
- λ¬Έμ νμ: μ컀 μ€λ λν λμ νμ₯κ³Ό Keep-Alive μ΅μ μ‘°μ ν μμ² μ²λ¦¬ λ‘μ§μμ λ³λͺ© λΆμ λ° λ°κ²¬
- λ³λͺ© κ³μΈ‘: μμ² ν€λ νμ± λ‘μ§μ΄ CPU μ¬μ©λμ λλ΅ 47% μ°¨μ§ + Minor GC λΆν
- μμΈ λΆμ: HTTP/1.1 μμ² ν€λλ₯Ό CRLF λ¨μλ‘ μ½κ³ λ¬Έμμ΄ λ³ν ν μ κ· ννμμΌλ‘ νμ±ν΄μ ν€λ μ μ₯
- ν΄κ²° λ°©μ: λΉ λ₯Έ λ°μ΄νΈ νμ±κ³Ό
Stringκ°μ²΄ μμ± μ΅μνλ₯Ό μν΄ Lazy Decodingμ΄ κ°λ₯ν ꡬ쑰 μ€κ³ λ° κ΅¬ν - κ°μ μ±κ³Ό: P99 Latency μ½ 2.21λ°° κ°μ
- μμΈ κ³Όμ‘:
- λ¬Έμ νμ: λμ μ μκ° λμ΄λ¨μ λ°λΌ Tail Latency μ¦κ°
- λ³λͺ© κ³μΈ‘: λμ μμ λΉλ‘νκ² μ컀 μ€λ λκ° λμ΄λλ P99 Latency μ¦κ°
- μμΈ λΆμ: 컀λ μ€λ λ μκ° λμ΄λ¨μ λ°λΌ 컨ν μ€νΈ μ€μμΉ λΉμ©λ μ¦κ°
- ν΄κ²° λ°©μ: λμ μ μ¦κ°μλ 컀λ μ€λ λ νμ νμ₯μμ΄ μμ²μ μ²λ¦¬
- κ°μ μ±κ³Ό: P99 Latency μ½ 6.95λ°° κ°μ
- μμΈ κ³Όμ :
- HTTP/1.1 μ§μ: HTTP/1.1 λ©μΈμ§ νμ±/μ§λ ¬ν , Keep-Alive νμμμ/μ΅λ κ°μ μ€μ , μμ² μ²λ¦¬ νμ΄νλΌμ΄λ,
- BIO κΈ°λ° λ€νΈμν¬ I/O:
java.net.Socketμ μ¬μ©ν Blocking I/O - **HTTP API λ±λ‘ λ° λΌμ°ν
**
- κ²½λ‘ λ³
HttpApiHandlerλ±λ‘ (/κΈ°λ³Έ νΈλ€λ¬ μλ λ±λ‘, λ―Έλ§€μΉ μ λ£¨νΈ νΈλ€λ¬ λ°ν) - GET, POST, PUT, PATCH, DELETE μ²λ¦¬ λ° λ―Έμ§μ λ©μλμ λν μλ¬ μλ΅(405/501)
- κ²½λ‘ λ³
- HTML Form λ°μ΄ν° νμ±
application/x-www-form-urlencodedνλΌλ©ν°λ‘ μ κ·Ό κ°λ₯multipart/form-data: κ° ννΈλ³λ‘ μ κ·Ό κ°λ₯, μμ νμΌ κ΄λ¦¬
- μ μ 리μμ€ μλΉ: λ£¨νΈ λλ ν°λ¦¬ λ΄ νμΌ νμ λ° μ½κΈ°, JAR νμΌ λ΄λΆ 리μμ€ νμ(Embedded JAR μ§μ)
- λ€μ΄λλ―Ή μ컀 μ€λ λν: μμ²λ μμΌ μμ μμ λ°λΌ μ§μ ν μ€λ λ ν μ΅μ/μ΅λ ν¬κΈ°λ§νΌ λμ μΌλ‘ νμ₯/μΆμ
- κ°μ μ€λ λ μ§μ: Java 21 μ΄μμμ
useVirtualThreadsμ΅μ μΌλ‘ νμ±ν- BIOλ‘ μΈν 컀λ μ€λ λμ λΈλ‘νΉμ νΌνμ¬ NIO λͺ¨λΈ κΈ°λ°μ WAS μ±λ₯ λλ¬
- λ¨, μ컀 μ€λ λ λ΄λΆ λ‘μ§μ
synchronizedμ¬μ©μ μ±λ₯ μ ν λ°μ
νμ¬ Maven/Gradle μ€μ μ μ₯μμ λ°°ν¬λμ΄ μμ§ μμΌλ―λ‘, λ‘컬 λΉλ ν μμ‘΄μ±μΌλ‘ μΆκ°νκ±°λ μμ€λ₯Ό μ§μ ν¬ν¨νμ¬ μ¬μ©ν©λλ€.
λ€μμ λΉλ μμ€ν μ λ°λ₯Έ μμ‘΄μ± μ€μ μμμ λλ€.
dochi.jar νμΌμ νλ‘μ νΈμ νΉμ κ²½λ‘μ μ μ₯ν©λλ€.
project/
ββ build.gradle
ββ settings.gradle
ββ libs/
β ββ dochi.jar
ββ src/build.gradleμ dochi.jar νμΌμ μ μ₯ν κ²½λ‘μ λν΄ μμ‘΄μ±μ μ€μ νλ€.
dependencies {
implementation files('libs/dochi.jar')
}- File
- Project Structure
- Libraries
- Add
dochi.jar
import org.dochi.webserver.bootstrap.WebAppServer;
import org.dochi.webserver.lifecycle.LifecycleException;
public class WebAppServerLauncher {
public static void main(String[] args) throws LifecycleException {
WebAppServer was = new WebAppServer(8080);
was.start();
}
}κΈ°λ³Έ μ€μ μΌλ‘ 8080 ν¬νΈμμ μλ²κ° μμλλ©°, webapp/ λλ ν°λ¦¬μ μ μ 리μμ€λ₯Ό μλΉν©λλ€.
κ° WebAppServer μΈμ€ν΄μ€λ ν¬νΈμ νΈμ€νΈ λ€μμΌλ‘ λ
립μ μΈ μλ²λ₯Ό λνλ
λλ€.
- ν¬νΈμ νΈμ€νΈ λ€μμ΄ λͺ¨λ κ°μ κ²½μ°μ
LifecycleExceptionμ΄ λ°μν©λλ€. - JVM μ’
λ£μ
ShutdownTasksManagerκ° μ€ννλ μμλλ‘ κ° μλ² μΈμ€ν°μ€λ₯Ό μμ νκ² μ’ λ£ν©λλ€. - μ€λ λνμ μΈμ€ν΄μ€λ§λ€ λ 립μ μΌλ‘ μμ±λλ―λ‘, μΈμ€ν΄μ€ μμ μ΅λ μ€λ λ μλ₯Ό ν¨κ» κ³ λ €νμ¬ μμ€ν 리μμ€λ₯Ό μ€κ³ν΄μΌ ν©λλ€.
public class WebAppServerLauncher {
public static void main(String[] args) throws LifecycleException {
// 8080 ν¬νΈμ λ°μΈλ©λ λ
립 μλ² μΈμ€ν΄μ€
WebAppServer remoteServer = new WebAppServer(8080, "0.0.0.0");
remoteServer.getWebService().setWebResourceRootPath("webapp");
remoteServer.getWebService()
.addService("/api/user", new UserApiHandler())
.addService("/api/post", new PostApiHandler());
remoteServer.getThreadPool().setMinSpareThreads(200);
remoteServer.getThreadPool().setMaxThreads(2000);
// 9090 ν¬νΈμ λ°μΈλ©λ λ
립 μλ² μΈμ€ν΄μ€
WebAppServer localServer = new WebAppServer(9090, "localhost");
// κ° μΈμ€ν΄μ€ λ
립μ μΌλ‘ μμ (ν¬νΈκ° λ¬λΌμΌ ν¨)
remoteServer.start();
localServer.start();
}
}AbstractHttpApiHandlerλ₯Ό μμνμ¬ μνλ HTTP λ©μλλ₯Ό μ€λ²λΌμ΄λν©λλ€.
import org.dochi.api.handler.AbstractHttpApiHandler;
import org.dochi.external.ExternalRequest;
import org.dochi.external.ExternalResponse;
import org.dochi.http.utils.HttpStatus;
import java.io.IOException;
public class UserApiHandler extends AbstractHttpApiHandler {
@Override
protected void doGet(ExternalRequest request, ExternalResponse response) throws IOException {
String userId = request.getParameter("userId");
// ...
response.send("userId=" + userId, "text/plain; charset=utf-8");
}
@Override
protected void doPost(ExternalRequest request, ExternalResponse response) throws IOException {
// Content-Type: application/x-www-form-urlencoded μλ νμ±
String name = request.getParameter("name");
String email = request.getParameter("email");
// ...
response.setStatus(HttpStatus.CREATED).send();
}
}WebAppServer server = new WebAppServer(8080);
server.getWebService()
.addService("/api/user", new UserApiHandler())
.addService("/api/post", new PostApiHandler());
server.start();/κ²½λ‘μλ κΈ°λ³Έμ μΌλ‘DefaultHttpApiHandler(μ μ 리μμ€ μλΉ)κ° λ±λ‘λμ΄ μμ΅λλ€.- κ²½λ‘κ° μΌμΉνλ νΈλ€λ¬κ° μμΌλ©΄
/νΈλ€λ¬λ‘ ν΄λ°±λ©λλ€.
WebAppServer κ°μ²΄λ₯Ό ν΅ν΄ ν¬νΈ, μ€λ λν, μμΌ, HTTP μ νκ° λ±μ μ€μ ν μ μμ΅λλ€.
WebAppServer server = new WebAppServer(8080, "localhost");
// μ컀 μ€λ λν μ€μ
server.getThreadPool().setMinSpareThreads(100);
server.getThreadPool().setMaxThreads(1000);
server.getThreadPool().setUseVirtualThreads(false); // Java 21+μμ trueλ‘ μ€μ κ°λ₯
// μμΌ μ€μ
server.getSocket().setKeepAliveTimeout(5000); // ms λ¨μ
server.getSocket().setMaxKeepAliveRequests(100);
// HTTP μμ² λ©μΈμ§ ν¬κΈ° μ ν
server.getHttp().getReqConfig().setRequestHeaderMaxSize(16 * 1024); // 16KB
server.getHttp().getReqConfig().setRequestPayloadMaxSize(10 * 1024 * 1024); // 10MB
// HTTP μλ΅ λ©μΈμ§ ν¬κΈ° μ ν
server.getHttp().getResConfig().setResponseHeaderMaxSize(8 * 1024); // 8KB
server.getHttp().getResConfig().setResponseBodyMaxSize(4 * 1024 * 1024); // 4MB
// μ μ 리μμ€ λ£¨νΈ λλ ν°λ¦¬ λ³κ²½ (κΈ°λ³Έκ°: "webapp")
server.getWebService().setWebResourceRootPath("static");
server.start();| νλͺ© | κΈ°λ³Έκ° |
|---|---|
| ν¬νΈ | 8080 |
| νΈμ€νΈ | localhost |
| μ΅μ μ€λ λ μ | 500 |
| μ΅λ μ€λ λ μ | 3000 |
| κ°μ μ€λ λ μ¬μ© | false |
| Keep-Alive νμμμ | 5000ms |
| μ΅λ Keep-Alive μμ² μ | 50 |
| μμ² ν€λ μ΅λ ν¬κΈ° | 8KB |
| μμ² λ°λ μ΅λ ν¬κΈ° | 2MB |
| μλ΅ ν€λ μ΅λ ν¬κΈ° | 8KB |
| μλ΅ λ°λ μ΅λ ν¬κΈ° | 2MB |
| μ μ 리μμ€ λ£¨νΈ λλ ν°λ¦¬ | webapp |
ExternalRequest μΈν°νμ΄μ€λ₯Ό ν΅ν΄ HTTP μμ² λ°μ΄ν°μ μ κ·Όν©λλ€.
// κΈ°λ³Έ λ©νλ°μ΄ν°
String method = request.getMethod(); // "GET", "POST", ...
String uri = request.getRequestURI(); // "/api/user?id=1"
String path = request.getPath(); // "/api/user"
String query = request.getQueryString(); // "id=1"
String protocol = request.getProtocol(); // "HTTP/1.1"
String contentType = request.getContentType(); // "application/json"
int contentLength = request.getContentLength();
// ν€λ μ‘°ν
String host = request.getHeader("Host");
String auth = request.getHeader("Authorization");
// νλΌλ―Έν° μ‘°ν (쿼리μ€νΈλ§ + application/x-www-form-urlencoded μλ νμ±)
String userId = request.getParameter("userId");
// μ
λ ₯ μ€νΈλ¦Ό μ§μ μ κ·Ό
InputStream in = request.getInputStream();ExternalResponse μΈν°νμ΄μ€λ₯Ό ν΅ν΄ HTTP μλ΅μ ꡬμ±ν©λλ€.
λ©μλ 체μ΄λμ μ§μνλ©°, send() λλ sendError() νΈμΆ μμ μ μλ΅μ΄ 컀λ°λ©λλ€.
// ν
μ€νΈ μλ΅
response.send("Hello, World!", "text/plain; charset=utf-8");
// μνμ½λμ ν€λλ₯Ό μ§μ μ€μ
response
.setStatus(HttpStatus.CREATED)
.setHeader("X-Custom-Header", "value")
.setCookie("sessionId=abc123; HttpOnly")
.send(responseBody, "application/json");
// λΉ μλ΅ (ν€λλ§ μ μ‘)
response.setStatus(HttpStatus.NO_CONTENT).send();
// μλ¬ μλ΅
response.sendError(HttpStatus.NOT_FOUND);
response.sendError(HttpStatus.BAD_REQUEST, "μλͺ»λ μμ²μ
λλ€.");
// μ€νΈλ¦¬λ° μλ΅ (OutputStream μ§μ μ¬μ©)
OutputStream out = response.getOutputStream();
out.write(data);κΈ°λ³Έμ μΌλ‘ νλ‘μ νΈ λ£¨νΈμ webapp/ λλ ν°λ¦¬μμ μ μ νμΌμ μλΉν©λλ€.
project-root/
βββ webapp/
βββ index.html -> GET / μμ² μ λ°ν
βββ style.css
βββ images/
βββ logo.png/κ²½λ‘ μμ² μ μλμΌλ‘index.htmlμ λ°νν©λλ€.- μ€ν κ°λ₯ν JARλ‘ ν¨ν€μ§λ κ²½μ°, JAR λ΄λΆμ 리μμ€λ μλμΌλ‘ νμν©λλ€.
- μ§μνλ MIME νμ
:
text/html,text/css,text/plain,application/javascript,application/json,application/xml,image/png,image/jpeg,image/x-iconλ±
multipart/form-data μμ²μμ getPart() λ©μλλ‘ κ° ννΈ(νλ λλ νμΌ)μ μ κ·Όν©λλ€.
@Override
protected void doPost(ExternalRequest request, ExternalResponse response) throws IOException {
// μΌλ° ν
μ€νΈ νλ
Part namePart = request.getPart("name");
String name = new String(namePart.getContent());
// νμΌ μ
λ‘λ
Part filePart = request.getPart("profile");
if (filePart.isFile()) {
String fileName = filePart.getFileName();
String contentType = filePart.getContentType();
byte[] fileData = filePart.getContent();
// νμΌ μ²λ¦¬ ...
}
response.setStatus(HttpStatus.CREATED).send();
}μ
λ‘λλ νμΌμ λ΄λΆμ μΌλ‘ multipart-tmp-file/ λλ ν°λ¦¬μ νμΌ μ΄λ¦ μ¦λ³΅μ λ§κΈ° μν΄ UUID κΈ°λ°μΌλ‘ μμ μ μ₯λλ©°, μμ² μ²λ¦¬κ° μλ£λ ν μλμΌλ‘ μμ λ©λλ€.
μ΄ νλ‘μ νΈλ νμ΅κ³Ό μ€ν λͺ©μ μΌλ‘ μ μλμμ΅λλ€.