독후감: Blue ocean shift, Beyond competing

예전에 이 책의 부모님 책쯤되는 “블루오션 전략(BLUE OCEAN STRATEGY)”라는 책을 읽었다. 한참이나 제품을 가지고 고민하던 시절이었고, 읽으면서 어떤 포인트에서 제품을 만들어야 할까를 많이 고민하게 했었다. 시절이 한참이나 지나서 올초 LAX 공항 서점에서 책을 고르다보니 이 책이 눈에 들어왔다.


블루오션 전략이라는 책이 출간되면서 레드오션과 블루오션이라는 두 단어가 일상화되었다. 레드오션에서 피터지게 싸워봐야 남는게 없다. 사용자들에 대한 이해를 바탕으로 그들이 진정 원하는 가치가 뭔지를 찾아라. 그 가치를 위한 제품이나 서비스를 만들고, 이걸로 새로운 시장을 만드는 것이 2000년대라는 새로운 시대에 삶아남는 방법이라고 이야기했다.

시장에 대한 새로운 접근 방법이었고, 제품이나 서비스를 만드는 입장에서 제대로 된 화두를 던졌다고 볼 수 있다. 10년이 넘은 세월이 흐른 뒤에 나온 이 책에서 저자들은 어떤 이야기를 하고 있을까?  저자들은 많은 사람들이 블루오션의 가치를 인식하지만 이를 실행하는데서 어려움을 겪는다고 한다. 아는게 다 잘 실행된다면 이 세상에 불행한 사람이 어디 있겠나?

새로운 시장을 창조한다고 이야기를 할 때, 흔히 혁신(Innovation)을 이야기한다. 혁신만이 새로운 시장을 이야기할 때 너무 강조되기 때문에 역설적으로 블루오션을 찾아내기 어렵다. 특히나 사람들은 자신이 갈망하는 것이 있다면 그것만 보게 되고 그 밖에 있는 더 큰 가치를 보지 못한다. 만약 이미 구조화된 조직내에서 이런 갈망을 추구했을 때 잃을 것들에 대한 두려움 때문에 혁신에 대한 도전을 주저하게 만든다. 결론적으로 블루오션을 하기 위해 필요하다고 많은 사람들이 이야기하는 “창조적 파괴” 라는 것에 이르지 못한다. 하지만 정말 창조적 파괴만이 이런 혁신을 달성하게 할까? 라고 저자들은 되려 질문한다. 새로운 시장을 만드는 전략적 방안으로 다음과 같은 3가지 옵션이 있다.

  • Offering a breakthrough solution for an existing industry problem
  • Redefining and solving an existing industry problem
  • Identifying and solving a brand-new problem or seizing a brand-new opportunity

블로오션을 개척한다는 것을 사람들은 너무 과하게 생각해서 뭔가를 없앤 이후에나 달성할 수 있다고 본다. 되러 이런 사고 방식이 관점의 전환을 어렵게 한다. 되려 이것보다는 현재의 문제점을 다른 관점에서 재해석하는 것이 필요하고 이를 통해 시장의 문제점을 파고들어 새로운 시장을 만들어내는 것(Redefining and solving an existing industry problem)이 좀 더 현실적이라고 충고한다.  이 때 중요한 해석의 기준이 바로 사용자의 가치를 혁신하는 가이다. 기술쪽에 있는 내 입장에서도 새로운 기술을 통한 혁신을 많이 이야기하고 듣기도 한다. 하지만 이런 시도가 과연 사용자에게 어떤 도움을 줄 수 있는가가 먼저 평가되어야 한다. 사용자들이 제품/서비스의 팬이 될 수 있다면 제대로 된 가치의 혁신을 이뤄냈다고 정의할 수 있다. 가치 혁신을 이뤄내기 위해, 즉 블루오션으로 전환할려는 사람들은 다음과 같은 마인드가 필요하다고 이야기한다.

  • Blue ocean strategists do not take industry conditions as given. Rather, they set out to reshape them in their favor.
  • Blue ocean strategists do not seek to beat the competition. Instead, they aim to make the competition irrelevant.
  • Blue ocean strategists focus on creating and capturing new demand, not fighting over existing customers.
  • Blue ocean strategists simultaneously purse differentiation and low cost. They aim to break, not make, the value-cost trade-off.

이런 것들을 추진하기 위해 가장 우선적으로 사람들이 변화와 행동의 필요성을 본인들 스스로 느껴야 한다. 역시나 일을 한다는 것의 기본은 사람이다. 같이 일하는 동료들이 이를 자각하지 못하고 시켜져 일을 한거나 두려워 주저한다면 말짱 도루묵이다. 집중할 수 있는 최소 단위로 일이나 문제를 재정의할 수 있어야 한다. 정의된 문제점들을 통해 사람들이 변화와 도전에 공감해야 한다. 물론 이런 도전의 결과로부터 자신들이 안전하다라는 것을 느낄 수 있도록 적절한 프로세스와 리더로부터의 충분한 설명도 뒤따라야 한다. 일련의 과정들은 투명하게 운영되어야 하며, 어떤 결과를 얻을 수 있는지에 대해 지속적인 대화가 이뤄져야한다. 즉 과정은 투명해야하며 결과는 공정해야 한다. ^^

그래서 다음의 절차에 맞춰 블루오션에 대한 시도를 해 볼 수 있다.

Step One – Get Started

  • Choose the right place to start your blue ocean initiative: The Pioneer-Migrator-Settler Map
  • Construct the right team for the initiative

Step Two: Understand Where You Are Now

  • Collectively build one simple picture that captures your current state of play: The Strategy Canvas
  • See and easily agree on the need for the shift

Step Three: Image Where You Could Be

  • Discover the pain points of buyers imposed by the industry: The Buyer Utility Map
  • Identify the total demand landscape you can unlock: The Three Tiers for Noncustomers

Step Four: Find How You Get There

  • Apply systematic paths to reconstruct market boundaries: The Six Paths Framework
  • Develop alternative strategic options that achieve differentiation and low cost: The Four Actions Framework (eliminate-reduce-raise-create)

Step Five: Make Your Move

  • Select your move at the blue ocean fair, conduct rapid market tests, and refine the move.
  • Finalize the move by formalizing the big-picture business model that delivers a win for both buyers and you.
  • Launch and roll out your move.

정리는 여기까지. 이 정도 내용을 적어놨으면 나중에 다시 되씹으면서 찾아볼 수는 있을 듯 하다.

글을 쓰거나 코딩을 하거나 같이 적용되는 진실이 하나 있다. 지속적으로 하지 않으면 퇴화된다는 것이다. 항상 새로운 글을 쓰거나 새로운 랭귀지, 프레임웍을 시도할 필요는 굳이 없다. 짧은 거라도 거르지 않고 계속해야한다.  마지막 글을 쓴 다음에 이 글을 올리는 기간이 이미 상당히 벌어졌다. 영어로 된 글을 단순히 옮겨적는 것에 지나지 않지만 그럼에도 글을 쓰는게 상당히 힘들어졌다.  바쁘다는건 핑계고 소주 한잔을 줄이면 글을 쓸 수 있는데 말이다.

 

Consideration in accessing API with the credential on the apache client libraries

본사 친구들이 신규 시스템을 개발하면서 기존에 연동하던 endpoint가 deprecated되고, 새로운 endpoint를 사용해야한다고 이야기해왔다. 변경될 API의 Swagger를 들어가서 죽 살펴보니 endpoint만 변경되고, 기능을 제공하는 URI에 대한 변경은 그닥 크지 않았다. curl을 가지고 테스트를 해봤다.

$ curl -X GET --header "Accept: application/json" --header "AUTHORIZATION: Basic Y29tbX****************XR5X3VzZXI=" \
"http://new.domain.com/abc/v2/username/abcd"
{"subject":"0a58c96afe1fd85ab7b9","username":"abcd","platform_code":"abc"}

잘 되네… 예전 도메인을 신규 도메인으로 변경하면 이상없겠네.

로컬 환경에서 어플리케이션의 설정을 변경하고, 실행한 다음에 어플리케이션의 Swagger 페이지로 들어가서 테스트를 해봤다.

음… 뭐지? 분명히 있는 사용자에 대한 정보를 조회했는데, 없다네? 그럴일이 없으니 console에 찍힌 Stack trace를 확인해보니 401 Unauthorized 오류가 찍혔다.

Caused by: org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:85)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:708)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:661)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:636)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:557)

401 오류가 발생했는데, 그걸 왜 사용자가 없다는 오류로 찍었는지도 잘못한 일이다. 하지만 원래 멀쩡하게 돌아가던 코드였고, curl로 동작을 미리 확인했을 때도 정상적인 결론을 줬던 건데 신박하게 401 오류라니??? 인증을 위해 Basic authorization 방식의 credential을 사용했는데, 그 정보가 그 사이에 변경됐는지도 본사 친구한테 물어봤지만 바뀐거가 없단다. 다른 짐작가는 이유가 따로 보이지 않으니 별수없이 Log Level을 Debug 수준으로 낮춘 다음에 HTTP 통신상에 어떤 메시지를 주고 받는지를 살펴봤다.

하지만 새로운 endpoint로 request를 쐈을 때에는 아래와 같은 response를 보낸 다음에 추가적인 request를 보내지 않고, 걍 실패해버린다.

2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 HTTP/1.1 401 Unauthorized
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Content-Encoding: gzip
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Content-Type: application/json
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Date: Tue, 17 Apr 2018 17:41:48 GMT
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Vary: Accept-Encoding
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 transfer-encoding: chunked
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Connection: keep-alive
2018-04-18 02:41:48.517 DEBUG 93188 : Connection can be kept alive indefinitely
2018-04-18 02:41:48.517 DEBUG 93188 : Authentication required
2018-04-18 02:41:48.517 DEBUG 93188 : new.domain.com:80 requested authentication
2018-04-18 02:41:48.517 DEBUG 93188 : Response contains no authentication challenges

이상한데???

Authentication required 라고 나오는데 코드상으로는 HTTP Connection factory를 생성할 때 Basic authorization을 아래와 같이 설정을 적용해뒀는데 말이다.

@Bean
public HttpClient httpClient() throws MalformedURLException {
PoolingHttpClientConnectionManager cm = Protocols.valueOf(new URL(domain).getProtocol().toUpperCase()).factory().createManager();
cm.setMaxTotal(connectionPoolSize);
cm.setDefaultMaxPerRoute(connectionPoolSize);

BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));

return HttpClients.custom()
.setConnectionManager(cm)
.setDefaultCredentialsProvider(credentialsProvider)
.build();
}

하지만 의심이 든다. 정말 요청할 때마다 Authorization header를 셋팅해서 내보내는건지. 이전 시스템과 어떤 방식으로 통신이 이뤄졌는지 궁금해서 endpoint를 이전 시스템으로 돌려서 확인을 해봤다. 정상적으로 데이터를 주고 받을 때는 아래와 같은 response를 endpoint에서 보내줬다.

2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "HTTP/1.1 401 Unauthorized[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Server: Apache-Coyote/1.1[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "WWW-Authenticate: Basic realm="Spring Security Application"[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Set-Cookie: JSESSIONID=78AEB7B20A1F8E1EA868A68D809E73CD; Path=/gas/; HttpOnly[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Content-Type: application/json;charset=UTF-8[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Transfer-Encoding: chunked[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Date: Tue, 17 Apr 2018 17:38:09 GMT[\r][\n]"

이 로그가 출력된 다음에 다시 인증 헤더를 포함한 HTTP 요청이 한번 더 나간다! 이전과 이후의 로그에서 인증과 관련되어 바뀐 부분이 뭔지 두눈 부릅뜨고 살펴보니 새로 바뀐놈은 WWW-Authenticate 헤더를 주지 않는다. 이 Response Header가 어떤 역할을 하는지 살펴보니 아래와 같은 말이 나온다.

(…)The response MUST include a WWW-Authenticate header field (section 14.47) containing a challenge applicable to the requested resource.(…)

근데 이게 문제가 왜 될까?? 생각해보니 Apache에 Basic 인증을 설정하면 요런 메시지 박스가 나와서 아이디와 암호를 입력하라는 경우가 생각났다.

아하… 이 경우랑 같은거구나! 실제 인증 과정에서 이뤄지는 Protocol은 아래 그림과 같이 동작한다.

이 그림에서 볼 수 있는 것처럼 하나의 API 요청을 완성하기 위해서 2번의 HTTP Request가 필요했던 것이다. 이런걸 생각도 못하고 걍 Basic Credential을 Example에서 썼던 것처럼 쓰면 문제가 해결된다고 아무 생각없이 너무 간단히 생각했던 것 같다. 연동해야할 Endpoint가 여러 군데인 경우에는 이런 설정이 제각각이기 때문에 많이 사용하는 RestTemplate 수준에서 Header 객체를 만들어 인증 값을 설정했을 것 같다. 물론 RestTemplate을 생성하는 Bean을 두고, Authorization 값을 설정하면 되긴 했겠지만 상황상 그걸 쓸 수 없었다. 변명을 하자면 그렇다는 것이다.

제대로 짜면 이렇다.

@Bean
public HttpClient httpClient() throws MalformedURLException {
....
Credentials credential = new UsernamePasswordCredentials(username, password);
HttpRequestInterceptor interceptor = (httpRequest, httpContext) ->
httpRequest.addHeader(new BasicScheme().authenticate(credential, httpRequest, httpContext));

return HttpClients.custom()
.setConnectionManager(cm)
.addInterceptorFirst(interceptor)
.build();
}

앞선 예제처럼 Credential Provider를 두는게 아니라 interceptor를 하나 추가하고, 여기에서 crednetial 값을 걍 설정해주는 것이다. 이러면 이전처럼 2번이 아니라 한번에 인증이 처리된다.

몸이 먼저 움직이기보다는 생각이 먼저 움직여야 하는데 점점 더 마음만 급해지는 것 같다.

참고자료

  • https://stackoverflow.com/questions/17121213/java-io-ioexception-no-authentication-challenges-found?answertab=active#tab-top
  • https://stackoverflow.com/questions/2014700/preemptive-basic-authentication-with-apache-httpclient-4

– 끝 –

Spring Data JPA와 AspectJ가 함께 친 사고

Spring JPA는 데이터베이스를 사용하는데 있어서 새로운 장을 열었다. 쿼리를 직접 사용해서 데이터베이스를 엑세스하는 MyBatis의 찌질한 XML 덩어리를 코드에서 걷어냄으로써 코드 자체도 간결해지고 직관적으로 특정 Repository 및 DAO가 어떤 테이블과의 매핑 관계가 있는지를 명확하게 파악할 수 있도록 해준다. 단점으로 생각되는 부분이 여러 테이블들을 복잡한 조인 관계를 설정하는게 상당히 난감하다. 하지만 역설적으로 이런 조인 관계를 왜 설정해야하는지를 역으로 질문해볼 필요가 있다. 우리가 복잡한 코드를 최대한 단순화시키려고 노력할 때, 그 코드가 달성할려고 하는 목적과 의미를 가장 먼저 생각하는 것처럼. 더구나 마이크로서비스 아키텍처 개념에서는 한 서비스가 자신의 독립적인 Repository를 가지는 것이 원칙이라고 할 때 해당 Repository의 구조는 단순해야한다. 그렇기 때문에 Spring JPA가 더욱 더 각광을 받는 것이 아닐까 싶다.

AspectJ 역시 특정 POJO 객체의 값을 핸들링하거나 특정 동작을 대행하는데 있어서 아주 좋은 도구이다. 주로 특정 Signature 혹은 Annotation을 갖는 객체 혹은 메소드가 호출되는 경우에 일반적으로 처리해야할 작업들을 매번 명시적으로 처리하지 않아도 AspectJ를 통해 이를 공통적으로 실행시켜줄 수 있기 때문이다.

작업하는 코드에 이를 적용한 부분은 특정 DB 필드에 대한 자동 암호화이다.  값을 암호화해서 저장하고, 저장된 값을 읽어들여 평문으로 활용하는 경우가 있다. 이 경우에는 이만한 도구가 없다.

이런 이해를 바탕으로 지금까지 서비스들을 잘 만들어서 사용하고 있었다. 그런데 새로운 기능 하나를 추가하면서 이상한 문제점이 발생하기 시작했다.  암호화된 테이블에 대한 저장은 분명 한번 일어났는데, 엉뚱하게 다른 처리를 거치고 나면 암호화 필드가 저장되어야 할 필드에 엉뚱하게 평문이 저장되었다. 그전까지 암호화된 값이 잘 저장되었는데… 더 황당한 문제는 내가 명시적으로 저장하지 않은 데이터까지 덤탱이로 변경이 되버린다!!!  개별 기능을 테스트 코드를 가지고 확인했을 때는 이상이 없었는데, 개발 서버에 올리고 테스트할려니 이런 현상이 발생한다.

 

새로운 기능이 동작하는 방식을 간단히 정리하면 아래와 같다.

  1. 기존 테이블에서 2라는 키값이 존재하는 데이터를 쿼리한다. 원래 시나리오상으로는 이런 데이터가 존재하지 않아야 한다.
  2. 2라는 PK값을 가지는 데이터를 저장한다.
  3. 저장 후 fk.for.grouping 이라는 그룹 키를 가진 항목들을 로딩해서 특정 값을 계산한다. 읽어들인 걸 저장하지는 않고, 읽어들여서 계산만 한다.
  4. 계산된 결과를 신규 테이블(New table)에 저장한다.

디버깅을 통해 확인해보니 2번 단계에서는 정상적으로 AspectJ를 통해 암호화 필드가 정상적으로 저장된다. 그런데 4번 과정을 거치고나면 정말 웃기게도 평문값으로 업데이트가 이뤄진다.  그림에서 따로 적지는 않았지만, 멀쩡하게 암호화되어 있던 1번 데이터도 평문화되는게 아닌가??? 몇 번을 되짚어봐도 기존 테이블에 저장하는 로직은 없다. 뭐하는 시츄에이션이지??

JPA와 AspectJ를 사용하는데, 우리가 놓치고 있었던 점은 없었는지 곰곰히 생각해봤다. 생각해보니 JPA를 사용자 관점에서만 이해를 했지, 그 내부에서 어떻게 동작이 이뤄지는지를 잘 따져보지는 않았던 것 같다. 글 몇 개를 읽어보니 Spring의 Data JPA는 이런 방식으로 동작하는 것으로 정리된다.

재미있는 몇가지 사실들을 정리하면 아래와 같다.

  • RDBMS를 위한 Spring Data는 JPA를 Wrapping해서 그저 쓰기 좋은 형태로 Wrapping한 것이고, 실제 내부적인 동작은 JPA 자체로 동작한다.
  • JPA는 내부적으로 EntityManager를 통해 RDBMS의 데이터를 어플리케이션에서 사용할 수 있도록 관리하는 역할을 한다.
  • EntityManager는 어플리케이션의 메모리에 적제된 JPA Object를 버리지 않고, Cache의 형태로 관리한다!!!

EntityManager가 관리를 한다고?? 그럼 그 안에 있는 EntityManager는 어떤 방식으로 데이터를 관리하지?

 

JPA Entity Lifecycle

 

오호.. 문제의 원인을 이해할 수 있을 것 같다.

  • Springboot application이 위의 처리 과정 3번에서 “fk.for.grouping 이라는 그룹 키를 가진 항목들을 로딩” 과정을 수행한다.
  • 정말 읽어들이기만 했다면 문제가 없었겠지만 AspectJ를 통해서 읽어들인 객체의 암호화 필드를 Decryption했다.
  • Entity Manager에서 관리하는 객체를 변경해버렸다! 객체의 상태가 Dirty 상태가 되버렸다.
  • 4번 과정에서 신규 테이블에 데이터를 저장할 때, Entity Manager는 관리하는 데이터 객체 가운데 Dirty 객체들을 테이블과 동기화시키기 위해 저장해버렸다.
  • 하지만 이 과정은 Entity Manager 내부 과정이기 때문에 따로 AspectJ가 실행해야할 scope를 당연히 따르지 않는다.
  • 결과적으로 평문화된 값이 걍 데이터베이스에 저장되어버렸다.

따로 저장하라고 하지 않았음에도 불구하고 엉뚱하게 평문화된 값이 저장되는 원인을 찾았다.

문제를 해결하는 방법은 여러가지가 있을 수 있겠지만, 가장 간단한 방법으로 취한 건 이미 메모리상에 로드된 객체들을 깔끔하게 날려버리는 방법이다.

public interface DBRepositoryCustom {
    void clearCache();
}

public class DBRepositoryImpl implements DBRepositoryCustom {
    @PersistenceContext
    private EntityManager em;

    @Override
    @Transactional
    public void clearCache() {
        em.clear();
    }
}

위와 같이 하면 Spring Data JPA 기반으로 EntityManager를 Access할 수 있게 된다. 이걸 기존 JPA Interface와 연동하기 위해, Custom interface를 상속하도록 코드를 변경해주면 된다.

@Repository
public interface DBRepository extends CrudRepository<MemberInfo, Long>, DBRepositoryCustom {
    ...
}

여기에서 주의할 점은 DBRepository라는 Repository internface의 이름과 Custom, Impl이라는 Suffix rule을 반드시 지켜야 한다. 해당 규칙을 따르지 않으면 Spring에서 구현 클래스를 정상적으로 인식히지 못한다.  따라서 반드시 해당 이름 규칙을 지켜야 한다.

  • DBInterface가 JPA Repository의 이름이라면
  • DBInterfaceCustom 이라는 EntityManager 조작할 Interface의 이름을 주어야 하며
  • DBInterfaceImpl 이라는 구현 클래스를 제공해야 한다.

이렇게 해서 위의 처리 단계 3번에서 객체를 로딩한 이후에 clearCache() 라는 메소드를 호출함으로써, EntityManager 내부에서 관리하는 객체를 Clear 시키고, 우리가 원하는 동작대로 움직이게 만들었다.

하지만 이건 정답은 아닌 것 같다. 제대로 할려면 AspectJ를 좀 더 정교하게 만드는 방법일 것 같다. 그러나 결론적으로 시간이 부족하다는 핑계로 기술 부채를 하나 더 만들었다.

 

참고자료

  • https://www.javabullets.com/access-entitymanager-spring-data-jpa/
  • https://www.javabullets.com/jpa-entity-lifecycle/

 

– 끝 –

블로그가 털렸네

회사에서 개발을 하면서 항상 고려할 최우선 순위 가운데 하나가 보안이다. 특히나 서비스내에서 제공되는 정보 가운데 개인과 관련된 민감한 정보가 있다면 보안은 최우선 순위 고려 사항이다. 회사에서 개발을 할 때는 이걸 항상 가장 먼저 생각하는데, 최근에 정말 어이없는 일을 겪었다.

뭐냐하면… 짜잔~

그렇다. 이 홈페이지가 털렸다. 친절하신 해커분께서 WordPress DB로 사용하는 MySQL 서버에 접속하셔서, 데이터를 몽땅 다운로드 받으신 다음에 모든 테이블을 싹~ 정리해주셨다. 그리고 친절하게 READ.me 라는 테이블에 비트코인을 보내주면 데이터를 보내주겠다는 친절한 멘트를 남겨주셨네?

처음에는 이상하게 홈페이지 방문자 카운트가 나오지 않았다. 단순 플로그인 문제로 생각했다. 몇 달동안 플러그인 버전업이 됐다는 메시지를 봐왔으니까. 홈페이지의 방문자 수를 보는게 일상의 소소한 재미였는데, 몇 일이 지나도 숫자가 그대로니 고쳐야겠다는 생각이 들었다. 그런데 이게 왠걸? 업데이트 하라는 플러그인들을 죄다 업데이트했는데도 여전히 카운트가 나오질 않네? 뭐지???

Frontend 단에서 JS 문제가 있는가 싶어서 크롬 개발자 모드로 들어가서, Refresh를 했다. 음! DATABASE CONNECTION ERROR 라는 아주 불친절한 문구가 떡 하니 나온다. 그 사이에 암호도 변경한게 없고, AWS에 돈도 따박따박 내고 있었는데 이게 뭐지? 내가 실수로 Security Group 설정을 변경했나? 장비에 들어가서 telnet 으로 접속되는지 확인해봤지만 정상이다.

$ telnet db.rds.domain.name 3306

데이터베이스 서버로 TCP 접속은 되는데, 데이터베이스 오류면 암호가 안먹는거라는 것 같은데? Mysql Workbench로 데이터베이스에 접속해봤더니 덜렁 READ.me 라는 테이블만 하나 덜렁있다 순간 내가 RDS Instance의 Security Group의 설정을 0.0.0.0/0 으로 설정했던 기억이 머리를 커다란 망치로 때린다. 왜 EC2 Instance는 Security Group을 나름 생각한다고 잡아두고, RDS는 이렇게 바보처럼 해놨을까?

망!!

네이버 퇴사하면서 그 이후에 작성한 글들은 죄다 여기에서 작성해왔는데 다 날라간건가? 글의 절대적인 가치는 보잘것 없겠지만 그래도 하나 쓰더라도 나름 신경을 썼던 글들이었다. 무엇보다도 지금까지 라이엇에서 한 작업들 가운데 까먹지 말자라는 차원에서 기록해둔 것들이었는데. 이대로 날려버린건가?

좌절모드에서 헤매고 있었는데, AWS Console에서 RDS 설정을 보니 Snapshot 백업을 설정해 둔게 기억났다. 부랴부랴 들어가서 확인해보니 최근 7일내의 데이터가 일 단위로 백업되고 있었다. 언제 해킹을 당했는지 WordPress 사이트에서 일단위 Visiting count 이력을 보니 아직 하루쯤 여유 시간이 남았네!


기쁘다 구주오셨다!

서둘러 Snapshot을 가지고 RDS Instance를 생성시켰다. 일단 생성시킨 RDS 접속해서 테이블의 상태를 확인해보니 모든 테이블과 데이터들이 온전히 살아있다. 다행히 최근에 이런 저런 일들이 있어서 글을 거의 안쓰고 있었는데 빠진 데이터없이 온전히 글들이 살아있다. 휴~ 일단은 다행이다.

  • 먼저 Security group의 설정부터 잡는다. EC2 Instance에서만 접근 가능하도록 했다. 물론 Workbench와 같은 쿼리 도구들을 바로 접속할 수 없긴 하지만 필요한 경우에는 AWS Console에서 필요할 때마다 Security Group의 설정을 변경하는게 아주 많이 안전하다.
  • 다음으로 정말 단순했던 데이터베이스 접근 암호를 수정한다. 기억력의 한계를 절감하는 나이라 그런지 사이트마다 암호를 달리 설정하는데 한계가 있다. 5 ~ 6 가지 정도의 암호를 몇가지 룰을 가지고 변형해서 사용해왔다. 털린 암호는 2개의 영문 단어를 조합한 형태였는데 Dictionary를 가지고 대입 공격을 하면 쉽게 뚫릴 수 있는 구조라는 생각이 들었다. 안타깝게도 MySQL에서는 암호에 특수 문자를 사용할 수 없다. 그럼에도 암호에 대한 복잡도를 높일 필요성이 있고, 3개의 단어와 숫자들을 조합해서 암호를 변경했다.
  • 가장 중요한 포인트인 것 같은데 암호를 변경할 날짜를 캘린더에 적어뒀다. 암호는 자주 바꿔주는게 최선인 것 같다. 바꾼 암호를 까먹지 않는다는 전제하에.

이렇게 설정을 마치고, WordPress의 연결 정보를 업데이트했다. 그리고 이렇게 글을 쓴다.

이번 일을 겪으면서 돌이켜 생각이 든다. 과연 나는 제대로 된 보안 정책을 가지고 개발을 하고 있는가? 귀찮기 때문에 혹은 설마라는 생각 때문에 어느 포인트에서 보안과 관련된 허점을 만들어두지는 않았을까? 이번 일을 겪으면서 보니 모를 일이라는 생각이 불현듯 스친다. 신규로 개발한 서비스들에 대해서는 모두 보안 리뷰를 받았다. 전문가 팀이 리뷰를 해준 사항이기 때문에 일정 수준은 안전하다라는 생각이 들기도 하지만 이 생각 자체도 만약의 사고에 스스로를 위한 면피가 아닐까 싶다.

개발할 때 자만하지 말고 개발하자. 그리고 잘하자. 쉬운길이 아니라 제대로 된 길로 가자.

– 끝 –

Git 기반 효율적인 이벤트 페이지 배포 환경 만들기

고객과 소통을 많이 할려다보면 이것 저것 알릴 내용들이 많다. 이건 게임 회사이기 때문이 아니라 소통에 대한 의지를 가진 회사라면 당연히 그래야한다.

SVN을 사용했었는데 무엇보다도 변경 사항에 대해 파악하는 것이 너무 힘들었다. 또한 매번 배포 때마다 브랜치를 머지하고 관리하는데 쉽지가 않다. 대부분의 프로젝트들은 모두 git을 사용하고, 전환했지만, 프로모션 영역은 7G라는 덩치의 Hell of Hell이었기 때문에 차일피일 미뤄지고 있었다.

기술 부채를 언제까지 끌고갈 수는 없다. 해야할 것을 미루기만 해서는 두고두고 골치거리가 된다.

이벤트/프로모션 페이지들은 배포되면, 이후의 코드 변경은 거의 발생하지 않는다. 하지만 다른 사이트등을 통한 참조가 발생할 수 있기 때문에 유지는 필요하다. 해당 페이지들을 통해 컨텐츠 혹은 정보들이기 때문에 그냥 404 오류가 발생하도록 놔둘 수는 없다. 따라서 기간이 지나면 관리해야하는 용량이 커질 수밖에 없다.  이렇게 커진 용량을 빌드/배포하는 건 전체 프로세스의 효율성을 확 떨어트린다. 특정 프로모션 영역(디렉토리)별로 배포하는 체계를 이미 갖췄기 망정이지, 그게 아니라면 7G 짜리를 매번 배포하는 최악의 배포 환경이 될 수 밖에는 없었을 것이다.

특정 영역별로 배포하는 방식에서 힌트를 얻었서 전체 코드들을 각 이벤트/프로모션 영역별로 쪼개서 각자 관리하기로 했다. 개별적인 성격의 프로모션 사이트로 볼 수 있기 때문에 각각의 디렉토리는 의존성이 없다. 때문에 개별 Repository로 나눠놓는 것이 완전 독립성 부여라는 관점에서 맞기 때문에 SVN repository를 git organization으로 만들고, 개별 디렉토리를 git repository로 만들었다. 이 방식의 문제점은 SVN 작업 이력을 git 환경으로 가져가지 못한다는 점이다.  하지만 “새 술은 새 부대에“라는 명언이 있지 않은가!!

맘을 정하고, Organization을 생성한 다음에 Repository를 Github을 통해 생성했다. 수련하는 마음으로 열심히 노가다를 하다보니 이내 모든 Repository를 만들긴 했는데… 이렇게 노가다한 결과 Repository를 세어보니 100개가 훌쩍 넘는다. 헐… 올리긴 해야하니까 스크립트의 도움을 받아 push했다.

쪼개놓는 건 일단 이쁘게 정리를 했는데 이제 배포 체계다. 일반적으로 개발 단게에서 master로 머지되는 코드는 자동으로 배포한다. 그래야 과정의 결과물을 관련된 사람들이 즉시즉시 확인할 수 있다. Git을 사용하는 경우, 이를 위해 webhook을 이용한다. Polling을 이용하는 경우도 있긴 하지만 이건 SVN을 쓰때나 써먹는 방법이다. 현대적이지도 않고 아름답지도 않다.  그런데 100개 이상이나 되는 코드에 일일히 webhook을 걸려고 생각해보니 이건 장난이 아니다. 노가다도 개발자의 숙명이라고 이야기하는 사람이 있을지 모르겠다. 하지만 프로모션이 늘어날때마다 webhook을 한땀한땀 설정하는 것도 웃기다. 누가 이 과정을 까먹기라도 한다면 사수에게 괴롭힘을 당할 수도 있기 마련이기도 하고. (안타깝지만 정말 이런게 어느 분야를 막론하고 흔하게 있다. 적폐에 타성으로 물든다고나 할까?)

자동화다. 개발자의 숙명은 적폐를 청산하고 사람의 개입없이도 돌아가는 시스템을 만들어내는 것이다. 다행이도 git의 경우에는 개별 repository에서 발생한 push 이벤트를 repository가 소속된 organization에 전달하는 기능이 있고, wehbook을 oragnization에 설정하는 것을 허용한다. 이 기능을 활용하면 신규 프로모션 작업을 위해 새로운 repository를 만들더라도 별도로 webhook을 설정할 필요가 없다.

(Jenkins는 application/json content-type만을 받아들인다. 괜히 urlencoded 형식으로 해서 안된다고 좌절하지 말자)

이제 배포를 위해 Jenkins에 해당 webhook을 이용해 정보를 전달하면 된다. 근데 어케 webhook payload를 jenkins가 이해하지? 그렇다. 여기서 다시 큰 문제점에 봉착한다. Jenkins에서 활용할 수 있는 git plugin은 이름이 지정된 특정 repository의 webhook을 인식할 수 있지만, 이 경우를 상대할려면 jenkins쪽에 각 repository들에 대응하는 jenkins job을 만들어줘야 한다. 이게 뭔 황당한 시츄에이션인가? 간신히 한 고비를 넘겼다고 생각했는데 앞에 비슷한 역대급 장애물이 기다리고 있다.

하지만 갈구하면 고속도로는 아니지만 길이 나타난다.  Jenkins에서 아래와 같은 두가지 아름다운 기능을 제공한다.

  • Parameterized build – 비드를 할 때 값을 파라미터로 정의할 수 있도록 하고, 이 파라미터 값을 빌드 과정에서 참조할 수 있도록 해준다.
  • Remote build trigger – Job에서 지정한 Token값이 HTTP authorization header를 통해 Jenkins에 전달되면 해당 Job이 실행된다. 와중에 Parameter 값을 별도로 설정도 할 수 있다.

이 두가지 기능을 활용하면, Job 하나만 만들어도 앞서 정의한 100개 이상의 repository의 빌드/배포를 실행할 수 있게 된다. 환경 설정을 위해 아래와 같이 Jenkins Job에 Repository 맵핑을 위해 String parameter를 정의하고, git repository 설정에서 이를 참조하도록 한다.

Jenkins Job을 선택하기 위한 Token은 아래 방식으로 설정한다. Jenkins는 해당 토큰값으로 어느 Job을 실행한지 선택하기 때문에 중복된 값을 사용해서 낭패보지 말길 바란다.

설정이 마무리됐다면 아래와 같이 테스트를 해보자.

 

curl -X POST "http://trigger:jenkins-trigger-user-credential@jenkins.sample.io/job/deploy-promo-dev/buildWithParameters?token=TOKEN&delay=0&PROMOTION=promotion”

Jenkins Host 이름 앞에 들어가는 건 Jenkins 접근을 위한 사용자 정보이다. 일반 사용자의 아이디 및 Credential을 바로 사용하지 말고, API 용도의 별도 계정을 생성해서 사용할 것을 권한다.

하지만 Build trigger를 누가 호출해주지? 누구긴, 당신이 짠 코드가 해야지! 이제 본격적인 코딩의 시간이다.

Git org에 설정한 webhook의 payload로부터 개발 작업이 이뤄진 repository와 branch를 확인하고, 이를 build trigger의 query parameter로 전송하면 된다. 일반적인 웹 어플리케이션처럼 상시적인 트래픽을 받는 시스템이 아니기 때문에 운영을 위해 별도의 어플리케이션 서버를 구축하는 건 비용 낭비다. 이를 경우에 딱 맞는 플랫폼이 AWS Lambda이다.  복잡한 코딩이 필요한 것도 아니기 때문에 Node.js를 활용해서 간단히 어플리케이션을 만들고, S3를 통해 이 어플리케이션이 Lambda에 적용될 수 있도록 했다. 실제 호출이 이뤄지도록 API Gateway를 앞단에 배치하면 끝!

Node.js를 이용한 Lambda 코드는 아래와 같이 작성해주면 된다.

var http = require('http');
var btoa = require('btoa');
exports.handler = (event, context, callback) => {
  var repository = event.repository.name;
  var options = {
    host: 'jenkins.sample.io',
    port: 80,
    headers: {
     'Accept': 'application/json', 
     'Authorization': 'Basic ' + btoa('trigger:jenkins-trigger-user-credential') 
    },
    path: '/job/deploy-promo-dev/buildWithParameters?token=TOKEN&delay=0&PROMOTION=' + repository,
    method: 'POST'
  };

  var refElements = event.ref.split('/');
  var branch = refElements[2];
  if (branch === 'master') {
    http.request(options, function(res) {
      console.log('STATUS: ' + res.statusCode);
      res.on('data', function(chunk) {
        console.log(chunk);
      })
    }).on('error', function(e) {
      console.loge('error',e);
    }).end();
    callback(null, 'Build requested');
  } else {
    callback(null, 'Build ignored for ' + branch + ' pushing');
  }
};

 

이제 개발하시는 분들이 개발을 막~~~ 해주시면 그 내용이 프로모션 웹 영역에 떡하니 표시되고, 프로모션 담당자들이 확인해주면 된다. 그리고 최종적으로 완료되면 라이브 환경에 배포를 해주면 된다.

근데 배포를 누가 해주지?? 라이브 배포는 자동으로 할 수 없으니까 개발 환경과 유사한 라이브용 Jenkins Job으로 개발자가 돌려야 하는거 아니가? 맞다. 걍 개발자가 하면 된다. 흠… 개발자가… 하지만 이 단계에서 개발자가 하는건 배포 버튼을 눌러주는게 다 아닌가? 개발과 라이브의 환경 차이가 물론 있긴하지만 프로모션이라는 특성상 그닥 크지 않다. 이미 개발 환경에서 프로모션을 담당자들이 깔끔하게 확인한 걸 개발자가 한번 더 확인할 필요도 없고 말이다.

게으른 개발자가 더 열열하게 게으르고 싶다. 어떻게 하지?? 뭘 어떻게 하긴, 열 코딩하는거지.

Git이란 환경은 정말 개발자에게 많은 것들을 아낌없이 나눠준다. 그 가운데 하나가 바로 API. 내가 사용하는 Git의 경우에는 Enterprise(Private) Git이기 때문에 적절하게 Credential만 맞춰주면 API를 호출할 수 있다. 보통은 이걸 위해 API 전용 Secrete을 생성해서 사용하는게 안전하다. (어느 바보가 자기 아이디 암호를 API 호출하는데 사용하지는 않겠지??)

Git API를 이용하면 Git org에 속한 모든 repository들을 모두 가져올 수 있다. 그럼 이 가운데 배포 대상을 하나 선택해서 프로모션 담당자가 Jenkins job을 trigger할 수 있도록 해주면 되는거 아닌가!! 쓰는 사람을 위해 배려 하나를 더해 준다면 가장 최근 작업 repository가 배포 대상이 될 것이라 업데이트 시간을 기준으로 최근 repository가 앞에 오게 하자. 아름다운 이야기다.

복잡하지 않다. jQuery를 이용한 간단한 웹 어플리케이션이면 족하다. 100줄 미만으로 구현된다. 물론 미적 추구를 더한다면 더 길어질 수도 있겠지만 개인적으로 절제된 공백의 아름다움이 최고라고 생각하는 1인이기 때문에. 물론 아무나 들어와서 마구 배포 버튼을 누르지 못하도록 적절한 예방 장치들을 마련되야 한다.

전체를 그림 하나로 그려면 대강 아래와 같다.

 

– 끝 –

참고한 것들

Spring batch를 Parallel로 돌려보자

Monolithic 아키텍처 환경에서 가장 잘 돌아가는 어플리케이션 가운데 하나가 배치 작업이다. 모든 데이터와 처리 로직들이 한군데에 모여있기 때문에 최소한의 비용으로 빠르게 기능을 돌릴 수 있다. 데이터 존재하는 Big Database에 접근하거나 Super Application Server에 해당 기능의 수행을 요청하면 된다. 끝!!!

하지만 요즘의 우리가 개발하는 어플리케이션들은 R&R이 끝없이 분리된 Microservices 아키텍처의 세상에서 숨쉬고 있다. 배치가 실행될려면 이 서비스, 저 서비스에 접근해서 데이터를 얻어야 하고, 얻은 데이터를 다른 서비스의 api endpoint를 호출해서 최종적인 뭔가가 만들어지도록 해야한다.  문제는 시간이다!

마이크로서비스 환경에서 시간이 문제가 되는 요인은 여러가지가 있을 수 있다. 배치는 태생적으로 대용량의 데이터를 가지고 실행한다. 따라서 필요한 데이터를 획득하는게 관건이다. 이 데이터를 빠르게 획득할 수 없다면 배치의 실행 속도는 느려지게 된다. 다들 아는 바와 같이 마이크로서비스 환경이 일이 돌아가는 방식은 Big Logic의 실행이 아니라 여러 시스템으로 나뉘어진 Logic간의 Collaboration이다. 그리고 이 연동은 대부분 RESTful을 기반으로 이뤄진다.

RESTful이란 뭔가? HTTP(S) over TCP를 기반으로 한 웹 통신이다. 웹 통신의 특징은 Connectionless이다. (경우에 따라 Connection oriented) 방식이 있긴 하지만, 이건 아주 특수한 경우에나 해당한다. TCP 통신에서 가장 비용이 많이 들어가는 과정은 Connection setup 비용인데, RESTful api를 이용하는 과정에서는 API Call이 매번 발생할 때마다 계속 연결을 새로 맺어야 한다. (HTTP 헤더를 적절히 제어하면 이를 극복할 수도 있을 것 같지만 개발할 때 이를 일반적으로 적용하지는 않기 때문에 일단 스킵. 하지만 언제고 따로 공부해서 적용해봐야할 아젠다인 것 같기는 하다.)

따라서 Monolithic 환경과 같이 특정 데이터베이스들에 연결을 맺고, 이를 읽어들여 처리하는 방식과는 확연하게 대량 데이터를 처리할 때 명확하게 속도 저하가 발생한다. 그것도 아주 심각하게.

다시 말하지만 배치에서 속도는 생명이다. 그러나 개발자는 마이크로서비스를 사랑한다. 이 괴리를 맞출려면…

  1. 병렬처리를 극대화한다.
  2. 로직을 고쳐서 아예 데이터의 수를 줄인다.

근본적인 처방은 두번째 방법이지만, 시간이 별로 없다면 어쩔 수 없다. 병렬 처리로 실행하는 방법을 쓰는 수밖에…
병렬로 실해시키는 가장 간단한 방법은 ThreadPool이다. Springbatch에서 사용 가능한 TaskExecutor 가운데 병렬 처리를 가능하게 해주는 클래스들이 있다.

  • SimpleAsyncTaskExecutor – 필요에 따라 쓰레드를 생성해서 사용하는 방식이다. 연습용이다. 대규모 병렬 처리에는 비추다.
  • ThreadPoolTaskExecutor – 쓰레드 제어를 위한 몇 가지 설정들을 제공한다. 대표적으로 풀을 구성하는 쓰레드의 개수를 정할 수 있다!!! 이외에도 실행되는 작업이 일정 시간 이상 걸렸을 때 이를 종료시킬 수 있는 기능들도 지원하지만… 이런 속성들의 경우에는 크게 쓸일은 없을 것 같다.

제대로 할려면 ThreadPoolTaskExecutor를 사용하는게 좋을 것 같다. 병렬 처리 가능한 TaskExecutor들은 AsyncTaskExecutor 인터페이스 페이지를 읽어보면 알 수 있다.

@Bean(name = "candidateTaskPool")
public TaskExecutor executor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
    executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
    return executor;
}

이 메소드 정의를 Job Configuration 객체에 반영하면 된다. ThreadPool을 생성할 때 한가지 팁은 Pool을 @Bean annotation을 이용해서 잡아두는게 훨씬 어플리케이션 운영상에 좋다. 작업을 할 때마다 풀을 다시 생성시키는 것이 Cost가 상당하니 말이다. 어떤 Pool이든 매번 만드는 건 어플리케이션 건강에 해롭다.

전체 배치 코드에 이 부분이 어떻게 녹아들어가는지는 아래 코드에서 볼 수 있다.

@Configuration
@EnableBatchProcessing
    public class CandidateJobConfig {
    public static final int CORE_TASK_POOL_SIZE = 24;
    public static final int MAX_TASK_POOL_SIZE = 128;
    public static final int CHUNK_AND_PAGE_SIZE = 400;

    @Bean(name = "candidateTaskPool")
    public TaskExecutor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
        executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
        return executor;
    }

    @Bean(name = "candidateStep")
    public Step step(StepBuilderFactory stepBuilderFactory,
                     ItemReader<User> reader,
                     ItemProcessor<User, Candidate> processor,
                     ItemWriter<Candidate> writer) {
        return stepBuilderFactory.get("candidateStep")
                                 .<User, Candidate>chunk(CHUNK_AND_PAGE_SIZE)
                                 .reader(candidateReader)
                                 .processor(candidateProcessor)
                                 .writer(candidateWriter)
                                 .taskExecutor(executor())
                                 .build();
    }

이렇게 하면 간단하다.

하지만 이게 다는 아니다. 이 코드는 다중 쓰레드를 가지고 작업을 병렬로 돌린다. 하지만 이 코드에는 한가지 문제점이 있다. 살펴보면 Chunk라는 단위로 작업이 실행된다는 것을 알 수 있다. Chunk는 데이터를 한개씩 읽는게 아니라 한꺼번에 여러 개(이 예제에서는 CHUNK_AND_PAGE_SIZE)씩 읽어 processor를 통해 실행한다. 배치의 실제 구현에 대한 이해나 고려가 필요하다.

Chunk를 사용해서 IO의 효율성을 높이는 방법은 흔하게 사용되는 방법이다. 하지만 입력 데이터를 Serialized된 형태로 읽어들여야 하는 경우라면 좀 더 고려가 필요하다. MultiThread 방식으로 배치가 실행되면 각 쓰레드들은 자신의 Chunk를 채우기 위해서 Reader를 호출한다. 만약 한번에 해당 Chunk가 채워지지 않으면 추가적인 데이터를 Reader에게 요청한다. 이 과정에서 쓰레드간 Race condition이 발생하고, 결국 읽는 과정에서 오류가 발생될 수 있다. 예를 들어 입력으로 단순 Stream Reader 혹은 RDBMS의 Cursor를 이용하는 경우에는.

문제가 된 케이스에서는 JDBCCursorItemReader를 써서 Reader를 구현하였다. 당연히 멀티 쓰레드 환경에 걸맞는 Synchronization이 없었기 때문에 Cursor의 내부 상태가 뒤죽박죽되어 Exception을 유발시켰다.

가장 간단한 해결 방법은 한번 읽어들일 때 Chunk의 크기와 동일한 크기의 데이터를 읽어들이도록 하는 방법이다. Tricky하지만 상당히 효율적인 방법이다.  Cursor가 DBMS Connection에 의존적이기 때문에 개별 쓰레드가 연결을 따로 맺어서 Cursor를 관리하는 것도 다른 방법일 수 있겠지만, 이러면 좀 많이 복잡해진다. ㅠㅠ 동일한 크기의 데이터를 읽어들이기 위해 Paging 방식으로 데이터를 읽어들일 수 있는 JDBCPagingItemReader 를 사용한다. 관련된 샘플은 다음 두개의 링크를 참고하면 쉽게 적용할 수 있다.

이걸 바탕으로 구현한 예제 Reader 코드는 아래와 같다. 이전에 작성한 코드는 이런 모양이다.

Before

public JdbcCursorItemReader<User> itemReader(DataSource auditDataSource,
                                                @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcCursorItemReader<User> reader = new JdbcCursorItemReader();
    String sql = "SELECT user_name, last_login_date FROM user WHERE last_login_date < '%s'";
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    reader.setSql(String.format(sql, sdf.format(elevenMonthAgoDate)));
    reader.setDataSource(auditDataSource);
    ...
}

After

public JdbcPagingItemReader<User> itemReader(DataSource auditDataSource,
                                             @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcPagingItemReader<User> reader = new JdbcPagingItemReader();

    SqlPagingQueryProviderFactoryBean factory = new SqlPagingQueryProviderFactoryBean();
    factory.setDataSource(auditDataSource);
    factory.setSelectClause("SELECT user_name, last_login_date ");
    factory.setFromClause("FROM user ");
    factory.setWhereClause(String.format("WHERE last_login_date < '%s'", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(oldDate)));
    factory.setSortKey("user_name");

    reader.setQueryProvider(factory.getObject());
    reader.setDataSource(auditDataSource);
    reader.setPageSize(CHUNK_AND_PAGE_SIZE);

    ...
}

여기에서 가장 핵심은 CHUNK_AND_PAGE_SIZE라는 Constant다. 이름에서 풍기는 의미를 대강 짐작하겠지만, Chunk에서 읽어들이는 값과 한 페이지에서 읽어들이는 개수가 같아야 한다는 것이다. 이러면 단일 Cursor를 사용하더라도 실행 Thread간의 경합 문제없이 간단히 문제를 잡을 수 있다. 하지만 명심할 건 이것도 뽀록이라는 사실.
이렇게 문제는 해결했지만 과연 마이크로서비스 환경의 배치로서 올바른 모습인가에 대해서는 의구심이 든다. 기존의 배치는 Monolithic 환경에서 개발되었고, 가급적 손을 덜 들이는 관점에서 접근하고 싶어서 쓰레드를 대량으로 투입해서 문제를 해결하긴 했다. 젠킨스를 활용하고, 별도의 어플리케이션 서버를 만들어서 구축한 시스템적인 접근 방법이 그닥 구린건 아니지만 태생적으로 다음의 문제점들이 있다는 생각이 작업중에 들었다.

  • SpringBatch가 기존의 주먹구구식 배치를 구조화시켜서 이쁘게 만든건 인정한다. 하지만 개별 서버의 한계를 넘어서지는 못했다.  한대의 장비라는 한계. 이게 문제다. 성능이 아무리 좋다고 하더라도 한대 장비에서 커버할 수 있는 동시 작업의 한계는 분명하다. 더구나 안쓰고 있는데도 장비가 물려있어야 하니 이것도 좀 낭비인 것 같기도 하고…
  • Microservice 환경의 Transaction cost는 기존 Monolithic에 비해 감내하기 힘든 Lagging effect를 유발시킨다. 그렇다고 개별 개별 서비스의 DB 혹은 Repository를 헤집으면서 데이터를 처리하는건 서비스간의 Indepedency를 유지해야한다는 철학과는 완전 상반된 짓이다. 구린 냄새를 풍기면서까지 이런 짓을 하고 싶지는 않다. Asynchronous하고, Parallelism을 충분히 지원할 수 있는 배치 구조가 필요하다.

한번에 다 할 수 없고, 일단 생각꺼리만 던져둔다. 화두를 던져두면 언젠가는 이야기할 수 있을거고, 재대로 된 방식을 직접 해보거나 대안제를 찾을 수 있겠지.

Frontend crossdomain issue in IE

최근에 서비스를 오픈하면서 겪은 경험담 하나 정리해볼려고 한다.

백엔드 개발자로써 격는 크로스도메인 이슈를 통칭해서 CORS와 관련된 문제라고 이야기한다. API에 대한 요청이 동일 도메인이 아닌 경우에 발생할 수 있는 이슈다. 대부분 정책적인 문제와 관련된 것이라 도메인에 대한 접근 제어 혹은 권한 제어를 통해 해결의 실마리를 찾는다.

이 비슷한 문제가 Frontend쪽에서도 발생할 수 있다는 걸 작업 과정에서 알게 됐다. 문제의 개요를 간단히 설명해보면…

  1. AA 도메인 및 BA 도메인은 A 도메인의 하위 도메인이다.
  2. AA 도메인에서 B 사이트에서 제공하는 기능을 이용해 공통 기능을 구현하였다.
  3. BA 도메인에서 AA 도메인의 기능을 이용하기 위해 AA 도메인에서 제공하는 웹 페이지를 팝업으로 실행한다.
  4. AA 도메인에서는 기본 설정을 잡고, B 도메인의 페이지를 호출한다.
  5. B 도메인에서는 기능을 모두 수행하고 그 결과를 AA 페이지에 반환한다.
  6. AA 도메인의 결과 페이지에서는 최종 결과를 팝업을 실행한 BA 도메인 페이지로 전달한다. 이 전달을 위해 일반적으로 widnow.opener를 사용한다. 보통은 window.opener.location.replace(…) 메소드를 활용해서 Opener 페이지를 다른 페이지로 redirection 시키는 방법이 적용된다.(물론 우리도 이런 방법을 사용했다.)

설명은 복잡하지만 아래 그림을 보면 좀 더 이해가 빠를 것이다.

일반적인 시나리오고, 대부분(?)의 경우에 이 방식은 정상적으로 잘 동작한다. 하지만 IE가 이 대부분의 경우에 포함되지 않는 것 같다.
IE는 마지막 5번 과정에서 팝업을 실행시킨 페이지가 지정된 페이지로 redirection되지 않는다. 기존 화면은 그대로 유지된 상태에서 새로운 탭 화면에서 replace() 메소드로 전달된 url이 나타난다. OMG

재미있는 건 이 현상은 크롬이나 파이어폭스에서는 나타나지 않는다. 오직 IE10, IE11등 IE 브라우저에서만 나타난다. IE에서 당췌 뭔 짓을 한거야???

문제를 좀 파보다보니 다음 문제 사항을 찾게 됐다.
팝업 혹은 frame, iframe 환경에서 서로 다른 도메인간에 데이터를 주고 받는 건 위험하다.
따라서 이걸 위한 적절한 안전 장치가 필요하다.
각 브라우저에 따라 안정 장치를 구현하는 방법이 틀린데 그럼 IE는 이걸 어떤 방식으로 구현했을까?

팝업된 화면에서 페이지 navigation등이 발생해서 opener와 다른 도메인으로 callee 화면이 이동된 경우, window.opener에 대한 callee쪽에서 호출하는 것을 차단한다.

그럼 어떤 방식으로 해야 callee쪽에서 호출했을 때 caller쪽의 페이지를 접근할 수 있는걸까? 여러 방법을 찾아봤지만, 뾰족한 방법이 보이지 않았는데 찾다보니 내부 사이트에 다음과 같은 코드가 존재하는 걸 확인했다.

document.domain

즉, document.domain의 값을 caller와 callee 사이에 맞추면 된다.  실행시킨 화면과 팝업 화면을 나타내는 document의 domain이 동일한 값을 가지면 document.location의 값을 참조하거나 replace() 등의 함수를 사용해서 변경할 수 있다. 하지만 그렇다고 아무 도메인 값을 document.domain 객체 값에 할당할 수 있는 것은 아니다. 현재 도메인이 sub.a.b.c 라고 한다면 document.domain의 값이 될 수 있는 대상자는 a.b.c, b.c 등이 될 수 있다. 아예 다른 도메인으로의 변경은 데이터의 hijacking을 고려해서 허용되지 않는 것 같다.

이 방식을 적용해서 위 그림의 시나리오를 정리해보면 아래와 같은 그림으로 풀어볼 수 있다.

 

  • AA 화면에서 “Click here to popup” 링크가 눌렸을 때 현재 화면의 document.domain의 값을 A.com 으로 JS로 변경한다.
  • 팝업 화면은 Redirection 혹은 내부 페이지 이동을 통해 각 페이지의 hostname이 domain 값이 된다.
  • 팝업의 최종 화면인 end.B.A.com에서 화면을 닫기 전에 A.com 값으로 팝업 페이지의 document.domain 값을 변경한다.  end.B.A.com 도메인의 상위 도메인인 A.com으로 값을 변경하는 것이기 때문에 보안적으로 허용된다.
  • 팝업 화면과 팝업 화면을 실행시키는 화면의 도메인이 이제 A.com 으로 동일해졌다. 이 시점부터 팝업 화면에서 실행 화면의 페이지를 window.opener 객체를 이용해서 제어할 수 있는 길이 생겼다.
  • 이 상태에서 Opener 페이지를 이동시키고, 팝업 화면은 닫히면 된다!!!

보안이라는 관점에서 보면 되려 이런 고려는 오히려 크롬이나 파이어폭스보다 IE가 좀 더 나은거 아닌가 하는 생각도 든다. (하지만 이런 정책이 있다는게 어딘가에 알려졌다면 더 좋았겠지만, 당췌 이 정보를 찾기가 넘 어렵다는게 함정!!!)

암튼 이렇게 해서 문제를 해결해서 어색한 동작을 없앴다라는게 기쁠 뿐이다.

좋은 코드에 대한 생각 – 3: 작업에 대한 기록

개발이라는 건 기록의 작업이다. 코드 한줄을 작성하더라도 이유없는 코드가 없다. 이런 이유로 코드를 작성할 때 그 근거를 기록으로 남길려고 하고 권장한다.

당신은 어떤 방식으로 기록하고 있나? Jira와 같은 티켓 관리 시스템을 이용할 수도 있겠고, 혹은 Confluence에 일지를 쓸수도 있겠다. 하지만 당신이 개발자라면 이 문제를 개발자스럽게 풀고 있을 것이라고 생각한다.

코멘트?

가장 흔하게 생각할 수 있는 방법이고 실제로 많은 개발자들이 작업에 대한 이력을 코멘트로 남긴다. 그림에서 보이는 녹색 코멘트가 가장 대표적인 경우다. 코드를 작성한 이유가 무엇이며 동작이 이런 방식으로 움직인다고 설명한다.

하지만 이런 코멘트는 비추다. 첫번째 이유는 이 코멘트에 사람들이 집중하지 않는다. 개발자라면 코드를 읽을려고 하지 문제가 있지 않는한 코멘트를 읽지 않는다. (사실 정말 그런지도 의문이다. 문제가 있다면 디버깅을 하거나 해당 코드 영역에 대한 테스트 코드를 살펴보지 않을까?)

정말 심각한 문제는 코드를 작성한 본인(들)도 본인들의 코멘트를 지키지 않는다는 사실이다. 기록을 해두면 된다라고 생각하지만 수정 가능한 코드에 대한 기록은 관리가 필요하다.  즉 코드가 바뀌면 코멘트도 바뀌어야 하는데 거의 대부분 코드만 수정하지 코멘트는 수정하지 않는다. 여러 이유가 있지만 그만큼 코멘트는 최초 작성자를 포함해 제대로 읽히지 않는다는 것을 의미한다.

코멘트는 남기지 않는게 좋다. 당신이 남기는 코멘트는 100% 쓰레기가 된다. 이런 쓰레기를 남기는 활동은 정신 건강에 좋지 않다. 그럼에도 불구하고 굳이 코멘트를 남기겠다면 코드 중간에 남기지 말고 Docs 문서 생성이 가능한 메소드 상단에 붙혀준다.

코드 중간에 남기는 코멘트는 진심 백퍼 쓰레기다. 되려 상단에 남기는 코멘트는 메소드의 의미를 설명해주기 때문에 사람들의 눈에 보여질 가능성도 훨 높을 뿐만 아니라 수정 가능성도 덩달아 높아진다.

의미에 이름을 주자.

특정 코드 블럭이 의미나 목적을 가진 부분이라면 이걸 굳이 코멘트로 남길려고 하지 말자.

질척거리는 코멘트보다 함수 혹은 메소드로 의미에 이름을 부여하는 것이 백배 좋다. 메소드의 이름을 적어주고 따로 Javadoc를 목적으로 한 것인지는 모르겠지만 따로 코멘트를 남기는 경우가 있다. 그렇게 남기는 설명이 메소드의 의미를 부연하기 위한 것이라면 차라리 이름을 충분히 길게 작성하자. 코드를 읽는 사람이 두 번 읽게 하는 것보다는 한번 읽어서 의미를 파악할 수 있게 하는게 좋다.

개발자가 싫어해야할 것은 같은 어떤 의미에서든 중복을 없애는 것이다. 그리고 이 규칙은 메소드 이름과 코멘트의 경우에도 마찬가지다. 그리고 이제 메소드의 코드를 최대한 간결하게 작성해서 술술 읽히게 만든다. 충실한 코멘트보다는 쉽게 읽히는 코드가 다른 개발자 혹은 동료를 위한 배려다.

소스 관리 도구들을 활용하자.

그럼에도 불구하고 다른 기록을 남기고 싶은 욕구가 생기는 경우가 있다. 개인 경험상 특정 버그 혹은 요청 티켓을 처리한 경우가 대표적이다. 지금 변경한 코드가 이 티켓에 해당하는 변경이라는 것으로 남기고 다른 사람도 그 티켓을 참조했으면 하고 바라기 때문이다. 앞서 이야기했지만 그렇다고 이걸 코멘트로 코드안에 남겨봐야 의미없다. 더 좋은 방법이 없을까?

코멘트보다 좀 더 효과적인 방법으로 추천할만한 내용은 커밋 로그(Commit log)를 활용하는 것이다.  커밋은 코드의 “특정 작업 단위가 마무리됐다는 것“을 말한다. 마무리된  작업의 내용을 설명하는 것이 커밋 로그이다. 또한 온라인 코드 리뷰 자체도 이 커밋 단위로 리뷰가 진행된다. 따라서 작업 설명은 충실하면 충실할수록 좋다. 작업 내용의 충분한 이해를 바탕으로, 리뷰어가 코드를 살펴보고 리뷰를 남겨준다면 리뷰를 받는 사람에게 더 도움이 된다.

결론은 충실하면 좋다긴 하지만 Source repository로 뭘 사용할 것인가에 따라 접근 방법이 틀려진다. 국내에서는 아직 SVN을 Repository로 사용하는 회사 혹은 개발팀이 많다. 하지만 알다시피 SVN은 중앙에서 소스를 관리한다. 그게 뭔 문제??? 관리 대상이 작다면 별 문제가 안되지만, 누구나 알듯이 많아지면 주구장창 느려지는 문제점이 있다. 더구나 SVN은 커밋을 하게되면 이를 서버에 전송하여 저장한다. 각 커밋 단위가 가지는 의미가 상당하다.

이런 이유로 SVN을 사용하는 대부분의 팀에서는 한 커밋 단위에서 자잘한 코드 변경을 커버하기 보다는 의미있는 단위로 작업하라고 권고한다. 한 커밋 변경에서 변경되는 작업 분량이 커지게 된다. 이 내용을 상세하기 적을려면 분량이 상당해진다. 바꿔 말하면 변경 내용을 알아보게 로그 형태로 적기 힘들다. 잘 적을까? 당연히 안적게 된다. 남길 수 있다면 티켓 번호 정도? 따라서 변경의 주요 내용을 커밋 로그에서 따라잡기 어렵다. 이 상황이 지속된다면 차라리 변경 주요 내용을 코멘트로 님기는게 더 나은 방법일 수도 있다.

그러니까 git을 사용해야 한다. Distributed source repository가 주는 장점이랄까? 혹은 로컬에서 자신의 Copy를 가지고 움직이기 때문에 갖는 장점일지는 모르겠다. 가볍고 빠른다. 그리고 Remote repository에 push를 통해 업로드하기 때문에 각 Commit 단위가 갖는 무게가 상대적으로 가볍다.

Git을 Source repository 사용한다면 다음의 규칙을 사용해서 로깅을 남겨보자.

» 서로 독립적인 변경이 있다면 각각에 대해 모두 커밋 로그를 남긴다. 예를 들어 불필요한 코멘트를 삭제했다면 그것도 커밋의 단위이고, 오타 한글자를 수정했었도 것도 따로 커밋한다.

» 코드 작성/변경 자체가 좁은 범위에 대해 이뤄진다. 로그에 쓸말도 많지 않아야 한다. 50 ~ 80자 이내로 코멘트를 작성하자. 바꿔 이야기하면 이 정도의 내용으로 커버될 내용이 한번의 커밋 대상이어야 한다.

» 다른 사람들과 공유해야하는 경우에만 Remote branch에 push한다. 모 회사의 광고 아닌 광고에 보면 불이 나도 push는 하고 가라는 말이 있긴 하지만 branch 방식으로 PR을 관리한다면 간간히 push를 하지 말고 자주 push를 하는게 좋다. 그렇게 모인걸 PR 날리면 땡이니 Remote에 얼마나 자주 push를 하던 문제가 안된다. (불날때 먼저 나가는게 중요하지 push는 하고 가라는 우스개 소리 아닌 소리를 하는 회사는 별로다.)

» 최종 PR의 타이틀은 간결해야한다. 내용에는 PR통해 반영될 기능들에 대한 설명과 리뷰를 해줬음 하는 대상자들의 이름을 적어둔다. 그리고 사람들 사이에 대화가 이어지면 된다.

 

Source repository를 쓴다면 이런 목적이어야 하지 않을까? 변경이 왜 일어났고, 언제 일어났으며, 누구에 의해 발생했는지를 한 눈에 명확하게 파악할 수 있다. 와중에 git 같은 툴을 사용하고 있다면, 코드가 발전되어 가는 형상을 관찰할 수도 있다. 그렇기 때문에 git을 써야한다.

개인적으로는 코드에 코멘트를 하나도 남기지 않는 것이 최선이라고 생각한다. 코드에 남기는 코멘트는 파일이 최초로 만들어졌을 때 자동 생성되는 작성자 이름과 작성일 정도면 충분할 것 같다. 그 이외에 자동으로 생성되는 나머지 코멘트들은 모두 지워버린다. 남겨두면 그게 득이 되는 경우를 본적이 없는 것 같다.

코딩 세상에서 미니멀리즘을 추구해야할 부분이 바로 이 포인트이지 않을까 싶다.

곱씹기 – 피터 드러커의 최고의 질문

리더쉽에 대한 조직장님의 추천이 있어서 읽게 된 책이다.

목차에 보면 위대한 질문들이 나온다.

» 미션: 왜 무엇을 위해 존재하는가?

» 고객: 반드시 만족시켜야 할 대상은 누구인가?

» 고객 가치: 그들은 무엇을 가치있게 생각하는가?

» 결과: 어떤 결과가 필요하며 그것은 무엇을 의미하는가?

» 계획 수립: 앞으로 무엇을 어떻게 할 것인가?

각 질문에서 이야기하는 것들은 책을 읽어보면 알 것 같고, 전체 문맥을 통해 저자가 이야기하고 싶은 것들을 정리하면 아래와 같은 그림이 되지 않을까 싶다. 그 최종 결과로 나오는 성과가 “고객의 일상을 어떻게 변화시키고 있는가?” 라는 질문에 똑 부르진 답을 내놓을 수 있다면 우리가 하는 일은 올바른 일을 하고 있다고 결론적으로 책에서 이야기한다.

 

개인적으로 간만에 읽은 한글 책이다. 소설책을 제외하고. 하지만 좀 번역이 아주 상당히 이상하다. 아무래도 직역을 한 부분들이 많은 것 같고, 그렇기 때문에 당췌 글이 머리에 잘 들어오지 않는다. 걍 원서로 읽을 걸 그랬다는 생각이 든다.

원서 링크는 여기에…

 

Slack as a slack – 슬랙을 슬랙답게 쓰자

일상 생황에서 여러 메신저 어플을 사용한다. 개인적으로는 전직장 사람들과는 라인으로 연락하고, 그외 일반인들과는 모두 카톡으로 개인적인 연락을 취한다. 하지만 회사 일과 관련된 연락은 슬랙(Slack)이라는 메신저를 통해 연락을 주고 받는다.

상사 갑질 이야기가 나올 때 항상 나오는 단골 이야기꺼리가 “단톡방”이다. 회사 업무를 위해서 단톡방을 만들고, 업무 이야기를 한다. 근데 그 단톡방에 포함이 안되면 “왕따”가 되는 것이고, 그렇다고 일원이 되면 일상이 피곤해진다. 일 이야기도 아닌 시덥지도 않은 이야기를 던지는 상사의 비위를 맞추기 위해서 “ㅎㅎㅎㅎㅎ” 혹은 “ㅋㅋㅋㅋㅋㅋㅋ” 를 본인의 증빙으로 남겨야 한다. 더불어 읽음 카운트가 주는 부담감에 눌려 살아야 한다. 와중에 업무 시간 끝났음에도 불구하고, 시도때도 울려대는 “깨톡깨톡” 이런 소리는 사람 짜증나게 한다.

그렇다고 카톡이나 라인이 잘못하는 건가? 절대 아니다. 카톡은 원래부터 개인적인 친분이 있는 사람들끼로 모여서 이야기하라고 단톡방이라는 것을 만들어줬을 뿐이다. 왕따라는 부작용이 나타나긴 했지만, 나름 그 사람들끼리는 이런 방식으로 알림과 이야기의 전달이라는게 의미가 있다. 개인적인 친분이 있는 사람들이니까.

잘못된 건 개인적이지도 않은 회사 사람들이 카톡으로 회사 업무를 이야기한다는 것이다. 바로 단톡방을 업무용으로 쓰자고 처음 생각해낸 사람의 잘못이다. 일단 내가 관심도 없고 상관도 없는 이야기가 단톡방에서 오고가는데, 그 이야기 알람을 내가 받을 필요가 1도 없다. 하지만 울려댄다. 끌수도 없다. 내가 필요한 정보 혹은 일에 대한 요청이 언제라도 이야기될 수 있으니. 그렇다고 관심있는 사람들끼리 이야기하는 새로운 단톡방을 만들면? 그럼 다른 사람들이 단톡방이 하나 더 있다고 민감하게 받아들인다. 새로운 단톡방에 빠진 사람들은 자신이 왕따가 된 기분을 느낄 수도 있다.

사람과 사람의 사교와 연결을 위한 개인용 메신저는 분명 일에는 스타일에는 잘 어울리지 않는다. 그래서 업무에 최적화된 업무용 메신저들이 따로 존재한다. 실제 기능적인 요구 사항에서 많은 차이점이 있기도 하지만, 그 이외에도 회사의 내부 기밀 정보가 일반인이 다루는 정보와 무분별하게 섞여서 관리되는 환경에 방치한다는 것도 어색하다.  물론 카톡이나 라인의 데이터 관리 및 보호(암호화) 수준은 최상급이다. 하지만 최악의 경우에 문제는 발생하기 마련이고, 그 경우에 대한 대비 혹은 대응을 해줄 수 있는 시스템이 있다면 그걸 거절할 이유가 없다.

업무용 메신저는 따로 사용해야한다. 큰 기업인 경우 자체적으로 사내 메신저를 만들어서 사용하는 케이스를 본다. 네이버의 경우에도 사내 메신저를 PC부터 모바일까지 죄다 자체적으로 만들어서 사용했었으니까. 하지만 쓸 회사가 자체적으로 만들어 사용하는 것은 정말 비추다. 특히 한국 기업들에서 이런 방식으로 참으로 독창적인 짓들을 많이 하는데, 걍 있는거 계약 잘 맺어서 썼으면 좋겠다. 완전 뻘짓이다.

몇가지 메신저를 봤지만, 기업용 메신저의 끝판왕은 슬랙(slack)인 것 같다. 여러 이유가 있지만 그 가운데서 가장 독보적인 이유는 차별화된 알림 서비스이다. 자신이 알림을 받는 경우들을 기본적으로 몇가지 설정에 따라 아주 상세하게 구분해놓을 수 있다. 개인의 전역 설정으로 이를 조정해 둘 수 있을 뿐만 아니라 “채널” 이라는 소위 단톡방 단위별로 이를 설정할 수 있다. 본인을 콕 찍어서 메시지를 보낸 경우 거의 대부분은 이를 알림으로 설정하지 않는다. 채널에 있는 전체를 지목(@channel)하거나 현재 온라인으로 연결된 사람들(@here)을 부르는 경우에도 무조건 받는 것은 아니라 선택할 수 있다. 더욱 더 획기적인 건 모바일과 PC에서 받는 알림의 수준도 가를 수 있다. 물론 필요에 따라 채널을 마음대로 만들 수 있고, 그 채널에 다른 사람들을 맘대로 초대할 수 있다. 더구나 그 채널은 기본적으로 모두에게 공개된 채널 방식으로 생성된다. 누구나 채널을 찾아볼 수 있고, 그 채널에 가입해서 오고간 내용들을 살펴볼 수 있다. 물론 Private 채널로 만들면 검색도 안되고, 아무나 들어올 수 없지만.

이런 설정 방식이 복잡하고 불편하지 않냐구? 오~~ No! No!! 당신이 알람을 받지 않을려는 데에는 그만한 이유가 있다. 그 중 첫번째 이유는 퇴근이다. 그리고 휴식이다. 휴식을 취하는 시간에 오는 연락은 정말 짜증이다. 정권이 바뀐 다음에는 이걸 금지하고 있다. 마땅히 그래야한다. (내가 이런 말을 할 자격이 있는지는 모르겠지만…)

좌우당간 PC 버전을 설치해놓은 당신은 일하고 있음을 증명한 거다. 알람 칼같이 온다. 모바일 버전은??? 깔아는 둬야겠지? 하지만 왠만히 연락 안되면 문자한다. 것도 아니라면 전화를 하던지… 당장 급한 상황임에도 불구하고 굳이 슬랙 메시지를 보내고 연락이 안된다고 성화인 사람! 스마트 폰의 본연의 기능인 전화 기능을 사용해라.

업무용 슬랙을 잘 사용하는 방법을 좀 적어보자.

  1. Public channel에서 이야기한다.
    업무용 메신저의 가장 큰 목적은 일하는데 필요한 정보를 나누는 것이다. 그리고 대부분의 정보는 일에 관심있는 모든 사람들에게 유익하다. 이 유익한 정보에 대한 접근에 대한 제한을 굳이 둬야할 이유는 없다. 재무나 경영상의 정보와 같이 민감한 정보가 아니라면 모든 걸 회사에 있는 사람들이 접근할 수 있도록 허용하는게 옳다. Public channel은 검색이 대상이 되기 때문에 필요한 정보들을 슬랙 수준에서 찾아보는데에도 아주 유용하다.

     

  2. Private channel을 가급적 만들지 않는다.
    하지만 정말 아무나 들여다보면 신경이 쓰이는 정보들이 있다. 그런 정보들의 경우에는 제한된 사람들이 보는게 맞고 그런 사람들을 위해 Private channel이라는게 있다. 하지만 두번 생각해볼 필요가 있다. 정말 필요한지. 뭔가 숨길만한 꺼리가 아니라면 Public channel로 채널을 유지하는게 맞고, 정히 숨겨야할 이야기라면 따로 이야기한다.

     

  3. 따로 이야기하지 않는다.
    왠만하면 DM(Direct messaging)을 사용하지 말라는 이야기다. 회사에서 주고 받는 대화는 대부분 일에 관련된 것이다. 일에 관련된 게 아니면 DM을 사용하거나 카톡을 사용하는게 맞겠다. 가쉽꺼리를 원한다면 카톡으로 가서 친구들이랑 이야기해라. 그리고 그렇게 쫄리는 이야기라면 회사에서 안하는게 좋다.
    한가지만 좀 더 첨언하자면, 임시로 이야기를 해야하는 경우에도 카톡에서 단톡방 만들듯이 꼭 사람들 불러모와서 DM 채널을 만드는 경우가 있다. 그거 알람 공해를 유발한다. 가능하면 그런 일을 하지 쓸데없는 신경을 덜어주는 배려가 아닐까 싶다. 멀쩡히 있는 채널 공간에서 이야기하는게 좋다.

     

  4. 하나의 주제로 모아져야 할 필요가 있을때는 쓰레드(Thread)로 글을 모은다.
    보통은 하나의 채널에서 특정 이슈에 대해 집중된 방식으로 논의를 하는 경우에 쓰레드(Thread)를 이용한다. 쓰레드는 영어 단어 그대로 특정 이슈에 대한 메시지들의 실타래라고 생각하면 된다.  그래서 관련된 이야기들이 하나의 내용으로 논의되고 일에 대한 이슈 제기와 끝이 모두 하나의 글의 실타래에서 마무리된다. 1~2주 전까지만해도 쓰레드에 이미지를 첨부할 수 있는 기능이 없어서 참으로 안타까워했는데 지난주부터 이미지가 들어가기 시작했다!!!!

     

  5. 어플들을 활용해라.
    개발자들이 사용하는 거의 모든 도구들이 연동된다. Jira, Git, Jenkins, Favro,… 그래서 개발자들이 슬랙을 좋아하는 것 같다.
    어플을 연동해둬야 하는 이유는 심플하다. 관련된 정보들을 슬랙 채널에 모으기 위해서다. 새로운 이슈가 생기던 진행되던 상태의 변화가 있던 모든 내용이 업무 채널에 존재한다. 그리고 그걸 화두로 이야기가 시작된다. 얼마나 멋진가?  그리고 이렇게 모아진 정보들에 대해 슬랙은 멋진 검색 기능을 제공한다. 적절한 검색 필터를 활용하면 효과적으로 자신의 정보를 찾을 수 있다.

     

  6. 슬랙은 업무용이다. 이점을 잊지 말자.

아무래도 마지막 내용이 가장 중요한 내용이 아닐까 싶다. 업무 용도로 쓰라고 하더라도 카톡을 쓰던 버릇을 그대로 옮겨와서 슬랙을 카톡이랑 동일하게 쓰시는 분들이 있다. 같이 일하는 동료에 대한 피해이고 같은 말 여러번 반복해서 굳이 짜증을 유발시킨다. 일을 투명하게 진행하는 여러분들이라면 커뮤니케이션도 당연히 투명하게 해야하는거 아닐까?

슬랙의 채널을 운영하는 방식은 현재까지의 느낌으로는 다음과 같은 채널 운영 정책을 가지면 될 것 같다.

  • 팀/조직 채널 – 개인이 속한 팀이나 조직의 채널을 생성한다. 가급적이면 해당 채널은 해당 구성원들 위주로 구성되는게 좋다.
  • 업무 문의 채널 – 팀 외부의 사람들이 팀에 업무에 대해 물어볼 일이 있다. 근데 팀 채널에 그 사람들이 들어와서 이야기하는 것도 좀 그렇고, 그렇다고 하지 말라는 DM을 매번 같은 사람에게 날리기도 뭐하다. 이런 경우를 위해 누구든 들어와서 팀에 대해 궁금한 점들을 질문한다. 그럼 팀의 구성원들이 해당 질문을 보고, 답변을 남긴다. 이 경우 보통은 답변 담당자를 두고, 해당 담당자는 채널에 올라오는 모든 메시지들을 알람으로 받는다. 모든 메시지가 신규 메시지로 넘어오면 것도 많이 피곤해진다.  그래서 질문은 메시지로 남겨지지만 그에 대한 답변은 쓰레드의 형태로 답변을 진행하는 것이 일반적이다.
  • 목적 채널 – 특정한 이벤트 혹인 목적을 가진 사람들의 토론이나 커뮤니케이션이 필요하다고 하면 그때마다 채널을 만들면 된다. 하지만 채널이 많아지면 실제로 필요한 채널을 손쉽게 찾는게 어려워진다. 그래서 이런 목적을 달성한 채널의 경우에는 어카이브(Archive)라는 기능으로 보관해둔다. 어카이브됐다고 하더라도 검색과 읽기는 모두 되기 때문에 채널에서 주고받은 이전 내용들을 찾아보는데는 문제없다.

기본적인 운영 정책이지만, 이 방식으로 채널을 유지한다면 상당히 효과적인 커뮤니케이션이 이뤄질 수 있을 것이다.

만약 당신 회사에서 업무용 메신저로 깨톡을 사용한다면… 당장 슬랙으로 바꾸길 권한다. 슬랙이 아니더라도 이와 유사한 국산 업무용 메신저들도 존재하는 것으로 알고 있다. 가능하면 벤치마킹해서 단톡방 공해에서 벋어날 수 있도록 배려를 해주면 좋겠다.

하지만… 슬랙을 좀 쓰다보니 과도한 업무 집중으로 인한 폐해도 발생한다. 이런 부분을 어떻게 극복할지에 대해 간단히 정리한 후속글이 있으니 이 내용도 같이 참고해보면 좋을 것 같다.

– 끝 –