
1996년 1월, 자바 1.0 버전이 등장했습니다. 최초의 자바 1.0 버전에서는 Thread 클래스와 Runnable 인터페이스, 그리고 synchronized, wait(), notify() 같은 동기화 메커니즘은 이 시점부터 이미 언어에 포함되어 있었습니다. 이는 자바가 단순한 절차적 언어를 넘어, 처음부터 멀티스레드 기반의 동시성 프로그래밍을 염두에 두고 설계되었음을 알 수 있었습니다. 이러한 설계 방향은 자바가 GUI 및 네트워크 프로그래밍을 주력으로 하던 시절의 요구사항에서 비롯되었습니다. 사용자 이벤트 처리, 네트워크 통신, 타이머 이벤트 등을 동시에 처리하려면 멀티스레딩은 필수 요소였기 때문이었습니다.
💡참고
동시성 프로그래밍은 여러 작업이나 요청을 동시에 처리하여 프로그램의 효율성을 높이고 사용자 경험을 개선하는 프로그래밍 기법입니다. 단 하나의 CPU 코어를 사용하는 시스템에서, 하나의 프로세스가 수행되는 동안 다른 작업을 수행하기 위해 CPU 스케줄링을 통해 다른 작업으로 전환하면서 동시에 여러 작업을 처리하는 것처럼 보이게 하는 것을 의미합니다.


당시 많은 언어들은 OS의 스레드 라이브러리에 의존하거나 외부 확장을 통해 동시성을 구현했지만, 자바는 동시성 기능을 언어 차원에 내장하여 플랫폼 독립성과 병행 처리의 안전성을 동시에 확보하려고 했습니다. 자바 1.0에서 동시성 프로그래밍을 위해 구현된 Thread 클래스는 독립 실행 흐름을 표현하는 기본 단위였고, Runnable 인터페이스는 실행 로직의 캡슐화를 가능하게 하며, 다양한 실행 방식과의 분리를 도왔습니다. synchronized 키워드는 객체 단위의 락을 통해 임계 구역의 안전한 접근을 보장했으며, wait()와 notify()는 스레드 간 협력을 위한 동기화 도구로 사용되었습니다.
이러한 기능들은 JVM 내부에서 운영체제의 스케줄링 기능과 연동되도록 설계되었으며, 개발자는 OS나 하드웨어 플랫폼에 관계없이 일관된 멀티스레드 프로그래밍 모델을 활용할 수 있었습니다.
💡참고
JVM(Java Virtual Machine)은 운영체제 위에서 실행되는 단일 프로세스이며, 하나의 사용자 프로세스로 동작합니다. 예를 들어, 실행할 클래스 이름을 MyApp으로 지정하고 java MyApp 명령어를 입력하면, 운영체제는 JVM을 하나의 프로세스로 인식하고 실행을 시작합니다. java, java.exe, javaw.exe 등은 모두 동일한 JVM 실행 도구입니다.
자바에서 Thread 클래스를 이용해 새로운 스레드를 생성하고 start()를 호출하면, JVM은 단순히 자바 객체만 생성하는 것이 아닙니다. 실제로는 운영체제에 스레드 생성을 요청하고, 운영체제는 커널 수준에서 실행 가능한 네이티브 스레드(native thread)를 하나 만들어 냅니다. 이 스레드는 JVM이 관리하는 자바 Thread 객체와 1:1로 매핑되며, 해당 자바 스레드는 곧 운영체제 스케줄러의 제어 대상이 됩니다. 이 방식은 흔히 1:1 스레드 모델(one-to-one thread model)이라고 합니다.
예를 들어, 자바 코드에서 Thread.start()를 호출하면, JVM은 내부적으로 리눅스에서는 pthread_create(), 윈도우에서는 CreateThread()와 같은 운영체제의 API를 호출합니다. 이로 인해 JVM 내부 스레드는 운영체제에서 커널 스레드로 등록되며, 운영체제는 해당 스레드의 실행 우선순위, 타임슬라이스, 인터럽트 등을 직접 제어합니다.
따라서 자바 개발자는 단지 Thread.sleep(), join(), yield() 같은 API만 사용하더라도, 실제로는 운영체제의 스케줄러가 자바 스레드의 상태(READY, BLOCKED 등)를 판단하고 CPU에 할당하는 방식으로 실행을 조율하게 됩니다. JVM은 스레드 실행의 세부적인 시점을 제어하지 않고, 대신 스레드 생성, 종료, 동기화, 인터럽트 처리 같은 추상화 계층만 담당하며, 실질적인 실행 순서는 운영체제가 결정합니다.
결국, 자바의 스레드 시스템은 JVM 내부에서 시작되지만, 그 실행은 운영체제의 커널 스레드를 기반으로 하고 있으며, 자바 개발자는 어떤 플랫폼에서든 일관된 동시성 프로그래밍 모델을 사용할 수 있도록 설계되어 있습니다. 이것이 바로 "JVM은 운영체제의 스케줄링 기능과 연동되도록 설계되었다"는 표현의 정확한 의미입니다.
좀 더 구체적으로 살펴보자면, 자바의 동시성 기능은 Thread와 같은 스레드 실행 API뿐 아니라, synchronized, volatile, Lock, ExecutorService 등 다양한 수준의 추상화 도구를 포함하고 있습니다. 이 기능들은 JVM 내부에서 처리되는 것처럼 보이지만, 실제로는 운영체제나 CPU의 하드웨어 명령과 밀접하게 연동되도록 설계되어 있습니다.
예를 들어, 자바의 Thread 클래스는 개발자가 코드 상에서 스레드를 생성하는 추상적 도구이지만, 내부적으로는 운영체제의 네이티브 스레드(native thread, 커널 스레드)에 직접 연결됩니다. JVM은 Thread.start()를 호출할 때 운영체제에 스레드를 생성해달라고 요청하며, 실제 스케줄링은 OS 커널이 담당하게 됩니다. 이와 같은 구조는 ExecutorService를 사용할 때도 동일하게 적용되며, 내부적으로는 ThreadPoolExecutor를 통해 OS 스레드를 재사용하는 구조로 구성됩니다.
반면, synchronized는 JVM에서 제공하는 모니터 기반 동기화 도구입니다. 컴파일 시 monitorenter, monitorexit 바이트코드로 변환되어, JVM 내부에서 락을 관리하며 필요 시 CPU의 메모리 배리어(fence)를 삽입해 메모리 일관성을 유지합니다. 락 충돌이 발생할 경우에는 JVM이 OS mutex(POSIX mutex 등)를 활용하는 경우도 있습니다.
volatile은 락을 사용하지 않으면서도 메모리 가시성(visibility)을 보장하는 키워드로, 변수의 읽기/쓰기 시점에 JVM이 CPU의 특정 메모리 배리어 명령어(ex. LOCK prefix, mfence)를 삽입합니다. 이로 인해 CPU 캐시 간의 일관성이 유지되며, 다른 스레드에서 최신 값을 읽을 수 있게 됩니다.
자바의 Lock 인터페이스와 ReentrantLock 클래스는 synchronized보다 유연한 락 기능을 제공하며, AbstractQueuedSynchronizer(AQS)라는 내부 구조를 기반으로 동작합니다. AQS는 대기 중인 스레드를 큐로 관리하고, CAS(Compare-And-Swap) 기반 스핀락을 통해 경쟁을 조절합니다. 상황에 따라 스레드가 park() 상태에 진입하면, 이는 JVM을 통해 OS의 futex(리눅스)나 WaitForSingleObject()(윈도우) 같은 커널 락 메커니즘과 연결되기도 합니다.
이처럼 자바의 동시성 기능은 단순히 자바 코드로 끝나는 것이 아니라, JVM을 매개로 하여 운영체제와 CPU 자원에 효과적으로 연동되도록 정교하게 설계되어 있습니다.
| 기능 | 연동 대상 | 설명 |
|---|---|---|
Thread | OS 커널 스레드 | 자바 스레드는 native thread로 매핑되어 OS가 실행 및 스케줄링 담당 |
ExecutorService | JVM 내 ThreadPool + OS 스레드 | 내부적으로 Thread를 사용하므로 결국 OS native thread 사용. 스케줄링은 JVM 내부 큐 기반 |
synchronized | JVM 모니터 + CPU memory barrier | JVM이 모니터(lock word) 관리, 필요 시 OS mutex 활용, CPU에 store/load barrier 삽입 |
volatile | CPU 캐시 + 메모리 배리어 | 변수 읽기/쓰기 시 JVM이 CPU 메모리 배리어(mfence 등) 삽입. 가시성 보장 |
Lock, ReentrantLock | JVM AQS + OS mutex (경우에 따라) | JVM 수준의 AQS 기반 큐/스핀락. 상황에 따라 park/unpark로 OS 커널 락 호출(futex 등) |
Thread, ExecutorService) → OS와 직접 연동volatile, synchronized) → JVM + CPU 배리어로 연동Lock, ReentrantLock) → JVM 구조 기반, 일부는 OS 락과 연동자바 1.0이 등장하던 1990년대 중반, 대부분의 개인용 컴퓨터와 워크스테이션은 단일 코어 CPU를 사용하고 있었습니다. 대표적으로 사용되던 인텔 CPU는 아래와 같습니다.


| CPU | 출시 연도 | 클럭 속도 | 코어 수 | 하드웨어 스레드 | L1 캐시 | L2 캐시 |
|---|---|---|---|---|---|---|
| Intel 80486DX4 | 1994 | 100 MHz | 1 | 1 | 8 KB | 외장형 (최대 256 KB) |
| Intel Pentium | 1993~1996 | 60~200 MHz | 1 | 1 | 16 KB (8+8) | 외장형 256~512 KB |
| Intel Pentium MMX | 1996 | 166~233 MHz | 1 | 1 | 32 KB | 외장형 512 KB |
당시에는 멀티 코어도, 하이퍼스레딩도 존재하지 않았으며, CPU는 오직 하나의 작업만을 동시에 실행할 수 있었습니다. 이 말은 곧 자바가 지원한 멀티스레딩은 물리적 병렬성(parallelism)이 아닌 논리적 동시성(concurrency)을 구현하기 위한 구조였다는 뜻입니다. 참고로 Hyper-Threading(하이퍼스레딩) 기술은 2002년 인텔 Pentium 4에서 최초 도입되었기 때문에, 1990년대에는 "코어 1개, 스레드 1개" 구조가 일반적이었습니다.
1990년대 중반의 운영체제는 하나의 CPU를 여러 스레드가 공평하게 사용하도록 하기 위해, 짧은 시간 간격으로 각 스레드에 CPU 실행 시간을 분배하는 시분할(time-slicing) 스케줄링 기법을 사용했습니다. 자바의 Thread 클래스는 이러한 운영체제 수준의 스레드를 직접 다루지 않도록 추상화했으며, 개발자는 하나의 API를 통해 논리적 병렬 처리를 설계할 수 있었습니다. 비록 실제 실행은 순차적일지라도, 여러 스레드가 동시에 실행되는 것처럼 보이는 구조 덕분에 이벤트 중심의 GUI, 비동기 네트워크 처리, 백그라운드 타이머 같은 동시 실행 모델을 자연스럽게 코드로 표현할 수 있었습니다.
자바 1.0이 처음 등장하던 당시 대부분의 컴퓨터는 단일 코어 CPU를 사용하고 있었기 때문에, 여러 스레드가 동시에 실행되는 물리적 병렬성(parallelism)은 존재하지 않았습니다. 하지만, 단일 CPU 환경에서도 운영체제는 여러 스레드를 빠르게 번갈아 실행하는 시분할 방식으로 멀티스레딩을 흉내 냈습니다. 이는 여러 스레드가 실제로는 동시에 실행되지 않더라도, 매우 짧은 시간 간격으로 컨텍스트 스위칭(context switching)이 발생하기 때문에, 마치 병렬로 실행되는 것처럼 보이는 효과를 보였습니다.
이러한 컨텍스트 스위칭의 타이밍은 개발자가 통제할 수 없기 때문에, 여러 스레드가 공유 자원에 동시에 접근하려는 시도가 종종 겹치게 되었습니다. 이때 적절한 동기화가 이루어지지 않으면, 두 스레드가 동일한 값을 읽거나 수정하면서 경쟁 조건(race condition)이 발생할 수 있었습니다. 예를 들어, 두 스레드가 같은 카운터 값을 증가시키는 로직을 갖고 있고, 동기화 없이 실행된다면, 최종 결과가 잘못된 값이 될 가능성이 매우 높습니다.
또한 스레드 간의 메모리 가시성 문제(visibility issue)도 중요한 동기화의 이유였습니다. 하나의 스레드가 변수 값을 변경했더라도, 다른 스레드가 해당 값을 메모리에서 즉시 볼 수 있다는 보장은 없었습니다. 이는 CPU 캐시 계층 구조나 명령어 재정렬 등의 최적화로 인해 발생할 수 있으며, 동기화가 없는 상태에서는 스레드 간 데이터 일관성이 깨지는 문제가 발생할 수 있습니다.
이러한 이유로 자바는 처음부터 객체 단위의 모니터(Monitor) 락 기반 동기화 모델을 채택하였습니다. synchronized 키워드를 통해 자바는 개발자가 지정한 코드 블록에 진입하기 전에 해당 객체의 모니터 락을 확보하게 했고, 락을 가진 스레드만이 해당 임계 구역(critical section)에 진입할 수 있도록 강제했습니다. 동시에 synchronized는 락을 획득하고 해제할 때 메모리 배리어(memory barrier)를 삽입하여, 스레드 간 메모리 가시성 또한 보장하도록 했습니다.
요컨대, 물리적으로 병렬 처리가 이루어지지 않더라도, 운영체제가 스레드를 빠르게 전환하고 자바 스레드가 공유 자원에 접근할 수 있는 환경에서는, 동기화는 선택이 아닌 필수였습니다. 자바는 이러한 상황을 언어 차원에서 안전하게 처리하기 위해, 객체 중심의 모니터 락 모델이라는 비교적 단순하면서도 강력한 구조를 채택한 것입니다.
자바는 동시성 제어를 위한 핵심 기초 구조로 모니터(Monitor) 개념을 채택했으며, 이 모니터를 모든 객체에 기본적으로 부여하는 방식을 선택했습니다. 이는 자바가 객체지향 언어로서의 일관성과, 동기화를 간단하고 안전하게 처리하려는 철학을 반영한 설계였습니다.
모니터는 단순히 락(lock)만을 의미하지 않습니다. 자바에서의 모니터는 락 기능과 조건 대기 기능(wait-notify 메커니즘)을 함께 제공하는 구조입니다. 즉, 하나의 객체에 대해 동시에 하나의 스레드만 접근할 수 있도록 제어하는 상호 배제(mutual exclusion) 기능과, 특정 조건이 충족될 때까지 스레드를 대기시키고 다시 깨우는 조건 동기화(condition synchronization) 기능을 통합한 것이 모니터입니다.
자바는 이러한 모니터를 객체 단위로 제공함으로써, 개발자가 별도로 락 객체를 생성하지 않아도 되고, 특정 객체에 대한 동기화를 synchronized(obj)와 같이 자연스럽게 객체 중심의 문법으로 작성할 수 있게 했습니다. 예를 들어, 한 공유 자원에 대해 락을 걸고 싶다면, 그 자원을 나타내는 객체를 그대로 synchronized 키워드의 기준으로 사용할 수 있습니다. 이처럼 동기화 기준이 되는 객체가 곧 모니터가 되는 방식은, 자바가 동시성 문제를 객체지향적인 방식으로 추상화했다는 강력한 장점이 있습니다.
또한 이 구조 덕분에 wait(), notify(), notifyAll() 메서드는 Object 클래스에 정의되어 있습니다. 모든 자바 객체가 모니터를 내장하고 있으므로, 어떤 객체든 조건 대기와 알림 기능을 수행할 수 있어야 하기 때문입니다. 예를 들어, 특정 조건이 충족되기 전까지 어떤 객체를 기준으로 스레드들이 대기하려면 obj.wait()을 호출할 수 있어야 하며, 조건이 만족되었을 때는 다른 스레드가 obj.notify()를 통해 대기 중인 스레드를 깨울 수 있어야 합니다.
이러한 구조는 개발자에게 익숙한 객체 단위 문법을 제공하면서도, 내부적으로는 JVM이 monitorenter, monitorexit, wait, notify 등의 명령을 통해 해당 객체의 모니터 상태를 관리하게 만듭니다. 객체 하나하나가 동기화의 기준점이 되므로, 스레드 간 자원 보호와 협업 처리를 위한 구조가 비교적 명확하고 일관성 있게 작동합니다.
결국 자바가 모든 객체에 모니터를 부여한 것은 단순한 편의 기능이 아니라, 동시성을 객체 중심으로 통합적으로 모델링하려는 철학에서 비롯된 구조입니다. 이는 자바의 "모든 것이 객체다"라는 철학과도 깊은 연관이 있으며, 동기화 기능 역시 그 철학 안에서 자연스럽게 흡수되도록 설계된 것입니다.
synchronized는 어떻게 작동하는가?자바에서 synchronized 키워드는 개발자가 신경 쓰지 않아도 내부적으로 JVM 수준에서 정확하고 안전한 락(lock) 메커니즘을 수행하도록 설계되어 있습니다. 개발자가 synchronized 키워드를 사용해 특정 코드 블록이나 메서드를 감싸면, 이 부분은 컴파일된 바이트코드 상에서 monitorenter와 monitorexit 명령어로 변환됩니다. 이 명령어들은 JVM에게 명시적으로 해당 객체의 모니터 락을 획득하고 해제하라는 신호를 보내는 역할을 합니다.
synchronized(obj) {
// 임계 구역
}
위와 같은 코드는 바이트코드로 다음과 같이 변환됩니다.
monitorenter
... // 임계 구역 bytecode
monitorexit
monitorenter는 스레드가 객체의 모니터 락을 획득하도록 지시하며, 락이 이미 다른 스레드에 의해 사용 중이라면 현재 스레드는 BLOCKED 상태로 대기하게 됩니다. 락을 획득한 후에만 임계 구역 내부의 코드가 실행됩니다. 코드 블록을 빠져나올 때는 monitorexit 명령어가 호출되어 락이 해제되고, 대기 중이던 다른 스레드가 이 객체의 모니터를 획득할 수 있는 기회를 얻게 됩니다.
특히 자바의 synchronized는 예외 안전성(exception safety)도 함께 고려되어 있습니다. 만약 임계 구역 내부에서 예외가 발생하더라도, JVM은 자동으로 monitorexit을 보장된 위치에 삽입하여 락이 반드시 해제되도록 설계되어 있습니다. 이것은 락 누수(lock leak)를 방지하고, 스레드가 영원히 대기 상태에 빠지는 문제를 예방하는 중요한 메커니즘입니다.
자바의 synchronized는 단순한 문법적 키워드처럼 보일 수 있지만, 내부적으로는 스레드 간 상호 배제(Mutual Exclusion)와 메모리 일관성을 동시에 보장하는 정교한 구조입니다. 락을 획득하거나 해제하는 순간 JVM은 CPU에게 메모리 배리어(memory barrier)를 삽입하도록 지시하며, 이를 통해 변수의 최신 상태를 주기억장치(main memory)에 반영하고, 다른 스레드가 해당 값을 정확히 볼 수 있도록 합니다. 이러한 메커니즘 덕분에 synchronized는 원자성(atomicity)과 가시성(visibility) 모두를 만족시키는 대표적인 동기화 수단으로 자리 잡았습니다.
자바 1.5 이후에는 ReentrantLock과 같은 더 유연한 락 구현도 등장했지만, synchronized는 여전히 자바에서 가장 기본적이고 안전한 동기화 수단이며, JVM 수준에서 최적화가 가장 잘 이루어진 구조 중 하나입니다.
wait()와 notify()는 어떻게 작동하는가? (조건 동기화의 본질)동시성 프로그래밍에서 중요한 개념 중 하나는 조건 동기화(condition synchronization)입니다. 이는 단순히 공유 자원에 대한 접근을 막는 것이 아니라, 특정 조건이 충족될 때까지 스레드를 일시 중단하고, 조건이 만족되면 다시 실행을 재개하도록 조율하는 메커니즘을 의미합니다. 자바는 이 기능을 wait(), notify(), notifyAll() 메서드를 통해 제공합니다.
흥미로운 점은 이 메서드들이 모두 Object 클래스에 정의되어 있다는 것입니다. 그 이유는 앞서 설명한 대로, 자바의 모든 객체는 내부적으로 모니터(Monitor)를 가지고 있으며, 모니터는 단순한 락 이상의 의미, 즉 조건 대기 큐(condition queue)를 함께 포함하고 있기 때문입니다. 따라서 자바의 모든 객체는 조건 동기화의 기준점으로 사용될 수 있고, 이로 인해 wait()와 notify()는 모든 객체에서 호출 가능한 메서드가 됩니다.
조건 동기화의 기본 흐름은 다음과 같습니다. 어떤 스레드가 특정 객체를 기준으로 wait()을 호출하면, 해당 스레드는 그 객체의 모니터 락을 놓고 대기 상태(WAITING)로 전환됩니다. 대기 중인 스레드는 해당 객체의 조건 큐에 들어가며, 이후 다른 스레드가 같은 객체에 대해 notify() 또는 notifyAll()을 호출할 때까지 깨어나지 않습니다.
synchronized(obj) {
while (!condition) {
obj.wait(); // 조건이 만족될 때까지 대기
}
// 조건을 만족한 후 작업 수행
}
반대로, 어떤 스레드가 notify()를 호출하면, 해당 객체의 조건 큐에 들어 있는 대기 중인 스레드 중 하나가 깨어나고, notifyAll()을 호출하면 모든 스레드가 깨어납니다. 하지만 여기서 중요한 점은, 깨어난 스레드가 바로 실행되는 것이 아니라, 모니터 락을 다시 획득해야만 실행을 재개할 수 있다는 것입니다. 즉, notify()는 스레드를 깨우기만 하고, 락을 넘겨주지는 않습니다.
이러한 구조는 생산자-소비자 문제 같은 고전적인 동기화 시나리오에서 유용하게 활용됩니다. 예를 들어 생산자는 큐에 데이터를 넣고 notify()를 호출하여 소비자를 깨우고, 소비자는 데이터가 없으면 wait()을 호출하여 기다리는 구조를 만들 수 있습니다.
wait()과 notify()는 synchronized 블록 내부에서만 호출할 수 있도록 강제됩니다. 이 제약은 해당 객체의 모니터 락을 소유하지 않은 상태에서 조건 큐를 조작하면, 동기화 불일치로 인해 심각한 오류가 발생할 수 있기 때문입니다. JVM은 이러한 사용 위반에 대해 IllegalMonitorStateException을 던지며, 동기화가 올바르게 이루어지지 않았음을 경고합니다.
결국 자바의 조건 동기화는 synchronized와 wait()/notify()가 협력하여 구성하는 구조이며, 객체 중심의 모니터 설계를 통해 상호 배제와 조건 대기를 통합적으로 구현한 대표적인 예라고 할 수 있습니다. 이는 자바가 단순히 락을 제공하는 수준을 넘어, 스레드 간의 협력을 구조적으로 모델링한 언어임을 보여주는 핵심적인 요소 중 하나입니다.
지금까지 살펴본 자바 1.0의 동시성 모델은 단순히 API 수준의 지식이 아니라, 시대적 배경과 하드웨어의 한계, JVM의 추상화 철학, 그리고 언어 설계의 일관성을 함께 이해하는 여정이었습니다. 단일 코어, 낮은 메모리 환경 속에서 자바는 객체 중심의 Thread, synchronized, wait() 같은 도구들을 통해 스레드 간 협력과 자원 보호를 동시에 추구했고, 이를 JVM과 운영체제가 함께 분담하는 구조로 설계했습니다.
이제 Thread는 단순한 실행 단위가 아니고, synchronized는 단순한 문법이 아니라는 사실을 명확히 인식하게 되었습니다. 그들은 모두 자바가 동시성과 안정성을 추구하기 위해 언어, JVM, OS의 경계를 넘나들며 만든 정교한 동작 체계의 일부입니다.
앞으로는 이 기반 위에서, 자바 5 이후에 등장한 ReentrantLock, Condition, ExecutorService, Future, 그리고 고성능 병렬 처리 도구인 ForkJoinPool이나 가벼운 스레드(Virtual Thread) 같은 현대 자바의 동시성 도구들을 탐구하게 될 것입니다. 이를 통해 자바가 어떻게 고전적인 동기화 구조를 넘어 비동기 처리, 대기 없는 병렬성, 구조적 동시성으로 진화했는지를 이어서 학습해 나갈 예정입니다.
이제 자바의 동시성을 단순히 "동작하는 코드"로만 보지 않고, 설계 철학과 하드웨어 현실에 기반한 시스템적인 시각으로 바라볼 준비가 되었다고 볼 수 있습니다. 앞으로 모든 동시성 개념과 고급 병렬 처리 모델을 배우는 데 가장 탄탄한 기반이 되어줄 것입니다.