Inertia

spring introduction

Spring in Action이라는 책을 스터디를 시작했다. 일주일에 한 챕터씩 진도를 나갈 예정인데 그에 맞춰 글로 기록을 남겨볼까 한다. 빼먹지 말고 끝까지 가야할텐데..

이번 진도는 챕터 1까지인데 주로 Spring의 근간을 이루는 요소들에 대한 간략한 소개가 나온다.

Spring 은 자바 개발을 쉽게 만들어 주는데 아래 주요 4가지 장점이 있다고 주장한다.

  • POJO, Spring은 스프링 관련 클래스를 extends 하거나 implement하라고 강요하지 않고 POJO를 많이 사용한다.
  • DI(Dependency Injection)
  • AOP(Aspect Oriented programming)
  • remove boilerplate code, 많이 사용하는 패턴들을 JdbcTemplate, RestTemplate등으로 구현해 놓았고 필요한 부분만 커스터마이징 할 수 있다.

위에서 보듯이 다 디펜던시를 줄이기 위한 방법들을 많이 사용하고 있는데 그 만큼 디펜던시가 강해지면 테스트 하기도 힘들고 유지 보수하기도 힘들어지기 때문이다. 그렇다고 커플링이 없는 모듈이란것은 아무것도 하지 않는 클래스 아니던가. 그래서 loosely coupling을 하기 위해 DI와 AOP, POJO를 들고 나온 것이다.

이렇게 해서 많은 장점을 제공해주기 때문에 우리는 스프링을 써야 한다.
머 이런 논리인듯 하다. 따라서 러닝 커브가 좀 있는 편.

cluster wide unique ID

unique id

cluster 단위의 유닉한 ID를 만들려면 어떻게 해야 할까? 출처는 여기

제일 쉬운 방법으로 UUID 같은걸 생각해볼수 있다. 이론적으로 가능한 조합은 16^32 승이나 되니 좀처럼 문제가 있어 보이진 않다. 하지만 실제로는 시드 같은걸 잘사용해야 하고
그렇지 않으면 충돌 확률이 꽤 올라갈 수 있다고 한다.

550e8400-e29b-41d4-a716-446655440000

두번째 방법으로는 유닛한 ID를 발급하는 서버(Ticket Server)를 하나두고 여기에 클라이언트들이 받아서 받아가는 방법. mysql의 auto increment index를 사용할 수도 있고, 2대로 한대에는 홀수개 생성, 나머지 한대는 짝수개로 생성하게 해서 이중화 할 수도 있다. 아니면 REDIS로 유닉한 값을 계속 만들어 사용할수도 있으나 persistent 하지는 않다. 이를 보완할 방법이 필요함.n
좋은 방법이기는 하지만 클러스터가 커질수록 ticket server에 부하가 걸리고, 이 서버가 SPOF 포인트가 된다.

세번째 방법으로는 여러개의 조합으로 각 노드에서 독립적으로 생성하는 방법. Twitter의 snowFlake가 이런 방식이라고 함. snowFlake인 이유는 눈송이의 모양은 하나 하나 모두 다르다고 함. 눈송이와 그 동작방식이 유사하여 이런 네이밍을 했다는데 잘 어울린다.

timestamp(42bit) + nodeID(10bit) + sequential ID(12bit)

위와 같은 공식으로는 하나의 노드는 밀리세컨드당 2^12 즉 4096개의 유닉한 ID를 생성할 수 있다. 클러스터 전체로는 2^22 즉, 4194304 4백만 개를 생성 할 수 있다. 이 정도면 왠만한 시스템은 다 커버할 수 있을 듯 하다.

이 시스템의 capa 즉 4096개가 넘는 요청이 들어오면 어떻게 하는가하고 찾아보니 다음 밀리세컨드까지 기다린다고 한다. 이것을 잘 모니터링 하다가 다음 버전을 준비하면 될듯 하다.

instagram shard id case

Instagram경우를 보면 아래처럼 data id를 정의해서 사용한다고 한다.

Each of our IDs consists of:

  • 41 bits for time in milliseconds (gives us 41 years of IDs with a custom epoch)
  • 13 bits that represent the logical shard ID
  • 10 bits that represent an auto-incrementing sequence, modulus 1024. This means we can generate 1024 IDs, per shard, per millisecond

mybatis mapper

주로 spring과 함께 사용하는 mybatis. java persistence framework으로서 Hibernate 같은 미들웨어 되겠다.

Dynamic SQL

1
2
3
4
5
6
7
8
9
10
11
12
 SELECT * FROM ORDER O
<trim prefix="WHERE" prefixOverrides="AND|OR">
<if test="searchText != null">
AND MATCH(O.name) AGAINST(#{searchText} IN BOOLEAN MODE)
</if>
<if test="price != null">
AND O.price = #{price}
</if>
<if test="searchText != null">
AND pattern like CONCAT('%',#{searchText},'%')
</if>
</trim>
  • concat example
  • conditional query with trim and if

여기서 trim은 2가지 일을 하는데 trim안의 컨텐츠가 있으면 prefix를 붙여주고, trim안에 sql문에 prefixOverrides에 있는 단어가 있으면 삭제해준다. 여기서는 AND나 OR가 된다.
마찬가지로 suffixOverrides는 끝에 붙는 단어를 삭제할 수 있다.
-

하나의 mapper에 여러개의 sql문 사용하기

하나의 mapper에 아래처럼 복수개의 sql문을 사용할 수 있을까? 결론은 YES!

1
2
3
4
<delete id="deleteById" parameterType="Integer">
DELETE FROM ORDER WHERE product_id=#{value};
DELETE FROM PRODUCT WHERE id = #{value}
</delete>

이것을 하려면 JDBC URL에 allowMultiQueries=true를 사용하면 된다. 아래처럼.

db.url=jdbc:mysql://127.0.0.1:33/orderdb?useSSL=false&useUnicode=true&characterEncoding=utf-8&autoReconnect=true&serverTimezone=UTC&allowMultiQueries=true

로깅

mybatis 쿼리가 제대로 동작안될때 Mapper안에 브레이크를 걸거나 할 수없기 때문에 디버깅이 어려워서 간단한 실수로 시간낭비를 하는 케이스가 꽤 있다.
이럴때 로깅 옵션을 enable해서 보면 도움이 된다.

logback 의 옵션은 아래처럼 mapper의 package name르 아래처럼 적어주면 된다.

1
<logger name="mapper.package.name" level="DEBUG"/>

angular form validation

앞서 기본적인 angular form 에 대해서 알아봤는데, 이번에는 form validation에 대해서 알아보자.

using built-in validator

angular에서 built-in validator의 종류는 다음과 같다.

name attribute selector desc
RequiredValidator [required]
MinLengthValidator [minlength]
MaxLengthValidator [maxlength]
PatternValidator [pattern]

기타로 EmailValidator, CheckboxRequiredValidator 등도 있다. 위의 Validator들은 아래처럼 selector로 directive가 적용되게 할 수 있다.

1
2
3
4
5
6
7
8
<form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm" #form>
<div class="form-group">
<label for="name">Name
<input class="form-control" name="name" required minlength="2" maxlength="100" pattern="[a-z0-9]+" >
</label>
</div>
<button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
</form>
  • input에 있는 required selector에 의해서 RequiredValidator가 적용됨
  • 마찬가지로 minlength|maxlength selector에 의해서 MinLengthValidator와 MaxLengthValidator가 적용됨
  • pattern selector에 의해서 소문자와 숫자만 입력할 수 있게 함.

showerror component

기본적으로 Validator를 적용해서 ValidationErrors가 발행하게 되면 control.errors에 에러가 들어오게 된다.
아래는 이것을 이용해서 form의 에러를 핸들링하는 컴포넌트다.

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
42
43
44
// show-errors.component.ts
import { Component, Input } from '@angular/core';
import { AbstractControlDirective, AbstractControl } from '@angular/forms';

@Component({
selector: 'show-errors',
template: `
<ul *ngIf="shouldShowErrors()">
<li style="color: red" *ngFor="let error of listOfErrors()">{{error}}</li>
</ul>
`,
})
export class ShowErrorsComponent {

private static readonly errorMessages = {
'required': () => 'This field is required',
'minlength': (params) => 'The min number of characters is ' + params.requiredLength,
'maxlength': (params) => 'The max allowed number of characters is ' + params.requiredLength,
'pattern': (params) => 'The required pattern is: ' + params.requiredPattern,
'custom': (params) => params.message
};

@Input()
private control: AbstractControlDirective | AbstractControl;

shouldShowErrors(): boolean {
return this.control &&
this.control.errors &&
(this.control.dirty || this.control.touched);
}

listOfErrors(): string[] {
return Object.keys(this.control.errors)
.map(field => this.getMessage(field, this.control.errors[field]));
}

private getMessage(type: string, params: any) {
if (params.message) {
return params.message;
}
return ShowErrorsComponent.errorMessages[type](params);
}

}

위 소스의 출처는 https://www.toptal.com/angular-js/angular-4-forms-validation 에서 가지고 왔고 getMessage부분을 약간 변형을 했다.

custome validator

하다보면 custom validator가 필요한데 이런식으로 만들어보면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, FormControl, ValidationErrors } from '@angular/forms';


@Directive({
selector: '[telephoneNumber]',
providers: [{provide: NG_VALIDATORS, useExisting: TelephoneNumberFormatValidatorDirective, multi: true}]
})
export class TelephoneNumberFormatValidatorDirective implements Validator {

validate(c: FormControl): ValidationErrors {
const isValidPhoneNumber = /^\d{3,3}-\d{3,3}-\d{3,3}$/.test(c.value);
const message = {
'telephoneNumber': {
'message': 'The phone number must be valid (XXX-XXX-XXX, where X is a digit)'
}
};
return isValidPhoneNumber ? null : message;
}
}

위 Validator를 적용하려면 앞서 예제와 같이 <input> tag에 validator의 selector인 telephoneNumber를 추가해주면 적용이 된다.

server validatoin

위의 예제에서 progrmatically 에러를 추가하고 싶을때(서버단에서 에러가 났을때) 이렇게 하면 된다.

우선 Form 변수를 @ViewChild로 매핑 시켜 주고, FormControl의 setErrors함수를 통해서 custom ValidationError를 Set할 수 있다.

1
2
@ViewChild('heroForm') form: NgForm;
this.form.controls['name'].setErrors({ server: { message: err.error.message } });

Reference

angular attribute directive

angular에서 3대 구성요소중 하나인 attribute directive에 대해서 정리.

attribute directive

highlight-directive.ts

1
2
3
4
5
6
7
8
9
10
11
import { Directive, ElementRef } from '@angular/core';

@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input() highlightColor: string;
constructor(el: ElementRef) {
el.nativeElement.style.backgroundColor = 'yellow';
}
}

test.html

1
<p appHighlight highlightColor="yellow">Highlighted in yellow</p>

위 예제는 여기서 가져왔음.

  • selector 문법인 [tag]는 attribute selector 이다. 예제는 https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors 여기를 보면 된다.
  • 위 html의 appHighlight를 사용하는 것만으로 p 에 HighlightDirective 구현을 적용하게 된다. 그렇게 되면 디렉티브의 변수인 highlightColor를 attribute처럼 사용할 수 있다.
  • directive의 selector에 app prefix를 붙이는 것은 충돌방지를 위한 것. 특별한 의미 없고 angular측에서 권장하는 practice.

angular form

angular form에 관한 정리. 폼은 template based로 구성할 수도 있도, 타입스크립트로도 구성할 수 있는데 여기서는 template based만 설명할 예정. 그것이 더 깔끔하다고 생각되기 때문.

form

1
2
3
4
5
6
7
8
<form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm" #form>
<div class="form-group">
<label for="name">Name
<input class="form-control" name="name" required [(ngModel)]="hero.name">
</label>
</div>
<button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
</form>

위 예제 출처는 angular.io 공식 사이트에서 가져왔다.

여기서 우리가 주의해서 봐야할 포인트는

  • #heroForm#formtemplate reference variable 임.
    • 기본적으로 #form은 dom reference-여기서는 <form>- 의 ElementRef 가 연결됨.
    • heroForm에서 사용된 ngForm은 angular의 내장되어 있는 ngForm directive를 사용한 것임. 그래서 기존 dom에는 없는 heroForm.form을 사용할 수 있는 것임. 이것은 type은 NgForm이 됨.
    • typescript 에서는 ViewChild('heroForm')으로 reference 를 받아올수도 있음
  • template reference variable은 template 같은 템플릿 안에서 사용할 수 있음
  • [(ngModel)] 은 양방향 바인딩한다는 뜻으로 typescript에 있는 hero.name 변수에 양방향 바인딩 된다.

github workflow

얼마전 gerrit workflow에 이어서 이번에는 github의 workflow를 정리해 본다. github는 거의 pull request(이하 PR)이 전부다. 요즘 거의 모든 github를 사용하는 프로젝트들은 대개 이런 flow로 개발이 되기 때문에 처음에 조금 복잡해 보일지라도 익혀 두면 도움이 될 것이다.

준비

우선 설명을 쉽게 하기 위해서 아파치 오픈소스 프로젝트인 Zeppelin을 기준으로 정리한다.

내가 이 Zeppelin프로젝트의 개발에 참여하기 위해서는 우선 이 프로젝트를 fork해야 한다. fork를 하게 되면 내 github의 계정에 fork된 repository가 생기게 된다. 나의 경우는 아래처럼 만들어졌다.

https://github.com/nberserk/zeppelin

이렇게 포크된 git repository를 내 로컬에 클론한다.

git clone git@github.com:nberserk/zeppelin.git

그리고 나중에 PR을 위해서 upstream 이라는 remote를 추가해준다.

git remote add upstream git@github.com:apache/zeppelin.git

따라서 origin은 nberserk/zepplein.git 이고 upstream은 apache/zeppelin.git이 된다.

PR

이제 PR을 날려보자. 우선 master branch로 변경하고 upstream의 최신 사항을 origin에 반영해 준다.

1
2
3
4
5
git checkout master
git fetch upstream # update apache remote
git rebase upstream/master # rebase upstream/master to current branch(master)
# make tracking branch 이렇게하면 upstream은 apache master와 싱크를 하게 되고, 로컬에 unpushed commit이 생기게 된다.
git push # git push로 origin의 git에 최신사항으로 업데이트 함.

우선 토픽 브랜치 만들고

git checkout -b topic

변경사항을 커밋하고

git add .
git commit -m “desc”

토픽 브랜치를 origin으로 push

git push -u origin topic

이 상태에서 github의 nberserk/zeppelin으로 가서 새 PR을 만들면 된다.

그러면 upstream의 maintainer가 리뷰하고 의견을 남겨주고, 몇번의 피드백이 오고간후 머지할 준비가 되면 머지가 된다. 그러면 업스트림에 나의 코드가 반영이 된 것이다.

conflict이 난 경우

PR중간에 conflict changes가 upstream에 커밋이 되었다면 PR에 conflict이 나기 때문에 머지가 불가능해 진다. 이럴때는 upstream을 토픽 브랜치로 머지를 해야 한다.

우선 upstream을 가져오고

git fetch upstream

topic 브랜치로 머지 한다.

git checkout topic
git merge upstream/master

conflict나는 부분을 해결한후 커밋을 하고 PR review를 다시 요청하면 된다.

Reference

gerrit workflow

gerrit을 사용할때의 workflow를 정리해 본다.

준비

아래처럼 git hook을 설정.

scp -p -P 29418 darren.ha@:hooks/commit-msg /.git/hooks/

이것은 git commit message에 아래와 같이 change-id를 삽입하게 되는데, 이 change-id가 리뷰의 단위가 된다.

1
2
3
4
5
6
7
commit af6e0970c05cde2f3a40f1b89374360aaf3bb972
Author: Darren Ha <>
Date: Tue May 29 17:22:00 2018 +0900

my changes

Change-Id: I6af3efd2b39707bcd0ec9b7815fe8ba656858456

변경사항 반영 및 리뷰 요청

1
2
3
4
git checkout -b topic
# submit some changes
git commit -m "my change"
git push origin HEAD:refs/for/master # request gerrit review

수정

위 상황에서 리뷰를 통해 더 수정해야 할 사항이 생기면 git commit --amend 를 통해서 수정해서 리뷰를 계속한다.

마무리

모든 리뷰가 끝나고 이제 반영해야 된다고 하면 gerrit에서 merge를 하고 master 브랜치로 돌아와서 origin과 싱크를 맞추면 된다.

1
2
3
git checkout master
git pull --rebase
git branch -d topic # (optional)

reference

Jekyll to hexo

원래 jekyll을 사용하고 있었는데 딱 하나 맘에 들지 않는것이 ruby를 사용한다는 것이었다. 그래서 최근에 hexo 라는 것을 보게 되었는데 node로 되어 있어서 migration을 해보기로 했다.

post

우선 post는 jekyll과 hexo모두 markdown포맷을 사용하고 있기 때문에 마크다운 포맷을 복사하는 수준에서 마이그레이션이 된다. jekyll/_posts/에 있는 *.md파일을 hexo/source/_posts/로 카피만 해주면 된다.

asset들은 jekyll/assets/하위 파일들을 hexo/source/images/로 복사를 하고. 아래처럼 링크를 걸면 된다.

![my image](/images/my-image.png)

disqus 커멘트 적용하기

레이아웃은 jekyll과 비슷한 템플릿 방식으로 되어 있어서 비교적 기계적으로 적용하면 된다. 나의 경우는 disqus comment를 적용했다. root의 _config.yml에 disqus_shortname을 선언하여 변수를 채워준 다음 layout/_partial/article.ejs의 끝에 아래 내용을 채워주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<% if (!index && post.comments && config.disqus_shortname){ %>
<section id="comments">
<div id="disqus_thread"></div>
<script type="text/javascript">
/* * * DON'T EDIT BELOW THIS LINE * * */
(function () {
var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
dsq.src = '//' + '<%= config.disqus_shortname %>' + '.disqus.com/embed.js';
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
})();
</script>
<noscript>Please enable JavaScript to view the
<a href="//disqus.com/?ref_noscript">comments powered by Disqus.</a>
</noscript>
</section>
<% } %>

theme

개인적으로 css 깔끔하게 하는 것은 자신이 없어서 보통 css framework은 bootstrap을 사용한다. bootstrap theme 을 기반으로 나의 theme작업을 했고 github에 공개했다.

데모는 http://nberserk.github.io에 가면 된다.

기존의 theme에 내가 추가적으로 작업한 것은

  1. disqus comment
  2. tags page
  3. archive layout

다른 포스트 링크 걸기

아래처럼 바로 post에 링크를 걸 수 있다.

/YYYY/MM/DD/slug

redirect

jekyll이랑 permanent link 방식이 조금 달라서, 기존 disqus comment의 링크가 다 깨졌는데 이것을 workaround하기 위해서 hexo-alias-generator 를 설치했다.

mockito

보통 디펜던시는 줄이는게 좋은 프랙티스로 인정받지만 실제 구현에서 디펜던시가 없을 수는 없다. 그래서 unit test를 작성할때 그런 dependency를 가지는 클래스들도 다 설정을 해야 해서 테스트가 어려워지는 문제가 있는데 이것을 획기적으로 해결한 것이 Mockito라는 프레임웍이다.

기본적으로 dynamic하게 class단위 mocking을 제공해서 내가 원하는 리턴값을 주게끔 하는 라이브러리인데 관심 있는 클래스의 로직만 테스트하기에는 이만한 것이 없다.

maven

메이븐 디펜던시는 아래처럼. spring boot의 경우도 1.5의 경우는 override를 해줘야 하는데, 아래처럼 모키토 버전만 잡아주면 된다.

1
2
3
4
5
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.15.0</version>
</dependency>

mocking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyTest{
@MockBean
private MessageRepository repository;
private MessageService service;

@Test
public void testGetMessages() {
String pid = "testUser";
String msg = "msg";

when(repository.getMessage(pid)).thenReturn(msg);
when(repository.getAllMessage(Mockito.anyString()))
.thenReturn(null);

assertTrue(service.getMessage(pid)==msg);
}
}

ArgumentMatcher

java8의 람다를 사용하면 ArgumentMatcher가 아주 나이스해지는데 아래 경우를 보자. 참고로 람다지원은 mockito 2.1.0 이후부터 가능하다.

1
2
3
4
5
6
7
8
verify(list, times(2)).add(argThat(string -> string.length() < 5));

// Java 7 equivalent - not as neat
verify(list, times(2)).add(argThat(new ArgumentMatcher(){
public boolean matches(String arg) {
return arg.length() < 5;
}
}));

Reference