시스템

도시가 생긴다면?

혼자서 도시를 세우라하면 못 세울 것이다.
그렇다고 이미 세워진 도시를 혼자 관리하라해도 못한다.
도시는 다양한 사람/팀들에 의해 관리된다.

적절한 추상화와 모듈화 덕분에 정상적으로 돌아갈 것이다.
-> 큰 그림을 이해하지 못해도 개개인이 관리하는 '구성요소'는 효율적으로 관리되고 돌아간다.


시스템 제작과 세스템 사용을 분리하라

제작과 사용은 다르다
불행히도 대다수 애플리케이션은 시작 단계라는 관심사를 분리하지 않는다.
준비 과정 코드를 주먹구구식으로 구현할 뿐만 아니라, 런타임 로직과 뒤섞는다.
다음은 전형적인 예다.

public Service getService(){
    if (service == null)
        service = new MyServiceImpl(....);
    return service;
}
  • 이것이 초기화 지연 / 계산 지연 기법이다.
    • 객체가 실제로 필요할 때까지 생성되지 않으므로 불필요한 부하가 걸리지 않는다.
      • 따라서 애플리케이션의 시작시간이 빨라진다.
    • 어떤 경우에도 null 포인터를 반환하지 않는다.

하지만 getService 메서닥 MyServiceImpl과 생성자 인수에 의존한다.

  1. 런타임 로직에서 MyServiceImpl을 사용하지 않더라도 의존성을 해결하지 않으면 컴파일이 안된다. -> 초기화 지연 기법이므로 문제가 있을 경우 컴파일이 안된다.(사용하지 않더라도)
  2. 테스트에서도 MyServiceImpl이 무거운 객체라면, 미리 준비해야하는 별도의 준비가 필요할 수 있다.

Main 분리

시스템 생성과 사용을 분리하는 방법

팩토리

예를 들어, 주문처리 시스템에서 App은 LineItem 인스턴스를 생성해 Order에 추가한다.
이때 Abstract Factory 패턴을 사용한다. -> LineItem을 생성하는 시점은 App이 결정하지만 LineItem을 생성하는 코드는 App이 모름.

추상팩토리 패턴 링크

의존성 주입

사용과 제작을 분리하는 방법 중 하나가 DI이다.

DI : IoC 기법을 의존성 관리에 적용한 메커니즘
제어 역전에서는 한 객체가 맡은 보조 책임을 새로운 객체에거 전적으로 떠넘긴다.
새로운 객체는 넘겨받은 책임만 맡으므로 SRP를 지킨다.

의존성 관리 맥락에서 객체는 의존성 자체를 인스턴스로 만드는 책임은 지지 않는다.
대신에 이 책임을 다른 '전담 메커니즘'에 넘겨야 한다.
대개 main 루틴이나 특수 컨테이너를 사용한다.

더 나아가 클래스의 인수나 생성자 주입을 통해 DI 컨테이너가 생성한 인스턴스를 주입한다.

-> 그렇다면 lazy 기법의 장점(미리 생성하지 않기 때문에 실행이 빠름, null을 반환 X)은 포기해야하는가?
-> 대부분의 DI 컨테이너는 필요한 경우에 인스턴스를 생성하여 주입한다.


확장

초기 도시에는 전력, 상수도, 인터넥과 같은 서비스가 없었따.

현재 차선을 넓히는 도로 공사를 보며, '애초에 넓게 만들지'라고 생각한 경험이 많다.
하지만, 차가 많이 다니지 않은 도로에 미리 6차선을 만드는 데 드는 비용을 정당화할 수 있는가?

-> 오늘은 오늘의 스토리에 맞춰 시스템을 구현해야 한다.
-> 내일의 스토리가 변경되더라도 그것은 내일 있을 일이다.
하지만 이렇게 자주 변경되기 위해서는 TDD, 리팩터링, 깨끗한 코드가 뒷받침되어야 한다.


자바 프록시

자바 프록시는 단순한 상황에 적합하다.
개별 객체나 클래스에서 메서드 호출을 감싸는 경우가 좋은 예다.
하지만 JDK에서 제공하는 동적 프록시는 인터페이스만 지원한다.
-> 이러한 프록시 API 코드를 작성하기 위해서는 코드가 많아지며 복잡해진다.

순수 자바 AOP 프로임워크

다행히 대부분의 자바 프레임워크는 내부적으로 프록시를 사용한다.

......

이해되지 않는 부분들이 많아 이후에 다시 읽어야 할 듯

클래스

여태까지는 코드 행과 블록을 올바로 작성하는 방법에 초점을 맞췄다.
하지만 코드의 표현력과 함수에 아무리 신경을 쓰더라도 더 높은 단계(클래스)까지 신경쓰지 않으면 깨끗한 코드를 얻기는 어렵다.

클래스 체계

캡슐화

변수와 유틸리티 함수는 가능한 공개하지 않는 편이 맞지만, 반드시 숨길 필요는 없다.
때로는 protected로 선언해 테스트 코드에서 사용을 허용하기도 한다.

클래스는 작아야 한다.

클래스를 만들 때 첫 번째 규칙은 크기이다.
그렇다면 대체 얼마나 작아야 하는가???
함수 : 물리적인 행 수
클래스 : 맡은 책임

우선 클래스 이름은 책임을 기술해야 한다.
Processor, Manager, Super 등과 같이 모호한 단어가 있다면 해당 클래스는 여러 책임을 갖고 있는 것이다.

또한 클래스의 설명은 if, and, or, but 을 사용하지 않고 25자 내로 설명가능해야 한다.
ex) "SuperDashboard는 마지막으로 포커스를 얻었던 컴포넌트에 접근하는 방법을 제공하며, 버전과 빌드 번호를 추적하는 메커니즘을 제공한다."
-> ~하며(and) 가 틀렸다? (만약 그렇다면 클래스가 너무 많아지는 것 아닌가?)

단일 책임 원칙(SRP)

클래스나 모듈을 변경할 이유가 단 하나 뿐이어야 한다.

SRP는 가장 이해시키기 쉬운 개념 중 하나다.
하지만 설계자가 가장 무시하는 규칙 중 하나다.
수많은 책임을 떠안은 클래스를 접하기가 정말 쉽다.
Why? SW를 동작시키는 것과 SW를 깨끗하게 만드는 것은 별개이다.
-> 대부분의 개발자들은 "깨끗한 SW" 보다 "돌아가는 SW"에 초점을 둔다. (사실인 것 같음... ㅂㅂㅂㄱ)
또한, 단일 책임 클래스(SRP를 만족하는 클래스)가 많아지면 큰 그림을 이해하기가 어렵다고 생각한다.(이것도 맞는 것 같은데...)

차이점 :

  1. 작은 서랍을 많이 두고 기능과 이름이 명확한 컴포넌트를 다룰 것인가?
  2. 큰 서랍을 몇 개 두고 서랍안에 많은 컴포넌트를 넣어 다룰 것인가?

응집도

클래스는 인스턴스 변수 수가 적어야 한다.
메소드 내에서 클래스 인스턴스를 많이 사용할 수록 응집도가 높다.

응집도를 유지하면 작은 클래스 여러개가 나온다.

큰 함수를 작은 함수 여럿으로 나누기만 해도 여러개의 클래스가 나온다.

if)

  1. 큰 함수 내의 어떤 로직을 작은 함수로 빼내고 싶다.
  2. 그 로직 내에서 큰 함수에 있는 로컬 변수 4개를 사용한다.
  3. 작은 함수의 매개변수로 4개를 모두 넘겨야 하나?
    -> No, 클래스 프로퍼티로 승격시키면 매개변수는 필요없어진다.
  4. 그렇다면 해당 프로퍼티는 작은 함수에서만 사용하는 것이 아닌가? 즉, 응집력이 낮아지는 것 아닌가?
    -> 해당 프로퍼티와 함수를 새로운 클래스로 쪼개라

변경하기 쉬운 클래스

SQL(select, selectAll, insert, findByKey ...)을 수행하는 클래스를 기능 별로 각각의 클래스로 나눈다.
(-> 하지만, 클래스가 너무 많아지는데..? 뭐 나눔으로써 SRP, OCP를 지원한다고 해도, 각각을 확인하기 또한 불편한 것 아닌가?)

변경으로부터 격리

요구사항을 변하기 마련이다. 따라서 코드 또한 변할 것이다.
OOP에서는 concrete Class 와 abstract Class가 존재한다.

ex) 주식 Portfolio 클래스를 만든다고 가정하자
Portfolio 클래스는 외부 TokyoStockExchange API를 사용해 값을 계산한다.
따라서 테스트 코드를 작성해도 시시 각각으로 변하는 시세로 인해 검증이 어려워진다.

-> Portfolio 클래스에서 TokyoStockExchange API를 직접 호출하는 대신 StockExchange라는 인터페이스를 생성한 후 메서드를 선언한다.

//abstract
public interface StockExchange{
    Money currentPrice(String symbol);
}

-> 다음으로 StockExchange 인터페이스를 구현하는 TokyoStockExchange Class를 구현한다.
또한, Portfolio 생성자를 통해 StockExchange를 주입한다.

public Portfolio{
    private StockExchange exchange;
    public Portfolio(StockExchange exchange){
        this.exchange = exchange;
    }
    //......
}

이제 TokyoStockExchange 를 흉내내는 테스트용 클래스를 만들 수 있다.
테스트 용 클래스는 StockExchange 인터페이스를 구현하며 고정된 주가를 반환한다.
또한 항상 일정한 주가를 반환한다.

따라서 이제는 시시각각 주가가 변동해도 테스트코드를 작성할 수 있다.

public class PortfolioTest{
    private FixedStockExchangeStub exchange;
    private Portfolio portfolio;

    @Before
    protected void setUp() throws Exception{
        exchange = new FixedStockExchangeStub();
        exchange.fix("BCT", 100);
        portfolio = new Portfolio(exchange);
    }

    @Test
    public void GivenFiveBCTTotalShouldBe500() throws Exception{
        portfolio.add(5, "BCT");
        Assert.assertEquals(500, portfolio.value());
    }
}

위와 같이 테스트가 가능할 정도로 시스템의 결합도를 낮추면 유연성과 재사용성이 높아진다.
결합도가 낮다 : 각 시스템 요소가 다른 요소로부터, 변경으로부터 잘 격리되어 있다.
결합도를 낮춤으로써 DIP를 따르는 클래스가 나온다.

DIP : concrete Class 가 아닌 abstract Class에 의존해야 한다.

우리가 만든 Portfolio Class는 Tokyo...라는 concrete Class가 아닌 StockExchange 인터페이스에 의존한다.
따라서 Tokyo... 가 아닌 NY...., Korea... 를 주입해서 사용할 수도 있다.

단위 테스트

이전까지의 단위 테스트란 프로그램이 '돌아간다' 라는 사실만 확인하는 정도였다.
현재는 TDD와 애자일 덕분에 단위 테스트를 자동화하는 프로그래머들이 이미 많아졌으며, 늘어나는 추세다.

TDD 법칙 3가지

요즘엔 실제 코드를 작성하기 전에 단위 테스트부터 짜라고 요구한다!

  1. 실패하는 단위 테스트를 작성할 때까지 코드를 작성하지 않는다.
  2. 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
  3. 현재 실패하는 테스트를 통과할 정도로만 코드를 작성한다.

이렇게 일하면 매일 수십개에서 매년 수천개의 테스트 케이스가 나온다.
하지만 정말 많아진 테스트코드는 심각한 관리 문제를 유발한다.

깨끗한 테스트 코드 유지하기

기존에 작성한 테스트 케이스를 위한 코드를 짰지만, 실제 코드는 계속해서 변경된다.
실제 코드가 변경됨으로써 이전에 작성한 테스트 케이스가 실패한다면 테스트 케이스를 통과시키기는 점점 더 어려워진다.
그렇게 유지되다보면 결국에는 테스트 케이스들을 모두 버리는 상황에 처한다.

BUT, 버린다면 실제 코드가 정상적으로 동작하는 지 확인할 수가 없다.
따라서 테스트 코드 또한 실제 코드만큼이나 중요하다!

테스트는 유연성, 유지보수성, 재사용성을 제공한다.

테스트 코드를 깨끗하게 유지하지 않으면 잃어버린다.
유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트이다. -> 테스트 케이스가 있으면 코드 변경이 두렵지 않으므로
하지만, 코드를 잘 짜도, 아키텍처를 유연해도, 설계를 잘 하더라도 테스트 케이스가 없으면 버그를 잡아내기 힘들다.

테스트 케이스의 커버리지가 넓을수록 코드 변경에 대한 두려움은 사라진다.

깨끗한 테스트 코드

세가지가 필요하다

  1. 가독성
  2. 가독성
  3. 가독성
    (;;;;; 뭐야 왜이래)

테스트 코드에서의 가독성을 굉장히 중요하다.
테스트 함수 내에 테스트를 위한 사전 작업들 같은 경우는 별도의 함수를 통해 호출하여 가독성을 높인다.

이중 표준

테스트 환경에서는 성능을 크게 신경쓰지 않아도 된다.
성능보다는 가독성과 테스트에만 집중한다.

테스트당 assert 하나

굳이 지킬 필요는 없다

테스트당 개념 하나

이게 차라리 맞다.

F.I.R.S.T

  • Fast: 테스트는 빨라야 한다. 초반에 자주 돌려야 문제를 찾기 쉽다.

  • Independent: 각 테스트는 서로 의존하면 안된다. 한 테스트가 다른 테스트의 환경을 준비해서는 안된다. 테스트 함수는 순서와 상관없이 보장되어야 한다.

  • Repeatable: 테스트는 반복 가능해야 한다. 실제 환경, QA 환경, 네트워크가 없는 노트북 환경에서도 실행되어야 한다.

  • Self-Validating: 테스트는 Bool 값으로 결과를 내야 한다. 성공 아니면 실패!
    통과 여부를 Bool로 내지 않으면 판단은 주관적이 된다.

  • Timely: 테스트는 적시에 작성해야 한다. 실제 코드를 구현하기 이전에! 작성되어야 한다.

결론

사실상 깨끗한 테스트 코드라는 주제는 정말 끝이 없는 주제이다.
실제 코드보다 더 중요할 수도 있다.
테스트 코드를 깨끗하게 유지하자.

경계

시스템에 들어가는 모든 SW를 개발하기는 드물다.
때로는 패키지를 사고, 때로는 오픈 소스를 사용한다. 또는 사내 다른 팀의 컴포넌트를 이용하기도 한다.

외부 코드 사용하기

인터페이스 제공자와 사용자 사이에는 긴장이 존재한다.
제공자는 적용성을 최대한 넓히려 애쓴다. -> 커버리지가 커야 많은 사용자가 구매하니까
반대로 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.

하나의 예로 java.util.Map 에는 clear() 메소드가 존재한다.
Map을 이리저리로 옮기면서 clear()라는 메소드를 누구나, 어디서든 호출할 수 있다.

ex) 센서를 담는 Map을 생성하자

Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);

해당 맵에는 어떤 객체라도 들어갈 수 있다.

-> 제네릭을 사용해보자

Map<String, Sensor> sensors = new HsahMap<Sensor>();
Sensor s = sensors.get(sensorId);

하지만 Map의 기본 API인 clear()와 같은 기능은 제어하지 못한다. (즉, 여전히 누구나 사용할 수 있다.)

-> 다른 방법을 사용해보자

public class Sensors{
    private Map sensors = new HashMap();

    public Sensor getById(String id){
        return (Sensor) sensors.get(id);
    }
    ...
}

제네릭을 사용하지 않더라도 사용하는 입장에서는 문제가 되지 않는다.
또한 필요한 API만 사용자에게 제공이 가능하다.(clear()와 같은 불필요한 API는 제공하지 않아도 된다.)
Map을 사용할 때마다 캡슐화하라는 소리가 아니다. Map을 여기저기 넘기지 말라는 말이다.

경계 살피고 익히기

외부 코드를 사용하면 적은 시간에 많은 기능을 출시하기 쉬워진다.
외부 패키지 테스트가 우리의 책임은 아니다. 하지만 우리가 사용할 코드를 테스트하는 것은 우리의 책임이다.

  1. 외부 라이브러리를 가져온다.
  2. 며칠 문서를 보며 사용법을 익힌다.
  3. 우리쪽 코드를 작성해 라이브러리를 검증한다.
  4. 우리 버그인지 라이브러리 버그인지 찾아내느라 시간이 정말 많이 걸린다.

이처럼 외부 코드를 익히기는 어렵다. 통합하기도 어렵다.
그렇다면 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 간단한 테스트 케이스를 작성하며 익히는 것은 어떤가?
-> 이를 학습 테스트라 부른다.

깨끗한 경계

경계에서는 흥미로운 일이 많이 벌어진다.
변경이 대표적인 예다.
SW 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다.
통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의지하는 편이 훨씬 좋다.

외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
Map에서 봤듯이, 새로운 클래스로 경계를 감싸거나, ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자.

+ Recent posts