제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



요상한 javascript

1. this 는 함수가 호출되는 시점에 결정된다.

var a = {name:'a', sayName: function (){ return this.name;} };
var b = {name:'b', sayHello : function(){ return 'hello ' + this.name;}};

alert( b.sayHello.apply(a)); //hello a 가 찍힘.

a에는 존재하지도 않는 함수를 호출하는 방법입니다.

javascript가 class 기반의 oop 언어와 상당히 다른 점 중 한 가지 입니다.

특정 객체에 소속되지 않는 함수에서의 this 는 window 객체가 됩니다.

ex> 
foo();
function foo(){
var me = this; // 함수 선언과 호출이 이와 같이 된다면( object.foo()나 method.call(object, ...) method.apply(object ,.. ) 의 형식이 아니면 ) this는 window 객체를 의미함.
}





2. debugger

firebug나 chrome debugger 등이 켜져 있을 때 자동으로 break point 의 역할을 하게 됩니다. 코드에 debugger; 라고 치면 그 라인에서 디버그가 걸려서 멈추게 됩니다. 디버거가 안 켜져 있으면 무시하고 지나갑니다. console.log 와 비슷하다고 이해하시면 됩니다.
HTTP get으로 js가 호출될 경우 브라우저에서 판단해서 굳이 호출하지 않고 브라우저의 cache를 쓰기도 합니다. 따라서 js파일을 수정했더라도 수정된 것이 반영되지 않는데 이런 현상이 일어나지 않게 하는 방법은 두 가지가 있습니다.
 1. 호출전에 브라우저 캐쉬를 지운다. ( 아~ 구찮아... )
 2. javascript library 등을 통해서 cache를 무력화 시키는 파라미터를 강제로 박는다. (Extjs의 _dc 파라미터가 하는 역할이 이겁니다.)
 
 1 번의 경우는.... 정말 걍 구찮습니다.
 2 번의 경우는 디버거 툴을 이용해서 break point를 걸어놓더라도 cache 무력화 파라미터 때문에 새로 호출하면 완전히 다른 파일로 인식하기 때문에 break point 가 동작하지 않습니다. ( /a.js?_dc=1 로 받아온 파일과 /a.js?_dc=2 로 받아온 파일을 브라우저는 다른 놈으로 인식한다는 뜻입니다.)

 이럴 때, 2번의 방법으로 하고 debugger; 라고 치면 그 라인에 break point가 걸립니다. 





3. javascript에서 함수는 변수다.

java 와 같은 언어에서는 함수와 변수가 완전히 다른 것이지만 javascript에서는 함수란 "실행가능한 변수"의 개념으로 이해하시는 게 좋습니다.

function foo(){ alert('merong');}
var foo = 'a';
foo(); //에러남.

foo 라는 변수가 foo라는 함수를 덮어쳐 버렸습니다.

함수를 함수의 인자로 넘기는 것이 가능한 것이 바로 이 때문입니다. command 패턴 같은 것을 쓸 때 굳이 인터페이스 같은 것을 만들 필요가 없어집니다.

기본적으로는 아래 두 구문은 동일합니다.

1. function foo (...
2. var foo = function (... 

다만 차이가 있는 것은 1의 경우는 함수 호출을 그 위쪽에서 해도 되지만 2의 경우는 그렇게 하면 함수를 찾을 수 없다고 나옵니다. (인터프리팅 후 런타임 단계로 넘어가는데 1은 인터프리팅이 되면 바로 함수가 생기지만 2는 런타임 시점에서 함수가 생긴다고 보시면 됩니다.)

ex> 
case 1 > 
foo();
function foo(){ alert ('merong');} //함수가 먼저 파싱되서 에러 안나고 함수 호출 잘 됨.

case 2>
foo(); // 아직까지는 foo가 뭔지 알 수 없는 상태라서 에러
var foo = function () { alert('merong');}


아래 코드는 특히 좀 골 때립니다. 

var foo = function (){alert('haha');};
function foo(){ alert('merong');}
foo(); //haha가 찍힘

이유는... function foo()가 먼저 인터프리팅되서 처리 끝난 후에 런타임에 var foo가 덮어쓰기 때문입니다.





4. javascript의 함수는 멤버를 가질 수 있다.

function foo(){ alert('merong');}
foo.member = function(){alert('member');};
foo(); //얘는 함수
foo.member(); //얼레? 얘도 함수

javascript에서의 함수는 실행가능한 object 의 형식으로 이해하시면 되겠습니다. object니까 당연히 멤버를 물고 다닐 수 있습니다.





5. default value 처리

a라는 변수에 b의 값이 있으면 b를 할당하고 없으면 c를 하려면 아래와 같이 하면 됩니다.

var a = b?b:c;

이것을 더 간단히 하면 아래와 같이 됩니다.

var a = b||c;

이것을 응용하면 함수의 인자의 기본값을 설정할 수 있습니다.

function eat(hamburger, beverage){
 beverage||(beverage='coke'); //음료가 없으면 음료는 coke로 설정!
 alert('eating ' + hamburger + ' with ' + beverage);
}

eat('치킨버거');
eat('불고기버거', '사이다');





6. javascript object에서 키값은 반드시 String

java의 object 처럼 키를 아무거나 넣을 수 있는 것이 아닙니다. string과 숫자만 됩니다. 

var s = {'name':'jack'};
var s = {name : 'jack'};

따라서 위 두 구문은 완전히 동일합니다.


var key = {}; //key로 쓰기 위한 어떤 object
var object = {}; //테스트용 object
object[key] = 1; //key로 object를 썼네 ?
alert(object[{}]); //뭐야? 예상한 대로 1이 나온다!!

뭐, key로 object를 넣어도 되는 것 같아 보이는 코드입니다. 하지만 맨 마지막 줄을 alert(object[{a:0}]) 과 같이 엄한 놈을 집어넣어도 1이 나옵니다. 잘 되는 게 아닙니다.

alert(object['[object Object]']); // 이거 해봐도 1이 나옮. 
var v = {toString : function (){ return '[object Object]';}}
alert(object[v]); // 이거도 1이 나옮

var v = {toString : function (){ return '이상한 문자';}}
alert(object[v]); // 이거는 1이 안 나옮. 결국은 toString 이란 얘기..


숫자도 주의해야 할 점이 있는데.

var a = {};
a[0] = 'int';
a['0'] = 'string';
alert(a[0]); // string이 찍힘

이유는.. object의 key 에 들어간 숫자도 string으로 고쳐지기 때문입니다.





7. NaN은 무엇과 비교해도 false

var nan1 = Number('a');
var nan2 = Number('a');
alert(nan1 == nan2); //false


또 재밌는 것은 alert(typeof nan1) 을 해보면 number라고 나옵니다. NaN 이 "Not a Number" (숫자가 아님) 인데 타입은 숫자 타입이랩니다.

비슷한 것으로 

alert(typeof(null)); // object

null의 타입은 object입니다.

또 비슷한 것으로 new Boolean(false)가 있습니다.

alert(new Boolean(false)); //false
if (new Boolean(false)){ //그러나 실행됨..
alert('true??'); //결국 이 alert도 실행됨..
}

alert에서 찍힘 false가 boolean값 false를 의미하는 게 아닙니다. 
아래쪽 if에 들어가서 실행됩니다. 그 이유인 즉슨, Boolean은 object이기 때문입니다.

alert( typeof new Boolean(false)); // object





8. 숫자 변환기 + 표시

var v = +new Date();  // ==> var v = new Date().getTime(); 과 같음.

+"1000" // ==> parseInt('1000'); 과 동일.
+true // ==> 1
+false //==> 0
+"1e3" //==> 1000

등과 같이 숫자로 변환할 수 있는 게 많습니다.

특정 클래스에 valueOf 함수가 있으면 + 표시는 valueOf 함수를 호출해 줍니다. 아래는 그 예제입니다.

function IntWrapper(a){
this.valueOf = function (){ return a;};
}

var a = new IntWrapper(1);
alert(+a); // 1

by 삼실청년 | 2013/07/09 23:10 | 컴터질~ | 트랙백(1) | 덧글(2)

제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



Google I/O 2011: Java Puzzlers - Scraping the Bottom of the Barrel

이 글은 http://www.youtube.com/watch?v=wbp-3BJWsU8 의 내용을 한글로 정리한 겁니다. 코드는 동영상에 나와있는 것을 거의 그대로 사용합니다.

1. 거스름돈 계산

package googleio2011;

public class Ex1 {
public static void main(String[] args) {
System.out.println(2.00 - 1.10);
}
}

$1.1 짜리 물건을 사는데, $2 를 냈으면 거스름돈은 얼마인가를 묻는 코드입니다. 위 프로그램의 결과 값은 무엇일까요?

0.8999999999999999 이 나옵니다. java에서 double은 정확한 값을 제공하지 않습니다. 따라서 BigDecimal 클래스를 사용하는 게 좋습니다. 그러면 아래 코드의 결과는 어떻게 될까요?

package googleio2011;

import java.math.BigDecimal;

public class Ex1_1 {
public static void main(String[] args) {
BigDecimal payment = new BigDecimal(2.00);
BigDecimal cost = new BigDecimal(1.10);
System.out.println(payment.subtract(cost));
}
}

a> 0.9
b> 0.90
c> 0.8999999999999999
d> 닶 없음.






--------------------------------- 여기서 부터 답 ---------------------------------

정답은 d>이며, 0.899999999999999911182158029987476766109466552734375 이 나옵니다.

이 나옵니다. 여기서 문제는 BigDecimal payment = new BigDecimal(2.00); 에서 constructor의 인자가 2.00 으로 double 값이 들어간 것입니다. 

BigDecimal(double val) 은 double의 값을 BigDecimal로 표현하는 것 뿐이기 때문에 이미 double에서 값이 나가리난 겁니다.


* float 이나 double은 정확한 연산이 필요한 곳에는 쓰이면 안 됩니다.

* new BigDecimal(double)을 쓰지 말고, new BigDecimal(String) 으로 해야 정확한 값이 나옵니다.


2. 사이즈 문제


package googleio2011;

import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class Size {
enum Sex {
MALE, FEMALE;
}
public static void main(String[] args) {
System.out.println(size(new HashMap<Sex, Sex>()));  -- line 14
System.out.println(size(new EnumMap<Sex, Sex>(Sex.class))); -- line 15
}
public static int size(Map<Sex, Sex> map) {
map.put(Sex.MALE, Sex.FEMALE);
map.put(Sex.FEMALE, Sex.MALE);
map.put(Sex.MALE, Sex.MALE);
map.put(Sex.FEMALE, Sex.FEMALE);
Set<Map.Entry<Sex, Sex>> set = new HashSet<Map.Entry<Sex, Sex>>(map.entrySet());  -- line 22
return set.size();
}
}

a> 2 1
b> 2 2
c> 4 4
d> 닶 없음






--------------------------------- 여기서 부터 답 ---------------------------------

정답은 a>

line 14의 HashMap의 경우는 2가 나오는 게 당연한데, line 15의 EnumMap의 경우 1이 나오는 게 이상합니다.

일단 line 22의 public HashSet(Collection<? extends E> c) 생성자를 까보면 

addAll(c);

와 같이 되어있습니다. 즉 addAll 함수를 호출하고 addAll 함수를 다시 까보면 아래와 같습니다.

    public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
Iterator<? extends E> e = c.iterator();
while (e.hasNext()) {
   if (add(e.next()))
modified = true;
}
return modified;
    }

결국 iterator를 불러서 한놈씩 돌면서 HashSet에 박아주는 겁니다.


EnumMap.entrySet() 을 까보면 

    private class EntrySet extends AbstractSet<Map.Entry<K,V>> {
        public Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }

위와 같이 되어있고 EnumMap.EntryIterator 를 까보면 


    private class EntryIterator extends EnumMapIterator<Map.Entry<K,V>>
        implements Map.Entry<K,V>
    {
        public Map.Entry<K,V> next() {
            if (!hasNext())
                throw new NoSuchElementException();
            lastReturnedIndex = index++;
            return this;
        }

와 같이 되어있습니다.

뭔소리냐 하면 보통 EntryIterator.next() 하면 Entry  가 새로 만들어져서 return 되는 게 일반적인 방식입니다. 그러나 EnumMap의 EntryIterator는 implements Map.Entry 를 하고 있으며, next() 메쏘드는 return this 로 하고 있습니다. 즉, Iterator 자체가 Entry역할까지 겸하며, Entry 자체는 1개만 만들어 지고, 그 Entry 안에서 Iterator의 next가 호출될 때마다 순서값만 변경을 시키며, Entry. getKey() 나 getValue()가 호출될 때는 순서값을 가지고 자신을 감싸고 있는 EnumMap에 가서 그 순서에 맞는 Key나 Value를 가져다가 return합니다. 이렇게 하면 외부에서는 계속 새로운 Entry를 뽑아내는 것 같은 모양새를 유지하면서 객체를 새로 생성하지 않을 수 있습니다.

IdenticalHashMap과 EnumMap이 이런 식으로 구현되어있고, ConcurrentHashMap도 예전에는 그랬었다고 합니다.

이 문제를 해결하려면 line  22 를 아래와 같이 바꿔서 iterator 돌면서 Entry를 새로 만들어내면 됩니다.


Set<Map.Entry<Sex, Sex>> set  = new HashSet<Map.Entry<Sex, Sex>>();
for(Map.Entry<Sex,Sex> e : map.entrySet())
set.add(new AbstractMap.SimpleImmutableEntry<Sex,Sex)(e));


3. Match Game


package googleio2011;

import java.util.regex.Pattern;

public class Match {
public static void main(String[] args) {
Pattern p = Pattern.compile("(aa|aab?)+");
int count = 0;
for(String s = ""; s.length() < 200 ; s+="a")
if (p.matcher(s).matches()) 
count++;
System.out.println(count);
}
}

정규식에 관련된 문제입니다.

a> 99
b> 1000
c> exception 발생
d> 닶 없음





--------------------------------- 여기서 부터 답 ---------------------------------

정답은 d> 연산 결과가 나오려면 10^15 년 걸린다고 합니다.

문제는 (aa|aab?)+ 라는 정규식 입니다. aa|aab? 는 aab? 와 같은 표현식입니다. 정규식에 대해 이해가 잘 안 가시면 요기를..

자바 뿐만 아니라 많은 언어에서 두가지 이상의 방법으로 동일한 것을 체크하는 경우 이런 식의 과부하가 발생한다고 합니다.


4. That Sinking Feeling


package googleio2011;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

abstract class Sink<T>{
abstract void add(T... elements);
void addUnlessNulll(T... elements){  //line 10
for (T element : elements) {
if (element != null) {
add(element);   // line 13
}
}
}
}

public class StringSink extends Sink<String>{
private final List<String> list = new ArrayList<String>();
void add(String... elements){  //line 21
list.addAll(Arrays.asList(elements));
}
public String toString(){
return list.toString();
}
public static void main(String[] args) {
Sink<String> ss = new StringSink();
ss.addUnlessNulll("null", null);  // line 29
System.out.println(ss);
}
}

a> [null]
b> [null, null]
c> NullPointerException
d> 닶 없음.





--------------------------------- 여기서 부터 답 ---------------------------------

정답은 d>이며 ClassCastException이 뜹니다.

에러 결과를 보면 아래와 같습니다.

Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
at googleio2011.StringSink.add(StringSink.java:1)
at googleio2011.Sink.addUnlessNulll(StringSink.java:13)
at googleio2011.StringSink.main(StringSink.java:29)

여기서 재미있는 부분은 error stack에서 1번째 줄에서 뭔가 나왔다는 겁니다. 1번째 줄은 패키지 선언입니다. stack trace에 들어갈 이유가 없는 곳이죠!

3가지 포인트가 있는데, 첫째는 Varargs ( T... 으로 표현된 부분) , 둘째는 erasure (generic에서 생기는 거. 자세한 건 요기), 세째는 bridge method 입니다.

line 29에서  Varargs 는 내부적으로 갯수가 변하는 변수로 처리하는 것이 아니라 Array로 처리한답니다. 따라서 line 29에서 String[] 이 생깁니다. 즉 new String[]{"null", null} 이 생성된다고 보시면 됩니다. 
line 10에서 T... 으로 받는 것은 사실은 그냥 Object[] 로 받는 것입니다. erasure 로 처리되는 거죠. 그래서 line 13에서 element는 String이 아니라 사실 Object입니다. 
그런데, Sink<T>에서 정의된 add method를 보면
abstract void add(T... elements) 라고 되어있습니다. 이것을 상속받아 구현한 method는 21번째 줄에서 void add(String ...)으로 되어있습니다. 
line 13에서는 line 29에서 String[]가 생성된 것과 마찬가지로 Object[]가 생성됩니다. 이게 결국 line 21의 method로 전달되어야 하는데 Object[]를 String[]으로 전달할 수는 없습니다. 이런 이유로 코드 상에서는 존재하지 않지만 java가 내부적으로 메쏘드를 생성하는데, 이게 bridge method입니다.

void add(Object[] args){   -- 여기가 실제로 abstract add(T... elements)를 override하는 부분이며,
add( (String[]) args); -- 여기서 실제로 존재하는 메쏘드인 void add(String ... element) 를 호출하도록 연결 시켜줍니다.
}

이 bridge method 안 쪽에서 Object[]를 String[]으로 바꾸려는 시도를 하게 된 것입니다. 실제로 존재하지 않는 코드이기 때문에 error stack에서는 line1 이라는 이상한 숫자가 찍히거구요. 

실제로 이클립스를 통해서 코드를 만들면 13번째 줄에서 "Type safety : A generic array of T is created for varargs parameter" 라는 warning이 뜹니다. 

이걸 해결하는 좋은 방법은 varargs와 array를 전부 collection으로 뜯어 고치는 것이랩니다.

수정된 코드는 아래와 같습니다.

abstract class Sink2<T>{
abstract void add(Collection<T> elements);
void addUnlessNulll(Collection<T>  elements){
for (T element : elements) {
if (element != null) {
add(Collections.singleton(element));
}
}
}
}

public class StringSink2 extends Sink2<String>{
private final List<String> list = new ArrayList<String>();
void add(Collection<String>  elements){
list.addAll(elements);
}
public String toString(){
return list.toString();
}
public static void main(String[] args) {
Sink2<String> ss = new StringSink2();
ss.addUnlessNulll(Arrays.asList( "null", null));
System.out.println(ss);
}
}


교훈 

1. varargs 는 쫌 조심해서 써라.
2. Generics랑 array는 별로 사이가 안 좋다. 따라서 generic이랑 varargs도 사이가 안 좋다. 
3. 긍까 array 같은 거 쓰지말고, collection 으로 가자. 특히 API 만들때는!!!
4. 컴파일러 말씀을 잘 듣자. 컴팔러가 괜히 지랄하는 게 아니다.!! 죽었다 깨어나도 확실하다 싶은 부분 에다가 @SuppressWarnings annotation을 쓰자. 



5. Glommer pile

package googleio2011;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;

public class Glommer<T> {   // line 7
String glom(Collection<?> objs){ // line 8
String result = "";
for (Object o : objs) { //line 10
result += o;
}
return result;
}
int glom(List<Integer> ints){ // line 15
int result = 0;
for (int i : ints) { // line 17
result += i; 
}
return result;
}
public static void main(String[] args) {
List<String> strings = Arrays.asList("1", "2","3");
System.out.println(new Glommer().glom(strings));  // line 24
}
}

a> 6
b> 123
c> exception
d> 답 없음





--------------------------------- 여기서 부터 답 ---------------------------------

c 입니다. 앞의 문제와 비슷한 것입니다. line 24에서 generic type이 지정되지 않은 것이 문제 입니다. 에러는 아래와 같습니다.

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
at googleio2011.Glommer.glom(Glommer.java:17)
at googleio2011.Glommer.main(Glommer.java:24)

일단 raw type이 들어오면 (즉, line 24에서 generic type을 지정하지 않으면 ) line 7, 8, 15, 23에 있는 generic parameter 들은 홀라당 날아갑니다. 

결국 glom(List ) 와 glom(Collection) 으로만 선택하려고 하면 List로 가는 게 맞죠. 따라서 glom(List<Integer ints) 안으로 List<String>이 들어가서 ClassCastException으로 이어지는 거죠.


그런데 잘 보면 line 7에서 지정한 T는 쓰지도 않습니다. 따라서 다음과 같은 해법들이 가능합니다.

1. line 24에서 new Glommer<아무거나>().glom(.... ) 으로 바꿔줍니다.
2. line 7에서 generic parameter를 날려 버립니다.
3. 2개의 glom method는 사실 아무런 멤버 변수에 접근하지 않습니다. 따라서 그냥 static method로 정의해 버립니다. 호출할 때도 객체 생성하지 말구요.

그래서 교훈은...

1. raw type을 쓰지 말자.
2. raw type 쓰면 generic type 정보는 홀라당 날아간다~
3. 컴팔러가 뭐라고 하는 건 다 이유가 있는 거다. 말 좀 잘 들어줘라.


6번 째꺼 있는데 별 내용 아닙니다. 대충 정리를 하자면.,,

첫째는 숫자 1과 영어 소문자 l 을 헤깔리게 써놓고 장난질하지 말자.. (컴팔러는 에러를 안 냅니다!!!! 그냥 long이려니 하고 말죠.)
둘짜는 숫자 01234 라고 쓰면 1234와 는 달리 8진수로 처리해서 10진수로 668이 된다는 얘깁니다. System.out.println(01234); 해보면 668찍힙니다.


by 삼실청년 | 2013/05/06 06:06 | 컴터질~ | 트랙백(1) | 덧글(2)

제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



내가 생각하는 좋은 개발자 되기.

가끔씩 어떻게 해야 좋은 개발자가 되냐는 질문을 받습니다. 저도 부족한 것이 많지만 제가 생각하는 것을 정리해보려고 합니다.

저는 개발자를 3단계로 구분합니다. 첫째는 그냥 초짜 단계입니다. 아무것도 모르고 열심히 공부할 때죠. 두 번째는 하래는 건 할 수 있다는 단계입니다. 정말 무지막지한 것을 시키지 않는 이상 적당히 하래는 것은 할 수 있고 자기가 생각하는 것도 적당히 만들 수는 있는 단계입니다. 세번째는 성능과 설계를 생각하는 단계입니다.

바보가 아닌 이상 개발을 꾸준히하면 두 번째 단계에는 도달합니다. 그냥 시간이 지나면 아이가 성인이 되는 것과 같습니다. 그리고 많은 수의 개발자들이 두 번째 단계에서 끝납니다. 더 나아가야할 필요가 없기 때문입니다. 하래는 건 다 할 수는 있으니까요. 특히 SI를 하게 되면 이 단계에서 영원히 벗어나지 못하는 일이 생깁니다. 제가 SI를 많이 해보진 않았지만 제가 본 SI하는 사람들 중에서는 두번째 단계를 벗어난 사람을 못 봤습니다.

두번째 단계에 있는 사람들은 주로 이론과 실제는 다르니까 해봐야 한다는 말을 합니다.
어디선가 TreeMap이 HashMap보다 빠르다고 주장하는 사람을 봤습니다. 해보니까 그렇더라였는데, 그 "해보니까"가 잘못되었었습니다. 성능에는 반드시 이유가 있습니다. 컴퓨터에서는 이론과 실제는 다르지 않습니다. 이론과 실제가 다른 것은 이론을 모르거나 잘못알고 있는 경우와 실제를 잘못 적용한 경우 밖에 없습니다. 테스트를 해보는 것은 자기가 이해한 것이 맞는 지 확인하는 용도여야 합니다.

SI 개발자 입장에서는 소프트웨어 튜닝이 필요가 없습니다. 

SI의 고객은 일반적으로 인건비는 아껴도 서버 사는 데는 돈을 아끼지 않는 사람들입니다. 성능이 안 나오면 더 좋은 서버를 사고 서버를 여러 대 사고 하는 식으로 해결해버립니다. 고객은 잘 모르니까 개발자가 "서버가 더 필요하다"고 하면 그런가 보다 하고 과감하게 지갑을 엽니다.

적당히 고객 구슬려서 고객돈으로 서버를 사게 하는 것과 자기가 고생해서 프로그램 수정하는 것 사이에서 별로 고민할 필요가 없습니다. 게다가 프로그램 수정했다가 잘 되던 거 안 되는 사태 발생하면 책임질 수도 없구요.

또 SI에서 만드는 것은 대부분 기업용이기 때문에 굉장한 부하를 유발하지도 않습니다. 동시 접속자가 많아야 몇 천 정도고 그 정도면 아주 예외적인 경우를 빼고는 뭔 짓을 해도 서버가 힘들어하지 않습니다.

그리고 SI는 한 번 만들고 나면 손 텁니다. 뒷일을 책임질 필요가 없기 때문에 설계를 생각할 필요가 없습니다. 대충 땜빵 코드로라도 돌아가게 하면 장땡입니다. 고객이 원하는 것도 딱 그 정도 수준이고요. 어떤 식으로 잘 해결했냐가 중요한 게 아니라 오늘은 몇 페이지 찍어냈냐가 중요한 거죠. 그래서 나중에는 renewal 보다는 재개발이 되는 겁니다.

두 번째 단계의 큰 함정 중 또 하나는 두 번째 단계에 있을 때는 자기가 두 번째 단계에 있다는 것을 인식하지 못합니다. 저도 몇 년 허우적거렸네요. 내가 허우적거렸다는 것을 나중에 알았습니다. 
디자인 패턴 열심히 공부하는데 뭔 소린지 하나도 모르겠더군요. iterator 패턴이 "집합체와 개별 원소 간의 분리"라는 걸 이해를 못했습니다. 그냥 list가지고 루프 돌면서 처리하면 되는 걸 왜 iterator 같은 거 만들어서 쓰지? 라고 생각했습니다. 
내가 OOP를 이해하고 있다고 착각해서 그런 거였습니다. 개발 경력 몇 년이 쌓이고 나서 head first java 같은 기초책을 봤습니다. 그걸 보고 내가 OOP를 제대로 이해하지 못하고 있었다는 것을 알았습니다. 그리고 디자인 패턴 책은 며칠 만에 다 봤습니다. 뭔소린지 이해가 가기 시작하더군요. effective java 1판 도 한 두달 동안 열심히 봤죠. 그리고 effective java 2nd edition은 침대에 누워서 몇 시간만에 다 봤습니다.


소프트웨어 개발이 창의적인 직업이라는 데에 저는 동의하지 않습니다. 
개발자가 겪게 되는 대부분의 문제는 다른 개발자가 겪었던 문제들이고 좋은 답을 만들어 놓은 경우가 대부분입니다. 창의성보다는 잘 모르겠으면 끈질기게 알 때까지 물고 늘어지는 습성이 훨씬 중요한 거 같습니다.
컴퓨터는 개발자가 창의적으로 생각해낸 답과 다른 개발자가 해 놓은 거 가져온 것을 구분하지 않습니다. "내가 생각해냈다" 는 건 별로 의미 없습니다. 다만 남이 생각한 것을 제대로 이해하지 못하면 제대로 된 코드를 짤 수 없습니다. 만병 통치약같은 것은 없습니다. 내가 직면한 문제가 무엇인지 정확히 알아야 정확한 해법을 쓸 수 있는 것입니다. 의사가 환자를 치료하려면 환자가 걸린 병이 무엇이고 어떤 약이 어떤 효과가 있는 지를 알아야 하는 것과 마찬가집니다. 환자를 앉혀 놓고 자신의 창의성을 발휘해 보며 실험해보면 안 됩니다.

세상 사가 다 그렇겠지만 개발자는 아는 만큼 보입니다. java sdk 나 아파치 재단에서 배포한 프로그램들을 보면 "좋은 프로그램의 규칙" 같은 것이 있습니다. 코드 작성자의 개성같은 건 별로 없습니다. 일관성있고 그 규칙을 어느 정도만 알면 누가 만들었건 보기가 참 좋습니다.

정리.
1. OOP를 제대로 이해해라!
2. 소프트웨어에서 성능 문제에는 다 이유가 있다. 이유를 알아내고 테스트는 자기가 이해한 것이 맞는지 확인하는 용도다.
3. 창의적이 되려고 하지말고 남들이 한 것을 보고 이해하려고 노력해라. 코드에 개성이 넘치면 누가 관리하겠는가!

by 삼실청년 | 2012/11/24 00:07 | 컴터질~ | 트랙백(2) | 핑백(1) | 덧글(36)

제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



대용량 서비스 - 6. 디비관련 프로그래밍 주의 사항

디비와 연동하는 프로그램을 짤 때 주의해야 할 사항을 정리해보겠습니다.


1. SQL Injection 막기

SQL injection 이라는 기초적인 해킹 기법이 있습니다. 파라미터에 이상한 값을 넣어서 원래 의도와 다르게 쿼리가 돌아가게 하는 방법입니다.


일반적으로 로그인 쿼리는 아래와 같이 만듭니다.

String query = "select * from user_table where user='" + request.getParameter("user") + "' and password='" + request.getParameter("password") + "'";

이 query를 실행시킬 때 사용자가 user 라는 파라미터에 

홍길동'  -- 

와 같이 입력을 했다고 치면, 최종적으로 만들어지는 쿼리는

select * from user_table where user='홍길동' -- ' and password='뭐시기'

가 됩니다. -- 뒤는 모두 주석 처리가 되기 때문에 패스워드와 상관없이 로그인이 됩니다. ( -- 는 mysql 기준이고, 다른 디비들은 다른 것으로 알고 있습니다. )

이것을 피하는 방법은  아래와 같이 PreparedStatement 를 쓰는 게 제일 쉽습니다.


PreparedStatement prep = conn.prepareStatement("select * from user_table where user= ? and password = ?");
prep.setString(1, request.getParameter("user"));
prep.setString(2, request.getParameter(" password "));
...

PreparedStatement를 쓰면 위와 같은 시도에 대해서 아래와 같은 쿼리가 만들어집니다.

select * from user_table where user='홍길동\' -- ' and password='뭐시기'

' 앞에 \가 붙어서 sql injection을 막습니다.


2. 쿼리 조합 금지 


코드를 줄이기 위해 쿼리를 조합하는 것은 별로 바람직하지 않습니다.


StringBuilder query = new StringBuilder("select * from user_table where writer = ?");
if(request.getParameter("start_dt") != null){
query.append(" and start_dt >= ").append(request.getParameter("start_dt"));
}
// 대충 쿼리 실행

위와 같은 방식으로 프로그램을 짜면 나중에 문제가 생겼을 때 찾기가 힘듭니다. 위의 경우야 그나마 단순하지만 위와 같이 쿼리를 조합할 경우 최종적으로 만들어지는 쿼리가 무엇인지 알기가 매우 어렵습니다. 복사-붙여넣기 신공을 통해서 조금씩 수정을 하더라도 쿼리를 조합하지 마십시오.

String query = null;
if(request.getParameter("start_dt") == null){
query = "select * from user_table where writer = ?";
}else{
query = "select * from user_table where writer = ? and start_dt >= ?";
}

위와 같이 하면 중복 코드는 있지만 하나의 쿼리를 이해하기 위해서는 한 부분만 보면 됩니다. 또 query 로그에서 찾아낸 문제가 있는 쿼리가 프로그램의 어디에 있는 것인지 쉽게 찾아낼 수 있으며, 그것을 변경했을 때 다른 부분에 미치는 영향을 줄일 수 있습니다.


3. 복잡한 쿼리 금지

복잡한 쿼리를 사용자용 페이지에 만들지 마세요. (사용자가 별로 없는 관리자 페이지에서는 어느 정도 괜찮을 수도 있습니다만... )

진짜 복잡한 쿼리가 필요한 경우는 극히 드뭅니다. 쿼리 한줄에 여러가지를 해결했다고 좋은 것은 아닙니다. 오히려 쿼리가 길면 성능이 떨어질 가능성이 크며, 쿼리를 이해하기도 훨씬 어려워집니다. 인덱스의 작동 방식을 이해하지 못한다면 join도 안 쓰는 게 좋습니다.

대부분의 복잡한 쿼리는 비정규화와 캐쉬 등을 통해서 해결할 수 있습니다. 

user table (사용자 아이디, 사용자 이름) 과 msg table(글쓴 사람, 글 내용) 이 있을 때, "홍길동"이라는 이름을 가진 사람이 쓴 가장 최근글과 홍길동에 대한 사용자 정보를  뽑으면 아래와 같은 쿼리가 나올 겁니다.

select msg.* , t.* from   
(select max(b.msg_id) as max_msg_id, a.* 
from user a left join 
msg b 
on a.user_id = b.writer where a.user_name = '홍길동'
) t 
left join msg 
on msg.msg_id = t.max_msg_id

이 프로세스를 조각내면

1. 사용자 이름으로 사용자 정보 찾기 ( user.user_name 으로 user.* 찾기)
2. 사용자 아이디로 가장 최근글 찾기 (1에서 뽑은 user.user_id 를 msg.writer 에 대입하여 max(msg_id) 찾기)
3. 특정 글번호로 글 내용 찾기 ( 2에서 뽑은 max_msg_id 로 msg.* 찾기)

와 같이 됩니다.

저 3단계는 전부 간단한 쿼리로 만들 수 있으며, 각각을 하나의 캐쉬로 쓰게 되면 DB에 접근도 안 하고 데이터를 뽑아오는 경우도 생길 수 있습니다.

이렇게 단계가 구분되는 프로세스는 조각조각내서 각각을 DataManager로 정의를 하고 DataManager 안에서는 chain of responsibility 를 이용해서 캐쉬를 잘 정리하는 게 좋습니다. 자세한 건 요기 를 참조하세요.

프로그램쪽에서만 보면..

UserData user = UserByName.get("홍길동");
int maxId = MaxMsgIdByUserId.get(user.getUserId());
MessageData msg = MessageById.get(maxId);

와 같이 됩니다. 위에서 3단계로 조각낸 "사람이 인지하는 프로세스"와 "프로그램 코드"는 대략 일치하지만 sql의 흐름은 괄호를 먼저 찾고 그 안에서 무엇을 뽑는지 보고 그걸 괄호 밖에서 어떻게 쓰는 지 보고 또 그 중에서 무엇을 뽑을 것인지를 살피는 등의 과정을 거쳐야 합니다. 

위의 경우는 최종적으로는 user와 msg 변수를 이용해서 필요한 정보를 쓰면 됩니다. 이렇게 하면 복잡한 쿼리보다 훨씬 직관적으로 볼 수 있습니다.

또한 이렇게 분리시키는 것은 캐쉬 재사용성을 높입니다. 어떤 사용자의 요청에 의해 만들어진 캐쉬를 다른 사용자도 공유해서 쓰게 될 가능성이 큽니다.

그리고 테이블에 컬럼이 추가되거나 하는 경우 미치는 영향을 파악하기가 쉽습니다. 저렇게 분리시키면 대부분의 테이블에 접근하는 케이스가 몇 가지 이내로 정리가 됩니다. 따라서 테이블에 접근하는 쿼리 갯수가 줄어들기 때문에 테이블이 변경되었을 때 수정되어야 할 부분도 줄어듭니다.

여러 테이블에서 조인하는 하나의 쿼리가 각각 테이블을 조회하는 여러 개의 쿼리보다 일반적으로 빠르긴 합니다. 하지만 여러 테이블을 조인하는 쿼리는 여러 개의 테이블에 대해서 lock을 잡기 때문에 실제 서비스에서는 더 느린 현상까지 발생할 수 있습니다. 특히 MyISam의 경우는 테이블 단위로 lock이 잡히기 때문에 MyIsam을 쓰는 경우는 더더욱 조인을 삼가하는 게 좋습니다.

A, B 테이블을 조인하는 쿼리는 A에 대한 변경(insert, update ,delete )과 B에 대한 변경 모두를 기다려야 하기 때문에 A,B에 대한 변경 비율이 같다고 치면 lock이 걸릴 확률이 A,B에 대해 쿼리를 따로 날리는 것보다 2배나 높습니다.

복잡한 쿼리가 없어도 모든 것이 가능하진 않습니다만 더 보기 쉽고 빠른 방법이 있는데도 복잡한 쿼리를 고집할 필요는 없습니다.


4. 방대한 데이터 중 너무 오래된 데이터는 제한


아래의 쿼리는 어떻게 돌아갈까요?

select * from msg order by msg_id desc limit 1000000, 10; (백만 , 십)

중간중간에 지워진 글 때문에  

select * from msg where msg_id <= 1000000 order by msg_id desc limit 10; 

로 쓸 수 없다고 가정합니다. 

msg_id에 index가 걸려있다고 하더라도 순서대로 1,000,000 번을 일단 움직여야 합니다. 이 연산은 너무 큽니다. 이런 문제를 가장 손쉽게 해결 하는 방법은 위와 같이 너무 오래된 데이터를 사용자가 못 보도록 미리 validation에서 걸러 버리는 겁니다. 페이지 당 10개의 글이라고 치면 100,000 번째 페이지 쯤 될 겁니다. 일반적으로 이만큼 오래된 데이터는 의미가 없습니다.

어쩔 수 없이 조회를 허락해야 하는 데이터라면, 비정규화를 통해서 해결을 하는 것이 좋습니다. 이런 경우 글을 블럭 단위로 구분하고 각 블럭에 있는 글 갯수를 가지고 있는 테이블을 구성하는 것입니다. 예를들어 글번호 100,000 개 당 하나의 블럭을 유지하는 경우

msg_id 가 1           ~ 100,000 사이에 남아 있는 글이 총 99,990 개 라고 치고 (10개의 글이 삭제되었다고 치면) 
msg_Id 가 100,001~ 200,000 사이에 남아 있는 글이 총 99,900 개 라고 치면 (100개의 글이 삭제)

아래와 같은 테이블에 저장을 하면 됩니다.


블럭의 첫글번호 블럭 안의 글 갯수
199,990
100,000199,900

이런 식으로 하면 1,000,000 번째의 글이 어디있는 지 훨씬 적은 연산으로 추적할 수 있습니다. (최근 데이터 부터 예전 데이터 방향으로 조회를 하면서 글 갯수 합을 내면 됩니다.)

다만 저번에 말씀드린 것처럼 비정규화를 시키면 비정규화 테이블에 대한 데이터 변경 및 관리 등의 비용이 들어갑니다.


5. index 강제하기.


다음의 쿼리가 모두 잘 돌아가기 위해서 필요한 인덱스는 어떤 것들이 있을까요? (msg 테이블의 데이터는 무진장 많다고 칩시다.)

select * from msg where writer = '홍길동' order by msg_id desc limit 10;

select * from msg where writer = '홍길동' and location = '서울' order by msg_id desc  limit 10 ;

첫번째는 ( writer , msg_id ) , 두번째는 (writer, location, msg_id) 입니다.


그러면 아래 쿼리를 돌렸을 때 위 두개 중 어떤 인덱스를 타는 게 좋을까요?

select * from msg where writer = '홍길동' and location != '서울' order by msg_id desc  limit 10 ;

이건 분포도를 따져야 합니다. location에 대한 분포가 적당히 고르게 되어있다고 치면, (writer, msg_id) 로 뽑는 게 좋습니다. (writer, msg_id) 인덱스를 탈 경우 인덱스를 타면서 데이터를 가져와서 location이 서울이 아닌지 확인하면서 데이터를 뽑습니다. 아주 깔끔한 인덱스는 아니지만 ordering을 따로 할 필요가 없기 때문에 크게 느리지는 않을 겁니다.

만약 location에 대한 분포가 서울에 집중되어있다면 (writer, msg_id) 인덱스를 타는 것은 좋지 않습니다. location이 서울인지 아닌지를 확인하기 위해서는 테이블을 다 뒤지기 때문입니다. 그럴 때는 차라리 (writer, location , msg_id) 인덱스에서 앞에 두 개만 써먹고 ordering을 하는 게 나을 수도 있습니다.

그러면 두 개의 인덱스가 잘 잡혀있고, 세번째 쿼리를 실행하라고 컴퓨터한테 시키면 컴퓨터는 어떤 인덱스를 선택할까요? 까봐야 알겠지만 경우에 따라 엉뚱한 짓을 하기도 합니다.

위와 같이 두 가지 이상의 가능한 인덱스가 있을 때 어떤 인덱스가 선택될 지를 컴퓨터에게 위임하지 마십시오. 종종 잘못된 선택을 합니다. mysql 의 경우 use index나 force index 등을 통해서 인덱스를 강제하는 게 필요할 때가 있습니다.(오라클 등 타 디비도 있습니다. 오라클은 만져본 지 하도 오래되서 사용법은 기억이 안나네요.ㅜㅜ ) explain 등으로 확인을 했더라도 파라미터가 바뀌면서(ex> location != '대전' 으로 바꾼다거나.. )  다른 인덱스를 타기도 합니다.

인덱스를 지정하지 않을 경우에 두번째 쿼리 ( writer, location, msg_id  인덱스가 바람직해보이는 거) 도 첫번째 인덱스 (writer, msg_id) 를 타기고 합니다. 조금이라도 혼란의 가능성이 있다면 인덱스를 강제해버리는 게 좋습니다.

일단 explain 으로 봐서 possible_keys 가 여러 개 나오면 index 강제하는 게 젤 편합니다.

by 삼실청년 | 2012/10/06 00:04 | 컴터질~ | 트랙백(1) | 덧글(3)

◀ 이전 페이지          다음 페이지 ▶