Redis 운영 관리

Redis 주요 특성

  1. Key-Value Store : 단순 스트링에 대한 key/Value 구조를 지원
  2. 컬렉션 지원 : List, Set, Sorted Set, Hash 등의 자료구조를 지원
  3. Pub/Sub 지원 : 서버간에 통지가 필요할 때 유용
  4. 디스크 저장 : 현재의 메모리 상태를 디스크에 저장 가능
  5. 복제 : 다른 노드에서 해당 내용을 복제할 수 있는 master/slave 구조 지원
  6. 빠른 속도 : 초당 50,000 ~ 60,000 QPS 이상의 처리 속도 지원

Single Thread

Redis 는 싱글 스레드이다. 그래서,
시간이 오래 걸리는 Redis 명령을 호출하면, 명령을 처리하는 동안 다른 client 의 요청을 처리할 수 없다.
이런 특성 때문에 주의해야하는 경우가 몇 가지 있다.

  1. 서버에서 keys 명령을 사용하지 마라.
    keys 명령은 서버에 저장된 key 목록을 볼 수 있는 명령이다.
    모든 key 를 대상으로 검색한다. 작업하는 동안 다른 client 의 요청을 처리할 수 없다.

  2. flushall/flushdb 명령을 주의해라.
    redis 는 db 라는 가상의 공간을 분리할 수 있는 개념이 있는데, select 명령으로 이동할 수 있다.
    그래서, 같은 key 이름이라도 db 개수에 따라 여러 개 만들 수 있다. 예를 들면,

    1
    2
    3
    4
    5
    6
    7
    select 0
    set jko "1234"
    select 1
    set jko "5678"
    get jko --> 결과 : 5678
    select 0
    get jko --> 결과 : 1234

    flushdb 는 하나의 db 를 선택해서 지우고, flushall 은 데이터 전체 내용을 지운다.
    flushall 의 경우 실제 존재하는 모든 데이터를 일일이 삭제하는데, 속도가 O(n) 이기때문에 데이터 양에 영향을 받는다.
    작업하는 동안 다른 client 의 요청을 처리할 수 없다.

Persistent : RDB/AOF

Redis 는 데이터를 디스크로 저장할 수 있는 Persistent 기능을 제공한다.
Redis 에서 제공하는 RDB 와 AOF 기능을 정리해보자.

  1. RDB
    'RDBMS' 가 아니라, 단순히 Redis 의 메모리 스냅샷을 파일로 저장한 파일 확장자이다.
    Redis 는 지속적인 서비스와 RDB 저장을 위해, fork 를 통해 자식 프로세스를 생성한다.
    이 RDB 저장을 위한 명령으로 SAVE 와 BGSAVE 가 있다.
    SAVE는, 모든 작업을 멈추고 현재 메모리 상태에 대한 RDB 파일을 생성한다.
    BGSAVE는, 자식 프로세스에서 RDB 파일을 생성한다.
  2. AOF (Append Only File)
    클라이언트가 Redis 에 업데이트 관련 명령을 요청하면, Redis 는 해당 명령을 AOF 에 저장한다.
    그리고, 실제로 해당 명령을 실행해서 메모리 내용을 변경한다.

RDB 를 저장할 때 fork 를 사용하여 문제가 되는 경우

이전에는 OS 가 자식 프로세스를 생성하면, 부모 프로세스의 메모리를 모두 자식 프로세스에 복사해야했다.
예를 들면, 부모 프로세스가 10 GB 메모리를 사용 중이라면, 자식 프로세스를 생성할 때 10 GB 메모리가 필요했다.
그런데 OS 가 발전하면서, COW (Copy On Write) 기술이 개발되었다. 그래서,
최초에 fork 로 자식 프로세스가 생성되면 부모 프로세스와 자식 프로세스는 같은 메모리를 공유한다.
그리고, 자식 프로세스에 write 가 발생할 때마다, 공유하는 해당 데이터는 자식 프로세스에 복사된다.
write 작업이 많아지면 부모 페이지에 있는 모든 페이지가 자식 프로세스에 복사되어 사용 메모리 양이 결국 두 배가 된다.

Read more

[실용주의 단위 테스트] 11장_테스트 리팩토링

프로덕션 시스템을 리팩토링하는 것 처럼, 테스트를 리팩토링해야한다.

1. 이해 검색

이해하기 힘든 다음 테스트 코드를 리팩토링 할 것이다. Test Smell 을 찾으며 리팩토링 해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class SearchTest {
@Test
public void testSearch() {
try {
String pageContent = "There are certain queer times and occasions "
+ "in this strange mixed affair we call life when a man "
+ "takes this whole universe for a vast practical joke, "
+ "though the wit thereof he but dimly discerns, and more "
+ "than suspects that the joke is at nobody's expense but "
+ "his own.";
byte[] bytes = pageContent.getBytes();
ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
// search
Search search = new Search(stream, "practical joke", "1");
Search.LOGGER.setLevel(Level.OFF);
search.setSurroundingCharacterCount(10);
search.execute();
assertFalse(search.errored());
List<Match> matches = search.getMatches();
assertThat(matches, is(notNullValue()));
assertTrue(matches.size() >= 1);
Match match = matches.get(0);
assertThat(match.searchString, equalTo("practical joke"));
assertThat(match.surroundingContext,
equalTo("or a vast practical joke, though t"));
stream.close();

// negative
URLConnection connection =
new URL("http://bit.ly/15sYPA7").openConnection();
InputStream inputStream = connection.getInputStream();
search = new Search(inputStream, "smelt", "http://bit.ly/15sYPA7");
search.execute();
assertThat(search.getMatches().size(), equalTo(0));
stream.close();
} catch (Exception e) {
e.printStackTrace();
fail("exception thrown in test" + e.getMessage());
}
}
}

2. 테스트 냄새 : 불필요한 테스트 코드

테스트 메서드에서 try/catch 의 가치는 없다. try/catch 블록을 제거하고 IOException 을 던지도록 변경하자.

1
2
3
4
5
public class SearchTest {
@Test
public void testSearch() throws IOException {
String pageContent =
...

그리고, 아래 코드에서 not-null 단언이 필요할까 ?

1
2
3
List<Match> matches = search.getMatches();
assertThat(matches, is(notNullValue()));
assertTrue(matches.size() >= 1);

matches.size() 에서 matches 가 null 을 참조한다면 예외를 던질 것이다.
따라서 다음과 같이 수정하자.

Read more

[전문가를 위한 스프링 5] 4장_스프링 구성 상세와 스프링 부트

1. Bean Life Cycle

IoC 컨테이너가 제공하는 주요 기능 중 하나는, 빈의 생성이나 소멸 같은 라이프 사이클의 특정 시점에 통지를 받을 수 있도록 빈을 생성하는 것이다.
일반적으로 두 가지 라이프사이클 이벤트가 있다.

  1. post-initialization event : 스프링이 개발자가 구성한 대로 빈에 모든 프로퍼티 값을 설정하고 의존성 점검을 마치자 마자 발생
  2. pre-destruction event : 스프링이 빈 인스턴스를 소멸시키기 바로 전에 발생

요청을 받을 때 마다 스프링 컨테이너가 매번 빈을 생성하는 Prototype 빈에는, 스프링이 소멸 전 이벤트를 통지하지 않는다.

빈이 이런 라이프사이클 이벤트를 받을 수 있는 메너니즘은 세 가지가 있다.

  1. inteface 기반
  2. method 기반
  3. annotation 기반

2. 빈 생성 시점에 통지 받기

  1. 빈 생성 시 메서드 실행
    초기화 메서드로 제대로 빈이 구성되었는지 확인할 수 있다.
    다음은, xml 파일에 default-init-method 애트리뷰트에 초기화 메서드를 지정하였다.

    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
    public class Singer {
    private static final String DEFAULT_NAME = "Eric Clapton";

    private String name;
    private int age = Integer.MIN_VALUE;

    public void setName(String name) {
    this.name = name;
    }

    public void setAge(int age) {
    this.age = age;
    }

    private void init() {
    System.out.println("빈 초기화");

    if (name == null) {
    System.out.println("기본 이름 사용");
    name = DEFAULT_NAME;
    }

    if (age == Integer.MIN_VALUE) {
    throw new IllegalArgumentException(
    Singer.class +" 빈 타입에는 반드시 age 프로퍼티를 설정해야 합니다.");
    }
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <?xml version="1.0" encoding="UTF-8"?>

    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd"
    default-lazy-init="true" default-init-method="init">

    <bean id="singerOne"
    class="com.apress.prospring5.ch4.Singer"
    p:name="John Mayer" p:age="39"/>

    <bean id="singerTwo"
    class="com.apress.prospring5.ch4.Singer"
    p:age="72"/>

    <bean id="singerThree"
    class="com.apress.prospring5.ch4.Singer"
    p:name="John Butler"/>
    </beans>
  2. InitializingBean 인터페이스 구현하기
    위 코드의 init() 메서드와 같은 역할을 하는 afterPropertiesSet() 메서드를 정의한다.

    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
    public class SingerWithInterface implements InitializingBean {
    private static final String DEFAULT_NAME = "Eric Clapton";

    private String name;
    private int age = Integer.MIN_VALUE;

    public void setName(String name) {
    this.name = name;
    }

    public void setAge(int age) {
    this.age = age;
    }

    public void afterPropertiesSet() throws Exception {
    System.out.println("빈 초기화");

    if (name == null) {
    System.out.println("기본 가수 이름 설정");
    name = DEFAULT_NAME;
    }

    if (age == Integer.MIN_VALUE) {
    throw new IllegalArgumentException(
    SingerWithInterface.class
    +" 빈 타입에는 반드시 age 프로퍼티를 설정해야 합니다.");
    }
    }
    }
  3. JSR-250 @PostConstruct 어노테이션 사용하기
    빈 클래스 내에서 스프링이 호출할 메서드를 지정한다.

    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
    public class SingerWithJSR250 {
    private static final String DEFAULT_NAME = "Eric Clapton";

    private String name;
    private int age = Integer.MIN_VALUE;

    public void setName(String name) {
    this.name = name;
    }

    public void setAge(int age) {
    this.age = age;
    }

    @PostConstruct
    private void init() throws Exception {
    System.out.println("빈 초기화");

    if (name == null) {
    System.out.println("기본 가수 이름 설정");
    name = DEFAULT_NAME;
    }

    if (age == Integer.MIN_VALUE) {
    throw new IllegalArgumentException(
    SingerWithJSR250.class
    +" 빈 타입에는 반드시 age 프로퍼티를 설정해야 합니다.");
    }
    }
    }
  4. @Bean 으로 초기화 메서드 선언하기
    자바 구성 클래스에서 빈을 선언할 때 사용한다. @Lazy 는 xml 의 default-lazy-init=”true” 와 동일하다.

    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
    public class SingerConfigDemo {

    @Configuration
    static class SingerConfig {
    @Lazy
    @Bean(initMethod = "init")
    Singer singerOne() {
    Singer singerOne = new Singer();
    singerOne.setName("John Mayer");
    singerOne.setAge(39);
    return singerOne;
    }

    @Lazy
    @Bean(initMethod = "init")
    Singer singerTwo() {
    Singer singerTwo = new Singer();
    singerTwo.setAge(72);
    return singerTwo;
    }

    @Lazy
    @Bean(initMethod = "init")
    Singer singerThree() {
    Singer singerThree = new Singer();
    singerThree.setName("John Butler");
    return singerThree;
    }
    }
    }

빈 생성은 다음과 같은 단계를 거친다.

  1. 빈 생성 위해 생성자 호출
  2. 의존성 주입 (수정자 호출)
  3. 사전 초기화 담당하는 BeanPostProcessor 기반 빈들에게 호출해야하는 메서드 있는지 확인 요청한다. 여기서 BeanPostProcessor 는 빈이 생성된 이후에 빈 조작을 수행하는 스프링에 특화된 인터페이스다.
  4. @PostContruct 어노테이션은 CommonAnnotationBeanPostProcessor 빈에 등록되므로, CommonAnnotationBeanPostProcessor 빈이 @PostContruct 어노테이션이 적용된 메서드 호출
  5. InitializingBean 의 afterPropertiesSet 메서드 실행
  6. init-method 애트리뷰트로 지정한 빈의 실제 초기화 메서드를 마지막에 실행
Read more

[실용주의 단위 테스트] 10장_Mock

Mock 객체를 사용해서 의존성을 끊는 방법을 정리한다.

1. 테스트 도전 과제

다음 클래스의 메서드는, 좌표를 기반으로 Address 객체를 생성해서 반환한다. retrieve() 메서드의 테스트를 작성해보자.
주의할점은, HttpImpl 클래스가 REST 호출을 실행한다는 것이다.

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
public class AddressRetriever {

public Address retrieve(double latitude, double longitude)
throws IOException, ParseException {
String parms = String.format("lat=%.6flon=%.6f", latitude, longitude);
String response = new HttpImpl().get(
"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"
+ parms);

JSONObject obj = (JSONObject)new JSONParser().parse(response);

JSONObject address = (JSONObject)obj.get("address");
String country = (String)address.get("country_code");
if (!country.equals("us"))
throw new UnsupportedOperationException(
"cannot support non-US addresses at this time");

String houseNumber = (String)address.get("house_number");
String road = (String)address.get("road");
String city = (String)address.get("city");
String state = (String)address.get("state");
String zip = (String)address.get("postcode");

return new Address(houseNumber, road, city, state, zip);
}
}

HttpImp 클래스는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HttpImpl implements Http {

public String get(String url) throws IOException {
CloseableHttpClient client = HttpClients.createDefault();
HttpGet request = new HttpGet(url);
CloseableHttpResponse response = client.execute(request);
try {
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity);
} finally {
response.close();
}
}
}

public interface Http {
String get(String url) throws IOException;
}

HTTP 호출을 실행하면 다음 두 문제가 있다.

  1. 실제 호출에 대한 테스트는, 다른 빠른 테스트들에 비해 느리다.
  2. HTTP API 가 항상 가용한지는 보장할 수 없다. 즉, 우리의 통제 밖이다.

2. 번거로운 동작을 Stub 으로 대체

HTTP 호출에서 반환되는 JSON 응답을 이용해서 Address 객체를 생성하는 로직을 검증해보자.
그러기 위해, HttpImpl 클래스의 get() 메서드 동작을 변경해보자. 단지, 테스트를 위해 하드 코딩한 JSON 문자열을 반환하도록 하자.
이렇게, 테스트 용도로 하드 코딩한 값을 반환하는 구현체를 Stub 이라고 한다.

Read more

[실용주의 단위 테스트] 7장_경계 조건

경계 조건과 관련된 결함을 방지할 수 있는 방법들을 정리한다. 약어로, CORRECT.

1. Conformance

기대하는 구조에 입력 데이터가 맞는지 확인해라.
예를 들면, 이메일 주소의 경우에 @ 기호가 없는 경우나 @ 의 앞부분이 비어있는 경우에 대처해야한다.

2. Ordering

데이터의 순서나 Collection 에 있는 데이터의 위치가 코드를 잘못되게 할 수 있다.

3. Range

기본형의 과도한 사용에 의한 code smell 을 Primitive Obsession 이라고 한다. 객체 지향 언어를 사용하면, 사용자 정의 추상화로 이 문제를 해결할 수 있다.
예를 들면, 원은 360 도 이다. 이동 방향을 자바 기본형으로 저장하기 보다 Bearing 클래스로 범위를 제약하는 로직을 캡슐화 할 수 있다.
다음 테스트 코드는, 유효하지 않은 값으로 Bearing 을 생성할 때 어떤 일이 발생하는지 보여준다.

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
public class BearingTest {

@Test(expected=BearingOutOfRangeException.class)
public void throwsOnNegativeNumber() {
new Bearing(-1);
}

@Test(expected=BearingOutOfRangeException.class)
public void throwsWhenBearingTooLarge() {
new Bearing(Bearing.MAX + 1);
}

@Test
public void answersValidBearing() {
assertThat(new Bearing(Bearing.MAX).value(), equalTo(Bearing.MAX));
}

@Test
public void answersAngleBetweenItAndAnotherBearing() {
assertThat(new Bearing(15).angleBetween(new Bearing(12)), equalTo(3));
}

@Test
public void angleBetweenIsNegativeWhenThisBearingSmaller() {
assertThat(new Bearing(12).angleBetween(new Bearing(15)), equalTo(-3));
}
}

제약 사항은 생성자에 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Bearing {
public static final int MAX = 359;
private int value;

public Bearing(int value) {
if (value < 0 || value > MAX) throw new BearingOutOfRangeException();
this.value = value;
}

public int value() { return value; }

public int angleBetween(Bearing bearing) { return value - bearing.value; }
}
Read more