설계는 단순히 어떻게 보이고 느껴지는가에 관한 것이 아니다. 설계는 어떻게 동장하는가에 관한 것이다.

  • 스티브 잡스

DDD는 우리가 높은 품질의 소프트웨어 모델을 설계할 수 있도록 해준다.

DDD는 전략적인 동시에 전술적(DDD-Lite)인 모델링 도구

나도 DDD를 할 수 있을까?

학습 곡선이 있다!!

DDD의 가장 중심에 있는 원리

  • 토의, 경청, 이해, 발견, 비즈니스의 가치
  • 모든 지식을 중앙화하는 모든 것

DDD는 객체지향적 방법으로 엔터프라이즈 애플리케이션을 해결하는 방법

DDD는 MSA를 지향한다. Monolithic은 지양한다.

시니어 개발자 : 이 조직은 내가 생각했던 것보다 파괴적 진보에 관심이 없더군요. 뭐 상관 없어요 나는 포기하지 않겠어요.

도메인 전문가 : 유비쿼터스 용어로 소통하자

내가 왜 DDD를 해야할까?

개발자 <- 유비쿼터스 언어 -> 도메인 전문가

설계는 코드이며, 코드가 설계다. 설계는 어떻게 작동하는가다. - Uncle Bob도 비슷한 말을 함

옳은 소프트웨어 개발 접근 방식에 투자한다는 생각

DDD가 해줄 수 있는 일

  • 도메인 전문가와 개발자는 가까운 거리에서 유비쿼터스 언어로 소통하면서 협업해야 한다.
  • 기술보다는 도메인 가치가 우선이다.

도메인의 복잡성과 씨름하기

핵심 도메인과 서브 도메인

ex) 커머스 솔루션에서 핵심 도메인은 결제이며, 서브 도메인은 배송이다.

DDD를 통해서 복잡화하지 말고 단순화하라

Anemic Domain Model : 빈약한 도메인 모델. 속성만 표현하는 단순한 데이타 홀더. 보통 트랜잭션 스크립트와 연결된다. Rich Domain Model : 풍부한 도메인 모델. DDD를 통해서 도메인을 표현한 풍부한 행위를 가지는 모델

과거에는 객체 관계형 임피던스 부조화 때문에 Anemic Domain Model을 많이 사용하게 되는데, 이제 ORM이 대중화 되어서 할만하다!!

왜 무기력(Anemic)증이 일어나는가?

  • 절차적 사고방식의 익숙함
  • 단순한 샘플코드를 참고
  • 비주얼 베이직 탓 : 클릭하고 드래그 앤 드랍 하면 프로그램이 만들어짐 - 이건 좀 ….

Getter, Setter의 자바빈을 숨기는 여러 방법이 있었지만, 대부분 개발자는 그러려고 하지 않았거나, 왜 그렇게 해야 하는지 이해조차 하지 못했다. 온통 무기력증이다.

일반적인 행위로 모든 상황(도메인 지식)을 커버하려 한다. 이것은 안티 도메인 모델이 된다. 코드로 말하면 Setter들의 향연이 벌어진다. 그저 데이터 홀더일 뿐이다. -> 무기력증

뷰를 위한 모델은 Getter, Setter로 구성되는 것이 좋다.(DTO 패턴) 하지만 도메인 모델은 아니다.

p.67 p.69 코드를 보면서 반성하자.

DDD는 어떻게 하는가?

유비쿼터스 언어 : 반운디드 컨텍스트 내에서 공유된 언어. 도메인 전문가화 개발자 모두에 의해 개발되어 공유된 언어. - 서로 많이 이야기 하자.

ex) 간호사가 독감 백신을 표준 용량으로 환자에게 투여한다. : nurse.administerFluVaccine(patient, vaccine);

  • 도메인 모델링 - 소프트웨어는 계속 진화한다. 모델 설계를 관리하지 말고 언제나 버릴 수 있어야 한다. 결국 코드가 설계이다.
  • 유비쿼터스 언어 용어집 만들기
  • 도메인 정제 (결국 많은 이야기를 해야한다)

p.75 샘플로 도메인적 표현을 가지는 모델링을 생각해보자

  • 가독성이 좋아졌다.
  • 도메인의 표현이 보인다.
  • 그리고 클라이언트 입장에서 테스트

DDD를 사용하는 데서 오는 비즈니스 가치

부제 : 여러분의 상사에게 DDD를 파는 방법

  1. 조직이 그 도메인에 유용한 모델을 얻는다.
  2. 정교하게 정확하게 비즈니스를 정의하고 이해한다.
  3. 도메인 전문가가 소프트웨어에 설계에 기여한다.
  4. 사용자 경험이 개선된다.
  5. 순수한 모델 주변에 명확한 경계가 생긴다.
  6. 엔터프라이즈 아키텍처의 구성이 좋아진다.
  7. 애자일하고, 반복적이고 지속적인 모델링이 사용된다.
  8. 전략적인 동시에 전술적인 새로운 도구가 적용된다.

DDD 적용의 난관

  • 유비쿼터스 언어 만들기
    • 도메인 전문가와 소통
  • 개발자의 사고방식 전환 : 기술 보다는 도메인이 먼저다.
    • 객체는 속성(데이터)이 중요한 것이 아니라 행동이 중요하다.

가장 중요한 것은 팀 내 문화와 DDD의 학습곡선 인 것 같다.

p.83을 보면 도메인 전문가와 친해지는 법이 나오는 게 진정 이런게 TIP 이다.

Example 백로그를 스프린트로 커밋한다.

Anemic Domain Model

backlogItem.setSprintId(sprintId);
backlogItem.setStatus(BacklogItemStatusType.COMMITED);
  • 행위가 원자적이지 않으며, 데이터 의존적이며, 불변식이 깨질 수도 있다.

Rich Domain Model

backlogItem.commitTo(sprint);
  • 도메인의 표현이 드러나고 원자적이며, 행동이 캡슐화 되어 있으며, 도메인 모델 밖으로 로직이 세어 나가지 않는다.

위 상태에서 아래와 같은 추가사항이 생길 시 어떤 방식이 더 기민하게 반응할 수 있을까?

만약 백로그 항목이 이미 다른 스프린트로 커밋됐다면, 먼저 언커밋 해야한다. 커밋이 완료되면 이해 당사자에 알려라(도메인 이벤트)

도메인 모델의 합리화

어짜피 대부분의 엔터프라이즈 웹애플리케이션에서는 도메인 모델이 좋은 선택이라고 본다.

DDD는 무겁지 않다.

with TDD. 클라이언트 입장에서 도메인 모델을 구현하는 것은 큰 도움이 된다.

가상 + 약간의 현실

협업툴!!

가상의 프로로젝트를 진행하는 것으로 이야기를 풀어보자

사스오베이션, 그들의 제품과 DDD의 사용

  • 콜랍오베이션 : SNS
  • 프로젝트오베이션 : ITS

DDD-Lite(전술)를 이용함. 즉 바운디드 컨텍스트(전략)는 무시함.

마무리

  • 복잡성 극복
  • 트랜잭션 스크립트의 단점
  • Rich Domain Model이 좋다.
  • DDD 약팔이

ToC

  1. 동기
  2. 사전지식
  3. LSP
  4. Practice
  5. Summary

동기

어느 날이었습니다. 팀 내에서 상속에 대한 이야기를 하다가 주니어가 그럼 상속은 어떻게 써야하는지 궁금해 했습니다. 그런데 이 상속의 위험성을 설명하려고 하니, 말로만 하기에는 부족한 것 같고, 그렇다면 worst-case 코드를 보여줘야 하는데 시간이 없었습니다. 그래서 상속의 위험성에 대한 소스코드와 그것을 설명하는 블로그 아티클을 작성해야겠다는 생각이 들었습니다.

사전지식

  • Java 언어를 기본으로 설명합니다.

가변과 불변

  • 객체지향은 가변(mutable)을 캡슐화(또는 관리)해서 복잡성을 제어합니다.
  • 하지만 근본적으로 가변은 부수효과(side-effect)를 동반합니다.
  • 그래서 가능하면 가변을 최소화 하는 것이 유리합니다.
  • 이를 해결하기 위해서 불변(immutable)을 이용하는 것도 좋은 방법입니다.

접근제어자

  • public : 모두 접근 가능
  • protected : 자식 클래스와 같은 패키지 상에서 접근 가능
  • default(package) : 같은 패키지 상에서 접근 가능
  • private : 클래스 내에서만 접근 가능

public, protected는 열려 있으며, default, private는 닫혀 있다. - Joshua bloch

  • 접근제어는 가능한 닫혀 있는 것이 좋습니다.
  • public field는 캡슐화되지 않으므로 Evil으로 규정합니다.

불변식(불변조건)

클래스 불변식(Class Invariant)은 해당 클래스의 오브젝트가 가지는 제약사항을 말합니다. 즉 불변식이 깨지면 해당 객체는 유효하지 않다고 봐야하며, 애플리케이션 내 클래스의 계약을 위배했으므로, 문제를 발생시킵니다.

예를 들어서 분수를 나타내는 클래스가 있다고 가정해 보겠습니다.

class 분수 {
    public int 분자;
    public int 분모;

    @Override
    public String toString() {
        return 분자 + "/" + 분모;
    }
}

class 분수Test {
    @Test
    public void test분수_invalid() {
        분수 분수객체 = new 분수();
        분수객체.분자 = 1;
        분수객체.분모 = 0;  // !!! 분모는 0이 아니여야함 (불변식이 깨짐)
    }
}
  • 내부 필드가 public이기 때문에 캡슐화를 통해서 불변식을 강제할 수가 없습니다.
  • 불변식 제약사항을 강제하는 메소드를 재정의함으로써도 깨질 수도 있습니다.

LSP

서브타입(sub-type)은 그것의 기반 타입(base-type)으로 치환 가능해야 한다.

그냥 단순하게 기반 타입으로 치환만 된다는 것을 의미하지는 않습니다. 기반 타입의 행위들을 서브 타입의 행위들로 대치해도 문제가 없고, 불변식도 깨지지 않아야 함을 의미합니다. LSP를 자체를 설명하기 보다는 LSP가 위배되는 상황을 통해서 역으로 LSP를 알아보겠습니다.

유명한 Rectangle(직사각형) - Square(정사각형) 예제를 통해서 이를 확인해 보겠습니다.

class Rectangle {
    private int width;
    private int height;

    public void setWidth(int width) {
        this.width = width;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public final int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        super.setWidth(width);
        super.setHeight(height);
    }

    @Override
    public void setHeight(int height) {
        this.setWidth(height);
    }
}

위 정도면 충분히 LSP 를 만족한다고 보입니다. 과연 그럴까요? 먼저 Square 클래스의 불변식을 알아봅시다. 정사각형이기 때문에 길이와 높이가 같은 것이 불변식입니다. Rectangle는 어떤 불변식을 가질까요? 길이와 높이가 무조건 같이 변경되면 직사각형의 불변식이 위배됩니다. - 두 타입 간 충돌이 발생하는 느낌도 있습니다.

Rectangle 불변식이 깨지는 테스트

@Test
public void testRectanbleInvariant() {
    Rectangle rectangle = new Square();
    rectangle.setWidth(10);
    rectangle.setHeight(5);

    // 과연 답이 50이 나오는가? 25가 나와서 테스트는 실패하고, LSP가 만족되지 않음을 의미한다.
    assertThat(rectangle.getArea(), is(50));
}

즉, Square는 길이와 높이를 무조건 같이 변경하게 되지만, Rectangle는 길이와 높이가 같이 변경되면 예상치 못한 부수효과로 인해 불변식이 깨지게 됩니다. Rectangle의 불변식이 깨지게 되어서 결국 LSP도 위배하게 됩니다. - 하지만 Square의 불변식은 유지됩니다.

물론 불변식이 깨진다고 해서 무조건 LSP가 위배되는 것은 아닙니다.

위 상황에서 setWidth(int), setHeight(int)를 재정의 하지 않고 setLength(int)와 같은 메서드를 구현하는 방법도 존재하나 그런 경우에는 width, height가 각각 변경될 수 있으므로 Square의 불변식(width, height는 동시에 변경되어야함)이 깨질 수 있습니다.

Solution

먼저 상속을 유지한 상태에서 해결 방안을 알아보겠습니다.

class Retangle {
    private final int width;
    private final int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    public final int getArea() {
        return width * height;
    }
}

class Square extends Rectangle {
    public Square(int length) {
        super(length, length);
    }
}

부수효과는 가변메서드(Setter)에서 발생합니다. 그럼 애초에 원인이 되는 가변을 모두 제거해서 위와 같이 불변(Immutable)을 통해서 문제를 해결할 수 있습니다.

Another Solution

다른 방법을 알아볼까요? 애초에 이 애플리케이션 세계에서 사각형과 정사각형은 상속구조가 어울리지 않는 것 같습니다.

interface Shape {
    int getArea();
}

final class Rectangle implements Shape {
    private final int width;
    private final int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public int getArea() {
        return width * height;
    }
}

final class Square implements Shape {
    private Rectangle target;

    public Square(int length) {
        setLength();
    }

    public void setLength(int length) {
        this.target = new Rectangle(length, length);
    }

    @Override
    public int getArea() {
        return target.getArea();
    }
}

상속 보다는 합성(Composition) 원칙에 입각해서 위와 같이 수정하는 것도 한 방법입니다.

  • 실제로 중요한 것은 넓이를 구하는 행위이지 사각형이냐, 정사각형이냐는 그 다음 문제입니다.
  • 또한 합성을 이용하면 Setter(‘setLength’)가 있더라도 불변식이 깨지는 부수효과가 발생하지 않습니다.

Practice

아래 다양한 예시를 통해서 더 안전한 상속을 구현하는 방법을 알아보겠습니다.

메서드 재정의

메서드가 재정의 불가능하게 final로 닫는 것이 좋습니다.

Bad

Good

Support 타입

상속을 단순 코드 재사용으로 사용하는 경우(추상 메서드가 없는 경우)에는 합성(Composition)을 사용하는 것이 좋습니다.

Bad

Good

Template

템플릿 메소드 패턴의 경우 아래와 같은 코드로 정형화 하는 것이 좋습니다. - 이것은 상속을 이용한 Good Practice 중 하나입니다.

  • 템플릿 패턴은 변하는 부분과 변하지 않는 부분의 관심사 분리가 중요합니다.
  • 변하는 부분은 다형성을 위해 열어두고 변하지 않는 부분은 불변 템플릿(final)으로 만듭니다.

Good Sample 중 일부 코드

public abstract class AbstractSafePrefixContentHolder implements ContentHolder {
    // 가능한 필드는 닫고 불변화 시킨다. 접근이 필요할 때만 점진적으로 연다.
    private final String content;

    public AbstractSafePrefixContentHolder(String content) {
        this.content = Objects.requireNonNull(content); // 여기에서 제약조건을 추가할 수 있다. : 선행조건으로 불변식 강제
    }

    @Override   // 템플릿 : 재정의 불가능하게 final
    public final String getContent() {
        return getPrefix() + content;
    }

    // 다형성으로써 추상 메서드만 오픈시킨다.
    abstract protected String getPrefix();
}

Bad

Good

Summary

  • 불변식을 지킵니다.
  • 접근제어는 가능한 닫습니다 : field는 private
  • 가능한 변경을 최소화 합니다 : final
  • 불변을 이용하거나, 인터페이스를 통한 합성으로 변경해 봅니다.

예외

Reference

github : https://github.com/redutan/dangers-of-inheritance/

ToC

  1. Consul
  2. Consul Configuration
  3. Consul Discovery
  4. Summary

Consul

Consul은 인프라 전체에서 서비스를 발견하고 구성하는 도구입니다. 주요 기능은 아래와 같습니다.

  • Service Discovery
  • Health Checking
  • KV Store : 서비스 구성 관리 가능 (for Spring Config)
  • Multi Datacenter : SPoF 예방

Consul은 예전부터 spring-cloud 적용할 시 관심이 가던 컴포넌트 였었습니다. 하지만 config는 Config Server(git or svn), discovery는 Eureka Server 를 주로 이용했었기 때문에 상대적으로 관심이 멀었지요.

그러던 중 팀을 옮기고 신규 프로젝트를 kickoff 함에 있어서 discovery와 config 서비스를 도입하면 어떠냐고 했다가 이에 관심을 가지는 콤틴 덕분에 consul 에도 관심이 생기기 시작했습니다.

과거 프로젝트에서는 예제도 상대적으로 부족하고 레퍼런스가 많은 인기 설정을 바탕으로 환경을 구성했는데, 콤틴의 말을 듣고 보니, consul이 상당히 매력적으로 느껴졌습니다. 저도 이전에 consul을 리서치해 보았는데요. 상대적으로 Netflix OSS(eureka)를 선호하게 되었고, 라이선스 이슈 때문에 넘어갔었습니다.

그러나 이제 한 번 consul이 얼마나 가능성이 있는지 확인해보고 싶었습니다.

준비

Install Consul

저는 macOs를 사용하기 때문에 패키지매니저 brew를 통해서 간단하게 설치하였습니다.

$ brew install consul
...
# 버전학인
$ consul -v
Consul v1.0.6
...
# 기동
$ brew services start consul
...
==> Successfully started `consul` (label: homebrew.mxcl.consul)

Consul Web UI

http://127.0.0.1:8500/

Consul Web UI

다른 OS를 사용하는 경우에는 공식 홈페이지 Getting Started를 참고해주세요

Spring Boot Prject

간단합니다. 이미 consul은 spring-boot에 통합이 잘 되어 있어서 SPRING INITIALIZR로 쉽게 프로젝트 생성이 가능했습니다. 이번에는 config, discovery 둘 다 consul 기반으로 설정해 봅니다.

SPRING INITIALIZR 사이트를 통해서 가능하지만 저는 편하게 IDEA를 이용해서 생성하였습니다.

Consul Configuration

pom.xml 상 의존성 정보

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
  • 본 예제에서는 configdiscovery 서비스를 한 애플리케이션에서 관리하는 것으로 설정했습니다.

bootstrap.yml

spring:
  application:
    name: config-client
  cloud:
    consul:
      host: localhost # (1)
      port: 8500
      config:
        format: FILES # (2)
  1. 서버 접속 정보를 설정합니다.
  2. format 방법이 여러가지가 있는데, FILES가 가장 직관적이고 스프링부트 설정파일과 유사합니다.

Spring Cloud Consul 기본 설정

Spring Cloud Consul 기본 Key 예시

config/application.properties      # (1), (2)
config/application-dev.properties  # (3)
config/config-client.yml           # (4)
config/config-client-dev.yml       # (5)
  1. 기본적으로 config 리소스로 시작합니다.
    • 커스터마이징 가능 : spring.cloud.consul.config.prefix
  2. config/application.properties : 기본적으로 모든 애플리케이션에 바인딩 될 구성
  3. config/application-dev.properties : 모든 애플리케이션에서 dev 프로필이 활성화 된 경우 바인딩 될 구성
  4. config/config-client.yml : config-client 애플리케이션에 바인딩 될 구성
  5. config/config-client-dev.yml : config-client 애플리케이션에 dev 프로필이 활성화 된 경우 바인딩 될 구성
    • ,로 프로필을 구분하지만 커스터마이징 가능 : spring.cloud.consul.config.profileSeparator

spring.cloud.consul.config.format=FILES 구성을 통해서 yml, properties 형식을 혼용해서 사용할 수 있습니다.

만약 위 구성에서 config-client 애플리케이션에 dev 프로필이 활성화 된 경우 아래 처럼 구성이 로딩되며 제일 마지막 구성으로 덮어쓰게 된다

  1. config/application.properties
  2. config/application-dev.properties
  3. config/config-client.yml
  4. config/config-client-dev.yml

Consul Web UI에서 구성하기

Consul Web UI에서 구성하기

구성 우선순위 예시

config/config-client-dev.yml

message:
  hello: "Hello ConfigClient dev"

ConfigClientAppliation.java

@Bean
CommandLineRunner init(@Value("${message.hello:Not found message!!}") String hello) {
    return (String... args) -> log.info(hello);
}
  • 위 상황에서 ConfigClientAppliationdev 프로필로 기동시키면 Hello ConfigClient dev 로그를 확인할 수 있다.
  • 기존에 먼저 바인된 구성에 중복되는 키가 있으면 무조건 덮어쓰게 되며, 없으면 기존 구성을 그대로 사용합니다.

properties VS yml

만약 config/application.propertiesconfig/application.yml 동시에 있다면 어떻게 될까?

답은 config/application.properties의 구성이 바인딩 됩니다.

Consul Configuration Summary

Consul ConfigurationZookeepr Configration과 유사한 부분이 많습니다. 그리고 구성 서버로써 기본적인 기능은 모두 만족하는 것 같습니다. 하지만 Config Server에 비하면 부족한 점이 보이는데요.

  1. 구성파일이 안전하지 못하며(구성파일을 실수로 삭제하면 복원불가)
  2. scale-out(수평확장) 시 구성파일 동기화하기 힘듭니다.(각 서버 간 KV Store를 동기화)
    • 물론 유료 라이선스를 이용하면 되긴하는데, OpenSource 라이선스 버전으로는 운영과 설정이 작업이 더 발생합니다.

Config Server의 경우 형상관리(ex:git)에 의존하기 때문에 삭제해도 복원이 쉬우며, 저장소가 분리되어 있고 분산되기 때문에 고가용성을 위해 쉽게 서버를 scale-out 할 수 있습니다.

하지만 git2consul 이라는 컴포넌트를 통해서 위 영속화 이슈를 해결할 수 있습니다. !!!

git2consul

git2consul is a Consul community project that loads files from a git repository to individual keys into Consul

.gitignore
application.yml
bar.properties
foo-development.properties
foo-production.yml
foo.properties
master.ref

위와 같이 git 저장소 내용을 consul의 KV Store에서 로딩해서 사용할 수 있습니다.

Consul Discovery

Service Discovery

consul 클라이언트는 api서비스 또는 mysql과 같은 플랫폼을 제공 할 수 있으며, consule discovery를 이런 클라이언트를 등록 관리합니다. 그래서 다른 애플리케이션에서 의존하는 서비스를 DNS 또는 HTTP를 사용하여 쉽게 찾을 수 있습니다. 소위 말하는 LoadBalancer를 대신할 수 있으며 동적으로 서비스 등록관리를 할 수 있습니다. 최근 클라우드 환경에서는 기본적으로 필요한 기능 중 하나입니다.

Configuration

pom.xml 상 의존성 정보

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

bootstrap.yml

spring:
  application:
    name: edge-service  # (1)
  cloud:
    consul:
      host: localhost   # (2)
      port: 8500        # (2)
      config:
        format: FILES
  • Consul Configuration을 사용한다고 가정하였습니다.
    1. application 이름을 등록하는 것은 중요합니다.
    2. consul 서버를 설정해서 config, discovery 기능을 사용할 수 있게 합니다.

{consul서버}/config/application.yml

management.endpoints.web.base-path=/actuator    # (1)
  1. spring-boot version 2 부터는 management.endpoints.web.base(as-is managment.context-path) 속성키로 변경되었습니다. 다음 설정에서 의존하기 위해서 미리 정의합니다.

{consul서버}/config/edge-service.yml

spring:
  cloud:
    consul:
      discovery:
        instance-id: ${spring.application.name}-${spring.application.instance-id:${random.value}}   # (1)
        health-check-path: ${management.endpoints.web.base-path}/health                             # (2)
        health-check-interval: 5s                                                                   # (3)

server:
  port: 0   # (4)
  1. instance-id를 랜덤 기반으로 정의합니다. : edge-service-1914caad6143e246306bb077b16f9484
  2. actuator의 health EndPoint를 바탕으로 헬스체크할 수 있게 설정합니다.
  3. 헬스체크 주기는 5초로 설정합니다. (default 10s)
  4. 0으로 설정하면 random으로 포트가 바인딩됩니다.

EdgeServiceApplicaton.java

@SpringBootApplication
@EnableDiscoveryClient  // (1)
public class EdgeServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(EdgeServiceApplication.class, args);
    }
}
  1. @EnableDiscoveryClient를 통해서 Service Discovery의 클라이언트로 등록시킵니다.

자 이제 서비스를 기동하면 정상적으로 Consul에 등록되는 것을 확인할 수 있습니다.

consul에 등록된 edge-service들

consul-edge-service

Call Service

Call 구성도

call-wiki-service

  • Client : 호출자입니다. 여기서는 실제 고객이기 보다는 edge-service를 이용하는 다른 서버로 생각하시면 편할 것 같습니다.(ex:nginx, apache)
  • edge-service : API-GW로 생각하면 편합니다.
  • wiki-service : 실제 위키라는 도메인을 가지는 API 서비스입니다.
  • consul-server : Service Discorvey와 Config를 책임집니다.

호출 흐름

  1. Clientconsul-server을 통해서 edge-service의 정보를 제공받습니다. (DNS, HTTP)
  2. 제공 받은 edge-service를 통해서 API를 호출합니다.
  3. edge-serviceconsul-server을 통해서 downstreamwiki-service 정보를 제공 받아서 호출합니다.(1과 유사함)
  4. Client 원하는 wiki 목록 응답을 받습니다.

Wiki API on edge-service

최상위 페이지들을 조회하는 api(/wiki/pages)를 호출한다고 가정하겠습니다.

이럴 위해서 edge-service에서 API를 제공해야 합니다.

edge-service의 Controller와 Service

@RestController
@RequestMapping("/wiki")
class WikiRestController {
    private final WikiService wikiService;

    public WikiRestController(WikiService wikiService) {
        this.wikiService = wikiService;
    }

    @GetMapping("/pages")
    public List<Page> topPages() {
        return wikiService.getTopPages();
    }
}

@Service
class WikiService {
    private final RestTemplate restTemplate;

    public WikiService(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public List<Page> getTopPages() {
        return restTemplate.exchange(
                "//wiki-service/pages",     // (1)
                HttpMethod.GET,
                null,
                new ParameterizedTypeReference<List<Page>>() {
                })
                .getBody();
    }
}
  1. 여기에서 가장 중요한 설정입니다. //를 통해서 protocol은 승계받은 상태에서 wiki-service를 즉 서비스아이디로 호출합니다.

그럼 위 호출을 가능하게 하는 설정은 무엇일까요? 바로 아래를 확인하시면 됩니다.

로드밸런싱되는 RestTemplate 구성

    @Bean
    @LoadBalanced   // (1)
    RestTemplate loadBalancedRestTemplate() {
        return new RestTemplate();
    }
  1. 모든 의문점은 여기서 풀립니다. @EnableDiscoveryClient이 활성화 되어 있으면서 RestTemplate bean에 @LoadBalanced만 달아주면 모든 설정을 spring boot에서 자동으로 해줍니다. 개발자가 기억할 것은 서비스 명 뿐이죠!!

그럼 이제 consul-server, edge-service, wiki-service 모두 기동해 보겠습니다.

IDEA 상 서버 구동 상태

applications-run-on-idea

현재 클라이언트 구현이 힘들어서 그냥 Direct로 edge-service 노드 중 하나를 직접 호출해 보겠습니다.

http://edge-service/wiki/pages 호출 결과

GET http://127.0.0.1:64777/wiki/pages

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 14 Apr 2018 09:07:45 GMT

[
  {
    "pageId": 1,
    "title": "기획팀 Home",
    "content": "기획팀 대시보드 페이지"
  },
  {
    "pageId": 2,
    "title": "개발팀 Home",
    "content": "개발팀 대시보드 페이지"
  },
  {
    "pageId": 3,
    "title": "디자인팀 Home",
    "content": "디자인팀 대시보드 페이지"
  }
]

Response code: 200; Time: 21ms; Content length: 174 bytes

Consul Discovery Summary

마지막으로 Consul Discovery를 정리해보겠습니다.

  • 기본적인 기능은 다른 서비스와 비교해서 전혀 문제가 없습니다. 유료 라이선스 시 에는 오히려 더 나은 점이 많습니다.
  • consul에서 Service Discovery Server를 직접 제공하기 때문에 Eukara Server에 애플리케이션 사용입장에서는 운영비용이 줄어듭니다.
  • 그렇다면 consul 자체에 대한 운영을 고민해야하는데, 고가용성을 위해서 consul 서버를 최소 2대 이상 구성해야할 것입니다.
  • 문제는 고가용성을 확보하기 위한 중앙화된 등록정보 영속화 기능이나, 등록정보 복제가 필요한데, Open Source 라이선스 상으로는 제공되지 않습니다.
    • 이 말은 서버 한대만 구성이 가능하며 SPoF가 될 수 있습니다. (다른 방법이 있을 수 있겠지만, 결국 커스터마이징 포인트가 되는 것 입니다.)
    • 쉽게 해결하기 위해서는 최소 Pro 라이선스가 요구됩니다. Enhanced read scalability

VS Zookeeper

  • 개발과 운영 편의를 위한 툴 자체가 consul이 압도적으로 낫습니다. 도메인에만 집중할 수 있는 면에서 종합선물세트와 같은 consul이 더 낫습니다.
  • 하지만 유로 라이선스가 요구됩니다.

Zookeeper Discovery는 직접 프로토타이핑 한 적이 없어서 의견이 조심스럽습니다.

VS Eureka

  • 기본적인 기능과 편의성은 비슷합니다만, Advanced 기능은 consul이 조금 더 나은 것 같습니다.
    • 둘 다 WEB UI를 제공하며, spring-boot 상에서 Discovery Client 기능을 이용하는데, 무리가 없습니다.
  • 역시나 유로 라이선스가 아닌 이상에는 Eureka가 더 낫지 않나 싶습니다.

Summary

한마디로 consul유료 라이선스 쓴다면 추천

  • Consul Configuration는 별로인 것 같습니다. Config Server가 여러모로 더 나은 것 같습니다.
    • 디렉토리 기반 Key-Value Store 기능은 너무 열악합니다. 그저 부가기능 정도의 느낌입니다.
    • 물론 git2consul를 이용하면 가능하지만 Config Server 보다 구성이 훨씬 불편합니다.
  • Consul Discovery는 상당히 추천할만 합니다.
    • 하지만 운영 환경에서는 최소 Pro 라이선스가 요구됩니다.
    • 개인적으로는 spring-cloud-netflix에서 제공되는 Eureka Server가 운영 환경을 만족하면서 간단하게 구성할 수 있는 것 같습니다.

consul 은 충분히 강력한 도구이지만, 사용할 기능 대비 구성의 복잡도가 높거나 운영 비용이 상대적으로 더 높은 것으로 판단됩니다.

사람들이 많이 쓰는 것은 그만한 이유가 있는 것 같습니다.

Reference

example github : https://github.com/redutan/spring-boot2-consul

PlantUml

@startuml
Client .> [consul-server] : Find service
Client -> [edge-service] : /wiki/pages
[edge-service] -> [wiki-service] : /pages(downstream)
[edge-service] ..> [consul-server] : config & discovery lookup
[wiki-service] ..> [consul-server] : config & discovery lookup
@enduml

동기

시작은 popit 아티클

어느날 popit에 sonarqube를 이용한 코드 자동리뷰 아티클이 올라왔습니다.

조직장(요다)이 이것을 팀 내에 적용해보자고 해서 (이미 저희 팀은 sonarqube로 정적분석을 하고 있습니다.) 적용을 시작합니다.

생각보다 적용이 쉽지 않으며, 설정 포인트도 많습니다. 물론 한 번 힘들게 설정하면 그 이후에는 자동화되기 때문에 tradeoff 할 만 하지요. 적용할 프로젝트 수 자체가 많으면 초기 설정이 조금 힘들 순 있습니다. - 하지만 단순 반복 작업이기 때문에 복사하기-붙여넣기를 잘하면 되긴 합니다. -

하지만 코드를 작성하고, 커밋하고, 푸시하고, PullRequest를 올려야지 정적분석 피드백을 받을 수 있습니다. 즉, 코드 작성 시와 정적분석 피드백 시 사이의 시간 차가 너무 큽니다.

그래서 코드를 작성과 가능한 가까운 시간에 바로 피드백을 받는 것이 더 좋지 않을까 하는 생각이 들었습니다.

대안 : SonarLint

그러던 중 팀원 콤틴이 IDEA plugin인 SonarLint를 사용하는 것을 추천하였습니다.

팀 내에서 IntelliJ IDEA를 사용하고 있었고, 확실히 합리적인 선택이 될 것 같았습니다.

SonarLint 적용

Install

  • preference > plugins
  • SonarLint 검색
  • 결과창 내 Search in repositories 링크 선택 or 하딘 Browse repositories... 버튼 선택

SonarLint 검색

  • Install and Restart IntelliJ IDEA

Staring과 다운로드 갯수가 많고, 최근 업데이트도 지속되고 있는 것 같아서 믿을만한 플러그인 같습니다

Install 확인

SonarLint after install

Configuration

기본적인 설정만으로도 충분히 사용할 수 있으며 sonarqube 서버가 없어도 사용에 문제가 없습니다.

SonarQube 서버 연동

서버 연동을 하게 되면 더 다양한 분석룰을 팀 내 전반적으로 일관성 있게 관리할 수 있습니다.

preference > Other Settings > SoanrLint General Settings

SoanrLint General Settings

  • Automatically trigger analysis 체크하는 것을 추천
    • 또는 해제하고 commit 시에만 분석하는것도 좋음

위 화면을 통해서 적절하게 설정하면 됨

SoanrLint Project Settings 를 통한 설정도 가능

SoanrLint Project Settings

Commit 시 분석 추가

커밋(Cmd + K) 창에서

SoanrLint Commit Setting

  • Perform SonarLint analysis 를 선택 (기본으로 체크되어 있음)
  • 개인적으로 추천 commit 할 때마다 분석이 됨

Usage

현재 파일 분석

mac : Cmd + Shift + S or

SoanrLint Analyze Current

그 외 가능한 사용법

  • 전체 파일 분석 : Analyze All Files with SonarLint
  • VCS(ex: git)상 변경된 분석 검사 : Analyze VCS Changed Files with SonarLint

SoanrLint IDEA Actio

*Cmd + Shift + A, SonarLint로 검색한 액션들

Analyze Example

public class AccountRestController {
...
    @PostMapping
    public ResponseEntity<?> create(@RequestBody AccountDto.Create create) {
        Account created = accountService.create(create);
        return ResponseEntity.created(toLocationUri(created.getId())).build();
    }
...
  • detection point : ResponseEntity<?>

SoanrLint analysis Current

비지터 패턴

클래스 계층 구조에 새로운 메소드를 추가할 필요가 있지만, 그렇게 하는 작업은 고통스럽거나 설계를 해치게 된다.

디자인 패턴의 비지터 집합

타입 계층 구조를 변경하지 않고도 새로운 메소드를 계층 구조에 추가할 수 있음

  • 비지터(Visitor)
  • 비순환 비지터(Acyclic Visitor)
  • 데코레이터(Decorator)

비지터 패턴 상세

이중 디스패치(dual dispatch) : 실행되는 연산이 요청의 종류와 두 수신자의 타입에 따라 달라진다는 뜻

비지터 패턴 클래스 다이어그램

비지터 패턴 클래스 다이어그램

비지터 패턴 시퀀스 다이어그램

비지터 패턴 시퀀스 다이어그램

연산의 변화 vs 구조의 변화

  • 새로운 구상체가 계속 추가된다면 Visitor은 독이 된다.
  • 새로운 연산이 계속 추가된다면 Vistor은 좋은 선택이다.

비순환 비지터 패턴

  • down-casting가 있어서 쓰기가 꺼려진다.
    • 타입안정성이 약해짐
  • 만약 generic을 이용할 수 있다면 더 낫지 않을까?
public void accept(ModemVisitor v) {
    try {
        ErniedModemVisitor ev = (ErnieModemVisitor) v;  // !!!
        ev.visit(this);
    } catch (ClassCastException e) {
    }
}

p.505 ErnieModem.java 일부

데코레이터 패턴

소리나는 다이얼 모뎀을 만들고 싶다

  • OCP, SRP 원칙을 지킬려면 어떻게 해야할까?
  • 데코레이터!!!
public class LoudDialModem implements Modem {
    private Model modem;

    public LoudDialModem(Modem m) {
        this.modem = m
    }
    public void dial(String pno) {
        modem.setSpeakerVolume(10); // 데코레이팅!!!
        modem.dial(pno)
    }
    ...
}

확장 객체 패턴

  • 아답터 패턴과 유사하다.
  • 단점은 역시나 많은 subclass를 동반하는 것이다.
  • 장점은 OCP와 SRP를 지킬 수 있다

확장 객체 패턴 클래스 다이어그램

확장 객체 패턴 클래스 다이어그램

참고 : The Extension Objects Pattern.pdf

스테이트 패턴

유한 상태 오토마타의 개괄

유한 상태 기계 : Finite State Machine

  • ex) 개찰구

개찰구 상태 다이어그램

개찰구 상태 다이어그램

구현기법 : 중첩된 switch/case

switch (state) {
    case LOCKED :
        switch (event) {
            case COIN :
                state = UNLOCKED;
                turnstileController.unlock();
                break;
            case PASS :
                turnstileController.alarm();
                break;
        }
        break;
    case UNLOCKED :
        switch (event) {
            case COIN :
                turnstileController.thankyou();
                break;
            case PASS :
                state = LOCKED;
                turnstileController.lock();
                break;
        }
        break;
}
  • turnstile : 개찰구

접근제어가 패키지인 상태변수

ISSUE 테스트를 진행할려면 테스트에서 상태를 변경할 수 있어야 한다.

  • 이를 위해서 같은 패키지에서 만들어지는 테스트를 위해서 상태 변수를 패키지 접근제어로 변경하였다.
  • effective-java에 의하면 패키지 접근제어도 캡슐화가 깨지지 않는 것으로 보이므로 큰 문제가 없다고 판단되며, 본인도 종종 이것을 애용하는 편이다.
    • 하지만 가능했다면, 패키지 생성자나 패키지 setter 메소드로 조금 더 제한하면 좋을 것 같다.
  • 만약 C++이었다면 friend로 해결가능했을 것이다.

중첩된 switch/case의 장단점

  • 간단한 상황에서는 가독성이 높고 효율적
  • 복잡한 상황에서는 가독성이 떨어지고 유지보수도 어려워지며, 실수에 취약하다.
    • 중복코드도 많이 생긴다.

구현기법 : 전이 테이블 해석

// 테이블 구축
public Trunstile(TurnstileController action) {
    turnstileController = action;
    addTransition(LOCKED, COIN, UNLOCKED, unlock());    // 마지막 메서드는 람다식으로 하면 더 나을 것 같다.
    addTransition(LOCKED, PASS, LOCKED, alarm());
    addTransition(UNLOCKED, COIN, UNLOCKED, thankyou());
    addTransition(UNLOCKED, PASS, LoCKED, lock());
}
...
// 전이 엔진
public void event(int event) {
    for (Transition transition : transitions) {
        // 아래 분기문은 transition의 메서드로 분기하면 좋을 것 같다.  : transition.isTransferable(state, event)
        if (state == transition.currentState && event == transition.event) {
            state = transition.newState;
            transition.action.execute();
        }
    }
}

전이 테이블 해석의 장단점

  • 정규적이며, 가독성이 좋다.
  • runtime 시 테이블 교체 가능(동적 제어)
  • ISSUE 테이블의 양이 커지면 검색 시간이 오래 걸린다??
  • ISSUE 테이블을 지원하기 위한 코드의 양도 많아 진다??

구현기법 : 스테이트 패턴

스테이트 패턴 클래스 다이어그램

  • ISSUE 하지만 예제에서는 Turnstile가 구상상태(TunstileLockedState 등)에 의존하고 있어서 DIP 원칙이 깨지고 있다. - p.544

Gof : 스테이트 패턴

스테이트 패턴 by Gof

Tcp를 예제로 한 스테이트 패턴 클래스 다이어그램

  • TCP Established : 연결상태
  • TCP Listen : 연결대기
  • TCP Closed : 종료

참고 : TCP 연결 상태 의미

스테이트와 스트래터지

strategy vs state

  • 스테이트 : 상태의 다형성 확보
  • 스트래터지 : 알고리즘(행위)의 다형성 확보
  • context의 상태 변경이 일어나는가?

스테이트 패턴의 장단점

  • OCP, SRP를 만족할 수 있다.
  • 상태 마다 각각 subclass가 필요해서 비용이 많이 든다.
  • 상태 기계의 모든 논리를 한 번에 파악할 수 없다 (vs 전이 테이블)

상태 기계 컴파일러(SMC)

전이 테이블 + 스테이트 패턴 = 3차원 유한 상태 기계(THREE-LEVEL FINITE STATE MACHINE)

SMC

상태 기계 컴파일러 장단점

  • 그냥 좋다. = Best practice