상세 컨텐츠

본문 제목

[TDD] SoundEX 개발하기 2번째

IT/프로그래밍

by James Lee. 2015. 12. 6. 20:01

본문


SoundEX Review

TDD를 어느정도 익숙해지고 다시 연습해 보았다.

문제의 요구사항

  • 각 알파벳은 다음과 같이 치환한다.

    • b, f, p, v → 1
    • c, g, j, k, q, s, x, z → 2
    • d, t → 3
    • l → 4
    • m, n → 5
    • r → 6
  • 모음(a, e, i, o, u)와 h, w는 생략한다.

  • 중복된 치환 값이 오면 제거한다. 단, 조건이 있다.

    • 값 사이에 모음이 오는 경우, 앞 자리와 뒷 자리를 비교하지 않고 넘어간다. ex) 1a1 -> 11
    • 값 사이에 h, w가 오는 경우, 앞 자리와 뒷 자리를 비교하여 중복이면 제거한다. ex) 1h1 -> 1
  • 결과값은 4자리까지만 표현한다.

    • 4자리 미만을 입력하는 경우 0으로 채운다

요구사항 테이블

inputoutput
aa000
RobertR163
AshcraftA261
TymczakT522
PfisterP236


설계

  1. 입력값이 들어온다.
  2. 입력값이 들어오면 (a11a2h3)와 같은 형태로 변환한다. (중복을 제거하는 과정에서 모음인 경우와 h,w의 경우 처리방법이 다르기 때문에 구분해줘야 한다)
  3. 치환된 값이 들어오면 중복을 제거한다.
  4. 나온 값에 0을 붙인 후 4자리까지 잘라낸다.

1번째 TDD Cycle

실패하는 테스트 코드 작성하기

테스트 클래스 SoundEXTest를 작성하고 

Caller Create 방식으로 프로덕션 클래스인 SoundEX를 생성한다.

가장 간단한 테스트부터 실행한다.

public class SoundEXTest {
 SoundEX se = new SoundEX();
 @Test
 public void testX() {
  assertEquals("a", se.convertName("a"));
 }
}

테스트 코드 통과시키기

 public String convertName(String inputName) {
  return "a";
 }

리팩토링

없음

코드 커밋하기

2번째 TDD Cycle

실패하는 테스트 케이스 작성하기

 @Test
 public void testX() {
  assertEquals("a", se.convertName("a"));
  assertEquals("b", se.convertName("b"));
 }

테스트 코드 통과시키기

가장 간단하게 통과시키는 것이므로 "b"가 들어왔을때 "b000"을 반환시키도록 한다.

 public String convertName(String inputName) {
  if (inputName == "b") {
   return "b";
  }
  return "a";
 }

리팩토링

공백에서 중복을 찾아내면 아래와 같다.

 public String convertName(String inputName) {
  if (inputName == "b") {
   return "b";
  }
  if (inputName == "a") {
   return "a";
  }
  return "";
 }

리팩토링

반환값에 적절한 이름인 result를 붙여준다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName == "b") {
   result = "b";
  }
  if (inputName == "a") {
   result = "a";
  }
  return result;
 }

리팩토링

앞에 나오는 b와 a는 inputName을 붙여준 것이다.

이제 두 if문 안의 내용이 완전히 동일해졌다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName == "b") {
   result = inputName;
  }
  if (inputName == "a") {
   result = inputName;
  }
  return result;
 }

리팩토링

if문 내부의 중복을 제거한다

 public String convertName(String inputName) {
  String result = "";
  result = inputName;
  return result;
 }

코드 커밋하기

2번째 TDD Cycle

실패하는 테스트 코드 작성하기

실패하는 테스트 케이스를 작성할 수 없다.

일반화된 솔루션이 제작되었다고 판단한다. (1글자일 경우에)

 @Test
 public void testX() {
  assertEquals("a", se.convertName("a"));
  assertEquals("b", se.convertName("b"));
  assertEquals("c", se.convertName("c"));
 }

실패하는 테스트 코드 작성하기

그렇다면 2글자의 이름을 시도해본다.

doubleNameTest라는 새로운 테스트 케이스를 만든다. (테스트 케이스와 테스트 코드의 차이.. 테스트 케이스는 테스트 메소드인가?)

 @Test
 public void doubleNameTest() {
  assertEquals("a1", se.convertName("ab"));
 }

테스트 코드 통과시키기

 public String convertName(String inputName) {
  String result = "";
  if (inputName == "ab") {
   result ="a1";
  } else {
   result = inputName;
  }
  return result;
 }

리팩토링

아래와 같이 표현할 수 있다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName == "ab") {
   result += "a";
   result += "1";
  } else {
   result += inputName;
  }
  return result;
 }

리팩토링

반복되는 result+="0";을 반복문을 이용하여 표현한다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName == "ab") {
   result += "a";
   result += "1";
  } else {
   result += inputName;
  }
  return result;
 }

리팩토링

result에 맨 처음 더해주는 값은 inputName의 첫번째 알파벳이다.

따라서 아래와 같이 표현한다.

점점 중복의 형태를 띄고 있다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName == "ab") {
   result += inputName.charAt(0);
   result += "1";
  } else {
   result += inputName.charAt(0);
  }
  return result;
 }

리팩토링

위쪽 if문에서 result+="1";은 무엇인가

inputName의 두번째 글자인 b가 치환 요구사항에 따라 치환된 값이다.

따라서 치환 요구사항을 처리해주는 기능을 작성한다.

 public String getCode(char ch) {
  String result = "";
  switch (ch) {
  case 'b':
  case 'f':
  case 'p':
  case 'v':
   result = "1";
   break;
  case 'c':
  case 'g':
  case 'j':
  case 'k':
  case 's':
  case 'x':
  case 'z':
   result = "2";
   break;
  case 'd':
  case 't':
   result = "3";
   break;
  case 'l':
   result = "4";
   break;
  case 'm':
  case 'n':
   result = "5";
   break;
  case 'r':
   result = "6";
   break;
  }
  return result;
 }


 public String convertName(String inputName) {
  String result = "";
  if (inputName == "ab") {
   result += inputName.charAt(0);
   result += getCode(inputName.charAt(1));
  } else {
   result += inputName.charAt(0);
  }
  return result;
 }

리팩토링

inputName이 2글자인 경우와 1글자인 경우로 구분한다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName.length() == 2) {
   result += inputName.charAt(0);
   result += getCode(inputName.charAt(1));
  }
  if (inputName.length() == 1) {
   result += inputName.charAt(0);
  }
  return result;
 }

코드 커밋하기

세번째 TDD Cycle

실패하는 테스트 코드 작성하기

aa를 입력하면 getCode에는 모음을 변환하는 기능이 아직 없기 때문에 오류가 난다.

모음과 (h,w)는 중복 제거에서 각각 따로 처리해야 하므로 구분을 해줘야 한다.

 @Test
 public void doubleNameTest() {
  assertEquals("a1", se.convertName("ab"));
  assertEquals("a2", se.convertName("ac"));
  assertEquals("a3", se.convertName("ad"));
  assertEquals("aa", se.convertName("aa")); // 오류
 }

리팩토링

모음인지 h,w인지에 따라 따로 처리를 해줘야 되기 때문에 getCode를 모음이면 "a" (h, w)이면 "h"로 반환하도록 한다. (모음끼리 구별하거나 h,w끼리 구별하는 것은 의미가 없다)

 public String getCode(char ch) {
  String result = "";
  switch (ch) {
  case 'b':
  case 'f':
  case 'p':
  case 'v':
   result = "1";
   break;
  case 'c':
  case 'g':
  case 'j':
  case 'k':
  case 's':
  case 'x':
  case 'z':
   result = "2";
   break;
  case 'd':
  case 't':
   result = "3";
   break;
  case 'l':
   result = "4";
   break;
  case 'm':
  case 'n':
   result = "5";
   break;
  case 'r':
   result = "6";
   break;
  //모음 처리
  case 'a':
  case 'e':
  case 'i':
  case 'o':
  case 'u':
   result = "a";
   break;
  //h, w 처리
  case 'h':
  case 'w':
   result = "h";
   break;
  }
  return result;
 }

코드 커밋하기

네번째 TDD Cycle

실패하는 테스트 코드 작성하기

3개의 이름을 변환 시도한다.

이름이 3개일 경우의 처리는 아직 프로덕션 코드에 없으므로 테스트 케이스는 실패한다.

 @Test
 public void threeNameTest() {
  assertEquals("a12", se.convertName("abc"));
 }

테스트 코드 통과시키기

3글자일때도 이전 방식과 동일하게 적용해준다.

3번째 글자에도 변환 과정을 동일하게 적용해준다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName.length() == 3) {
   result += inputName.charAt(0);
   result += getCode(inputName.charAt(1));
   result += getCode(inputName.charAt(2));
  }
  if (inputName.length() == 2) {
   result += inputName.charAt(0);
   result += getCode(inputName.charAt(1));
  }
  if (inputName.length() == 1) {
   result += inputName.charAt(0);
  }
  return result;
 }

리팩토링

getCode로 변환시키는 중복된 과정을 반복문을 이용하여 바꾸면 아래와 같다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName.length() == 3) {
   result += inputName.charAt(0);
   for (int i = 1; i < 3; i++) {
    result += getCode(inputName.charAt(i));
   }
  }
  if (inputName.length() == 2) {
   result += inputName.charAt(0);
   for (int i = 1; i < 2; i++) {
    result += getCode(inputName.charAt(i));
   }
  }
  if (inputName.length() == 1) {
   result += inputName.charAt(0);
   for (int i = 1; i < 1; i++) {
    result += getCode(inputName.charAt(i));
   }
  }
  return result;
 }

리팩토링

반복문의 최대 반복횟수는 현재 inputName의 길이와 같다.

 public String convertName(String inputName) {
  String result = "";
  if (inputName.length() == 3) {
   result += inputName.charAt(0);
   for (int i = 1; i < inputName.length(); i++) {
    result += getCode(inputName.charAt(i));
   }
  }
  if (inputName.length() == 2) {
   result += inputName.charAt(0);
   for (int i = 1; i < inputName.length(); i++) {
    result += getCode(inputName.charAt(i));
   }
  }
  if (inputName.length() == 1) {
   result += inputName.charAt(0);
   for (int i = 1; i < inputName.length(); i++) {
    result += getCode(inputName.charAt(i));
   }
  }
  return result;
 }

리팩토링

각 if문의 내부가 완전히 동일해졌으므로 통일한다.

 public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  return result;
 }

코드 커밋하기

다섯번째 TDD Cycle

실패하는 테스트 코드 작성하기

여러가지 케이스를 시도해봐도 실패하는 테스트 케이스를 찾을 수 없다.

따라서 치환하는 기능에 대한 일반적인 솔루션이 작성되었다고 판단한다.

 @Test
 public void threeNameTest() {
  assertEquals("a12", se.convertName("abc"));
  assertEquals("a1a", se.convertName("aba"));
  assertEquals("aaa", se.convertName("aae"));
  assertEquals("ahh", se.convertName("ahw"));
 }

여섯번째 TDD Cycle

중복을 제거하는 기능을 작성한다.

실패하는 테스트 코드 작성하기

 @Test
 public void removeDuplicationTest() {
  assertEquals("a1213", se.removeDuplication("a1122113"));
 }

테스트 코드 통과시키기

 public String removeDuplication(String duplicatedString) {
  String result = "";
  if (duplicatedString == "a1122113") {
   result = "a1213";
  }
  return result;
 }

리팩토링

중복이 제거되는 기능은 아래와 같은 과정을 거친다.

 public String removeDuplication(String duplicatedString) {
  String result = "";
  if (duplicatedString == "a1122113") {
   result += "a";
   result += "1";
   result += "2";
   result += "1";
   result += "3";
  }
  return result;
 }

리팩토링

 public String removeDuplication(String duplicatedString) {
  String result = "";
  if (duplicatedString == "a1122113") {
   result += duplicatedString.charAt(0);
   result += duplicatedString.charAt(1);
   result += duplicatedString.charAt(3);
   result += duplicatedString.charAt(5);
   result += duplicatedString.charAt(7);
  }
  return result;
 }

리팩토링

 public String removeDuplication(String duplicatedString) {
  String result = "";
  if (duplicatedString == "a1122113") {
   result += duplicatedString.charAt(0);
   result += duplicatedString.charAt(1);
   result += duplicatedString.charAt(3);
   result += duplicatedString.charAt(5);
   result += duplicatedString.charAt(7);
  }
  return result;
 }

리팩토링

 public String removeDuplication(String duplicatedString) {
  String result = "";
  if (duplicatedString == "a1122113") {
   result += duplicatedString.charAt(0);
   if (duplicatedString.charAt(1) != duplicatedString.charAt(0)) {
    result += duplicatedString.charAt(1);
   }
   if (duplicatedString.charAt(3) != duplicatedString.charAt(2)) {
    result += duplicatedString.charAt(3);
   }
   if (duplicatedString.charAt(5) != duplicatedString.charAt(4)) {
    result += duplicatedString.charAt(5);
   }
   if (duplicatedString.charAt(7) != duplicatedString.charAt(6)) {
    result += duplicatedString.charAt(7);
   }
  }
  return result;
 }

리팩토링

 public String removeDuplication(String duplicatedString) {
  String result = "";
  if (duplicatedString == "a1122113") {
   result += duplicatedString.charAt(0);
   if (duplicatedString.charAt(1) != duplicatedString.charAt(0)) {
    result += duplicatedString.charAt(1);
   }
   if (duplicatedString.charAt(2) != duplicatedString.charAt(1)) {
    result += duplicatedString.charAt(2);
   }
   if (duplicatedString.charAt(3) != duplicatedString.charAt(2)) {
    result += duplicatedString.charAt(3);
   }
   if (duplicatedString.charAt(4) != duplicatedString.charAt(3)) {
    result += duplicatedString.charAt(4);
   }
   if (duplicatedString.charAt(5) != duplicatedString.charAt(4)) {
    result += duplicatedString.charAt(5);
   }
   if (duplicatedString.charAt(6) != duplicatedString.charAt(5)) {
    result += duplicatedString.charAt(6);
   }
   if (duplicatedString.charAt(7) != duplicatedString.charAt(6)) {
    result += duplicatedString.charAt(7);
   }
  }
  return result;
 }

리팩토링

중복을 반복문으로 제거하면 아래와 같다.

 public String removeDuplication(String duplicatedString) {
  String result = "";
  if (duplicatedString == "a1122113") {
   result += duplicatedString.charAt(0);
   for (int i = 1; i < 8; i++) {
    if (duplicatedString.charAt(i) != duplicatedString
      .charAt(i - 1)) {
     result += duplicatedString.charAt(i);
    }
   }
  }
  return result;
 }

리팩토링

8은 duplicationString의 길이를 의미하므로 length로 바꿔주고

duplicationString.charAt(i-1);은 이전 문자와 비교한다는 뜻이므로

previousChar변수를 추가하여 가독성을 명확하게 한다.

 public String removeDuplication(String duplicatedString) {
  String result = "";
  char previousChar;
  if (duplicatedString == "a1122113") {
   result += duplicatedString.charAt(0);
   previousChar = getCode(duplicatedString.charAt(0)).charAt(0);
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) != previousChar) {
     result += duplicatedString.charAt(i);
     previousChar = duplicatedString.charAt(i);
    }
   }
  }
  return result;
 }

코드 커밋하기

일곱번째 TDD Cycle

실패하는 테스트 코드 작성하기

 @Test
 public void removeDuplicationTest() {
  assertEquals("a1213", se.removeDuplication("a1122113"));
  assertEquals("a121312", se.removeDuplication("a111112221312"));
 }

테스트 코드 통과시키기

이전의 코드를 그대로 써도 통과된다.

 public String removeDuplication(String duplicatedString) {
  String result = "";
  char previousChar;
  if (duplicatedString == "a111112221312") {
   result += duplicatedString.charAt(0);
   previousChar = getCode(duplicatedString.charAt(0)).charAt(0);
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) != previousChar) {
     result += duplicatedString.charAt(i);
     previousChar = duplicatedString.charAt(i);
    }
   }
  }
  if (duplicatedString == "a1122113") {
   result += duplicatedString.charAt(0);
   previousChar = getCode(duplicatedString.charAt(0)).charAt(0);
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) != previousChar) {
     result += duplicatedString.charAt(i);
     previousChar = duplicatedString.charAt(i);
    }
   }
  }
  return result;
 }

리팩토링

if문의 중복된 부분을 통합한다.

 public String removeDuplication(String duplicatedString) {
  String result = "";
  char previousChar;
  if (duplicatedString.contains("a")) {
   result = "A11";
  } else {
   result += duplicatedString.charAt(0);
   previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
     .charAt(0);
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) != previousChar) {
     result += duplicatedString.charAt(i);
     previousChar = duplicatedString.charAt(i);
    }
   }
  }
  return result;
 }

코드 커밋하기

여덟번째 TDD Cycle

실패하는 테스트 코드 작성하기

a가 들어가있을때는 뒤에 오는 문자를 붙인다.

이 기능을 따로 처리하는 구문이 없기 때문에 테스트 코드는 실패한다.

 assertEquals("A11", se.removeDuplication("A1a1")); //오류

테스트 코드 통과시키기

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a11") {
    result = "A11";
   }
  }

리팩토링

없다.

코드 커밋하기

아홉번째 TDD Cycle

실패하는 테스트 코드 작성하기

  assertEquals("A11", se.removeDuplication("A1a1")); 
  assertEquals("A11", se.removeDuplication("A1a11")); //오류

통과하는 테스트 코드 작성하기

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result = "A11";
   }
   if (duplicatedString == "A1a11") {
    result = "A11";
   }
  }

리팩토링

중복이 제거된 문자열이 나오는 과정을 하나씩 보면 아래와 같다.

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += "A";
    result += "1";
    result += "1";
   }
   if (duplicatedString == "A1a11") {
    result += "A";
    result += "1";
    result += "1";
   }
  } 

리팩토링

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    result += duplicatedString.charAt(1);
    result += duplicatedString.charAt(3);
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    result += duplicatedString.charAt(1);
    result += duplicatedString.charAt(3);
   }
  } 

리팩토링

비교 대상 문자와 비교하여 일치하지 않을때만 result에 값이 추가된다.

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    if (duplicatedString.charAt(1) != getCode(duplicatedString.toLowerCase().charAt(0)).charAt(0))
    {
     result += duplicatedString.charAt(1);
    }
    if(duplicatedString.charAt(3)!=duplicatedString.charAt(2))
    {
     result += duplicatedString.charAt(3);
    }
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    if (duplicatedString.charAt(1) != getCode(duplicatedString.toLowerCase().charAt(0)).charAt(0))
    {
     result += duplicatedString.charAt(1);
    }
    if(duplicatedString.charAt(3)!=duplicatedString.charAt(2))
    {
     result += duplicatedString.charAt(3);
    }
    if(duplicatedString.charAt(4)!=duplicatedString.charAt(3))
    {
     result += duplicatedString.charAt(4);
    }
   }

리팩토링

가독성을 명확하게 하기 위하여 비교 대상 문자를 previousChar에 저장한다. 

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     result += duplicatedString.charAt(1);
     previousChar = duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     result += duplicatedString.charAt(3);
     previousChar = duplicatedString.charAt(3);
    }
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     result += duplicatedString.charAt(1);
     previousChar = duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     result += duplicatedString.charAt(3);
     previousChar = duplicatedString.charAt(3);
    }
    if (duplicatedString.charAt(4) != previousChar) {
     result += duplicatedString.charAt(4);
    }
   }
  } 

리팩토링

아래의 코드를 보자

A1a1의 3번째 a가 나온 부분에서는 값을 더하지 않고 비교문자만 옮긴 것을 알 수 있다.

   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     result += duplicatedString.charAt(1);
     previousChar = duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(2); //여기는 왜 순차적으로 더해지지 않았을까?
    if (duplicatedString.charAt(3) != previousChar) {
     result += duplicatedString.charAt(3);
     previousChar = duplicatedString.charAt(3);
    }
   }

따라서 아래와 같은 코드가 생략되었다고 볼 수 있다. (A1a11도 마찬가지)

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     result += duplicatedString.charAt(1);
     previousChar = duplicatedString.charAt(1);
    }
    if (duplicatedString.charAt(2) != 'a') { //추가
     result += duplicatedString.charAt(2);
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     result += duplicatedString.charAt(3);
     previousChar = duplicatedString.charAt(3);
    }
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     result += duplicatedString.charAt(1);
     previousChar = duplicatedString.charAt(1);
    }
    if (duplicatedString.charAt(2) != 'a') { //추가
     result += duplicatedString.charAt(2);
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     result += duplicatedString.charAt(3);
     previousChar = duplicatedString.charAt(3);
    }
    if (duplicatedString.charAt(4) != previousChar) {
     result += duplicatedString.charAt(4);
    }
   }
  }

리팩토링

하지만 들어온 문자가 a임에도 불구하고 여전히 비교 문자는 옮겼음을 알 수 있다.

    if (duplicatedString.charAt(2) != 'a') { //추가
     result += duplicatedString.charAt(2);
    }
    previousChar = duplicatedString.charAt(2);

이를 통해서 비교문자를 옮기는 부분 previousChar = duplicatedString.charAt(2);은 문자가 더해지든 안더해지든 실행됨을 알 수 있다.

  if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     result += duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(1);
    if (duplicatedString.charAt(2) != 'a') { 
     result += duplicatedString.charAt(2);
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     result += duplicatedString.charAt(3);
    }
    previousChar = duplicatedString.charAt(3);
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     result += duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(1);
    if (duplicatedString.charAt(2) != 'a') {
     result += duplicatedString.charAt(2);
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     result += duplicatedString.charAt(3);
    }
    previousChar = duplicatedString.charAt(3);
    if (duplicatedString.charAt(4) != previousChar) {
     result += duplicatedString.charAt(4);
    }
    previousChar = duplicatedString.charAt(4);
   }
  }

리팩토링

그리고 a를 검사하는 부분도 각 검사 구문에 동일하게 적용된다.

이제 각 if검사구문이 동일해졌음을 알 수 있다. 어서 중복을 제거하고싶다.....어서

   if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     if (duplicatedString.charAt(1) != 'a')
      result += duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(1);
    if (duplicatedString.charAt(2) != previousChar) {
     if (duplicatedString.charAt(2) != 'a') { 
      result += duplicatedString.charAt(2);
     }
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     if (duplicatedString.charAt(3) != 'a')
      result += duplicatedString.charAt(3);
    }
    previousChar = duplicatedString.charAt(3);
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    if (duplicatedString.charAt(1) != previousChar) {
     if (duplicatedString.charAt(1) != 'a')
      result += duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(1);
    if (duplicatedString.charAt(2) != previousChar) {
     if (duplicatedString.charAt(2) != 'a') { 
      result += duplicatedString.charAt(2);
     }
    }
    previousChar = duplicatedString.charAt(2);
    if (duplicatedString.charAt(3) != previousChar) {
     if (duplicatedString.charAt(3) != 'a')
      result += duplicatedString.charAt(3);
    }
    previousChar = duplicatedString.charAt(3);
    if (duplicatedString.charAt(4) != previousChar) {
     if (duplicatedString.charAt(4) != 'a')
      result += duplicatedString.charAt(4);
    }
    previousChar = duplicatedString.charAt(4);
   }
  }

리팩토링

드디어 반복문을 이용하여 중복을 제거한다!

   if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    for (int i = 1; i < 4; i++) {
     if (duplicatedString.charAt(i) != previousChar) {
      if (duplicatedString.charAt(i) != 'a')
       result += duplicatedString.charAt(i);
     }
     previousChar = duplicatedString.charAt(i);
    }
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    for (int i = 1; i < 5; i++) {
     if (duplicatedString.charAt(i) != previousChar) {
      if (duplicatedString.charAt(i) != 'a')
       result += duplicatedString.charAt(i);
     }
     previousChar = duplicatedString.charAt(i);
    }
   }
  }

리팩토링

각 for문의 최대 반복 횟수인 4와 5는, duplicateString.length()와 같다.

   if (duplicatedString.contains("a")) {
   if (duplicatedString == "A1a1") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    for (int i = 1; i < duplicatedString.length(); i++) {
     if (duplicatedString.charAt(i) != previousChar) {
      if (duplicatedString.charAt(i) != 'a')
       result += duplicatedString.charAt(i);
     }
     previousChar = duplicatedString.charAt(i);
    }
   }
   if (duplicatedString == "A1a11") {
    result += duplicatedString.charAt(0);
    previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
      .charAt(0);
    for (int i = 1; i < duplicatedString.length(); i++) {
     if (duplicatedString.charAt(i) != previousChar) {
      if (duplicatedString.charAt(i) != 'a')
       result += duplicatedString.charAt(1);
     }
     previousChar = duplicatedString.charAt(i);
    }
   }
  }

리팩토링

이제 두개의 if문 내부가 완전히 동일해졌으므로 if문을 제거하고 코드를 통합한다.

   if (duplicatedString.contains("a")) {
   result += duplicatedString.charAt(0);
   previousChar = getCode(duplicatedString.toLowerCase().charAt(0)).charAt(0);
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) != previousChar) {
     if (duplicatedString.charAt(i) != 'a')
      result += duplicatedString.charAt(1);
    }
    previousChar = duplicatedString.charAt(i);
   }
  }

현재 removeDuplication 메소드의 코드는 아래와 같다

  public String removeDuplication(String duplicatedString) {
  String result = "";
  char previousChar;
  if (duplicatedString.contains("a")) {
   result += duplicatedString.charAt(0);
   previousChar = getCode(duplicatedString.toLowerCase().charAt(0)).charAt(0);
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) != previousChar) {
     if (duplicatedString.charAt(i) != 'a')
      result += duplicatedString.charAt(i);
    }
    previousChar = duplicatedString.charAt(i);
   }
  } else {
   result += duplicatedString.charAt(0);
   previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
     .charAt(0);
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) != previousChar) {
     result += duplicatedString.charAt(i);
    }
    previousChar = duplicatedString.charAt(i);
   }
  }
  return result;
 }

리팩토링

a가 포함되어 있을때와 포함되어 있지 않을때의 코드가 거의 같아졌다. 

사실 a가 포함되어 있을때만 현재 문자가 a인지 검사하고 더해주는 것이므로 a가 포함되어 있지 않을때와 검사 구문이 똑같다고 할 수 있다.

따라서 두 if else 문도 통합한다.

  public String removeDuplication(String duplicatedString) {
  String result = "";
  char previousChar;
  result += duplicatedString.charAt(0);
  previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
    .charAt(0);
  for (int i = 1; i < duplicatedString.length(); i++) {
   if (duplicatedString.charAt(i) != previousChar) {
    if (duplicatedString.charAt(i) != 'a')
     result += duplicatedString.charAt(i);
   }
   previousChar = duplicatedString.charAt(i);
  }
  return result;
 }

리팩토링

하지만 코드를 심플하게 하는 데에는 가독성이 굉장히 중요하다고 배워왔다.

위의 코드는 과연 가독성이 좋다고 할 숫 있을까?

아니다.. if문 내부에 if문이 또 있기 때문에 생각이 굉장히 복잡해지게 된다.

따라서 우리는 가독성을 더 좋게 만들 방법에 대하여 생각해봐야 한다.

가드 클로즈라는 말을 아는가?

아래 코드에서 주석 표시한 if문은 != 논리 연산자를 이용했다.

  for (int i = 1; i < duplicatedString.length(); i++) {
   if (duplicatedString.charAt(i) != previousChar) { //여기
    if (duplicatedString.charAt(i) != 'a') //여기
     result += duplicatedString.charAt(i);
   }
   previousChar = duplicatedString.charAt(i);
  }

이것을 반대로 표현하면 아래와 같다.

아무것도 실행하지 않고 바로 previouseChar = duplicateString.charAt( i );를 실행하는 것이다. 그렇다면 이것을 좀 더 단순하게 표현한다면?

  for (int i = 1; i < duplicatedString.length(); i++) {
   if (duplicatedString.charAt(i) == previousChar) {
    //아무것도 하지 않는다.
   } else {
    if (duplicatedString.charAt(i) == 'a') {
     //아무것도 하지 않는다.
    } else {
     result += duplicatedString.charAt(i);
    }
   }
   previousChar = duplicatedString.charAt(i);
  }

아래처럼 continue를 써서 건너뛴다는 것을 좀더 간단명료하게 표현할 수 있다.

이것을 가드 클로즈라고 한다.

하지만 previouseChar = duplicateString.charAt( i );은 꼭 실행되야 하기 때문에 continue문 위에 들어가야 된다. 이것은 중복 문장이다.

이것을 어떻게 해결하면 좋을까?

  for (int i = 1; i < duplicatedString.length(); i++) {
   if (duplicatedString.charAt(i) == previousChar) {
    previousChar = duplicatedString.charAt(i);
    continue;
   }
   if (duplicatedString.charAt(i) == 'a') {
    previousChar = duplicatedString.charAt(i);
    continue;
   }
   result += duplicatedString.charAt(i);
   previousChar = duplicatedString.charAt(i);
  }

생각해보면 현재 문자열이 이전 문자열과 같을때에는 이전 문자열이 비교 문자열에 들어가 있을 테니까 굳이 현재 문자열을 비교문자열에 넣을 필요가 없다.

  for (int i = 1; i < duplicatedString.length(); i++) {
   if (duplicatedString.charAt(i) == previousChar) {
    previousChar = duplicatedString.charAt(i); //불필요하므로 생략
    continue;
   }
   if (duplicatedString.charAt(i) == 'a') {
    previousChar = duplicatedString.charAt(i);
    continue;
   }
   result += duplicatedString.charAt(i);
   previousChar = duplicatedString.charAt(i);
  }

리팩토링

  for (int i = 1; i < duplicatedString.length(); i++) {
   if (duplicatedString.charAt(i) == previousChar) {
    continue;
   }
   if (duplicatedString.charAt(i) == 'a') {
    previousChar = duplicatedString.charAt(i);
    continue;
   }
   result += duplicatedString.charAt(i);
   previousChar = duplicatedString.charAt(i);
  }

코드 커밋하기


열번째 TDD Cycle

실패하는 테스트 코드 작성하기

h, w에 대한 테스트 코드 작성

 @Test
 public void removeDuplicationTest() {
  assertEquals("A1213", se.removeDuplication("A1122113"));
  assertEquals("A121312", se.removeDuplication("A111112221312"));
  assertEquals("A11", se.removeDuplication("A1a1")); 
  assertEquals("A11", se.removeDuplication("A1a11"));
  
  //h 비교
  assertEquals("A1", se.removeDuplication("A1h1"));
 }

테스트 코드 통과시키기

들어온 문자열에 h가 포함되어 있을때 "A1"반환

 public String removeDuplication(String duplicatedString) {
  String result = "";
  result += duplicatedString.charAt(0);
  char previousChar = getCode(duplicatedString.toLowerCase().charAt(0))
    .charAt(0);
  if (duplicatedString.contains("h")) {
   result = "A1";
  } else {
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) == previousChar) {
     continue;
    }
    if (duplicatedString.charAt(i) == 'a') {
     previousChar = duplicatedString.charAt(i);
     continue;
    }
    result += duplicatedString.charAt(i);
    previousChar = duplicatedString.charAt(i);
   }
  }
  return result;
 }

리팩토링

A1h1에서 A1이 나온 이유는 무엇일까?

h는 a와 똑같은 원리로 사라질 것이다.

하지만 h뒤에 나오는 1이 사라진 이유는?

h전에 나왔던 1과 비교를 한 것이다.

그렇다면 h가 나왔을때 비교대상 문자열을 옮기지 않았다는 것이다.

a였을때의 처리 코드를 그대로 가져와서 'h'와 비교하는 부분과 비교문자열을 옮기는 부분을 수정했다.

  if (duplicatedString.contains("h")) {
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) == previousChar) {
     continue;
    }
    if (duplicatedString.charAt(i) == 'h') {
     // previousChar = duplicatedString.charAt(i);
     continue;
    }
    result += duplicatedString.charAt(i);
    previousChar = duplicatedString.charAt(i);
   }
  }

이제 두 소스의 형태가 비슷해졌다.

if (duplicatedString.contains("h")) {
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) == previousChar) {
     continue;
    }
    if (duplicatedString.charAt(i) == 'h') {
     continue;
    }
    result += duplicatedString.charAt(i);
    previousChar = duplicatedString.charAt(i);
   }
  } else {
   for (int i = 1; i < duplicatedString.length(); i++) {
    if (duplicatedString.charAt(i) == previousChar) {
     continue;
    }
    if (duplicatedString.charAt(i) == 'a') {
     previousChar = duplicatedString.charAt(i);
     continue;
    }
    result += duplicatedString.charAt(i);
    previousChar = duplicatedString.charAt(i);
   }
  }

리팩토링

두 if문을 합친다.

이렇게 중복을 제거하는 기능도 완료되었다.

  public String removeDuplication(String duplicatedString) {
  String result = "";
  result += duplicatedString.charAt(0);
  char previousChar = getCode(duplicatedString.toLowerCase().charAt(0)).charAt(0);
  for (int i = 1; i < duplicatedString.length(); i++) {
   if (duplicatedString.charAt(i) == previousChar) {
    continue;
   }
   if (duplicatedString.charAt(i) == 'a') {
    previousChar = duplicatedString.charAt(i);
    continue;
   }
   if (duplicatedString.charAt(i) == 'h') {
    continue;
   }
   result += duplicatedString.charAt(i);
   previousChar = duplicatedString.charAt(i);
  }
  return result;
 }

converName() 메소드에 방금 완성한 중복 제거 기능을 추가시킨다.

 public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  inputName = inputName.toLowerCase();
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  result = removeDuplication(result);
  return result;
 }

코드 커밋하기

열한번째 TDD Cycle

이제 완성된 이름을 4글자로 잘라야 한다.

완성된 이름이 4글자가 안되면 0을 붙인다.

실패하는 테스트 코드 작성하기

N360이 나와야 하는데 N36이 나온다. 

  @Test
 public void cutNameTest()
 {
  assertEquals("N360", se.convertName("Nethru"));
 }

테스트 코드 통과시키기

  public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  inputName = inputName.toLowerCase();
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  result = removeDuplication(result);
  if (result.length() == 3) {
   result += "0";
  }
  return result;
 }

리팩토링

4글자를 채워야 하므로 결과값의 길이에 따라 0을 더해준다.

  public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  inputName = inputName.toLowerCase();
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  result = removeDuplication(result);
  if (result.length() == 3) {
   result += "0";
  }
  if (result.length() == 2) {
   result += "0";
   result += "0";
  }
  if (result.length() == 1) {
   result += "0";
   result += "0";
   result += "0";
  }
  return result;
 } 

중복이 보이지만 아직 한가지 케이스가 더 남아 있으므로 중복 제거를 잠시 보류한다.

그것은 결과값이 4보다 큰 경우이다.

코드 커밋하기

열두번째 TDD Cycle

실패하는 테스트 케이스 작성하기

H435이 나와야 하지만 H4536이 나온다 (4보다 크다)

  @Test
 public void cutNameTest() {
  assertEquals("N360", se.convertName("Nethru")); // 문자열의 길이가 4보다 짧을때
  assertEquals("H453", se.convertName("HelloNethru")); // 문자열의 길이가 4보다 클때
 }

테스트 코드 통과시키기

길이가 4보다 길면 현재에서 4번째 길이까지만 반환한다.

  public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  inputName = inputName.toLowerCase();
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  result = removeDuplication(result);
  if (result.length() == 3) {
   result += "0";
  }
  if (result.length() == 2) {
   result += "0";
   result += "0";
  }
  if (result.length() == 1) {
   result += "0";
   result += "0";
   result += "0";
  }
  if (result.length() > 4) {
   result = result.substring(0, 4);
  }
  return result;
 }

리팩토링

공백에서 중복을 발견하면 아래와 같다.

  public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  inputName = inputName.toLowerCase();
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  result = removeDuplication(result);
  if (result.length() == 3) {
   result += "0";
   result = result.substring(0, 4);
  }
  if (result.length() == 2) {
   result += "0";
   result += "0";
   result = result.substring(0, 4);
  }
  if (result.length() == 1) {
   result += "0";
   result += "0";
   result += "0";
   result = result.substring(0, 4);
  }
  if (result.length() > 4) {
   result += "0";
   result += "0";
   result += "0";
   result = result.substring(0, 4);
  }
  return result;
 }

리팩토링

중복을 제거하면 아래와 같다.

  public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  inputName = inputName.toLowerCase();
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  result = removeDuplication(result)+"000";
  result = result.substring(0, 4);
  return result;
 }

리팩토링

이름의 길이를 조절하는 기능은 별개로 adjustNameLength()라는 메소드로 따로 뽑는다.

  public String convertName(String inputName) {
  String result = "";
  result += inputName.charAt(0);
  inputName = inputName.toLowerCase();
  for (int i = 1; i < inputName.length(); i++) {
   result += getCode(inputName.charAt(i));
  }
  result = adjustNameLength(result);
  return result;
 }
 private String adjustNameLength(String result) {
  result = removeDuplication(result) + "000";
  result = result.substring(0, 4);
  return result;
 }

코드 커밋하기

열세번째 TDD Cycle 

실패하는 테스트 케이스 작성하기

실패하는 테스트 케이스를 작성할 수 없다.

따라서 일반화된 솔루션이 작성되었다고 판단한다.

  @Test
 public void finalTest() {
  assertEquals("R163", se.convertName("Robert")); 
  assertEquals("A261", se.convertName("Ashcraft")); 
  assertEquals("T522", se.convertName("Tymczak")); 
  assertEquals("P236", se.convertName("Pfister")); 
 }


 

관련글 더보기

댓글 영역