HashMapIteration.java
0.00MB

필요한 기능 

1. Iterator : Iterator는 Java Collection Framework에 속하는 컬렉션 객체들(Map과 List interface를 상속받은 객체들)에 저장되어있는 value를 읽어오는 방법을 표준화하기위한 interface이다. 

2. entrySet & keySet : HashMap의 Loop를 실행하고 데이터를 사용하기 위해서는 HashMap이 가지는 Key를 알 필요가 있다. Key를 알아내기 위한 수단으로 사용할 method들이다.

 

비교할 대상

실행 시간을 비교할 조건은 Iterator의 사용 유무와 HashMap의 Key 조회 방법, 그리고 HashMap과 LinkedHashMap간의 차이를 비교할 것이다. 

그리고 하는 김에 ArrayList의 loop도 비교해볼 것이다. 

 

테스트 환경

jdk 1.8

 

테스트 방법 

간단하게 HashMap에 숫자를 0~1000000까지 넣고 반복문을 돌릴 것이다.

 

사용할 변수 선언 및 초기화 

1
2
3
4
5
6
7
8
9
10
11
12
13
    static HashMap<String,Integer> testMap = new HashMap<String,Integer>();
    static LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<String, Integer>();
    static ArrayList<Integer> arrayList = new ArrayList<Integer>();
 
    static
    {
        for(int i=0; i< 1000000; i++)
        {
            testMap.put("key_" + i, i);
            linkedHashMap.put("key_" + i, i);
            arrayList.add(i);
        }
    }
 
 

 

HashMap

[HashMap, entrySet]

1
2
3
4
5
6
startTime = Calendar.getInstance().getTimeInMillis();
for (Map.Entry<String,Integer> entry : testMap.entrySet()) {
    entry.getKey();
    entry.getValue();
}
System.out.println("    1 Using entrySet() in for-each loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[HashMap, keySet]

1
2
3
4
5
startTime = Calendar.getInstance().getTimeInMillis();
for (String key : testMap.keySet()) {
    testMap.get(key);
}
System.out.println("    2 Using keySet() in for-each loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[HashMap, entrySet, Iterator]

1
2
3
4
5
6
7
8
9
10
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<Map.Entry<String,Integer>> itr1 = testMap.entrySet().iterator();
while(itr1.hasNext())
{
    Map.Entry<String,Integer> entry = itr1.next();
    entry.getKey();
    entry.getValue();
}
System.out.println("HashMap iterator loop");
System.out.println("    3 Using entrySet() and iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[HashMap, keySet, Iterator]

1
2
3
4
5
6
7
8
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<String> itr2 = testMap.keySet().iterator();
while(itr2.hasNext())
{
    String key = itr2.next();
    testMap.get(key);
}
System.out.println("    4 Using keySet() and iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

 

LinkedHashMap

[LinkedHashMap, entrySet]

1
2
3
4
5
6
startTime = Calendar.getInstance().getTimeInMillis();
for (Map.Entry<String,Integer> entry : linkedHashMap.entrySet()) {
    entry.getKey();
    entry.getValue();
}
System.out.println("    1 LinkedHashMap entrySet() loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[LinkedHashMap, keySet]

1
2
3
4
5
startTime = Calendar.getInstance().getTimeInMillis();
for (String key : linkedHashMap.keySet()) {
    linkedHashMap.get(key);
}
System.out.println("    2 LinkedHashMap keySet() loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[LinkedHashMap, entrySet, Iterator]

1
2
3
4
5
6
7
8
9
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<Map.Entry <String,Integer>> itr4 = linkedHashMap.entrySet().iterator();
while(itr4.hasNext())
{
    Map.Entry<String,Integer> entry = itr4.next();
    entry.getKey();
    entry.getValue();
}
System.out.println("    3 LinkedHashMap entrySet() iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[LinkedHashMap, keySet, Iterator]

1
2
3
4
5
6
7
8
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<String> itr3 = linkedHashMap.keySet().iterator();
while(itr3.hasNext())
{
    String key = itr3.next();
    linkedHashMap.get(key);
}
System.out.println("    4 LinkedHashMap keySet() iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

 

ArrayList

[ArrayList]

1
2
3
4
5
startTime = Calendar.getInstance().getTimeInMillis();
for(int i = 0 ; i < arrayList.size() ; i ++) {
    arrayList.get(i);
}
System.out.println("    1 ArrayList loop : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

[ArrayList, Iterator]

1
2
3
4
5
6
startTime = Calendar.getInstance().getTimeInMillis();
Iterator<Integer> itr5 = arrayList.iterator();
while(itr5.hasNext()) {
    int val = itr5.next();
}
System.out.println("    2 ArrayList iterator : " + (Calendar.getInstance().getTimeInMillis() - startTime));
 

 

실행 시간 결과 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HashMap just loop
    1 Using entrySet() in for-each loop : 65
    2 Using keySet() in for-each loop : 81
==================================
HashMap iterator loop
    3 Using entrySet() and iterator : 52
    4 Using keySet() and iterator : 60
==================================
LinkedHashMap just loop
    1 LinkedHashMap entrySet() loop : 45
    2 LinkedHashMap keySet() loop : 64
==================================
LinkedHashMap iterator loop
    3 LinkedHashMap entrySet() iterator : 43
    4 LinkedHashMap keySet() iterator : 56
==================================
    1 ArrayList loop : 22
    2 ArrayList iterator : 28
 

 

정리 

1. HashMap의 경우, entrySet이 keySet으로 순환하는 것 보다 더 빠르다. 그리고 Iterator로 순환하는 것이 아닌 것 보다 더 빠르다.

가장 빠른 것 : Iterator entrySet loop

 

2. LinkedHashMap의 경우, HashMap에 Link가 추가되어 있어 전체적으로 HashMap보다 실행 속도가 더 빠르다. 그리고 나머지 조건은 HashMap과 동일하게 entrySet이 keySet보다 더 빠른 것을 확인할 수 있다. 

가장 빠른 것 : Iterator entrySet loop

 

3. ArrayList의 경우, 이미 indexing이 되어 있기 때문에 별도의 Iterator 객체를 만들어 순환하는 것보다 그냥 size를 가지고 순환하는 것이 더 빠르다. 

가장 빠른 것 : non-Iterator loop

사용 환경 

Python(3.8) + Redis

python의 Redis 함수명과 Redis-cli에서 사용하는 명령어가 거의 유사하여 Python으로 진행하였다. 

 

사용할 데이터 타입 6가지 (몇 개 더 있을건데 사용할 법한 애들만)

String, Set, Hash ,SortedSet, List, Bitmap

 

1. String

조회 : get

입력 : set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import redis
 
= redis.StrictRedis(host="localhost", port=6379, db=0)
key = "String key"
 
def printFormat(key):
    result = r.get(key)
    print("=============================")
    print('type : {}'.format(type(result)))
    print('result : {}'.format(result))
 
value = "String value"
r.set(key, value)
printFormat(key)
 
value = "String value 2"
r.set(key, value)
printFormat(key)
 
 

하나의 키에 하나의 String value가 입력된다. 

예시 코드에서 같은 키에 Value를 두 번 입력하게 되면, 해당 키의 값이 교체된다. 

1
2
3
4
5
6
=============================
type : <class 'bytes'>
result : b'String value'
=============================
type : <class 'bytes'>
result : b'String value 2'
 

위와 같이 출력해보면 교체된 값을 확인할 수 있고,

Redis 전체 조회를 했을 때의 최종 결과 값이 마지막 값으로 저장된 것을 알 수 있다. 

 

2. Set

조회 : smembers

입력 : sadd

삭제 : srem

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
import redis
 
= redis.StrictRedis(host="localhost", port=6379, db=0)
key = "Set key"
 
def printFormat(key):
    result = r.smembers(key)
    print("=============================")
    print('type : {}'.format(type(result)))
    print('result : {}'.format(result))
 
# 입력
value = 1
r.sadd(key, value)
printFormat(key)
 
# 입력
value = 2
r.sadd(key, value)
printFormat(key)
 
# 중복 입력 : 저장되지 않음
value = 2
r.sadd(key, value)
printFormat(key)
 
# 입력
value = 3
r.sadd(key, value)
printFormat(key)
 
# 삭제 : set value 중 값이 3인 것을 삭제 -> 삭제됨
r.srem(key, 3)
printFormat(key)
 
# 삭제 : set value 중 없는 것을 삭제 시도 -> 별 이상 없음
r.srem(key, 4)
printFormat(key)
 
 

Set 자료구조의 특징에 맞게 하나의 Key에 Value가 중복 없이 저장된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
=============================
type : <class 'set'>
result : {b'2', b'1'}
=============================
type : <class 'set'>
result : {b'2', b'1'}
=============================
type : <class 'set'>
result : {b'2', b'1'}
=============================
type : <class 'set'>
result : {b'3', b'2', b'1'}
=============================
type : <class 'set'>
result : {b'2', b'1'}
=============================
type : <class 'set'>
result : {b'2', b'1'}
 

Value 2를 여러번 입력했지만 한번만 저장된 것을 확인할 수 있다. 

없는 값을 삭제 시도해도 결과에 영향은 없다.

 

3. Hash

조회 : hset

입력 : hget

삭제 : hdel

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
import redis
 
= redis.StrictRedis(host="localhost", port=6379, db=0)
key = "Hash key"
 
def printFormat(key, hashKey):
    result = r.hget(key, hashKey)
    print("=============================")
    print('type : {}'.format(type(result)))
    print('result : {}'.format(result))
 
r.hset(key, "a""A")
printFormat(key, "a")
 
r.hset(key, "b""B")
printFormat(key, "b")
 
# 값 B가 들어있던 키 b에 F를 입력한다. -> Hash의 규칙에 따라 값이 교체된다. 
r.hset(key, "b""F")
printFormat(key, "b")
 
r.hset(key, "c""C")
printFormat(key, "c")
 
r.hset(key, "d""D")
printFormat(key, "d")
 
 

하나의 Key 아래 [Key:Value] 구조로 데이터를 저장할 수 있다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
=============================
type : <class 'bytes'>
result : b'A'
=============================
type : <class 'bytes'>
result : b'B'
=============================
type : <class 'bytes'>
result : b'F'
=============================
type : <class 'bytes'>
result : b'C'
=============================
type : <class 'bytes'>
result : b'D'
 

조회할 때, Redis Key와 Redis Key의 Value내에서 사용한 Key를 사용하여 Value 하나 만을 가져올 수 있다. 

위와 같이 Key와 Value 쌍으로 저장되는 것을 확인 할 수 있다. 

 

 

4. SortedSet

조회 : zrange

입력 : zadd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import redis
 
= redis.StrictRedis(host="localhost", port=6379, db=0)
key = "Sorted Set key"
 
# sorted set 은 key와 score의 구조를 갖는다.
value = {}
value['f'= 1
value['c'= 4
value['e'= 2
value['a'= 6
value['d'= 3
value['b'= 5
 
r.zadd(key, value)
 
print(r.zrange(key, 05))
 
listResult = []
listResult = r.zrange(key, 05)
 
for idx in listResult:
    print(idx)
 
 

SortedSet의 경우, 파이썬 버전마다 입력 방식이 다르다고 한다. *작성 버전은 3.8이다. 

입력 시, 위 코드처럼 사용할 값이 Dict의 Key이고, 정렬 index가 Dict의 Value가 된다. 

index로 사용되어야 하기 때문에, int값이 들어가야 한다. 

1
2
3
4
5
6
7
[b'f', b'e', b'd', b'c', b'b', b'a']
b'f'
b'e'
b'd'
b'c'
b'b'
b'a'
 
 

입력한 key의 순서는 f,c,e,a,d,b 였지만, index의 오름차순 순서로 인해 조회했을 때의 결과가 f,e,d,c,b,a인 것을 확인할 수 있다. 

Redis 저장 결과 또한 그런 것을 확인이 가능하다.

 

5. List

조회 : lrange, lindex

입력 : rpush

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import redis
 
= redis.StrictRedis(host="localhost", port=6379, db=0)
key = "List key"
 
r.rpush(key, 1)
r.rpush(key, 2)
r.rpush(key, 3)
r.rpush(key, 4)
r.rpush(key, 5)
r.rpush(key, 6)
 
listResult = r.lrange(key, 0-1)
print(listResult)
 
listResult2 = r.lindex(key, 1)
print(listResult2)
 
 

Redis의 List는 입력하는대로 중복을 허용하며 저장된다. 

한 번 실행하면, 

1
2
[b'1', b'2', b'3', b'4', b'5', b'6']
b'2'
 
 

한 번 더 실행하면, 

1
2
[b'1', b'2', b'3', b'4', b'5', b'6', b'1', b'2', b'3', b'4', b'5', b'6']
b'2'
 
 

이와 같이 두 번 다 저장된 것을 확인할 수 있다. 

 

6. Bitmap

조회 : getbit

입력 : setbit

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
# boolean 값을 저장할 때 사용된다 .
# 적은 용량으로 많은 데이터의 상태값을 유지시킬 수 있다.
# 512mb 메모리로 40억개의 boolean 상태값을 유지할 수 있다. (라고 한다.)
import redis
 
= redis.StrictRedis(host="localhost", port=6379, db=0)
key = "Bitmap key"
 
r.setbit(key, 11)
r.setbit(key, 20)
r.setbit(key, 30)
r.setbit(key, 40)
r.setbit(key, 51)
r.setbit(key, 61)
 
print(r.getbit(key, 1))
print(r.getbit(key, 2))
print(r.getbit(key, 3))
print(r.getbit(key, 4))
print(r.getbit(key, 5))
print(r.getbit(key, 6))
 
r.setbit(key, 10)
print("--------------")
print(r.getbit(key, 1))
print(r.getbit(key, 2))
print(r.getbit(key, 3))
print(r.getbit(key, 4))
print(r.getbit(key, 5))
print(r.getbit(key, 6))
 
 

Hash처럼 Redis Key 아래 [key, value]단위로 데이터를 저장하게 된다. 0,1만 저장되는 저용량 Hash라고 생각하면 될 듯 하다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
1
0
0
0
1
1
--------------
0
0
0
0
1
1
 

Key 1 의 값이 교체된 것을 알 수 있다. 

이건 이상하게 Redis 툴에서 값을 읽지는 못한다. 

이유가 있나? Redis Key는 조회가 되는데 Value 조회가 안된다.

'공부 - 개념, 철학 > Redis' 카테고리의 다른 글

Redis mget과 pipeline 차이점  (0) 2021.07.06
[Redis] Redis 란  (0) 2020.02.24

TDD란?

Test Driven Development의 줄임말이다. 

Agile 방법론을 실천하기 위한 여러 개발론 중 하나다. 

테스트가 개발의 축이 된다. 

 

어떻게?

Agile 방법론 자체가 동적이고 능동적으로 기획과 개발의 선을 넘나들며 개발을 진행하는 방식이다. 

정형화된 [기획->개발->테스트->수정]의 과정을 가진 Water Fall 방법론이나 Bohem's spiral 방법론과는 다른

Agile만의 장점이자 단점으로 꼽히는 점이다. 

 

그 중에서도 TDD는 매우 짧은 개발 사이클을 가지는 개발론이다.

 

보편적으로 소프트웨어 개발을 진행할 때, 기획을 받아 기능을 구현한 후 해당 기능을 대상으로 테스트를 진행한다. 

 

TDD는 이것을 역순으로 진행한다고 생각하면 쉽다. 

 

과정 1

1. 먼저 테스트 케이스(입력 인자와 반환 결과)를 구성한다. 

2. 그 후 테스트 케이스를 성공시키기 위한 소스 코드를 개발한다. 

3. 마지막으로 작성한 코드를 표준에 맞게 다듬는다.(리팩토링)

 

하지만 이것을 구현 후 테스트 하던 작업 순서에서 바로 적용하면 바로 어려움에 부딪힐 것이다.

 

서비스 기획 요구 사항을 구현하기 위해 개발자는 여러 기능을 만들어 하나의 함수로 묶어놓게 되고 그 함수들을 또 하나로 묶게 되고 이것을 반복할 것이다. 그리고 이것을 통째로 테스트를 돌리게 된다.

물론 그 결과는 참담할 것이다. 에러는 발생할 것이고, 발생 지점을 찾기 위해 그 긴 코드를 여러 번 정독해야 한다. 

 

과정 2

이것을 예방하기 위해서 필요한 것은 무슨 훌륭한 기술이 아닌 개발 방향과 태도다. 

그리고 그 방법은 과정 1의 절차 크게 차이 나지 않는다. 

 

1. 먼저 테스트 케이스(입력 인자와 반환 결과)를 구성한다.

2. 그 후 테스트 케이스를 성공시키기 위한 돌아가기만 하는 최대한 짧고 간단한 소스 코드를 개발한다. 

3. 마지막으로 작성한 코드를 표준에 맞게 다듬는다.(예외처리 + 리팩토링)

 

개발자가 최대한 짧고 간단하게 개발해 소스 코드 파악과 테스트 진행을 수월하게 하는 것이 전제이다. 

짧고 간단한 코드를 구현하여 기능의 분할 및 객체화를 우선시 한다. 

또한 이렇게 되기 위해서는 기능 기획의 규모를 작게 해야한다. 

 

[작고 단순하고 명확한 기능 기획 -> 테스트 케이스 작성 -> 짧고 간단한 구현 -> 검증] 으로 전체 개발 사이클을 매우 짧게 반복하는 것이다. 

 

효과

짧은 코드를 구현하여 각 기능들이 명확해진다. 

테스트 케이스가 작성되어 있기 때문에, 기존 소스 코드를 수정한다 하더라도 동일한 테스트와 결과를 얻을 수 있다. 

최종적으로 소스 코드의 검증 시간이 줄고 안정적으로 사용할 수 있다는 것이다. 

 

자신이 개발한 기능이 아니지만 수정을 해야하는 경우가 빈번한데, 이럴 때 해당 기능이 이미 테스트 케이스를 가지고 있다면 안정적으로 수정할 수 있을 것이다. 

 

결론

개발 방법론이라는 것이 항상 그렇듯이, 개발만이 아니라 기획에서도 도와줘야 하기 때문에 처음부터 적용되어 진행된 것이 아니라면 사실 100% 활용하기 힘들다. 

하지만 TDD의 경우, 기획에서 단순 명확한 기획을 주지 않는다고 하여도 개발에서만이라도 적용할 여지가 충분히 있다. 기획을 분할하여 내부 기능들을 만들고, 그것에 대한 테스트 케이스를 만들어, 각 기능 함수별로 테스트를 진행하면 충분히 그 효과를 얻을 수 있다는 것이다. 

Java의 JUnit과 같은 함수를 대상으로 단위 테스트를 진행한다는 것은 큰 도움이 될 수 있다.

 

단위 테스팅 툴의 적용은 오래 걸리거나 힘들지도 않고, 상용화에 영향을 주지도 않는다. 만약 사용하고 있지 않다면, 남는 시간에 써보는 것이 좋을 거라고 생각한다. 

일하다가 답답해서 써본 결과, 썩 나쁘지 않다. 좋으면 좋았지.

1. JUnit이란?

Java에서 사용하는 기능 단위별 테스트를 위한 라이브러리다.

* Java를 배우는 학부생들이라면 한번즘 접했을거라 생각된다.

 

2. 중요한가?

궁극적인 목표를 보고 생각한다면, JUnit과 유사한 기능들을 가진 툴/라이브러리들이 중요한 것은 아니다. 

중요한 것은 소프트웨어 개발 과정 중에 정리된 케이스로 테스트를 진행한다는 단계가 있다는 것이다.

문서로 잘 정리할 귀찮음을 감수할 자신이 있거나 혹은 자기 입맛에 맞는 테스팅 툴을 개발할 수 있다면 그걸로 상관없다.

* 자바로 개발하다가 파이썬으로 개발할 일이 생긴다면 어차피 JUnit은 쓸 수도 없다. 

 

3. 사용 방식 

테스트할 기능(method)를 대상으로 "예상되는 결과""method 호출 시 실제 반환 결과"를 비교하는 것이다. 

* 단위 테스팅 툴을 사용하지 않는 기업에서는 기능을 통째로 실행하거나 하위 기능을 복붙해서 실행하는 방식으로 많이 개발한다. 당장 내가 다녀본 회사나 주변 지인들이 있는 곳 대부분이 그렇게 하고 있다. (규모가 있거나 여유가 있는 곳이 아니면 대부분 이런 식으로 진행할 것이다.)

 

4. 환경 및 dependency

Spring boot + gradle

org.springframework.boot:spring-boot-starter-test

내가 주로 개발하는 환경이 Spring이어서 이렇게 설정한 것이지 JUnit만 사용할 거라면 JUnit라이브러리만 import하면 된다.

 

5. Spring boot 구성도

Spring boot 구성

Spring 환경에서 개발을 하다보면 src하위에 자동으로 생성되는 test 폴더를 본 적이 있을 것이다. 

개발된 기능의 test용 코드를 관리하라고 주는 별도의 경로이니, Spring에서 진행중이라면 활용하자.

 

테스트의 대상은 servicelogic/tdd/Service/impl에 위치한 TddServiceImpl이고, 

테스트 실행은 test폴더 하위에 있는 servicelogic/tdd/controller에 위치한 TddControllerTest이다. 

실행 방법은 TddControllerTest에서 TddServiceImpl의 method를 호출하여 TddControllerTest에 설정된 예상된 결과와 비교하는 것이다. 

 

6. 소스 코드 

A. TddServiceImpl : 서비스 로직이 위치할 코드

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class TddServiceImpl implements TddService {
 
    @Override
    public List selectTest() throws Exception {
 
        System.out.println("TddServiceImpl > selectTest");
 
        List<Integer> temp = new ArrayList<Integer>();
        temp.add(1);
        temp.add(2);
        temp.add(3);
        temp.add(4);
        temp.add(5);
 
        return temp;
    }
}
 
 
 

테스트 용 결과값으로 [1,2,3,4,5] 가 저장된 List를 반환해준다. 

 

B. TddControllerTest : 서비스 로직을 테스트할 코드 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class TddControllerTest {
 
    @Autowired
    TddService tddService;
 
    @Test
    public void test_Call() throws Exception {
        List testResult = new ArrayList();
        testResult.add(1);
        testResult.add(2);
        testResult.add(3);
        testResult.add(4);
        testResult.add(5);
        assertEquals(testResult, tddService.selectTest());
    }
    
}
 
 

TddServiceImpl의 반환값과 비교할 값을 설정한 후, 대상 method를 호출하여 비교한다. 

 

17번째 줄의 assertEquals 함수가 JUnit에서 제공되는 비교 함수이다. 

만약 반환값의 특정 키값만 확인하고 싶을 때와 같이 값 이외의 다른 것을 비교하고 싶다면, 테스팅 코드 내에 원하는 조건을 추가해 확인할 수 있다.

또한 같은 코드에 여러 테스팅 호출 함수를 작성하더라도, 원하는 method만을 별도로 실행할 수 도 있다. (이게 단위 테스트 강점이 아닐까)

 

7. 정리

서버 API를 개발할 때, 하나의 API를 대상으로 개발할수록 점점 기능이 쌓여간다.

API내에 백엔드에서 사용할 데이터가 발생하고 저장되며 별도의 처리를 하게 된다. 

이렇게 쌓여갈 때 마다 테스트하기 점점 힘들어지고 소스코드 몇 줄 추가하여 거기만 테스트 하려고 하면 그 전에 구현된 전체 로직을 계속 반복 실행하며 쌓여가는 로그를 쳐다보고 있어야 한다. 

학부생 때 처음이자 마지막으로 써봤던 JUnit을 이제와서 찾게 된 것은 사실 실무에서 이러고 있는게 너무 귀찮고 짜증나서이다. 

가끔 API 호출 조건 자체가 까다로울 때가 있는데, 그럴 땐 진짜 환장한다. (호출 시 파라미터가 암호화가 되어있다던가)

 

Agile 개발론 중에는 TDD(Test Driven Development - 테스트 주도적 개발)라는 것이 있다. (사실 아는게 이거밖에 없다.)

TDD를 지향하기 위해서 테스트 케이스를 정형화하고 테스트 실행에 쉽게 접근할 수 있어야 하는 법인데, 그 대표적인 방식이 바로 단위 테스트이다. 

 

학생땐 이걸 왜 배우나 했지... 어허허 

학생 때 배우는 거 쓸모 없다는 사람 많지만 절대 그렇지 않으니까 공부 열심히 하자. 

가장 좋은건 원문을 보는 것이다. 

https://redis.io/topics/introduction

 

Introduction to Redis – Redis

*Introduction to Redis Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker. It supports data structures such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps, hyperlog

redis.io

 

Redis의 핵심

 

1. In-Memory data structure store : 메모리에서 데이터 구조(자료구조)를 저장하는 소프트웨어

2. 주로 데이터베이스, 캐시, 메시지 브로커로 사용된다. 

3. BSD 라이센스의 오픈 소스이다. ( *참고 : https://ko.wikipedia.org/wiki/BSD_%ED%97%88%EA%B0%80%EC%84%9C )

 

지원하는 자료 구조 

 

1. String

2. Set

3. Hash

4. Sorted Set (Range 쿼리 포함)

5. List

6. Bitmap

7. Hyerloglogs ( 정의 : 매우 적은 메모리로 집합의 원소 개수를 추정할 수 있는 방법, *참고 : https://d2.naver.com/helloworld/711301 , 잘 모르겠다.)

8. Geospatial indexes with radius queries and streams 

  -  8번의 경우, 4번의 Range 쿼리와는 개념은 같지만 성질이 다르다. 반경 쿼리(radius query)라는 말은 데이터 군집을 조회한다는 개념일 것이다. 완전히 이해하고 사용하려면 군집 조회의 바탕이 되는 N 차원 데이터 분포를 이해해야 할 필요가 있어보인다. 학부생이었을 때 볼 수 있는 가장 유사한 구현 로직으로는 알고리즘 수업이나 데이터 마이닝 수업에서 자주 나오는 K-means, K-mid(맞나?) 알고리즘과 같은 데이터 군집 및 센트로이드(기준점) 생성을 하는 알고리즘들이 있을 것이다. 근데 정작 캐시, 메시지 브로커로 사용될 때는 쓸모없어 보인다. (그리고 그런 거 공부할 거면 대학원 갔어야지...)

 

기능

1. Persistence

DataSet을 Disk로 Dumping하거나, 각 command를 log로 저장하여 데이터가 유지될 수 있도록 한다. 

In-memory의 데이터 휘발성(기기의 물리적 종료 시 데이터가 사라지는 현상)을 막아준다.

 

Snapshot : Dumping 방식이다. DB 파일 자체의 복사본을 만들어 Disk에 저장하는 방식이다. 

  -  save 명령 : Blocking 방식. 실행 시, Redis를 블록시켜 데이터 관리를 못하게 막은 후 실행된다. 

  -  [Redis 권고 명령어] bgsave 명령 : background save의 줄임말. Non-Blocking 방식. 자식 프로세스를 생성 후 백그라운드에서 스냅샷을 생성한다. 

  -  Snapshot은 재실행 시 그대로 불러와서 저장하면 되지만, 저장된 시점만의 데이터가 복구되기 때문에 저장 시점 이후의 데이터는 복구하지 못한다.

 

AOF : Append Only File의 줄임말. 간단하게 말하면 RDB의 Transaction log를 저장하는 방식. Redis의 write/update command log를 기록한다. 서버가 재시작될 때, 순차적으로 재실행하여 데이터를 복구한다. 

  -  AOF의 경우 모든 시점에 대한 데이터를 복구할 수 있지만, 재실행할 때 마다 데이터 입력/수정 로그를 실행하여 복구하기 때문에 복구 시간이 느려진다. 

 

Redis의 Persistence 규격은 [주기적인 Snapshot 생성(default 15분) + Snapshot 생성 시점 이후의 발생 데이터는 AOF로 저장]이다. 

2. Master-Slave Asyncronous replication

(주) 데이터 베이스 - (부) 데이터 베이스로 비동기 복제가 가능하다. 

HA(High Availability - 장기간동안 지속적 운영이 가능함을 의미)를 가능하게 한다. 

3. Transaction

RDB를 공부하다보면 Transaction log의 commit 시점을 따라 과거 시점으로 복구시킬 수 있다는 것을 알 것이다. 1번의 command를 log로 저장한다는 것이 transaction log와 동일한 역할을 하는 log를 저장한다는 의미이다. 

4. pub/sub

Publish/Subscript 구독형 사용이 가능하다. Messaging 구조가 가능하다는 의미로, 메시지 브로커로서 사용될 때 사용하는 기능으로 보인다. 

5. Lua scripting

루아 스크립트를 사용하여 Redis의 직접 디버깅이 가능하다.

6. Keys with a limited time-to-live

만료 시간을 설정할 수 있다. 캐시로 사용할 때, 시간이 지나면 의미가 없어지는 데이터의 경우 만료시간을 설정하여 최신화를 시켜줄 수 있다. 실무에서도 자주 사용하는 방법이다. 

7. LRU eviction of keys

캐시로 사용할 때 신규 데이터의 유입을 위해 과거 데이터 삭제 방법을 기본 설정으로 두게 되면, LRU(Least Frequently Used 가장 적게 사용된 데이터를 축출하는 알고리즘) 방식으로 삭제한다. 이런식으로 쓸 거 아니면 서버 로직에서 주기적으로 갱신을 시켜주든지 하면 되는거라 딱히 쓸모는 없어보인다. 되려 캐시 데이터는 의도적으로 갱신/수정하는 경우가 대부분이라서 그런가보다 하면 될 것 같다. 

8. Automatic failover

Redis의 Clustering을 사용한 기능 중 하나이다. 분산 Redis 서버 하나가 죽으면, 하위이 Slave를 Master로 승격시켜 기능이 정지하는 것을 방지한다. 

Redis Cluster HA에 더하여 Sharding을 지원한다. (Sentinel과는 완전히 다른 형태의 솔루션이라고 한다.)

DataSet을 여러 node에 나누어 저장을 시키고(Sharding), 자체적인 프로토콜(protocol)을 통해 node to node로 통신을 한다.

ex) RDB로 예를 들면, 하나의 테이블을 index를 기준으로 A인덱스 구간과 B인덱스 구간을 따로 저장하고 각 구간을 node로 연결한다.

각 node는 N대의 인스턴스로 구성이 가능하며, 1대의 Master와 N-1대의 Slave로 구성된다. 

참고 

* Redis의 개발단계에서 Linux와 OS X를 주 대상으로 테스트가 진행되고, 권장 실행 환경은 Linux이다. 

* 웹 서버 개발시에는 Redis를 성능 향상을 위한 캐싱으로 사용하는 경우가 많다. 

* 웹 서버의 경우에 그렇다는 것이지, 채팅 서버와 같은 실시간 서버의 경우 메인 DB로 사용할 수도 있다. Disk 조회 시간보다 메모리 조회 시간이 훨씬 빠르면서, 데이터 영속성(Persistence)가 보장되기 때문에 가능하다. 

 

 

'공부 - 개념, 철학 > Redis' 카테고리의 다른 글

Redis mget과 pipeline 차이점  (0) 2021.07.06
[Redis] Python으로 Redis 사용하기  (0) 2020.04.04

원문 : https://www.infoworld.com/article/3063161/why-redis-beats-memcached-for-caching.html

 

Why Redis beats Memcached for caching

Memcached is sometimes more efficient, but Redis is almost always the better choice

www.infoworld.com

17년도에 작성된 비교 글이기도 하고 Redis 찬사로만 도배되어있어서 좀 그렇기는 한데 Redis 관련 내용이 많아서 번역해봤다. 

맘에는 안든다. 

회사에서 Memcached에서 Redis로 바꾼다고 해서 조사차 했다. 

 

왜 캐싱 기능에서 Redis가 Memcached보다 유리한가. 

Memcached가 어쩌다 더 효율적이긴 하지만 Redis가 거의 항상 더 좋은 선택이다. 


Memcached를 쓸까 Redis를 쓸까? 현대 DB 기반 웹앱의 성능을 더 끌어올리기 위한 토론에서 거의 항상 떠오르는 질문(주제)이다. 성능 향상이 필요할 때 첫 번째 방안으로 사용되는 것이 캐싱이고, Memcached나 Redis난 대표적으로 가장 처음 언급(사용)된다. 

이렇게 잘 알려진 캐시 엔진은 많은 공통점을 가지지만, 또한 큰 차이점이 있다. 더 최신 캐시 소프트웨어이면서 더 기능이 많은 Redis가 대부분 더 좋은 선택이다. 

 

Redis vs Memcached


공통점부터 시작하자. Redis가 자료구조 단위 저장에서 더 정밀하다고 언급은 되지만, 두 방식 모두 메모리 기반에 key-value 단위 데이터 저장을 지원한다. 둘 모두 NoSQL 환경의 데이터 관리 솔루션에 속하고 key-value 데이터 모델을 기반으로 한다.(같은 내용을 반복하다니 별로 잘쓴 글은 아닌 것 같다. 라떼는 이렇게 쓰면 교수님한테 찢겼다.) 둘 모두 캐싱 계층에서 아주 유용하게 만들기 위해 RAM에 모든 데이터를 저장한다. 성능 면에서보면 두 방식은 매우 비슷하면서도 처리량과 지연율에서 거의 동일한 특징들을 보여준다. 

Memcached와 Redis 모두 어느 정도 안정됐고 매우 인기있는 오픈 소스 프로젝트다. Memcached는 LiveJournal이란 사이트를 위해 2003년에 Brad Fitzpatrick이 개발했다. 그 이후로 Memcached는 Perl로 개발되었던 오리지널 버전에서 C로 재작성되었고 공용(public domain)으로 풀리면서 현대 웹앱의 초석이 되었다. 현재 Memcached의 발전은 새로운 기능을 추가하는 것보다 안정성과 최적화에 초점을 두고 있다. 

Redis는 2009년에 Salvatore Sanfilippo에 의해 만들어졌고, 그는 오늘날의 프로젝트의 lead developer(programmer)가 되었다(...번역할수록 내가 틀리게 번역한건지 글이 이상한건지 의문이다. 의역하면 이게 맞는거같은데?). Redis는 Memcached를 사용하면서 얻은 교훈을 바탕으로 만들어져서 가끔씩은 "뽕 맞은 Memcached"라고도 불린다. Redis는 Memcached보다 더 많은 기능들을 가지고 있고 때문에 더 강력하고 유연하다. 

많은 기업에서 사용되고 셀 수 없이 많은 mission-critical 생산 환경에서, 둘 모두 생각할 수 있는 모든 개발 언어를 지원하고 개발자들을 위한 많은 패키지들에 내장되어 있다. 사실 Memcached나 Redis를 내장 지원하지 않는 웹 스택이 더 드물다. 

왜 이렇게 인기가 많을까? 얘네들은 극도로 효율적일 뿐만 아니라 상대적으로 매우 간단하다. 개발자에게 이 둘을 사용하는 것은 쉬운 작업이다. 설치하는에 몇 분밖에 안걸리고 앱과 함께 작동시킬 수 있다. 적은 시간 투자와 노력으로 바로 성능의 극적인 효과를 가질 수 있다. 마법과도 같이 간단한 솔루션으로 큰 이익을 얻을 수 있다. 

 

Memcached를 사용할 때


Memcached는 HTML 코드 파편(fragment)와 같은 상대적으로 작고 정적인 데이터를 캐싱할 때 선호된다. Redis만큼은 아니지만 Memcached의 내부 메모리 관리는  메타 데이터에 상대적으로 메모리 리소스를 덜 소모하기 때문에 간단한 유즈 케이스(use case)에 더 효과적이다. Memcached에서 지원되는 유일한 데이터 형인 Strings는 추가적인 처리를 요구하지 않기 때문에 읽기(read)만 될 데이터를 저장하는데 이상적이다. 

항상 더 많은 저장 공간을 필요로 하는 큰 데이터 셋은 자주 직렬화된(serialized) 데이터에 포함한다. Memcached 직렬화 데이터 저장에 제한된 반면, Redis에 데이터 구조는 직렬화 처리 시간을 줄이면서 네이티브 데이터를 그대로 저장할 수 있다. 

Memcached가 Redis보다 이점을 얻는 두번째 시나리오는 스케일링(scaling)에 있다. Memcached는 멀티 스레드를 지원하기 때문에, 캐시된 데이터의 일부 혹은 전체를 잃으면서 더 많은 연산 자원으로 스케일 업(scale up)을 쉽게 할 수 있다. 대부분 싱글 스레드인 Redis는 데이터의 손실 없이 클러스터링을 통해 수평적으로 스케일링을 할 수 있다. 클러스트링은 효과적인 스케일링 솔루션이지만, 상대적으로 설정하고 운영하기에는 더 복잡하다. 

 

Redis를 사용할 때 


너희들은 Redis를 그것의 자료 구조 때문에 쓰고싶어할 것이다. 캐시로서의 Redis로 넌 더 최적화된 캐시 컨텐츠와 내구성 등의 강력함과 전체적으로 더 큰 효율성을 얻을 것이다. 너가 한번 자료 구조를 사용해본다면, 특정 앱 시나리오에서의 효율성 증대가 굉장해질 것이다. 

Redis의 우월함은 캐시 관리의 모든 면에서 나타난다. 캐시들은 메모리에서 오래된 데이터를 삭제해서 새 데이터를 위한 공간을 만드는 'Data Eviction'이라는 메커니즘을 사용한다. Memcached의 Data Eviction 메커니즘은 LRU(Least Recently Used) 알고리즘을 사용하고 새 데이터와 비슷한 용량의 데이터를 독단으로 삭제한다. 

반면 Redis는 6개의 다른 삭제 정책 중 사용자가 골라서 사용할 수 있다. Redis는 또한 메모리 관리와 삭제 대상 선택에 더 정교한 접근법들을 사용한다. Redis는 더 많은 공간이 필요하거나 필요해질 것이라고 판단될때만 데이터가 삭제되는 lazy eviction과 active eviction을 모두 지원한다. 

Redis는 캐시를 할 수 있는 객체들을 고려하며 훨씬 더 많은 유연함을 준다. Memcached가 키 이름을 250바이트로 제한하고 plain string(단순 문자열)로만 작동하는 반면에 Redis는 키 이름과 값(key-value) 각각 512MB까지 허용하고 바이트 스트림으로 안전하게 저장한다. 또한 Redis는 5개의 원시 자료 구조(primary data structures-primitive data structure를 말하는 것 같다)를 골라 사용할 수 있다. 

 

데이터 지속성에서의 Redis


Redis 자료 구조들을 사용함으로서 캐싱 뿐만 아니라 데이터가 지속되고 항상 사용 가능하게 하고 싶은 것같이 여러가지 작업들의 단순화 및 최적화를 할 수 있다. 예를 들어 직렬화된 string값과 같은 객체를 저장하는 대신에 개발자들은 객체의 필드와 값을 저장하기 위해 Redis Hash를 사용할 수 있고 그것들을 유일키(single key)로 관리할 수 있다. Redis Hash는 개발자들이 사소한 수정건 때문에 전체 문자열을 가지고 와서 역직렬화하고, 값을 수정하고, 재직렬화해서 캐시에 전체 문자열을 교체하게 하는 작업에서 빠져나오게 해준다. 이것은 더 적은 리소스를 소모하고, 성능이 좋아진다는 소리다. 

Redis가 제안한 다른 자료구조들(lists, sets, sorted sets, hyperloglogs, bitmaps, geospatial indexes)은 더 복잡한 시나리오를 시행하는데 사용될 수 있다. 시간 연속성 데이터 발생 및 분석을 위한 Sorted sets은 엄청나게 줄어든 복잡성과 더 낮아진 대역폭 소모를 가능하게 해주는 Redis 자료 구조의 또다른 예시다. 

Redis의 또다른 중요한 이점은 저장된 데이터가 이해하기 쉬워 서버가 직접 다루기가 쉽다는 것이다. Redis에서 사용 가능한 180개 이상의 명령어의 대부분이 데이터 처리 기능(data processing operation)과 데이터 임베딩 로직(data embedding logic)에 심혈을 기울이고 있다. 이런 내장 명령어들과 유저 스크립트들은 네트워크를 통해 또다른 처리 시스템으로 데이터를 나를 필요 없이 Redis에서 데이터 처리 작업을 직접 처리할 수 있는 유연함을 보여준다. 

Redis는 고의적 종료나 예기치 못한 오류가 발생한 후에 캐시를 실행할 수 있도록 설계된 선택적이고 수정 가능한 데이터 지속 방안을 제공한다. 우리가 캐시 데이터를 휘발성에 임시적이라고 보는 동안, 디스크에 영속중인 데이터는 캐싱 시나리오를 따라 값을 메모리에 지속시킬 수 있다. 재실행 직후 즉시 데이터를 로드하여 캐시 데이터를 사용 가능하도록 하는 이 방식으로 캐시 기능을 더 빠르게 사용할 수 있고 원본 데이터로부터 캐시 내용물을 재활성화하고 재연산하는 로딩 구간을 없앨 수 있다. 

 

인 메모리 데이터 복제 


Redis는 관리하는 데이터를 복제할 수 있다. 복제는 오류를 견디고 중단되지 않는 서비스를 제공하는 매우 유용한 캐시 설치를 가능하게 해준다.  사용자 경험 영향과 앱의 성능에서 캐시의 실패가 아주 잠깐 일어날 뿐이어서, 캐시의 내용물과 서비스 유용성을 보장하는 검증된 솔루션으로서 큰 이점을 가진다.

기능 가시성의 측면에서, Redis는 기능의 용도와 비이상적 행동을 감시, 추적하는 많은 분석들과 내부 확인을 위한 명령어들을 제공한다. (데이터 베이스의 모든 측면들에 대한 실시간 통계, 실행되고 있는 모든 명령어의 출력, 클라이언트 연결의 나열 및 관리 등)

개발자들이 Redis의 영속성과 in-memory 복제 기능들의 효율성을 알아차리면, 고속의 데이터 분석 및 처리를 위한 주 데이터베이스로 사용하거나 이력(로그)를 저장하는 부 데이터베이스로 사용한다. 이런 방법들로 사용될 때, Redis는 분석 기능에서 이상적일 수 있다. 

 

데이터 분석을 위한 Redis


세가지 분석 시나리오가 바로 생각날 것이다. 첫 번째 시나리오는 대량의 데이터를 반복적으로 처리하는 아파치 스파크와 같은 것을 사용할 때, 스파크로 계산되기 전의 데이터를 전송하는 계층으로 Redis를 사용할 수 있다. 두 번째 시나리오에서는 너가 공유한 in-memory에 분산된 데이터 저장 공간으로서의 Redis는 스파크의 속도를 45에서 100만큼의 증가시킬 수 있다. 마지막으로 정말 평범한 시나리오는 보고서와 분석이 사용자에 의해 커스텀이 가능해야하는 것인데, 선천적으로(어쩔 수 없이) 하둡이나 RDBMS와 같은 일괄 데이터 저장 방식에서는 시간이 너무 오래 걸린다. 이런 경우에는 Redis와 같은 in-memory 자료 구조 저장 방식이 짧은 페이징 시간과 응답 시간을 가질 수 있는 실용 가능한 유일한 방법이다. 

극단적으로 큰 기능의 데이터 셋이나 분석량을 사용할 때에는 싹 다 in-memory에서 돌리기에는 비용 효율이 떨어질 수 있다. 저비용에 짧은 실행 시간의 효율을 얻기 위해, Redis 연구팀은 RAM-to-flash 비율을 설정할 수 있는 옵션을 추가하여 Ram과 flash 공간의 결합으로 실행할 수 있는 Redis를 만들었다. 이 기능이 업무 처리 속도를 늘려주는 새로운 방안들이 됐기 때문에, 개발자들에게는 flash 메모리 상에서의 캐시(cache on flash)를 쓸 수 있는 선택권이 생겼다. 

오늘날 오픈 소스 소프트웨어는 사용 가능한 최고의 기술들을 계속 제공하고 있다. 캐싱으로 앱 성능을 빠르게 해주기 때문에, Redis와 Memcached는 가장 인정받고 검증된 후보들이었다. 하지만 Redis의 기능이 풍부해지고, 개선되었고, 잠재력있는 사용법들이 있고, 규모 대비 비용 효율성이 좋아졌기 때문에, Redis는 거의 모든 경우에서 1순위가 되었다. 

원문 : https://www.xda-developers.com/asynctask-deprecate-android-11/

 

Google is deprecating Android's AsyncTask API in Android 11

The Android API for asynchronous logic, AsyncTask, is on its way out. According to an AOSP commit, the API will be deprecated in Android 11.

www.xda-developers.com

몇 년동안 AsyncTask는 초보/전문 개발자들에게 중요한 기능이었다. 만약 안드로이드 비동기 로직에 대한 튜토리얼을 찾아본 적이 있다면, 맨 처음 나오는 검색 결과들이 AsyncTask를 사용할 것을 제안했을 것이다. AsyncTask는 백그라운드 기능과 앱 UI간 상호작용을 간단하게 하기 위해 만들어졌다. 한동안 잘 돌아갔다. AsyncTask는 정말로 비동기 작업을 단순화 시키는데 도움이 되었지만, 그렇다고 완벽하지는 않았다. 

많은 앱들이 (원격) 서버에서 정보를 가져온다. 네트워크 요청은 어느 정도 시간을 필요로 하기에, 앱이 멈추지 않도록 하기 위한 비동기 처리를 해야 한다. 기능이 완료되면 UI가 업데이트 된다. 하지만 네트워크 요청이 끝나거나, 해당 부분의 UI가 종료되어 충돌(crash)이나 다른 버그들을 일으킬 때까지만 가능하다. AsyncTask는 비동기 처리의 전체 프로세스를 더 간단하게 만들기 때문에, 안드로이드 앱의 라이프사이클을 고려(존중)하지 않는다.(안드로이드 앱 라이프 사이클을 따르지 않는다는 것 같다.) 이 말은 UI 변경 후에 비동기 처리 종료 작업에서 안드로이드 자체의 보호를 바랄 수 없다는 것이다. 개발자가 수작업으로 직접 확인 및 보호 기능을 넣어줄 수도 있지만, 그것은 대량의 중복 코드를 발생시킨다. 이런 문제 때문에 AsyncTask는 어느 정도 사장되었다. 구글 또한 이걸 딱히 수정하지 않았다(건드리지 않았다). 

구글은 AsyncTask를 구제불능이라고 생각한 것으로 보인다. 최근 AOSP(Android Open Source Project)에 커밋된 것을 보면, AsyncTask는 위에서 언급한 것과 유사한 이유를 대며 삭제(deprecated - 미사용 api 처리)되었다. 앱 사용자들에게는 신경쓸 문제도 아니지만, 개발자에게는 많은 것을 의미한다. 만약 오래된 코드를 계속 쓰거나 비동기 작업을 새로 만들려고 한다면, 코드를 좀 많이 수정해야 할 것이다. 다행히도 구글은 개발자들일 내팽겨치지 않았다. 

AsyncTask의 한계점 때문에, RxJava와 코틀린의 새로운 Coroutines 라이브러리와 같은 대한책들이 계속 나오고 있다. 이런 대안들은 AsyncTask보다 훨씬 더 많은 유연함과 특징들을 가지는 경향이 있어 꽤 인기를 끌었다. AsyncTask 삭제 공지로 인해, 구글은 Java의 Concurrency framework나 Kotlin의 Coroutines을 사용할 것을 권하고 있다. 

이하 포스팅 작성자의 개인 의견으로 스킵. 

애당 이슈 커밋 로그
https://android-review.googlesource.com/c/platform/frameworks/base/+/1156409

 

1. 가격

4인이 가서
감자탕(대) 37000원
공기밥 1000원 * 2 (반공기씩)
볶음밥 2000원 * 3 (2천원이 맞나 헷갈림)

2. 맛


...그냥 감자탕이다.
맛...없지는 않은데
근데 맛집이라고 하기 애매한 것이, 보통 돈 주고 사먹는 감자탕에 바라는 점은 진한 국물에 풍부한 우거지 등등이 있지 않나?
그런데 국물은 보통 동네 감자탕집에서 먹는 것보다 연했고(첫 한입을 먹자마자 "국물이 연한데..?" 라고 생각했다.) 구성물도 그냥 뭐...
예전에 먹어본 친구 말로는 전보다 국물이 많이 연해졌다고 한다. 24시간 하는 집이라고 하니까 냄비에 물 부어넣은 타이밍에 주문한건가 싶은 생각도 들었다.

3. 평가


안갈거다.
맛이 없다는 건 아니다. 그냥 저냥이다. 깔끔하지는 않다. 옛날부터 있던 가게라 그런지 그냥 진짜 오래된 가게다.
먹고 나오면서 알았는데 삼대천왕에 나온 곳이더라. 삼대천왕 나온 집들은 서너곳 가봤는데 항상 그냥 평타 이상 상타 미만인 기분이 든다.

4. 사진

밥, 깻잎, 참기름, 김


오래되서 가격은 기억이 안난다.
아마 6000원대였던 걸로 기억한다.

밀면 먹어본 사람들은 그냥 밀면집이겠지만 난 면도 좋아하고, 밀면도 좋아하는 입장에서 아주 맛있게 먹었다.
안먹어본 친구도 밀면의 독특한 향 때문에 좋아할지 불안했었는데 잘먹는거 보고 내심 안심했다.


1. 가격

주 메뉴 : 오리지널 돈코츠 라멘 8000원
추가 메뉴 : 계란 1000원, 차슈(3장) 2000원

2. 후기

국내에서 먹어본 일본식 라멘집 중에 가장 나았음. 가게가 작아서 10~20분정도 웨이팅이 있을 수도 있지만 그정도는 충분히 기다려서 먹을만 한 맛이다.
그와중에 가격도 싼 편.
사골국물 맛이 나는 아오리 라멘도 나쁘지 않았지만 개인적으론 여기가 일본 놀러갔을 때 먹은 이치란 라멘 맛에 가까웠던 것 같다.
체인점 라멘집 먹어본 건 산쵸메, 아오리, 그리고 오늘 먹어본 우마이도 세종류인데
우마이도 > 산쵸메 > 아오리 순으로 맛있는 것 같다.

3. 사진

그림 1. 오리지널 돈코츠 라멘
그림 2. 반숙 계란이다. 라면에 기본 계란이 있다.
그림 3. 추가로 시킨 계란과 차슈
그림 4. 후식으로 먹은 tiger 흑당 밀크티

+ Recent posts