본문으로 건너뛰기
오늘의 흐름
기타GeekNews··원문 약 3

단순함을 쉽게 만들기 (2011)

단순함은 요소가 서로 얽히지 않은 상태이고, 쉬움은 접근성·익숙함·개인의 역량에 따라 달라짐 — 기술을 선택할 때는 작성 편의보다 실행하고 유지보수할 소프트웨어의 구조를 평가해야 함 인간이 동시에 이해할 수 있는 요소는 제한적이므로 복잡성은 이해·변경…

단순함을 쉽게 만들기 (2011) 대표 이미지

핵심 요약

자동 요약
  1. 1단순함은 요소가 서로 얽히지 않은 상태이고, 쉬움은 접근성·익숙함·개인의 역량에 따라 달라짐 — 기술을 선택할 때는 작성 편의보다 실행하고 유지보수할 소프트웨어의…

원문 본문

출처 · GeekNews

단순함을 쉽게 만들기 (2011)

(youtube.com)1P by GN⁺ 57분전 | ★ favorite | 댓글 1개
  • 단순함은 요소가 서로 얽히지 않은 상태이고, 쉬움은 접근성·익숙함·개인의 역량에 따라 달라짐 — 기술을 선택할 때는 작성 편의보다 실행하고 유지보수할 소프트웨어의 구조를 평가해야 함
  • 인간이 동시에 이해할 수 있는 요소는 제한적이므로 복잡성은 이해·변경·디버깅 능력을 제한함 — 테스트와 타입 검사는 안전망이지만 프로그램을 이해하는 일을 대신하지 못함
  • 상태·객체·상속·조건 분기처럼 익숙한 도구도 독립적일 수 있는 개념들을 결합함 — 값·함수·데이터·독립적으로 연결 가능한 다형성·큐·선언적 처리로 같은 수준의 시스템을 더 단순하게 만들 수 있음
  • 모듈화나 캡슐화만으로는 단순해지지 않음 — 작은 추상화와 하위 구성요소를 만들고, 무엇을·누가·어떻게·언제·어디서·왜 수행하는지를 분리해야 함
  • 단순함에는 초기에 생각하고 분리하는 작업과 지속적인 주의가 필요함 — 구성요소 수가 늘더라도 얽힘이 줄면 변경의 독립성과 유연성을 확보할 수 있음

단순함과 쉬움은 다른 기준임

  • 단순함은 신뢰성의 전제조건”이라는 Dijkstra의 명제처럼, 좋은 시스템을 만들려면 단순한 시스템을 만들어야 함
  • 단순함(simple) 의 어원은 하나의 접힘·꼬임을 뜻하며, 복잡함(complex)은 여러 요소가 함께 꼬이거나 접힌 상태를 뜻함
    • 소프트웨어에서는 하나의 역할·작업·목표·개념·문제 차원에 집중하고, 다른 요소와 뒤섞이지 않는지를 살펴야 함
    • 구성요소가 하나뿐이거나 인터페이스의 연산이 하나뿐이어야 한다는 뜻은 아님
    • 중요한 것은 개수가 아니라 얽힘의 유무이므로, 단순함은 구조를 살펴 판단할 수 있는 객관적 기준임
  • 쉬움(easy) 은 가까움으로 이해할 수 있으며, 무엇에 가까운지에 따라 세 가지로 나뉨
    • 도구를 바로 얻거나 설치해 사용할 수 있는 접근성임
    • 이미 알고 있는 지식과 기술에 가까운 익숙함임
    • 자신의 이해 능력으로 감당할 수 있는 정도임
  • 쉬움의 어원을 ‘가까이 놓임’과 연결하는 마지막 단계는 추정적이며, 어려움(hard)의 어원이 반대로 ‘멀리 있음’을 뜻하는 것은 아님
  • 쉬움은 상대적이므로 반드시 ‘누구에게 쉬운가’를 물어야 함
    • 독일어를 모른다고 독일어 자체를 읽을 수 없는 언어라 할 수 없듯, 낯선 코드를 읽지 못하는 것과 그 구조가 복잡한 것은 별개임
    • 익숙한 것만 요구하면 기존 지식과 크게 다른 것을 배울 수 없음
    • 익숙함을 단순함이라고 부르면 소프트웨어의 객관적 품질을 논의하기 어려워짐

작성 도구보다 그 도구가 만드는 결과물을 평가해야 함

  • 언어 구성요소와 라이브러리를 사용하는 경험과, 그것으로 만들어진 결과물의 특성은 구분해야 함
    • 타이핑이 짧고 세미콜론이 없는 것은 작성 경험의 특성임
    • 실행 동작·성능·장기간의 기능 추가·변경 가능성은 결과물의 특성에 해당함
  • 사용자가 사용하는 것은 소스 코드의 모양이 아니라 실행되는 소프트웨어
    • 요구한 일을 하는지, 품질과 신뢰성이 충분한지, 문제를 고칠 수 있는지, 새 요구사항에 맞춰 바꿀 수 있는지가 중요함
  • 고용주 역시 익숙한 기술과 즉시 투입 가능한 개발자에 집중할 수 있음
    • 같은 도구를 알고 코드를 익숙하게 읽는 사람으로 기존 개발자를 교체하기는 쉬워짐
    • 하지만 실제로 그 코드를 이해할 수 있는지는 별도의 문제임
  • 기술은 입력할 때의 모양이나 문화적 선호보다 장기간 사용할 결과물의 특성을 기준으로 평가해야 함

인간의 이해 능력과 안전망의 한계

  • 얽힌 요소는 독립적으로 생각할 수 없음

    • 일부 시스템에서는 유연성·확장성·동적 동작을 늘리는 대신 동작을 이해하고 정확성을 확인하는 능력을 희생할 수 있음
    • 이해와 정확성 확인이 필요한 시스템에서는 인간의 제한된 인지 능력을 고려해야 함
    • 서로 얽힌 요소는 독립적으로 생각할 수 없어, 한 부분을 이해할 때 연결된 다른 부분도 함께 머릿속에 올려야 함
    • 얽힘이 추가될수록 고려해야 할 조합의 부담이 커지고, 프로그램을 이해할 수 있는 범위가 줄어듦
  • 테스트와 타입 검사는 이해를 대신하지 못함

    • 소프트웨어를 변경하려면 현재 동작·변경 영향·수정 위치를 판단해야 함
    • Agile이나 XP, 테스트와 리팩터링 도구도 이 판단을 없애주지 못함
    • 필요한 것은 형식적 증명이나 범주론이 아니라 일상적인 비형식적 추론임
    • 현장에서 발견된 버그는 이미 타입 검사와 기존 테스트를 통과한 버그
    • 테스트가 있으니 무엇이든 바꿀 수 있다는 태도는 가드레일에 부딪히며 운전하는 것과 같음
    • 가드레일은 목적지로 향하는 방향을 정해주지 않음
    • 안전망이 놓친 문제를 해결하려면 논리적으로 원인 후보를 배제하고 조사할 부분을 좁힐 수 있어야 함

초기 속도와 장기 속도는 다름

  • 쉬움에만 집중하고 복잡성을 무시하면 처음에는 빠르게 진행해도 장기적으로 느려짐
    • 스프린트를 반복한다고 누적된 복잡성이 초기화되지는 않음
    • 각 스프린트의 성과가 줄고, 이미 만든 것을 다시 만드는 작업이 늘어 실질적인 전진이 멈출 수 있음
  • 단순함을 우선하면 시작 전에 생각하고 구조를 정리하는 시간이 필요함
    • 쉬우면서 단순한 도구를 선택하더라도 문제 자체를 단순하게 만드는 설계 작업은 남아 있음
    • 매우 단기적인 작업이라면 이러한 접근이 필요하지 않을 수 있음
  • 초기 속도와 장기 속도를 비교한 그래프는 수치나 측정에 근거한 결과가 아니라 경험을 나타낸 도식
  • 짧게 작성할 수 있고 익숙하며 구하기 쉬운 도구도 복잡한 결과물을 만들 수 있음
    • 도구 선택으로 생겼고 사용자의 요구에는 없던 복잡성은 우발적 복잡성(incidental complexity)임
    • 기능의 이점만 보지 말고 함께 따라오는 비용과 부산물도 평가해야 함
  • 단순함은 이해·변경·디버깅을 쉽게 하고, 정책 변경이나 구성요소 이동의 유연성을 높임
    • 결정들이 얽히지 않으면 위치와 성능 같은 결정을 더 독립적으로 내릴 수 있음
    • 테스트와 리팩터링 도구가 있다고 얽혀 짜인 성을 Lego 성보다 쉽게 고칠 수 있는 것은 아님

쉬움을 만드는 방법과 Lisp 괄호의 사례

  • 접근성과 익숙함은 설치와 학습으로 개선할 수 있음
    • 도구 사용 승인을 받고 설치하거나, 책·튜토리얼·설명·실습으로 익숙해질 수 있음
    • 반면 지식을 늘리는 것과 동시에 고려할 수 있는 요소 수를 크게 늘리는 것은 다름
  • 인지 능력의 한계에는 단순화로 대응해야 함

    • 사람 사이에 능력 차이가 있어도, 만들어낼 수 있는 복잡성에 비하면 모두 제한적임
    • 제한된 인지 자원을 도구가 추가한 복잡성에 쓸지, 실제 문제에 쓸지 선택해야 함
  • Lisp 괄호의 어려움에도 서로 다른 원인이 있음

    • 괄호가 어렵다는 불만에는 사용자가 해결할 부분과 언어 설계자가 해결할 부분이 함께 있음
    • 괄호 대응이나 구조적 편집을 지원하는 편집기를 준비하지 않은 것은 도구 접근성의 문제임
    • 연산자 앞에 괄호가 오는 표기에 익숙하지 않다면 학습으로 대응할 수 있음
    • Common Lisp과 Scheme에서 괄호가 호출·그룹화·자료구조를 모두 감싸는 것은 여러 의미를 겹친 구조적 복잡성
    • 자료구조와 표기를 구분하면 괄호의 의미 중복을 줄일 수 있음
    • 자료구조가 늘어나도 자기 자료구조로 정의되는 Lisp의 성격을 잃는 것은 아님
    • 구조적 중복을 해결하고 나면 남은 낯섦은 사용자가 학습할 수 있는 문제임
    • 과거 Lisp의 동적 기능은 가치가 있었지만 당시 하드웨어에서 메모리 할당과 평가의 성능 비용도 컸음
    • 도구의 장점만 나열하고 그 대가를 묻지 않는 태도는 Lisp에 한정되지 않음

얽기와 조합하기, 모듈화의 차이

  • 얽기(complect) 는 서로 끼워 넣고 꼬아 얽는다는 뜻이며, 복잡성을 만드는 행위를 가리킴
    • 같은 구성요소로도 서로 분리된 구조와 매듭진 구조를 모두 만들 수 있음
    • 사후에 풀기보다 처음부터 얽지 않는 것이 중요함
  • 조합하기(compose) 는 함께 놓는다는 뜻이며, 단순한 구성요소를 조합하는 것이 견고한 소프트웨어를 만드는 방법임
  • 모듈화만으로는 단순함을 얻을 수 없음

    • 별도 클래스나 계층으로 나뉘어 있어도 서로의 숨은 세부사항에 의존하면 여전히 얽혀 있음
    • 직접 호출하지 않더라도 상대가 특정 값을 반환하지 않는다는 식의 가정을 공유할 수 있음
    • 구성요소는 서로의 내부가 아니라 제한된 추상화만 알아야 함
    • 단순한 구성요소는 수평 분리와 수직 계층화를 가능하게 함
    • 반대로 분리와 계층화 자체가 구성요소의 단순함을 보장하지는 않음
    • 코드가 보기 좋게 조직되어 있다는 이유로 구조적 의존성을 놓치면 안 됨

상태는 값과 시간을 얽음

  • 상태를 숨겨도 외부 동작에 복잡성이 남을 수 있음

    • 같은 질문이나 같은 인자로 다른 결과가 나오면, 값을 시간과 독립적으로 이해할 수 없음
    • 익숙하고 모든 언어에서 쉽게 사용할 수 있다는 사실이 상태를 단순하게 만들지는 않음
    • 상태를 메서드 안에 숨겨도 외부 동작이 상태에 의존하면 복잡성이 새어 나옴
    • 감싼 객체와 그 객체를 감싼 계층도 같은 입력에 다른 출력을 내면 여전히 상태를 가짐
    • 내부 상태의 복잡성을 외부로 전파하지 않으려면 같은 입력에 같은 출력을 내는 함수형 인터페이스가 필요함
  • 상태의 어려움은 동시성에만 국한되지 않음

    • 단일 스레드 프로그램도 오류 당시의 상태를 재현해야 한다면 디버깅이 어려움
    • 테스트와 타입 검사를 통과했다는 사실만으로 당시 상태를 복원할 수는 없음
  • 참조에서 값을 꺼내 이후 처리를 값으로 이어가야 함

    • var, 참조, 관리되는 참조도 상태 자체를 단순하게 만들지는 못함
    • 다만 상태가 존재하는 위치를 드러내는 것은 도움이 됨
    • 불변성을 기본으로 하고 상태 도입에 명시적 노력이 필요하면, 애초에 불필요했던 상태를 크게 줄일 수 있음
    • Clojure와 Haskell의 참조는 시간에 대한 추상화와 값을 추출하는 기능을 조합함
    • 참조에서 값을 꺼내 이후 처리를 값으로 이어가는 것이 단순함으로 돌아가는 경로임
    • 계속 변하는 대상을 가리키는 참조를 전달하면 그 복잡성이 시스템의 다른 부분으로 퍼짐
    • 메모리 주소를 한 번 역참조해 얻는 것은 워드나 스칼라이므로, 가변 복합 객체 전체를 한 번에 값으로 얻는 것과는 다름

익숙한 구성요소가 무엇을 얽는가

  • 객체와 메서드는 독립적일 수 있는 개념들을 함께 묶음
    • 객체는 상태·정체성(identity)·값을 뒤섞음
    • 메서드는 보통 함수와 상태를 결합하고, 일부 언어에서는 이름공간 역할까지 겸함
    • 클래스의 메서드 공간이 빈약한 이름공간으로 쓰이면 이름 충돌 문제도 생김
  • 문법과 상속에도 얽힘이 있음
    • 문법은 의미와 순서를 결합하며, 데이터보다 조작에 제약이 있음
    • 상속은 타입들을 서로 묶음
  • switch와 패턴 매칭은 ‘누가 처리하는가’와 ‘무엇을 하는가’의 여러 쌍을 한곳에 닫힌 형태로 모음
  • 명령형 루프와 fold 는 수행할 일과 수행 방법을 결합함
    • 루프는 처리 내용과 반복 방법을 함께 기술함
    • fold도 순서, 특히 왼쪽에서 오른쪽으로 진행한다는 제약을 포함할 수 있음
  • 액터(actor) 는 할 일과 그 일을 수행할 주체를 결합함
  • ORM은 복잡한 결합을 만들고, 데이터에 대한 논리를 표현 방식에 묶음
  • 프로그램 곳곳의 조건문은 정책과 규칙을 프로그램의 구조·조직에 얽어 놓음
  • 비일관성은 서로 맞지 않는 여러 상태를 동시에 생각하게 만듦
    • 최종적 일관성을 제공하는 시스템을 사용하는 프로그램도 이러한 복잡성을 감당해야 함

더 단순한 도구로 같은 수준의 시스템 만들기

  • 복잡한 도구로 만들던 것과 같은 수준의 시스템을 더 단순한 도구로 만들 수 있음
    • 단순한 대안이 절대적으로 복잡성이 없다는 뜻은 아님
    • 대안 목록에서 벗어나야 할 이유가 있을 수 있지만, 처음부터 더 단순한 선택지로 시작할 수 있음
  • 값과 함수를 우선 사용함

    • 언어가 제공하는 final, val 등의 불변성 수단을 활용함
    • 복합 자료구조도 값으로 다루려면 영속 자료구조(persistent collections) 라이브러리나 이를 기본 제공하는 언어가 필요함
    • 함수는 상태 없는 메서드처럼 사용할 수 있으며, 이름공간은 언어의 지원이 중요함
  • 데이터를 직접 다룸

    • 맵·집합·선형 또는 순차 구조라는 기본 범주 위에 수많은 전용 형식을 덧씌우지 않아도 됨
    • 통신에서도 임의의 명령 문자열과 임의의 문자 출력을 주고받으며 파서를 추가하기보다 데이터를 사용함
  • 다형성의 정의와 연결을 분리함

    • 독립적으로 연결 가능한 다형성(polymorphism à la carte) 을 활용함
    • Clojure의 프로토콜과 Haskell의 타입 클래스는 자료구조 정의, 함수 집합 정의, 둘의 연결을 각각 독립적으로 수행하게 함
    • 일반화된 동작을 특정 자료구조에 미리 묶지 않아도 됨
    • 언어 자체가 지원하지 않을 때 이를 대체할 라이브러리는 흔하지 않음
  • 관리되는 참조·집합 연산·큐를 활용함

    • 관리되는 참조는 일반 변수보다 단순할 수 있지만 여전히 복잡성을 가짐
    • 다른 JVM 언어에서 Clojure의 참조를 활용하는 방법도 고려할 수 있음
    • 집합 연산과 큐는 라이브러리로 얻을 수 있으며, 큐를 위해 특별한 통신 언어가 필요한 것은 아님
  • 데이터 조작과 규칙을 선언적으로 다룸

    • 선언적 데이터 조작에는 SQL·LINQ·Datalog를 사용할 수 있음
    • 이러한 기능과 언어의 긴밀한 통합은 아직 쉽지 않으며, LINQ는 이를 시도하는 사례임
    • 선언적 규칙 시스템으로 조건과 정책을 별도로 모을 수 있음
    • 규칙 라이브러리나 Prolog 같은 언어를 활용할 수 있음
    • 일관성이 필요하면 트랜잭션과 값을 사용해야 함

실행 환경의 복잡성은 별도로 남음

  • 프로그램은 다른 프로그램 및 자신의 다른 부분과 메모리·CPU 자원을 두고 경쟁함
    • 이 환경적 복잡성은 사용자 요구 자체는 아니지만 구현 환경에 내재함
    • 가비지 컬렉션 문제처럼, 도구 선택에 따른 우발적 복잡성과 구별되는 제약이 있음
  • 자원 분할은 한 가지 대응이지만 낭비를 만들 수 있음
    • 메모리와 CPU를 미리 할당하면 사용하지 않는 몫이 생기고 동적인 활용이 어려워짐
  • 개별 구성요소의 자원 정책은 쉽게 조합되지 않음
    • 모든 구성요소가 스레드 풀 크기를 코어 수와 같게 정하면 전체 프로그램에서는 성립하지 않을 수 있음
    • 이런 결정을 각 부분에서 독립적으로 내리는 것이 오히려 복잡성을 만들 수 있음
    • 전체 정보를 더 잘 아는 곳에서 결정을 조정해야 하지만, 이를 시스템 안에서 통합하는 좋은 수단은 충분하지 않으며 명확한 해법도 남은 과제임

추상화는 숨기기가 아니라 분리하기임

  • 프로그래밍의 중심은 타이핑보다 사고에 있음
    • 단순한 결과물을 만드는 도구를 선택하는 것에 더해, 직접 만드는 구성요소도 단순하게 설계해야 함
  • 추상화(abstraction) 는 구체적·물리적 성질에서 무언가를 떼어내는 것이지, 복잡한 것을 단순히 감추는 일이 아님
  • 설계할 때 누가·무엇을·언제·어디서·왜·어떻게를 따로 검토하면 얽힌 결정을 분리하는 데 도움이 됨
    • 다른 구성요소의 세부사항을 ‘모르며, 알고 싶지도 않다’는 태도를 유지해야 함

무엇을 하는지와 어떻게 하는지를 분리하기

  • 무엇을(what) 은 수행할 연산과 목표이며, 이름을 붙인 작은 함수 집합으로 추상화함
    • 언어가 제공하는 인터페이스·프로토콜·타입 클래스로 함수들의 명세를 나타냄
    • 보통 만드는 것보다 훨씬 작은 단위를 지향해야 함
    • 큰 Java 인터페이스처럼 여러 능력을 한 인터페이스에 묶으면 프로그램을 분해하기 어려워짐
  • 추상화의 정의는 값과 다른 추상화만 사용해야 함
    • 인자와 반환값도 값이나 추상화로 구성하고, 구체 구현에 의존하지 않도록 함
    • 명세 대신 구체 함수나 클래스를 바로 사용하면 ‘무엇’과 ‘어떻게’가 결합함
    • fold처럼 의미에 수행 순서가 포함되어도 구현 방법을 간접적으로 강제할 수 있음
  • 무엇과 어떻게를 엄격히 분리하면 구현 방법을 다른 시스템에 맡길 수 있음
    • 데이터베이스 엔진에 실행 방법을 맡기거나, 논리 엔진에 탐색 방법을 맡길 수 있음
  • 어떻게(how) 에 해당하는 구현들은 가능한 한 서로 독립된 섬이어야 함
    • 명세와 구현은 다형성 수단으로 연결하고, switch나 패턴 매칭 한곳에 모두 모으지 않음
    • 열린 다형성은 확장에 유리하며, 런타임에도 열려 있으면 더욱 강력함
    • 선언적인 명세일수록 뒤에서 구현할 사람의 선택을 덜 제한함

주체·위치·시간·정책을 분리하기

  • 누가(who) 는 추상화가 연결될 데이터나 엔티티에 해당함
    • 하위 구성요소를 내부에 고정하지 말고 인자로 주입해 조립함
    • 보통보다 작은 인터페이스와 더 많은 하위 구성요소를 허용해야 함
    • 정책을 비롯한 여러 책임을 나누되, 구성요소 사이에 숨은 내부 의존성을 만들지 않음
  • 어디서(where)와 언제(when) 를 직접 호출에 묶지 않음
    • A가 B를 호출하면 A는 B의 위치를 알아야 하고, 실행 시점도 A의 호출에 종속됨
    • 사이에 큐를 두면 이 결합을 끊을 수 있으므로 큐를 적극 활용해야 함
  • 왜(why) 에 해당하는 정책과 규칙은 애플리케이션 전체에 흩어놓지 않음
    • 정책이 일반 코드 곳곳에 있으면 고객과 실제 동작을 검토하기 어려움
    • 영어 문장을 붙인 테스트만 보여주기보다, 실제 일을 수행하는 규칙을 읽을 수 있게 해야 함
    • 선언적 시스템이나 규칙 시스템으로 정책을 별도 위치에 모을 수 있음

정보를 객체로 감싸기보다 데이터로 유지하기

  • 정보 자체는 단순함 — 맵과 집합을 직접 사용하고, 새 정보가 생겼다는 이유만으로 클래스를 만들 필요는 없음
  • 화면이나 마우스 같은 입출력 장치의 캡슐화와 정보를 객체로 감싸는 일은 구별해야 함
    • 정보에 객체 구조를 덧씌우면 범용 데이터 조작 기능을 만들고 재사용하기 어려워짐
    • 데이터를 데이터로 유지하면 조작 기능을 한 번 만들고 정확성을 확인한 뒤 여러 곳에서 재사용할 수 있음
  • 객체와 ORM은 논리를 데이터 표현 방식에 결합할 수 있으므로, 정보를 다루는 본질적인 연산과 표현상의 제약을 분리해야 함

기존 복잡성을 풀고 단순함을 유지하기

  • 다른 사람이 만든 코드나 문제 공간을 단순화하는 일은 얽힘을 푸는 작업
    • 먼저 연결이 어디로 이어지는지 추적하고 각 부분을 식별해야 함
    • 구체적인 단순화 절차 전체는 별도의 주제가 될 만큼 큼
  • 단순함은 선택이며 지속적인 주의가 필요함

    • 복잡한 결과물을 만드는 도구를 계속 사용하는 문화는 스스로 강화됨
    • 코드 이름·모양·세미콜론보다, 독립적일 수 있는 요소 사이의 연결을 알아보는 감각을 길러야 함
    • 테스트·타입 검사 같은 신뢰성 도구는 이차적인 안전망임
    • 유용하지만 얽힘 자체를 없애거나 단순함을 만들어주지는 않음
    • 더 단순한 결과물을 만드는 구성요소를 선택하고, 구현 전에 문제를 단순화하는 시간을 써야 함
    • 작성 경험에 대한 선호와 장기간 감당할 결과물의 구조를 구분해야 함
    • 단순함을 토대로 추상화를 만들고, 복잡한 결과물을 만드는 구성요소는 피함
  • 구성요소 수보다 얽힘을 줄이는 것이 중요함

    • 단순화하면 구성요소 수가 늘어날 수 있음
    • 적은 요소가 매듭져 있는 것보다, 더 많은 요소가 서로 얽히지 않고 놓여 있는 편이 나음
    • 분리된 요소는 더 독립적으로 바꿀 수 있으며, 그 변경 가능성이 단순함의 중요한 이점임

댓글과 토론

이 글은 GeekNews 의 원문을 정제해 보여드립니다. 저작권은 원저작자에게 있습니다.

전체 내용이 궁금하다면

GeekNews 원문에서 이어 읽기

원문 보기

비슷한 글

5유사도 추천