최근 한일

- Spring Security & OAuth2

  • 개인 프로젝트를 진행 해 보기 앞서 스프링 시큐리티를 다시 해보고

  • OAuth2도 해 봄

  • 스프링 부트 2.x대의 경우 Spring Security OAuth 프로젝트의 일부 기능들이 Spring Security로 마이그레션 된 상황이라 일부 변동이 있다.(Spring Boot 2.0.0 M5 Release Notes, 2017. 10. 19)

  • 이와 같은 이유로 일반적으로 1~2년 사이 작성된 UserInfoTokenServicesResourceServerProperties를 사용한 OAuth2 과정들이 불가 한 상황인데 버전을 1.5x 대로 낮춰서 실습 할때는 문제 없이 동작 한다. 2.x에서도 할 수 있는 몇가지 해결 책들이 보이는데 최근 업데이트 된 부분이다 보니 많이 있지는 않다.

- EclEmma 플러그인 설치

  • 최근 프로젝트의 커버리지 :

    • 웹서버 구현 : 65.2%

    • 프레임워크 구현 : 55.5%

    • 트렐로 구현 : 79.4%

  • 로컬이 아니라 웹으로 가면서 커버리지가 줄어 들고, Controller나 몇몇 부분에 대한 테스트 코드를 작성 할 줄 몰랐던 프레임워크 제작 당시에는 또 한번 확 줄었다가, Acceptance Test에 대해 알게 되고서 다시 한번 상승 한점이 딱 눈에 보인다.(사실 인수와 통합 테스트 개념은 아직 확실히 잡히지 않아서 이 부분은 실무를 경험하면서 여러 상황들을 보기로 했다.)

- 마틴 파울러의 “리팩토링” 읽는 중

  • 생각보다 재밌다. 특히 생각해 볼 거리들이 많아서 더 재밌는 것 같다.

최근 느낀점

  • 금방 될 줄 알았던 OAuth2에서 생각보다 시간이 걸렸다. 스프링 부트 2.x가 문제가 될 줄 몰라서 상당히 긴 기간 다른 부분에서 헤맸다. 겸사 겸사로 이번 경험으로 얻은 두 가지는 안정성 때문이라는 점에서 좀 다르지만 역시 최신 버전을 쓰는 건 예상치 못한 상황을 일으킬 수 있다는 점이다…..ㅜㅜ

    한가지 더는 스프링이라는 프레임워크에 대해 공부해야 하고 싶어지는 의욕이 더 붙었다는 것이다. 사실 내가 스프링이 돌아가는 원리를 좀 더 잘 알았으면 이번처럼 엄청 헤매지는 않았을 것이다. 최근에 괜찮아 보여서 사둔 스프링 철저 입문을 봐야겠다.

  • 커버리지에 대해서는 알고 있었지만 직접적으로 신경 쓰고 작업하진 않았는데 나쁘진 않은 결과가 나온 것 같다. 솔직히는 좀 더 높았을 줄 알았다가 생각보다는 낮아서 실망을 했지만 확인을 좀 해보니 의외로 도메인쪽이 커버리지가 낮고, 원인이 toSring, hashCode(), equals() 혹은 getter/setter 부분이 문제였고 한번 그 부분들도 해결을 해보니 마지막 프로젝트 같은 경우는 한 번에 90%대로 올라갔다.

    근데 굳이 이런 부분들까지 테스트 코드를 작성하는 것에 대해서는 좀 의문점이 들기 때문에 지양하려고 한다.(만약 엄청 중요하게 작용하는 상황이 있다면 테스트 코드를 작성하겠지만 말이다) 커버리지는 80% 전후를 목표로 작업해봐야겠다. 아 그리고 사실 커버리지를 보면서 느낀 점 중 하나는 테스트 코드만 있으면 코드 영역에 대한 초록색이 뜬다. 근데 문제점은 성공 케이스 하나만 있어도 뜬다는 것이다. 실패 케이스를 소홀히 하지 않게 조심하자.

  • 리팩토링 책이 생각보다 재밌다. 아마 포비를 통해 리팩토링이나 객체지향적인 프로그래밍에 대해서 배우지 않았다면 책에서 하는 말을 이해하지 못하고 제대로 읽히지 않았을 것이다. 말로 표현하자면 포비를 통해 배우지 않았다면 책을 보면서 “이렇게까지 해야 하나?” 같은 식의 의문들만 나열됐을 상황이 “이렇게도 생각해 볼 수 있구나”, “이건 이렇지” 같은 상황으로 읽힌다. 포비의 가르침 덕분에 해당 책에서 무엇을 말하고자 하는 부분도 알게 되었지만 더불어 마틴 파울러라는 이름, 유명하고 좋은 책이라는 이유 때문에 무조건적으로 받아들이지 않고 내 생각을 한번 더 해볼 수 있게 된 것 같다. 정말 많은 걸 배웠던 것 같다. 좀 더 내 것으로 소화 시키게 노력을 하자.

var를 통한 변수 선언과 없는 경우의 차이

자바스크립트를 사용하다 보면 var 선언 없이도 변수를 사용 할 수 있습니다. 그런데 var 선언이 있는 경우와 없는 경우는 어떤 차이가 존재할까요??

한번 보도록 하죠.


첫번째 경우

일단 간단한 예제 코드와 출력 결과입니다.

코드만 보고 먼저 결과를 생각해보세요.

function foo(){
    var x = "foo";
    console.log("foo : " + x);
}

function bar(){
    x = "bar";
    console.log("bar : " + x);
}
foo();
bar();
console.log("global : " + x);

출력

foo : foo
bar : bar
global : bar

예상하신 결과가 나왔나요??

위의 코드에서 foo와 bar의 차이는 var로 x가 선언이 되어 있는지 아닌지입니다.

일단 결론부터 말씀드리면 foo의 경우 foo 스코프안에 x가 생기지만, var가 없는 bar의 경우 현재 상태에서는 전역에 x라는 변수가 생성이 됩니다.

var x;
function bar(){
    x = "bar";
    console.log("bar : " + x);
}

이런 상태인거죠.

짧은 제 자바스크립트 지식으로 생각해보면 스코프 체인을 고려하여, x를 bar에서 사용하려고 하면

  • 현재 스코프에 x가 있는지 찾아보고 있으면 사용한다.

  • 없으면 상위 스코프에서 찾는다.(찾을 때까지 반복)

  • 최상위인 전역에서 찾아보고 여기도 없으면 x를 생성한다.

위와 같은 과정을 걸치는 것 같습니다.

그럼 다시 처음 코드를 생각해보면

var x;
function foo(){
    var x = "foo";
    console.log("foo : " + x);
}

function bar(){
    x = "bar";
    console.log("bar : " + x);
}
foo();
bar();
console.log("global : " + x);

이런 경우 “global : bar”가 출력 되는게 자연스럽죠.


두번째 경우

function foo(){
    var x = "foo";
    function bar(){
        console.log("bar : " + x);
    }   
    bar();
}

foo();
console.log("global : " + x);

출력

bar : foo
console.log("global : " + x);
                           ^

ReferenceError: x is not defined

이번 같은 경우는 bar가 사용한 x는 foo의 스코프 안에 선언 되어 있는 x입니다.

위에서 설명 드린 것 처럼 상위 스코프를 찾아 가보니 x가 존재하기 때문에 전역에 새로 x를 만들지도 않고 애초에 전역 스코프까지 가지도 않습니다.

그렇기때문에 전역에는 x가 존재하지 않아서 에러가 발생합니다.

bar에 있는 var 키워드를 지우면 아래처럼

function foo(){
    x = "foo";
    function bar(){
        console.log("bar : " + x);
    }   
    bar();
}

foo();
console.log("global : " + x);
bar : foo
global : foo

정상적으로 출력을 합니다.


마치며

var를 선언이 있으면 해당 스코프에 변수를 선언하여 사용 하는 것이고, 없다면 스코프 체인을 통해 가장 가까운 스코프에서 해당 변수를 찾고 만약 전역 스코프에서도 찾지를 못하면 동적으로 변수를 만들어 사용하게 됩니다.

그렇기 때문에 거꾸로 생각하면 전역에서 var없이 사용을 하면 스코프 체인이 안 일어날 수도 있을 것 같네요.

혹시 헷갈리는 부분이 있으면 스코프(scope) 체인을 공부 해보시면 생각보다 쉽고 당연한 개념이란 걸 알수 있을겁니다. 그렇기 때문에 스코프 체인을 공부 하시는걸 추천드립니다.

그리고 혹시 아직 아리송 한 부분이 있다면 아래 코드와 결과를 보여드리는 것으로 마무리 하도록 하겠습니다.

var x = "global";
function foo(){
    x = "foo";
    function bar(){
        x = "bar";
        console.log("bar : " + x);
    }   
    console.log("foo1 : " + x);
    bar();
    console.log("foo2 : " + x);
}

console.log("global1 : " + x);
foo();
console.log("global2 : " + x);

출력

global1 : global
foo1 : foo
bar : bar
foo2 : bar
global2 : bar
GET/POST 요청에 따른 테스트 코드 작성하기

시작하며..

간단한 Post, Get 요청에 따른 테스트 코드를 보는 시간을 가지도록 하겠습니다.

이론적인 부분은 제외하고 간단한 틀을 보는 형태로 복잡한 요청/응답이 필요한 상황이 아니라면 간단한 응용만으로 자신의 프로젝트에 이용 가능 하실 겁니다.

테스트 코드만 살펴 볼 계획이기 때문에 컨트롤러 및 스프링을 통한 웹 애플리케이션 구현 코드는 테스트 코드를 보고 직접 작성 하시기 바랍니다.(여차하면 추후 추가하도록 하겠습니다.)

REST api 테스트를 하고 싶다면 REST-assured의 사용을 추천드립니다. 개인적인 생각엔 api에 대한 테스트 코드를 작성하는 점에서 REST-assured가 JSON 데이터를 다루는 방법 등 여러가지 면에서 더 편리하게 사용 가능합니다.(추후 기회가 된다면 포스팅 하도록 하겠습니다.)

요구사항

아주 간단히 로그인, 회원가입 기능만 있고, 로그인과 회원가입 성공시에는 /로 리다이렉트 하고 로그인 실패시에는 login_failed로 이동하면 됩니다. 추가로 index 페이지에서는 로그인, 회원가입 페이지로만 이동이 가능합니다.

index

  • /users/createGET 요청, 회원가입 페이지 이동

  • /users/login으로 GET 요청, 로그인 페이지 이동

회원가입

  • /users/createPOST 요청, 회원 가입 성공 시 /로 redirect

로그인

  • /users/loginPOST 요청, 로그인 성공시 /로 redirect, 실패시 login_failed.html 전송

HTML form

일단 보여질 페이지가 중요한게 아니기때문에 html은 단순하게 작성했습니다.

index.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<a href="users/login">로그인</a>
	<a href="/users/create">회원가입</a>
</body>
</html>

create.html

<form name="create" method="post" action="/users/create">
    <label for="username">사용자 아이디</label> <input class="form-control"
      id="username" name="username" placeholder="User ID">
    <label for="password">비밀번호</label> <input type="password"
      class="form-control" id="password" name="password"
      placeholder="Password">
    <label for="email">이메일</label> <input type="email"
      class="form-control" id="email" name="email" placeholder="Email">
  <button type="submit" class="btn btn-success clearfix pull-right">회원가입</button>
</form>

login.html

<form name="login" method="post" action="/users/login">
    <label for="username">사용자 아이디</label> <input class="form-control"
      id="username" name="username" placeholder="User ID">
    <label for="password">비밀번호</label> <input type="password"
      class="form-control" id="password" name="password"
      placeholder="Password">
  <button type="submit" class="btn btn-success clearfix pull-right">로그인</button>
  <div class="clearfix" />
</form>

가입의 경우 : username, password, email

로그인의 경우 : username, password


테스트 코드 작성

테스트 코드는 아래와 같은 애노테이션을 사용한 클래스에서 작성 하면 됩니다.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserAcceptanceTest {
	private static final Logger log = LoggerFactory.getLogger(MemberAcceptanceTest.class);

	@Autowired
	private TestRestTemplate template;

  ...

}

webEnvironment = WebEnvironment.RANDOM_PORT : 현재 사용하지 않고 있는 port 중에서 임의로 할당 해 줍니다.

참고로 포트를 할당 해주는 것을 보면 아시다시피 서버를 키고 실제 서버에 요청을 하는 방식으로 테스트가 진행이 됩니다. 그렇기 때문에 일반적인 테스트보다 시간이 걸리는 편입니다.

GET

@Test
public void createForm() {
  ResponseEntity<String> response = template.getForEntity("/users/create", String.class);
  assertThat(response.getStatusCode(), is(HttpStatus.OK));
  log.debug("body : {}", response.getBody());
}

@Test
public void loginForm() {
  ResponseEntity<String> response = template.getForEntity("/users/login", String.class);
  assertThat(response.getStatusCode(), is(HttpStatus.OK));
  log.debug("body : {}", response.getBody());
}

getForEntity(uri,반환될 객체 타입)

log.debug("body : {}", response.getBody())는 불필요시 지워도 되며, 응답의 body를 확인 하기 위한 용도입니다. 만약 Logger을 잘 모르신다면 System.out.println으로 출력을 해도 됩니다.

혹은,

assertThat(response.getBody().contains("특정 내용"), is(true));

//만약 회원 프로필 상세 페이지라면 유저 이메일이 body에 있는지 확인 해 본다.
assertThat(response.getBody().contains(loginUser.getEmail()), is(true));

body에 특정 내용이 포함 되어 있는지 테스트 코드를 작성 할 수도 있습니다.

POST

@Test
public void create() throws Exception {
  // 헤더 세팅
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
  headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

  // 자신이 작성한 form에 맞추어 값들을 할당 하면 됩니다.
  MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
  params.add("username", "testUser");
  params.add("password", "password");
  params.add("email", "tester@korea.kr");

  // request 생성
  HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<MultiValueMap<String, Object>>(params, headers);

  ResponseEntity<String> response = template.postForEntity("/users/create", request, String.class);

  assertThat(response.getStatusCode(), is(HttpStatus.FOUND));

  // 원한다면 Dao 혹은 Repository를 통해 실제 db에 데이터가 저장 되었는지도 확인 할 수 있습니다.
  // assertNotNull(userRepository.findByUserName("testUser"));

  // 메인 페이지로 redirect 하는 경우 이와 같이 확인 할 수 있습니다.
  assertThat(response.getHeaders().getLocation().getPath(), is("/"));
}

@Test
public void loginSuccess() throws Exception {
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
  headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

  MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
  // testUser가 db에 저장되어 있다는 가정하에 수행
  params.add("username", "testUser");
  params.add("password", "password");

  HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<MultiValueMap<String, Object>>(params, headers);

  ResponseEntity<String> response = template.postForEntity("/users/login", request, String.class);

  assertThat(response.getStatusCode(), is(HttpStatus.FOUND));
  // 세션을 활용 한다면 jsessionid가 추가되는지 여부로 login 유무를 확인 할 수 있다.
  assertTrue(response.getHeaders().getLocation().getPath().contains("/;jsessionid="));
}

template.postForEntity(url, Request, 반환될 객체 타입)

GET과 다른 과정은 header와 body에 MultiValueMap<String, Object> params로 body에 필요한 내용들을 생성하고, 그 두개를 활용하여 Request를 만든다는 것입니다.

현재 중복이 많이 일어나고 있긴 하지만 중복을 줄인 코드보다는 현재의 코드가 무엇이 필요한지 알기엔 더 나을 것 같아 그대로 놔뒀지만, 실제 작성 하실 때는 assert**위로는 중복을 제거하시는게 수월 하실 겁니다.

로그인 실패 케이스 만들기

login_failed.html

<h1>아이디 혹은 비밀번호 틀립니다.</h1>
<form name="login" method="post" action="/users/login">
    <label for="username">사용자 아이디</label> <input class="form-control"
      id="username" name="username" placeholder="User ID">
    <label for="password">비밀번호</label> <input type="password"
      class="form-control" id="password" name="password"
      placeholder="Password">
  <button type="submit" class="btn btn-success clearfix pull-right">로그인</button>
  <div class="clearfix" />
</form>
@Test
public void loginOtherPassword() {
  HttpHeaders headers = new HttpHeaders();
  headers.setAccept(Arrays.asList(MediaType.TEXT_HTML));
  headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

  MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
  // testUser가 db에 저장되어 있다는 가정하에 수행
  params.add("username", "testUser");
  //틀린 패스워드 사용
  params.add("password", "otherPassword");

  HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<MultiValueMap<String, Object>>(params, headers);

  ResponseEntity<String> response = template.postForEntity("/users/login", request, String.class);
  assertThat(response.getStatusCode(), is(HttpStatus.OK));
  assertTrue(response.getBody().contains("틀립니다."));
}

현재 실패 케이스는 한개만 만들었지만 실제로는 더욱 많은 케이스들을 만들어야 됩니다. 테스트 코드를 작성하는 초기엔 성공 케이스 위주로 작성하기 쉬운데 실패 케이스들을 소홀히 하다 보면 예상치 못한 예외 상황이 발생 할 확률이 높아 집니다.


마치며…

Controller 및 웹 애플리케이션에 대한 구현 코드는 없다보니 많이 빈약하지만 해당 Test 코드에 대한 정보를 찾는 분이라면 이정도 기능을 구현하는 것 자체에는 어려움이 없을 것 같다는 판단에 작성을 하지 않았습니다.

게다가 요구사항과 테스트 코드만 가지고 직접 만들어 보는게 더 의미도 클 것이라고 생각이 들었고, 위의 상황과 같은 경우는 너무 간단하다보니 이정도 선의 테스트 코드가 필요한 분이라면 약간의 변형을 해서 사용이 가능 할 것이라 생각됩니다.

(지속적으로 학습 하면서 더 나은 포스팅을 하도록 노력 하겠습니다.)

압축 (compression) – 6

인프런의 영리한 프로그래밍을 위한 알고리즘 강좌를 보고 작성한 문서입니다.


인코딩하기

  • 압축파일의 맨 앞부분(header)에 파일을 구성하는 run들에 대한 정보를 기록한다.

  • 이때 원본 파일의 길이도 함께 기록한다(왜 필요할까?)


outputFrequencies

// fIn은 입출할 파일, fOut은 압축된 파일이다.
private void outputFrequencies(RandomAccessFile fIn, RandomAccessFile fOut) throws IOException {
  // 먼저 run의 개수를 하나의 정수로 출력한다.
  fOut.writeInt(runs.size());

  // 원본 파일의 크기(byte단위)를 출력한다.
  fOut.writeLong(fIn.getFilePointer());

  // 각각의 run들을 출력한다.
  for (int j =0; j < runs.size(); j++){
    Run r = runs.get(j);
    fOut.write(r.symbol); //write a byte
    fOut.writeInt(r.runLen);
    fOut.writeInt(r.freq);
  }
}

compressFile

// fIN은 압축할 파일, inFileName은 그 파일의 이름이다. 파일의 이름을 추가로 바든ㄴ 이유는 압축된 파일의 이름을 정하기 위해서이다.
public void compressFile(String inFileName, RandomAccessFile fIn) throws IOException {
  // 압축파일의 이름은 압축할 파일의 이름에 확장자를 .z를 붙인 것이다.
  String outFileName = new String(inFileName + ".z");

  // 압축파일을 여기서 생성하여 outputFrequencies와 encode메서드에게 제공한다.
  RandomAccessFile fOut = new RandomAccessFile(outFileName, "rw");

  collectRuns(fIn);
  outputFrequencies(fIn, fOut);
  createHuffmanTree();
  assignCodewords(theRoot, 0, 0);
  storeRunsIntohashMap(theRoot);
  fIn.seek(0);
  encode(fIn, fOut);
}

main

public class HuffmanCoding {
  ...

  public void compressFile(String inFileName, RandomAccessFile fIn) throws IOException {
    ...
  }

  static public void main(String args[]){
    HuffmanCoding app = new HuffmanCoding();
    RandomAccessFile fIn;
    try {
      fIn = new RandomAccessFile("sample.txt", "r");
      app.copressFile("sample.txt", fIn);
      fIn.close();
    } catch (IOException io) {
      System.err.println("Cannot open " + fileName);
    }
  }
}

encode()

huffman_encode1

private void encode(RandomAccessFile fIn, RandomAccessFile fOut) {
  while there remains bytes to read in the file {
    recongnise a run;
    find the codeword for the rn;
    pack the codeword into the buffer;
    if the buffer becomes full
          write the buffer into the compressed file;
  }
  if buffer is not empty {
    append 0s into the buffer;
    write the buffer into the compressed file;
  }
}

class HuffmanEncoder

public class HuffmanEncoder {
  static public void main(String args[]){
    String fileName = "";
    HuffmanCoding app = new HuffmanCoding();
    RandomAccessFile fIn;
    Scanner kb = new Scanner(System.in);
    try {
      System.out.print("Enter a file name: ");
      fileName = kb.next();
      fIn = new RandomAccessFile(fileName, "r");
      app.compressFile(fileName, fIn);
      fIn.close();
    } catch (IOException io) {
      System.err.println("cannot open " + fileName);
    }
  }
}
압축 (compression) – 5

인프런의 영리한 프로그래밍을 위한 알고리즘 강좌를 보고 작성한 문서입니다.


Codeword 검색하기

  • 데이터 파일을 압축하기 위해서는 데이터 파일을 다시 시작부터 읽으면서 run을 하나씩 인식한 후 해당 run에 부여된 codeword를 검색한다.

  • Huffman트리에는 모든 run들이 리프노드에 위치하므로 검색하기 불편하다.

  • 검색하기 편리한 구조를 만들어야 한다.


Array of Linked Lists

Array_of_Linked_Lists1


storeRunsIntoArray

private Run [] chars = new Run [256];
// Huffman 트리의 모든 리프노드들을 chars에 recursion으로 저장한다.
private void storeRunsIntoArray( Run p ) {
  if (p.left == null && p.right == null ){
    insertToArray(p); // 배열 chars[(unsigned int)p.symbol)]가 가리키는 연결리스트의 맨 앞에 p를 삽입한다.
  }
  else {
    storeRunsIntoArray(p.left);
    storeRunsIntoArray(p.right);
  }
}

public void compressFile(RandomAccessFile fIn){
  collectRuns(fIn);
  createHuffmanTree();
  assignCodeWords(theRoot, 0, 0);
  storeRunsIntoArray(theRoot);
}

Run 검색하기

  • Symbol과 runLength가 주어질 때 배열 chars를 검색하여 해당하는 run을 찾아 반환하는 메서드를 작성한다.

    public Run findRun(byte symbol, int length){
      // 배열 chars에서 (symbol, length)에 해당하는 run을 찾아 반환한다.
    }