저번주에 목요일, 금요일 이렇게 이틀 동안 생활코딩 오프라인 수업 작심삼일을 다녀왔습니다. 이 수업은 egoing(@egoing)님께서 진행하시는 무료 강좌로 이 강좌에서는 HTML, CSS, Javascript, AWS(Amazon Web Services)를 사용한 Apache 웹서버, 기초적인 Unix 명령어, PHP 등 웹서비스를 구축할 수 있는 기본 기술들을 두루두루 다룹니다.


이 수업에 대한 후기를 쓰기 전에 egoing님에 대해서 조금 더 간략히 설명 드리겠습니다. egoing님은 개발자로써 프로그래밍에 대한 열정과 애착이 있으십니다. 자신이 공부하고 하는 일에 재미를 느끼시기에 자신이 즐기는 것을 여러 사람들에게 전파하는 IT 기술의 저도사라고나 할까요? 어떻게 보면 많은 사람들이 쉽게 접할 수 없는 프로그래밍이라는 것을 쉽게 접하고 재미를 붙이고 생활 속에서 활용할 수 있도록 생활코딩이라는 이름으로 Opentutorials라는 사이트를 운영하고 계십니다. 페이스북에서도 생활코딩이라는 그룹을 개설해서 여러 개발자들이 서로 어려움을 공유하고 문제 해결 방안을 찾을 수 있도록 공간을 마련해 주고 계시기도 하고 생활코딩 페이지를 통해서 개발자에게 유용한 정보와 Opentutorials 사이트의 업데이트 상황을 공유해 주고 계십니다. 은은하고 감미로운 목소리와 훈훈한 인상을 갖고 계신 훈남이시기도 합니다.



그러면 다시 무료 강좌 후기로 돌아와서...


수강 신청은 OnOffMix에서 이루어졌습니다. 그리고 무료 강좌가 진행된 곳은 불광역 근처에 있는 청년 일자리 허브입니다. 현재 한달에 한두번 강좌를 하고 계신것 같던데(제가 갔던 강좌는 8번째라고 강좌라고 합니다), 장소는 매번 섭외 가능한 곳에서 하고 계십니다. 원래는 첫날 약 2시간 정도의 오리엔테이션이 있고, 이틀간 오전 10시 부터 8시 까지 수업을 하셔서 무료 강좌 이름이 작심삼일인듯 하지만, 저는 오리엔테이션은 가지는 않았습니다.


오리엔테이션에서 이틀간 다룰 전반적인 내용을 한번 짚고 넘어가고, 이틀간 그 내용들을 자세히 다룹니다. 사실 이틀간 마스터하기에는 아주 많은 양의 주제를 다룹니다. 하지만 이 강좌의 핵심은 수강생들에게 각 기술의 아주 기초적인 것을 원리적으로 이해시키고 재미를 붙일 수 있도록 하는 것이라고 생각합니다. 그래서 egoing님은 수업을 아무나 부담없이 들을 수 있을 정도에 맞춰서 진행하십니다. 각 기술들의 탄생 배경도 재미있게 설명해 주시고 사용법은 적절한 비유를 통해 그 원리를 쉽게 설명해 주십니다. 하지만 전산학을 전공하고 개발자로 7년이 넘게 살아온 저에게는 아주 쉬운 내용이라도, 프로그래밍을 처음 해보는 사람들에게는 아무리 쉽게 설명을 한다고 해도 당연히 어려운 내용일 수 있습니다. 그래서 egoing님도 사람들이 강좌를 듣고 한번에 모든것을 이해할 것을 기대하지 않으십니다. 그래서 이해가 잘 안가시는 분들은 강좌를 몇번이고 들으실 것을 권하십니다. 그러니 정말 부담 없이 들으셔도 됩니다.


Egoing님은 정말 본인이 말씀하시는 것 처럼 자신이 공부한 내용을 사람들에게 전달하고 공유하는 것을 즐거워 하시며 정말 사람들이 생활 속에 프로그래밍을 즐길 수 있기를 바라시는 것 같습니다. 그래서 늘 한결같이 웃음을 잃지 않으시고 열정적으로 최선을 다해 수업을 진행하시는데 그 모습이 참 보기 좋았습니다.



제가 이 강좌를 듣게 된 이유는 선천적으로 스스로 문서를 보면서 공부하는 것 보다는 다른 사람을 통해 배우거나 학습의 방향을 제시 받아 학습에 대한 시행 착오를 최대한 줄이는 것을 좋아하기 때문입니다. 결과적으로는 제가 생각 했던 것 보다는 간단하고 쉬운 내용들 이었지만 그래도 의미있는 강좌였습니다. Egoing님께서 각 기술들에 대한 중요한 특성들을 꼭꼭 찝어주시면서 설명해 주셔서 그동안 궁금했지만 일일히 찾아보지 않은 내용들을 머리 속에 정리할 수 있었고 직접 실습까지 해볼 수 있어서 아주 좋았습니다. 게다가 제 옆에 앉아계신 할머니께도 실습에 잘 따라올 수 있도록 도움을 드릴 수 있어서 의미 있는 시간이었습니다. 덕분에 저는 할머니께서 고맙다고 하시며 주신 간식도 얻어 먹고 얼마 드시지 않은 물이 타 먹는 비타민제도 받았습니다!!! 제가 조교를 했던 것은 아니지만 egoing님께서 이런 강좌를 개설 하시면 조교도 모집하니 도움을 주실 수 있는 분들은 좋은 일에 동참 하시는 것도 권해 드립니다. 그리고 egoing님의 삶에 대한 자세를 본받는 의미에서 프로그래밍에 문외한 분들 뿐만 아니라 프로그래밍을 할 줄 아는 개발자들도 강좌를 듣거나 참여하시면 분명 제가 느낀 것 처럼 의미있는 시간을 보낼 수 있으리라 생각됩니다.


강좌에서 설명된 모든 내용은 Opentutorials 사이트의 웹서비스 만들기 강좌에 있어서 오프라인 강좌를 듣지 않으셔도 혼자 학습이 가능하기도 하니 평소에 웹서비스 구축이나 프로그래밍에 관심 있었던 분은 직접 공부를 시도해 보시는 것도 좋을 것 같습니다. 혹시 추후에 있을 강좌에 관심이 있으신 분들은 http://codingeverybody-notify.appspot.com 에 가셔서 정보를 입력하시면 앞으로 열릴 강좌에 대한 정보를 받아보실 수 있다고 합니다.


그리고 지방에 계셔서 무료 강좌를 듣기 어렵고 온라인으로 학습하는 것이 불편하신 분들은 egoing님께서 쓰신 생활코딩이라는 도 있으니(얼마나 많은 부분을 다루고 있는지는 잘 모르겠습니다만) 책도 살펴보시기 바랍니다. 


저는 이제 웹 서비스를 개발할 것입니다... 음하하하!!!


Posted by Dansoonie

This post is an update or an extension to the post 2012/07/10 - [OpenGL] When textures do not show properly without any glerror in Android posted yesterday.


Previously, I found out that for some cases if the image resource is packaged in res/drawable it will cause some problems to create textures. In spite of this discovery, for some images the texture still did not render at all on some devices(namely the Galaxy Player). The research went on.


The research continued on the simple example I have created which I have mentioned in my previous note. The image that wasn't working as a texture was a 1024x512 size image of the earth's surface. Once again, I checked the bitmap's internal format, type, and config. However, I could not see any difference from the case where I was using a different image that works.


The blame now went to the dimension of the image. The image that worked as a texture on the Galaxy Player had a size of 512x512. Could it be possible that it works because it has a dimension of a square? So I scaled the image that isn't working as a texture down to 512x512 and it worked(I was wrong. It still didn't work. I got my situation messed up. updated 2012/7/16). But that didn't make much sense since I wasn't able to find any requirements about textures to have a square dimension. Moreover, OpenGL ES 2.0 specifies that the dimensions of the image does not have to be in the power of two.


Stripping out the power of two requirement and putting in a requirement that a image used for a texture must be a square sounds ridiculous. Also, then why is it working on other devices?


More research went on and I finally found out that the malfunctioning has something to do with setting the GL_TEXTURE_MIN_FILTER. It turns out that on Galaxy Player, the texture does not properly render if the GL_TEXTURE_MIN_FILTER is set to a mipmap filter(either linear or nearest) when the dimension of the image is not a square.


I'm pretty obvious that this is an OpenGL implementation bug on the device, but I had to check. So, I posted a question about this on stackoverflow. If you have any useful information to tell me, please leave a comment here or on stackoverflow. 


Thanks.


Posted by Dansoonie

While I was exploring the features of Rajawali creating some sample code, I have encountered into a strange situation where textures show up on one device and another didn't. Nothing complicated in the sample code going on. Just rendering a sphere object with a texture of the earth's surface. The image used for the texture was saved in res/drawable and the bitmap was created at runtime using BitmapFactory.decodeResource(). Now the most strange thing was that glError was not flagged at any point(at least I think I checked thoroughly).


FYI, the working devices was Galaxy Nexus, and the non-working device was Galaxy Player GB70


To attack this issue, I've created a simple project which renders a flat square with the image that I'm having trouble to use it as the sphere's texture in Rajawali.


The first thing I noticed was that the only difference between the working device and the non-working device was that the image was decoded into a ARGB8888 bitmap config on the working device and RGB565 bitmap config on the non working device. However, if I force the image to be decoded into RGB565 bitmap config on the working device, it still worked.


I've tried changing parameters for glTexImage2D and converting the image file to use another bitmap config(via Bitmap.copy()) and all sorts of things without much luck. So I did more Googling to do more research.


There is probably almost any information you want on the Internet. And I have found the reason why the texture was not showing properly. A piece of meaningful information here. The person who was having a similar problem that I was having posted a question on stackoverflow. Luckily he found the solution on his own and was nice enough to share the information he learned. Special thanks to him/her.



In Android, image resources could be packaged in path res/drawable. Since there exists many Android devices with different screen resolution the image resources are designed to be packaged in various size in drawable-ldpi, drawable-mdpi, drawable-hdpi under res/. And for the sake of convenience you can package resources under simply res/drawable and then the system would automatically handle the resizing. Here's a quote from the Android developer's page regarding supporting multiple screen resolutions.


The "default" resources are those that are not tagged with a configuration qualifier. For example, the resources in drawable/ are the default drawable resources. The system assumes that default resources are designed for the baseline screen size and density, which is a normal screen size and a medium density. As such, the system scales default density resources up for high-density screens and down for low-density screens, as appropriate.


http://developer.android.com/guide/practices/screens_support.html


This is something that I wasn't completely unaware of, but it bit me. The problem might have been when the resource was decoded into a bitmap using the BitmapFactory, the size of the image changes into probably something not in the dimensions of power of two. The OpenGL ES 2.0 specification indicates that it supports non-power of two textures. See the OpenGL ES 2.0 common profile specification  p. 17 on section 3.8 Texturing. However, for some reason I'm suspicious about every OpenGL ES 2.0 implementation strictly following this specification. 


What I didn't really know was that drawable resources under res/drawable-nodpi is dpi independent resource which the system does not perform any resizing when decoded into bitmap. Honestly, I thought drawable resources under res/drawable would be decoded in a dpi independent manner too.

 

I'll have to see if the problem was caused by resizing the resource into a non-power of two dimension. If this were true I'm also surprised that glError was not flagged at all. Anyway, but for now, if you are having trouble loading textures in Android check if your drawable resource that you are using as your texture is packaged under res/drawable.


Problem partly solved, but still the texture is not showing up for some cases on the sphere when using Rajawali. so the research goes on...

If you have any knowledge about this problem or if I have written something incorrect here please leave a comment and let me know.


Posted by Dansoonie
My previous post was about unexpected behavior of Buffer's in Honeycomb which seemed like a bug. The bug I found was about float values in cloned read-only FloatBuffer being interpreted differently from the contents in the original FloatBuffer(2011/04/07 - Unexpected behavior of Buffers in Honeycomb(Android 3.0)). I submitted a bug report to Google and it was confirmed as a bug and now fixed internally for future IceCreamSandwich release.

Please refer to http://code.google.com/p/android/issues/detail?id=15994 for more information. My first successful bug report on a commercial product. Yay~

I thank all my colleagues at work who gave me this opportunity and helped me tackle this issue. 
Posted by Dansoonie
I am an android software developer. At work I am developing a 3D GUI framework called Tiffany. OpenGL is used at the core of Tiffany. Like any other OpenGL application, our product uses float type values to define the location(position) of the vertices that consist 3D objects. Therefore, FloatBuffers are used frequently.

Tiffany has been working great until now with all Android versions. However, I recently got a report that Tiffany behaves a bit weird on Honeycomb, that is Android 3.0 which is an Android version for tablet devices. I was able to track down the cause of the problem and found out what was going on with the help of Mr. Shin, whom we think of as a genius. The unexpected behavior was originating from ByteBuffer/FloatBuffer.

In a portion of our code there was something going on like the following.
ByteBuffer byteBuffer =
ByteBuffer.allocateDirect(n*4).order(ByteOrder.nativeOrder())
FloatBuffer buffer = byteBuffer.asFloatBuffer();

//...

//We put some float values in the buffer

//...

FloatBuffer copiedBuffer = buffer.asReadOnlyBuffer();

//...

//use the values in copiedBuffer

//... 


As I was debugging the code line by line, I found out that the values retrieved from copiedBuffer were interpreted incorrectly in Honeycomb. This was a very unexpected behavior as this was working perfectly on previous Android versions.

Here is what was happening. Buffers in Android has a property called Order. This property indicates whether the buffer uses big endian or little endian. In other words it defines how the bytes in the buffer will be interpreted. It turns out that this property is altered in the copied version of the buffer when using asReadOnlyBuffer(). And what is more interesting is that this problematic phenomenon is only spotted when the ByteOrder of the ByteBuffer is specified using the method order(ByteOrder byteOrder) from ByteBuffer.

Here is a simple example which illustrates this problem.

ByteBuffer byteBuffer0 =

ByteBuffer.allocateDirect(4).order(ByteOrder.nativeOrder());

FloatBuffer buffer0 = byteBuffer0.asFloatBuffer();

buffer0.put(0.1f);

FloatBuffer copiedBuffer0 = buffer0.asReadOnlyBuffer();

Log.d(TAG, "buffer0 endian: " + buffer0.order());

Log.d(TAG, "buffer0[0]: "  + buffer0.get(0));

Log.d(TAG, "copiedBuffer0 endian: " + copiedBuffer0.order());

Log.d(TAG, "copiedBuffer0[0]: " + copiedBuffer0.get(0));


ByteBuffer byteBuffer1 =

ByteBuffer.allocate(4).order(ByteOrder.nativeOrder());

FloatBuffer buffer1 = byteBuffer1.asFloatBuffer();

buffer1.put(0.1f);

FloatBuffer copiedBuffer1 = buffer1.asReadOnlyBuffer();

Log.d(TAG, "buffer1 endian: " + buffer1.order());

Log.d(TAG, "buffer1[0]: "  + buffer1.get(0));

Log.d(TAG, "copiedBuffer1 endian: " + copiedBuffer1.order());

Log.d(TAG, "copiedBuffer1[0]: " + copiedBuffer1.get(0));


FloatBuffer buffer2 = ByteBuffer.allocateDirect(4).asFloatBuffer();

buffer2.put(0.1f);

FloatBuffer copiedBuffer2 = buffer2.asReadOnlyBuffer();

Log.d(TAG, "buffer2 endian: " + buffer2.order());

Log.d(TAG, "buffer2[0]: "  + buffer2.get(0));

Log.d(TAG, "copiedBuffer2 endian: " + copiedBuffer2.order());

Log.d(TAG, "copiedBuffer2[0]: " + copiedBuffer2.get(0));


FloatBuffer buffer3 = ByteBuffer.allocate(4).asFloatBuffer();

buffer3.put(0.1f);

FloatBuffer copiedBuffer3 = buffer3.asReadOnlyBuffer();

Log.d(TAG, "buffer3 endian: " + buffer3.order());

Log.d(TAG, "buffer3[0]: "  + buffer3.get(0));

Log.d(TAG, "copiedBuffer3 endian: " + copiedBuffer3.order());

Log.d(TAG, "copiedBuffer3[0]: " + copiedBuffer3.get(0));


The result in Honeycomb(Android 3.0) AVD would look like the following.

buffer0 endian: LITTLE_ENDIAN

buffer0[0]: 0.1

copiedBuffer0 endian: BIG_ENDIAN

copiedBuffer0[0]: -4.2949213E8


buffer1 endian: LITTLE_ENDIAN

buffer1[0]: 0.1

copiedBuffer1 endian: BIG_ENDIAN

copiedBuffer1[0]: -4.2949213E8


buffer2 endian: BIG_ENDIAN

buffer2[0]: 0.1

copiedBuffer2 endian: BIG_ENDIAN

copiedBuffer2[0]: 0.1


buffer3 endian: BIG_ENDIAN

buffer3[0]: 0.1

copiedBuffer3 endian: BIG_ENDIAN

copiedBuffer3[0]: 0.1


The result in Android 2.X AVD would look like the following.

buffer0 endian: LITTLE_ENDIAN

buffer0[0]: 0.1

copiedBuffer0 endian: LITTLE_ENDIAN

copiedBuffer0[0]: 0.1


buffer1 endian: LITTLE_ENDIAN

buffer1[0]: 0.1

copiedBuffer1 endian: LITTLE_ENDIAN

copiedBuffer1[0]: 0.1


buffer2 endian: BIG_ENDIAN

buffer2[0]: 0.1

copiedBuffer2 endian: BIG_ENDIAN

copiedBuffer2[0]: 0.1


buffer3 endian: BIG_ENDIAN

buffer3[0]: 0.1

copiedBuffer3 endian: BIG_ENDIAN

copiedBuffer3[0]: 0.1

 
So, here is my conclusion. Dalvik uses big endian and Linux which is the operating system I am using at work uses little endian. As a result, ByteBuffers are created to use big endian by default. However, when the ByteOrder is specified to be little endian, the Order property isn't properly copied to the new Buffer in Honeycomb. I suspect that this is a bug in Honeycomb because Honeycomb is the only Android version working differently and also it doesn't logically make sense to use a different endian system to interpret a copied buffer from the original buffer. Moreover, the Order property not being properly copied seems much like a mistake since you cannot set the Order property for FloatBuffers.

I must admit that specifying the ByteOrder of the buffer is an unnecessary step, still Honeycomb's behavior of handling Buffers doesn't make much sense.
Posted by Dansoonie
Are you developing an Android app that requires a long click, tap, touch or whatever? Then you should be adding an OnLongClickListener to your view. And if you are also going to carry out complex tasks with touch events, you might be adding an OnTouchListener to your view too! Same thing what I was doing at work. unfortunately, I was having a weird problem. Obviously, I was expecting the OnLongClickListener to capture the long click event only when I press my finger on the view for a certain amount of time. However, OnLongClickListener's onLongClick() was being called every time there was a touch event along with OnTouchListener's onTouch().

I was googling to find out why, eagerly seeking for a solution. Surprisingly, it seemed like there weren't many people having the same problem (or maybe I wasn't using the right keyword). Eventually I found out what was causing the problem, I have decided to share the experience. Not that it is something very unusual, but for those who are looking for a quick answer.

The problem was in my implementation of onTouch() in OnTouchListener. Traditionally (way back from Windows programming), when you are dealing with events, you would return true when the event handler handles the event, or more precisely when the event handler consumes it and return false when the event is meaningless to the event handler so that it passes on the event to the next available event handler according to the hierarchy of the user interface. This convention I was following was causing the problem.

In order for Android to detect long clicks, Android must keep track of the time after a touch down event has occurred. After reading the Android developer's document and third QnA threads from party forums I have reached to a conclusion that this time checking is taking place at a somewhat unexpected place. Honestly, I don't have much experience in Windows programming, so I'm not really sure if it's the same case for Windows programming, but in order to capture long click events correctly the onTouch event must return false on ACTION.DOWN events. 

To explain in detail, let's take a look at some code.

public class LongClickTest extends Activity {

public class TestView extends View {


final static private String TAG = "LongClickTest";

public TestView(Context context) {

super(context);

this.setOnTouchListener(mTouchListener);

this.setOnLongClickListener(mLongClickListener);

}

private OnLongClickListener mLongClickListener = 

new OnLongClickListener() {


@Override

public boolean onLongClick(View view) {

Log.d(TAG, "LongClick !!!");

return true;

}

};

private OnTouchListener mTouchListener = 

new OnTouchListener() {


@Override

public boolean onTouch(View view, MotionEvent event) {

boolean consumed = false;

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN:

Log.d(TAG, "ACTION_DOWN");

consumed = true;

break;

case MotionEvent.ACTION_MOVE:

Log.d(TAG, "ACTION_MOVE");

consumed = true;

break;

case MotionEvent.ACTION_UP:

Log.d(TAG, "ACTION_UP");

consumed = true;

break;

}

return consumed;

}

};

}

    /** Called when the activity is first created. */

    @Override

    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        LayoutParams layoutParams = 

new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);

        setContentView(new TestView(this), layoutParams);

        

    }

}


Here I have made a inner class inside of an Activity class for convenience. The inner class called TestView inherits View and an instance of that TestView is set as the content view of the Activity class. Inside the inner class, I have implemented the OnTouchListener and OnLongClickListener to log about the occurring event and set each listeners to the corresponding listeners. If you execute the program, what you'll see is a blank black empty screen. But it's sufficient for testing.

First, let's try executing the above code as it is. In logcat, all you will see are logs of ACTION_DOWN, ACTION_MOVE, and ACTION_UP events. onLongClick() in OnLongClickListener is never triggered. As I have mentioned earlier, this is because the OnTouchListener has consumed all the events and Android has no chance of keeping track of how long the time elapsed before ACTION_UP after the ACTION_DOWN.

So, let's make some change in the OnTouchListener's onTouch() like the following.

@Override

public boolean onTouch(View view, MotionEvent event) {

boolean consumed = false;

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN:

Log.d(TAG, "ACTION_DOWN");

consumed = false;

break;

case MotionEvent.ACTION_MOVE:

Log.d(TAG, "ACTION_MOVE");

consumed = true;

break;

case MotionEvent.ACTION_UP:

Log.d(TAG, "ACTION_UP");

consumed = true;

break;

}

return consumed;

}



 By returning false in case of ACTION_DOWN events, Android is now able to keep track of the duration of time after the ACTION_DOWN occurs. As a result, after a decent amount of time (about 1 second) after your fingertip touches the screen, you will see the log "LongClick !!!" in logcat. However, the problem is that you will find out that onLongClick() will be called every time you touch the screen leaving the log "LongClick !!!" in spite of the fact that you have lifted up your fingertip from the screen. This is the problem I was having. I have to admit that the code I was working on was poorly written, because my original intention was to return true whenever the OnTouchListener spotted a ACTION_DOWN event and handled it. Anyway, this was the flaw in my code and it was causing the problem that I am trying to illustrate here. The problem is that Android is now able to measure the amount of time passed after the ACTION_DOWN event, but it never knows when to stop measuring. This is because my OnTouchListener has consumed the ACTION_UP event and hence, Android has no clue when the ACTION_UP has occurred. Do you see what is going on???

Therefore my conclusion here is to return false no matter what. In this case, returning true for ACTION_DOWN and ACTION_UP will be enough to solve the problem. However, if we have to deal with more complex touch event sequence, it should return false for all events so that Android will be able to capture the event whatever is needed to trigger other events. Some other event's might require ACTION_MOVE to be captured outside the OnTouchListener you are implementing.

Therefore, the resulting code shall be the following.

@Override

public boolean onTouch(View view, MotionEvent event) {

switch(event.getAction()) {

case MotionEvent.ACTION_DOWN:

Log.d(TAG, "ACTION_DOWN");

break;

case MotionEvent.ACTION_MOVE:

Log.d(TAG, "ACTION_MOVE");

break;

case MotionEvent.ACTION_UP:

Log.d(TAG, "ACTION_UP");

break;

}

return false;

}


So this is a simple example demonstrating why your onLongClick() is called along with onTouch(). I haven't put much thought to this problem to determine whether this is a good design or not, but I don't like this event handling method at the moment. If it were to operate this way, why is there the need to return a boolean in the OnTouchListener in the first place...

Anyway, this is how Android is, and can't blame the dudes in Google because they probably know what they are doing...

Read http://developer.android.com/guide/topics/ui/ui-events.html for more information, especially the part where is starts with "onTouch:" and "note:"(<- be aware of the colon included. I've added that to explicitly indicate where the information is).
Posted by Dansoonie
I have learned a lesson at work about what I shouldn't do when using a for-each loop in Java. The for-each loop in Java is used as the following.

for (type var : arr) {
   // body of loop
}

// This is equivalent to the following for loop
for (int i=0; i<arr.length; i++) {
   type var = arr[i];
   // body of loop


The for-each loop structure can be also used with collections. So, it can also be used as the following.
for (type var : coll) {
   // body of loop
}

// This is equivalent to the following for loop using the iterator
for (iterator<type> iter=coll.iterator; iter.hasNext(); ) {
   type var = iter.next();
   // body of loop
}

At work I was working with collections, and had to do an iterative task on the items in the collection. After I finished my code, I tried to execute it and was surprised to see that I got a ConcurrentModificationException.

My shifu(master) at work, advised me that I might be carrying out some operations incorrectly among threads. However, I double checked my critical section points and I found myself doing everything right(probably???)... 

I was tracking down the point where the exception occurred, and finally realized I was doing something I wasn't supposed to do. And yet I was even more surprised to see I wasn't able to find any official technical reference to my mistake.

Here is what I was doing wrong. Some of you might have already figured out the point I'm trying to make from the title of this post. I think you aren't supposed to mess with the iterator of the collection that are using in the for loop.

So here is an example code that I have constructed to illustrate the problem.

public class IteratorTest {
private LinkedList<Integer> list = new LinkedList<Integer>();
public static void main(String[] argv) {
IteratorTest test = new IteratorTest();
test.initList();
test.test();
}

public void initList() {
for (int i=0; i<10; i++) {
list.add(new Integer(i));
}
}

public void test() {
for (Integer i : list) {
if (i.equals(3))
list.remove(list.get(1));
}
}
}

If you execute this short program, this will result in the following error message.
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.LinkedList$ListItr.checkForComodification(Unknown Source)
at java.util.LinkedList$ListItr.next(Unknown Source)
at IteratorTest.test(IteratorTest.java:18)
at IteratorTest.main(IteratorTest.java:9)

The remove method call on the list will mess with the iterator, and supposedly the ordering of the iterator for the for loop will be corrupted. I guess that is why Java throws a ConcurrentModificationException.

After having to get to know what my problem is, the behavior of how all the for-each loop and the iterator thing works seems so obvious. However, I just think that the for-each loop thing shouldn't be part of the Java language.

Basically, for many of the developers out in the wild, the for-each loop is just another option for the repetition control structure. But, the for-each loop is using the Iterator class for traversing all the items in a collection. While the Iterator class is not part of the language, but a class that is part of the core Java library which is created with the language (I'm not sure if I'm using the correct terminology, but I'm sure you'll get my point), I don't think Java should be supporting this feature. The syntax for the basic control structures should not be dependent to something which is not part of the language itself.

The for-each loop certainly makes the code more simpler and elegant. But I'm not sure what Sun had in mind when they were adding this feature. I'm just curious what people think about my position on the for-each loop and the iterator.

Posted by Dansoonie
As far as I know the Android emulator does not provide simulation of hardware sensors. I spent plenty of time searching for options to throw hardware sensor events to the emulator in Eclipse and the Android developers site. I came accross in the Android developer's guide where it describes means to send events to the emulator, but the explanation was so brief I had no idea what to do. Also, I'm not sure if that was the part which describes what I want to acheive. In other words, it isn't so clear in any of the reference I haved looked about how you can simulate the hardware sensors in the emulator. For this reason, if you are developing an Android application which operates based on the orientation(or acceleration) of the device, you might be having trouble debugging your app.

So be it... I was so sure there must be some way to do it. After googling a while, I was led to openintents' google code project page. There I was able to find a sub-project called "Sensor Simulator". It was exactly the thing I was looking for, and it served its purpose right. I was able to simulate hardware sensor and send events to the Android emulator using it. The wiki page in the goole code project site seems to be outdated and you must make modifications to the resulting code of the instructions listed there in order to get it work. So I thought I should share the information about how I managed to make it work along with my understanding of how the thing works since the document on the wiki page delivers partly false instructions.


The distributed zip file of Sensor Simulator contains the hardware sensor simulator server, client, and an external jar library file. The hardware sensor simulator server is where you can change the orientation of the device in a GUI environment and generates the sensory input values. The client is an Android package that must be installed in the emulator(the Android device) which will read generated sensory input from the hardware sensor simulator server. The external jar library file consists of classes that provide an abstract layer to the SensorManager for reading sensory input values generated from the sensor simulator client which was read from the server.

To reduce confusion for future readers, I might as well aknowledge the version I downloaded and worked on for the people who might refer to this post later on when there is an updated version available and this post becomes invalid. But it's already been a while since the last update was done to the distributed zip file which was in July 2009. Everything is working quite well, and I see no reason for future updates in the near future unless there is a major change in the Android class architecture that relates to hardware sensors. Anyway, The version I worked with is 1.0.0-beta1 on Android SDK 2.0.


The Sensor Simulator wiki page can be found here.
And the distribution file I downloaded can be found here.

The following instructions are based on the project's wiki page, modified and added instructions and their description is based on my personal experience.

1. Installing the client in the Android emulator
Install SensorSimulatorSettings.apk in the bin directory to the Android emulator. I'm pretty sure there is a way to install Android packages from the DDMS perspective in Eclipse, but I personally prefer using the adb tool from the command prompt. 
adb install SensorSimulatorSettings.apk
You might have to add options to adb if you have multiple devices connected or emulators running. 

2. Execute the server and configure the cilent to make a connection
Execute sensorsimulator.jar in the bin directory which is the executable jar file of the server. The following is a screenshot of the server application. (The appearance of the server app may look weird as the layout is all messed up as the textboxes appear smaller than they are supposed to. You can correct this by resizing the window)

<An instance of the Sensor Simulator Server Application>

 
Scroll up the coresponding textbox which is marked with a red rectangle in the above picture in the server application. As you scroll up, you may see the possible IPs that the client can use to identify and connect to the server. Do not quit the server application.



Now start the Sensor Simulator client which you installed in your Android emulator by installing the sensorsimulatorsettings.apk to your emulator. The client Android app will look like the screenshot on the right.

Enter one of the possible IPs that was shown in the server application in the IP address field. If you did not make any modifications to the port number from the server application, you can use the default value 8010 which is the default value for the port number being used for the server application.






Then go to the Testing tab so you will see something like the screenshot on the left. Press the connect button and select the sensors you will like to retreive infromation from. The order of pressing the connect button and selecting the sensors does not seem to matter. Since the purpose of this post is demonstrating how to simulate hardware sensors, I will be enabling the orientation sensor only.

Now play around with the server. You can change the orientation of the device by clicking and dragging the mobile device looking like figure or by using slide bars. See if the values change in the client where it is marked with a red rectangle in the left picture.



If you can see the values change in the Android emulator by playing around with the server application, it means that the Android emulator is successfully retrieving the simulated hardware sensory input values.


3. Modifying the code to use simulated hardware sensory input
A portion of your application will have something similar to the following code if you were developing an Android application that reads hardware sensor values. The highlighted part is subject to change later on. This code is not tested on an actual device, so I will not guarantee it will work. But I have checked it is buildable. Also, keep in mind that this code contains a lot of deprecated API which I did not bother to make changes. I was referring to an outdated book published in the days of Android SDK 1.5 to make this quick example.

Here are the changes you must make to use the sensory values from the Sensor Simulator to simulate hardware sensors.

  1. The type of mSensorManager must be changed to SensorManagerSimulator. Create a SensorManagerSimulator instance by using the static method SensorManagerSimulator.getSystemServices(Context, String).
  2. Connect SensorManager to the simulator by calling static method SensorManagerSimulator.connectSimulator() before registering SensorListener.
The resulting code may look like the following. The lines highlighted in pink are the newly added lines and the yellow highlighted lines are lines that are modified.

There isn't much to modify after all. Here is how the instructions here are different from the project wiki page.
 The instruction where it says to retrieve the content resolver and create an Intent instance to start an Activity with it is left out because it seems to be unnecessary code. It explains as if it is a very important step, but it causes syntax errors and I have no idea how to interpret in order to make proper modifications. And it still works without those lines. 
 The step where unregistering the SensorListener before registering a new SensorListener from the wiki page is also left out here because I see no reason to do that in my simple example. Maybe that step might be required for some special cases, but I don't think that should happen too often.
 The way how the SensorManagerSimulator instance is created different. SensorManagerSimulator constructor is now private and the instance can be only created via the static method getSystemService().


4. The result

<You can see the simulated hardware sensory input value displayed in the TextView>


if your application stops unexpectedly, review the logs from logcat. See if you find anything similar to the following phrase from your log,

"Permission denied (maybe missing INTERNET permission)."

If this is the case, add the following line right before </manifest> at the end of AndroidManifest.xml.

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









Posted by Dansoonie
2009/09/10 - The Mythical Man-Month Chapter 1
2009/10/08 - The Mythical Man-Month Chapter 2

Chapter 3 The Surgical Team

 컴퓨터 소사이어티 미팅에 가면, 젊은 프로그래밍 매니져가 다수의 그저그런 인력보다는 소수의 뛰어난 인력을 선호한다고 단언하는 것을 종종 들을 수 있다. 우리 모두 그렇다. 하지만 이 주장은 크고 복잡한 문제에 대해서는 해결책을 제시해줄 수 없다.

 프로그래밍 매니져들은 개발자 각각의 생산성에 많은 차이가 있음을 예전부터 잘 알고 있었다. 하지만 실제로 실험적으로 측정된 생산성에 대한 각자의 결과는 놀랍다. Sackman, Erikson, 그리고 Grant에 의해 시행된 실험의 결과에 따르면 개발자들간의 퍼포먼스 차이는 최대 10배 까지 차이가 났으며, 일의 효율에 있어서는 5배까지 차이가 났다고 한다. 그리고 연봉은 퍼포먼스와 효율과 큰 연관성음 없었다고 한다(하지만 일반적으로 그럴 것이라고 생가하지는 않는다).

 앞서 말했지만, 프로젝트의 비용의 대다수는 커뮤니케이션과, 잘못된 커뮤니케이션이 이루어짐에 따라 발생한 문제를 해결하는데서 발생한다. 이것 역시 왜 소수의 뛰어난 사람을 프로그래밍 매니져들이 선호하는지에 대한 증거가 될 수 있다. 그래서 결론은 간단하다. 200명이 투입된 프로젝트에서 25명의 매니져의 직분에 있는 사람만이 경쟁력이 있고, 실력이 있을만큼 경험이 풍부하다면, 나머지 175명은 해고하는 것이다.

 하지만 이 결론을 너무 성급히 내린것은 아닌지 생각해 보자. 우선 25명은 이상적인 소규모 그룹이라고 하기에는 너무 많은 인원이다. 조사에 따르면 일반적으로 프로젝트 내에서의 소그룹은 10명 이상이 되지 않는 것이 좋다고 한다. 25명의 인원은 최소한 2단계의 관리 체계가 필요로 하여 5명의 중간 단계 매니져들이 필요로 하게 된다. 더욱더 나아가 200명의 인력은 아주 큰 프로젝트를 하기에도 너무나 적은 인원이다. OS/360의 예를 살펴보면, 가장 많은 인력이 투입된 시점에서는 프로그래머, 서기, 비서, 매니져, 개발지원 등의 여러가지 일이 분담된 1000명 이상의 인력이 개발에 참여하고 있었다. 1963년 부터 1966년까지 아마도 5000 man-years가 개발 과정에 투입되었던것 같다. 200명의 인력으로는 25년이 걸렸을 일이다. 이것이 작지만 뛰어난 그룹의 문제점이다. 아주 큰 프로젝트를 진행하기 위해서는 터무니 없이 작은 인력이라는 것이다.

 이것이 딜레마다. 매니저는 효율성과 시스템 정체성 유지에 대한 문제 때문에 소수의 뛰어난 디자이너와 개발자를 선호한다. 하지만 아주 큰 프로젝트에 있어서는 아무리 뛰어난 소수의 인력만 가지고는 기한내에 일을 끝마치지 못한다. 이 두가지 문제점을 어떻게 해결할 수 있을까?

 Harlan Mills는 우리에게 신선하고 창의적인 해결책을 제시해준다. 큰 일을 작은 일의 단위로 나누고, 나뉘어진 작은 단위의 일들은 수술실에 투입되는 수술팀과 같이 작고 잘 조직화된 팀으로 해결해 나아가는 것이다. 한 팀의 다수의 인력이 한 문제를 두고 달려들어 해결하는 것이 아니라, 수술실에서 시술자가 집도하고 나머지 사람들은 수술을 거들어주는 방식으로 일을 해결하면 일은 효과적이고 효율적으로 잘 진행될 것이라는 것이다. 이것이 과연 우리가 원하는 결과를 효과적으로 가져다 줄 수 있을지, 아니면 일이 진행되기나 할지 의문을 갖게 될 것이다. 여러 사람이 일을 모두 함께 한다고 해도, 직접적인 설계와 작업에는 소수의 사람만 투입된다. 가능하기나 한 일인가? 은유법을 사용하여 일이 어떻게 분담되는지 설명해 보겠다.

시술자 (The surgeon)
Mills는 chief programmer라고 말한다. 이 사람은 기능(functional) 및 성능(performance) 명세서(specification)를 정의하고, 설계하고, 구현하고, 검증하고, 문서화하는 일까지 한다. 경험이 매우 풍부해야 하며, 수학적인 지식이나 애플리케이션이 적용될 도메인에 대해서 잘 알아야 한다.

조수 (The copilot)
시술자의 일을 뭐든지 대신 할 수 있는 그의 분신. 그러나 시술자 보다 경험이 약간 부족한 사람. 시술자와 함께 같이 일을 하면서 사고하고, 논의하고, 그의 일을 평가해주는 사람. 하지만 모든 결정권은 시술자에게 있다. 현재 작업중인 코드에 대해서 굉장히 자세히 알고 있고, 다른 설계방안에 대해서 생각해볼 여유가 있는 사람. 가끔 코딩도 할 수 있지만, 그 코드들에 대해서는 책임을 지는 위치는 아니다.

관리자 (The administrator)
시술자가 두목이고, 시술자가 모든 인사 및 행정 결정권을 가지고 있지만, 그가 이런 일에 시간을 써서는 절대 안된다. 그러므로 돈, 사람에 대한 문제들을 전문적으로 다루는 관리자가 있어야 한다.

편집자 (The editor)
시술자는 생성되는 모든 문서에 대해서 책임을 져야 한다(최대한 명확하게 기술되어야 하기 때문에 본인이 직접 써야 한다). 편집자는 이런 문서들에 대해 비판하고, 수정하고, 참고문헌들까지 정리해서 최종적으로 온전한 문서가 될수 있도록 도와줘야 한다.

비서들 (Two secretaries)
관리자와 편집자는 비서들이 필요하여, 프로젝트의 대외적인 일을 담당할 것이며, 아울러 프로젝트와 직접적으로 관련이 없는 문서들을 정리하게 될 것이다.

서기관 또는 사무관 (The program clerk)
프로그램 개발 과정에 대한 모든 기록들을 관리하게 된다.

대장장이 (The toolsmith)
파일 편집기, 문서 편집기, 디버깅 서비스들은 요새는 대부분 주어진 상태에서 일이 진행되기 때문에 자체적인 기계 운용 요원들이 필요 없다. 하지만 위에서 언급된 기능들은 항상 개발자들이 만족할만한 수준과 높은 신뢰성이 유지될 것이 요구된다. 시술자의 판단하에 언제든지 필요한 서비스나 기능들이 제공되도록 툴의 제공, 업그레이드, 유지 및 보수에 대한 서비스를 제공할 수 있는 사람이 필요하다.

테스터 (The tester)
기능에 대한 요구 명세서에 따라 테스트 케이스를 만들어서 전문적으로 테스트를 하는 사람.

언어 변호사? (The language lawyer)
Algol과 같은 언어를 사용해 시스템을 유용하게 활용하는데 도가 터있어서 시술자에게 조언을 해줄 수 있고, 시술자가 언제든지 조언을 구할 수 있는 사람.

 이렇게 구성된 수술팀과 유사한 팀은 효율적인 프로젝트 운용을 위한 요구사항에 딱 들어맞는다. 일반적으로 두명의 프로그래머들로 이루어진 팀과 시술자와 조수로 이루어진 수술팀과 유사한팀을 비교해 보자.

 일반적인 두명의 프로그래머들로 이루어진 팀에서는 일을 분담하고 각 프로그래머가 자기가 맡은 부분의 디자인과 구현에 책임을 지게 된다. 하지만 수술팀과 같은 팀에서는 시술자와 조수 모두 같은 부분에 대해서 일을 하게 된다. 그 부분의 코드에 대해서 두명이 세밀한 정보까지 공유할 수 있게 되고 일의 분담에 대한 노력도 줄어든다. 그리고 무엇보다도 시스템의 정체성이 지켜질수 있다는 것이 가장 큰 수확일 것이다. 또, 만약 두 프로그래머가 동등한 위치에 있다고 가정한다면, 서로 이견이 있을 경우에는 타협이 이루어져야 한다. 일을 나눠서 하게 되는 경우에는 한정된 자원을 나눠서 해야 하는데, 자원 분배의 판단 기준은 전체적인 안목에서 결정되어야만 함에도 불구하고, 분업이 이루어지면 각자 맡은 일의 관점에 따라 판단이 이루어지게 되므로 편파적인 결정이 이루어질 수 밖에 없다. 따라서 시술자와 보조자로 이루어진 팀과 같은 경우는 일의 분담에 대한 문제가 없고, 상하 주종의 관계가 있기 때문에 두 프로그래머로 이루어진 팀 보다는 uno animo로 작업하는 것이 가능해 진다.

 시스템 개발 이외의 일에 대해서는 각각 세분화시켜 전문가에게 맡기기 때문에 효율은 증대되고 커뮤니케이션의 구조는 매우 간단해져 일이 효과적으로 진행된다. 실제로 행해진 작은 규모의 실험에서는 위와 같이 일을 분담한 팀을 구성할 경우 좋은 결과를 가져온다는 것이 입증되었다. 하지만 이 방법을 큰 규모의 프로젝트에 많은 인력에 대해서 적용할 수 있을까? 오늘날의 많은 일이 5000 man-year가 필요한 것을 감안하면 매우 중요한 사안이다. 큰 프로젝트를 위와 같은 방식의 팀으로 나누는데 있어서 가장 중요한 것은 개발하는 시스템의 각 요소의 정체성이 보존될 수 있는 범위 내에서 일이 나누는 것이다. 이것에 대해서는 뒤에서 또 언급 한다. 여러개의 10명정도로 이루어진 수술팀이 큰 일을 분담하게 되면, 전체적인 사안에 대한 결정을 내려야 할 경우에는 각 팀의 시술자들만 모여서 협의하면 된다.



역시 요약하여 정리하는 것은 힘듭니다...
요약이 아니라 거의 번역이군요... 그렇다고 해서 수준높은 번역은 아닌데요... ㅡ.ㅡ;
조금이나마 도움이 되셨으면 좋겠습니다...
개인적으로 이 단원 마지막 소단원인 Scaling Up은 잘 이해가 안되는군요. 다른사람들이 요약한것을 보니 제가 잘못 이해 한듯 하여, 다른 사람들의 요약한 내용을 기반으로 정리했습니다. 혹시 이 책 읽어보신 분이나 읽게 되는 분 계시면 나중에 저랑 짧게 이 부분에 대해서 의견교환 해봤으면 좋겠습니다... 답글남겨주세요~
Posted by Dansoonie
2009/09/10 - The Mythical Man-Month Chapter 1


Chapter 2 The Mythical Man-Month

  소프트웨어 프로젝트의 대부분은 다른 문제보다도 납기를 지키지 못하는 문제를 가장 많이 가지게 된다. 왜 이런 문제가 보편적으로 발생할까?
  1. 프로젝트 수행 기간을 예상하는 기술이 부족하기 때문이다.
  2. 시간과 노력을 투자하면 무조건 성과가 나타나리라 착각하고 있다.
  3. 프로젝트 관리자는 인내심이 없어진다.
  4. 프로젝트 진행 상황이 잘 감시되지 않는 경우가 많다.
  5. 시간에 쫓기면 인력 추가를 가장 손쉬운 해결책으로 사용한다.

 특히 5번의 문제는 불에 기름을 뿌리는 것과 같은 부작용을 가져오게 하여 문제를 더 크게 한다. 프로젝트 진행상황에 대한 감시 및 관찰은 다른 에세이에서 다루기로 하고, 일단 인력추가가 왜 일을 빨리 진행시키기 위한 좋은 방법이 아닌지 알아보자.

  모든 소프트웨어 개발자는 낙천주의자다. 항상 프로젝트는 잘 짜여진 계획대로 진행될 것이라고 생각하고, 디버깅 하여 잡은 버그가 최후의 버그라고 생각한다. 이런 낙관주의적 태도가 프로젝트 진행에 미치는 영향을 분석해볼 필요가 있다.

  소프트웨어 개발 이외의 창작 활동은 물리적인 매체를 사용한다. 그런 창작 활동은 때로는 물리적인 매체의 성질을 잘 이해하지 못함으로써 문제가 생긴다. 하지만 소프트웨어 개발은 물리적인 매체에 대한 이해가 필요 없이 우리의 생각을 바로 프로그램으로 만들 수 있기 때문에 우리는 때로는 그 작업을 너무 쉽게 생각하는 경향이 있다. 이것이 소프트웨어 개발에 대한 낙천적인 생각의 근원이다. 우리는 결점이 없는 사고를 하기 힘들다. 하지만 소프트웨어 개발은 많은 세분화된 작업과 그것들의 단계로 이루어지는데, 우리는 우리가 완벽하지 않음을 잊어버리고 그 점을 간과하기 때문에 프로젝트는 일정은 지체된다.

  우리가 소프트웨어를 아무 문제 없이 개발 할 수 있다는 생각 자체가 프로젝트 수행 기간을 잘못 예상하는 가장 큰 문제이다. 소프트웨어 개발 작업은 세분화 될 수 있기 때문에 일반적으로 사람을 프로젝트에 많이 투입함으로써 그 기간은 단축되리라고 생각한다. 하지만 개개인이 맡은 일에 들어가는 노력과 프로젝트의 진행이 비례하지만은 않는다는 사실을 우리는 기억해야 한다. 소프트웨어 개발 작업이 세분화 되어 세분화된 작업을 투입된 사람들이 각각 자기가 맡은 일을 하게 되면, 모든 작업이 유기적으로 이루어져야 한다. 그래서 사람들은 서로 communiate해야 할 필요가 있는데, 사람이 많으면 많아질 수록 communication이 이루어지는데 부하가 걸리게 된다. 따라서 투입된 사람의 수에 따라 프로젝트가 단축될 수 있는 시간에는 한계가 있다. 사람 수가 어느 한계점을 넘어서게 되면 communication 때문에 걸리는 부하가 장애로 작용하기도 한다. 

 바람직한 개발 계획 일정은 다음과 같이 각각의 단계에 시간을 다음과 같은 비율로 할애하여 계획하는 것이 바람직하다.

  1. 1/3은 계획하기
  2. 1/6은 구현하기
  3. 1/4은 컴포넌트 테스팅과 초기 시스템 테스팅
  4. 1/4은 전반적인 시스템 테스팅
  이런 방식으로 프로젝트 일정을 계획하는 것은 기존의 방법과 다음과 같은 면에서 다르다고 할 수 있다.
  1. 일반적인 경우보다 계획 단계에 많은 시간이 할애 된다. 그럼에도 불구하고, 구체적인 사양서 및 섬세한 기술 검토를 위한 시간은 부족하다.
  2. 테스팅과 디버깅에 할애된 기간은 전체의 반을 차지한다.
  3. 구현이 가장 작은 비율을 차지한다.
  거의 극소수의 프로젝트들만이 테스팅에 전체 개발 기간의 반을 할애한다. 하지만 결과적으로는 대부분의 프로젝트는 원래의 전체 개발 기간의 반이라는 시간이 걸리게 된다. 테스팅 및 디버깅에 너무 짧은 시간을 할애 하게 되면, 대부분의 일정 지연이나 문제점들은 프로젝트 막바지에 나타나기 때문에 치명적일 수 밖에 없다.

  간혹 개발 일정이 지연되면, 인력을 더 추가하는 경우가 다반사인데, 새로 투입된 인력의 훈련과 교육, 그리고 추가된 인원 사이에서 이루어져야 하는 communication의 부하 때문에 일정은 단축되기 보다는 점점 지연되는 불상사가 발생한다. 이것이 수많은 프로젝트들이 개발 일정에 뒤쳐지는 가장 큰 이유이다. 지연된 프로젝트에 추가적인 인원을 투입하는 것은 그 프로젝트를 더 지연시키는 결과를 낳게 된다.


  두번째 단원 요약입니다. 실제로 책을 읽어보면 소단원이 여러개로 나뉘어 있는데, 읽다보면 공감이 가는 부분이 많음에도 불구하고, 각 소단원의 연관성은 잘 못찾겠네요 ㅡ.ㅡ;
그래서 대충 소단원별로 순차적으로 요약했습니다. 제가 요약한 것을 읽어보셔도 내용전개가 약간 부자연스러울지도 모르겠습니다.

 책의 내용중에 빼먹은 사소한 것들도 있고. 책에는 일정 지연과 인력 투입에 대해서 좀더 자세하게 다루고 있으니 관심있으신 분은 꼭 책을 읽어보시기 바랍니다. ^^

Posted by Dansoonie