PHP : 예외 vs 오류?


116

PHP 매뉴얼 어딘가에서 누락되었을 수 있지만 오류와 예외의 차이점은 정확히 무엇입니까? 내가 볼 수있는 유일한 차이점은 오류와 예외가 다르게 처리된다는 것입니다. 그러나 예외의 원인과 오류의 원인은 무엇입니까?

답변:


87

예외가 발생합니다. 예외가 발생 합니다. 오류는 일반적으로 복구 할 수 없습니다. 예를 들어, 데이터베이스에 행을 삽입하는 코드 블록이 있습니다. 이 호출이 실패 할 수 있습니다 (중복 ID).이 경우 "예외"인 "오류"가 발생합니다. 이 행을 삽입 할 때 다음과 같이 할 수 있습니다.

try {
  $row->insert();
  $inserted = true;
} catch (Exception $e) {
  echo "There was an error inserting the row - ".$e->getMessage();
  $inserted = false;
}

echo "Some more stuff";

프로그램 실행은 계속됩니다-예외를 '발견'했기 때문입니다. 예외가 발견되지 않으면 오류로 처리됩니다. 실패 후에도 프로그램 실행을 계속할 수 있습니다.


29
Errors are generally unrecoverable<-사실, 이것은 사실이 아닙니다. E_ERROR그리고 E_PARSE가장 일반적인 두 가지 복구 할 수없는 오류 (다른 사람의 몇 가지가있다)하지만 DEV에서 당신이 볼 오류의 대부분은 (복구 할 수 있습니다 E_NOTICE, E_WARNING등). 불행히도 PHP의 오류 처리는 완전히 엉망입니다. 모든 종류의 것들이 불필요하게 오류를 유발합니다 (예를 들어, 대부분의 파일 시스템 기능). 일반적으로 예외는 "OOP 방식"이지만, 안타깝게도 PHP의 기본 OOP API 중 일부는 예외 대신 오류를 사용합니다
DaveRandom

1
@DaveRandom E_NOTICE, E_WARNING은 정의상 "오류"가 아닙니다. 나는 항상 프로그래머가 작성한 코드에 문제가있을 수 있음을 프로그래머에게 알리기 위해 '메시지'PHP 디스플레이라고 생각했습니다.
slhsen

2
@slhsen 문제는 정말 엉터리 용어입니다. 이러한 메시지의 모든 형태는 PHP의 "오류 처리 시스템"을 통과합니다. 의미 상 통지 / 경고가 "와 거의 동일하지 않더라도 의미 상 모든 이벤트는"오류 "입니다. 그 맥락에서 오류 ". 고맙게도 다가오는 PHP7은 최소한 이러한 것들을 잡을 수있는 예외로 바꾸는 (새로운 Throwable인터페이스를 통해) 이 문제를 분류 할 수있는 길을 열었고 , 둘 다 실제를 구별하고 적절하게 처리하는 훨씬 더 표현적이고 절대적인 방법을 제공했습니다. 문제 및 권고 메시지
DaveRandom

"프로그램 실행이 계속됩니다"가 변경되었다고 생각합니까? PHP는 "예외가 발생하면 명령문 다음에 오는 코드는 실행되지 않습니다"라고 말하므로 ( php.net/manual/en/language.exceptions.php )
Robert Sinclair

1
OP가 의미하는 것은 ErrorVS의 자손과 Exception.
XedinUnknown

55

나는 일반적으로 set_error_handler오류를 받아들이고 예외를 던지는 함수를 사용하므로 어떤 일이 발생하더라도 처리 할 예외가 있습니다. 더 이상은 없어@file_get_contents 멋지고 깔끔한 try / catch가 .

디버그 상황에서는 페이지와 같은 asp.net을 출력하는 예외 처리기도 있습니다. 나는 이것을 도로에 게시하고 있지만 요청이 있으면 나중에 예제 소스를 게시 할 것입니다.

편집하다:

약속대로 추가하여 샘플을 만들기 위해 일부 코드를 잘라서 붙여 넣었습니다. 아래 파일을 내 워크 스테이션에 파일로 저장했습니다 . 링크가 끊어 졌기 때문에 더 이상 여기에서 결과를 볼 없습니다 .

<?php

define( 'DEBUG', true );

class ErrorOrWarningException extends Exception
{
    protected $_Context = null;
    public function getContext()
    {
        return $this->_Context;
    }
    public function setContext( $value )
    {
        $this->_Context = $value;
    }

    public function __construct( $code, $message, $file, $line, $context )
    {
        parent::__construct( $message, $code );

        $this->file = $file;
        $this->line = $line;
        $this->setContext( $context );
    }
}

/**
 * Inspire to write perfect code. everything is an exception, even minor warnings.
 **/
function error_to_exception( $code, $message, $file, $line, $context )
{
    throw new ErrorOrWarningException( $code, $message, $file, $line, $context );
}
set_error_handler( 'error_to_exception' );

function global_exception_handler( $ex )
{
    ob_start();
    dump_exception( $ex );
    $dump = ob_get_clean();
    // send email of dump to administrator?...

    // if we are in debug mode we are allowed to dump exceptions to the browser.
    if ( defined( 'DEBUG' ) && DEBUG == true )
    {
        echo $dump;
    }
    else // if we are in production we give our visitor a nice message without all the details.
    {
        echo file_get_contents( 'static/errors/fatalexception.html' );
    }
    exit;
}

function dump_exception( Exception $ex )
{
    $file = $ex->getFile();
    $line = $ex->getLine();

    if ( file_exists( $file ) )
    {
        $lines = file( $file );
    }

?><html>
    <head>
        <title><?= $ex->getMessage(); ?></title>
        <style type="text/css">
            body {
                width : 800px;
                margin : auto;
            }

            ul.code {
                border : inset 1px;
            }
            ul.code li {
                white-space: pre ;
                list-style-type : none;
                font-family : monospace;
            }
            ul.code li.line {
                color : red;
            }

            table.trace {
                width : 100%;
                border-collapse : collapse;
                border : solid 1px black;
            }
            table.thead tr {
                background : rgb(240,240,240);
            }
            table.trace tr.odd {
                background : white;
            }
            table.trace tr.even {
                background : rgb(250,250,250);
            }
            table.trace td {
                padding : 2px 4px 2px 4px;
            }
        </style>
    </head>
    <body>
        <h1>Uncaught <?= get_class( $ex ); ?></h1>
        <h2><?= $ex->getMessage(); ?></h2>
        <p>
            An uncaught <?= get_class( $ex ); ?> was thrown on line <?= $line; ?> of file <?= basename( $file ); ?> that prevented further execution of this request.
        </p>
        <h2>Where it happened:</h2>
        <? if ( isset($lines) ) : ?>
        <code><?= $file; ?></code>
        <ul class="code">
            <? for( $i = $line - 3; $i < $line + 3; $i ++ ) : ?>
                <? if ( $i > 0 && $i < count( $lines ) ) : ?>
                    <? if ( $i == $line-1 ) : ?>
                        <li class="line"><?= str_replace( "\n", "", $lines[$i] ); ?></li>
                    <? else : ?>
                        <li><?= str_replace( "\n", "", $lines[$i] ); ?></li>
                    <? endif; ?>
                <? endif; ?>
            <? endfor; ?>
        </ul>
        <? endif; ?>

        <? if ( is_array( $ex->getTrace() ) ) : ?>
        <h2>Stack trace:</h2>
            <table class="trace">
                <thead>
                    <tr>
                        <td>File</td>
                        <td>Line</td>
                        <td>Class</td>
                        <td>Function</td>
                        <td>Arguments</td>
                    </tr>
                </thead>
                <tbody>
                <? foreach ( $ex->getTrace() as $i => $trace ) : ?>
                    <tr class="<?= $i % 2 == 0 ? 'even' : 'odd'; ?>">
                        <td><?= isset($trace[ 'file' ]) ? basename($trace[ 'file' ]) : ''; ?></td>
                        <td><?= isset($trace[ 'line' ]) ? $trace[ 'line' ] : ''; ?></td>
                        <td><?= isset($trace[ 'class' ]) ? $trace[ 'class' ] : ''; ?></td>
                        <td><?= isset($trace[ 'function' ]) ? $trace[ 'function' ] : ''; ?></td>
                        <td>
                            <? if( isset($trace[ 'args' ]) ) : ?>
                                <? foreach ( $trace[ 'args' ] as $i => $arg ) : ?>
                                    <span title="<?= var_export( $arg, true ); ?>"><?= gettype( $arg ); ?></span>
                                    <?= $i < count( $trace['args'] ) -1 ? ',' : ''; ?> 
                                <? endforeach; ?>
                            <? else : ?>
                            NULL
                            <? endif; ?>
                        </td>
                    </tr>
                <? endforeach;?>
                </tbody>
            </table>
        <? else : ?>
            <pre><?= $ex->getTraceAsString(); ?></pre>
        <? endif; ?>
    </body>
</html><? // back in php
}
set_exception_handler( 'global_exception_handler' );

class X
{
    function __construct()
    {
        trigger_error( 'Whoops!', E_USER_NOTICE );      
    }
}

$x = new X();

throw new Exception( 'Execution will never get here' );

?>

도움이 될 것입니다. PHP를 다루는 시간을 줄이는 것이 도움이 될 것입니다. :-)
Jason Baker

좋은 코드, 감사합니다. 그래도 클래스 X가 어디에서 왔는지 알 수 없으며 그 목적은 무엇입니까?
Alec

"set_exception_handler ( 'global_exception_handler');"아래의 모든 것 데모 일 뿐이며 필요하지 않습니다. 일반적으로 예외가 아닌 오류 상황에서 어떤 일이 발생하는지 보여주기위한 것입니다.
Kris

표준 PHP는 특히 일반 오류 처리기에서 throw되는 ErrorException을 정의합니다. 귀하의 게시물을 편집하고 업데이트 할 수 있습니까?
Tiberiu-Ionuț Stan 2013 년

@ Tiberiu-IonuțStan : 물론입니다.하지만 작동하는 예제는 동기화되지 않습니다. 또한, 요즘 나는 아마 사람들을 가리키는 것 github.com/theredhead/red.web/blob/master/src/lib/bootstrap.php 에서 private-void.com 대신.
Kris

21

대답은 방에있는 코끼리에 대해 이야기 할 가치가 있습니다.

오류는 런타임에 오류 조건을 처리하는 오래된 방법입니다. 일반적으로 코드는 set_error_handler일부 코드를 실행하기 전에 다음과 같은 것을 호출 합니다. 어셈블리 언어 인터럽트의 전통을 따릅니다. 다음은 몇 가지 BASIC 코드의 모습입니다.

on error :divide_error

print 1/0
print "this won't print"

:divide_error

if errcode = X
   print "divide by zero error"

set_error_handler올바른 값으로 호출 되는지 확인하기가 어려웠습니다 . 더 나쁜 것은 오류 처리기를 변경하는 별도의 프로 시저를 호출 할 수 있다는 것입니다. 또한 여러 번 호출이 set_error_handler호출 및 처리기 로 산재 해있었습니다 . 코드가 빠르게 통제에서 벗어나는 것이 쉬웠습니다. 예외 처리는 좋은 코드가 실제로하는 일의 구문과 의미를 공식화함으로써 구출되었습니다.

try {
   print 1/0;
   print "this won't print";
} catch (DivideByZeroException $e) {
   print "divide by zero error";
}

별도의 기능이 없거나 잘못된 오류 처리기를 호출 할 위험이 없습니다. 이제 코드는 동일한 위치에 있습니다. 또한 더 나은 오류 메시지가 표시됩니다.

PHP는 다른 많은 언어가 이미 바람직한 예외 처리 모델로 발전했을 때만 오류 처리 기능을 사용했습니다. 결국 PHP 제작자는 예외 처리를 구현했습니다. 그러나 이전 코드를 지원할 가능성이 높기 때문에 오류 처리를 유지하고 오류 처리를 예외 처리처럼 보이게 만드는 방법을 제공했습니다. 그 외에는 일부 코드가 예외 처리가 제공하는 오류 처리기를 재설정하지 않을 수 있다는 보장이 없습니다.

최종 답변

예외 처리가 구현되기 전에 코딩 된 오류는 여전히 오류 일 수 있습니다. 새로운 오류는 예외 일 수 있습니다. 그러나 오류가 있고 예외 인 설계 나 논리는 없습니다. 그것은 단지 그것이 코딩되었을 때 사용 가능한 것과 그것을 코딩하는 프로그래머의 선호도를 기반으로합니다.


3
이것이 예외와 오류가 공존하는 진짜 이유입니다. 처음부터 디자인 한 경우 PHP는 둘 중 하나만 포함해야합니다.
Tomas Zubiri

1
가장 상세하고 설명이 적기 때문에 제 생각에는 최고의 답변입니다.
Robert Kusznier

8

여기에 추가 할 한 가지는 예외 및 오류 처리에 관한 것입니다. 응용 프로그램 개발자의 목적을 위해 오류와 예외는 모두 응용 프로그램의 문제에 대해 알아보기 위해 기록하려는 "나쁜 것"입니다. 따라서 고객은 장기적으로 더 나은 경험을 할 수 있습니다.

따라서 예외에 대해 수행하는 작업과 동일한 작업을 수행하는 오류 처리기를 작성하는 것이 좋습니다.


링크를 제공해 주셔서 감사합니다!
Mike Moore

@ 알렉스 와인 스타 인 : 링크가 깨진
마르코 Demaio을

7

다른 답변에서 언급했듯이 오류 처리기를 예외 발생기로 설정하는 것이 PHP에서 오류를 처리하는 가장 좋은 방법입니다. 좀 더 간단한 설정을 사용합니다.

set_error_handler(function ($errno, $errstr, $errfile, $errline ) {
        if (error_reporting()) {
                throw new \ErrorException($errstr, 0, $errno, $errfile, $errline);
        }
});

error_reporting()보관 확인을 유의하십시오@ 운영자 작업을. 또한 사용자 정의 예외를 정의 할 필요가 없습니다. PHP에는이를위한 멋진 클래스가 하나 있습니다.

예외 발생의 큰 이점은 예외에 스택 추적이 연결되어 있으므로 문제가있는 곳을 쉽게 찾을 수 있다는 것입니다.


5

Re : "하지만 오류와 예외의 차이점은 정확히 무엇입니까?"

여기에 차이점에 대한 좋은 답변이 많이 있습니다. 아직 언급되지 않은 성능을 추가하겠습니다. 특히 이것은 예외 발생 / 처리와 리턴 코드 처리 (성공 또는 일부 오류)의 차이점입니다. 일반적으로, PHP,이 수단 반환 false하거나 null,하지만 그들은 더 많은 파일 업로드와 같은 설명 할 수 있습니다 http://php.net/manual/en/features.file-upload.errors.php 당신은 예외 객체를 반환 할 수 !

다른 언어 / 시스템에서 몇 가지 성능 실행을 수행했습니다. 일반적으로 예외 처리는 오류 반환 코드를 확인하는 것보다 약 10,000 배 느립니다.

따라서 절대적으로 시작하기 전에 실행을 완료해야하는 경우 시간 여행이 존재하지 않기 때문에 운이 좋지 않습니다. 시간 여행이 없으면 반환 코드가 가장 빠른 옵션입니다.

편집하다:

PHP는 예외 처리에 최적화되어 있습니다. 실제 테스트에서는 예외를 던지는 것이 값을 반환하는 것보다 2 ~ 10 배 더 느리다는 것을 보여줍니다.


3
물론입니다.하지만 예외를 던지기 위해 잃어버린주기의 양은 예외로 얻는 추가 설명 능력으로 보충 된 것보다 많습니다. 특정 유형의 예외를 throw 할 수 있으며 오류 코드를 포함하도록 예외에 데이터를 추가 할 수도 있습니다. 나는 당신의 10,000 * 주장도 심각하게 의심합니다. 시차에 대해 옳다고해도 실제 시나리오에서 return & if vs. new Execption, throw, catch에 소요되는 시간은 실행 된 코드에 비해 너무 적기 때문에 확실히 조기 최적화입니다. 예외를 던지면 90 %의 시간을 처리하는 것이 더 좋습니다.
gnarf 2012-08-02

1
1. 10,000x는 정확합니다. 언어 및 컴파일러 옵션에 따라 약간의 차이가 있습니다. 2. null / false를 반환 할 필요가 없습니다. 최대 MAX_ULONG 개의 반환 코드를 반환 할 수 있습니다. 또는 실패 문자열을 반환하고 성공 문자열이나 int 또는 null을 확인할 수 있습니다. 3. 실제 시나리오에서는 모든 클럭 사이클이 중요합니다. Facebook의 일일 활성 사용자는 5 억 5 천 5 백만 명입니다. 예외가 2 배에 불과하고 사용자 / 통과를 확인하는 데 .001 초가 걸린다고 가정하면 매일 153 시간의 처리 시간을 절약 할 수 있습니다. 10,000 배로 175 년을 절약합니다. 로그인 시도를 확인하기 위해-매일.
에반

@evan : 참고로, 여기에서 예외가있는 코드를 테스트
했는데

@MarcoDemaio 그 질문은 예외를 던지지 않고 try / catch 블록 만 다룹니다. 더 나은 테스트는 noexcept ()에 값을 반환하고 except ()에 예외를 던지는 것입니다. 또한 여러 기능을 통해 버블 링되어야합니다. stackoverflow.com/a/104375/505172 는 PHP의 차이가 실제로 54x라고 말합니다. 실시간으로 직접 테스트를 실행했는데 2 ~ 10 배 느린 것 같습니다. 이것은 예상보다 훨씬 낫습니다.
에반

@evan : 저는 걱정하지 않을 것입니다. 예상치 못한 / 복구 불가능한 오류를 추적하는 데만 예외를 사용하므로 100 배 더 느려도 상관하지 않습니다. 내 걱정은 단순히 try / catch 블록을 추가하여 코드를 느리게 만드는 것이 었습니다.
Marco Demaio 2013 년

4

나는 당신이 찾고있는 anwser가 그것이라고 생각합니다;

오류는 존재하지 않는 $ 변수를 에코하는 것과 같이 익숙한 표준 항목입니다.
예외는 PHP 5 이상에서만 발생하며 객체를 다룰 때 발생합니다.

간단하게 유지하려면 :

예외는 객체를 다룰 때 발생하는 오류입니다. try / catch 문을 사용하면 이에 대해 작업을 수행 할 수 있으며 if / else 문과 매우 유사하게 사용됩니다. 문제가되지 않으면 이렇게 해보세요.

예외를 "catch"하지 않으면 표준 오류가됩니다.

오류는 일반적으로 스크립트를 중지시키는 PHP의 기본 오류입니다.

Try / catch는 PDO와 같은 데이터베이스 연결을 설정하는 데 자주 사용됩니다. 스크립트를 리디렉션하거나 연결이 작동하지 않는 경우 다른 작업을 수행하려는 경우 괜찮습니다. 그러나 오류 메시지를 표시하고 스크립트를 중지하려는 경우 필요하지 않은 경우 포착되지 않은 예외가 치명적인 오류로 바뀝니다. 또는 사이트 전체의 오류 처리 설정을 사용할 수도 있습니다.

도움이되는 희망


3
예외는 PHP의 절차 코드에서도 사용할 수 있습니다.
Tiberiu-Ionuț Stan 2013 년

2

PHP 7.1 이상에서 catch 블록은 파이프 (|) 문자를 사용하여 여러 예외를 지정할 수 있습니다. 이것은 다른 클래스 계층 구조의 다른 예외가 동일하게 처리 될 때 유용합니다.

try {
  // do something
} catch (Error | Exception $e) {
  echo $e->getMessage();
}

1

예외는 throw, 오류를 사용하는 코드에 의해 의도적으로 throw됩니다.

오류는 일반적으로 처리되지 않는 결과로 발생합니다. (IO 오류, TCP / IP 오류, null 참조 오류)


1
이것은 반드시 사실이 아닙니다. 대부분의 경우 오류를 확인하고 반환 코드를 적절하게 의도적으로 다시 보냅니다. 사실, 그것은 모든 비 객체 지향 언어의 경우입니다. 예외는 규칙에 대한 예외이기도합니다. 두 경우 모두 문제가 발생하고이를인지하고 처리해야합니다. PHP 파일 업로드는 반환 코드를 통해 의도적 인 오류 처리의 한 예입니다 - php.net/manual/en/features.file-upload.errors.php
에반

1

오류 제어에 대해 가장 특이한 논의를하려고합니다.

몇 년 전에 아주 좋은 오류 처리기를 한 언어로 만들었고 이름 중 일부가 변경되었지만 오류 처리의 원칙은 오늘날 동일합니다. 맞춤형 멀티 태스킹 OS를 가지고 있었고 메모리 누수, 스택 증가 또는 충돌없이 모든 수준에서 데이터 오류를 복구 할 수 있어야했습니다. 그래서 다음은 오류와 예외가 어떻게 작동해야하며 어떻게 다른지에 대한 나의 이해입니다. 나는 try catch의 내부가 어떻게 작동하는지 이해하지 못한다고 말할 것이므로 어느 정도 추측하고 있습니다.

오류 처리를 위해 커버 아래에서 발생하는 첫 번째 일은 한 프로그램 상태에서 다른 프로그램 상태로 점프하는 것입니다. 어떻게 된 거죠? 나는 그것을 얻을 것이다.

역사적으로 오류는 더 오래되고 단순하며 예외는 더 새롭고 조금 더 복잡하고 유능합니다. 오류는 버블 링이 필요할 때까지 잘 작동합니다. 이는 관리자에게 어려운 문제를 전달하는 것과 같습니다.

오류는 오류 번호와 같은 숫자 일 수 있으며 때로는 하나 이상의 관련 문자열이있을 수 있습니다. 예를 들어 파일 읽기 오류가 발생하면 오류를보고 할 수 있으며 정상적으로 실패 할 수 있습니다. (헤이, 예전처럼 무너진 것보다 한 단계 올라간 것입니다.)

예외에 대해 자주 언급되지 않는 것은 예외가 특별한 예외 스택에 계층화 된 객체라는 것입니다. 프로그램 흐름에 대한 반환 스택과 비슷하지만 오류 시도 및 포착에 대해서만 반환 상태를 유지합니다. (저는 그것들을 ePush와 ePop이라고 부르 곤했는데,? Abort는 ePop과 그 레벨로 회복하는 조건부 던지기 였고, Abort는 완전 다이 또는 종료였습니다.)

스택 맨 아래에는 초기 호출자에 대한 정보가 있습니다.이 개체는 외부 시도가 시작되었을 때의 상태를 알고있는 개체입니다.이 개체는 종종 프로그램이 시작되었을 때입니다. 그 위에 또는 스택의 다음 레이어 (위는 자식, 아래는 부모)는 다음 내부 try / catch 블록의 예외 개체입니다.

시도 안에 시도를 넣으면 외부 시도 위에 내부 시도를 쌓는 것입니다. 내부 try에서 오류가 발생하고 내부 catch가 처리 할 수 ​​없거나 오류가 외부 try에 throw되면 제어가 외부 catch 블록 (객체)에 전달되어 오류를 처리 할 수 ​​있는지 확인합니다. 당신의 상사.

따라서이 오류 스택이 실제로하는 일은 프로그램 흐름과 시스템 상태를 표시하고 복원 할 수 있다는 것입니다. 즉, 프로그램이 반환 스택을 손상시키지 않고 일이 잘못되었을 때 다른 데이터 (데이터)를 엉망으로 만들지 않도록합니다. 따라서 메모리 할당 풀과 같은 다른 리소스의 상태도 저장하므로 catch가 완료되면 정리할 수 있습니다. 일반적으로 이것은 매우 복잡한 일이 될 수 있으며, 이것이 예외 처리가 종종 느린 이유입니다. 일반적으로 꽤 많은 상태가 이러한 예외 블록에 들어가야합니다.

그래서 try / catch 블록은 다른 모든 것이 엉망이되었을 때 돌아갈 수있는 상태를 설정합니다. 마치 부모 같아요. 우리의 삶이 엉망이되었을 때 우리는 부모의 무릎으로 다시 넘어갈 수 있고 그들은 다시 모든 일을 잘 할 것입니다.

실망시키지 않았 으면 좋겠어요.


1

이 댓글을 추가 할 수 있습니다.

function doSomething()
{
   /** @noinspection PhpUnhandledExceptionInspection */
   throw new Exception();
}

0

set_error_handler ()가 정의되면 오류 처리기는 Exception과 유사합니다. 아래 코드를 참조하십시오.

 <?php
 function handleErrors( $e_code ) {
   echo "error code: " . $e_code . "<br>";
 }

 set_error_handler( "handleErrors" ); 

 trigger_error( "trigger a fatal error", E_USER_ERROR);
 echo "after error."; //it would run if set_error_handler is defined, otherwise, it wouldn't show
?>
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.