[펌]개발은 암기과목이 아니다

가끔씩 질문 게시판에 들러 글을 읽다보면 오류 내용을 통째로 복사해서 붙여 넣고 "이런 오류가 생겼는데 어떻게 해야 하나요?" 와 같은 식의 질문이 너무 많은 것 같아 안타까운 마음이 듭니다.

개발은 절대 암기로 배울 수 있는 영역이 아닙니다. 어떤 메시지가 나오면 어떤 부분을 고치면 된다는 식의 사례를 많이 외운다고 디버깅 능력이 높아지지는 않습니다.

프로그래밍 과정에서 발생하는 오류 메시지는 복사해서 검색을 하라고 나오는 것이 아니라 읽고 원인에 대해 생각하라고 제공되는 것입니다. 디버깅이 이런 경우에 이렇게 고치고 저런 경우에 저렇게 고치는 공식을 외워서 적용하는 과정이라면 오류 사전 같은 게 벌써 나왔겠지요.

예를들어, 커넥션 풀을 사용하다 "Connection Pool Exception: Cannot get a connection, pool error Timeout waiting for idle object"와 같은 오류를 접했다면, 가장 먼저 할 일은 복사해서 구글에서 검색하거나 질문 답변 게시판에 "이런 오류 났는데 어떻게 해야되요?"라고 묻는 것이 아니라 오류 내용을 읽어보는 것입니다.

그래서 '커넥션 풀', '타임아웃', '아이들 객체' 같은 개념들이 어떤 의미를 가지는지 이해하려고 노력하고, 안되면 관련 문서를 찾아보는 과정을 거치는 것이 정상입니다.

'커넥션 풀'이 무슨 개념이고 왜 사용하는지를 이해했다면 아마도 '아이들 객체'가 대기 상태의 데이터베이스 연결을 뜻함은 어렵지 않게 유추할 수 있을 것입니다. 스택트레이스를 읽을 줄 안다면 해당 오류가 'commons-dbcp'라는 커넥션 풀 라이브러리에서 발생하는 것임도 알아볼 수 있을테니 필요하면 해당 라이브러리의 프로젝트 페이지에 찾아가 문서를 참조할 수도 있을 것입니다. (인터넷 검색은 보통 스택트레이스를 통째로 검색하는 게 아니라 이런 경우 'dbcp'라는 키워드로 프로젝트 페이지를 찾을 때 쓰는 것입니다.)

이렇게 기본 개념들을 다시 정리했으면 이젠 왜 그런 문제가 생겼는지 연역적 추리를 통해 원인을 찾아보면 됩니다.

타임아웃이 될 때까지 대기 상태의 연결을 얻지 못하고 포기했다는 건 어떤 경우에 발생할까 생각해보면 어렵지 않게 '모든 연결이 타임아웃 시간 동안 사용 중인 경우'가 원인임을 유츄할 수 있습니다. 그럼 다음 단계로, 어떤 경우에 타임아웃 시간 동안 연결을 항상 사용하게 되는 지 고민해보면 대략 다음과 같은 논리적 '경우의 수'가 발생하는 것을 알 수 있습니다:

  1. 연결을 한 번에 너무 오래 사용하는 경우
  2. 커넥션 풀이 예약하는 연결 갯수가 지나치게 적은 경우.
  3. 지나치게 짧은 시간 동안 대기하는 경우.

이런 경우 이해가 가지 않으면 머릿속으로 비슷한 경우를 형상화하거나 비유해서 이해해보는 것이 도움이 됩니다. 예컨대 커넥션 풀의 자원 관리 모델은 은행을 방문해서 대기표를 받고 업무를 보는 것과 비슷합니다.

은행 업무를 보기 위해 들렀다가 사람이 너무 많아서 점심시간 내내 기다리다 되돌아갔다면 이유가 무엇이었을지는 아마 은행 사진을 찍어서 게시판에 질문하지 않아도 누구나 쉽게 이해할 수 있을 것이라 생각합니다.

점심시간이라는 정해진 기간이 타임아웃 기간이고, 은행 창구가 얻으려는 리소스, 즉 데이터베이스 연결이고, 해당 지점 창구의 갯수가 커넥션 풀의 크기('max idle')라면 앞서 발생한 오류의 원인이 되는 경우의 수를 직관적으로 이해할 수 있을 것입니다:

  1. 연결을 한 번에 너무 오래 사용하는 경우 -> 예) 진상 고객.
  2. 커넥션 풀이 예약하는 연결 갯수가 지나치게 적은 경우 -> 창구나 담당 직원 부족.
  3. 지나치게 짧은 시간 동안 대기하는 경우 -> 바빠서 빨리 사무실도 돌아가야하는 경우.

이렇게 경우의 수까지 추출했다면 그 다음으로는 실제 지금 프로젝트가 어느 경우에 해당하는지 검증만 하면 됩니다. 1번의 경우라면 정말 오래 걸리는 질의가 있는지, 혹은 질의는 짧게 끝나도 연결을 닫지 않아 계속 사용 중 상태로 남아있는 경우가 있는지 찾아보면 되겠지요.

사실 이번 오류의 예는 얼마전 실제로 이곳 질문 게시판에서 읽은 사례입니다. 질문자는 역시 오류 메시지만 복사해서 질문을 올렸고 이에 대한 답은 "'removeAbandoned' 속성을 설정하면 되더라" 였습니다.

DBCP 라이브러리의 'removeAbandoned' 속성은 연결을 일정 시간 닫지 않았을 때 이를 강제로 해제하는 속성입니다. 이는, 앞서 말한 경우의 수 중 1번의 경우, 또 그 중에서도 질의가 오래 걸리는 것이 아니라 개발자가 연결 해제를 잊었을 경우에만 적용될 수 있는 해법입니다. 그나마도, 정상적인 해결 방법은 해당 부분을 찾아서 연결 해제 코드를 넣는 것이기 때문에 임시 방편이나 미봉책에 불과하다고 할 수 있습니다.

이는 비유를 하자면, 내가 은행 지점장인데 점심시간에 사람이 붐벼서 고객 불만 사항이 많은 경우에 덮어 놓고 "한 사람당 창구는 3분씩만 쓰게 하라"고 강제하는 것입니다. 실제로는 창구를 하나 더 열거나 창구 하나 잡고 한 시간씩 떼를 쓰는 진상 고객 한 명만 처리하면 될 문제를 덮어놓고 "창구가 붐비면 이용 시간을 제한한다"는 '해법'을 공식처럼 적용한 결과입니다.

그럼에도 불구하고 이런 식으로 문답이 오가는 것은 질문자나 답변자 모두 올바른 디버깅 방법을 이해하지 못하고, 그냥 어떤 오류가 발생하면 어떻게 대처한다는 단편적 지식을 많이 쌓으면 개발 능력이 높아질 것으로 기대하는 잘못된 인식을 가지고 있기 때문입니다.

그런 습관이 생긴 경우 흔히 보는 현상이 무작정 아무 것이나 바꾸어 보는 것입니다. 검색해서 해결이 안되면 지푸라기라도 잡는 심정으로 개발 환경도 새로 설치해보고, 어제는 된 것 같은데 오늘부터 안된다면 어제자 소스로 롤백해보기도 하는 식으로 시간 낭비를 하는 것은 흔하게 볼 수 있습니다.

역시 은행 지점의 예로 비유를 하자면 사람이 많아 불만이 접수되면 일단 인테리어도 바꾸어보고 그런 문제가 없는 지점과 직원들도 서로 교체해보고 하는 식의 쓸데없는 시간 낭비를 하는 것입니다.

혹시 해당 질문 답변 글을 쓰신 분들이 이 글을 보시고 기분이 상하셨다면 죄송합니다만, 저는 단지 해당 사례가 바로 기억에 떠올라서 예시로 들었을 뿐 이는 그 분들만의 문제도 아니고 그 분들의 잘못이라고만 할 수 있는 것도 아니라고 봅니다.

이 곳의 질문 답변 게시판만 봐도 단순히 오류 내용 복사해 붙여넣고 내용을 이해하려는 어떠한 노력 없이 "이런 오류가 생겼는 데 어떻게 하나요?"하고 물어보는 글은 너무도 흔하게 접할 수 있습니다. 그리고 "이 것 저 것 다해봐도 안되요", "어제까진 됐는데 갑자기 안되요", "이클립스도 다시 깔아봤어요" 같은 절박한 푸념도 쉽게 보는 내용입니다.

제 생각에 이는 신입 개발자의 자질보단 그런 신입 개발자를 키워내는 시스템의 문제가 더 크지 않나 싶습니다. 개발자를 양성하는 학원은 많지만 개념이나 접근 방법을 제대로 가르치기보단 단 시간에 스프링 MVC 예제 따라해보기 같은 식으로만 수업을 진행하니 정작 중요한 내용은 수박 겉핥기 식으로 넘어가게 된 것이 아닌가 생각합니다.

또한 실무에서도 회사들이 면접에서 "스프링은 써봤어요?" 같은 걸 물어보지 디버깅을 어떻게 하는지, 객체지향을 얼마나 이해하고 있는지 같은 근본적인 지식을 따지지 않기 때문에 개발자로 입문하는 분들이 무턱대고 준비없이 완전히 이해하지 못하는 코드를 따라 치는 연습만 하니까 원래 개발은 그런 식으로 하는 것이다라는 착각을 하게 되는 게 아닌가 싶습니다.

정리하면, 개발은 원리를 이해하고 이를 적용해서 문제를 해결하는 최적의 답을 찾는 과정이지 '스프링-마이바티스' 같은 정해진 답 하나를 반복 숙달해서 아무데나 가져다 쓰는 단순 반복 작업이 아닙니다.

또한 디버깅은 문제의 원인을 연역적 사고 과정을 통해 찾아내는 절차이지, 결코 "A의 오류 메시지에는 B로 대응"과 같은 공식을 찾아 적용하는 작업이 아닙니다.

혹시라도 그런 식의 잘못된 접근 방법으로 개발에 입문한 신입 개발자분들이 계시면 지금부터라도 올바른 방법이 무엇인지 고민해 보셨으면 좋겠습니다.

그래서 앞으로는 질문 답변 게시판에 "이런 오류가 났는데 도와주세요" 같은 글보다는 최소한 "이런 오류가 났는데 메시지에서 이런 개념이 이해가 가지 않습니다" 또는 "이런 오류는 이럴 때 발생하는 것 같은 데 다른 경우가 있을까요?" 같은 최소한의 사고 과정이 드러나는 질문을 보다 많이 볼 수 있게 되었으면 좋겠습니다.

 

출처:http://okky.kr/article/311337

이글을 보고 많이 반성하게 되네요. 저또한 문제에 대한 정확한 해결분석보다 문제 대한 빠른 해답을 찾을려고 하는 경향이 있는데 앞으로 이런 습관을 고치려고 노력해야겠다는 생각이드네요.

이상하게 한국 남자들은 사진을 찍을 때 잘 웃지 않는다. 그런 점에서는 기자 또한 마찬가지. 왠지 사진 찍으면서 웃으면 나답지 않은 사진이 나와 버릴 것만 같은 생각이 들어서 무의식 적으로 입을 다물게 된다. 분위기 있게 찍으려면 근엄한 표정을 지어야 할 것 같은 생각이 들기 때문이기도 하다. 하지만 막상 사진을 보면 근엄하게 찍어봐야 못생긴 얼굴 어디 안 간다. 차라리 웃으며 찍은 사진이 그나마 남들 보기에 기분이라도 좋다.

어쩌면 우리는 프로그래밍에 임할 때에도 너무 근엄하게 혹은 너무 진지하거나 책임감에 억눌려 있는 것은 아닐까? 마이루비닷넷(
http://myruby.net)이란 블로그를 운영하고 있는 강문식 씨는 특집 6부에서 그러한 개발자들의 문제점들을 해소하는 것이야말로 개발 고수로 가는 길이라고 제시해 주고 있다.

한 달이 멀다하고 쏟아져 나오는 새로운 기술들. 개발자는 정말 배우고 익혀야 할 기술들이 너무나 많다. 어찌 생각하면 이런 일들은 개발자들에게 부담으로 느껴진다. 짜증나고 지겨운 현업에 어렵고 복잡한 새 기술들을 더해야 하니 그럴 만도 하겠다. 하지만, 강문식 씨는 오히려 그것들을 즐기는 법을 터득한 고수다.

5년 전부터 운영해 오고 있는 그의 블로그가 바로 그 증거다.

그는 새로운 기술이 나올 때마다 그것들을 이용하여 작고 재미있는 애플리케이션을 만들어본다. 아주 작고 간단하게 만들 수 있는 애플리케이션들이기 때문에 만드는데 많은 시간과 노력을 들일 필요는 없다. 단지, 새 기술에 자신의 아이디어를 더하여 무언가 창조해 낸다는 것이 중요한 것이다. 이러는 과정에서 새 기술에 대해 이해하고 익히게 되는 것은 당연한 일. 하지만, 여기에 또 한 가지 그만의 비법을 더함으로서 재미삼아 만든 새 애플리케이션에 생명이 더해진다. 바로 블로그다. 자신이 만든 것들에 대한 정보와 소스코드를 몽땅 블로그에 공개하여 그것을 다른 사람들에게 전파하고, 자신이 미처 생각지 못했던 것들에 대한 피드백을 받으며 자신의 아이디어를 성장시켜 가는 것이다.

이렇게 성장한 아이디어는 강문식 씨 자신의 업무에 도움을 주기도 하고, 새로운 프로젝트에 적용되기도 한다. 새로운 기술이 나올 때마다 스트레스 없이 자신의 취미를 즐길 수 있고, 미리미리 습득해 둔 신기술은 누구보다 트렌드에 빠른 개발자로 그를 성장시키고 있다. 말로 내뱉기는 쉽지만 실천하기는 어려운 일이기에 지난 5년간 그가 블로그를 통해 또 커뮤니티를 통해 쌓아온 일들의 가치가 더욱 크게 느껴지는 것일 터다.
****************************************************************************

프로그래밍은 재미있다. 그 이유를 꼽자면 여러 가지가 있겠지만, 우리보다 훨씬 먼저 이에 대해 이야기한 사람이 있으니 그의 글을 인용해보자. 프레드 브룩스(Frederick P. Brooks, Jr)는 『The Mythical Man Month』에서 프로그래밍이 재미있는 이유를 다음과 같이 말했다.

무엇인가를 만드는 일, 즉 프로그래밍은 창조의 즐거움을 준다. 그리고 이렇게 만든 결과물이 다른 이에게도 유용한 것이 되므로 보람과 기쁨을 느낄 수 있다. 또한 정교한 퍼즐 문제를 풀기위해 몰입하고, 끝내 이를 풀어냈을 때 느낄 수 있는 희열도 있다.

이런 느낌은 독자들도 경험해봤을 것이다. 그리고 항상 무언가를 배우고 있다는 기쁨도 빼놓을 수 없다. 프로그래머의 일상은 단순 반복이 아니기 때문이다. 마지막으로 비교적 다루기 쉬운 수단을 가지고 일하는 즐거움이다.

대부분 공감할 수 있는 내용들이다. 프로그래밍은 본디 재미있는 일이다.

필자가 프로그래밍을 처음 접한 것은 초등학생 때였다. 학원을 더 오래 다닌 형들이 배우는 C 언어가 엄청 대단해 보이던 시절이었다. GW-Basic 화면에 PLAY 명령과 함께 EDCDEEE2DFEDC4처럼 알 수 없는 코드들을 입력하면 익숙한 음악이 연주되던 그 프로그램이 정말 신기하고 재미있었다. 그리고 LINE, CIRCLE 등의 명령으로 그려지는 그래픽은 아름답기까지 했다.

그 때부터였다. 프로그래밍 세계에 중독된 것이 말이다. 다들 그런 경험이 한번쯤 있을 것이다. 내 손으로 처음 만든 프로그램이 잘 돌아갈 때의 그 즐거웠던 경험은 잊을 수 없다. 독자 여러분도 이런 생각이 들지는 않는지 모르겠다. 지금 혹시 어떤 이유에서건 개발이 즐겁지 않다면, 그 때를 잠시 떠올려보면 어떨까? '그 때 참 재미있게 프로그래밍 했었지, 실력도 많이 늘었던 것 같아.'라고 말이다.

어떤 일을 하건 마찬가지겠지만, 특히나 프로그래머는 즐겁게 일해야 한다. 개인적인 소망은 모든 개발자들이 프로그래밍을 처음 접하던 그때의 즐거움을 계속 간직할 수 있으면 좋겠다.

코드는 프로그래머의 생각을 비춰주는 거울과도 같다. 즐겁게 행복하게 개발해야 더 나은 제품이 나온다. 지금부터는 즐거운 프로그래밍을 위해 필자가 종종 활용하는 방법을 몇 가지 소개해볼까 한다.

새로움 더하기
작년 초의 일이다. 스프링노트 서비스 개발을 시작한지도 꽤 되어서인지 이제 슬슬 개발에 익숙해지기도 하고, 또 일부 코드는 자신을 리팩토링 해달라며 나쁜 냄새를 조금씩 풍기기 시작했다. 당시 스프링노트 소스는 단위 테스트도 부족해서, 매번 불편함을 느끼고 있었다. 이 불편함이 쌓이면 안 될 것 같기도 했고, 개인적으로도 분위기 전환이 필요하다고 느꼈다.

그래서 현재 개발 중인 코드에 조금 새로운 시도를 해보기로 결정했다. 당시 많이 쓰이고 있던, 단위 테스트 라이브러리인 xUnit 대신, BDD 프레임워크인 RSpec(http://rspec.rubyforge.org/)을 활용해 기존 코드에 스펙을 붙이고, 또 새로 추가되는 기능은 BDD를 따르기 시작했다.

처음 해보는 RSpec 코딩은 무척 즐거웠다. 그리고 테스트 커버리지 툴인 rcov로 자주 테스트 커버리지를 측정하며 목표 수치(90%)에 조금씩 다가갔다. 지금 돌이켜서 생각해보니, 적절한 시점(프로젝트가 지겨워질 만한)에 새로움을 더해 업무 속도도 떨어뜨리지 않고, 개인적인 수련도 하고, 코드 품질도 올라가는 일석삼조의 효과를 거둔 것 같다.

자신만의 도전과제 만들기
필자는 항상 프로젝트를 시작할 때 그 프로젝트팀의 성공 외에도 개인적인 도전 과제(개발자로써)를 부여하는 편이다.

예를 들면, 윈도우 애플리케이션을 개발할 때에는, 당시에 익숙했던 MFC(Microsoft Foundation Class Library) 대신, 처음 사용해보는 WTL(Windows Template Library)을 도입해서 장단점을 확인해 보았다. 또 PHP를 이용해 개발하는 프로젝트에서 MVC 프레임워크를 직접 만들어 보기도하였다.

그렇게 함으로써 조금 더 프로젝트의 열정을 가질 수 있게 되었다. 현재는 스프링노트 프로젝트의 테스트 커버리지 수치를 100%로 계속 유지하며 개발해보는 실험을 재미있게 진행하고 있기도 하다.

이미 진행 중인 프로젝트가 있다면, 거기에 약간의 새로움을 가미해보길 바란다. 그렇다면 더 즐겁게 일할 수 있을 것이다. 물론 그 새로움이란, 자기계발과 프로젝트에 모두 도움이 되는 것으로 선택하는 것이 좋다. 그리고 작은 목표(기왕이면 측정 가능한 것)도 도움이 된다. 예를 들면 코드 품질을 나타낼 수 있는 몇 가지 방법(Code Metrics)을 적용할 수 있을 것이다.

작은 아이디어 구현하기
기존 프로젝트에서 새로움을 쉽게 찾지 못한다면, 가끔은 완전히 다른 프로젝트를 시도해 보는 것도 좋다. 그러기 위해서는 평소에 생각나는 아이디어, 한번쯤 만들어보고 싶은 프로그램을 위키 등에 정리해두는 것이 도움이 된다. 메모를 할 때는 간단한 아이디어 설명과 왜 만들어보고 싶은지, 만들면 얼마나 걸릴지를 대략적으로 적어두는 것이 좋다.

그러면 나중에 여유가 생겼을 때 이 페이지를 열고 아이디어 목록 중 하나를 골라서 만들어볼 수 있어서 좋다. 거창한 프로젝트보다는 작은 장난감 프로그램이나 프로토타입을 만드는 것이 적당하다. 예를 들어 <화면 1>은 필자가 최근에 적은 아이디어 메모다.

<화면 1> 아이디어 메모의 예



그리고 어느 날 집중력이 떨어지는 느낌이 들어 위키에서 이 아이디어를 꺼내서 직접 구현해봤다. <화면 2>는 이렇게 구현된 애플리케이션이다. 내가 직접 사용할 프로그램이라 스펙이 명확하고, 크기도 작아서 예상했던 대로 한 시간 안에 충분히 만들 수 있었다. 하지만 지금까지도 잘 사용하고 있는 유용한 프로그램이기도 하다. 물론 코코아 프레임워크를 이용해 처음 만들어본 프로그램(예제가 아니라)이라는 면에서도 새로웠다.

<화면 2> <화면 1>의 아이디어로 만든 애플리케이션



'작은 아이디어 구현하기'는 내가 평소에 해보고 싶었던 것을 직접 만들어보는 것이므로 즐겁다. 그리고 짧은 기간에 작은 성공을 통한 성취감도 맛볼 수 있다. 현재 업무에 지치고, 피곤해서 일이 손에 잡히지 않는다면 작은 아이디어 구현하기를 통해 새로운 에너지를 얻을 수 있다.

새 시대의 장난감, 오픈API
최근에 구현한 또 다른 아이디어는 슬러거(Slugger, http://myruby.net/pages/391476)라고 이름 붙인 블로깅 툴이다. 이 프로젝트가 재미있었던 점은 일반적인 웹 애플리케이션과 달리, 데이터베이스를 전혀 사용하지 않고 만든 순수 매시업 애플리케이션(Mashup Application)이라는 점이다.

슬러거에서 블로그 포스트와 환경 설정 등은 스프링노트에 저장하고, 각 포스트에 대한 사용자의 댓글은 미투데이에 저장했다. 그 결과 두 서비스의 장점을 모두 취할 수 있었다. 평소에 계속 글감을 만들어두었던 스프링노트의 글들이 바로 블로그 포스트로 노출되어 편하기도 하고, 이 글이 미투데이에서 유통되어 그 무섭다는 무플도 피할 수 있었다.

결과뿐만 아니라, 과정에서도 데이터베이스를 설치하고 호출하는 것보다 쉬운 방법으로 이미 잘 만들어진 OpenAPI를 사용해 개발에 필요한 시간을 단축할 수 있었다. 현재 필자의 블로그에서도 사용하고 있는 슬러거는 고작 100여 줄의 코드로 4시간 동안 만든 것이다.

매쉬업 애플리케이션이 좋은 이유는 소위 말하는 맨땅에 헤딩을 피할 수 있기 때문이다. 좋은 기능들을 웹 서비스 형태로 노출하는 웹사이트가 많으므로 이를 잘 선택해 활용하면, 만들 때도 재미있고, 꼭 만들어보고 싶은 가치에 집중할 수 있다. 물론 이 편이 더 만들기도 쉽다. 그리고 매쉬업을 만들면 기존 서비스를 사용하던 사용자가 함께 따라온다. 따라서 더 빠르고 많은 사용자들의 목소리를 들을 수도 있다.

이렇게 만든 애플리케이션을 오픈 소스로 커뮤니티에 공개하면 이 프로젝트는 더 큰 생명력을 얻게 된다. 그리고 여러 면에서 내게 도움이 되는 피드백을 받을 수도 있다. 슬러거의 경우, 직접 설치 매뉴얼을 만들어주신 분도 있었고, 필자의 소스에서 레일스의 새로운 면을 배울 수 있어서 좋았다는 분도 있었다. 이처럼 여러 사람들에게 관심을 받을 수 있는 것도 개발자의 즐거움 중 하나다.

즐거운 언어, 루비
필자와 친하게 지나는 한 선배가 프로그래밍을 잘하려면 매년 새로운 언어를 하나씩 배우라고 조언을 해준 적이 있다. 이 말은 여러 언어를 통해 다양한 관점을 가지라는 의미일 것이다. 필자가 최근 주로 사용하고, 좋아하는 언어는 루비(Ruby)다. 루비를 통해 개발의 재미를 많이 느꼈는데, 그래서 이 글에서도 잠깐 소개해볼까 한다.

루비가 가진 모토는 '프로그래머의 단짝 친구'고, 개발자인 마츠는 루비를 기계가 아닌 프로그래머 자신을 위해 만들었다고 말한다. 이 말은 문법이 친근하다는 의미이기도 하지만, 필자는 개발자를 키우는 언어라는 의미로 받아들인다. 루비는 말랑말랑한 문법 때문에 표현력이 뛰어나다.

그래서 한 가지 로직을 가지고도 정말 다양한 방식으로 표현 가능하다. 그리고 코드 가독성이나 미적인 면(Code Beauty)을 중시하는 커뮤니티의 분위기 덕분에 항상 내가 작성한 코드보다 더 나은 코드를 찾아볼 수 있다. 그래서 개발자에게 지속적인 자극을 주고, 여기서 힌트를 얻은 개발자는 더 나은 코드를 작성할 수 있게 된다. 이것이 개발자를 키우는 언어라는 표현의 의미다.

그리고 특별히 더 즐거운 이유는 커뮤니티의 생동감 넘치는 분위기 때문이기도 하다.

최근 커뮤니티에서는 루비 2.0과 레일스 2.0이 활발하게 개발되고 있고, 이에 대한 열띤 토의도 오가고 있다. 말하자면 루비 커뮤니티는 건강하다. 이런 커뮤니티를 지켜보고 그 일원이 되는 것도 즐거운 경험이다.

나는 프로그래머라서 행복하고 즐겁다. 그리고 내 동료 프로그래머들도 모두 즐거웠으면 좋겠다. 즐겁기만도 부족한 시간들이다. 마지막으로 한 말씀 드리자면

"현재를 즐기세요. 그러면 어느새 수퍼 개발자가 되어 있을 거예요!" @

스프링노트 OpenAPI  

스프링노트(http://www.springnote.com/)는 위키 기반 인터넷 노트 서비스다. 이 서비스는 REST 방식의 OpenAPI를 제공하고 있어서, 개발자들이 다양하게 활용할 수 있다.

API를 활용하여 쉽게 페이지 리소스를 만들고 읽으며 쓰고, 지울 수 있다. 혹시 레일스 개발자라면 2.0에서 추가될 액티브 리소스의 장점을 스프링노트에서 체험해볼 수도 있다(SpringnoteResources라는 액티브리소스 Wrapper 라이브러리를 배포하고 있다). 자바나 PHP를 포함한 다양한 언어에 대한 바인딩이 있고, 없더라도 HTTP에 대한 이해만 있으면 어렵지 않게 API를 호출할 수 있다.

자세한 도움말은 http://dev.springnote.com/를 방문해보길 바란다. 스프링노트 OpenAPI를 활용한 매쉬업이 많이 나올 수 있기를 기대한다.



<화면 3> 스프링노트 개발자 페이지
 
 

 

출처:http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=5044&MAEULNO=28&no=38156&page=1

스피너선택에 따른 엑티비티 전환

 

public class PaymentActivity extends AppCompatActivity {

Button button;
ArrayList arrayList;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_payment);


arrayList = new ArrayList();
arrayList.add("1");
arrayList.add("2");


final String[] select_item = {""};



Spinner spinner = (Spinner) findViewById(R.id.spinner);
ArrayAdapter<String> adapter = new ArrayAdapter<String>
(this, android.R.layout.simple_spinner_dropdown_item, arrayList);
spinner.setAdapter(adapter);

Button button = (Button) findViewById(R.id.button);



spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
select_item[0] = String.valueOf(arrayList.get(arg2));
}

@Override
public void onNothingSelected(AdapterView<?> arg0) {

}
});



button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (select_item[0].equals("1")) {
Intent intent = new Intent(PaymentActivity.this, Activity1.class);
startActivity(intent);
finish();

} else if (select_item[0].equals("2")) {
Intent intent = new Intent(PaymentActivity.this, Activity2.class);
startActivity(intent);
finish();
}
}
});


}


}

안드로이드에서 html 문서 파싱을 하기 위해 검색을 해보았다. 


RSS를 지원하는 사이트의 파싱은 Xmlpullparser를 이용하는 것이 가장 효율적이라고 한다.

RSS를 지원하지 않는 사이트의 파싱은 DOM이나 Jericho를 사용할 수 있다.


===========================================================================================

첫번째 걸린 블로그이다.  http://blog-kr.specialguy.net/130


자바 html 파서 jericho의 안드로이드용이 다음 주소에 있다.

 jericho-android.jar 를 다운로드 하여 임포트 하여 쓰면 된다고 한다.

http://blog.naver.com/zeanz?Redirect=Log&logNo=110092582999


===========================================================================================


이클립스에서 jericho.jar를 임포트 하는 방법이다. 

출처는 http://dodo4989.tistory.com/106


먼저 Project에 라이브러리를 import시킨다.

즉, Project -> import -> General -> File System -> jar파일등록하면 

Project에 해당 Library가 추가된다.

 

다음으로는 해당 Library를 Project에서 사용할 수 있도록 설정을 해주어야 한다.

Project선택 후

Properties(Alt + Enter) -> Java Build Path -> Libraries Tab -> Add JARs -> JAR파일 선택

 


또는 import 시키지 않고 외부에서 Library를 가져올 수 도 있다.

 

Project선택 후

Properties(Alt + Enter) -> Java Build Path -> Libraries Tab -> Add External JARs -> JAR파일 선택

 

 

 

*. 추가 확인 사항

Jericho HTML Parser를 설치한 후에 java 소스 상에서 아래와 같이 사용할 경우.

Source source = null;

source = new Source(new URL(sourceUrlString));

Source Import시 두가지 중 import net.htmlparser.jericho.Source; 를 설정해 주어야 한다


 

 

===========================================================================================

사용 예제 하나 

http://blog.naver.com/mysk4521?Redirect=Log&logNo=40093081572


===========================================================================================

다른 블로그의 포스트 내용 중 일부다. http://blog.naver.com/freemagick/100138927843

1. jericho 파서를 다운받아 넷빈즈 프로젝트폴더의 libs안에 넣어준다.

2. manifest에서 

<uses-permission android:name="android.permission.INTERNET" /> 를 넣어준다. </application>다음에 넣어주도록한다.(이걸 안해주면 실행시 source객체를 이용할때 에러가뜨며 종료됨)


===========================================================================================

또 다른 사용 예를 살펴본다. 코드 출처는 http://ckritbuddy.tistory.com/42


이 예제는 주어진 url의 html을 읽어온 후 구문분석을 하고 그 중에서 text만 추출하여

텍스트뷰에 출력하는 작업을 수행한다.


// Jericho.java

package aa.bb.zz.Jericho;


import java.net.URL;

import net.htmlparser.jericho.Source;


import android.app.Activity;

import android.os.Bubdle;

import android.widget.TextView;


public class Jericho extends Activity {

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);


String yourUrl = "http://code.google.com/intl/ko/android/";


TextView tv = (TextView)findViewById(R.id.mainTextView);

tv.setText(getHtmltoText(yourUrl));

}


public String getHtmltoText(String sourceUrlString) {

Source source = null;

String content = null;


try {

//url에서 html 소스를 읽어온다.

source=new Source(new URL(sourceUrlString));

// 한글사용을 위해서는 윗 줄의 코드를 아래처럼 수정

// URL sUrl = new URL(sourceUrlString);

// InputStream is = sUrl.openStream();

// Source source= new Source(new InputStreamReader(is,"euc-kr"));

// "euc-kr" 부분은 charset에 맞춰 변경해준다. UTF-8, KSC5601 etc...


// 전체 소스 구문을 분석한다.

source.fullSequentialParse();

// HTML markup에서 text contents만 가져와서 스트링으로 변환한다.

content=source.getTextExtractor().toString();

} catch (Exception e) {

e.printStackTrace();

}

return content;

}

}



===========================================================================================

사용 예 하나더 , 택배조회 하는 예제 // 코드 출처는 http://webfine.tistory.com/4

CJ 택배 운송장 조회사이트를 파싱하여 배송현황만 추출하고 콘솔에 결과를 출력하는 소스이다.

사이트 모양은 위 링크 참조


List를 사용하는 부분들은 자료값을 가지고 와서 콘솔창에 찍어주기 위해 적은 부분으로, 마지막을 DB부분에 넣거나 View 페이지로 자료 값을 넘겨주어서 노출 시킬 수도 있습니다.

여기서 가장 중요한 부분은 getAllElements(HTMLElementName.태그이름.get(순서) 부분으로 실질적으로 해당 태그안의 값을 가져오는 부분입니다. 해당이름으로 된 태그의 자식태그들을 모두 가져 오는 것이 getAllElements() 메소드입니다.

해당 페이지를 소스 보기 하신 후 자식태그를 제외한 부모태그들만 순서를 계산하여서 가지고 오는 것이지요. 순서는 0번 부터 입니다.

그 데이터를 가져 온 부분에서 계속 하위로 검색을 하다보면 마지막에 자신이 원하는 값을 가진 HTML 부분은 파싱할 수 있을 것입니다.

단점은 해당 페이지의 디자인이 변경된다면 다시 소스를 작업해야 한다는 것이지만, 실제 DB에 접속이 불가능하지만 웹사이트에서 노출 되는 부분들이 있다면 이 방식으로 데이터를 가져올 수 있습니다.




import java.io.IOException;

import java.net.MalformedURLException;

import java.net.URL;

import java.util.ArrayList;

import java.util.Iterator;

import java.util.List;

 

import net.htmlparser.jericho.Element;

import net.htmlparser.jericho.HTMLElementName;

import net.htmlparser.jericho.Source;

 

public class HtmlTableTagParse {

    public static void main(String[] args) 

      throws MalformedURLException, IOException {




        //송장 번호를 받는다.

        String num = "602411606750";

         

        //송장 번호와 cj택배에 배송추적 관련 주소와 연결 시킨다.

          String url ="http://www.cjgls.co.kr/kor/delivery/service/

 


            searchView_1.asp?slipnom=" + num;

         

        //해당 URL 페이지를 가져온다.

        Source source = new Source(new URL(url));

         

        //메소드 찾기를 위해 시작부터 끝까지 태그들만 parse 한다 (?)

        source.fullSequentialParse();

         

        //해당 데이터가 있는 부분을 찾는 부분.

        Element div = source.getAllElements(HTMLElementName.DIV).get(2);

         

        Element table = div.getAllElements(HTMLElementName.TABLE).get(1);

         

        List trList = table.getAllElements(HTMLElementName.TR);

         

        Iterator trIter = trList.iterator();

         

        trIter.next();

         

        while(trIter.hasNext()){

            Element tr = (Element) trIter.next();

 

            List dataList = tr.getAllElements(HTMLElementName.TD);

             

            Iterator dataIter = dataList.iterator();

             

            int chk = 0;

             

            List resultList = new ArrayList();

             

            //원하는 결과 값이 들어가는 부분.

            List rowList = new ArrayList();

             

            while(dataIter.hasNext()){

                Element data = (Element) dataIter.next();

                String value = 

                          data.getContent().getTextExtractor().toString();

                rowList.add(chk,value);

                 

                chk++;

                 

                if(chk == 5){

                    resultList.add(rowList);

                    chk = 0;

                }

            }

             

            //콘솔 창에서 확인 하기 위한 부분.

            Iterator resultIter = resultList.iterator();

             

            while(resultIter.hasNext()){

                List list = (ArrayList)resultIter.next();

                 

                System.out.println((String)list.get(0) + list.get(1) +

                                list.get(2) + list.get(3) + list.get(4));

            }

        }

         

    }


 

안드로이드/Android Custom Dialog 만들기 

TestCustomDialogActivity.Java 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package arabiannight.tistory.com;
 
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import arabiannight.tistory.com.popup.CustomDialog;
 
public class TestCustomDialogActivity extends Activity {
 
    private CustomDialog mCustomDialog;
     
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }
 
    public void onClickView(View v){
        switch (v.getId()) {
        case R.id.bt_main:
            mCustomDialog = new CustomDialog(this,
                    "8월의 크리스마스!!",
                    "영화보러가자~!!!",
                    leftClickListener,
                    rightClickListener);
            mCustomDialog.show();
            break;
        }
    }
 
    private View.OnClickListener leftClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(getApplicationContext(), "왼쪽버튼 Click!!",
                    Toast.LENGTH_SHORT).show();
        }
    };
 
    private View.OnClickListener rightClickListener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Toast.makeText(getApplicationContext(), "오른쪽버튼 Click!!",
                    Toast.LENGTH_SHORT).show();
        }
    };
}



CustomDialog.Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package arabiannight.tistory.com.popup;
 
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import arabiannight.tistory.com.R;
 
public class CustomDialog extends Dialog{
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
         
        WindowManager.LayoutParams lpWindow = new WindowManager.LayoutParams();   
        lpWindow.flags = WindowManager.LayoutParams.FLAG_DIM_BEHIND;
        lpWindow.dimAmount = 0.8f;
        getWindow().setAttributes(lpWindow);
         
        setContentView(R.layout.custom_dialog);
         
        setLayout();
        setTitle(mTitle);
        setContent(mContent);
        setClickListener(mLeftClickListener , mRightClickListener);
    }
     
    public CustomDialog(Context context) {
        // Dialog 배경을 투명 처리 해준다.
        super(context , android.R.style.Theme_Translucent_NoTitleBar);
    }
     
    public CustomDialog(Context context , String title ,
            View.OnClickListener singleListener) {
        super(context , android.R.style.Theme_Translucent_NoTitleBar);
        this.mTitle = title;
        this.mLeftClickListener = singleListener;
    }
     
    public CustomDialog(Context context , String title , String content ,
            View.OnClickListener leftListener , View.OnClickListener rightListener) {
        super(context , android.R.style.Theme_Translucent_NoTitleBar);
        this.mTitle = title;
        this.mContent = content;
        this.mLeftClickListener = leftListener;
        this.mRightClickListener = rightListener;
    }
     
    private void setTitle(String title){
        mTitleView.setText(title);
    }
     
    private void setContent(String content){
        mContentView.setText(content);
    }
     
    private void setClickListener(View.OnClickListener left , View.OnClickListener right){
        if(left!=null && right!=null){
            mLeftButton.setOnClickListener(left);
            mRightButton.setOnClickListener(right);
        }else if(left!=null && right==null){
            mLeftButton.setOnClickListener(left);
        }else {
             
        }
    }
     
    private TextView mTitleView;
    private TextView mContentView;
    private Button mLeftButton;
    private Button mRightButton;
    private String mTitle;
    private String mContent;
     
    private View.OnClickListener mLeftClickListener;
    private View.OnClickListener mRightClickListener;
     
    /*
     * Layout
     */
    private void setLayout(){
        mTitleView = (TextView) findViewById(R.id.tv_title);
        mContentView = (TextView) findViewById(R.id.tv_content);
        mLeftButton = (Button) findViewById(R.id.bt_left);
        mRightButton = (Button) findViewById(R.id.bt_right);
    }
     

<Android> Volley 이미지 불러오기 예제

Android Volley ImageLoader

Volley uses ImageLoader to load images from network, and also to cache them into your Android phone’s in-memory cache by using the LruCache class. A good approach would be to use Android ImageLoader in a singleton pattern, as same cache should be used throughout the application. Also if used in a singleton pattern your cache would not be activity dependent. Hence whenever you close and open the activity, your images could be picked up from the cache, saving network requests. Now this brings up a question, how to make this volley singleton class? please have a look at the code below:

In the above class you may see that a CustomVolleyRequestQueue is set up which can return an object of ImageLoader. The class above also creates an ImageCache which works with normal volley cache, but only caching images for requests. Please note here: I just modified the singleton class from my previous Volley Example. Therefore as a whole, the above class can be used to make any type of Android volley requests, be it a JsonRequest, ImageRequest, or just a simple StringRequest.

Android Volley NetworkImageView

A new view introduced with Android volley is NetworkImageView. As a matter of fact this view replaces the standard ImageView, when comes to the usage of volley. Android Volley NetworkImageView is designed in such a way that it creates an image request, sends it and even displays the image, after it is downloaded from the URL. Also it cancels the request on its own if it is detached from the view hierarchy. Hence no need to handle the request cancellations with Volley NetworkImageView. Have a look at the layout for this Android Volley ImageLoader and NetworkImageView Example, it just includes a NetworkImageView instead of ImageView:

Next lets have a look at the code for the main activity class:

As you may see in the above class I used an instance of CustomVolleyRequestQueue to get the volley ImageLoader object. Further I just specified the URL through setImageUrl method of NetworkImageView. This handles all the functionality by itself.

Want to display images in a ListView using volley?

If you need to display images in a ListView just use the setImageUrl method of NetworkImageView in the getView method of your adapter. This will handle all your image requests automatically. Just a reminder, don’t forget to use the  NetworkImageView instead of normal ImageView.

Screenshot of Android Volley ImageLoader Example:
Android Volley NetworkImageView

 

 

 

 

 

출처:http://www.truiton.com/2015/03/android-volley-imageloader-networkimageview-example/


1. AndroidManifest.xml 에 권한 추가하기


<uses-permission
  android:name="android.permission.CAMERA" />
<uses-permission
  android:name="android.permission.WRITE_EXTERNAL_STORAGE" />


2. main.xml 레이아웃 만들기


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
<Button
android:id="@+id/button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="이미지 가져오기"
/>
<ImageView
android:id="@+id/image"
android:layout_width="90dp"
android:layout_height="90dp"
android:layout_gravity="center"
android:layout_margin="50dp"
android:background="#eee"
/>
</LinearLayout>


3. 소스 코드 작성


package pe.kr.theeye.cameracrop;

import java.io.File;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ImageView;

public class CameraCropActivity extends Activity implements OnClickListener
{
  private static final int PICK_FROM_CAMERA = 0;
  private static final int PICK_FROM_ALBUM = 1;
  private static final int CROP_FROM_CAMERA = 2;

  private Uri mImageCaptureUri;
  private ImageView mPhotoImageView;
  private Button mButton;

  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mButton = (Button) findViewById(R.id.button);
    mPhotoImageView = (ImageView) findViewById(R.id.image);

    mButton.setOnClickListener(this);
  }

  /**
   * 카메라에서 이미지 가져오기
   */
  private void doTakePhotoAction()
  {
    /*
     * 참고 해볼곳
     * http://2009.hfoss.org/Tutorial:Camera_and_Gallery_Demo
     * http://stackoverflow.com/questions/1050297/how-to-get-the-url-of-the-captured-image
     * http://www.damonkohler.com/2009/02/android-recipes.html
     * http://www.firstclown.us/tag/android/
     */

    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

    // 임시로 사용할 파일의 경로를 생성
    String url = "tmp_" + String.valueOf(System.currentTimeMillis()) + ".jpg";
    mImageCaptureUri = Uri.fromFile(new File(Environment.getExternalStorageDirectory(), url));

    intent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, mImageCaptureUri);
    // 특정기기에서 사진을 저장못하는 문제가 있어 다음을 주석처리 합니다.
    //intent.putExtra("return-data", true);
    startActivityForResult(intent, PICK_FROM_CAMERA);
  }

  /**
   * 앨범에서 이미지 가져오기
   */
  private void doTakeAlbumAction()
  {
    // 앨범 호출
    Intent intent = new Intent(Intent.ACTION_PICK);
    intent.setType(android.provider.MediaStore.Images.Media.CONTENT_TYPE);
    startActivityForResult(intent, PICK_FROM_ALBUM);
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data)
  {
    if(resultCode != RESULT_OK)
    {
      return;
    }

    switch(requestCode)
    {
      case CROP_FROM_CAMERA:
      {
        // 크롭이 된 이후의 이미지를 넘겨 받습니다.
        // 이미지뷰에 이미지를 보여준다거나 부가적인 작업 이후에
        // 임시 파일을 삭제합니다.
        final Bundle extras = data.getExtras();

        if(extras != null)
        {
          Bitmap photo = extras.getParcelable("data");
          mPhotoImageView.setImageBitmap(photo);
        }

        // 임시 파일 삭제
        File f = new File(mImageCaptureUri.getPath());
        if(f.exists())
        {
          f.delete();
        }

        break;
      }

      case PICK_FROM_ALBUM:
      {
        // 이후의 처리가 카메라와 같으므로 일단  break없이 진행합니다.
        // 실제 코드에서는 좀더 합리적인 방법을 선택하시기 바랍니다.

        mImageCaptureUri = data.getData();
      }

      case PICK_FROM_CAMERA:
      {
        // 이미지를 가져온 이후의 리사이즈할 이미지 크기를 결정합니다.
        // 이후에 이미지 크롭 어플리케이션을 호출하게 됩니다.

        Intent intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(mImageCaptureUri, "image/*");

        intent.putExtra("outputX", 90);
        intent.putExtra("outputY", 90);
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        intent.putExtra("scale", true);
        intent.putExtra("return-data", true);
        startActivityForResult(intent, CROP_FROM_CAMERA);

        break;
      }
    }
  }

  @Override
  public void onClick(View v)
  {
    DialogInterface.OnClickListener cameraListener = new DialogInterface.OnClickListener()
    {
      @Override
      public void onClick(DialogInterface dialog, int which)
      {
        doTakePhotoAction();
      }
    };

    DialogInterface.OnClickListener albumListener = new DialogInterface.OnClickListener()
    {
      @Override
      public void onClick(DialogInterface dialog, int which)
      {
        doTakeAlbumAction();
      }
    };

    DialogInterface.OnClickListener cancelListener = new DialogInterface.OnClickListener()
    {
      @Override
      public void onClick(DialogInterface dialog, int which)
      {
        dialog.dismiss();
      }
    };

    new AlertDialog.Builder(this)
      .setTitle("업로드할 이미지 선택")
      .setPositiveButton("사진촬영", cameraListener)
      .setNeutralButton("앨범선택", albumListener)
      .setNegativeButton("취소", cancelListener)
      .show();
  }}


테스트 이미지는 생략하겠습니다. 위의 소스로 충분히 좋은 설명이 될 수 있을것이라 생각합니다. 두번의 Intent 호출을 통해 이미지를 촬영하고 크롭을 하는 과정을 거치게 됩니다. 테스트 해보니 잘 되는군요.

참고 : http://stackoverflow.com/questions/1973359/android-crop-an-image-after-taking-it-with-camera-with-a-fixed-aspect-ratio

출처:http://theeye.pe.kr/archives/1285

원문: http://stackoverflow.com/questions/31954437/cant-type-into-android-studios-built-in-terminal

 

 

1. 터미널 열기

   다들 아시겠지만, 윈도우 8부터 윈도우의 검색기능이 강화되었습니다. 단축키는 window+q 를 누르시면 바로 검색 할 수 있습니다.

   이 검색창에서 cmd입력하여 cmd창을 열어줍니다.







2.속성

  cmd창의 제목표시줄에서 마우스 우클릭을 하면 다음과 같이 속성(P)가 보입니다.

  속성창으로 들어가 줍니다.






3. 레거시 콘솔 체크

  여기에서 레거시 콘솔을 체크해줍니다. 다시 실행해야 한다는데 저 말은 윈도우 재 부팅이 아닌 cmd창을 다시 시작 하라는 말입니다.







4. 안드로이드 스튜디오

  이제 안드로이드 스튜디오에서 신나게 타이핑을 합니다.

  참고: 만약 안되실 경우 X를 눌러 터미널 종료 후 다시 실행하시면됩니다.


참조:http://storyofdream.tistory.com/129

우체국 오픈API를 이용하여 우편번호를 얻어오는 안드로이드 예제입니다.

 먼저 우체국 오픈API를 사용하기 위해 아래 사이트에서 인증키를 발급받읍시다!

http://biz.epost.go.kr/customCenter/custom/custom_9.jsp?subGubun=sub_3&subGubun_1=cum_17&gubun=m07 

다음으로 안드로이드 프로젝트를 생성합니다.

이 때, AndroidManifest.xml 에 다음과 같이 권한을 설정해둡시다! (인터넷을 사용해야 하니까..!)

AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

 그 다음 주소를 사용자로부터 입력받고

우체국 오픈API로부터 받아온 우편주소를 출력하는 액티비티는 다음과 같이 구성하였습니다.

 

activity_main.xml

 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/LinearLayout1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="${relativePackage}.${activityClass}">

<TextView
android:id="@+id/textPost"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="주소입력 예)정릉동" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<EditText
android:id="@+id/addressedit"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="3"
android:ems="10">

<requestFocus />
</EditText>

<Button
android:id="@+id/btnsearch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="검색" />
</LinearLayout>

<ListView
android:id="@+id/addresslist"
android:layout_width="match_parent"
android:layout_height="wrap_content"></ListView>
</LinearLayout>

 

마지막으로 메인액티비티 코드입니다.

 

우체국 오픈API로부터 받아온 우편주소는 XML형태이기 때문에

Dom을 사용하여 쉽게 정보를 뽑아내고 있습니다.

 

안드로이드는 기본적으로 네트워크 통신은 스레드에서만 하기를 권장하므로 

AsyncTask를 사용하여 구현하였습니다. 

 

MainActivity.java

package worldpay.com.post;

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.TextView;

import org.apache.http.HttpResponse;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

public class MainActivity extends Activity {

//우체국 오픈api인증키
private String key = "우체국 API키";
private TextView addressEdit;
private Button searchBtn;
private ListView addressListView;
private ArrayAdapter<String> addressListAdapter;
//사용자가 입력한 주소
private String putAddress;
//우체국으로부터 반환 받은 우편주소 리스트
private ArrayList<String> addressSearchResultArr = new ArrayList<>();


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

addressEdit = (EditText) findViewById(R.id.addressedit);
searchBtn = (Button) findViewById(R.id.btnsearch);
addressListView = (ListView) findViewById(R.id.addresslist);
searchBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

getAddress(addressEdit.getText().toString());
}
});


}

private void getAddress(String kAddress) {
putAddress = kAddress;
new GetAddressDataTask().execute();
}

private class GetAddressDataTask extends AsyncTask<String, Void, HttpResponse> {
@Override
protected HttpResponse doInBackground(String... urls) {
HttpResponse response = null;
final String apiurl = "http://biz.epost.go.kr/KpostPortal/openapi";
ArrayList<String> addressInfo = new ArrayList<String>();
HttpURLConnection conn = null;
try {
StringBuffer sb = new StringBuffer(3);
sb.append(apiurl);
sb.append("?regkey=" + key + "&target=post&query=");
sb.append(URLEncoder.encode(putAddress, "EUC-KR"));
String query = sb.toString();
URL url = new URL(query);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("accept-language", "ko");
DocumentBuilder docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
byte[] bytes = new byte[4096];
InputStream in = conn.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while (true) {
int red = in.read(bytes);
if (red < 0) break;
baos.write(bytes, 0, red);
}
String xmlData = baos.toString("utf-8");
baos.close();
in.close();
conn.disconnect();
Document doc = docBuilder.parse(new InputSource(new StringReader(xmlData)));
Element el = (Element) doc.getElementsByTagName("itemlist").item(0);
for (int i = 0; i < ((Node) el).getChildNodes().getLength(); i++) {
Node node = ((Node) el).getChildNodes().item(i);
if (!node.getNodeName().equals("item")) {
continue;
}
String address = node.getChildNodes().item(1).getFirstChild().getNodeValue();
String post = node.getChildNodes().item(3).getFirstChild().getNodeValue();
Log.w("jaeha", "address = " + address);
addressInfo.add(address + "\n우편번호:" + post.substring(0, 3) + "-" + post.substring(3));
}
addressSearchResultArr = addressInfo;
publishProgress();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (conn != null) conn.disconnect();
} catch (Exception e) {
}
}
return response;
}

@Override
protected void onProgressUpdate(Void... values) {

//TODO Auto-generated method stub

super.onProgressUpdate(values);
String[] addressStrArray = new String[addressSearchResultArr.size()];
addressStrArray = addressSearchResultArr.toArray(addressStrArray);
addressListAdapter = new ArrayAdapter<String>(MainActivity.this, android.R.layout.simple_list_item_1, addressStrArray);
addressListView.setAdapter(addressListAdapter);
}
}
}

+ Recent posts