어떻게든 ApplicationContext만 가져오면 되는데 이미 SpringMVC에서 다 제공을 해주더군요

  1. RequestContextAwareTag을 상속한다.
    • 재미있는 부분은 해당 Tag클래스를 Spring Tag Library 구현체 모두 상속받아서 사용한다는 점입니다. 당연한 이야기 겠네요.
    • 예를 들면 MessageTag도 위 클래스를 상속받는데 jsp tag로 보면 <spring:message> 입니다.
  2. WebApplicationContext를 얻는다.
  3. 스프링 빈 주입

예제코드를 확인해봅시다.

// 1. RequestContextAwareTag 상속
public class CodeTag extends RequestContextAwareTag {
    @Autowired
    CodeService codeService;

...
    @Override
    protected int doStartTagInternal() throws Exception {
        if (codeService == null) {
            // 2. WebApplicationContext를 얻는다.
            WebApplicationContext wac = getRequestContext().getWebApplicationContext();
            AutowireCapableBeanFactory beanFactory = wac.getAutowireCapableBeanFactory();
            // 3. 스프링 빈 주입
            beanFactory.autowireBean(this);
        }
        // TODO working
        return SKIP_BODY;
    }
}

참 쉽죠 ~

참조 : http://stackoverflow.com/questions/3445908/is-there-an-elegant-way-to-inject-a-spring-managed-bean-into-a-java-custom-simpl

최근에 타 업체와 같이 암호화에 관해서 협의한 적이 있었습니다.

다들 어떻게 하는지는 잘 모르겠지만, 일반적으로 아래와 같이 의견교환을 할 것입니다.

암호화 스팩

  • 알고리즘 : AES-256
  • 암호화키 : abcdefghijklmnopqrstuvwxyz123457890
  • 인코딩 : UTF-8

알고리즘, 암호화키 만 보내는 경우가 일반적이며, 인코딩을 보내지 않는 경우도 심심찮게 확인할 수 있습니다. 그리고 샘플코드 라도 첨부파일로 보내주면 다행인데, 그렇지 않은 경우도 허다합니다. 일부 경우에는 검증되지 않은 블로그 포스트나 링크를 보내주고 그대로 해달라고 하기도 합니다.

참으로 답답한 현실이 아닐수가 없습니다. 저도 과거 주니어 시절에는 잘 모르고 위 처럼 의사소통을 하기도 하였는데, 지금 생각하면 얼굴이 붉어지는 일입니다 ㅠ

많은 개발자 들이 암호화에 대한 정확한 이해가 없이 의사소통을 하고 이런 방식이 어느 순간부터 대중화(?) 된 것 같습니다. 여기에 관련해서 해당 부분에 대한 간략한 이론적인 내용을 알아보고, 이제부터는 어떻게 암호화 스팩을 상호 간 공유해야 하는지 알아보겠습니다.

짚고 넘어가야할 부분은 여기서는 대중적으로 상호 간 통신 시 가장 많이 쓰이는 대칭키 암호화 방식으로 이야기 하겠습니다. 코드는 Java 기준으로 설명하겠습니다.

암호화 인터페이스

public interface Crypto {
  // 암호화
  String encrypt(String plain);
  // 복호화
  String decrypt(String cipher);
}

일반적으로 대부분의 개발자들은 개발 시 암/복호화를 위해서 위와 같은 인터페이스가 필요할 것입니다.

반론으로 왜 byte[] 가 지원되지 않는가?, 왜 암호화 키를 입력 받지 않는가도 있지만, 그것은 생각하지 않겠습니다.

위와 같은 간단한 인터페이스를 통해서 간단하게 암호화가 가능하게 할려면 최소 아래와 같은 내용을 확인해야합니다.

대칭키 암호화 시 스팩

  • 암호화 알고리즘
    • key size
    • 필요에 따라서 block size
  • 암호화 모드
  • Padding 방식
  • 암호화 키
    • 일부 암호화 모드에서는 초기화벡터 도 필요함
  • 암호문자열 바이트인코딩 + 문자인코딩
  • 키문자열(암호화키, 초기화벡터) - 바이트 간 인코딩 방식 (문자인코딩, 바이트인코딩 둘 다 가능)
    • 암호 문자열과 인코딩 방식과 키문자열 인코딩 방식이 다르면 필요

말로 풀어서 쓰니 어려우니 예를 들어서 위 내용을 풀어보겠습니다.

  • 암호화 알고리즘 : AES
    • key size : 256 bit
    • block size : 128 bit (AES의 블록사이즈는 128로 고정입니다. 고로 block size는 skip해도 됩니다.)
  • 암호화 모드 : CBC
  • Padding 방식 : PKCS5
  • 암호화키 : 12345678901234567890123456789012 (32자)
    • 초기화벡터 : 1234567890123456 (16자)
  • 암호문 인코딩 방식 : Base64 + UTF-8
  • 키 인코딩 방식 : ASCII (문자인코딩)

이렇게 보아도 이해가 잘 안되는 거 같습니다. 그래서 위에서 선언한 인터페이스를 구현하면서 어떻게 해당 내용이 구현되는지 보면서 알아보겠습니다.

암호화 인터페이스 구현

암호화 알고리즘, 모드, 패딩

public class AES256Crypto implements Crypto {

  // 알고리즘/모드/패딩
  private static final String algorithm = "AES/CBC/PKCS5Padding";

  // 암호화
  public String encrypt(String plain) {
    Cipher c = Cipher.getInstance(algorithm);
    // TODO
    return null;
  }
  ...
}

기본적으로 알고리즘, 모드, 패딩이 필요합니다.

  • 알고리즘은 암호화 알고리즘이며 예제의 경우 AES 방식을 사용합니다. 블록암호화 방식이기 때문에 Byte Padding이 필요합니다.
  • 모드는 암호화 방식입니다. 예제의 경우 CBC를 사용하는데 이럴 경우 초기화 벡터가 필요합니다.
  • 블록 암호화 방식이기 때문에 padding 방식이 필요합니다.

예제의 경우 우선 암호화(encrypt) 부터 구현하겠습니다.

참고 : 블럭암호 AES Padding와 암호화 모드

암호화 키, key size

public class AES256Crypto implements Crypto {
  // 알고리즘/모드/패딩
  private static final String algorithm = "AES/CBC/PKCS5Padding";
  // 암호화 키
  private final String secretKey;

  public AESCrypto(String secretKey) {
    if (secretKey.length != 256 / 8) {
      throw new IllegalArgumentException("'secretKey' must be 256 bit");
    }
    this.secretKey = secretKey;
  }

  // 암호화
  public String encrypt(String plain) {
    Cipher c = Cipher.getInstance(algorithm);
    // TODO
    return null;
  }
  ...
}

256 bit 암호화 방식이기 때문에 키 입력에 대한 유효성을 추가하였습니다. - 아직은 논란이 있는 코드입니다. key size가 암호화 알고리즘의 bit 수를 가르키는 것과 동일하게 됩니다. 즉 AES256 이라는 것은 암호화 키 사이즈가 256 bit 라는 말과 동일합니다.

초기화 벡터, block size

public class AES256Crypto implements Crypto {
  // 알고리즘/모드/패딩
  private static final String algorithm = "AES/CBC/PKCS5Padding";
  // 암호화 키
  private final String secretKey;
  // 초기화 벡터
  private final String iv;

  public AESCrypto(String secretKey, String iv) {
    if (secretKey.length != 256 / 8) {
      throw new IllegalArgumentException("'secretKey' must be 256 bit");
    }
    if (iv.length != 128 / 8) {
      throw new IllegalArgumentException("'iv' must be 128 bit");
    }
    this.secretKey = secretKey;
    this.iv = iv;
  }

  // 암호화
  public String encrypt(String plain) {
    Cipher c = Cipher.getInstance(algorithm);
    // TODO
    return null;
  }
  ...
}

AES의 block size는 128 bit 고정이기 때문에 별 다른 변화가 없습니다.

AES의 모태가 되는 Rijendael알고리즘 경우에는 128 192 256 bit이기 때문에 달라질 수 있습니다.

단, 초기화벡터(iv)의 경우 block size와 같아야 하기 때문에 128 bit 여야합니다. iv의 경우 더 강력한 암호화를 위해서는 암호화 요청 시 마다 달라지는 것이 보안에 더 좋으나, 예제이므로 우선은 초기 세팅으로 표현하겠습니다.

암호문 인코딩

public interface Encoder {
  // string -> bytes
  byte[] encode(String str);
  // bytes -> string
  String decode(byte[] bytes);
}

갑자기 새로운 인터페이스가 추가되었습니다. 암호문을 인코딩 하기 위해서는 위와 같은 인터페이스가 필요하기 때문입니다.

간단하게 설명하면 string -> bytes 가 encode이며, bytes -> string 는 decode입니다. 기본적으로 인코딩은 charset와 연관관계가 깊은 문자인코딩으로 생각하기 쉬운데, 암호화의 경우에는 암호화로 인해 문자인코딩과는 별개의 규칙이 없는 bytes가 반환되므로 문자로 표현할 수 없게 됩니다.

고로 암호문을 위한 인코딩 은 1차적으로 문자(charset)와 연관없는 인코딩이 가능해야합니다. 즉 문자열을 기준으로 해서 byte화 시키는 문자인코딩과는 다르게 bytes를 기준으로 문자열화 시키는 인코딩 방식이 필요 하게 됩니다.

가장 간단하게는 bytes를 16진수 기반으로 표현하는 Hex 방식이나 Base64 방식을 사용하는 것이 좋습니다.

예제는 Base64 방식으로 진행하겠습니다.

암호문 Base 64 인코딩 구현

public class Base64Encoder implements Encoder {
  // base64는 아스키 코드 내로 표현가능한 인코딩 방식
  private static final String ASCII = "US-ASCII";

  // string -> bytes
  public byte[] encode(String str) {
    return Base64.encodeBase64(str.getBytes(ASCII));
  }
  // bytes -> string
  public String decode(byte[] bytes) {
    return new String(Base64.decodeBase64(bytes), ASCII);
  }
}

편의상 Exception 핸들링은 제외하였습니다.

위 구현체를 바탕으로 AES256Crypto 를 계속 구현해 보겠습니다. 아까 예시에서 정의한 “암호문 인코딩 방식 : Base64 + UTF-8” 에서 UTF-8 은 이후에 나옵니다. Base64Encoder에 정의된 것과 혼동하면 안됩니다. : base64 스팩 자체가 ASCII에 의존하는 방식입니다.

참고 : 위키백과-base64

암호문 구현체에 암호문 인코더 주입

public class AES256Crypto implements Crypto {
  // 알고리즘/모드/패딩
  private static final String algorithm = "AES/CBC/PKCS5Padding";
  // 암호화 키
  private final String secretKey;
  // 초기화 벡터
  private final String iv;
  // 문자인코딩 방식
  private final String charset = "UTF-8";
  // 암호문 바이트 인코더
  private Encoder encoder = new Base64Encoder();
  ...

  // 암호화
  public String encrypt(String plain) {
    // 암호화 키 생성
    byte[] keyData = secretKey.getBytes("US-ASCII");
    SecretKey secureKey = new SecretKeySpec(keyData, "AES");

    Cipher c = Cipher.getInstance(algorithm);
    // 암호화 키 주입, iv 생성 주입, 초기화 - 이상하지만 우선 무시
    c.init(Cipher.ENCRYPT_MODE, secureKey, new IvParameterSpec(iv.getBytes("US-ASCII")));
    // 문자인코딩 방식을 통한 string -> byte 변환 후 암호화
    byte[] encrypted = c.doFinal(plain.getBytes(charset));
    // encoder#encode를 통한 byte -> string 변환
    return encoder.encode(encrypted);
  }
  ...
}

객체 변수로 charset 항목이 UTF-8로 추가되었습니다. 그리고 encoder 객체도 default로 생성된 상태입니다.

입력받은 plain은 문자열 이기 때문에 문자인코딩 방식을 통해서 byte로 변환하겠습니다.

// 문자인코딩 방식을 통한 string -> byte 변환
plain.getBytes(charset)

Java에서는 기본적으로 String#getBytes(String charset) 메소드가 있기 때문에 이것을 이용해서 bytes로 변환하였습니다.

암호화 한 후 bytes 로 반환된 값을 문자열로 출력해야하는데 이럴 경우에는 바이트 인코딩 방식이 필요합니다. 위에서 미리 선언해 둔 Encoder인터페이스를 통해서 변환하겠습니다.

// encoder.encode를 통한 byte -> string 변환
encoder.encode(encrypted);

이렇게 해서 1차적으로 암호화 부분 구현이 완료되었습니다.

키 관련된 부분은 우선 무시하겠습니다. 그냥 봐도 좀 이상하지만 다음에 수정하겠습니다.

키 인코더 구현

public class StringEncoder implements Encoder {
  // 문자인코딩
  private final String charset;

  public StringEncoder(String charset) {
    this.charset = charset;
  }
  // string -> bytes
  public byte[] encode(String str) {
    return str.getBytes(charset);
  }
  // bytes -> string
  public String decode(byte[] bytes) {
    return new String(bytes, charset);
  }
}

갑자기 매우 단순한 키 인코더 구현체가 나와서 당황스러울 듯 합니다. 하지만 키 인코딩의 경우 문자인코딩, 바이트인코딩 방식 둘 다 가능하기 때문에 위와 같이 Encoder 인터페이스를 통한 구현체를 제공하는 것이 다형성이 도움이 되기 때문에 이렇게 구성해보았습니다.

지금의 예제는 ASCII 방식으로 암호화 키와 초기화 벡터를 제공하지만 이런 경우에는 키 bytes 구성이 아스키 기반으로 인해서 단순해 집니다. 고로 가능하면 다양한 바이트 구성이 가능한 base64hex를 이용해서 키 bytes를 제공할 수 있게 하는 것이 보안에 유리합니다.

우선 예제를 위해서 위 StringEncoder를 사용하는 것으로 진행하겠습니다.

*중요한 점은 위 인코더는 문자인코딩 방식이기 때문에 암호문 인코더로 사용하면 정상적인 암/복호화가 불가능 하게 됩니다.**

암호문 구현체에 키 인코더 주입 후 변경

public class AES256Crypto implements Crypto {
  // 알고리즘/모드/패딩
  private static final String algorithm = "AES/CBC/PKCS5Padding";
  // 암호화 키
  private final String secretKey;
  // 초기화 벡터
  private final String iv;
  // 문자인코딩 방식
  private final String charset = "UTF-8";
  // 암호문 바이트 인코더
  private Encoder encoder = new Base64Encoder();
  // 키 인코더
  private Encoder keyEncoder = new StringEncoder("US-ASCII");
  ...

  // 암호화
  public String encrypt(String plain) {
    // 암호화 키 생성 - keyEncoder를 이용
    byte[] keyData = keyEncoer.encode(secretKey);
    SecretKey secureKey = new SecretKeySpec(keyData, "AES");

    Cipher c = Cipher.getInstance(algorithm);
    // 암호화 키 주입, iv 생성 주입, 초기화 - iv의 경우 keyEncodr를 이용
    c.init(Cipher.ENCRYPT_MODE, secureKey, new IvParameterSpec(keyEncoer(iv));
    byte[] encrypted = c.doFinal(plain.getBytes(charset));
    return encoder.encode(encrypted);
  }
  ...
}

keyEncoder를 이용해서 암호화 키와 초기화 벡터를 bytes로 성공적으로 변환하였습니다. 여기에서 keyEncoder의 경우 ASCII charset을 기반으로 생성하였는데, 대부분의 경우 암호화 키를 예제(12345678901234567890123456789012)와 같이 영어와 숫자 또는 특수문자 기반으로 문자열을 제공하기 때문에 ASCII 만으로도 충분합니다.

만약 한국어 등을 이용한다면 해당 문자를 표현할 수 있는 charset(ex:UTF-8, EUC-KR)로 변환해야 합니다만 그런 케이스는 거의 없을 것입니다.

위에도 설명했지만 가능하면 문자열 키도 base64 과 같은 바이트 인코딩 방식으로 제공하는 것이 보안에 좋습니다.

복호화 메소드 구현

public class AES256Crypto implements Crypto {
  // 알고리즘/모드/패딩
  private static final String algorithm = "AES/CBC/PKCS5Padding";
  // 암호화 키
  private final String secretKey;
  // 초기화 벡터
  private final String iv;
  // 문자인코딩 방식
  private final String charset = "UTF-8";
  // 암호문 인코더
  private Encoder encoder = new Base64Encoder();
  // 키 인코더
  private Encoder keyEncoder = new StringEncoder("US-ASCII");
  ...

  // 복호화
  public String decrypt(String cipher) {
    // 암호화 키 생성 - keyEncoder를 이용
    byte[] keyData = keyEncoer(secretKey);
    SecretKey secureKey = new SecretKeySpec(keyData, "AES");

    Cipher c = Cipher.getInstance(algorithm);
    c.init(Cipher.DECRYPT_MODE, secureKey, new IvParameterSpec(keyEncoer(iv));
    // encoder.decode 를 통해서 string -> bytes 변환
    byte[] encrypted = encoder.decode(cipher);
    // 복호화 후 문자인코딩 방식을 통한 byte -> string 변환 후 반환
    return new String(c.doFinal(encrypted), charset);
  }
}

암호화와는 반대로 진행하는 것을 확인할 수 있습니다.

암호화

  1. String#getBytes(String)(문자 인코딩)를 이용해서 bytes로 변환
  2. 암호화
  3. encoder.encode(바이트 인코딩)를 이용해서 String으로 변환

복호화

  1. encoder.decode(바이트 디코딩)를 통해서 bytes로 변환
  2. 복호화
  3. new String(byte[], String)(문자 디코딩)을 이용해서 String으로 변환

암/복호화 흐름

구현체에 중복코드가 많아서 약간의 리펙토링을 진행하고 전체코드를 보겠습니다.

AES256Crypt 리펙토링

public class AES256Crypto implements Crypto {
  public static final int KEY_SIZE = 256;
  public static final int BLOCK_SIZE = 128;
  private static final String AES = "AES";
  // 알고리즘/모드/패딩
  private static final String algorithm = AES + "/CBC/PKCS5Padding";
  // 암호화 키
  private final SecretKey secretKey;
  // 초기화 벡터
  private final IvParameterSpec iv;
  // 문자인코딩 방식
  private final String charset = "UTF-8";
  // 암호문 인코더
  private Encoder encoder = new Base64Encoder();
  // 키 인코더
  private Encoder keyEncoder = new StringEncoder("US-ASCII");

  public AESCrypto(String secretKey, String iv) {
    this.setSecretKey(secretKey);
    this.setIv(iv);
  }

  private void setSecretKey(String secretKey) {
    byte[] keyBytes = keyEncoder.encode(secretKey);
    if (keyBytes.length != KEY_SIZE / 8) {
      throw new IllegalArgumentException("'secretKey' must be "+ KEY_SIZE +" bit");
    }
    this.secretKey = new SecretKeySpec(keyBytes, AES);
  }

  private void setIv(String iv) {
    byte[] ivBytes = keyEncoder.encode(iv);
    if (ivBytes.length != BLOCK_SIZE / 8) {
      throw new IllegalArgumentException("'iv' must be "+ BLOCK_SIZE +" bit");
    }
    this.iv = new IvParameterSpec(keyEncoer(iv)
  }
  // 암호화
  public String encrypt(String plain) {
    Cipher c = Cipher.getInstance(algorithm);
    c.init(Cipher.ENCRYPT_MODE, this.secretKey, this.iv);
    byte[] encrypted = c.doFinal(plain.getBytes(charset));
    return encoder.encode(encrypted);
  }
  // 복호화
  public String decrypt(String cipher) {
    Cipher c = Cipher.getInstance(algorithm);
    c.init(Cipher.DECRYPT_MODE, this.secureKey, this.iv);
    byte[] encrypted = encoder.decode(cipher);
    return new String(c.doFinal(encrypted), charset);
  }
}

사용 예시

public static Example {
  // 암호화 키
  static final String key = "12345678901234567890123456789012";
  // 초기화 벡터
  static final String iv = "1234567890123456";

  public static void main(String[] args) {
    // 암호화 객체 생성
    Crypto aes256 = new AES256Crypto(key, iv);

    String plain = "1234 가나다라 !@#$"
    // 암호화
    String cipher = aes256.encode(plain);

    // 출력
    System.out.println("plain = " + plain);
    System.out.println("cipher = " + cipher);

    // 복호화
    String plain2 = aes256.decode(cipher);
    System.out.println("plain2 = " + plain2);

    // 검증
    assert plain.equals(plain2);
  }
}

결론

다음과 같은 스팩을 바탕으로 암호화 방식을 공유하자

  • 암호화 알고리즘 : AES
    • key size : 256 bit
    • block size : 128 bit (AES의 블록사이즈는 128로 고정입니다. 고로 block size는 skip해도 됩니다.)
  • 암호화 모드 : CBC
  • Padding 방식 : PKCS5
  • 암호화키 : 12345678901234567890123456789012 (32자)
    • 초기화벡터 - CBC모드일 경우 : 1234567890123456 (16자)
  • 암호문 인코딩 방식 : Base64(바이트인코딩) + UTF-8(문자인코딩)
  • 키 인코딩 방식 - 암호문 인코딩과 다른경우 : ASCII (문자인코딩, 바이트인코딩 둘 다 가능)

본 포스트는 도메인 주도 설계 라는 책을 참조하였습니다. 가능하면 본 책을 직접 보시고 DDD를 이해하는 것을 추천드립니다. http://www.aladin.co.kr/shop/wproduct.aspx?ItemId=12174216

  • 모델과 구현은 상세 수준에서 연결돼야 한다.
  • 객체 간 연관관계(association)를 설계하고, 합리적으로 구성
  • 하지만 실제로 구현 하는 것은 생각보다 힘든 일이다.

Entity, Value Object, Service, Module

Entity : 연속성(continuity)과 식별성(identity)을 가지는 객체 - 유일성

Value Object : 단순 값(상태를 기술하는 속성에 불과)을 가지는 객체

Service : 행동(action)이나 연산(operation)으로 명확하게 표현되는 객체

  • 상태를 주고 받지 않는 활동 을 모델링
  • 하지만 결국에는 중요 행위는 Entity가 담당해야하며, Service는 위임에 가깝다.

Module : 모델의 한 부분. 고로 모델처럼 도메인의 개념을 반영

연관관계

모델 내의 모든 탐색 가능한(trabersable) 연관관계에 대해 그것과 동일한 특성을 지닌 메커니즘이 소프트웨어에도 있다. ?? 정확한 이해가 안됨

연관관계를 쉽게 다루는 세가지 방법

탐색 방향을 부여한다.

  • 양방향 탐색에 비해 상호 의존성이 줄어들어 설계가 단순해 진다.
  • 추가적으로 도메인의 본질적인 방향성이 드러날 수도 있다.

양방향 상태

  • 미국의 대통령은 조지 워싱턴이다. 조지 워싱턴은 미국의 대통령이다.

단방향 정리

  • 미국의 대통령은 누구입니까? : 적합한 방향성
  • 조지 워싱턴이 대통령이었던 나라는 어디입니까? : 어색한 방향성

어떤 탐색 방향은 도메인의 본연적인 특성을 반영하기도 한다.

한정자(qualifier)를 추가해서 사실상 다중성(multiplicity)을 줄인다.

  • 한정자를 통해서 1:N 과 같은 다중성이 1:1 관계로 줄일 수 있다. - 제약

미국(1)의 대통령은 여러 명(N)이다. 하지만 1790년(한정자)에 미국(1)의 대통령은 조지 워싱턴(1)이다.

제약이 더해진 연관관계는 더 많은 지식과 실제적인 설계를 전해준다.

중요하지 않은 연관관계를 제거한다.

  • 양방향에서 단방향으로 한가지 연관관계 제거
  • M:N > 1:N > 1:1 으로 연관관계 단순화

도메인의 특성을 찾아내서 연관관계를 일관되게 제약(한정자를 통해서)하면 의사전달력이 풍부해지고 구현이 단순해지며, 나머지 양방향 연관관계도 의미를 지니게 된다. 또한 문제가 되지 않거나 중요한 모델 객체가 아니라면 궁극적인 단순화는 연관관계를 완전히 제거하는 것이다.

Entity

객체는 속성이 아닌 연속성과 식별성이 이어지느냐를 기준으로 정의된다 - 유일성

속성은 객체를 상태일 뿐이다.

어떤 객체를 일차적으로 해당 객체의 식별성으로 정의할 경우 그 객체를 Entity라 한다. Entity는 자신의 생명주기 동안 형태와 내용이 급격하게 바뀔 수도 있지만 연속성은 유지해야 한다. 식별성을 기반으로 Entity를 추적(CRUD)한다.

Entity의 클래스 정의와 책임, 속성, 연관관계는 Entity에 포함된 특정 속성보다는 Entity의 정체성에 초점을 맞춰야한다. 의미에 따라 Entity를 분류한다면 모델이 더욱 투명해지고 구현은 견고해질 것이다.

예를 들면 사람, 도시, 자동차, 티켓, 은행거래와 같은 것이 Entity가 될 수 있다. 역으로 모델 내의 모든 객체가 Entity는 아니다.

Entity의 식별성과 언어상의 동일성(자바기준 == 연산자)을 연결시키지 말자. 하지만 최근의 ORM 기술은 Entity의 식별성을 언어에서 제공하는 동일성으로 커버할 수 있게 되었다 !!!

Entity의 생명주기의 연속성과 식별성에 집중하고, 클래스의 정의를 단순하게 한다. 객체의 속성으로 동일성을 판단하는 것은 매우 위험하다. 객체의 유일한 결과를 반환하는 즉 유일성을 나타낼 수 있게 하는 연산을 정의하라.

위에서 제공하는 유일성 판단방법은 모델에서 식별셩을 구분하는 방법과 일치해야한다. 모델은 동일하다는 것이 무슨 의미인지 정의해야 한다.

ORM 기술을 이용하면 위 Entity와 관련된 기술적 요구사항을 대부분 만족시킬 수 있다.

  • 생명주기와 연속성 : EntityManager, 영속화 Context
  • 식별성 : @Id, ==,

예를 들면 경기장 좌석 예매 시스템에서 지정좌석(좌석번호)과 참석자(아이디)는 Entity로 다룰 수 있다. 지정좌석의 경우 예매 후 경기가 끝날 때까지 유지되어야 한다.

좌석번호가 없는 자유석의 경우에는 식별자가 존재하지 않으므로 Entity로 다룰 수 없다.

Entity 모델링

  • 속성 ?
  • 행위 ?
  1. 본질적인 특징
  2. (개념에) 필수적인 행위만
  3. 행위에 필요한 속성만
  4. 연관관계
    • 다른 Entity나 Value Object
![그림 5-5 식별성과 연관관계에 있는 속성은 Entity에 그대로 남는다](/images/2015/10/ddd-5-5.png)

식별 연산의 설계

식별성 : 동일성 판단

식별성의 정의는 도메인의 이해에서

  • 유니크 인덱스 ?
  • 복합키 ?
  • 자연키
  • 대리키 : ID기호

중요한 것은 유일성

자연키를 예를 들면

  • 주민등록번호
  • 전화번호

대리키를 예를 들면

  • 택배번호
  • 예약번호

Value Object

개념적 식별성이 없는 객체도 많은데, 이런 객체는 사물의 어떤 특징을 표현한다. - 사물을 서술하는 객체

예를들면

  • 쇼핑몰의 고객에게는 주소는 Value Object
  • 우체국 주소관리 체계에서 주소는 Entity

위의 예시 또한 도메인과 설계에 따라서 달라질 수 있다.

일반적인 값 객체도 Value Object

  • Integer
  • Color
  • Route : 경로
    • Route의 경우 도시라는 Entity에 연관관계를 가지고 있지만 Value Object 이다

어떤 요소의 속성에만 관심이 있으면 ValueObject이다.

Value Object는 불변(immutable)객체여야 한다. 또한 식별성을 부여하지 않는다. 즉 단순하게 설계한다.

Value Object의 설계

모든 값이 같다(동등성)고 해서 같은 객체(동일성)일 필요는 없다. - 불변식

다이어그램으로 예를들면

불변식 샘플

모든 값이 같다고 해서 같은 Value Object를 참조해서는 안된다. Value Object 공유로 인해서 예상치 못한 속성 변화가 생길 수 있다.

모든 값이 같다고 다른 곳에서 참조하면 불변식이 위배되므로, (Entity 별로) 다른 Value Object(새로운 레퍼런스)를 참조해야 한다.

Value Object를 포함한 연관관계 설계

불변식은 필수다

Value Object 간 양방향 연관관계는 완전히 제거하도록 노력해야한다.

DDDQ(Domain Driven Design Quickly) - 도메인 주도 설계란 무엇인가? 라는 책의 간단 요약 정리

컨텍스트 기반 다양한 협업 상황에서의 모델 관리

모델 내부의 일관성. 즉 통일성(unification)을 유지해야함

  • 하지만 현실적으로 엔터프라이즈 어플리케이션을 하나의 모델로 통합하는 것은 거의 불가능
  • 고로. 의도적으로 여러 개의 모델로 분할 관리 (각 조직별로)
  • 아래 그림과 같은 패턴으로 모델의 무결성을 유지하도록 노력해야한다.

context map

이미지출처 : https://commons.wikimedia.org/wiki/File:Maintaining_Model_Integrity.png

분할된 컨텍스트

적절한 모델과 적절한 컨텍스트 범위를 정해서 분할 관리한다 일반적으로 하나의 모델은 하나의 팀에 할당하는게 좋다 모델의 범위를 정하고 컨텍스트 간의 경계를 확실하게 설정한 다음 모델의 통합 상태를 유지해야 한다.

분할된 컨텍스트는 발전하는 모델에 담길 논리적인 프레임 - 모듈이 아님

예시

  • 전자상거래 어플리케이션
  • 리포팅 어플리케이션

각각으로 분리된 모델을 만들고 독립적인 어플리케이션으로 구축한다. 둘 사이의 인터페이스만 확실하면 문제되지 않는다.

지속적인 통합

아무리 컨텍스트를 분리한다고 할지라도 통합은 필수적으로 필요하다. 빌드(테스트)자동화 등을 이용해서 연관된 컨텍스트 간 인터페이스에 대해서 상호간 테스트와 통합을 지속 해야한다. 작업 편의를 위해 컨텍스트로 나누지만 결국은 하나의 모델이기 때문이다

컨텍스트 맵

분할된 컨텍스트와 그들 간 관계를 표현한 문서(다이어그램)

공유커널

컨텍스트 간 교집합(공유지점)

재사용성을 위한 중복코드 관리지점

상호 컨텍스트 담당 간 공유와 테스트가 필수적으로 요구됨

고객-공급자(customer-supplier)

한 컨텍스트(고객)가 다른 하나의 컨텍스트(공급자)에 완전히 의존하는 관계

상호 컨텍스트 간 인터페이스는 정교하게 정의해야하고, 강력한 테스트케이스가 존재해야함(안전장치)

순응

고객이 공급자에 완벽하게 순응??

변질 방지 레이어

컨텍스트 간 상호 작용 시 다른 도메인과 언어 간 번역 제공

분할 방식

오픈 호스트 서비스

openapi 방식

증류

핵심개념을 분리하고 그것에 집중

정제와 지속적인 리펙토링을 통해서 핵심도메인을 한층 더 명확하게 한다.

서브도메인(증류부산물) 구현 방법

  • 상용솔루션 : 의존성 이슈 (버그픽스나 업데이트 시)
  • 아웃소싱 : 통합 시 불편함
  • 기존모델 : 손쉬운 해결안
  • 사내개발 : 통합은 편하지만 유지보수 비용 발생

DDDQ(Domain Driven Design Quickly) - 도메인 주도 설계란 무엇인가? 라는 책의 간단 요약 정리

지속적인 리팩터링

설계와 구현사이 피드백을 통한 코드 고도화

코드 리펙터링

  • 애플리케이션의 기능에 변화를 주지 않고 코드를 더 좋게 만들기 위해 재설계하는 절차
  • 많은 방법론과 패턴이 존재한다.

DDD 리펙터링

프로젝트 진행 중에 도메인에 대한 새로운 통찰이 생기고 어떤 것들은 점점 명확해지며, 둘간의 관계가 발견되기도 한다. 이러한 것은 모두 리펙터링을 통해 설계에 반영되어야 한다.

  • 위와 같은 깊은 통찰 을 위한 리펙터링은 패턴이 존재하지 않는다.
  • 모델링 : 비즈니스 명세서를 읽고 명사와 동사를 찾는 것이다. 명사는 클래스로 동사는 메소드로 변환된다.
  • 하지만 위와 같은 기본적인 모델링 은 아주 심한 단순화이며, 여기에서 점점 깊은 통찰을 가지도록 개선(refactor) 해야 한다.
  • 유연한 설계가 바탕이 되어야한다. 유연하지 못하면 개선 에 따른 비용이 커진다
  • 가능한 작은 단위로 나누어서 진행해야 한다.

정교한 도메인 모델은 도메인 전문가와 개발자들이 밀접하게 엮인 조직이 반복적으로 리팩터링을 수행하지 않는다면 만들어질 수 없다.

핵심 개념 드러내기

리펙터링은 작은 단계로 나누어 진행되며, 그 결과 작은 개선의 연속으로 나타난다. 그런데 소규모의 변경이 큰 차이를 초래하는 경우가 있다. 바로 이것이 도약(Breakthrough) 이다.

어떤 암시적 개념이 핵심개념이며, 이것을 명시적 개념으로 모델링하게되면 도약의 기회가 생긴다.

암시적개념 : 도메인 전문가와 이야기할 때 외부로 노출되지 않은 것

암시적 개념을 발견하는 방법

  • 언어를 주의 깊게 듣기
  • 설계 영역에서 분명하지 않는 부분을 주의 깊게 들여다 보기
  • 지식체계(모델링 관계 설정 등)를 만들 때 모순에 부딪칠 경우
  • 해당 도메인의 문헌을 활용 - 기존예시
(암시적인 것을) 명시적인 개념으로 만들어 낼 때 유용한 추가개념

제약조건(constraint)

  • 불변식(invariant)?를 표현하는 간단한 방법

예제

as-is

public class Bookshelf {
  private int capacity = 20;
  private Collection content;
  public void add(Book book) {
    if(content.size() + 1 < = capacity) {
      content.add(book);
    } else {
      throw new IllegalOperationException(
        "The bookshelf has reached its limit.");
      )
    }
  }
}

제약사항 메소드 분리를 통한 리팩토링 후

public class Bookshelf {
  private int capacity = 20;
  private Collection content;
  public void add(Book book) {
    if(isSpaceAvailable()) {
      content.add(book);
    } else {
      throw new IllegalOperationException(
        "The bookshelf has reached its limit.");
      )
    }
  }
  // 제약사항 메소드 분리
  private boolean isSpaceAvailable() {
    return content.size() < capacity;
  }
}

처리(process)

  • 일반적으로 처리는 절차적인 코드로 표현된다.
  • OOP를 사용하므로 절차적 접근은 허용하지 않는다.
  • 고로, 처리를 구현하는 최고의 방법은 서비스(Service)를 이용하는 것

명세(specification)

  • 객체가 특정 기준을 만족하는지 여부를 확인
  • 명세 패턴을 참조
    • 추가 블로그 자료 : http://vandbt.tistory.com/4