SQL 주입 (SQL Injection)
핵심 인사이트 (3줄 요약)
- 본질: SQL 주입(SQL Injection)은 애플리케이션이 SQL 쿼리를 구성할 때 사용자 입력을 안전하게 분리하지 않고 문자열 연결(Concatenation)로 쿼리에 삽입하여, 공격자가 의도하지 않은 SQL 명령을 실행할 수 있게 하는 취약점이다.
- 가치: OWASP Top 10에서 지속적으로 상위권을 차지하며, 2017년 Equifax 데이터 유출(1억4,700만 명 정보 유출), 2022년 LAUSD 데이터 유출 등 대규모 보안 사고의主要原因이다. 성공 시 전체 데이터베이스의 데이터 유출, 수정, 삭제, 심지어 OS 명령 실행까지 가능하다.
- 융합: SQL 주입은 데이터베이스(DBMS) 내부 동작(쿼리 파싱, 테이블 조인, 권한 모델), 네트워크 보안(방화벽, IDS), 운영체제(파일 시스템 접근, 명령 실행), 암호학(DB 내 암호화된 데이터 유출)과 깊이 결합한다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
개념 정의
SQL 주입은 애플리케이션이 데이터베이스와 상호작용할 때 정적 SQL 쿼리骨架에 사용자 입력값을 문자열 연결이나 포맷팅으로 직접 삽입하여, 입력값이 SQL 쿼리의 문법적 일부로 해석되게 하는 공격 기법이다. 예를 들어 SELECT * FROM users WHERE id = ' + userInput + '에서 userInput에 1' OR '1'='1을 입력하면 쿼리가 SELECT * FROM users WHERE id = '1' OR '1'='1'이 되어 조건이 항상 참이 되어 전체 사용자 데이터가 반환된다. 공격자는 이 기법을 확장하여 UNION 주입, 블라인드 주입, 시간 지연 주입 등을 통해 데이터베이스의 모든 정보에 접근하거나, 다중 문(Multiple Statements)을 지원하면 DROP TABLE, DELETE 등破坏적 명령도 실행할 수 있다.
필요성
SQL 주입이 발생하는根本적 이유는 SQL 쿼리가 데이터와 코드를 구분하지 않는다는 점이다. SQL 문법에서 문자열 구분이 명확하지 않으면 입력값의 일부가 쿼리 구조의 확장으로 해석되어, 본래 의도한 데이터 조회 功能이 아닌 명령 실행으로 전이될 수 있다. RDBMS厂商들이PreparedStatement, Parameterized Query 등의 안전한 API를 제공함에도 불구하고, 레거시 시스템이나 개발자의无知로 인해 위험한 문자열 연결 방식의 쿼리 구성이 여전히 광범위하게 존재한다. 또한 화이트박스/블랙박스 테스팅 도구로도 모든 동적 쿼리를漏らさず探索하기 어려워, 코드 레벨의 보안 리뷰 없이는 취약점 발견이 어렵다.
💡 비유
SQL 주입은 고급 레스토랑의 주방에서 요리사에게 "주문표에 적힌 재료로만 요리해달라"고 하는데, 누군가 주문표에 "포장 후毒약 넣어서 가져다줘"라고 적어서 주문표 자체를 요리사로 오인하게 하는 것과 같다. 원래 주문표(쿼리骨架)는 재료 목록(데이터)이었을 뿐인데, 주문표가 요리 명령(코드)으로 인식되어 의도치 않은 결과(데이터 유출/削除)가 발생한다.
등장 배경 및 발전 과정
SQL 주입은 1998년 스필portuna Magazine에 등재될 만큼初期 웹에서 이미知られていた 취약점이었다. 2002년 @stake라는 보안 회사가公开한 SQL 주입 튜토리얼이 업계에 확산의 계기가 됐으며, 2005년 Sony Pictures, 2006년 Adobe, 2007년 Heartland Payment Systems 등 대기업 유출 사고가 연이어 발생했다. 2008년 Bernardo Damele와 G0tmi1k가 MySQL의 INTO OUTFILE 기능을 利用한 OS 명령 실행 방법을 공개하면서, SQL 주입은 단순 데이터 탈취를 넘어 시스템 침투의 발판이 되었다. 현재는 OWASP ESAPI, Hibernate, MyBatis, SQLAlchemy 등 ORM 라이브러리가PreparedStatement를 기본으로 지원하여 신규 개발에서의 SQL 주입 위험은 크게 줄었지만, 동적 쿼리 빌더나 복잡한 검색 기능에서는 여전히 주의가 필요하다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
SQL 주입 유형
SQL 주입은 공격 방식과目標에 따라 유니온 기반(Union-based), 오류 기반(Error-based), 블라인드(Blind), 시간 지연(Time-based blind), 스택된 쿼리(Stacked Queries) 등으로 분류된다. 각 유형은 서로 다른 데이터베이스 특성을 利用하며, 방어 방법도稍有 다르다.
┌─────────────────────────────────────────────────────────────────────┐
│ SQL 주입 유형별 분류 및 동작 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [1. 유니온 기반 주입 (Union-based)] │
│ SELECT name, price FROM products WHERE id = '1' │
│ UNION SELECT username, │
│ password FROM users -- │
│ │
│ ◀── 원본 쿼리와 공격자 쿼리의 결과를 합침 (UNION) │
│ │
│ [2. 오류 기반 주입 (Error-based)] │
│ SELECT * FROM users WHERE id = '1' AND 1=CONVERT(int, │
│ (SELECT TOP 1 table_name FROM information_schema. │
│ tables))-- │
│ │
│ ◀── 의도적 SQL 오류를 통해 데이터베이스 내부 정보 추출 │
│ │
│ [3. 블라인드 주입 (Blind Injection)] │
│ SELECT * FROM users WHERE id = '1' AND 1=1 ◀── TRUE: 페이지 정상 │
│ SELECT * FROM users WHERE id = '1' AND 1=2 ◀── FALSE: 페이지 다름 │
│ │
│ ◀── 오류 메시지 없이 TRUE/FALSE 응답만으로 데이터 추출 │
│ │
│ [4. 시간 지연 주입 (Time-based Blind)] │
│ SELECT * FROM users WHERE id = '1'; WAITFOR DELAY '0:0:5'-- │
│ │
│ ◀── 응답 시간 차이로 TRUE/FALSE 판단 (MySQL: SLEEP(), PostgreSQL: pg_sleep())│
│ │
│ [5. 스택된 쿼리 (Stacked Queries)] │
│ SELECT * FROM users WHERE id = '1'; DROP TABLE users-- │
│ │
│ ◀── ;로 다중 문 실행 (PHP mysqli_multi_query(), SQL Server) │
│ │
└─────────────────────────────────────────────────────────────────────┘
[다이어그램 해설] 유니온 기반 주입은 원본 쿼리와 공격자 쿼리의 결과 집합을 UNION으로 결합하여, 원래 접근 불가했던 테이블(예: users)의 데이터를 추출한다. UNION 연산은 양쪽 쿼리의 컬럼 수가 같아야 하므로, 공격자는 컬럼 수를 맞추기 위해 trial-and-error로 컬럼을 채워나간다. 오류 기반 주입은 데이터베이스의 오류 메시지에 정보가 포함되는 점을 利用하며, CONVERT()나 EXTRACTVALUE() 등 의도적 오류를 발생시켜 information_schema로부터 메타데이터를 추출한다. 블라인드 주입은 더 이상 오류나 UNION을 利用할 수 없는 경우에도, TRUE/데局面별 응답 차이(페이지 내용, HTTP 상태码,是否存在 여부)로 정보를 추출한다. 시간 지연 주입은 데이터베이스에 SLEEP() 같은 시간 지연 함수를注入하여 응답 시간으로 TRUE/FALSE를 판단하며, 가장隐秘적이지만 네트워크 지연에 영향을 받는다.
PreparedStatement 동작 원리
PreparedStatement(파라미터화된 쿼리)는 SQL 주입 방어의根本적인 방법이다. 그 동작原理를 이해하면 왜 문자열 연결이 위험하고 PreparedStatement가 안전한지 명확해진다.
┌─────────────────────────────────────────────────────────────────────┐
│ PreparedStatement vs 문자열 연결 쿼리 동작 비교 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [위험한 문자열 연결 방식] │
│ │
│ String query = "SELECT * FROM users WHERE id = '" + userId + "'"; │
│ │
│ userId = "1' OR '1'='1" ──▶ │
│ SELECT * FROM users WHERE id = '1' OR '1'='1' ◀── 조건 항상 참 │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 쿼리 구조가 컴파일 때 고정되지 않고,用户提供자 입력에 따라 동적으로 │ │
│ │ 변조될 수 있음 → 공격자가 쿼리 구조 자체를 변경할 수 있음 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ [안전한 PreparedStatement 방식] │
│ │
│ String query = "SELECT * FROM users WHERE id = ?"; │
│ PreparedStatement pstmt = conn.prepareStatement(query); │
│ pstmt.setString(1, userId); │
│ │
│ userId = "1' OR '1'='1" ──▶ │
│ SELECT * FROM users WHERE id = '1\' OR \'1\'=\'1' ◀── 전체가 문자열 │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 쿼리骨架가 DBMS에 먼저 전달되어 컴파일(파싱, 최적화)됨 │ │
│ │ 이후 ? 파라미터에만 값이 바인딩되므로, 값이 쿼리 구조로 해석될 │ │
│ │ 수 없음 → 구조와 데이터가 엄격히 분리됨 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
[다이어그램 해설] 문자열 연결 방식에서는 SQL 쿼리가 실행 시점에 처음 생성되므로, 공격자의 입력인 1' OR '1'='1이 쿼리骨架의 WHERE id = ' 부분과 결합하여 OR '1'='1'라는 새로운 조건절을 생성한다. 반면 PreparedStatement에서는 먼저 SELECT * FROM users WHERE id = ?라는 쿼리骨架가 DBMS에 전송되어 컴파일(파싱, 실행 계획 생성)된다. 그 후 setString(1, userId)를 통해 파라미터 값만 별도로 바인딩되는데, 이때 userId 값인 1' OR '1'='1은 전체가 단순한 문자열 데이터로 취급되어 quoting 처리되고, 절대로 쿼리 구조로 해석되지 않는다. 이것이 PreparedStatement가 SQL 주입을防止하는 핵심 원리이며, 모든 동적 사용자 입력에 대해 PreparedStatement 또는 ORM의 파라미터 바인딩을 사용해야 한다.
다중 방어 레이어
SQL 주입 방어에는 PreparedStatement가 가장 효과적이지만, 그것만으로는 충분하지 않다. Defense in Depth 원칙에 따라 입력 검증, 최소 권한, 출력 인코딩, 오류 처리, 모니터링 등의 추가적 방어 레이어가 필요하다.
┌─────────────────────────────────────────────────────────────────────┐
│ SQL 주입 다층 방어 전략 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: 입력 검증 │
│ - 화이트리스트: numeric 타입 파라미터는 숫자만 허용 │
│ - SQL 키워드(DROP, UNION, --, /*) 감지 시 거부 │
│ │
│ Layer 2: PreparedStatement (파라미터화 쿼리) │
│ - 모든 동적 SQL에는 ? 파라미터 사용 │
│ - ORM (Hibernate, MyBatis) 활용 │
│ │
│ Layer 3: 최소 권한 원칙 │
│ - 애플리케이션 DB 계정은 DDL 권한 없음 │
│ - 읽기 전용 테이블 접근만 허용 │
│ │
│ Layer 4: 오류 처리 및 정보 숨김 │
│ - 상세 DB 오류 메시지를 사용자에게 노출하지 않음 │
│ - 일반화된 오류 페이지만 표시 │
│ │
│ Layer 5: 웹 애플리케이션 방화벽 (WAF) │
│ - SQL 주입 패턴을 서명으로 탐지하여 사전 차단 │
│ │
│ Layer 6: 모니터링 및 로깅 │
│ - 의심스러운 SQL 쿼리 패턴 감시 │
│ - 비정상 쿼리 발생 시 알림 │
│ │
└─────────────────────────────────────────────────────────────────────┘
[다이어그램 해설] Layer 1 입력 검증은 첫 번째 관문으로, 예상치 못한 입력 형태를early에 차단한다. numeric ID에는 regexp ^[0-9]+$처럼 숫자만 허용하고, UNION, DROP, --, /*, */ 등의 SQL 주입 시그니처를 탐지하여 거부한다. Layer 2 PreparedStatement가 핵심 방어선이지만, Layer 3 최소 권한 원칙은万一 PreparedStatement를 우회하거나 적용되지 않은 쿼리가 있을 경우 피해 규모를 최소화한다. Layer 4 오류 처리는 SQL 오류 상세 정보를 사용자에게 노출하지 않아 블라인드 주입의情報 수집을 방해한다. Layer 5 WAF는 네트워크 경계에서 알려진 SQL 주입 패턴을 탐지하고, Layer 6 모니터링은 새로운 우회手法를 탐지하여 보안 팀에 경고한다.
- 📢 섹션 요약 비유: SQL 주입 방어는 은행 금고의 다중 잠금장치와 같다. 예보인(PreparedStatement)은 금고 문(쿼리 구조)을 열고, 방문자证件 확인(입력 검증)은 입구에서, 권한分层(최소 권한)은 금고 관리 규칙을, 보안 요원(WAF)은 외부 위협을 탐지하며, CCTV(모니터링)는 이상 상황을 기록한다.
Ⅲ. 융합 비교 및 다각도 분석
비교 분석: ORM vs 순수 SQL vs 동적 쿼리 빌더
애플리케이션이 데이터베이스에 접근하는 방식에 따라 SQL 주입 위험 수준이 크게 달라진다. ORM은 대부분의 일반적 操作에서 안전하지만, 동적 쿼리 빌더나 네이티브 쿼리에서는 주의가 필요하다.
| 접근 방식 | SQL 주입 위험 | 장점 | 주의사항 |
|---|---|---|---|
| PreparedStatement | 매우 낮음 | 파라미터와 쿼리 구조 완전 분리 | 동적 컬럼/테이블명에는 사용 곤란 |
| ORM (Hibernate, JPA) | 낮음 | 파라미터 자동 바인딩 | 네이티브 쿼리, 동적 쿼리 주의 |
| 쿼리 빌더 (QueryDSL) | 중간 | 동적 쿼리에较强 유연성 | 메서드 체이닝 올바른 사용 필수 |
| 동적 SQL 문자열 연결 | 매우 높음 | 최대 유연성 | 절대 사용 금지 |
과목 융합 관점
- 네트워크 보안: WAF가 SQL 주입 패턴을 서명으로 탐지하고, IPS가 의심스러운 트래픽을 차단한다. 그러나 새로운 우회手法(WAF 우회 SQL 주입)에는 대응이 어려울 수 있어, WAF는 보조 수단으로만 사용해야 한다.
- 운영체제: 데이터베이스 서버의 OS 수준 권한을最小화하고, DBMS 프로세스의 파일 시스템 접근을 제한하여,万一 SQL 주입으로 OS 명령 실행이 가능하더라도 피해 규모를 최소화한다.
- 암호학: 데이터베이스 내 민감 데이터(개인정보, 비밀번호 등)는 암호화되어 저장되어야 하며, SQL 주입으로 DB 접근이 성공하더라도 암호화된 데이터의 복호화 키는 분리 관리되어야 한다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 로그인 인증 우회:
SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'형태의 쿼리로 인증하는 레거시 시스템에서, username에 admin'--를 입력하면 비밀번호 검증이コメント아웃되어 관리자 계정으로 인증 없이 로그인되는 상황. 아키텍트는 PreparedStatement로 전환하고, 계정 잠금 정책(5회 실패 시 30분锁定)을 추가하여 무차별 대입 공격도 함께 방어했다. -
시나리오 — 블라인드 주입을 통한 데이터 추출: 상세 오류 메시지가 비활성화된 환경에서, 공격자가 TRUE/FALSE 응답 차이(페이지 내용 변경, HTTP 상태码)로 비밀번호 해시의 각 문자를 순차적으로 추출하는 상황. 아키텍트는 응답 시간 기반 탐지(평균 응답 시간 + 3σ 임계값), 요청 빈도 제한, CAPTCHA 추가 도입으로 자동화된 블라인드 주입 봇을 차단했다.
┌─────────────────────────────────────────────────────────────────────┐
│ 블라인드 SQL 주입 시 eq Literate 추출 과정 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ [목표: users 테이블의 password 해시 첫 문자 추출] │
│ │
│ 기본 쿼리: SELECT * FROM users WHERE id = 1 │
│ │
│ 문자 추출 쿼리 (TRUE: admin@demo.com 페이지 정상 반환) │
│ SELECT * FROM users WHERE id = 1 AND │
│ SUBSTRING(password, 1, 1) = 'a' ◀── 맞으면 TRUE │
│ │
│ [二分 탐색으로 효율적 추출] │
│ │
│ SUBSTRING(password, 1, 1) > 'm' ◀── FALSE (첫 문자 ≤ 'm') │
│ SUBSTRING(password, 1, 1) > 'g' ◀── TRUE (첫 문자 > 'g') │
│ SUBSTRING(password, 1, 1) > 'j' ◀── FALSE (첫 문자 ≤ 'j') │
│ ... │
│ 결과: 첫 문자가 'h'임을 확인 │
│ │
│ 반복: 두 번째 문자, 세 번째 문자... 전체 해시 추출 │
│ │
│ [방어: 응답 시간 분석으로 탐지] │
│ - 정상 요청 평균 응답 시간: 50ms │
│ - SLEEP(5) 포함 요청 응답 시간: 5000ms+ │
│ - 비정상적 응답 시간 패턴 탐지 → 계정 잠금 + 보안 알림 │
│ │
└─────────────────────────────────────────────────────────────────────┘
[다이어그램 해설] 블라인드 SQL 주입은 데이터베이스가 오류 메시지나 직접 데이터를 반환하지 않더라도, TRUE/FALSE 응답 차이를利用하여 데이터를 문자별로 추출한다. SUBSTRING(password, 1, 1) = 'a'와 같은 조건을 부여하여 페이지가 정상 반환되면 TRUE(해당 문자가 맞음), 그렇지 않으면 FALSE(해당 문자가 틀림)라고 판단한다. 이 과정을 자동화하면 수백~수천 개의 요청으로 전체 비밀번호 해시를 탈취할 수 있다. 방어를 위해서는 응답 시간 분석(평균 + 표준편차 기반 이상 탐지), 요청 빈도 제한(초당 N회 이상 요청 시 CAPTCHA challenge), 계정 잠금 정책 등을 조합해야 한다. 네트워크 레벨의 IDS/IPS도 비정상적 SQL 패턴을 탐지하여 차단할 수 있다.
도입 체크리스트
- 기술적: 모든 SQL 쿼리가 PreparedStatement 또는 파라미터 바인딩을 使用하고 있는가? 동적 테이블명/컬럼명을 다루는 쿼리에 대한 보완 검증이 있는가?
- 운영·보안적: DB 계정의 권한이 필요한 최소한으로 설정되어 있는가? SQL 오류 상세 메시지가 사용자에게 노출되지 않는가? WAF에서 SQL 주입 탐지 서명이 업데이트되고 있는가?
안티패턴
-
문자열 연결 SQL 구성: 사용자 입력을 SQL 쿼리 문자열에 직접 연결하는 것은 명백한 SQL 주입 취약점이다.
-
네이티브 쿼리濫用: ORM에서 네이티브 SQL(@NativeQuery)나 동적 쿼리 빌더를 사용할 때, 파라미터 바인딩 없이 문자열을 구성하면 PreparedStatement 도입 의미가 없어진다.
-
오류 메시지 노출: 데이터베이스 오류 상세 메시지(테이블명, 컬럼명, SQL 문법)를 사용자에게 그대로 노출하면, 블라인드 주입의情報 수집이 획기적으로簡単화된다.
-
📢 섹션 요약 비유: SQL 주입은 레스토랑 주방에서 요리사에게 재료 목록(쿼리骨架)만渡고, 누군가 그 목록에 "毒약 넣어서"라는 지시(사용자 입력)를 추가하여 주방 전체(데이터베이스)를 위험에 빠뜨리는 것과 같다. 재료와 지시를 분리(PreparedStatement)하고, 수상한 재료는 입구에서 검사(입력 검증)하며, 주방 접근 권한을 제한(최소 권한)하는 다중 방어가 필요하다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 방어 도입 전 | 방어 도입 후 | 개선 효과 |
|---|---|---|---|
| 정량 | SQL 주입 취약점 12건 | 0건 | 취약점 100% 제거 |
| 정량 | 데이터 유출 위험 100% | PreparedStatement +最小権限 적용으로 위험 최소화 | 위험 95% 감소 |
| 정성 | 레거시 문자열 연결 SQL, 보안 기술 부채 | ORM/PreparedStatement 전환 | 보안 기술 부채 해소 |
미래 전망
SQL 주입은 20년 넘게 웹 보안의 주요 위협으로 자리잡고 있으나, ORM의 보급과 개발자 보안 인식 향상에 따라 감소 추세이다. 그러나 레거시 시스템의 방어 되지 않는 SQL 주입은 여전히 큰 위험이며, GraphQL, NoSQL 등 새로운 API 기술의 등장은 NoSQL 주입이라는 새로운 공격 벡터를 만들어냈다. 또한 ORM의 복잡한 동적 쿼리 기능이나 저장 프로시저(Stored Procedure)의 잘못된 사용도 SQL 주입과 유사한 취약점을 만들 수 있어, 개발자는 SQL뿐만 아니라 모든 데이터베이스 상호작용에서 입력의 분리/검증을意識해야 한다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| PreparedStatement | SQL 주입 방어의根本方法으로, 쿼리骨架와 사용자 입력을 분리하여 입력이 쿼리 구조로 해석되는 것을防止한다. |
| ORM (Object-Relational Mapping) | Hibernate, JPA, SQLAlchemy 등 ORM 라이브러리는 대부분의操作에서 파라미터 바인딩을 자동 적용하여 SQL 주입 위험을줄인다. |
| 블라인드 주입 (Blind Injection) | 오류 메시지나 데이터 반환이 없는 상황에서도 TRUE/FALSE 응답 차이로 데이터를 추출하는 고난도 공격 기법이다. |
| WAF (Web Application Firewall) | 네트워크 경계에서 SQL 주입 패턴을 서명으로 탐지하여 공격을 사전 차단하며, 애플리케이션 레벨 방어를補完한다. |
| Stored Procedure | 사전 정의된 SQL 쿼리를 DB에 저장하여 사용하면 동적 SQL 구성을 줄일 수 있지만, 문자열 연결로 작성되면 주입 취약점이 될 수 있다. |
👶 어린이를 위한 3줄 비유 설명
-
SQL 주입은 편지 내용에 위조 지시를 넣는 것과 같아요. "사과 5개 보내줘"라고 쓴 주문서에, "그리고毒薬도 같이 넣어줘"라고 내용을篡改해서 보내면, 받은 사람이 원래 주문이 아니라 그 지시를 실행하게 돼요.
-
컴퓨터에서는 데이터베이스에 "사용자 이름 찾기"라는 질문을 하는데, 누군가 이름 자리에 "비밀 데이터 다 보여줘"라고PROGRAM를 조작해버릴 수 있어요. 그래서 안전한 방법(PreparedStatement)은 질문의 구조와 넣고 싶은 말을 미리 분리해서, 말 자체가PROGRAM로 해석되지 않게 하는 거예요.
-
만약 위조 지시가 가능하면, 가게 전체를 무너뜨리거나(DELETE DATABASE), 다른 사람 정보를 빼돌릴 수 있어요. 그래서 컴퓨터에서는 질문 내용(QUERY)과 넣고 싶은 말(DATA)을 꼭! 분리해야 하며, 추가로 여러 안전장치(입력 검증, 권한 제한, 오류 숨김)를 같이 사용해요.