breathed.kr · 서버 구성 문서

이 서버는 이렇게 돌아간다

도메인 세 개, 컨테이너 여섯 개가 서버 한 대 위에서 어떻게 나뉘어 도는지, 그리고 같은 것을 손으로 다시 만드는 방법까지 정리했습니다.

2026-09-15 기준 · breathed-home (211.227.66.46)

도메인 세 개가 무엇을 담당하나

2026-09-15 개편 이후

주소내용스택
breathed.kr랜딩 · 성경 타임라인 · 종교개혁 자료breathed
teams.breathed.krINSOO CMS 콘솔이 사이트 루트insoo
teams.breathed.kr/2026jecheon/제천 프로젝트insoo
teams.breathed.kr/2026kyrgyzstan/키르기스스탄 프로젝트insoo
insoo.breathed.kr/sermon/설교문insoo
insoo.breathed.kr/_otl/container/이 문서 — 서버 구성 해설insoo

www.breathed.kr 로 들어오면 301 한 번으로 breathed.kr 에 도착합니다. 주소를 하나로 모으기 위해 80번 단계에서 이미 apex 로 보내기 때문에, 중간에 두 번 튕기지 않습니다.

앞으로 프로젝트가 늘어나면 teams/ 아래에 폴더를 만들고 관리자에서 공개 경로를 /2027xxx/board 형태로 지정하면 됩니다.

왜 컨테이너인가

시작은 MySQL 하나 때문이었다

이관해 온 사이트는 MySQL 로 돌고 있었습니다. 서버에 직접 설치하려면 MySQL 이든 MariaDB 든 하나를 골라야 하고, 그 선택이 앞으로 올릴 모든 사이트에 적용됩니다. 나중에 다른 DB 를 쓰는 사이트를 옮겨오면 방법이 없습니다.

서버에 직접 설치 사이트 A 사이트 B 공유 · 하나뿐 PHP MySQL B 가 다른 DB 를 원하면 → 방법이 없다 컨테이너 — 사이트마다 따로 사이트 A PHP 8.5 MySQL 9.7 사이트 B PHP 7.4 MariaDB 서로 다른 버전이어도 충돌하지 않는다 그래서 DB 를 변환하는 작업이 아예 없어졌다 원본과 같은 버전을 띄우고 덤프를 그대로 넣었다
처음 질문은 “MySQL 로 만든 사이트를 MariaDB 로 옮길 수 있냐” 였습니다. 컨테이너로 가면서 그 질문 자체가 사라졌습니다.

가상머신과는 다르다

컨테이너는 운영체제를 복제하지 않습니다. 리눅스 커널은 이 서버 것을 빌려 쓰고 앱과 필요한 라이브러리만 격리합니다. 그 차이가 숫자로 드러납니다.

컨테이너정체메모리
insoo-db-1MySQL 9.7 (CMS 데이터)548 MiB
breathed-db-1MySQL 9.7 (아직 비어 있음)446 MiB
insoo-php-1PHP 8.5 + ffmpeg19 MiB
breathed-php-1PHP 8.512 MiB
insoo-web-1nginx 1.286 MiB
breathed-web-1nginx 1.285 MiB
합계 6개전체 7.2 GiB 중약 1.0 GiB

가상머신 6대라면 각각 OS 를 올려야 하니 10 GB 이상은 각오해야 합니다. 대부분이 MySQL 두 개이고, nginx·PHP 컨테이너는 각각 20 MB 안쪽입니다. 이래서 “사이트마다 하나씩”이 현실적인 선택이 됩니다.

이미지 · 컨테이너 · 볼륨

알아야 할 말 세 개

image

이미지

앱이 통째로 담긴 읽기 전용 설계도. 실행되지 않고 디스크에 있습니다. mysql:9.7 같은 이름이 이것입니다.

container

컨테이너

이미지로 실제로 켠 것. 지우고 다시 만들어도 이미지는 그대로라 언제든 같은 상태로 돌아갑니다.

volume

볼륨

컨테이너 안 폴더를 서버의 실제 폴더에 연결. 컨테이너를 지워도 데이터가 남는 이유입니다.

이 서버에 실물 예가 있습니다. nginx:1.28-alpine 이미지 하나insoo-web-1breathed-web-1 두 컨테이너를 켰습니다. 같은 설계도에 설정과 포트만 다르게 넣은 것입니다.

요청이 흐르는 길

브라우저에서 화면까지

사이트가 컨테이너 안에 있으면 문제가 하나 생깁니다. 웹 포트는 서버에 하나뿐인데 도메인은 셋이라는 것. 그래서 누군가는 들어온 요청의 Host 를 보고 어디로 보낼지 정해줘야 합니다. 그 역할이 호스트 nginx 입니다.

인터넷 :443 호스트 nginx — TLS 를 풀고 Host 로 분기 인증서 하나로 4개 이름 · PHP 는 실행하지 않는다 www → breathed.kr 로 301 breathed.kr → :8082 teams. → :8081 insoo. → :8081 insoo 스택 · 독립 네트워크 insoo-web-1 · nginx server 블록 2개로 Host 재분기 php:9000 insoo-php-1 · PHP 8.5 db:3306 insoo-db-1 · MySQL 9.7 /var/www/teams /var/www/insoo 게시글 288 첨부 3,697 breathed 스택 · 독립 네트워크 breathed-web-1 · nginx php:9000 breathed-php-1 · PHP 8.5 breathed-db-1 · MySQL 9.7 /var/www/html 두 스택은 네트워크가 분리되어 서로에게 접속할 수 없다
8081·8082 는 서버 내부(127.0.0.1)에만 열려 있습니다 — 인터넷에서 직접 두드릴 수 없고 반드시 호스트 nginx 를 거칩니다. insoo 스택은 Host 를 한 번 더 보고 teamsinsoo 중 어느 문서 루트를 쓸지 정합니다.

디렉터리 구조

URL 과 폴더가 일치하도록 맞춰 두었다

/home/sites/
├─ breathed/                      # breathed.kr
│  ├─ compose.yaml  .env          # 스택 설계도 · 비밀값(0600)
│  ├─ public/       → /var/www/html
│  │  ├─ index.php                # 랜딩
│  │  ├─ bible.php                # 성경, 기록의 시간
│  │  └─ reformation.php
│  ├─ php/ nginx/ db/ logs/ backups/
│
└─ insoo/                         # teams. + insoo. (한 스택이 둘 담당)
   ├─ compose.yaml  .env
   ├─ app/          → /var/www
   │  ├─ teams/     → /var/www/teams   # teams.breathed.kr
   │  │  ├─ index.php app.php lib/ admin/ member/ …  # CMS 가 루트
   │  │  ├─ uploads/                   # 12G
   │  │  ├─ 2026jecheon/
   │  │  └─ 2026kyrgyzstan/
   │  └─ insoo/     → /var/www/insoo   # insoo.breathed.kr
   │     ├─ index.php
   │     ├─ sermon/
   │     └─ _otl/container/        # 이 문서
   ├─ php/ nginx/ db/ logs/ backups/
   └─ restore-from-backup.sh

왜 이렇게 맞췄나 — URL 과 폴더 이름이 같으면 머릿속에서 변환할 일이 없습니다. teams.breathed.kr/2026jecheon/app/teams/2026jecheon/ 이고, 컨테이너 안에서는 /var/www/teams/2026jecheon/ 입니다.

파일 올리는 법

가장 실용적인 부분

“컨테이너 안에 사이트가 있다” 면 파일을 고칠 때마다 컨테이너를 다시 만들어야 할까요. 아닙니다. 볼륨이 그 문제를 해결합니다.

서버(호스트)의 실제 폴더 /home/sites/breathed/public/ ├ index.php └ bible.php 여기에 파일을 놓는다 같은 파일 복사 아님 컨테이너 안에서 보이는 경로 /var/www/html/ ├ index.php └ bible.php PHP·nginx 가 여기를 읽는다 파일 하나가 두 경로로 보이는 것 — 놓는 즉시 사이트에 반영된다
복사가 아니라 같은 파일을 두 이름으로 보는 것입니다. 그래서 파일을 올린 뒤 컨테이너를 재시작할 필요가 없습니다. compose.yaml./public:/var/www/html 한 줄이 이 연결을 만듭니다.
# breathed.kr 에 올릴 때
$ cp 내파일.php /home/sites/breathed/public/
$ chown 33:33  /home/sites/breathed/public/내파일.php
$ chmod 644    /home/sites/breathed/public/내파일.php

# teams.breathed.kr 에 새 프로젝트를 올릴 때
$ cp -r 새프로젝트 /home/sites/insoo/app/teams/
$ chown -R 33:33 /home/sites/insoo/app/teams/새프로젝트

# insoo.breathed.kr 에 올릴 때
$ cp -r 자료 /home/sites/insoo/app/insoo/
$ chown -R 33:33 /home/sites/insoo/app/insoo/자료

33 이 뭔가 — 컨테이너 안 www-data 계정의 번호입니다. 리눅스는 계정을 이름이 아니라 번호로 판단하므로, 호스트에서 33 으로 맞추면 컨테이너 안에서 정상적으로 읽힙니다.

손으로 구축하는 전체 절차

처음부터 다시 만든다면 이 순서

1부 · 서버 준비 (한 번만)

  1. Docker 저장소 등록

    $ apt-get install -y ca-certificates curl gnupg
    $ install -m 0755 -d /etc/apt/keyrings
    $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
          -o /etc/apt/keyrings/docker.asc
    $ chmod a+r /etc/apt/keyrings/docker.asc
    $ cat > /etc/apt/sources.list.d/docker.sources <<EOF
    Types: deb
    URIs: https://download.docker.com/linux/ubuntu
    Suites: $(. /etc/os-release; echo $VERSION_CODENAME)
    Components: stable
    Architectures: $(dpkg --print-architecture)
    Signed-By: /etc/apt/keyrings/docker.asc
    EOF
    $ apt-get update
  2. 데이터 저장 위치를 먼저 정한다

    $ mkdir -p /etc/docker /home/docker
    $ cat > /etc/docker/daemon.json <<'EOF'
    {
      "data-root": "/home/docker",
      "log-driver": "json-file",
      "log-opts": { "max-size": "10m", "max-file": "3" },
      "live-restore": true,
      "default-address-pools": [ { "base": "172.20.0.0/14", "size": 24 } ]
    }
    EOF

    왜 먼저인가 — 설치 후에 옮기려면 컨테이너를 전부 내려야 합니다. 이미지 몇 개만 받아도 수 GB 라, 작은 디스크는 금방 찹니다.

  3. Docker 설치·기동

    $ apt-get install -y docker-ce docker-ce-cli containerd.io \
          docker-buildx-plugin docker-compose-plugin
    $ systemctl enable --now containerd docker
    $ docker info --format '{{.DockerRootDir}}'   # /home/docker 확인
  4. Docker 의 방화벽 우회 차단

    Docker 는 포트를 열 때 ufw 를 통하지 않고 iptables 에 직접 규칙을 넣습니다. Docker 가 건드리지 않는 DOCKER-USER 체인에 기본 차단을 둡니다.

    $ cat > /usr/local/sbin/docker-firewall-guard <<'EOF'
    #!/usr/bin/env bash
    set -u
    LAN="172.30.1.0/24"          # 내부망에 맞게 수정
    DOCKER_POOL="172.20.0.0/14"  # daemon.json 의 주소 풀
    DOCKER_BRIDGE="172.17.0.0/16"
    apply() {
      local ipt="$1"
      "$ipt" -L DOCKER-USER -n >/dev/null 2>&1 || return 0
      "$ipt" -F DOCKER-USER
      "$ipt" -A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
      "$ipt" -A DOCKER-USER -s "$DOCKER_POOL"   -j RETURN
      "$ipt" -A DOCKER-USER -s "$DOCKER_BRIDGE" -j RETURN
      "$ipt" -A DOCKER-USER -i lo -j RETURN
      if [ "$ipt" = "iptables" ]; then
        "$ipt" -A DOCKER-USER -s 127.0.0.0/8 -j RETURN
        "$ipt" -A DOCKER-USER -s "$LAN" -j RETURN
      fi
      "$ipt" -A DOCKER-USER -j DROP
    }
    apply iptables
    apply ip6tables
    EOF
    $ chmod 0755 /usr/local/sbin/docker-firewall-guard

    주의 — 컨테이너 대역을 172.16.0.0/12 로 뭉뚱그리면 안 됩니다. 내부망이 172.30.1.0/24 라면 그 안에 포함되어 랜 전체가 허용돼 버립니다.

    이걸 docker.service 뒤에 붙는 systemd 유닛으로 등록해 재부팅에도 유지되게 합니다.

  5. 방화벽

    $ ufw allow 22/tcp          # ★ enable 전에 먼저
    $ ufw limit 22/tcp
    $ ufw allow 80/tcp
    $ ufw allow 443/tcp
    $ ufw show added            # 22 가 있는지 눈으로 확인
    $ ufw --force enable

    순서를 지킬 것 — 22 를 허용하지 않고 enable 하면 원격 접속이 그 자리에서 끊깁니다.

2부 · 사이트 하나 추가 (매번 반복)

  1. 폴더와 비밀값

    $ mkdir -p /home/sites/mysite/{php/conf.d,nginx/conf.d,public,logs,db/data}
    $ cd /home/sites/mysite
    $ umask 077
    $ cat > .env <<EOF
    COMPOSE_PROJECT_NAME=mysite
    SITE_HTTP_PORT=8083
    MYSQL_ROOT_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 28)
    MYSQL_DATABASE=mysite
    MYSQL_USER=mysite
    MYSQL_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=' | head -c 28)
    EOF
    $ chmod 600 .env

    포트 — 8081(insoo)·8082(breathed) 는 쓰는 중이니 8083 부터 하나씩 늘려 배정합니다.

  2. compose.yaml

    services:
      db:
        image: mysql:9.7
        restart: unless-stopped
        command:
          - --character-set-server=utf8mb4
          - --collation-server=utf8mb4_0900_ai_ci
          # MySQL 8.4 부터 --default-authentication-plugin 은 제거됐다
          - --authentication-policy=caching_sha2_password
        environment:
          MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?}
          MYSQL_DATABASE: ${MYSQL_DATABASE:?}
          MYSQL_USER: ${MYSQL_USER:?}
          MYSQL_PASSWORD: ${MYSQL_PASSWORD:?}
          TZ: Asia/Seoul
        volumes:
          - ./db/data:/var/lib/mysql
        # 포트를 열지 않는다 — php 컨테이너만 내부에서 접근한다
        healthcheck:
          test: ["CMD","mysqladmin","ping","-h","127.0.0.1","-u","root","-p${MYSQL_ROOT_PASSWORD}"]
          interval: 10s
          timeout: 5s
          retries: 12
          start_period: 60s
        networks: [internal]
    
      php:
        build: { context: ./php }      # 확장이 필요 없으면 image: php:8.5-fpm
        restart: unless-stopped
        depends_on:
          db: { condition: service_healthy }
        volumes:
          - ./public:/var/www/html
          - ./php/conf.d/zz-site.ini:/usr/local/etc/php/conf.d/zz-site.ini:ro
        networks: [internal]
    
      web:
        image: nginx:1.28-alpine
        restart: unless-stopped
        depends_on: [php]
        ports:
          # ★ 반드시 127.0.0.1 — 빼면 인터넷에 그대로 열린다
          - "127.0.0.1:${SITE_HTTP_PORT}:80"
        volumes:
          - ./public:/var/www/html:ro
          - ./nginx/conf.d:/etc/nginx/conf.d:ro
        networks: [internal]
    
    networks:
      internal: { driver: bridge }

    줄별로 무슨 뜻인가

    image:어떤 이미지로 켤지. 태그를 반드시 고정. latest 는 어느 날 갑자기 올라간다
    volumes:서버경로:컨테이너경로. :ro 는 읽기 전용
    ports:127.0.0.1:8083:80 — 앞의 IP 가 외부 노출을 막는다
    networks:같은 network 안에서만 통신. 그래서 php 가 db 라는 이름으로 DB 를 찾는다
    depends_on:service_healthy 는 DB 가 준비될 때까지 기다린다 — 켜진 것과 다르다
    ${VAR:?}.env 에서 읽고, 값이 없으면 기동을 거부한다
  3. 컨테이너 안 nginx

    server {
        listen 80;
        server_name _;
        root /var/www/html;
        index index.php index.html;
        client_max_body_size 64M;
    
        # 호스트 nginx 가 넘겨준 실제 방문자 IP 를 로그에 남긴다
        real_ip_header X-Forwarded-For;
        set_real_ip_from 172.16.0.0/12;
        real_ip_recursive on;
    
        # 백업·임시 파일 차단 — 원본 코드에 .bak/.orig 가 섞여 있기 쉽다
        location ~* \.(bak|orig|old|save|swp|tmp|premigrate)([^/]*)$ { deny all; }
        location ~* \.(sql|log|sqlite3?|db)$ { deny all; }
    
        location / { try_files $uri $uri/ =404; }
    
        location ~ \.php$ {
            fastcgi_split_path_info ^(.+?\.php)(/.*)$;
            try_files $fastcgi_script_name =404;
            set $path_info $fastcgi_path_info;
            fastcgi_pass php:9000;          # ← compose 의 서비스 이름
            include fastcgi_params;
            fastcgi_param PATH_INFO $path_info;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            fastcgi_param HTTPS $http_x_forwarded_proto if_not_empty;
        }
    
        location ~ /\. { deny all; }        # .env, .git 차단
    }

    fastcgi_pass php:9000 — IP 가 아니라 compose 서비스 이름입니다. 컨테이너가 재시작돼 IP 가 바뀌어도 이름은 그대로입니다.

  4. 기동하고 컨테이너에 직접 물어본다

    $ docker compose config --quiet   # 문법 검사
    $ docker compose up -d
    $ docker compose ps               # db 가 healthy 인지
    $ curl -I http://127.0.0.1:8083/  # ★ 호스트 nginx 연결 전에 확인

    이 순서가 중요하다 — 뒤(컨테이너)가 응답하는 걸 확인한 다음 앞(호스트 nginx)을 붙입니다. 반대로 하면 502 가 났을 때 원인이 앞인지 뒤인지 알 수 없습니다.

  5. 호스트 nginx 에 길 추가

    upstream mysite_stack { server 127.0.0.1:8083; keepalive 16; }
    
    server {
        listen 443 ssl;
        server_name mysite.breathed.kr;
        ssl_certificate     /etc/letsencrypt/live/breathed.kr/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/breathed.kr/privkey.pem;
        include /etc/letsencrypt/options-ssl-nginx.conf;
    
        location / {
            proxy_pass http://mysite_stack;
            proxy_http_version 1.1;
            proxy_set_header Connection "";
            proxy_set_header Host              $host;
            proxy_set_header X-Real-IP         $remote_addr;
            proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    $ nginx -t                  # ★ 반드시 먼저
    $ systemctl reload nginx    # reload 는 접속을 끊지 않는다
    
    # 인증서에 이름 추가
    $ certbot --nginx -d breathed.kr -d www.breathed.kr \
          -d teams.breathed.kr -d insoo.breathed.kr -d mysite.breathed.kr

    nginx -t 를 건너뛰지 말 것 — 문법 오류인 채로 restart 하면 nginx 가 아예 뜨지 않아 서버의 모든 사이트가 동시에 죽습니다.

마무리 점검

$ ss -tlpn | grep 8083     # 127.0.0.1 인지 (0.0.0.0 이면 즉시 고칠 것)
$ ss -tlpn | grep 3306     # 아무것도 안 나와야 정상
$ iptables -S DOCKER-USER  # 끝줄이 -j DROP
$ docker inspect mysite-web-1 --format '{{.HostConfig.RestartPolicy.Name}}'

실제로 부딪힌 함정

문서에 안 나오는 것들 — 다음에 또 만난다

trap 01 · 이미지

php:8.5 에는 이미 들어 있는 확장이 있다

mbstringZend OPcache 는 공식 이미지에 내장돼 있습니다. 모르고 다시 설치하면 만들 게 없어 이미지 빌드가 실패합니다.

cp: cannot stat 'modules/*': No such file or directory

확장을 추가하기 전에 항상 docker run --rm php:8.5-fpm php -m 으로 먼저 확인하세요.

trap 02 · 버전 차이

MySQL 8.4 부터 옛 옵션이 사라졌다

--default-authentication-plugin제거됐습니다. 지정하면 기동이 실패하고, 더 나쁘게는 데이터 폴더가 반쯤 초기화된 채로 깨집니다. 그 경우 db/data 를 비우고 다시 시작해야 합니다. 후속 옵션은 --authentication-policy 입니다.

trap 03 · 보안

Docker 는 방화벽을 우회한다

포트는 반드시 127.0.0.1:8083:80 형태로 씁니다. 8083:80 이라고만 쓰면 0.0.0.0 에 열려 인터넷에 그대로 노출되고, ufw 에서 막아놨어도 통하지 않습니다.

trap 04 · 서버 이전

가동 중인 MySQL 데이터 폴더를 복사하면 안 된다

파일 크기가 같아 보여도 InnoDB 는 메모리에 아직 안 내려간 페이지가 있어 일관성이 보장되지 않습니다. mysqldump --single-transaction 으로 덤프해서 적재하세요.

trap 05 · nginx

iflocation 보다 먼저 실행된다

server 레벨의 if ($host = …) { return 301; } 은 rewrite 단계에서 location 매칭을 앞지릅니다. 인증서 갱신용 /.well-known/acme-challenge/ location 을 아무리 앞에 둬도 리다이렉트가 먼저 먹습니다. 리다이렉트를 location / 안으로 옮겨야 합니다.

trap 06 · 작업 습관

파이프·리다이렉트가 결과를 숨긴다

자주 쓰는 명령

스택 폴더로 이동한 뒤 실행한다

$ cd /home/sites/insoo        # 또는 /home/sites/breathed

$ docker compose ps            # 상태
$ docker compose up -d         # 켜기
$ docker compose down          # 끄기 (데이터는 남는다)
$ docker compose restart php   # 코드 바꾼 뒤 반영
$ docker compose logs -f php   # 로그 실시간 (Ctrl+C 로 나옴)
$ docker compose build php     # Dockerfile 고친 뒤
$ docker compose exec php bash # 컨테이너 안으로

# DB 접속
$ docker compose exec db mysql -uroot \
      -p"$(grep MYSQL_ROOT_PASSWORD .env | cut -d= -f2)" insoo

# 호스트 쪽
$ nginx -t && systemctl reload nginx
$ certbot certificates
$ certbot renew --dry-run      # 갱신이 되는지 미리 확인
$ ufw status
$ fail2ban-client status sshd

down 은 컨테이너만 지웁니다. DB 데이터는 db/data, 사이트 파일은 app/·public/서버 쪽에 남아 있으니 안심하고 써도 됩니다. 다시 up -d 하면 그대로 돌아옵니다.

현재 구성

2026-09-15 실측

항목
서버breathed-home · Ubuntu 26.04.1 LTS
CPU · 메모리4 core i3-8121U · 7.2 GiB (+swap 4 GiB)
디스크/ 228G (204G 여유) · /home 469G (433G 여유)
네트워크공인 IP 가 랜카드에 직결 (NAT 아님)
Docker29.8.0 · Compose v5.5.1 · data-root /home/docker
컨테이너6개 · 합계 약 1.0 GiB
CMS 데이터14 테이블 · 게시글 288 · 첨부 3,697 · 회원 54
업로드12 GB (teams/uploads)
TLS인증서 1장에 이름 4개 · 만료 2026-12-13 · 자동 갱신
방화벽ufw (22 rate-limit · 80 · 443) + fail2ban + DOCKER-USER 기본 DROP
백업스택별 backup.sh — DB 14일 · 코드 8주

알아둘 것

공인 IP 가 DHCP 로 배정된다

랜카드가 proto dhcp 로 주소를 받고 있습니다. 통신사가 다른 IP 를 주면 도메인이 가리키는 곳이 어긋나 사이트가 멈춥니다. 고정 IP 인지 확인하거나 DDNS 를 걸어두어야 합니다.

업로드 12GB 는 자동 백업에서 빠져 있습니다. 같은 디스크에 복사하는 것은 디스크 고장에 무의미해서 일부러 제외했습니다. 외부 스토리지 정책이 별도로 필요합니다.