Spring 의 새로운 클래스

오늘 이야기 할 내용은 Spring과 Spring boot의 새로운 클래스에 대해서 알아보려고 한다.  제목은 Spring 이라고 했지만 Spring boot 도 포함되어 있다. 대략 4가지 정도 클래스를 알아볼 예정인데 각 설명마다 프로젝트와 버전을 명시하겠다. 

보다 많은 클래스가 추가 되었지만 다 알아 볼 수도 없고 또 한 클래스들이 디펜더시도 있어 유틸성의 클래스들만 알아보도록 하자.

DataSize

DataSize 클래스는 Spring 5.1에 포함될 예정이다. 아직 릴리즈는 되지 않았지만 util 클래스라 크게 바뀌지 않을 듯으로 보인다. 한번 알아보도록 하자.

DataSize dataSize = DataSize.of(10, DataUnit.TERABYTES);
System.out.println(dataSize);
System.out.println(dataSize.toGigaBytes());

아주 간단하다. 특정한 DataSize를 작성하면 그에 맞게 원하는 데이터 형식으로 바꿔준다.  위의 코드는 10테라 바이트를  기가 바이트로 바꾸어 리턴해주는 코드이다. 실제로 내부적으로는  long 형태의 byte 로 저장하고 있다. 지금 현제 지원해주는 단위는 BYTES, KILOBYTES, MEGABYTES, GIGABYTES, TERABYTES 를 지원해주 고 있다. 뭐 아직까지는 테라바이트 이상으로는 필요 없을 듯으로 판단하여 그런듯 하다. 

위의 예제는 of 메서드를 이용해서 생성했지만 굳이 그럴 필요 없이 ofXXX 메서드를 이용해서 사용하면 더욱 편리하다.

DataSize bytes = DataSize.ofBytes(100);
DataSize kiloBytes = DataSize.ofKiloBytes(100);
DataSize megaBytes = DataSize.ofMegaBytes(100);
DataSize gigaBytes = DataSize.ofGigaBytes(100);
DataSize teraBytes = DataSize.ofTeraBytes(100);

좀 더 간편하게 사용할 수 있을 듯하다. 이것은 Spring에서 지원해주는 클래스지만 Spring boot 2.1에서도 직접적으로 사용할 수 있다.

@ConfigurationProperties("foo")
public class DataSizeProperties {

    private DataSize tempSize;

    public DataSize getTempSize() {
        return tempSize;
    }

    public void setTempSize(DataSize tempSize) {
        this.tempSize = tempSize;
    }
}

우리가 자주 사용하는 @ConfigurationProperties 어노테이션에서도  DataSize 클래스를 사용해서 매핑할 수 있다. 

사용법은 아래와 같이 간단하다.

foo.temp-size=10
foo.temp-size=10MB
foo.temp-size=10GB
...

기본적으로 아무 단위가 없다면 Byte로 설정된다. 만약 원하는 단위가 있다면 위 처럼 해당 단위를 작성해주면 된다. 하지만 여기서 주의할 점은 모두 대문자를 이용해야 한다는 것이다.  왜소문자는 파싱을 안되게 했을까? 흠흠 Enum 타입에 있는 문자로 결정하는 것 같은데 자세히는 살펴보지 않았다. 아무튼 그렇다. 

만약 해당 타입을 기본적인 단위로 지정해주고 싶다면 다음과 같이작성해도 무방하다.

@ConfigurationProperties("foo")
public class DataSizeProperties {

    @DataSizeUnit(DataUnit.GIGABYTES)
    private DataSize tempSize;

    public DataSize getTempSize() {
        return tempSize;
    }

    public void setTempSize(DataSize tempSize) {
        this.tempSize = tempSize;
    }
}

위와 같이 @DataSizeUnit 어노테이션을 이용해서 해당 단위를 기본적으로 설정 할 수 있다. 그러면 프로퍼티에 굳이 해당 단위를 명시해주지 않아도 된다. 만약 명시해준다면  기본타입은 무시가 되고 작성한 타입으로 동작된다.

AnnotatedClassFinder

이 클래스는 Spring boot 2.1 에 새롭게 나타난 클래스이다. 그렇다고 해서 완전하게 새로운 클래스는 아니다.(?) 기존의 있던 SpringBootConfigurationFinder 클래스를 살짝 변경한? 클래스이다. 기존에는 @SpringBootConfiguration 어노테이션만을 파싱하기 위한 클래스라면 AnnotatedClassFinder  클래스 경우에는 원하는 어노테이션을 파싱할 수 있다. 

SpringBootConfigurationFinder() {
    this.scanner = new ClassPathScanningCandidateComponentProvider(false);
    this.scanner.addIncludeFilter(
            new AnnotationTypeFilter(SpringBootConfiguration.class));
    this.scanner.setResourcePattern("*.class");
}

위의 코드는 기존의 클래스이며 아래의 코드는 새롭게 탄생한 코드이다.

public AnnotatedClassFinder(Class<? extends Annotation> annotationType) {
    Assert.notNull(annotationType, "AnnotationType must not be null");
    this.annotationType = annotationType;
    this.scanner = new ClassPathScanningCandidateComponentProvider(false);
    this.scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
    this.scanner.setResourcePattern("*.class");
}

차이라곤 뭐 생성자에 어노테이션을 받는 부분이 추가 된 것 뿐이다.  

사용법은 해당 클래스를 보면 쉽게 알 것 같다.  그래서 생략 하겠다..

PropertyMapper

PropertyMapper 클래스는 Spring boot 2.0 에서 추가된 클래스이다.
이 클래스의 용도는 프로퍼티들을 매핑 시켜주는? 그런 역할을 하는 클래스이다. 예를들어 Spring boot 의 @ConfigurationProperties 를 사용해서 프로퍼티들을 매핑 시켰다면 그 이후에 그 프로퍼티들을 실제 사용하는 프로퍼티에 매핑을 시켜주는 그런? 클래스이다. 흠 말로는 어려우니 코드를 살펴보도록 하자.

class FooProperties {
    private int timeout;
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    public int getTimeout() {
        return timeout;
    }
}

class FooTemplate {
    private int timeout;
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    public void bar() {
        //...
        //this.timeout
    }
}

예를 들어 위와 같은 클래스가 있다고 가정하자. FooProperties 클래스는 클라이언트로부터 프로퍼티를 받는 클래스이고 FooTemplate 은 실제 그 프로퍼티로 어떤 작업등을 하는 클래스라고 가정해보자. 그럼 다음과 같이 사용할 수 있다.

PropertyMapper map = PropertyMapper.get();
FooProperties properties = new FooProperties();
properties.setTimeout(10);
FooTemplate fooTemplate = new FooTemplate();
map.from(properties::getTimeout).to(fooTemplate::setTimeout);

위와 같이 FooProperties 클래스로 부터 받은 프로퍼티를 FooTemplate 의 속성으로 넣는 작업이다. 실제로 FooTemplate 의 timeout 속성에는 10이라는 값이 들어가 있다. 

또한 좀 더 나은 방식도 제공해준다. 예를들어 source 의 프로퍼티가 null일 경우에 굳이 target 프로퍼티에게 null을 넣을 필요가 없다면 다음과 같이 작성하면 된다.

map.from(properties::getTimeout).whenNonNull().to(fooTemplate::setTimeout);

그러면 fooTemplate 클래스에 timeout은 영향 받지 않는다. 또한 간단하게 타입도 변경할 수 있다. 예를들어  FooTemplate의 timeout의 속성이 String이라면 다음과 같이 타입도 변경할 수 있다.

map.from(properties::getTimeout).as(String::valueOf).to(fooTemplate::setTimeout);

필자가 말한 위의 메서드뿐만아니라 많은 메서드가 존재하니 필요하다면 한번씩 살펴보는 것도 나쁘지 않다.

TestPropertyValues

이 클래스는 Spring boot 2.0에 새로생긴 클래스 이다. 하지만 우리가 딱히 직접적으로 사용할 클래스는 아니다.  기존의 존재했던 클래스 (EnvironmentTestUtils) 클래스가 Deprecated 되고 해당 클래스가 생겼다.

예전에 Test 할 때 주로 EnvironmentTestUtils.addEnvironment 메서드를 사용했지만 이제는 TestPropertyValues를 사용하면 되는데 해당 클래스를 직접적으로 사용할 일은 없다. 뭐 있을 수는 있겠지만 딱히 커스텀하게 만들지 않는 이상은..

왜냐하면 기존의 Config를 테스트 하던 클래스가 Spring boot 2.0 부터 새롭게 바뀌면서 ApplicationContextRunner 를 이용하면 되기 때문이다. 

예전에 RC 버전일때 포스팅한 내용은 여기있다. 궁금하다면 참고하면 되겠다.

여기에 존재하는 withPropertyValues 가 내부적으로 바로 TestPropertyValues 이다. 하지만 우리는 그냥 String 타입으로 넘겨서 알아채지 못했을 수도 있다.

private final TestPropertyValues environmentProperties;

//...

public SELF withPropertyValues(String... pairs) {
    return newInstance(this.contextFactory, this.initializers,
            this.environmentProperties.and(pairs), this.systemProperties,
            this.classLoader, this.parent, this.configurations);
}

딱히 사용할일은 없지만 그래도 이런게 있다고는 알아봤다. 오늘 내용은 Spring 의 새로운 클래스들을 몇개 알아봤다. 물론 다 알아 보고 싶지만 그러지 못한다. 깊은 내용의 클래스들도 있고 뭐가 뭔지 모르는 클래스도 있고 아직 확인 못한 클래스도 있으니.. 오늘은 여기까지만 알아보도록 하자. 추후에 더 알아 볼 수 있으면 알아보도록 하자. 

유틸성 클래스들이라 해당 클래스 직접사용해봐도 쉽게 이해할 수 있을 것으로 판단 된다. 

그럼 오늘은 이만!

개방 폐쇄 원칙

오늘은 저번시간에 이어 개방 폐쇄 원칙(Open-clased principle) 대해서 알아보도록 하자. 예제를 고민하느라 책도 많이 참고 했고 인터넷 정보도 많이 활용했다. 계속 이 원칙들을 살펴봐야 되는데 중간중간에 다른 포스팅을 하느라 저번 포스팅이 어디있는지도 모르겠다. 오늘도 역시 단일책임원칙은 못한다.

개방 폐쇄 원칙 (OCP)

조금 모순이 있는 단어같다. 개방과 폐쇄가 공존한다. 하지만 여기에는 깊은 뜻이다. 일단 개방 폐쇄 원칙이 무엇인지 살펴보자. 개방 폐쇄 원칙이란 (OCP) 확장에는 열려 있어야 하고, 변경에는 닫혀있어야 한다. 라고 정의 되어있다. 흠.. 무슨 이런 어이 없는 말을 할까하는데 좀 더 풀어서 이야기 하면 다음과 같다.
기능을 변경 또는 확장할 수 있으면서 그 기능을 사용하는 코드는 수정하지 않아야 한다. 라고 하면 좀 더 와닿을까? 근데 그게 가능할까나 어떻게 변경하거나 확장하면서 그를 사용하는 코드는 수정하지 않아야 한다니 말이다. 먼저 글(의존 역전의 원칙) 을 읽었다면 가능해 보인다. 왜나면 의존역전의 원칙을 잘 지키면 개방 폐쇄 원칙도 알아서 잘 지켜지니 말이다.

일단 우리는 어떤 파일을 읽어와 엑셀로 다운로드 한다는 기능이 있다고 가정하자. 그걸 코드로 작성해보면 다음과 같다.

public class ExcelController {

    private final FileByteReader fileByteReader = new FileByteReader();

    public void download() {
        byte[] read = fileByteReader.read();

        //blabla
    }
}

public class FileByteReader {
    public byte[] read() {
        //파일을 읽는다.
        return new byte[0];
    }
}

아주 간단한 코드를 작성했다. ExcelController는 다운로드 하는 부분의 로직이 들어가면 되고 FileByteReader 클래스는 파일을 읽어와 byte 배열로 리턴해주는 코드가 들어가면 되겠다. ExcelController 클래스는 FileByteReader 클래스를 사용하고 있다. 그러다 어느날 Database에서 값을 꺼내 다운로드 하라는 요청이 들어왔다. 그럼 우리는 다음과 같이 변경해야 한다.

public class ExcelController {

  private final DataBaseByteReader dataBaseByteReader = new DataBaseByteReader();

  public void download() {
    byte[] read = dataBaseByteReader.read();

    //blabla
  }
}

public class DataBaseByteReader {
  public byte[] read() {
    //데이터 베이스에서 값을 가져온다.
    return new byte[0];
  }
}

우리는 DataBaseByteReader 클래스를 생성하고 다시 ExcelController에서 사용되었던 FileByteReaderDataBaseByteReader 클래스로 변경해야 한다. 요구사항이 변경될 때 마다 계속 ExcelController 클래스를 변경해야 한다. 이것은 개방 폐쇄 원칙을 지키지 않아 일어난 일이다. 눈치 빠른 사람들은 눈치 챗겟지만 인터페이스를 이용하면 좀 더 확장성 있게 되지 않을까 생각 한다. 그럼 인터페이스를 만들어보자.

public interface ByteReader {
  byte[] read();
}

public class DataBaseByteReader implements ByteReader {
  @Override
  public byte[] read() {
    //데이터 베이스에서 값을 가져온다
    return new byte[0];
  }
}

public class FileByteReader implements ByteReader {
  @Override
  public byte[] read() {
    //파일을 읽는다.
    return new byte[0];
  }
}

ByteReader 라는 인터페이스에 read() 라는 메서드를 만들고 구현은 해당 클래스의 맞게 개발하면 된다. 그럼 한번 사용해보자.

public class ExcelController {

  private final ByteReader byteReader = new DataBaseByteReader();

  public void download() {
    byte[] read = byteReader.read();

    //blabla
  }
}

인터페이스를 이용하여 DataBaseByteReader 클래스를 생성하였다. 하지만 아직까지 FileByteReader 클래스로 변경해야 된다면 ExcelController의 클래스를 변경해야 한다. 아직까지 완벽한 개방폐쇄가 되지 않았다. 엇 이건 저번에 봤던 의존역전원칙과 비슷한 구석이 많다고 생각 들 수 있다. 맞다. 저번에도 말했지만 의존역전원칙을 잘 지키면 개방폐쇄원칙도 자연스레 지키게 된다.

다시 코드를 변경해 보자.

public class ExcelController {

  private final ByteReader byteReader;

  public ExcelController(ByteReader byteReader) {
    this.byteReader = byteReader;
  }

  public void download() {
    byte[] read = byteReader.read();

    //blabla
  }
}

ByteReader 인터페이스는 사용하는 클라이언트에서 결정하면 된다. 만약 또 다시 요구사항이 추가 되었다고 생각해보자. 이번에는 DataBase에서 값을 가져오는 것이 아니고 타 API를 이용해서 가져와야 한다고 가정해보자.

public class RequestByteReader implements ByteReader {

  @Override
  public byte[] read() {
    //api 호출
    return new byte[0];
  }
} 

API를 통해 값을 가져와 Byte 배열로 가져오는 RequestByteReader 클래스를 만들었다. 그리고 ExcelController를 변경하려고 보니까 그럴 필요가 없어졌다. 왜냐하면 ExcelController 클래스는 직접적으로 어떤 클래스를 사용한게 아니라 의존성 주입을 받고 있기에 ExcelController는 변경이 일어나지 않는다. 이로써 우리는 수정과 확장은 하였지만 코드는 변경하지 않은 개방폐쇄 원칙을 따르게 된 것이다.
개방폐쇄원칙은 꼭 인터페이스만 사용하라는 법은 없다. 상속을 통해서도 개방폐쇄원칙을 지킬 수 있다.

class HttpServlet {

  protected void doGet(Request request) {
    response.send(400);
  }
  protected void doPost(Request request) {
    response.send(400);
  }
  // etc..
  public void service(Request request) {
    if(method.equals("get")) {
      doGet(request);
    } else {
      doPost(request);
    }
  }
}

어떤 request는 받는 클래스가 있다고 생각해보자. service()라는 메서드는 요청을 받아서 각각의 상태에 맞게 doGet과 doPost로 전달하는 역할을 한다고 치자. 기본적으로 각각의 상태코드는 400을 리턴한다. 하지만 만약 계정 정보를 조회하는 요구사항이 들어왔다고 가정해보자. 그리고 계정정보의 조회의 상태코드는 get일 경우 에만 200의 상태코드를 리턴해야 한다고 하면 다음과 같이 할 수 있다.

class AccountServlet extends HttpServlet {

  @Override
  protected void doGet(Request request) {
    response.send(200);
  }
}

이렇게 HttpServlet 클래스는 변경하지 않고 확장해 나갔던 이유는 바로 개방폐쇄 원칙을 잘 지켜서 할 수 있는 일이 되었다. 가만 보면 위의 코드는 어느 디자인 패턴과 비슷해보인다. 맞다. 디자인 패턴 중 템플릿 메서드 패턴을 사용한 것이다. 템플릿 메서드 패턴은 상위 클래스에서 실행할 기본 코드를 만들고 하위 클래스에서 필요에 따라 확장해가는 패턴으로 아주 유용하게 쓰인다. 나중에 기회가 된다면 패턴들도 한번씩 살펴보기로 하자.
실제 위의 코드는 java servlet의 HttpServlet 클래스를 간단하게 구현해 본 것이다. HttpServlet 클래스는 템플릿 메서드 패턴으로 구현되어 있다.

이렇게 오늘은 객체지향의 5대 원칙 중 개방폐쇄의 원칙을 살펴봤다. 내용자체는 그리 어려운 말은 아니지만 실제로 구현하려면 많이 생각하고 구현해야 될 듯 싶다. 언제쯤 단일책임원칙의 예제가 생각날까.. 리스코프 치환원칙은 아주 유명한 예제가 있어서 그걸 기반으로 설명하면 될 듯 싶은데.. 단일 책임 원칙은.. 왜이렇게 생각이 나지 않을까

일단 아마도 다음시간에는 리스코프 치환원칙을 살펴볼 듯 싶다. 그럼 오늘은 이만.