2007년 03월 26일
Help yourself! programmer...
"프로그래머들은 남들 편리하게 해주는 도구는 만들면서 정작 자신을 위한 도구는 만들지 않죠."
정말 맞는 말씀이다. 그런데 이런 얘기를 들으면 대부분의 프로그래머들은 변명하기 마련이다.
"프로젝트 마감에 쫒기는데 그럴 시간이 어디 있습니까?"
"우리 회사는 R&D하려고 하지를 않아요."
"초보 프로그래머에게 그럴 능력 있나요?"
"한국에서 개발은 꿈꿀 수 없는 일이라구요."
환경이 뒷받침 되지 못해서 못한다고들 말한다. 그럼 환경이 변할 때까지 기다려야 할까?
정부가 해결해 주겠는가.. 아니면, 프로그래밍 신이 계시를 내려 주시겠는가... 차라리, 로또를 사시는게 낫겠다.
당장 밥벌이가 급급하니까 어쩔 수 없다는 걸 인정한다고 해도, 그렇다면 당신의 미래는 누가 책임져 주는가? 할 줄 아는게 프로그래밍 밖에 없는데 그러다 점점 경력이 쌓이고, 매일 똑같은 작업이 지루해지고, 나보다 더 잘하는 후배들이 밀려들고, 자꾸 새로운 언어와 환경이 나오면 어디가서 무엇을 하려는가? 당신이 가장 잘할 수 있는 일을 더 잘할 수 있는 방법을 찾아야 한다. 그래야 살아 남을 수 있다. 더 많은 보수를 받을 수 있다. 더욱 존경 받을 수 있다. 무엇보다 그게 제일 재밌는 인생이다. 반면에, 하기 싫은 관리나 영업을 나이 들어 해봤자 처음부터 그 분야를 파던 사람보다 잘할리가 없지 않은가... (이런 말 하고 있지만, 나도 관리하고 영업하고, 기획하고, 제안서 쓰고, 술 접대하고, 아부하는 거 다 한다. 하지만 그것들은 늘 부업이라고 생각한다.)
서론이 길었다. 그럼 어떤 툴을 만들어야 할까? 당장 필요로 하는 걸 만들어야 한다. 데이터베이스는 거의 모든 프로젝트에서 사용되고 있고, 고성능, 대용량 시스템을 개발할 수 있다는 것은 데이터베이스를 잘 다룬다는 말과 다름이 아니다. 그런데, 개발자들은 의외로 데이터베이스에 무관심하거나 잘 모르는 경우가 많다. 물론 나 역시 그렇다. 현실적으로 데이터베이스를 잘 다루려면 몇 달 혹은 몇 년은 공부해야 하기에 그럴 수 밖에 없는 면도 있다.
데이터베이스를 잘 다루지 못하지만 개발은 빨리 진행해야 한다. 어려운 문제인데, 어떻게 해야할까? 범위를 조금 좁혀서 쿼리 문제를 다뤄보자. 거의 모든 포털 서비스는 게시판을 쓴다. 조금 과장해서 게시판만 만들 줄 알면 포털을 개발할 수 있다. 더 큰 문제는 문제는 어떻게 빠른 응답 속도를 유지하느냐 하는 것이다. 쿼리를 잘 작성하는게 답이다. 그냥 select, insert, update, delete 문장을 작성할게 아니라, 특정 영역을 빠르게 읽어낼 수 있는 쿼리를 만들어야 한다.
지금 다니는 회사에서 표준으로 사용하는 오라클용 게시판 쿼리들은 다음과 같다.
[ pk가 2개이고 order by가 없는 경우 ]
SELECT a.*
FROM T_CODE a,
( SELECT code_grp, code_id
FROM ( SELECT /*+ INDEX_DESC(code_grp) */
ROWNUM num, code_grp, code_id
FROM T_CODE
WHERE ROWNUM <= 10 * 101 /** pageList * pageNum **/
AND code_grp = 'B04' ) A
WHERE num > 10 * 100 ) b /** pageList * pageNum - 1 **/
WHERE a.code_grp = b.code_grp
AND a.code_id = b.code_id
[ pk가 2개이고 order by가 있는 경우 ]
SELECT a.*, rownum
FROM T_CODE a,
( SELECT code_grp, code_id, code_name
FROM ( SELECT ROWNUM num, code_grp, code_id, code_name
FROM ( SELECT /*+ INDEX_DESC(CODE_GRP) */
ROWNUM num, code_grp, code_id, code_name
FROM T_CODE
Where code_grp LIKE 'B04'
ORDER BY code_name DESC )
WHERE ROWNUM <= 10 * 1 ) A /** pageList * pageNum **/
WHERE num > 10 * 0 ) b /** pageList * pageNum - 1 **/
WHERE a.code_grp = b.code_grp
AND a.code_id = b.code_id
ORDER BY b.code_name DESC
[ pk가 1개이고 order by가 없는 경우 ]
SELECT /*+ INDEX_DESC(T_BOARD) */
A.*
FROM ( SELECT ROWNUM num, doc_seq, doc_title
FROM T_board
WHERE doc_title LIKE '%5%'
AND doc_seq > 0 /** index hint **/
AND ROWNUM <= 10 * 301 ) A /** pageList * pageNum **/
WHERE num > 10 * 300 /** pageList * pageNum - 1 **/
[ pk가 1개이고 order by가 있는 경우 ]
SELECT /*+ INDEX_DESC(T_BOARD) */
A.*
FROM ( SELECT ROWNUM num, doc_seq, doc_title
FROM ( SELECT ROWNUM num, doc_seq, doc_title
FROM T_board
WHERE doc_title LIKE '%5%'
AND doc_seq > 0 /** index hint **/
ORDER BY doc_title ASC )
WHERE ROWNUM <= 10 * 1 ) A /** pageList * pageNum **/
WHERE num > 10 * 0 /** pageList * pageNum - 1 **/
어떠신가? 머리가 아파오지 않는가...? 대규모 프로젝트를 하다보면 이런 쿼리를 일상적으로 사용하게 된다.
그렇다면 매일 이런 쿼리를 작성하고 있을까?
아무리 경력이 많은 사람도 이런 쿼리를 하루에 몇개씩 작성하라고 하면 실수하게 마련이다.
처음에는 무언가 배우는 것 같아 즐겁지만, 금새 지루해지게 마련이다.
게다가, 몸값 비싼 대리, 과장들이 이런 쿼리를 작성해서는 돈 못 번다.
그래서, 쿼리를 자동으로 변경해주는 프로그램을 작성해서 쓰고 있다.
아래와 같은 쿼리를 입력하면 위와 같은 쿼리로 변경해 주는 것이다.
SELECT doc_seq, doc_title FROM T_board WHERE doc_title LIKE '%5%'
완벽한 것은 아니고, 제약도 많지만 갖 입사한 개발자들을 데리고 대형 프로젝트를 하는 상황에서 놀라운 생산성 향상 효과를 가져온다. 첨부한 소스가 완벽한 것도 아니고, 프레임워크의 일부라서 가져가신다고 한들 바로 동작하지는 않는다. 말하고자 하는 것은 소스를 공개하고 무료로 가져다 쓰시라는 것이 아니다. 남들을 돕기에 앞서 자기 자신을 도와야 한다는 것이다. 하늘을 스스로 돕는 자를 돕는다고 하지 않았나?
이런 프로그램들을 만들어 내기 위해 어려운 점도 많았다. 개발자 생활 10년 넘게 하면서 프로젝트 지연시키는 주범이라고 욕도 많이 먹었다. 심지어 나를 잘라야 한다고 사장에게 말하는 상사도 있었다. 하지만 무릎을 꿇으라는 크세르크세스 왕 앞에서 끝까지 저항했던 레오니다스 처럼 저항해 왔다. 연구하지 않는 개발자에게 미래는 없다. 미래를 위해서 잘리는 한이 있더라도 연구하고 개발할 것이다. 그런데, 점점 나를 잘라야 한다는 사람은 줄어들고 오히려 찾는 사람이 늘어가더라.
첨부한 소스를 포함한 프레임워크는 5년 넘게 만들어 온 것이고, 앞으로 하나씩 공개해볼 생각이다.
package com.c9.web.sql.scroller;
import com.c9.web.sql.wrapper.ORList;
import com.c9.web.sql.wrapper.ORWrapper;
import com.c9.web.sql.wrapper.FieldMapping;
import com.c9.web.sql.wrapper.ORWrapperHolder;
import com.c9.common.exception.C9Exception;
import com.c9.common.util.StringFormat;
import java.sql.SQLException;
import java.util.List;
import java.util.Iterator;
/**
* 오라클용 ListScroller
* 2개 이상의 테이블을 조인할 경우에는 사용할 수 없음.
*
* @author sunnykwak
* @version 1.0
* @since 2007. 3. 21 오후 4:35:05
*/
public abstract class OraListScroller extends ListScroller
{
/**
* OR Wrapper
*/
private ORWrapper orWrapper;
/**
* 테이블 속성을 파악하기 위해 ORWrapper를 설정한다.
* @param orWrapper 쿼리에 포함된 테이블에 대한 접근 정보를 가진 Wrapper
*/
public void setORWrapper( ORWrapper orWrapper )
{
this.orWrapper = orWrapper;
}
/**
* 테이블 속성을 파악하기 위해 ORWrapper를 소유한 인터페이스를 설정한다.
* @param holder 쿼리에 포함된 테이블 접근 정보를 가진 인터페이스
*/
public void setORWrapHolder( ORWrapperHolder holder )
{
orWrapper = holder.getORWrapper();
}
/**
* @param strQuery
* @param params
*
* @return
*
* @throws java.sql.SQLException
* @throws com.c9.common.exception.C9Exception
*
*/
public ORList selectList( String strQuery, String[] params ) throws SQLException, C9Exception
{
if( orWrapper == null )
throw new C9Exception( "Did not set ORWrapper. call setORWrapper() or setORWrapHolder() plz.");
selectCount( strQuery, params );
return select( pagedQuery( strQuery ), params );
}
/**
* 가변적 조건 검색
* 사용예)
* String []params = {};
* m_srchCondList.add( new SearchCondition( BudgetRegister.BUDGET_DETAIL_CODE_TEXT,
* BudgetRegister.BUDGET_DETAIL_CODE_TEXT, "like" ) );
* selectList( "SELECT * FROM budget_list WHERE $01 #02", params, m_srchCondList );
*
* 조건 앞에 AND를 포함시킬 경우에는 #nn 을 사용,
* 조건 앞에 AND를 포함시키지 않고자 할 경우에는 $nn을 사용
* 조건 앞에 OR를 포함시킬 경우에는 @nn을 사용
*
* @param strQuery
* @param params
*
* @return
*
* @throws SQLException
* @throwsC9Exceptionn
*/
public ORList selectList( String strQuery, String[] params, List srchCondList ) throws SQLException, C9Exception
{
String[] paramsAdded;
if( params != null )
{
paramsAdded = new String[params.length];
System.arraycopy( params, 0, paramsAdded, 0, params.length );
}
else
paramsAdded = new String[0];
boolean isFirstCond = true;
for( int i = 0; i < srchCondList.size(); i++ )
{
StringBuffer strbufWhere = new StringBuffer();
SearchCondition srchCond = ( SearchCondition ) srchCondList.get( i );
String strHolderSeq = StringFormat.formatNumber( i + 1, 2 );
int nLogicOperator = OPERATOR_NOT_FOUND;
// AND 조건 여부 검사
String strHolder = "#" + strHolderSeq;
if( strQuery.indexOf( strHolder ) > -1 )
nLogicOperator = AND_OPERATOR;
// 찾지 못했다면, OR 조건 검사
if( nLogicOperator == OPERATOR_NOT_FOUND )
{
strHolder = "@" + strHolderSeq;
if( strQuery.indexOf( strHolder ) > -1 )
nLogicOperator = OR_OPERATOR;
}
// 조건 없음 검사
if( nLogicOperator == OPERATOR_NOT_FOUND )
{
strHolder = "$" + strHolderSeq;
if( strQuery.indexOf( strHolder ) > -1 )
nLogicOperator = NO_OPERATOR;
}
if( nLogicOperator == OPERATOR_NOT_FOUND )
{
StringBuffer sb = new StringBuffer( 50 );
sb.append( "Cannot find search condition in query : " )
.append( "#" ).append( strHolderSeq ).append( " or " )
.append( "@" ).append( strHolderSeq ).append( " or " )
.append( "$" ).append( strHolderSeq ).append( "." );
throw new C9Exception( sb.toString() );
}
String strValue = getParamValue( srchCond.getParamName() );
if( strValue != null && strValue.length() > 0 )
{
if( isFirstCond && strQuery.indexOf( "WHERE" ) == -1 )
{
isFirstCond = false;
strbufWhere.append( " WHERE " );
}
// "AND owner_id = ?"
else if( nLogicOperator == AND_OPERATOR )
strbufWhere.append( " AND " );
// "OR owner_id = ?"
else if( nLogicOperator == OR_OPERATOR )
strbufWhere.append( " OR " );
strbufWhere.append( srchCond.getFieldName() )
.append( " " )
.append( srchCond.getOperator() )
.append( " ?" );
if( srchCond.getOperator().equals( "like" ) )
strValue = new StringBuffer( "%" ).append( strValue ).append( "%" ).toString();
String[] paramsTemp = new String[paramsAdded.length + 1];
System.arraycopy( paramsAdded, 0, paramsTemp, 0, paramsAdded.length );
paramsTemp[paramsTemp.length - 1] = strValue;
paramsAdded = paramsTemp;
}
strQuery = StringFormat.replaceInString( strQuery, strHolder, strbufWhere.toString() );
}
return selectList( strQuery, paramsAdded );
}
static final String SELECT_WORD = "SELECT ";
static final String FROM_WORD = "FROM ";
static final String WHERE_WORD = "WHERE ";
static final String ORDER_WORD = "ORDER BY ";
/**
* 특정 페이지의 목록만을 선택하는 쿼리를 작성한다.
*
* @param strQuery
*
* @return
*/
private String pagedQuery( String strQuery )
{
// 쿼리를 세부 절(paragraph)로 분리한다.
String strQueryUpper = strQuery.toUpperCase();
int nSelectIdx = strQueryUpper.indexOf( SELECT_WORD );
int nFromIdx = strQueryUpper.indexOf( FROM_WORD );
int nWhereIdx = strQueryUpper.indexOf( WHERE_WORD );
int nOrderIdx = strQueryUpper.indexOf( ORDER_WORD );
String strSelectParagraph = strQuery.substring( nSelectIdx, nFromIdx );
// 모든 컬럼을 선택 했는가? 그렇다면, 각 컬럼 이름을 풀어쓴다.
// SELECT * -> SELECT a, b, c
if( strSelectParagraph.indexOf("*") > -1 )
{
StringBuffer sb = new StringBuffer();
sb.append( SELECT_WORD );
List fldList = orWrapper.getORMapping().getFieldList();
Iterator iter = fldList.iterator();
while( iter.hasNext() )
{
FieldMapping field = (FieldMapping)iter.next();
sb.append( field.getFieldName() );
if( iter.hasNext() )
sb.append( ", " );
else
sb.append( " " );
}
}
String strFromParagraph;
if( nWhereIdx == -1 )
{
if( nOrderIdx == -1 )
strFromParagraph = strQuery.substring( nFromIdx );
else
strFromParagraph = strQuery.substring( nFromIdx, nOrderIdx );
}
else
strFromParagraph = strQuery.substring( nFromIdx, nWhereIdx );
String strWhereParagraph = null;
if( nWhereIdx > -1 )
{
if( nOrderIdx > -1 )
strWhereParagraph = strQuery.substring( nWhereIdx, nOrderIdx );
else
strWhereParagraph = strQuery.substring( nWhereIdx );
}
// 페이지 쿼리 작성
String strPagedQuery;
// primary key가 둘 이상인 경우...
if( orWrapper.getPrimaryKeySize() > 1 )
{
if( nOrderIdx > -1 )
{
strPagedQuery =
makeQuery4MultiOrder( strFromParagraph, strSelectParagraph, strQuery );
}
else
{
strPagedQuery =
makeQuery4Multi( strFromParagraph, strSelectParagraph, nWhereIdx, strWhereParagraph );
}
}
// primary key 하나인 경우...
else
{
if( nOrderIdx > -1 )
{
strPagedQuery =
makeQuery4SingleOrder( strSelectParagraph, strQuery );
}
else
{
strPagedQuery =
makeQuery4Single( strFromParagraph, strSelectParagraph, nWhereIdx, strWhereParagraph );
}
}
return strPagedQuery;
}
/**
* [ pk가 1개이고 order by가 없는 경우 ]
* SELECT A.*
* FROM ( SELECT ROWNUM num, doc_seq, doc_title FROM T_board WHERE doc_title LIKE '%5%'
* AND doc_seq > 0 AND ROWNUM <= 10 ) A
* WHERE num > 10 * 300
*
* @param strFromParagraph
* @param strSelectParagraph
* @param nWhereIdx
* @param strWhereParagraph
* @return
*/
private String makeQuery4Single( String strFromParagraph, String strSelectParagraph, int nWhereIdx, String strWhereParagraph )
{
StringBuffer strPagedQuery = new StringBuffer();
strPagedQuery
.append( "SELECT a.* FROM (" )
.append( strSelectParagraph )
.append( ", rownum as num " )
.append( strFromParagraph );
if( nWhereIdx > -1 )
{
strPagedQuery
.append( strWhereParagraph )
.append( " AND rownum <= " )
.append( getLastRowNo() );
}
else
{
strPagedQuery
.append( " WHERE rownum <= " )
.append( getLastRowNo() );
}
strPagedQuery
.append( ") a WHERE num > " )
.append( getFirstRowNo() );
return strPagedQuery.toString();
}
/**
* [ pk가 1개이고 order by가 있는 경우 ]
* SELECT A.*
* FROM ( SELECT ROWNUM num, doc_seq, doc_title
* FROM ( SELECT ROWNUM num, doc_seq, doc_title FROM T_board WHERE doc_title LIKE '%5%' AND doc_seq > 0
* ORDER BY doc_title ASC )
* WHERE ROWNUM <= 1 ) A WHERE num > 10
*
*/
private String makeQuery4SingleOrder( String strSelectParagraph, String strQuery )
{
StringBuffer strPagedQuery = new StringBuffer();
strPagedQuery
.append( "SELECT a.* FROM ( " )
.append( strSelectParagraph )
.append( ", rownum as num FROM ( " )
.append( strQuery )
.append( ") WHERE rownum <= " )
.append( getLastRowNo() )
.append( ") a WHERE num > " )
.append( getFirstRowNo() );
return strPagedQuery.toString();
}
/**
* prmary key가 둘 이상이고, order by 절이 있는 쿼리
*
* [ pk가 2개이고 order by가 있는 경우 ]
* SELECT a.*, rownum FROM T_CODE a,
* ( SELECT code_grp, code_id, code_name
* FROM ( SELECT ROWNUM num, code_grp, code_id, code_name
* FROM ( SELECT ROWNUM num, code_grp, code_id, code_name FROM T_CODE WHERE code_grp LIKE 'B04'
* ORDER BY code_name DESC )
* WHERE ROWNUM <= 10 * 1 ) A
* WHERE num > 10 * 0 ) b
* WHERE a.code_grp = b.code_grp AND a.code_id = b.code_id
*
* @param strFromParagraph
* @param strSelectParagraph
* @return
*/
private String makeQuery4MultiOrder( String strFromParagraph, String strSelectParagraph, String strQuery )
{
StringBuffer strPagedQuery = new StringBuffer();
strPagedQuery
.append( "SELECT a.* " )
.append( strFromParagraph )
.append( " a, ( " )
.append( strSelectParagraph )
.append( "FROM ( " )
.append( strSelectParagraph )
.append( ", rownum as num FROM ( " )
.append( strQuery )
.append( ") WHERE rownum <= " )
.append( getLastRowNo() )
.append( ") a WHERE num > " )
.append( getFirstRowNo() )
.append( ") b WHERE " );
List pkList = orWrapper.getORMapping().getPKFieldList();
Iterator pkIter = pkList.iterator();
while( pkIter.hasNext() )
{
FieldMapping field = ( FieldMapping ) pkIter.next();
strPagedQuery
.append( "a." ).append( field.getFieldName() )
.append( " = " ).append( "b." ).append( field.getFieldName() );
if( pkIter.hasNext() )
strPagedQuery.append( " AND " );
}
return strPagedQuery.toString();
}
/**
* primary key가 둘 이상이고, order by 절이 없는 쿼리
*
* [ pk가 2개이고 order by가 없는 경우 ]
* SELECT a.* FROM T_CODE a,
* ( SELECT code_grp, code_id
* FROM ( SELECT ROWNUM num, code_grp, code_id FROM T_CODE WHERE ROWNUM <= 10
* AND code_grp = 'B04' ) A
* WHERE num > 1 ) b
* WHERE a.code_grp = b.code_grp AND a.code_id = b.code_id
*
* @param strFromParagraph
* @param strSelectParagraph
* @param nWhereIdx
* @param strWhereParagraph
*
* @return
*/
private String makeQuery4Multi(
String strFromParagraph, String strSelectParagraph,
int nWhereIdx, String strWhereParagraph )
{
StringBuffer strPagedQuery = new StringBuffer();
strPagedQuery
.append( "SELECT a.* " )
.append( strFromParagraph )
.append( " a, ( " )
.append( strSelectParagraph )
.append( "FROM (" )
.append( strSelectParagraph )
.append( ", rownum as num " )
.append( strFromParagraph );
if( nWhereIdx > -1 )
{
strPagedQuery
.append( strWhereParagraph )
.append( " AND rownum <= " )
.append( getLastRowNo() );
}
else
{
strPagedQuery
.append( " WHERE rownum <= " )
.append( getLastRowNo() );
}
strPagedQuery
.append( ") a WHERE num > " )
.append( getFirstRowNo() )
.append( ") b WHERE " );
List pkList = orWrapper.getORMapping().getPKFieldList();
Iterator pkIter = pkList.iterator();
while( pkIter.hasNext() )
{
FieldMapping field = ( FieldMapping ) pkIter.next();
strPagedQuery
.append( "a." ).append( field.getFieldName() )
.append( " = " ).append( "b." ).append( field.getFieldName() );
if( pkIter.hasNext() )
strPagedQuery.append( " AND " );
}
return strPagedQuery.toString();
}
}
# by | 2007/03/26 11:28 | Development | 트랙백(3) | 핑백(1) | 덧글(3)





제목 : 나를 위한 코딩을 해 보자.
# Help yourself! programmer... 포스트 내용이 구구절절 공감이 된다.프로그래머로 사회생활을 시작한 지 1년 반 정도 되었는데, 종종 코드들을 돌아보면 엄청난 양의 중복이 존재함을 깨닫는다.하지만 그 놈의 '귀차니즘'이 뭔지, 퇴근이 뭔지... 최대한 시간을 줄이기 위해 소스를 통째로 찾아서 갖다 붙이는 작업을 하게 된다.그 때 당시에는 라이브러리로 만드는 것보다 시간이 덜 걸린다. 하지만 그걸 여러 번 반복하게 되니 이 ......more
제목 : 자신을 위한 개발을 하자.
오랜만에 써니님 블로그 들어갔다가... 휴.. 못읽은 피드가 서른개.. 정말 오래도 안들어갔네요. 글 하나 하나에 열정과 자신이 하는 일에 대한 직관과 존경할만한 시각을 보여주는 글들.. 또하나.. 생각해 볼만한글. Help yourself! programmer......more
제목 : 나를 위한 코딩을 해 보자.
# Help yourself! programmer...포스트 내용이 구구절절 공감이 된다.프로그래머로 사회생활을 시작한 지 1년 반 정도 되었는데, 종종 코드들을 돌아보면 엄청난 양의 중복이 존재함을 깨닫는다.하지만 그 놈의 '귀차니즘'이 뭔지, 퇴근이 뭔지... 최대한 시간을 줄이기 위해 소스를 통째로 찾아서 갖다 붙이는 작업을 하게 된다.그 때 당시에는 라이브러리로 만드는 것보다 시간이 덜 걸린다. 하지만 그걸 여러 번 반복하게 되니 이 작......more
... 17;s CodeList 검색 Home Mar 26 나를 위한 코딩을 해 보자. Tag: Dev.Think — Heart @ 11:44 오후 # Help yourself! programmer… 포스트 내용이 구구절절 공감이 된다. 프로그래머로 사회생활을 시작한 지 1년 반 정도 되었는데, 종종 코드들을 돌아보면 엄청난 양의 중복이 존재 ... more
같은 일을 하루이틀 하게 되는 것도 아니기 때문에 당장 늦는 한이 있더라도 준비를 해 두면 엄청난 시간을 세이브할 수 있겠죠.
5년동안 모으신 프레임워크라면 엄청 방대할 것 같습니다. 대단하십니다. ^^
밤늦게까지 일하고 자기전에 책보고 주말에 책보고
결국 40대가 되었지만 천지개벽의 실력도 없고 애도 없고 마누라도 없도 벌어논 돈도 없고.
괜히 이길로 왔나 하는 생각을 한적은 인생에 딱 한번있습니다.
나가야죠.. 그나마 버텨온 것이니 굷지 않을 정도의 기술은 지켰을 겁니다. 근근히 꽤 많은 책을 읽어왔더군요.