기술은 항상 발전하는데

  • 왜 우리의 프로젝트는 여전히 힘든가?
  • 어떻게 하면 프로젝트를 성공적으로 할 수 있을까?
  • 과연 중요한 것은 무엇인가?

라는 의문이 있었습니다.

복잡해서?

어플리케이션에는 많은 복잡성이 있기 때문이었습니다.

어플리케이션 개발은 결국 복잡도와의 싸움이며 이 복잡도는 크게 2가지로 나눌 수 있습니다.

  • 필수적 복잡성(Essential complexity) : 어플리케이션이 해결해야하는 본질적 문제에 따른 복잡성 - 도메인
  • 부수적 복잡성(Accidental complexity) : 어플리케이션 개발을 위해 사용하는 도구에 따른 복잡성 - 언어, 기술, 도구

은탄환은 없다(No silver bullet - 프레데릭 브룩스)

그 중에서 부수적 복잡성에 집중하고 있었으며, 이 부수적 복잡성을 해결하기 위해서 에너지를 주로 사용하니 결국 필수적 복잡성을 해결할 여력이 없어지게 되는 것이었습니다.

기술 관련된 내용에 집중하고 어느 순간 이 복잡성에 집중하고 해결하기 위한 인지과부하가 생겨서 진짜 문제인 도메인 해결을 등한 시 하게 됩니다.

  1. 어플리케이션은 계속 변화하고
  2. 이 변화는 결국 필수적 복잡성을 증대시키고,
  3. 여력이 없기 때문에 일회성 코드(절차적인 코드)가 계속 반복되고,
  4. 이 복잡성이 감당할 수 없을 만큼 커져서 결국 개편을 시도합니다.
  5. 하지만 이것도 결국 복잡함이 해결되지 않은 레거시에 의존하게 됩니다.
  6. 결국 악순환의 고리가 반복되는 것입니다.

이로 인해 소프트웨어 위기 가 발생하게 됩니다. - 유지보수를 하면 할 수록 비용이 더 증가 - 이는 기술적 부채와 연결할 수 있습니다.

시간이 지날수록 생산성이 저하

출처 : https://www.butterfly.com.au/blog/website-development/clean-high-quality-code-a-guide-on-how-to-become-a-better-programmer

소프트웨어 위기 해결사 OOP

이 소프트웨어 위기를 구원하는 여러 방안들 중 OOP 가 등장하게 됩니다.

제가 OOP에 관심을 가지게 된 것도 위와 같은 과정을 직접 겪었기 때문입니다. OOP의 강력한 무기(캡슐화, 추상화, 다형성, 상속, SOLID)를 바탕으로 이 복잡성을 다룰 수 있다고 믿었기 때문입니다. 물론 OOP를 익히기 위해서는 그에 상응하는 댓가(학습곡선)가 필요하지만 이를 넘어서면 OOP를 바탕으로 어떤 복잡한 도메인 일지라도 다룰 수 있다고 생각합니다.

간단한 산수 예를 들면

가정

  • 프로젝트를 기존방식대로 하면 비용:5
  • 프로젝트를 OOP방식대로 하면 비용:3
  • OOP를 익히는 비용:5

라고 한다면

  • 기존방식대로 프로젝트 2회 : 5 + 5 = 10
  • OOP방식대로 프로젝트 2회(OOP 익히는 비용 포함) : 5 + 3 + 3 = 11

2회일 경우 기존방식이 유리합니다. 하지만 3회 이상부터는 OOP를 익힐 시 더 낮은 비용이 들어갑니다. 또한 유지보수 상황에서도 필수적 복잡성을 더 효율적으로 해결할 수 있는 OOP의 비용이 당연히 더 낮을 것입니다.

학습곡선이 있는 기술이라도 그에 상응하는 보상(복잡성 감소)이 있다면 배우는 것이 좋지 않을까요?

넷플릭스와 같은 회사는 항상 최고의 인재만을 추구한다고 합니다. 결국은 그것이 서비스의 품질 향상으로 이어지고 있구요. 당장 인력 비용은 높아지지만 복잡성을 감소시키고 유지보수 비용을 낮출 수 있다면 장기적으로 회사 입장에서도 그것이 더 이득이 되지 않을까요?

추상화

잠깐 추상화의 이야기를 해보자면 원래 추상화라는 개념은 인간이 근본적으로 가지고 있는 개념이며 아리스토텔레스의 철학에서도 확인할 수 있습니다. - 분류추상화

예를 들면 인간은 개라는 추상화 존재를 인지합니다. 바둑이, 점박이, 스누피 등 여러 개(Dog) 인스턴스가 있는데, 본능적으로 내제된 추상화를 바탕으로 “털로 덮여 있으며 인간을 잘 따르는 4족 보행의 동물”을 개로 분류(Classification) 수 있습니다. 실질적으로 개는 존재하지 않습니다. 바둑이가 존재할 뿐이죠. 만약 모든 개(Dog) 인스턴스를 각각 기억할려면 아마도 인지과부하가 걸릴 것입니다.

이 추상화를 통해서 문제를 더 단순하게 해결할 수 있습니다.

그런데 OOP가 더 복잡한 거 같아요

하지만 많은 개발자들은 절차적 코드를 더 편하게 생각합니다.

Divide and Conquer

가장 큰 이유는 OOP의 강점인 캡슐화를 제대로 다루지 못해서라고 생각합니다.

한 트랜잭션이 존재할 시 만약 OOP방식이라면 서비스에서 각종 도메인 로직을 위임기반으로 호출했을 것입니다. 그 중 결제알림을 이용할 시 해당 서비스에서는 결제를 알림도메인을 호출했을 것입니다.

결제 -> 결제알림 -> ?

결제알림 도메인 로직은 적절한 인터페이스로 추상화되어 있고 결제 입장에서는 상세 결제 알림을 알 필요가 없습니다. 그저 결제알림만 호출하면 그뿐이죠 결제의 책임은 여기까지 이고 알림의 책임은 결제알림에 위임하면 되는 것입니다.

결제알림의 세부구현을 알 필요가 없는 것(정보 은닉)입니다. OOP는 이것을 캡슐화해서 결제알림 인터페이스를 클라이언트(결제) 측에 제공할 뿐입니다. 그 캡슐화의 경계가 대부분 인터페이스(추상화)가 됩니다. 클라이언트의 인터페이스 호출과 구상체의 인터페이스 구현 사이

즉, 캡슐화 경계(Scope)를 기점으로 나누고 정복하는 것(Divide and Conquer)입니다. 일반적으로 관심사의 분리에서 시작합니다.

관심사를 분리해서 분리된 조각 별로 단순하게 해결할 수 있습니다.(복잡성 낮아짐) 하지만 많은 개발자들이 머릿속에서 Stack 처럼 모든 흐름을 기억하고 가려고 하기 때문입니다.

기존 흐름은 머릿속에서 휘발시키고 현재 관심사 영역만 집중합시다. - 작업 끝나면 그 때 다시 불러오면 됩니다.

결제를 할 때는 결제 도메인에 집중하고 다시 결제알림 모듈을 개발할 시에는 결제알림 구현에 오롯이 집중해야합니다. - 결제는 잊어버리고 이런 식으로 OOP에 접근하는 사고방식이 있어야지 그 복잡성의 늪에서 빠져나올 수 있습니다.

적절한 추상화

또한 경계 사이마다 적절한 추상화 가 필요합니다.

이 적절한 추상화를 할 수 있는 설계 능력이 중요한 핵심 중 하나인데, 이 부분이 가장 어려운 것 같습니다. 너무 일반적이거나 너무 구체적이면 아래와 같은 문제점이 보이게 됩니다.

추상화 레벨 결과
높음 오버엔지니어링, 도메인 표현이 약해짐
적절 Good!
낮음 결합도 높아짐, 재사용성 낮아짐

TDD

경계 사이 구현 전에 테스트를 먼저 생성하면 금상첨화입니다.

But

컴포넌트 사이 이동에 비용은?

  • 각 컴포넌트 이동 시 이전 구현에 대한 휘발을 확실히 해야합니다. - 하지만 그게 어려움
  • 의식적으로 방금 개발한 내용은 잊어버리도록 해야합니다. - 지속적 훈련 필요?!
  • 마치 Context switching 와 비슷? - 그럼 휘발시키는 것도 비용?

요약

  • 기술의 복잡성 함정에 빠지지 말고 도메인에 먼저 집중 (No silver bullet)
  • 캡슐화 경계를 바탕으로 Divide and Conquer
  • 적절한 추상화

1장 맛보기 예제

코드를 복사해서 붙이게 되면 나중에 그 코드를 수정할 때마다 계속 같은 여러 부분을 복사해서 붙여야 하기 때문에 아주 번거롭고 에러가 생길 수 있다.

리팩토링의 첫 단계는 리팩토링할 코드 부분에 대한 신뢰도 높은 각종 테스트를 작성하는 것이다.

(호출을 통해 위임 받는 메소드 내에서) 변경되지 않는 변수는 매개변수로 전달할 수 있다. 변경되는 변수는 (매개변수로 넘길 시) 더 주의해야한다.

리팩토링 단계의 또 한가지 문제점은 성능이다. (수정 전 코드는 루프가 1회이지만 리팩토링 후에는 3회…) 하지만 최적화 단계에서 걱정해도 늦지 않다. 최적화 단계가 성능 해결의 적기이며 효과적인 최적화를 위한 더 많은 선택의 여지가 있다.

타 객체의 속성을 switch문의 인자로 하는 것은 나쁜 방법이다. switch문의 인자로는 타 객체의 속성이 아닌 자신의 속성을 사용해야한다. -> 조건문을 재정의로 변환 기법을 사용하자.

‘간단한 수정 -> 테스트’를 리듬처럼 반복해야한다. like TDD

2장 리팩토링 개론

리팩토링의 목적

  1. 소프트웨어를 더 이해하기 쉽고 수정하기 쉽게 만드는 것
  2. 겉으로 드러나는 소프트웨어 기능에 영향을 주지 않고 개선하는 것

Why 리팩토링?

  1. 소프트웨어 설계 개선
  2. 소프트웨어 이해가 쉬워짐
  3. 버그 찾기가 쉬워짐
  4. 프로그래밍 속도가 빨라짐(장기적으로)

When 리팩토링?

  1. 같은 중복 작업이 3번째 일 시
  2. 기능을 추가할 시
  3. 버그를 수정할 시 (with 실패하는 테스트)
  4. 코드를 검수할 시

일정이 빠듯할 땐 리팩토링 얘길 꺼내질 말고 몰래 실시하자 ㅋㅋㅋㅋㅋㅋ

리팩토링의 장애물

  1. 데이터베이스!!
  2. Published 인터페이스 변경 (모듈 외부로 노출된 인터페이스)
  3. 설계 수정
  4. 리팩토링을 하면 안되는 상황

리팩토링과 설계

여전히 사전 설계는 해야 하지만, 사전 설계 과정에서는 완벽한 솔루션을 찾을 필요 없이 적당한 솔루션만 생각하면 된다. like Agile

리팩토링과 성능

리팩토링을 통해 코드르 잘게 분리하면 추후 성능 개선 시 편하게 할 수 있다.

3장 코드의 구린내

  1. 중복 코드(Duplicated Code)
  2. 긴 메서드(Long Method)
  3. 큰 클래스(Large Class)
  4. 긴 매개변수들(Long Parameter List)
  5. 수정의 산발(Divergent Change) : 한 클래스가 다양한 원인으로 때문에 자주 수정될 경우 > SRP 위배
  6. 산탄총 수술(Shotgun Surgery) : 한 기능을 수정하는데 여러가지 클래스가 변경되는 경우
  7. 잘못된 소속(Feature Envy) : 속성과 행위의 분리 > 응집력 없음
  8. 데이터 뭉치(Data Clumps) : 비슷한 속성들 끼리 몰려다님 > 값객체로 전환
  9. 강박적 기본타입 사용(Primitive Obsession) > 값객체로 전환
  10. switch 문(Switch Statements)
  11. 평행 상속 계층(Parallel Inheritance Hierarchies) : 상속 시 다른 상속도 같이 해야함
  12. 게으른 클래스(Lazy Class) : 비효율적으로 클래스로 분리된 것 > 적절한 클래스로 병합
  13. 막연한 범용 코드(Speculative Generality) : 미래에 사용될 거 같은 기능 선구현
  14. 임시 필드(Temporary Field)
  15. 메시지 체인(Message Chains) : obj1.do().other().something()
  16. 과잉 중개 메서드(Middle Man) : 과한 위임 > 이게 과연 나쁜냄세일 수도 있는가?
  17. 지나친 관여(Inappropriate Intimacy) : 강결합을 약결합으로
  18. 인터페이스가 다른 대용 클래스(Alternative Classes with Different Interfaces) : 같은 기능 다른 인터페이스 > 합쳐라
  19. 미흡한 라이브러리 클래스(Incomplete Library Class)
  20. 데이터 클래스(Data Class) : DTO > getter & 선택적 setter를 통한 필드 캡슐화
  21. 방치된 상속물(Refused Bequest) : 사용하지 않는 부모의 유산
  22. 불필요한 주석(Comments) : 주석을 넣어야겠다는 생각이 들 땐 먼저 코드를 리팩토링해서 주석을 없앨 수 있게 만들어보자.

4장 테스트 작성

모든 테스트를 완전히 자동화하고 결과를 자체적으로 검사하게 하자

완벽한 테스트를 작성하려다 아예 테스트를 포기하느니, 차라리 불완전한 테스트를 작성해 실행하는 편이 낫다.

5장 리팩토링 기법 카탈로그에 대해

디자인 패턴은 목표 지향점이고, 리팩토링은 다른 상태에서 그 지향점까지 도달하는 방법이다.

Rule은 하나의 테스트 클래스 내에서 동작 방식을 재정의 하거나 추가하기 위해 사용됩니다. 특히 다른 테스트클래스에서도 재사용성할 때 유용합니다.

기본 Rule 클래스

규칙이름 설명
TemporaryFolder 임시폴더 관리. 테스트 후 삭제
ExternalResources 자원(DB, 파일, 소켓) 관리
ErrorCollector 지속적 테스트 실패 수집
Verifier 별개 조건 확인 (vs assert*)
TestWatcher 테스트 인터셉터 (starting, succeeded, failed, finished…)
TestName 테스트 메소드명을 알려줌
Timeout 테스트 클래스 전역 timeout 설정 (vs @Timeout)
ExpectedException 예외 직접 확인 (vs @Expected)
DisableOnDebug Rule 디버그 비활성화 데코레이터
RuleChain 복수 Rule chaining 복합체
ClassRule 테스트슈트 전체에 Rule 적용

상속구조

TemporaryFolder

public static class HasTempFolder {
  @Rule
  public TemporaryFolder folder = new TemporaryFolder();

  @Test
  public void testUsingTempFolder() throws IOException {
    File createdFile = folder.newFile("myfile.txt");
    File createdFolder = folder.newFolder("subfolder");
    // ...
  }
}

ExternalResources

외부 자원(파일, 소켓, DB커넥션 등) 관리. @Before, @After과 별 차이가 없으나, 특정 자원이 다른 테스트케이스에서 재사용성이 요구될 경우 유용함

public static class UsesExternalResource {
  Server myServer = new Server();

  @Rule
  public ExternalResource resource = new ExternalResource() {
    @Override
    protected void before() throws Throwable {
      myServer.connect();
    };

    @Override
    protected void after() {
      myServer.disconnect();
    };
  };

  @Test
  public void testFoo() {
    new Client().run(myServer);
  }
}

ErrorCollector

오류를 계속 쌓아둘 수 있으며, 오류가 발생해도 테스트를 계속 진행시킬 수 있을 때 유용하다.

  • checkThat
  • checkSucceeds
  • addError
public static class UsesErrorCollectorTwice {
  @Rule
  public ErrorCollector collector = new ErrorCollector();

  @Test
  public void example() {
    collector.addError(new Throwable("first thing went wrong"));
    collector.addError(new Throwable("second thing went wrong"));
  }

  @Test
  public void testGetAmount() throws Exception {
    Order order1 = ...
    collector.checkThat(10000, order1.getAmount());  // 오류가 발생해도 아래 줄 실행됨
    Order order2 = ...
    collector.checkThat(20000, order2.getAmount());
  }
}

Verifier

테스트 자체를 검증하는 assert* 메소드와는 달리, 일반적으로 테스트케이스 실행 후 만족해야하는 환경조건이나 Global조건(객체들의 종합 상태)을 검사하는데 쓰인다.

private static String sequence;

public static class UsesVerifier {
  @Rule
  public Verifier collector = new Verifier() {
    @Override
    protected void verify() {
      sequence += "verify ";
    }
  };

  @Test
  public void example() {
    sequence += "test ";
  }

  @Test
  public void verifierRunsAfterTest() {
    sequence = "";
    assertThat(testResult(UsesVerifier.class), isSuccessful());
    assertEquals("test verify ", sequence);
  }
}

TestWatcher

  • apply
  • succeeded
  • failed
  • skipped
  • starting
  • finished

TestName

그냥 테스트 이름 호출할 뿐

public class NameRuleTest {
  @Rule
  public TestName name = new TestName();

  @Test
  public void testA() {
    assertEquals("testA", name.getMethodName());
  }
}

Timeout

테스트 클래스 전역 timeout 설정

public static class HasGlobalTimeout {
  public static String log;

  @Rule
  public TestRule globalTimeout = new Timeout(20);

  @Test
  public void testInfiniteLoop1() {
    log += "ran1";
    for(;;) {}
  }

  @Test
  public void testInfiniteLoop2() {
    log += "ran2";
    for(;;) {}
  }
}

ExpectedException

@Expected는 단순하게 클래스 타입만 검증하면 메세지도 검증가능하며, 확장하면 코드성 Exception 검증도 가능

public static class HasExpectedException {
  @Rule
  public ExpectedException thrown = ExpectedException.none();

  @Test
  public void throwsNothing() {
  }

  @Test
  public void throwsNullPointerException() {
    thrown.expect(NullPointerException.class);
    throw new NullPointerException();
  }

  @Test
  public void throwsNullPointerExceptionWithMessage() {
    thrown.expect(NullPointerException.class);
    thrown.expectMessage("happened?");
    thrown.expectMessage(startsWith("What"));
    throw new NullPointerException("What happened?");
  }
}

DisableOnDebug

-Xdebug 또는 -agentlib:jdwp 옵션 시 비활성화

@Rule
public TestRule timeout = new DisableOnDebug(new Timeout(20));

RuleChain

말 그대로 Rule chaining

public static class UseRuleChain {
    @Rule
    public TestRule chain = RuleChain
                           .outerRule(new LoggingRule("outer rule"))
                           .around(new LoggingRule("middle rule"))
                           .around(new LoggingRule("inner rule"));

    @Test
    public void example() {
        assertTrue(true);
    }
}

ClassRule

@RunWith(Suite.class)
@SuiteClasses({A.class, B.class, C.class})
public class UsesExternalResource {
  public static Server myServer = new Server();

  @ClassRule
  public static ExternalResource resource = new ExternalResource() {
    @Override
    protected void before() throws Throwable {
      myServer.connect();
    };

    @Override
    protected void after() {
      myServer.disconnect();
    };
  };
}

참고로 커스텀 룰 구현도 가능합니다.

https://github.com/junit-team/junit4/wiki/Rules#custom-rules

참조

설명

단위테스트를 진행하다 보면 여러가지 입력 값에 대한 테스트를 한 번에 수행할 필요가 있습니다.

예를 들면 추천 수가 100개 이상일 시, 월 로그인 횟수가 30회 이상 일 시, 금액이 10만원 이상일 시, 법인회원일 시 이러한 조건에 따라서 요구사항이 달라지는 경우가 많습니다.

아래 상세하게 예를 들어서 추천 수가

  • 9개 미만이면 일반게시물 - 경계조건 추천 9
  • 10개 이상이면 베스트 게시물 - 경계조건 추천 10
  • 100개 이면 베스트게시물로 분류 - 경계조건 추천 100(99)

속성 상태가 로직의 변화를 일으키는 경계조건 을 테스트해야하는 경우가 있습니다. 특별히 경계조건을 테스트할 시 더 유리하지만 그것 뿐만 아니라 단순히 여러가지 값을 검증할 시에도 유용한 Parameterized Test 를 JUnit 예제로 알아보겠습니다.

예제

결제금액 별로 취소수수료 다른 조건을 Parameterized Test로 풀어보는 예제입니다.

예제조건

가격 별 취소수수료

  • 0 ~ 9999 : 0%
  • 10000 ~ 49999 : 10%
  • 50000 ~ : 20%

예제코드

/**
 * 가격 별 취소수수료
 * 0 ~ 9999 : 0%
 * 10000 ~ 49999 : 10%
 * 50000 ~ : 20%
 */
@RunWith(Parameterized.class)   // 파라메터화된 테스트를 위한 선언
public class RefundServiceTest {

    // 파라미터들 제공 메소드 : static 이면서 Collection을 반환해야한다. 경계영역이 잘 설정되어야함.
    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][]{
                { 9999L,     0L},
                {10000L,  1000L},
                {49999L,  4999L},
                {50000L, 10000L}
        });
    }

    // @Parameter로 주입 시 public 으로 선언되어야 한다.
    @Parameter(0)   // data() 항 항목의 첫번째 인자
    public long amount;
    @Parameter(1)   // data() 항 항목의 두번째 인자
    public long refundFee;

    Order order;
    RefundService refundService;

    @Before
    public void setUp() throws Exception {
        refundService = new RefundService();
        order = new Order(amount);
    }

    @Test
    public void testGetRefundFee() throws Exception {
        assertThat(refundService.getRefundFee(order), is(this.refundFee));
    }
}

class Order {
    private long amount;

    public Order(long amount) {
        this.amount = amount;
    }

    public long getAmount() {
        return this.amount;
    }
}

class RefundService {
    public long getRefundFee(Order order) {
        long amount = order.getAmount();
        if (amount < 10000L)    // 0%
            return 0;
        else if (amount < 50000L)   // 10%
            return amount * 10 / 100;
        else    // 20%
            return amount * 20 / 100;
    }
}

Point

  • @RunWith(Parameterized.class) : 파라메터화된 테스트 클래스 선언
  • @Parameters : 파라미터들 제공 메소드 어노테이션 : static 이면서 Collection을 반환해야한다. 경계영역이 잘 설정되어야함.
  • @Parameter : @Parameters 가 반환하는 항목 인자. 꼭 public 으로 선언.
    • @Parameter(0) 은 첫번째 인자, @Parameter(1) 은 두번째 인자… (0 base)
    • @Parameter 대신에 인자 순서대로 생성자로 받아도 된다.
long amount;
long refundFee;

RefundServiceTest(long amount, long refundFee) {
    this.amount = amount;
    this.refundFee = refundFee;
}

추가사항

@Parameters 즉 여러가지 입력 값을 반환하는 메소드는 위와 같이 간단한 경우에는 직접 값을 코드에 입력 가능하나, 입력 값이 매우 복잡한 경우에는 json 이나 excel 또는 헬퍼객체를 통해서 입력 받는 것을 추천합니다.

참조

정리

1. 깨끗한코드

DSL은 무엇인가?

특정 문제 도메인, 특정 문제 표현 기법, 특정 문제 해결 기법에 사용할 목적으로 만든 프로그래밍 언어나 명세 언어를 의미한다. 해당 분야 전문가들의 의사소통을 돕기 위해, 모호함을 없애며 표현력을 높인 특징이 있다.

르블랑의 법칙 : (나쁜 코드를 해결할) 나중은 결코 오지 않는다.

생산성 대비 시간 그래프

출처 : https://www.butterfly.com.au/blog/website-development/clean-high-quality-code-a-guide-on-how-to-become-a-better-programmer

나쁜코드의 책임은 우리에게 있다.

명언

  • 명쾌한 추상화 !!!!
  • 한 가지를 제대로 한다 : SRP
  • 코드는 문학적 : Readability
  • 중복이 없다 : DRY

코드를 작성하는 시간보다 읽는 시간이 훨씬 많다 : 고로 잘 읽을 줄 알아야한다.

보이스카우트 규칙

캠프장은 처음 왔을 때보다 더 깨끗하게 해놓고 떠나라.

연습해 연습!!

2. 의미 있는 이름

의도를 분명히 밝혀라

before

public List<int[]> getThem() {
    List<int[]> list1 = new ArrayList<>();
    for (int[] x : theList)
        if (x[0] == 4)
            list1.add(x);
    return list1;
}

after

public List<Cell> getFlaggedCells() {
    List<Cell> flaggedCells = new ArrayList<>();
    for (Cell cell : gameBoard)
        if (cell.isFlagged())
            flaggedCells.add(cell);
    return flaggedCells;
}

그릇된 정보를 피하라

accountList -> accounts, accountGroup, brunchOfAccounts

의미 있게 구분하라

NO!!!

  • o1, o2, o3
  • clazz, klass, NameString, theName
getActiveAccount();
getActiveAccountDetail();
getActiveAccountInfo();

발음하기 쉬운 이름을 사용하라

genymdhms : generateDateYearMonthDayHourMinuteSecond

before

class DtaRcrd102 {
    private Date genymdhms;
    private Date modymdhms;
    private final String pszqint = "102";
}

after

class Customer {
  private Date generationTimestamp;
  private Date modificationTimestamp;
  private final String recordId = "102";
}

검색하기 쉬운 이름을 사용하라

인코딩을 피하라

Anti 헝가리안 표기법 Anti 맴버변수 접두어 : m_??? Anti IFactory + Factory : Factory + DefaultFactory

  • 동적언어는 어떻게 하지?

자신의 기억력을 자랑하지 마라

i, j, k 정도는 넘어가주자

클래스 이름

  • 명사
  • Manager, Processor, Data, Info는 피하자???

메서드 이름

  • 동사
  • 정적팩토리 메서드 좋음

기발한 이름은 피하라

한 개념에 한 단어를 사용하라

fetch or retrieve or get

일관성 있는 어휘를 사용하자.

말장난을 하지 마라

VS 한 개념에 한 단어를 사용하라

개념이 달라진 부분에서는 일관된 단어가 아니라 다른 단어를 사용해야한다.

해법 영역에서 가져온 이름을 사용하라

For Concrete Class

SimpleAdder, Accumulator

문제 영역에서 가져온 이름을 사용하라

For Interface

Adder

의미 있는 맥락을 추가하라

  • 맥락이 이어진다면 prefix를 추가해보자
    • addrFirstName, addrLastName, addrState
  • prefix보단 타입(Class)으로 구분하는 것이 더 낫다
    • class Address : firstName, lastName, state

불필요한 맥락을 없애라

GSDAccount, GSDAddress VS Account, Address

인코딩을 없애라 가 여기에 포함되는 것 같음

3. 함수

작게 만들어라!

쉽게 읽히고 이해할 수 있어야 한다.

한 가지만 해라!

함수는 한 가지를 해야 한다. 그 한 가지를 잘해야 한다. 그 한 가지만을 해야한다.

함수 당 추상화 수준은 하나라!

그리고 위에서 아래로 내려가기

Switch 문

  • 다형성을 이용하자.
  • 한번은 참아준다

서술적인 이름을 사용하라!

함수 인수

3개 이하로 유지하라?!

  • CQS
  • 플래그 인수는 과연 악인가?
  • 인자 수가 4개 이상이면 악인가?

인자 객체 추천!!! : 개념이 묶인다면

동사와 키워드 : write(name)

부수 효과를 일으키지 마라!

  • 일시적인 결합 : 실행 순서와 동시성에 따른 부수효과
    • A 메서드 호출 전 B 메서드를 호출해야 정상작동 한다.
    • 보고서는 한 번에 오직 하나만 실행 가능
    • 버튼 클릭을 처리하면 먼저 화면이 갱신되어야 한다.
  • 순서 종속성 : 메서드들을 정해진 순서대로 실행해야 정상 작동

출력 인수 는 상태가 변하는 경우 반환하는 것이 좋다.

명령과 조회를 분리하라 (CQS)

before

if (set("username", "unclebob"))...

after

if (attributeExists("username")) {
    setAttribute("username", "unclebob");
}

과연 항상 분리하는 것이 옳은가?

오류 코드보다 예외를 사용하라!

절차적보다는 명령형으로

try-catch 뽑아내기 : 항상 지켜야하는가?

  • 오류처리도 한가지 작업이므로 메서드를 분리한다.
  • 오류반환코드 의존성을 제거한다.

반복하지 마라! (DRY)

구조적 프로그래밍

  • no goto, break, continue
  • one input one output

함수를 어떻게 짜죠?

글짓기와 비슷하다.

결론

  • 길이가 짧고
  • 이름이 좋고
  • 체계가 잡힌
  • 가장 중요한 것은 도메인 문제 해결

시스템

‘처음부터 올바르게’ 시스템을 만들 수 있다는 믿음은 미신이다.

창발성

단순한 설계 규칙

  1. 모든 테스트를 실행한다.
  2. 중복을 없앤다.
  3. 프로그래머 의도를 표현한다.
  4. 클래스와 메서드 수를 최소로 줄인다. (중복제거)

2~4는 리팩터링을 통해 개선 가능

중복은 추가작업, 추가 위험, 불필요한 복잡도를 뜻한다.

좋은 표현 규칙

  1. 좋은 이름
  2. 함수와 클래스 크기를 가능한 줄인다. (for SRP)
  3. 표준 명칭을 사용한다.
  4. 단위 테스트 - 예제 문서

하지만 가장 중요한 것은 위 규칙을 실천하고자 하는 노력

동시성

동시성과 깔끔한 코드는 양립하기 어렵다. 아주 어렵다.

동시성은 결합(coupling)을 없애는 전략이다. 즉, 무엇(what)과 언제(when)를 분리하는 전략이다.

동시성 방어 원칙

  1. 동시성 코드는 다른 코드와 분리하라
  2. 자료를 캡슐화하라. 공유 자료를 최대한 줄여라
  3. 자료 사본을 사용하라
  4. 쓰레드는 가능한 독립적으로 구현하라

동시성 II

프로그램에서 스레드 관련 코드를 분리하면 조율과 실험이 가능하므로 통찰력이 높아져 최적의 전략을 찾기 쉬워진다.

Multicore SDK 라는 제품을 통해서 동시성 테스트가 조금 더 쉬워진다

서버-클라이언트 샘플코드

의문

창발성

p.216

클래스와 메서드 수를 최소로 줄인다.

단순한 설계 규칙이라고 했는데, 클래스와 메서드의 크기를 줄이는 것이 더 낫다고 생각한다. 아마 중복되는 클래스나 메서드를 줄이라는 의미인 것으로 해석된다.