2015. 5. 9. 13:43

JBUG(JBoss User Group) 스터디 모임에서 "기업 통합 패턴 Enterprise Integration Patterns"을 번역하신 차정호 님의 강의를 듣는 귀한 기회를 얻었다.


 


강의 초반에 '알고리즘의 시대에서 패턴의 시대'로 변모하는 것이 아닌가 라는 말씀을 하신다. 소프트웨어 개발에 있어서 알고리즘(algorithm)와 패턴(pattern)은 모두 중요하다고 생각한다. 어느 한 쪽이 더 중요하다 하기도 어렵고, 두 가지 기술의 적용 범위 혹은 특징이 상이하지 않은가 라는 생각에 강의를 들으면서 짧은 노트를 적어 봤다. 거듭 말하지만 두 가지 기술은 반대되는 개념은 아니다, 다만 몇 가지 면에서 차이를 비교해볼 수 있다.


1.문제 풀이를 위한 공식(formula)인가? 아니면, 문제를 풀기 위한 행동 방식인가?


"알고리즘"은 개별적인 '문제(사례)를 해결하기 위한 논리와 기법'이다. 즉, 알고리즘은 문제 자체를 해결하는데 쓰인다. 알고리즘은 그 자체가 가치를 지닌다. 완성된 논리이며, 코드로 씌어졌을 때 즉각적으로 유용하다. "패턴"은 빈번하거나, 다양한 문제들을 해결하는 과정에서 발견된 문제 풀이에 도움을 주는'유용한 사례들'이다. 패턴은 소프트웨어 개발자 간의 의사 소통을 돕고, 문제 해결을 위한 실마리가 된다. 패턴은 문제 자체를 해결해주지는 않는다. 패턴을 현실 세계에 비유하자면 레고 블록과 같다. 개별 레고 블럭(패턴)은 가치가 없다. 그것들을 연결했을 가치가 생겨난다.


2. 상대적으로 좁거나 반대로 넓은 적용 범위


대다수의 "알고리즘"은 특정 문제를 해결하기 위해 고안된 것이다. 다양한 문제가 있다면, 그만큼 많은 알고리즘이 만들어진다. 하나의 문제에 대해서도 복수의 알고리즘이 있을 수 있다. 반면에 "패턴"의 적용 범위는 넓다. 특정 문제 자체를 위한 해결책이 아니라, 본래 다양한 사례를 조사하고 그 과정에서 반복적으로 쓰이는 기법을 문장의 숙어(idiom)처럼 골라낸 것이다. 요약하면, 알고리즘은 좁은(narrow) 범위를 해결하는데 사용되고, 패턴은 넓은(wide) 범위의 문제들을 해결할 때 유용하다. (어느 한 쪽이 우월하다고 해석해서는 안된다.)


3. 관습적이거나, 개성적이거나...


"알고리즘"이나 "패턴"이나 모두 문제 해결을 위한 도구이다. 그러나, "패턴"은 보다 관습적이다. 누군가 패턴을 제안하거나, 발견할 수 있지만 그것이 과연 유용한 것인지는 많은 개발자들의 선호도(?) 라거나, 지지 여부에 따라 가치가 달라진다. 반면에 알고리즘은 알고리즘을 고안한 사람의 개성을 뚜렷히 드러낸다. 또한 그 성능과 가치는 (대게) 객관적으로 조사 가능하다. 각각의 패턴을 속도라거나, 자원 사용량 등으로 비교할 수는 없는 노릇이다.


4. 논리적이거나, 직관적이거나..


알고리즘은 수학적으로 증명 가능해야 한다. 주어진 입력에 대해 정확한 출력 값을 내놓는다는 증거(혹은 테스트)를 거친다. 하지만, 패턴은 나름 인간적이다. 증명을 거치지 않는다, 오히려 아름다운지 여부를 판단한다. 알고리즘 또한 아름다울 수 있다. 수학적 혹은 코드의 아름다움은 정교할 뿐만 아니라 간결한 형태를 통해 완벽함, 경이로움을 느끼는 것이리라. 하지만, 패턴에서 느끼는 아름다움은 정교함 보다는 단순함, 그리고 완벽함 보다는 복잡한 세상에서 찾아낸 보편성에 대한 감탄이라고 표현할 수 있다.

Posted by 곽중선
2015. 5. 4. 16:05

AlgoSpot Quiz '사각형 그리기 (DRAWRECT)' 문제 풀이.


▶ DRAWRECT 문제 분석


- input       : 입력의 첫 행은 T 값이며, 테스트 케이스(test case)의 갯수이다. 

                 나머지 입력은 x, y 좌표 값을 공백으로 구분한 행들이며, 3 * T 갯수 만큼 입력된다.

- processing : T 만큼 반복하면서, 3개의 2차원 좌표를 이용해 마지막 좌표를 계산한다.

- output     : 계산된 마지막 좌표를 출력한다.


입력 값의 범위와 유형은 아래와 같이 표현할 수 있습니다.




▶ 문제 분할

문제 자체가 복잡하지 않기 때문에 세밀한 문제 분할은 필요가 없습니다.


▶ 핵심 논리 구성

4개의 점 중에서 하나가 누락될 경우, 3개의 점으로 구성할 수 있는 경우의 수를 구해 봅니다. 수학의 '조합(combination)'을 활용하면 되겠습니다. 조합(Combination)이란, 서로 다른 개 중 순서를 무시하고 개를 택하는 것이며,  기호로 나타냅니다.



조합을 이용해 4개의 점 중에서 3개의 점을 순서를 구분하지 않고 선택하는 경우의 수를 계산하면, 아래와 같습니다.



4개의 경우의 수를 표 형태로 표현해 보면 다음과 같습니다.


 조합

 Case #1

Case #2 

Case #3 

Case #4 

 좌표

 p1 (x1, y1)

 p2 (x2, y1)

 p3 (x1, y2)

 p1 (x1, y1)

 p2 (x2, y1)

 p4 (x2, y2)

 p1 (x1, y1) 
 p3 (x1, y2)
 p4 (x2, y2)
 p2 (x2, y1)
 p3 (x1, y2)
 p4 (x2, y2)

 비고

 p4 제외

 p3 제외 

 p2 제외  p1 제외 


위 표를 통해 얻을 수 있는 핵심 논리는 아래와 같습니다.


직사각형의 3개의 점(point)가 입력되었을 때, x와 y 좌표는 항상 2개는 같고 나머지 하나는 다르다.

다른 값을 가지는 2개의 x, y 좌표 값을 결합하면 4번째 좌표를 얻을 수 있다.


▶ 플랫폼 문제 해결

본 문제는 플랫폼 문제를 해결할 필요가 없습니다.


▶ 핵심 정리

알고리즘 문제 해결을 위해 가장 좋은 도구는 '수학'입니다. 또한, 문제를 풀기 위해서 좌표계 등의 시각적인 도구를 활용하면, 직관적으로 문제를 이해하는데 있어 큰 도움이 됩니다.

Posted by 곽중선
2015. 5. 2. 00:30

AlgoSpot Quiz 'ENDIANS' 문제 풀이 과정을 통한 소프트웨어 설계 연습.


▶ 소프트웨어 설계 연습 서론

소프트웨어 문제를 풀이를 할 때, "어떤 절차 혹은 방식"으로 프로그램을 작성해야 하는 것인지 감이 잘 잡히지 않는 분들이 더러 있습니다. 여러분의 머리가 나쁘기 때문에 그런 것이 아니라, 논리적으로 문제를 푸는 연습(혹은 훈련)이 부족하기 때문에 어렵게 느껴지는 것입니다.


달리기를 잘하고 싶다면, 달리기 연습부터 해야 합니다. 걷지도 못하는 아이가 뛰려고 하면 넘어질 뿐이죠. 자전거를 타고 싶다고 당장 안장에 올라타서 패달부터 밟으면 어찌 되겠습니까? 논리적으로 사고하는 능력도 훈련이 필요한 것입니다. 대한민국에서 고등학교 수준의 교육을 정상적으로 이수했다면 프로그래밍을 하는데 있어서 필요한 '학습 능력'이 부족한 것이 아닙니다. 다만, 여러분이 논리 문제를 풀어 본 적이 별로 없다는 점이 여러분의 프로그래밍을 힘들게 만드는 것입니다. 단순한 예제를 풀어가는 과정을 통해 논리로 문제를 푸는 연습을 해보고자 합니다.


 ENDIANS 문제 분석 및 논리 전개

알고스팟 왕초보 문제 "ENDIANS" 에 제시된 문제를 먼저 읽어보시기 바랍니다.


문제를 읽고 나서, 별거 아닌데? 생각하고 바로 코딩부터 하려는 분도 있을 수 있습니다. 잠시 그 손을 멈추시기 바랍니다. 운동할 때, 100 미터쯤 대충 달리는 거 문제가 아닙니다. 그런데, 왜 운동 선수들을 매일 똑같은 달리기를 반복할까요? 그저 내달리기만 하는 것은 아닐 겁니다. 멀리 오래도록 달리기 위해서는 달리는 자세와 호흡법, 페이스 조절 능력 등이 함께 필요한 법입니다.


보다 복잡하고, 어려운 문제를 풀기 위해서는 작은 문제를 풀 때도 좀 더 차분하게 접근할 필요가 있다는 얘기입니다. 저는 아래에 서술하는 내용을 일단, 종이에 적어가면서 천천히 음미하고 반추하는 시간을 가졌습니다. 그리고, 이제 키보드를 치며 다시 문서로 정리하고 그런 연후에 코드를 작성할 것입니다.  여러분도 좀 더 여유를 가지고 프로그래밍에 임하셨으면 좋겠습니다.


제 방식이 무조건 옳다라고 말씀 드리기는 어렵지만, 제가 프로그램을 작성하는데 있어 가급적 지키고 있는 절차는 다음과 같습니다.


  1. 문제 분석 (problem analysis)
    : 주어진 문제를 논리 흐름(logical flow)에 따라 다시 정리합니다. 가장 쉬운 방법은 입력, 처리, 출력 3단계로 문제를 재정의하는 것입니다.

  2. 문제 분할 (break down problem) 
    : 문제를 해결하기 쉽도록 보다 작은 문제로 쪼개 봅니다. (분할 및 정복 :devide & conquor 라고도 합니다.)

  3. 핵심 논리 구성 (composite core logic) 
    : 대다수의 문제는 쉬운 부분을 제외하고 나면, 한 두개의 어렵거나 까다로운 부분이 드러나게 됩니다.

  4. 플랫폼 문제 해결 (solving platform issue)
    : 플랫폼이라 함은 하드웨어, 운영체제, 프로그래밍 언어, 라이브러리 및 프레임워크 등 어플리케이션 프로그램을 작성하는데 있어서 의존해야 하는 모든 것을 말합니다. 풀어내고자 하는 문제를 해결하기 위해 어떤 API를 사용해야 하는지? 하드웨어 어떤 기능을 이용해야 하는지에 대한 문제를 해결합니다. 이미 알고 있는 지식이라면 쉽게 해결되지만 잘 모르거나, 처음 접하는 문제라면 적극적으로 검색 기술을 활용합니다.

  5. 코딩 (coding)
    : 이제, 코딩할 수 있습니다.!

"ENDIANS" 문제에 대한 분석 및 논리 구성 과정 사례는 아래와 같습니다.

  1. 문제 분석
    소프트웨어는 거의 "입력, 처리, 출력" 등 3단계의 흐름을 설계할 수 있습니다.

    - input         : n개의 32 bit unsigned int 값이 한 줄(line)에 하나씩 있는 텍스트 파일을 읽어들인다.
    - processing   : 엔디언 변환(endian conversion)을 수행한다.
    - output       : 변환한 숫자를 출력한다.

  2. 문제 분할
    문제를 해결하기 쉽도록 좀 더 작은 문제들(혹은 자세한 절차)로 쪼개 봅니다.

    1) 텍스트 파일을 연다.
    2) 한 줄의 텍스트 문자열을 읽어들인다.
    3) 읽어들인 문자열을 32 bit unsigned int 형 값으로 변환한다.
    4) 엔디언 변환(endian conversion)을 수행한다.
    5) 변환 결과를 출력한다.
    6) 더 이상 읽어들일 라인이 없을 경우, 파일을 닫고 프로그램을 종료한다.
    7) 아니면, 다음 행을 읽어들이기 위해 2) 단계로 이동한다.

  3. 핵심 논리 구성
    위와 같은 흐름에서 가장 중요한 (혹은 어려운) 문제는 32 bit 부호없는 정수 값의 엔디언을 변환하는 것입니다.

    1) 입력된 32 bit 부호없는 정수 값을 4 byte 배열 형태로 분리(혹은 변환)한다.
    2) 바이트 배열에 담긴 데이터를 역순(reverse order)으로 재배치 한다.
    3) 재배열된 바이트 배열을 다시 부호 없는 정수 값으로 변환한다.
    4) 변환된 정수 값을 출력한다.

  4. 플랫폼 문제 해결
    문제를 코드로 작성할 때 다양한 기술적 문제를 고려할 필요가 있습니다.

    프로그래밍 언어, 운영체제, 하드웨어, 라이브러리 등 어플리케이션 프로그램 자체에서 직접 구현하는 것이 아니라, 
    어플리케이션 하위에 위치한 (혹은 어플리케이션이 의존하고 있는) 시스템에서 제공 받는 기능들을 활용하고자 한다면, 원하는 기능이 있는지 코딩하기 전에 확인할 필요가 있고, 최악(?)의 경우에는 직접 구현해야 하는 경우도 발생합니다.

    ENDIANS 문제를 자바로 구현하고자 할 경우에 다음과 같은 문제들을 고려할 필요가 있습니다.

    텍스트 파일을 열고, 한줄 씩 읽어들이는 방법은 무엇인가?
    : java.io.File 클래스와 InputStream, BufferedReader 등 JDK에서 제공하는 API를 활용하면 됩니다.

    자바 언어에서 '부호없는 32 bit 정수' 타입을 지원하는가?
    자바는 부호 없는 정수형 타입을 지원하지 않습니다. 그러나, 64 bit 정수형인 long 타입을 사용하면 32 bit unsigned int 값을 저장할 수 있습니다.

    바이트 배열과 32 bit 정수 값을 상호 변환하는 방법은 무엇인가?
    우리에게는 구글과 StackOverflow가 있습니다. StackOverflow 참조 : 정수와 바이트 배열 간의 상호 변환 

 핵심 정리

간혹 어디까지가 알고리즘인가? 라는 의문이 들 때가 있습니다. 예를 들자면, 정수형 데이터와 바이트 배열 간에 상호 변환하는 과정도 알고리즘의 일부가 아니냐는 질문을 받습니다. 이런 과정을 직접 생각해야지 검색해서 남의 해법을 참고하는 것은 '편법' 혹은 틀린 자세는 아닌가 라는 의견을 접하고는 합니다. 소프트웨어 구현 과정의 모든 것을 스스로 해야 한다면, 파일을 읽고 쓰는 자바 API와 운영체제 까지도 직접 작성해야 하지 않을까 라는 반문을 해봅니다. 제가 정의하는 알고리즘은 '문제를 이해하고 논리적인 절차를 정의하는 것'이며, 구현 방안(방식)은 알고리즘의 본질이 아니라고 생각합니다.



Posted by 곽중선
2015. 5. 1. 15:11

알고리즘(수학적 논리), 영어 그리고 인문과 역사에 대한 이야기를 좀 해볼까 합니다.


"알고스팟'이라는 컴퓨터 알고리즘 수련 사이트에 올라온 문제를 풀이하는 스터디를 진행 중입니다. 우선 알고스팟의 튜터리얼 (왕초보) 문제를 풀고 있는데, 두번째 문제의 시험 문제가 영문이네요. 스터디에 참여하시는 분들이 문제 해석부터 난관에 부딧친 상황입니다.


어느 스터디 멤버 분의 고민 :


"해석은 얼추 했는데 정확히 문제가 원하는게 무엇인지 잘 모르겟네요."


문제를 해석하는데 있어서 가장 큰 문제는 영어와 인문학적 경험입니다. 아래 문장을 보시죠.


"People from Lilliput are called little-endians, since they open eggs at the small end. People from Blefuscu are called big-endians, since they open eggs at the big end."


'small end'와 'big-end'라는 표현이 대체 무슨 뜻일까요? 걸리버 여행기를 한글 책으로 읽어 본 경험이 있는 사람이라면, 금새 이해할 수 있습니다. 아래는 제가 번역한 문장입니다.


"릴리퍼트 사람들은 리틀 엔디언(little-endians)이라 불리며, 달걀의 뾰족한 끝(little-end)을 깨고 먹는다. 블레퍼스큐 사람들은 빅 엔디언(big-endians)이라 불리며, 뭉툭한 끝(big-end)을 깨고 먹는다."


그런데, 이제 또 다른 벽이 나타납니다. 알고리즘 문제 내용의 일부를 보겠습니다.


"이런 식의 논쟁은 그들의 아침식사 문제 뿐만 아니라, 컴퓨터에서도 발생하고 있다. 릴리퍼트와 블레퍼스큐의 컴퓨터는 정수(integers) 값을 저장하는 방식이 다른데 바이트(bytes) 데이터들을 서로 다른 순서로 저장하는 것이다. 릴리퍼트(리틀 엔디언)의 컴퓨터는 LSB(least significant byte)부터 MSB(most significant byte) 순서로 정렬하고, 블레퍼스큐의 컴퓨터는 정확히 반대로 동작한다.


따라서, 블레퍼스큐와 릴리퍼트의 컴퓨터 간에 정보를 주고 받을 필요가 있다면, 어떤 식이건 변환 절차가 필수적이다. 아무도 데이터를 보내면서 변환하는 것을 원하지 않기 때문에, 수신자들이 항상 변환 작업을 수행해야만 한다."


스터디 내에서 이런 식의 문제가 왜 필요한가? 어디에 쓰이는 건가? 라는 질문이 나옵니다. 컴퓨터의 발전 역사에 대한 지식이 없으면, 출제 의도를 전혀 이해할 수 없습니다. 다음 링크 "위키피디아, 엔디언이란?"을 참고하시면, 아오지탄광(알고스팟의 별칭)의 출제자들이 왜 이런 문제를 냈는가를 이해할 수 있습니다. 우리가 사용하는 모든 컴퓨터는 늘상 네트워크를 통해 데이터를 주고 받고 과정에서 엔디언 문제와 씨름하고 있습니다. 단지, 눈에 띄지 않을 뿐이지요.


정리하자면, "알고리즘"을 공부하는데 있어서 수학적 재능과 노력만 필요한 게 아닙니다. 영어, 상식 그리고 (컴퓨터) 역사 등의 배경 지식과 이론이 없으면 앞으로 나아가기 어렵습니다. 컴퓨터 소프트웨어의 목표는 현실 세계의 문제를 가상의 세계 (cyberspace)로 이식하는 것입니다. 그런데, 현실 세계의 문제들을 이해조차 하기 어렵다면, 어떻게 문제를 풀 수 있겠습니까? 자, 이래도 어쨋건 코딩만 잘하면 (빨리 빨리 코드를 만들어 내기만 하면) 되는 걸까요?


문제 출처 : "AlgoSpot : ENDIAN Quiz" 



Posted by 곽중선
2015. 5. 1. 11:50

알고스팟 (통칭 아오지탄광) 문제풀이 스터디를 진행 중입니다.

지원자 12명 중에서 11명이 튜터리얼 중에서 가장 쉬운 첫번째 문제를 풀이한 상태이며,

초보자들이 지나치기 쉬운 잘못된 코딩 습관을 통계내 봤습니다.

참고 하시기 바랍니다.


분류 유형
코드
좋은 않은 코딩 스타일 코드 예시 설명 (진지하게 읽지 마세요.) 발생
건수
발생
빈도
Bad B1 무의미한 제어문 사용
(불필요한 논리)
while ( flag ) { break; } if 조건 없이 break 실행 2 18%
B2 의미 없거나 불필요한 변수 사용 flag = true; 사용 안하는 변수 선언 1 9%
Not
Good
N1 루트 패키지 사용 패키지 선언 없음 9 82%
N2 입력 값 범위 체크 안함 if( inputNum < 10 ) { … } 음의 정수를 입력할 수 있으나,
그러지 않기를 바래요.
8 73%
N3 불분명하거나
의미없는 클래스 명칭
Main { … } 어떤 기능을 수행하는지 알 수 없음.
(내 마음 알고 싶나요? 나도 몰라요.)
6 55%
N4 불명확한 변수 명칭 int num; 데이터 타입은 알 수 있으나,
용도는 파악 안됨
4 36%
N5 들여쓰기 일관성 없음 석봉아 이제 불을 끄자꾸나…
어머니는 떡을 써시고, 나는 코딩을…
3 27%
N6 연산자와 조건문의 결함  while( i-- > 0 ) { ... } 로직을 한눈에 파악하기 어려움. 2 18%
N7 패키지 명칭에 대문자 사용 package KingBeginner; 그런데, 번역이 맞습니까? 1 9%
N8 Camel Case와 Snake Case 혼용 int input_Num; 엎어 치던가.. 매치던가.. 하나만.. 1 9%
N9 API 에 대한 불충분한 이해 exit(0); 오류가 발생했으나, 정상 종료 처리함 1 9%
N10 불필요한 주석 표기 // input number 코드만 보고 충분히 이해할 수 있음에도
주석을 쓰려면 차라리 한글로 쓰시는게?
1 9%
N11 주석에 적힌 파일명과
실제 파일명칭 불일치
gistfile1.cpp != main.cpp 암 유발자…. ? 1 9%
N12 소스 주석에 한글 영어 혼용 Copyright (c) 2015년 암 유발자…. ? (2) 1 9%
N13 잘못된 주석 위치 unsigned int inputVal;
// unsigned int로 선언
난 네가 이미 읽고 이해한 것,
그것 조차 설명하겠다!
1 9%
N14 값 비교 시, 작은 값 부터
비교하는 것을 권장함
if(n <= 10 && n > 0) 큰 것이 좋아? (응?) 1 9%
총 참가자 수  11




Posted by 곽중선
2015. 4. 30. 21:17

Quiz LINK


Posted by 곽중선
2015. 4. 20. 23:57

자바 필기 및 실기 문제

다음 내용은 위에 나온 문제 중에서 필기 6번 문제와 해설입니다.


[Q6] 아래 프로그램의 출력 결과를 적으시오.

 

public class SetValues {

   

    public static void main(String[] argv) {

 

        String stringObj = "Hello";

        int intValue = 0;

        Float floatObj = new Float(1.0);

 

        setValues(stringObj, intValue, floatObj);

 

        System.out.println( stringObj + ", "

                       + intValue + ", " + floatObj );

 

    }

 

    private static void setValues(String strValue, int intValue, Float floatObj) {

        strValue.replace("H", "h");

        strValue += " World";

        intValue = 99;

        floatObj.valueOf((float) 2.0);

    }

}

 

위 문제는 call by value, call by reference의 차이를 식별할 줄 아는지를 묻는 것이라고 착각을 유도하는 문제이다. 시험 응시자가 자칫 핵심을 잘못 간파해서 엉뚱한데 집중하지는 않는지 여부와 자바에서 제공하는 기본 메소드들의 동작 방식을 제대로 이해했는가 여부를 파악하고자 출제된 것이다.


아래 주석을 통해 객체가 어떻게 메소드 간에 전달되고, 어떻게 변화하는지를 설명하였다.
(
객체의 ID는 임의로 부여한 것이며, 실제 실행 중에는 달라질 수 있다.)

 

public class SetValues {

   

    public static void main(String[] argv) {

 

        // stringObj (object id = 15)

        String stringObj = "Hello";

        // intValue (, 객체 아님)

        int intValue = 0;

        // floatObj (object id = 16)

        Float floatObj = new Float(1.0);

 

        setValues(stringObj, intValue, floatObj);

 

        System.out.println( stringObj + ", "

                       + intValue + ", " + floatObj );

     }

  

    // strValue (object id = 15), call by reference 방식으로 전달

    // intValue (, 객체 아님), call by value 방식으로 전달

    // floatObj (object id = 16), call by reference 방식으로 전달

    private static void setValues(String strValue, int intValue, Float floatObj) {

 

        // replace 메소드는 새로운 String 객체 (object id = 17) 만들어 반환하지만,

        // 변수에 반환 값이 할당되지 않으므로, 무시됨.

        strValue.replace("H", "h");

 

        // += (문자열 조합) 연산자를 실행하면, 새로운 객체(object id = 18) 생성되며,

        // strValue (지역변수) 참조하는 객체는 object id = 18 변경됨.

        strValue += " World";

       

        // intValue 변수는 기본형(primitive type)이므로, intValue 변수에 저장된 값이

        // 99 변경됨.

        intValue = 99;

 

        // Float 클래스의 valueOf() 메소드는 static 메소드이며,

        // 2.0 값을 지니는 새로운 Float 객체 (object id = 19) 생성해 반환함.

        // 반환된 객체는 어느 변수에도 할당되지 않기 때문에 무시됨.

        floatObj.valueOf((float) 2.0);

    }

}

 

결국 메인 함수로 복귀(return)하게 되면, setValues 함수 내에서 생성된 모든 객체는 무시되고, setValues 메소드 호출 이전 상태의 값들이 그대로 출력된다.

Posted by 곽중선
2015. 4. 18. 16:02

Original Link : The Magic of Strace (by Chad Fowler)


Thanks to Chad Fowler! this document is korean translated version of "Chad Fowler's great writing"

If you read this and got some insight, visit and read more posts at http://chadfowler.com/


원 저자이신, 채드 파울러(Chad Fowler)님의 허락을 얻어 번역한 글입니다.


내가 IT 경력을 쌓은지 얼마되지 않았을 때, 동료와 나는 며칠에 걸쳐 로터스 도미노 서버(Lotus Domino Server) 장애와 씨름하고 있는 팀을 지원하기 위해 멤피스(Memphis)에서 올란도(Orlando)까지 비행기를 타고 가야 했다. 올란도에 있는 팀은 장애를 해결하지 못하고 며칠째 아무 곳에도 가지 못하며 옴쭉달짝을 못하고 있는 상황이었다. 나는 그들이 나와 동료가 도움을 될거라 생각한다는 사실을 믿기 어려웠다. 우리는 로터스 도미노에 대해서는 아무 것도 모르는 상태였기 때문이다. 하지만, 나는 UNIX 를 열심히 공부했고 또 잘 다룰 줄 알았다. 아무래도 그들은 자포자기 상태였던 듯 하다.


로터스 도미노는 소스가 공개되지 않는 블랙박스 형태의 "그룹웨어(groupware)" 서버였다. 그 때 문제가 정확히 무엇이었는지 기억 나지는 않지만, 데이터베이스의 파일들을 제대로 읽고 쓰지 못하는 문제를 해결했던 것 같고, 확실한 것은 로터스 기술팀의 고급(escalated) 기술지원으로도 문제를 해결하지 못했었다.


그 경험은 내 경력을 거쳐 익힌 수많은 학습 중에 가장 깊이 있는 것 중에 하나였다. 내가 사용하는 다양한 유닉스 툴(tool) 중에서 지금까지도 가장 중요하게 여기는 strace.* 명령을 그 때 배웠다.


근래에 들어서도 프로그래머이자 시스템 엔지니어로서 일하면서 거의 매일 strace 혹은 그와 유사한 명령들을 사용하고 있다. 이 글에서 왜 그리고 어떻게 strace 명령을 사용하는지 설명하고, 그리고 너무나도 훌륭하고 강력한 작은 도구를 활용하는 몇가지 팁(tip)을 보여주고자 한다.


strace 는 무엇인가 (What is strace?)


strace 는 특정 프로세스와 자식 프로세스의 시스템 호출(system call)과 프로세스에 전달되는 시그널(signal)을 추적할 수 있는 명령행 도구(command line tool)이다. strace 명령을 이용해 프로그램을 시작할 수도 있고, 실행 중인 프로그램의 시스템 호출을 추적하는데 사용할 수도 있다. 간단한 예제를 살펴보자. 다음과 같은 간단한 C 프로그램을 작성했다고 가정하자.


[traceme.c]

#include <stdio.h>
void main() {
  printf("hi\n");
}


많은 기능을 담고 있지는 않다. 그냥 화면에 "Hi"라는 문자열을 프린트 할 뿐이다. 프로그램을 컴파일한 후에, strace 명령을 이용해 실행해보면 프로그램이 수행하는 시스템 호출을 모두 볼 수 있다.


$ strace -s 2000 -f ./traceme

execve("./g", ["./g"], [/* 25 vars */]) = 0

brk(0)                                  = 0x1816000

access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)

mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2ffa263000

access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)

open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

fstat(3, {st_mode=S_IFREG|0644, st_size=35614, ...}) = 0

mmap(NULL, 35614, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2ffa25a000

close(3)                                = 0

access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)

open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3

... (중략) ...

write(1, "hi\n", 3hi

)                     = 3

exit_group(3)    


프로그램을 실행하기 위해, strace 명령과 프로그램 명칭을 입력했다. (더불어, 프로그램 실행에 필요한 인자를 추가할 수 있다.) 위 예시에서는 '-s' 옵션(flag)를 추가해 strace가 출력하는 문자열의 최대 크기를 지정했다. 이렇게 하는 것이 함수들의 인자를 조회하는데 도움이 된다. 여기서는 임의로 2000을 지정했는데, 이 값은 우리가 실행하는 프로그램에서 보고자 하는 정보를 얻는데 충분하다. 기본값은 32인데, 내 경험으로는 프로그램을 추적하는 과정에서 필요한 정보를 캐내기에는 절대적으로 부족하다. 또한, '-f' 옵션을 지정했으며, 이는 strace로 하여금 우리가 실행한 프로세스의 자식 프로세스도 추적하라고 지시하는 것이다. 위 예제에서는 자식 프로세스가 없지만, strace를 사용할 때는 일반적으로 프로세스의 자식까지 추적하도록 설정하는 습관을 가지는 것이 프로그램의 실행 중에 벌어지는 모든 것을 알 수 있기에 권장한다.


명령을 실행하고 나면, 좀 장황한 시스템 호출 기록들을 모두 보게 된다. 일부(거의 모두?)는 마치 횡설수설하는 것처럼 보인다. 들여다 봐도 대부분 이해할 수는 없겠지만, 그리 오래지 않아 쓸만한 정보들을 찾아낼 수 있을 것이다.


추적(trace)의 첫번째 라인은 execve() 호출이다. 놀라울 것도 없이, execve() 이 수행하는 것은 프로그램을 실행하는 것이다. 프로그램의 경로와 인자(argument)의 배열, 그리고 프로그램 설정을 위한 환경 변수 목록을 받아들인다. (너무 데이터가 많기 때문에 출력 결과에서는 생략된 형태로 보인다.)


마지막 2 라인은 알아볼 수 있는 또 다른 정보를 제공한다. 먼저, write() 호출은 C 프로그램에 포함된 "hi\n" 문자열을 포함하고 있다. write() 의 첫번째 인자는 문자열이 출력되는 대상 파일 디스크립터(file descriptor)이다. 여기서는 '1'이며, 프로세스의 표준 출력 스트림(standard output stream)을 의미한다. write 호출 이후 (알아보기 약간 어려울 수 있는데 실제 화면 출력과 strace 명령의 출력이 섞여서 보이기 때문이다), 프로그램은 exit_gorup() 을 호출한다. 이 함수는 exit() 와 비슷하게 동작하나, 프로세스의 모든 스레드(thread)를 종료한다. 


execve() 호출과 write() 호출 사이에 존재하는 모든 호출들은 대체 무엇인가? 메인 프로그램이 시작하기 전에 모든 표준 C 라이브러리들을 호출하는 작업들이다. 이런 호출들은 기본적으로 런타임(runtime)을 초기화 하는 작업이다. 눈여겨 들여다 보면 몇가지 파일들을 점검하고, 파일들에 접근 가능한지 확인한 후 파일을 연다. 그리고, 그것들을 메모리 위치에 매핑(mapping)한 후에 닫는다.


한가지 중요한 힌트 : 이러한 함수들 모두 문서화 되어 있으며, man 페이지를 이용해 읽어볼 수 있다. 만일, mmap()이 무엇을 하는 것인지 모른다면, "man mmap" 명령을 실행해 보면 도움말(정의/사용법/옵션 등)을 찾아볼 수 있다.


위에 예시한 strace 출력 결과에 나오는 모든 함수를 찾아보고 공부하기 바란다. 책보다 쉽게 배울 수 있다!


현실에서 실행 중인 프로세스를 추적하기 (Tracing a running, real-world process)


내가 strace 같은 툴을 필요로 하는 순간은 이미 실행 중인 프로세스가 정상적으로 동작하지 않을 때이다. 일반적인 상황에서는 이러한 프로세스가 초기화 시스템(init system)에 의해 구동되기 때문에 문제해결을 하기가 여간 힘든 것이 아니다. 따라서, 로그를 분석하거나 모니터링 도구를 외부에서 구해야만 한다.


strace 는 이미 구동 중인 프로세스의 내부 동작을 손쉽게 들여다 볼 수 있게끔 해준다. 운영 중인 루비 유니콘 프로세스 (루비 웹 서버)에 문제가 있고, 프로세스의 로그를 들여다 봐서는 도저히 쓸만한 정보를 얻을 수 없는 상태라고 가정해보자. strace 의 '-p' 옵션을 사용해 프로세스에 연결할 수 있다. 운영 중인 서버의 가동율이 높아서 꽤 많은 출력이 발생할 것이라고 예상되면 '-o' 옵션을 사용해 출력 결과를 로그 파일에 저장할 수 있다.


$ sudo strace -o /tmp/strace.out -s 2000 -fp 12152 Process 12152 attached with 4 threads - interrupt to quit ^CProcess 12152 detached Process 12155 detached Process 12160 detached Process 3146 detached


(strace 를 이용해 실행 중인 프로세스를 추적하니) 단 몇 초 만에 로그 파일의 크기는 9,000 라인을 넘어 버렸다. 아래에 어쩌면 흥미로울지도 모르는 로그 일부를 캡쳐(capture) 해두었다.


3419 select(16, [14 15], NULL, [7 8], {29, 0}) = 1 (in [14], left {28, 987147}) 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 122710542}) = 0 3419 fcntl(14, F_GETFL) = 0x802 (flags O_RDWR|O_NONBLOCK) 3419 accept4(14, 0, NULL, SOCK_CLOEXEC) = -1 EAGAIN (Resource temporarily unavailable) 3419 getppid() = 25937 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 123156611}) = 0 3419 clock_gettime(CLOCK_MONOTONIC, {92751, 940365496}) = 0 3419 select(16, [14 15], NULL, [7 8], {29, 0}) = 1 (in [14], left {28, 972271}) 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 152520833}) = 0 3419 fcntl(14, F_GETFL) = 0x802 (flags O_RDWR|O_NONBLOCK) 3419 accept4(14, 0, NULL, SOCK_CLOEXEC) = -1 EAGAIN (Resource temporarily unavailable) 3419 getppid() = 25937 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 152849344}) = 0 3419 clock_gettime(CLOCK_MONOTONIC, {92751, 970066349}) = 0 3419 select(16, [14 15], NULL, [7 8], {29, 0}) = 1 (in [14], left {28, 958562}) 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 195372960}) = 0 3419 fcntl(14, F_GETFL) = 0x802 (flags O_RDWR|O_NONBLOCK) 3419 accept4(14, 0, NULL, SOCK_CLOEXEC) = -1 EAGAIN (Resource temporarily unavailable) 3419 getppid() = 25937 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 195710712}) = 0 3419 clock_gettime(CLOCK_MONOTONIC, {92752, 12883582}) = 0 3419 select(16, [14 15], NULL, [7 8], {29, 0}) = 1 (in [14], left {28, 956099}) 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 240006588}) = 0 3419 fcntl(14, F_GETFL) = 0x802 (flags O_RDWR|O_NONBLOCK) 3419 accept4(14, 0, NULL, SOCK_CLOEXEC) = 13 3419 fstat(13, {st_mode=S_IFSOCK|0777, st_size=0, ...}) = 0 3419 recvfrom(13, "GET /a/check HTTP/1.0\r\nX-Forwarded-For: 127.0.0.1\r\nHost: localhost\r\nX-Request-Start: t=1390981158239000\r\nConnection: close\r\nUser-Agent: curl/7.29.0\r\nAccept: */*\r\n\r\n", 16384, MSG_DONTWAIT, NULL, NULL) = 167 3419 ppoll([{fd=13, events=POLLOUT}], 1, NULL, NULL, 8) = 1 ([{fd=13, revents=POLLOUT}]) 3419 write(13, "HTTP", 4) = 4 3419 ppoll([{fd=13, events=POLLOUT}], 1, NULL, NULL, 8) = 1 ([{fd=13, revents=POLLOUT}]) 3419 write(13, "/1.1 ", 5) = 5 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 241130051}) = 0 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 241273452}) = 0 3419 stat("/opt/app/tmp/cache/CE5/510/http%3A%2F%2Flocalhost%2Fa%2Fcheckh%3F", 0x7fffcb874db0) = -1 ENOENT (No such file or directory) 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 241762672}) = 0 3419 clock_gettime(CLOCK_REALTIME, {1390981158, 242012184}) = 0 3419 sendto(16, "Q\0\0\0\rSELECT 1\0", 14, MSG_NOSIGNAL, NULL, 0) = 14 3419 poll([{fd=16, events=POLLIN|POLLERR}], 1, 4294967295) = 1 ([{fd=16, revents=POLLIN}]) 3419 recvfrom(16, "T\0\0\0!\0\1?column?\0\0\0\0\0\0\0\0\0\0\27\0\4\377\377\377\377\0\0D\0\0\0\v\0\1\0\0\0\0011C\0\0\0\rSELECT 1\0Z\0\0\0\5I", 65536, 0, NULL, NULL) = 66 3419 rt_sigprocmask(SIG_SETMASK, ~[HUP INT QUIT KILL USR1 SEGV USR2 TERM CHLD STOP TTIN TTOU VTALRM WINCH RTMIN RT_1], NULL, 8) = 0


모든 라인을 분석할 수는 없지만, 일단 첫번째 라인을 확인해보면, select() 호출이다. 프로그램은 select()를 이용해 시스템에 위치한 파일 디스크립터(file descriptor)의 변화(활동)를 감시한다. select() man 페이지에 의하면, 두번째 인자는 읽기 처리를 감시하기 위한 파일 디스크립터 리스트이다. select() 는 설정 가능한 시간만큼 블록(block)되며 파일 디스크립터의 활동을 주시하고, 활동이 감지된 파일의 갯수를 반환한다.


일단 나머지 인자들에 대해서는 무시하고, select() 호출이 14 및 15번 파일 디스크립터의 읽기 처리를 감시하고 (추적 결과의 오른편을 주시할 것), '1' 값을 반환한다. strace는 몇가지 시스템 호출에 대한 정보를 추가로 제공하기 때문에 우리는 가공되지 않은(raw) 결과 뿐만 아니라, 어떤 파일 디스크립터(14)가 변화했는지를 파악할 수 있다. 가끔은 시스템 호출에 대한 가공되지 않은 정보가 필요한 경우가 있으며, 이럴 때는 "-e raw=select" 옵션을 strace 명령에 추가하면 된다. 이렇게 하면 strace 가 select() 호출에 대한 가공되지 않은 형태의 실행 결과를 출력한다.


그렇다면, 14 및 15 파일 디스크립터는 무엇인가? 무언지 알 수 없다면, 앞서 확인한 단서는 무용지물이다. lsof 명령을 이용해 어떤 파일인지 확인해 보자.


COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ruby 3419 app DEL REG 0,4 26473410 /dev/zero ruby 3419 app 0r CHR 1,3 0t0 14 /dev/null ruby 3419 app 1w REG 202,1 18 159903 /opt/app/log/unicorn-out.log ruby 3419 app 2w REG 202,1 621 159902 /opt/app/log/unicorn-err.log ruby 3419 app 3r FIFO 0,8 0t0 36967385 pipe ruby 3419 app 4w FIFO 0,8 0t0 36967385 pipe ruby 3419 app 5r CHR 1,3 0t0 14 /dev/null ruby 3419 app 6w CHR 1,3 0t0 14 /dev/null ruby 3419 app 7r FIFO 0,8 0t0 36967386 pipe ruby 3419 app 8w FIFO 0,8 0t0 36967386 pipe ruby 3419 app 9w FIFO 0,8 0t0 26472351 pipe ruby 3419 app 10u IPv4 36967388 0t0 TCP some-internal-host:60490->some-internal-host:6379 (ESTABLISHED) ruby 3419 app 11u unix 0xffff8800e8589a00 0t0 26473540 socket ruby 3419 app 12u IPv4 36967390 0t0 TCP some-internal-host:53691->some-internal-host:6379 (ESTABLISHED) ruby 3419 app 14u unix 0xffff8800e80a23c0 0t0 26473408 /opt/app/sockets/unicorn.socket ruby 3419 app 15u IPv4 26473409 0t0 TCP *:http-alt (LISTEN) ruby 3419 app 16u IPv4 36967393 0t0 TCP some-internal-host:49929->some-internal-host:postgresql (ESTABLISHED) ruby 3419 app 17u IPv4 36968294 0t0 UDP *:42720 ruby 3419 app 18u IPv4 36969521 0t0 TCP some-internal-host:43407->some-internal-host:mysql (ESTABLISHED) ruby 3419 app 19u IPv4 36969774 0t0 TCP some-internal-host:53698->some-internal-host:6379 (ESTABLISHED) ruby 3419 app 21u IPv4 36969842 0t0 TCP some-internal-host:33576->some-internal-host:mysql (ESTABLISHED) ruby 3419 app 22u IPv4 36969020 0t0 TCP some-internal-host:52827->some-internal-host:postgresql (ESTABLISHED) ruby 3419 app 23u IPv4 36969112 0t0 TCP some-internal-host:33209->178.236.7.109:https (ESTABLISHED) ruby 3419 app 24u IPv4 36969137 0t0 TCP some-internal-host:35439->some-internal-host:mysql (ESTABLISHED) ruby 3419 app 25u IPv4 36969148 0t0 TCP some-internal-host:49847->some-internal-host:mysql (ESTABLISHED) ruby 3419 app 26u IPv4 36970664 0t0 TCP some-internal-host:35217->some-internal-host:mysql (ESTABLISHED) ruby 3419 app 27u IPv4 54296608 0t0 TCP some-internal-host:48903->some-external-host.com:http (ESTABLISHED)


"FD"라는 타이틀로 표시된 lsof 출력의 4번째 컬럼을 보면, 프로그램이 사용하는 파일 디스크립터라는 것을 알 수 있다. 그렇다! 14, 15 는 유니콘(Unicorn) 웹 서버가 listen 하고 있는 TCP 및 UNIX 소켓이다. 프로세스가 select() 를 이용해서 포트를 주시하고 있다는 말이 된다. 그리고, UNIX 소켓 (파일 디스크립터 14)를 통해서 통신이 발생하고 있다는 것 또한 알 수 있다.


다음으로, 우리는 몇 개의 시스템 호출을 통해 UNIX 소켓을 통해 수신되는 통신을 받아들이려 시도하고 있고, EAGAIN이 반환되고 있다는 것을 확인할 수 있다. 이것은 멀티 프로세싱 서버(multi processing server)에서 정상적인 동작 방식이다. 프로세스는 수신되는 데이터를 대기(감시)하고, 읽기를 반복한다.


결과적으로 accept4() 호출 이후에 파일 디스크립터(13)가 오류 없이 반환된다. 이제 요청(request)을 처리할 시간이다. 먼저 프로세스는 파일 디스크립터의 정보를 fstat() 호출을 이용해 확인한다. fstat()의 두번째 인자는 'stat' 구조체이며, fstat() 호출이 파일 상태 정보를 채워준다. 모드(S_IFSOCK)와 크기(일반적인 파일이 아니므로 0으로 보인다)를 확인할 수 있다. 짐작컨데 소켓 파일 디스크립터일 것이고, 프로세스는 recvform() 호출을 이용해 데이터를 읽어낸다.


흥미로운 점에 대해 생각해보기 (Here’s where things get interesting)


fstat() 처럼 recvform()의 첫번째 인자는 데이터를 읽기 위한 파일 디스크립터이며, 두번째 인자는 읽어들인 데이터를 담을 버퍼이다. 이것이 프로그램을 디버깅할 때 진정 흥미로운 포인트다 : 웹 서버 프로세스로 전송된 HTTP 요청 데이터 전체를 볼 수 있다! 가독성을 높인 다음 예시를 참고하기 바란다. :


GET /a/check HTTP/1.0 X-Forwarded-For: 127.0.0.1 Host: localhost X-Request-Start: t=1390981158239000 Connection: close User-Agent: curl/7.29.0 Accept: */*


충분히 제어할 수 없는 프로세스에 대한 문제해결(troubleshooting)을 시도할 경우, 위와 같은 정보는 아주 큰 도움이 된다. recvfrom() 호출의 응답 데이터는 수신한 데이터의 바이트 수이다 (167). 이제 응답(response)할 시간이다.


프로세스는 먼저 ppoll을 이용해 시스템 측에 소켓에 쓸 수 있는 시점이 언제인지를 확인한다. ppoll()은 파일 디스크립터 목록을 받고, 이벤트를 설정한다. 여기서는 프로세스가 소켓이 블록 해지 되고(POLLOUT) 쓸 수 있는 시점에 통지(notified) 받기를 요청한다. 소켓에 쓸 수 있다고 통지되면, HTTP 응답 헤더를 write() 호출을 이용해 쓰기 시작한다.


다음으로 유니콘 프로세스의 내부 라우팅(routing) 동작이 어떻게 동작하는지를 들여다 볼 수 있다. stat() 을 이용해 요청된 주소(URL 경로)를 위해 사용할 수 있는 물리적 파일이 파일 시스템 내에 있는지를 확인한다. stat()이 ENOENT를 반환하면, 해당 경로에 파일이 존재하지 않는다는 것을 의미한다. 파일이 존재하지 않는 것을 확인한 후 프로세스는 다음 단계의 작업을 수행한다. 일반적으로 정적인 파일 기반의 레일스 시스템 캐싱(caching on Rails systems)은 이와 같이 동작한다. 요청에 해당(대응)하는 정적 파일이 존재하는지를 확인하고, 없다면 코드를 실행한다.


마지막으로 프로세스가 어떻게 동작하는지를 가볍게 흟어보자면, SQL 쿼리가 파일 디스크립터 16에 쓰였다는 것을 확인할 수 있다. lsof 출력 결과를 다시 보면, 파일 디스크립터 16은 또 다른 호스트의 postgresql 포트 (번호와 이름의 매핑은 /etc/services 파일에 설정되어 있음)로 연결된 TCP 연결이라는 것을 알 수 있다. sendto() 를 이용해 포맷된 SQL 쿼리를 postgresql 서버로 전송한다. 세번째 인자는 메시지의 길이이며, 네번째 인자는 플래그(flag) - 여기서는 MSG_NOSIGNAL - 인데 운영체제로 하여금 원격 연결 중에 오류가 발생하더라도 SIGPIPE 신호를 이용해 인터럽트(interrupt)하지 말 것을 요청하는 것이다.


프로세스는 다음으로 poll() 함수를 이용해 읽기를 준비하거나, 소켓 오류를 처리하거나, 데이터를 읽을 수 있으면 recvfrom()를 이용해 postgresql 서버의 응답을 받아들인다.


몇가지 세밀한 것들은 건너 뛰었지만, strace, lsof와 시스템 man 페이지를 조합해 프로그램의 보이지 않는 부분이 어떻게 동작하는지를 파악할 수 있다는 것을 이해했으리라 믿는다.


정상이란? (What’s “normal”?)


가끔은 프로세스의 개략적인(overview) 동작 방식만을 알고자 할 때도 있다. 90년대 후반에 "엔터프라이즈 자바"로 개발된 공급망관리시스템(supply chain management system) 제품의 장애를 해결할 때 이런 필요가 있었다. 잘 동작하다가도 특정 시간에 알 수 없는 상태에 빠지고 연결이 안되는 문제가 발생했었다. 우리는 소스 코드를 가지고 있지도 않았고, 기술지원 담당의 수준 낮은 답변에 지쳐 있었다. (결국 우리 스스로 모든 문제를 해결했다.) 그래서, 나는 "정상""적인 동작과 비정상적인 동작을 비교해보기로 결정했다.


주기적으로 프로세스의 시스템 호출 기록을 샘플링(sampling)한 후에 정상적인 샘플과 시스템 장애가 발생했을 때의 샘플을 비교해 보았다. 그 당시의 정확한 결과를 기억하지는 못하지만, 이전에는 쓰지 않았던 트릭(trick)을 사용해서 해결해냈다. 최근까지도 항상 strace를 동작시키는 스크립트를 작성하고, 결과를 기록하며, 집계를 생성하고는 하고 있으며, 문제를 해결하기 위해 strace 로그를 활용한다.


이제 유니콘 프로세스를 다시 살펴보자.


sudo strace -c -p 1346 Process 1346 attached - interrupt to quit ^CProcess 1346 detached % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 97.06 0.062667 29 2132 select 1.23 0.000791 0 7035 clock_gettime 1.19 0.000767 2 316 poll 0.42 0.000269 0 2116 2105 accept4 0.05 0.000030 0 137 write 0.04 0.000024 0 173 close 0.03 0.000017 0 2184 fcntl 0.00 0.000000 0 350 36 read 0.00 0.000000 0 79 open 0.00 0.000000 0 351 14 stat 0.00 0.000000 0 93 fstat 0.00 0.000000 0 79 mmap 0.00 0.000000 0 79 munmap 0.00 0.000000 0 3 rt_sigaction 0.00 0.000000 0 54 rt_sigprocmask 0.00 0.000000 0 156 ioctl 0.00 0.000000 0 2 alarm 0.00 0.000000 0 83 socket 0.00 0.000000 0 86 2 connect 0.00 0.000000 0 218 sendto 0.00 0.000000 0 195 10 recvfrom 0.00 0.000000 0 3 recvmsg 0.00 0.000000 0 11 shutdown 0.00 0.000000 0 1 bind 0.00 0.000000 0 5 getsockname 0.00 0.000000 0 1 getpeername 0.00 0.000000 0 1 getsockopt 0.00 0.000000 0 2 clone 0.00 0.000000 0 156 gettimeofday 0.00 0.000000 0 11 getrlimit 0.00 0.000000 0 2094 getppid 0.00 0.000000 0 12 futex 0.00 0.000000 0 196 ppoll ------ ----------- ----------- --------- --------- ---------------- 100.00 0.064565 18414 2167 total


strace 명령에 '-c' 옵션을 추가해 시스템 호출 횟수를 출력하도록 했다. '-c' 옵션을 사용해서 strace를 실행할 경우, 일정 시간만큼 실행하도록 한 후에, 프로세스에 인터럽트를 보내서 (ctrl-c)  집계 결과를 출력해야 한다. 출력 결과는 따로 설명이 필요없을 만큼 깔끔하다.


나는 지금 오전 7시에 글을 쓰고 있고, 시스템은 업무 시간 중에 가동된다. 따라서, 지금 추적하는 유니콘 프로세스는 거의 동작하지 않는 상태이다(inactive). 위에 출력된 추적 결과를 보았다면, 놀랄만한 데이터가 없다는 것을 알 수 있을 것이다. 유니콘은 대부분의 시간을 select() 호출에 사용한다. 우리는 이제 select() 를 이용해 들어오는 연결을 기다린다는 것을 안다. 그러니, 프로세스는 대부분의 시간을 클라이언트(브라우저)의 요청을 대기하는데 사용한다. 이건 말이 된다.


더불어 accept4() 의 에러 반환 값이 상대적으로 많다는 것을 볼 수 있다. 위에서 본 예제처럼, accept4() 는 일상적으로 EAGAIN 오류를 받고, 다시 연결을 기다리기 위해 select() 호출을 실행한다.


남은 함수 목록들은 C 시스템 호출들을 다시 공부하는데 도움이 될만한 좋은 예제이다. 할일 목록(to-do list)에 등록하고 하루에 하나씩 읽어보고 이해할 수 있도록 하는 것이 좋다. 이렇게 하면, 다음에는 유니콘 프로세스가 부하가 발생하거나 문제가 생겨서 분석할 때, 좀 더 수월하게 시스템 호출들을 분석할 수 있을 것이다.


어디가 느린거지? (Finding out what’s slow)


시스템의 성능 문제가 발생했을 때 - 시스템이 느려진 상태일 때 - strace를 어떻게 활용할 수 있는지에 대한 이야기로 마무리하고자 한다. 최근들어 업무에서 트롤 같은 문제(gremlin-like problems)가 간혹 발생한 적이 있어서 strace 를 이용해 원인을 추적한 적이 있는데, 분산 시스템 상에서 서로 관련이 없는 컴포넌트들의 성능이 저하되는 것으로 보이는데 이유를 알 수가 없었다. 데이터베이스 중에서 하나가 느려지고, 일부 웹 서비스의 성능이 저하되었다. 결국은 sudo 까지도 느려지고 말았다.


그것이 최종적인 단서였다. 우리는 sudo 명령을 strace로 추적했고, sudo 가 수행하는 각각의 시스템 호출 시간을 측정했다. 결국은 로그 명령문이 성능 저하의 원인이라는 것을 밝혀낸 것이다!  보아하나 syslog 서버를 제대로 확인하지 않고 잘못 설정해 놓았고, 그 서버로 로그를 전송하는 모든 서버가 이해할 수 없을만큼 느려지고 만 것이다.


간단한 예제를 이용해서 프로세스의 성능 문제를 추적해보도록 하자. 다음과 같은 C 소스로 작성된 프로그램을 떠올려보고, 왜 실행하는데 2초 이상의 시간이 소모 되는지를 밝혀내 보고자 한다.


#include <stdio.h> #include <unistd.h> void main() { printf("Hallo there!\n"); sleep(2); printf("Goodbye\n"); }


코드를 흟어보면, 눈에 띄는 명백한 문제는 없다 (헛!), 그러니 답을 찾기 위해 strace 를 실행할 것이다. '-t' 옵션을 추가해 추적을 하는 동안 각각의 시스템 호출이 수행되는데 걸리는 시간을 측정하도록 한다. 아래에 추적 결과를 보자:


$ strace -T ./time_me execve("./time_me", ["./time_me"], [/* 25 vars */]) = 0 <0.000307> brk(0) = 0x75f000 <0.000022> access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) <0.000027> mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3bb589a000 <0.000021> access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) <0.000021> open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 4 <0.000023> fstat(4, {st_mode=S_IFREG|0644, st_size=35614, ...}) = 0 <0.000019> mmap(NULL, 35614, PROT_READ, MAP_PRIVATE, 4, 0) = 0x7f3bb5891000 <0.000021> close(4) = 0 <0.000017> access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) <0.000021> open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 4 <0.000025> read(4, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\37\2\0\0\0\0\0"..., 832) = 832 <0.000020> fstat(4, {st_mode=S_IFREG|0755, st_size=1852120, ...}) = 0 <0.000019> mmap(NULL, 3966008, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 4, 0) = 0x7f3bb52b1000 <0.000022> mprotect(0x7f3bb5470000, 2093056, PROT_NONE) = 0 <0.000027> mmap(0x7f3bb566f000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 4, 0x1be000) = 0x7f3bb566f000 <0.000027> mmap(0x7f3bb5675000, 17464, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f3bb5675000 <0.000025> close(4) = 0 <0.000019> mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3bb5890000 <0.000021> mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3bb588e000 <0.000021> arch_prctl(ARCH_SET_FS, 0x7f3bb588e740) = 0 <0.000018> mprotect(0x7f3bb566f000, 16384, PROT_READ) = 0 <0.002229> mprotect(0x600000, 4096, PROT_READ) = 0 <0.000027> mprotect(0x7f3bb589c000, 4096, PROT_READ) = 0 <0.000026> munmap(0x7f3bb5891000, 35614) = 0 <0.000036> fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 <0.000018> mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3bb5899000 <0.000021> write(1, "Hallo there!\n", 13Hallo there! ) = 13 <0.000035> rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0 <0.000018> rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0 <0.000019> rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 <0.000018> nanosleep({2, 0}, 0x7fffaa25c770) = 0 <2.000153> write(1, "Goodbye\n", 8Goodbye ) = 8 <0.000070> exit_group(8) = ?


보시다시피, strace 출력 결과의 모든 시스템 호출에 마지막 항목에 실행 시간(초 단위)이 포함되어 있다. 한줄 한줄 따라가 보면, 대부분의 호출 수행 시간이 매우 짧은데 마지막에서 2초 이상의 시간을 소모하는 호출을 찾을 수 있다! 미스터리는 풀렸다. 우리의 프로그램 코드 중에서 nanosleep() 함수를 호출하면서, 2초라는 인자를 넘기는 코드가 있는 것 같다.


실제로 추적할 때는 방대한 양의 추적 데이터를 파일에 저장한 후에 마지막 컬럼을 기준으로 정렬한 후, 문제가 되는 함수 호출부터 순서대로 확인하는 방식이 좋다.


남은 이야기 (There’s more!)


strace는 놀랍도록 많은 기능을 제공하는 툴(tool)이다. 그중에서 자주 사용하는 몇몇 옵션들만을 언급했을 뿐이며, 더 많은 옵션들이 있다. strace man 페이지를 찾아 더 많은 기능들을 살펴 보기 바란다. 그중에서 '-e' 옵션은 정규표현식을 입력받아 추적 결과를 필터링(filtering)하는 식으로 제어할 수 있기 때문에 꼭 살펴봐야 한다.


이 글을 끝까지 읽는 동안 언급된 모든 함수들의 의미를 이해하지 못했을 수도 있다. 그러나, 너무 걱정하지 말기 바란다. 나조차도 다 아는 것은 아니다. 대부분의 지식을 오동작하는 프로그램을 추적하면서 익히고, man 페이지를 읽으면서 공부했다. 만일 UNIX/LINUX 환경에서 개발하고 있다면, 개발하고 있는 프로그램을 strace로 추적해보기 바란다. 시스템 수준에 어떤 일이 벌어지는지를 학습해라. man 페이지를 읽어라. 탐험해라(Explore). 즐거울 뿐만 아니라, 이해하게 되고 이전보다 일도 잘하게 될 것이다.


추가 : 로터스 도미노 서버의 운명은? (UPDATE: And the fate of Lotus Domino server?)


듣자하니, 이 글이 도미노 서버를 설치하는 과정에서 발생한 드라마 같은 이야기로 알려지고 있는 것 같다. 트위터 같은 사이트에서 모든 사람들이 내게 "세상에나, 도미노 서버는 결국 어떻게 된거죠?" 라고 묻는다. 그러니, 이제 이야기의 끝자락을 마무리 하고자 한다.


어떻게든 시스템 호출을 추적하고 난 후에 도미노 서버를 고치고야 말았다. 유감스럽게도 그 당시에 정확히 어떻게 고쳐냈는지 기억하지 못하지만, 기적적으로 고쳐냈고 그 작업에 관련된 모든 사람들이 '엄청나게(huge)' 놀라워 했다는 것만은 확실하다.


나와 동료는 출장을 위해 빌렸던 빨간색  포트 무스탕 컨버터블에 타기 위해 주차장으로 갔고, 음악을 듣기 위해 사두었던 단 한장의 CD를 꽂고 볼륨을 높였다. (Guns & Roses’ Appetite for Destruction, 파괴하고픈 욕망) 차를 몰고 무사히 공항에 도착했지만, 우리는 CD를 잊고 차에 두고 내리고야 말았다. 무사히 아틀란타를 거쳐 멤피스에 도착한 후에야 CD를 놓고 왔다는 사실을 깨달아 버린 것이다.


누군가 우리 다음에 그 차를 빌린 사람은 우리만큼 글램 메탈(glam metal, hair metal) 음악을  즐겼기를 바란다.


  • 상용 유닉스에서는 strace 명령이 아니라, truss 라는 명령을 사용해야 합니다.
  • poor man's profiler 라는 글도 읽어보시면 좋습니다.


Posted by 곽중선
2015. 4. 17. 20:15

예전에 모 대기업 의뢰를 받아 "중급 자바 개발자 실력 검증"을 위해 출제한 문제들입니다.


필기 문제 20문항과 실기 문제 1문항으로 이루어져 있으며, 

자바 문법 / 객체지향 이해도 / 자바 프로그램의 동작 원리 이해 등을 묻는 질문으로 구성했습니다.


[MS 워드 형식]


JAVA 필기문제.docx


JAVA 필기문제_답안.docx


JAVA 실기문제.docx


AddressBook_exam.zip


[PDF 형식]


JAVA 필기문제.pdf


JAVA 필기문제_답안.pdf


JAVA 실기문제.pdf


Posted by 곽중선
2015. 4. 4. 11:05

"Gregor Hohpe의 횡성수설"이라는 글 모음 중 하나를 번역했습니다. 소프트웨어 설계에 대한 훌륭한 통찰(insight)를 일상생활을 통해 얻을 수 있다는 깨달음을 주는 훌륭한 글입니다. 데이터베이스 관련 기술을 이야기 하고 있지만, 동기/비동기 처리 방식을 왜 구분하고, 어떻게 써야 하는지에 이해하는데 도움이 됩니다.


원문 사이트 : Starbucks Does Not Use Two-Phase Commit 


스타벅스는 2단계 커밋을 사용하지 않는다.


Hotto Cocoa o Kudasai (ホットココアください, Hot Cocoa, please,  핫 코코아 주세요.)


방금 2주간의 일본 출장을 마치고 돌아왔습니다. 출장 중에서 접한 익숙한 광경 중 하나는 신주쿠와 록본기 주변의 말도 안될만큼 많은 스타벅스 커피숖들이었습니다. 주문한 "핫 코코아"를 기다리면서 스타벅스가 어떻게 음료 주문을 처리하는지를 가만히 생각해보기로 했습니다. 스타벅스는 대부분의 다른 기업들처럼 주문 처리량을 극대화하는데 매우 큰 관심을 가지고 있습니다. 많은 주문은 많은 수익을 의미하죠. 결과적으로 그들은 비동기 처리 방식(asynchronous processing)을 사용합니다. 당신이 주문을 하면, 점원은 커피 한잔의 주문을 입력하고 대기열(queue)에 추가합니다. 대기열 혹은 큐(queue)는 문자 그대로 에스프레소 기계의 상단에 늘어서 쌓여 있는 빈 커피 컵들을 의미합니다. 이 대기열은 점원과 바리스타를 분리시키고 바리스타(barista)가 잠시 동안 숨을 돌리거나 반대로 매우 바쁜 경우에도 점원이 주문을 계속 받을 수 있도록 해줍니다. 이런 방식은 카페가 바쁜 시간에 다수의 바리스타들을 "경쟁적인 처리자(Competing Consumer)" 시나리오대로 움직이게끔 합니다.


상관 관계 (Correlation)


비동기 일처리 방식의 잇점을 선택함으로써 스타벅스는 비동기 방식이 근본적으로 가지고 있는 문제를 다룰 수 밖에 없게 됩니다. 예를 들자면, 상관관계입니다. 음료 주문은 2가지 이유 때문에 반드시 주문한 순서대로 처리해야 할 필요가 없습니다. 첫째, 바리스타들이 서로 다른 도구를 이용해 음료를 만든다는 점입니다. (드립 커피용 도구와 에스프레소 머신 등등) 또, 혼합 음료(blended drink)는 드립(drip) 커피 보다는 만드는데 시간이 더 걸립니다. 둘째, 바리스타들은 처리 시간을 최적화한 일괄(batch) 작업을 통해 한번에 여러가지 음료들을 만들 수 있습니다. 결과적으로 스타벅스는 상관관계라는 문제를 안고 있는 것입니다. 음료는 주문 순서에 상관없이 만들어지겠지만, 그것을 주문한 고객에게 정확히 전달 되어야 합니다. 스타벅스는 이러한 문제를 메시징 아키텍쳐(messaging architectures)에서 사용하는 상관관계 식별자(Correlation Identifier) 패턴으로 해결하고 있습니다. 대다수의 미국 스타벅스 매장에서는 명확한 상관관계 식별자인 고객의 이름을 컵에 쓰고, 커피가 완성되면 컵에 적힌 고객의 이름을 부릅니다. 다른 나라들에서는 음료의 종류를 이용해 상관 관계를 식별합니다.


예외 처리 (Exception Handling)


비동기 메시지 시나리오(asynchronous messaging scenario)에서 예외 처리는 어려운 문제입니다. 현실 세계에서 예외(문제)를 처리하는 최적의 사례를 살펴보려면 스타벅스가 문제를 해결하는 방식을 관찰해 보면 됩니다. 만일 당신이 마치 지갑을 깜박 잊고 있었다면, 스타벅스의 점원들은 어떻게 대응할까요? 이미 음료를 만들었다면 치워 버릴 것이고, 아직 만들기 전이라면 주문 목록(대기열 혹은 큐)에서 제외할 것입니다. 만일, 당신이 주문한 음료를 마음에 들어하지 않거나 주문하지 않은 음료를 내놓았을 경우에는 음료를 다시 만들어 낼 것입니다. 만일 커피 머신이 고장나서 주문한 커피를 제공할 수 없다면, 환불해 줄 것입니다. 각각의 시나리오는 제각기 다른 상황이지만, 일반적인 오류 처리 전략(error handling strategy)에 대해 설명하고 있습니다.


가격 인하 혹은 취소 (Write-off) - 다양한 오류 처리 전략 중에서 가장 단순한 것입니다 : 아무 것도 하지 않거나, 이미 벌어진 일을 취소하는 것입니다. 썩 좋아 보이지는 않지만 실전 비즈니스에서는 그럭저럭 수용할만한 선택입니다. 손실이 작을 경우, 제대로 일처리하기 위해 오류 정정 시스템을 구축하는 것이 오히려 더 많은 비용을 지출하는 것일 수도 있습니다. 예를 들자면, 제가 컨설팅을 수행했던 여러 ISP(Internet Service Provider)사업자들은 과금(billing) 및 프로비저닝(provisioning) 싸이클(cycle)에서 발생하는 에러를 이런 방식으로 처리했습니다. 결과적으로, 고객은 비용을 청구받지 않고 서비스를 누리게 되는 것입니다. 이런 방식을 도입할지라도 수익 상의 손실은 사업을 유지하는데 있어 지장이 없을 만큼 작았습니다. 주기적으로, "무료(free)" 계정을 찾아 정리하기 위한 "조정 보고서"를 작성하는 작업을 수행하고, "무임승차"한 고객들을 처분합니다.


재시도 (Retry) - ("트랜잭션"이라고 부르는) 큰 작업 그룹을 실행하는 중에 오류가 발생했을 때, 우리는 기본적으로 두 가지를 선택할 수 있습니다. 이미 수행된 작업을 취소하는 것 혹은 실패한 작업들을 재시도하는 것입니다. 재시도가 현실적으로 성공할 가능성이 있다면 그럴싸한 선택입니다. 예를 들어 비즈니스 규칙을 위반한 것이라면, 재시도는 성공할 수 없을 것입니다. 그렇지만, 외부 시스템이 일시적으로 응답하지 않는 상황이라면 재시도는 성공할 가능성이 있습니다. 

단, 특별한 경우는 멱등 수신기(Idempotent Receiver : 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 가진 경우)입니다. 이러한 경우는 성공한 수신 처리 이후에 반복되는 메시지는 모두 무시되기 때문에, 간단히 모든 작업을 다시 시도할 수 있습니다.


보상 처리(Compensating Action) - 마지막 옵션은 일관성 있는 상태로 시스템을 돌려놓기 위해 이미 수행된 작업을 취소하는 것입니다. 통화(금융) 시스템을 다루는 경우 이러한 "보상 처리""는 잘 작동하는데, 인출된 돈을 "얼마든지" 재입금할 수 있기 때문입니다.


이러한 전략들은 분리된 준비와 실행 절차에 의해 동작하는 "2단계 커밋(2 phase commit)"과는 다릅니다. 스타벅스의 사례에 접목해보면, "2단계 커밋"은 점원(cashier)이 커피가 완성될 때까지 한 고객만을 바라보며 영수증과 돈을 테이블 위에 올려놓고 기다리는 것과 같습니다. 마지막으로 음료가 준비되면, 영수증과 돈으로 일거에 교환하는 것입니다. 이런 상황에서는 점원뿐만 아니라 고객까지도 "트랜잭션(거래)"이 끝날 때 까지 자리를 떠날 수 없게 됩니다. 이러한 "2단계 커밋" 방식을 적용하게 되면 일정 시간에 서비스 할 수 있는 고객 수는 극적으로 감소할 것이고, 스타벅스의 비즈니스를 확실하게 망하게 만들 것입니다. 이것은 2단계 커밋이 일상을 좀 더 단순하게 만들 수는 있지만, 메시지들의 자유로운 흐름을 방해하게 된다(그리고 확장성을 해친다)는 사실을 일깨워 주는 사례입니다. 왜냐하면, 비동기의 다중 작업 흐름 속에서 트랜잭션의 상태를 관리해야 하기 때문입니다.


대화 (Conversations)


커피숖 상호작용 사례는 간단하면서 일반적으로 대화 패턴(Conversations pattern)입니다.


두 그룹(고객과 커피숖)간의 상호작용은 짧은 동기 상호작용(주문과 지불)들과  하나의 긴 비동기 상호작용 (음료 만들기와 받기)로 구성됩니다. 이러한 대화(혹은 작동방식)는 구매 시나리오에는 매우 일반적인 것입니다.  예를 들어, 아마존(Amazon) 인터넷 쇼핑몰에서 주문할 경우, 주문 번호 발행과 그에 따른 처리 절차(신용카드 지불 요청, 포장, 발송) 등은 짧은 동기적 상호작용이지만, 전체 흐름은 비동기적으로 처리됩니다. 추가적인 단계들이 처리될 때마다 (비동기적으로) 이메일을 통해 통보 받을 수 있습니다. 만일 어떤 것이라도 잘못 처리되는 것이 있다면 아마존은 보상(신용카드 결제를 취소하는 등)하거나, 재처리(분실된 물품을 다시 배송하는 등)를 해줄 것입니다.

 

요약하면 실제 세계에서 벌어지는 일들은 대게 비동기적으로 동작한다는 것을 알 수 있습니다. 우리의 일상생활은 조화롭지만 비동기적인 상호작용(이메일을 읽고 쓰는 것, 커피를 사는 것 등등)들로 이루어져 있습니다. 이것이 의미하는 바는 비동기적인 메시지 아키텍쳐가 이러한 상호작용을 수행하는데 있어서 가장 자연스러운 방식이라는 것입니다. 또한 우리는 일상적인 생활 속에서 성공적인 메시징 솔루션을 설계할 수 있는 아이디어를 얻게 된 것입니다. 도모 아리가또 고자이마스. (Domo arigato gozaimasu! 감사합니다.)


참고 : 호토 코코아를 검색하면 , 일본 애니메이션 "호토 코코아"가 나오는 군요.

Posted by 곽중선