페이지

레이블이 Code인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Code인 게시물을 표시합니다. 모든 게시물 표시

2026년 7월 23일 목요일

[Code] Web Audio API 소리 (Javascript) - 첫 소리가 잘리는 현상

JavaScript Web Audio API로 알람이나 알림음을 재생하는 기능을 만들다 보면, 이상한 버그를 하나 만나게 됩니다.

앱을 켜고 처음 알람이 울릴 때는 소리가 뭉개지거나, 앞부분이 잘려서 나오거나, 아예 안 들리는 경우가 있습니다. 그런데 같은 코드로 두 번째, 세 번째 알람을 재생하면 멀쩡하게 잘 들립니다.
일정시간 동안 아무런 동작을 안하고, 소리출력을 하면 앞부분이 누락되어 들립니다. 

같은 함수, 같은 파라미터인데 왜 첫 번째 소리만 짤리는 것입니다. 


원인: 오디오 하드웨어의 "절전모드"

브라우저(그리고 OS)의 오디오 출력 경로는 계속 켜져 있지 않습니다. 실제 스피커나 오디오 드라이버 쪽에서는 소리가 한동안 나오지 않으면 절전 상태로 들어가고, 다시 소리를 출력하라는 신호가 오면 하드웨어를 깨우는 과정을 거칩니다.

문제는 이 깨어나는 과정에 시간이 걸린다는 점입니다. AudioContext를 새로 만들거나, 한동안 소리를 내지 않다가 갑자기 osc.start()를 호출하면, 오디오 파이프라인이 완전히 준비되기 전 짧은 구간 동안 출력이 손실되거나 왜곡됩니다. 알람처럼 "짧고 정확한 타이밍의 소리"를 재생하는 경우, 하필 그 손실 구간이 소리의 시작 부분과 겹쳐서 정작 중요한 첫 비트가 날아가는 현상이 발생하는 것입니다.


해결 아이디어: 진짜 소리를 내기 전에 "예열"한다

사용자에게 들려줄 알람음을 재생하기 직전에, 사람은 거의 인지할 수 없는 아주 작은 소리를 짧게 먼저 흘려보내서 오디오 하드웨어를 미리 깨워두는 것입니다.


사운드 워밍업 함수

function warmSound() {

  try {

    const ctx = ensureAudioCtx();

    const osc = ctx.createOscillator();

    const gain = ctx.createGain();


    osc.type = 'sine';

    osc.frequency.value = 5;       // 5Hz, 사실상 들리지 않는 저역

    gain.gain.value = 0.0001;      // 최소 볼륨 (사람이 거의 못 듣는 수준)


    osc.connect(gain).connect(ctx.destination);

    osc.start(ctx.currentTime);        // 지연 없이 즉시 출력

    osc.stop(ctx.currentTime + 0.5);   // 0.5초간 유지 후 정지

  } catch (e) {

    /* 워밍업 실패는 무시 - 워밍업은 보조 수단일 뿐, 실패해도 본 알람 재생에 영향 없어야 함 */

  }

}



실제 알람 재생 흐름

async function playAlarmBeep() {
  if (!alarmEnabled) return;
  const soundChoice = localStorage.getItem(ALARM_SOUND_KEY) || 'beep';
  await ensureRunning();   // AudioContext resume 등 상태 확인
  warmSound();             // 본 소리 재생 전, 오디오 하드웨어 예열
  if (soundChoice === 'dingdong') {
    setTimeout(playDingdong, 500);
  } else {
    setTimeout(playBeepTone, 500);
  }
}


실제 비프음 재생 함수

function playBeepTone() {
  try {
    const ctx = ensureAudioCtx();
    const WARMUP_DELAY = 0;      // 이미 warmSound에서 예열했으므로 추가 지연 없음
    const BEEP_DURATION = 1;     // 1초간 재생
    const startAt = ctx.currentTime + WARMUP_DELAY;
    const osc = ctx.createOscillator();
    const gain = ctx.createGain();
    osc.type = 'sine';
    osc.frequency.value = 880; // A5음
    // 갑자기 소리가 튀지 않도록 짧게 페이드 인 / 페이드 아웃
    gain.gain.setValueAtTime(0.0001, startAt);
    gain.gain.exponentialRampToValueAtTime(0.2, startAt + 0.02);
    gain.gain.exponentialRampToValueAtTime(0.0001, startAt + BEEP_DURATION - 0.05);
    osc.connect(gain).connect(ctx.destination);
    osc.start(startAt);
    osc.stop(startAt + BEEP_DURATION);
  } catch (e) {
    /* 오디오 재생 실패는 무시 (자동재생 정책 등) */
  }
}

여기서 gain.gain.exponentialRampToValueAtTime을 이용해 볼륨을 서서히 올리고 내리는 것도 눈여겨볼 부분입니다. 워밍업으로 하드웨어 자체는 깨워뒀더라도, 볼륨이 순간적으로 0에서 최대치로 튀면 노이즈가 발생할 수 있기 때문에, 페이드 인/아웃을 함께 적용해 자연스러운 소리를 만들어줍니다.




흐름을 정리하면 다음과 같습니다.


 1. ensureRunning()으로 AudioContext가 정상 동작 중인지 확인 (suspended 상태면 resume) 

 2. warmSound()를 호출해 0.5초짜리 무음에 가까운 신호를 출력, 오디오 하드웨어를 미리 깨움

 3. 워밍업 신호가 끝나는 시점(setTimeout(..., 500))에 맞춰 실제 알람음(playBeepTone 또는 playDingdong)을 재생


즉, "진짜 소리"가 나가기 0.5초 전에 미리 저강도 신호로 파이프라인을 활성화시켜 놓고, 하드웨어가 완전히 깨어난 타이밍에 실제 알람음을 흘려보내는 구조입니다.




시행착오


osc.frequency.value = 0;   // 주파수 0

gain.gain.value = 0;       // 볼륨 0


주파수가 0이거나 gain이 완전히 0이면, 오디오 그래프상으로는 신호가 흐르지만 실제 하드웨어 출력 레벨에서는 "무음"과 다를 바 없이 처리되어 절전모드를 깨우지 못했습니다. 오디오 드라이버 입장에서는 "출력할 데이터가 없다"고 판단해버리는 것이죠. 즉, 워밍업이 성립하려면 하드웨어가 실제로 "출력 신호가 존재한다"고 인식할 수 있는 최소한의 값이 필요했습니다.


"0이 아닌 아주 작은 값"을 적용해야 합니다. 


[참고]
윈도우OS 크롬, 엣지에서 첫소리가 짤리는 현상이 발생하여, 워밍업 소리 출력을 해야 했습니다. 
그러나, 맥OS 크롬에서 첫소리가 짤리는 증상은 없습니다.

맥OS에서 웹앱을 개발하다가, 윈도우OS 웹브라우저에서 이런 증상이 나와서 시행착오를 많이 했습니다. 
 

2026년 7월 4일 토요일

[Code] 모듈러 모놀리딕(Modular Monolithic) + 프로세스 분리 & Python 예제 #2

모듈러 모놀리딕(Modular Monolithic) + 프로세스 분리 & Python 예제 #2

모듈러 모놀리딕으로 개발하다가, 특정모듈을 분리하려고 합니다. 여기서 분리는 프로세스(실행파일)을 독립적으로 분리하려고 합니다. 분리하려는 이유는 여러가지를 가정할 수 있을 것 같습니다. 

초기 개발할 때는 모듈러 모놀리딕으로 진행하다가 MacroService 또는 Citadel 아키텍처로 변환이 되는 시작점이 되는 것으로 보입니다. 개발 초기부터 MSA로 시작할 필요가 없다고 생각하고, 또한 MSA가 최종 목표도 아닙니다. 

아키텍처 전환 시, 발생하는 변화가 있습니다. (쉽게 표현하면, 프로세스가 나누어지면)
통신방식의 변환이 필요한 것입니다. In-Memory -> IPC/Network로 변경해야 하고, 코드가 더 들어갑니다. 
다시 말해서, 노출된 함수를 직접 호출하는 방식이 아닌, IPC 또는 Network로 통신을 해야 합니다. 


main_app            # [프로세스 1]
 - main.py
 - cli.py
 - feature_a.py
 - feature_b.py   # 로직수행하지 않음. 프로세스2로 요청을 보내는 대리인 역할

service_b            # [프로세스 2]
 - service_b.py    # 자체 웹 서버를 탑재한 B 기능 실행 파일



1. [프로세스 1] main.py | cli.py | feature_a.py (변경없음)


2. [프로세스 1] feature_b.py 
기존 비즈니스 로직 코드는 삭제되고, 독립된 B 프로세스 웹 서버로  HTTP POST 요청을 보내는 코드로 대체합니다. 

# main_app/feature_b.py
import requests

# 독립 실행된 B 프로세스의 REST API 주소
B_SERVICE_URL = "http://127.0.0.1:8000/execute"

def run(input_value: str) -> str:
    print(" [네트워크] 독립된 B 프로세스로 REST API 요청 전송 중...")
    
    # REST API에 보낼 JSON 데이터 구성
    payload = {"text": input_value}
    
    try:
        # B 프로세스 서버로 POST 요청 발송 (타임아웃 3초 설정)
        response = requests.post(B_SERVICE_URL, json=payload, timeout=3)
        
        if response.status_code == 200:
            data = response.json()
            return data.get("result", "응답 데이터 누락")
        else:
            return f" B 서비스 오류 (HTTP {response.status_code})"
            
    except requests.exceptions.ConnectionError:
        # B 프로세스가 죽어있어도 메인 시스템은 크래시 나지 않고 결함이 격리됨
        return "B 서비스 프로세스에 연결할 수 없습니다. (서버가 꺼져있음)"


3. [프로세스 2] service_b.py (분리된 독립 B 서비스 코드)
프로세스1과 완전히 별개로 실행되는 가벼운 웹 서버입니다. 기존 B기능의 비지니스 로직이 이관된 것입니다. (사전에 pip install fastapi uvicorn 설치 필요) FastAPI 사용

# service_b/service_b.py
from fastapi import FastAPI
from pydantic import BaseModel
import uvicorn

app = FastAPI(title="독립된 B 기능 매크로서비스")

# 요청 데이터 구조 정의 (DTO)
class RequestData(BaseModel):
    text: str

def _pure_business_logic(data: str) -> str:
    """기존 b.py에 있던 핵심 비즈니스 로직"""
    return data[::-1]

@app.post("/execute")
def handle_b_feature(data: RequestData):
    """메인 프로세스로부터 REST API 요청을 받는 엔드포인트"""
    print(f"[B 프로세스 수신] 요청 데이터: '{data.text}'")
    
    # 비즈니스 로직 수행
    processed_text = _pure_business_logic(data.text)
    
    # JSON 형태로 응답 반환
    return {
        "status": "success",
        "result": f"[B 원격 프로세스 완료] 뒤집힌 결과 -> {processed_text}"
    }

if __name__ == "__main__":
    print("B 기능 독립 프로세스 서버를 시작합니다 (Port: 8000)...")
    # 8000번 포트에서 독립된 웹 서버 프로세스로 가동
    uvicorn.run(app, host="127.0.0.1", port=8000)



2개 실행파일을 모두 실행해야 합니다. 2개의 프로세스이므로 터미널에 두개 열어서 각각 실행합니다. 
 - python service_b.py
 - python main.py


추가 고려사항
 - 프로세스간에 통신하는 방식은 여러가지가 있지만, 여기서는 REST를 사용한 것입니다. 다른 방식으로 선정했다면, 그에 맞는 코드로 적용하면 됩니다. 

 - B기능이 자원(CPU, Memory)를 많이 소모하는 경우, B 실행파일만 별도의 고성능 서버에 실행할 수 있습니다. 코드에서 IP, Port만 변경해주면 됩니다. 

 - B기능이 자주 변경되어 배포해야하는 것은 메인 프로세스를 중단하지 않고 B 모듈/서비스만 배포하여 실행할 수 있습니다. (반대의 경우도 동일함)

 - B기능에 문제가 발생하면(메모리 누수로 인한 비정상 종료), 메인 프로세스에 영향이 거의 없습니다. 




Step #1 : 모든 코드가 하나의 큰 프로세스 내부에 있으며, In-Memory 함수 호출로 통신함
Step #2 : 메인 프로세스는 핵심기능(A)를 유지하고, 분리된 독립 B 서비스가 독립적으로 존재. 메인 프로세스 내부의 Feature_b.py는 로직수행이 아닌 REST 클라이언트가 되어 원격으로 독립 B 서비스를 REST API요청을 보냅니다. 


[Code] 모듈러 모놀리딕(Modular Monolithic) & Python 예제 #1

모듈러 모놀리딕과 파이썬 예제 #1

모률러 모놀리딕(Modular Monolithic) 아키텍처는 시스템을 하나의 단일 프로세스(Monolithic)로 배포하되, 내부 코드는 독립적인 모듈(Module)단위로 엄격하게 분리하여 결합도를 낮추는 아키텍처입니다. 각 모듈은 내부 구현을 숨기고(캡슐화), 명확한 인터페이스를 통해서만 소통해야 합니다.

기본예제 Project를 main.py | cli.py | feature_a.py | feature_b.py 로 구성하였고, 파이썬으로 언어입니다.

 - main.py 애플리케이션 진입
 - cli.py 사용자 인터페이스 입/출력 담당 모듈
 - feature_a.py 비지니스 로직 A 모듈
 - feature_b.py 비지니스 로직 B 모듈


1. 비지니스 로직 A 모듈 (feature_a.py)
 외부에는 데이터를 처리하는 핵심 인터페이스(run)만 노출하고, 내부 동작 알고리즘(_secret_process)은 숨김힙니다. 

# feature_a.py
def _secret_process(data: str) -> str:
    """모듈 내부에서만 사용하는 캡슐화된 메서드 (Private)"""
    return f"[A 모듈 가공] -> {data}"

def run(input_value: str) -> str:
    """외부에서 호출하는 명확한 Public API 인터페이스"""
    print("⚙️ A 기능 모듈 작동 중...")
    result = _secret_process(input_value)
    return f"A 기능 완료 결과: {result}"


2. 비지니스 로직 B 모듈 (feature_b.py)
 A 모듈과 완전히 독립되어 있으며, 자신만의 비즈니스 로직을 수행합니다. 

# feature_b.py
def run(input_value: str) -> str:
    """외부에서 호출하는 명확한 Public API 인터페이스"""
    print("⚙️ B 기능 모듈 작동 중...")
    # 간단한 비즈니스 로직 예시 (역순 정렬)
    reversed_data = input_value[::-1]
    return f"B 기능 완료 결과: {reversed_data}"


3. 입출력 담당 모듈 (cli.py)
 비지니스 로직(A, B)이 어떻게 돌아가는지 전혀 알 필요가 없습니다. 오직 사용자에게 입력을 받고 결과를 출력하는 UI역할에만 충실합니다. 

# cli.py
def get_user_choice() -> tuple[str, str]:
    """사용자로부터 메뉴 선택과 입력값을 받는 인터페이스"""
    print("\n==============================")
    print("1: A 기능 실행")
    print("2: B 기능 실행")
    print("q: 프로그램 종료")
    print("==============================")
    
    choice = input("원하는 기능을 선택하세요: ").strip()
    if choice == 'q':
        return choice, ""
        
    user_input = input("기능에 전달할 텍스트를 입력하세요: ")
    return choice, user_input

def display_result(result: str):
    """결과 화면을 출력하는 인터페이스"""
    print("\n📢 [CLI 출력]")
    print(f"결과: {result}")


4. 진입점 및 모듈 조립 (main.py)
 애플리케이션을 초기화하고, 각 모듈의 인터페이스를 연결하는 역할을 합니다. 중요한 점은 A모듈과 B모듈이 서로 참조하지 않고, CLI모듈도 A, B을 알지 못하며, 오직 main.py가 이들을 중재한다는 것입니다. 

# main.py
import cli
import feature_a
import feature_b

def bootstrap():
    """프로그램 초기 실행 및 준비 단계"""
    print("🚀 시스템 초기화 중... 모듈 로드 완료.")
    print("모듈러 모놀리딕 애플리케이션을 시작합니다.")

def main():
    bootstrap()
    
    while True:
        # 1. CLI 모듈을 통해 사용자 입력 수집
        choice, user_data = cli.get_user_choice()
        
        if choice == 'q':
            print("👋 프로그램을 종료합니다.")
            break
            
        # 2. 제어 흐름 분기 및 모듈 간 중재
        result = ""
        if choice == '1':
            # CLI에서 받은 데이터를 A 모듈 인터페이스로 전달
            result = feature_a.run(user_data)
        elif choice == '2':
            # CLI에서 받은 데이터를 B 모듈 인터페이스로 전달
            result = feature_b.run(user_data)
        else:
            result = "❌ 잘못된 선택입니다. 다시 시도하세요."
            
        # 3. 처리된 결과를 다시 CLI 모듈을 통해 출력
        cli.display_result(result)

if __name__ == "__main__":
    main()


모듈러 아키텍처 관점에서의 핵심 포인트
 - 낮은 결함도(Low Coupling) : feature_a.py와 feature_b.py는 서로의 존재를 모릅니다. 나중에 B기능을 삭제하거나, C기능으로 대체해도 A기능에 영향이 없습니다. 

 - 높은 응집도(High Cohesion) : cli.py는 입출력만 담당하고, feature_a는 본인의 데이터 처리에만 집중합니다. UI가 웹(Web)이나 GUI로 바뀌더라도 cli.py만 수정하거나 교체하면 됩니다. 

 - 단일 진입점(Single Entry Point) : main.py가 전체 흐름을 제어하는 컨트롤 타워 역할을 수행하여 모듈 간의 무분별한 직접 참조(Spaghetti Code)를 방지합니다. 




모듈러 모놀리딕 배포관점 및 개발/논리 관점 
 - 배포관점 : 전체 시스템이 하나의 거대한 '애플리케이션'으로 묶여 있으며, 단일 프로세스와 메모리 공간을 공유합니다. 배포관점은 모놀리딕 입니다. 

 - 개발/논리 - 아키텍처 관점 : 시스템 내부가 모듈별로 엄격하게 분리되어 있습니다. 각 모듈은 자신만의 내부 로직과 데이터 액세스 계층을 가지며, 외부에는 명확한 'Public API 인터페이스'만 노출하여 소통합니다.