반응형

1. 서론

이번 포스팅에서는 2장인 스프링 부트로 마이크로서비스 구축에 대해 알아보도록 하겠습니다.

 

2. 아키텍트의 이야기: 마이크로서비스 아키텍처 설계

 

마이크로서비스 아키텍처를 구축할 때 프로젝트의 아키텍트는 아래 세 가지 일에 집중해야 합니다.

 

  1. 비즈니스 문제의 분해
  2. 서비스 세분화의 확정
  3. 서비스 인터페이스의 정의

 

1) 비즈니스 문제의 분해

 

마이크로 서비스 아키텍트는 비즈니스 문제를 각 영역을 대표하는 덩이로 분해하고, 영역의 특정 부분과 연관된 비즈니스 규칙과 데이터 로직을 이 덩이들 안에 캡슐화해야합니다.

 

분해하는것은 말처럼 쉬운일이 아닙니다.

하지만, 아래와 같은 지침을 사용한다면 분해하는데에 있어 도움이 될 수 있습니다.

 

  1. 비즈니스 문제를 기술하고 그 문제를 기술하는 데 사용된 명사에 주목 : 문제 기술에 자주 반복되는 명사로 논리적인 영역이 정해집니다.
  2. 동사에 주목 : 문제 기술의 동사를 본다면 행위를 명확히 알 수 있습니다.
  3. 데이터 응집성을 찾아라 : 서로 연관성이 높은 데이터 부분들을 찾아 마이크로서비스가 자기 데이터를 완전히 소유하도록 해야 합니다.

 

아래는 EagleEye의 모놀리식 서비스의 그림입니다.

 

 

이를 분해를 한다면 아래와 같은 데이터 모델이 나옵니다.

 

 

 

 

2) 서비스 세분화의 확정

 

위 데이터 모델을 본다면 조직, 계약, 자산, 라이선스로 4개의 마이크로서비스가 나올것을 예상할 수 있습니다.

 

그렇다면, 위 4개의 서비스는 서로 독립적으로 빌드하고 배포할 수 있어야 합니다.

 

하지만, 데이터 모델에서 서비스를 추출하는 일은 쉬운일이 아닙니다.

이유는, 서비스가 접근하는 실제 데이터베이스 테이블을 서비스에 따라 정리하고 각 서비스가 특정 도메인의 테이블만 액세스하도록 하는 등의 부가적인 일들이 동반되기 때문입니다.

 

아래는 위 데이터 모델을 기반으로 마이크로서비스로 분해했을때의 전체적인 그림입니다.

 

 

 

마이크로 서비스 아키텍처를 구축 시 세분화는 아래와 같은 개념을 이용할 수 있습니다.

 

  1. 큰 마이크로서비스에서 시작해 작게 리팩토링
  2. 서비스간 교류하는 방식에 먼저 집중
  3. 문제 영역에 대한 이해가 깊어짐에 따라 서비스 책임도 계속 변함.

 

3) 서비스 인터페이스의 정의

 

서비스 인터페이스는 마이크로 서비스가 대화하는 방식을 정의하는 것입니다.

 

서비스 인터페이스를 설계할땐, 아래와 같은 지침을 사용할 수 있습니다.

 

  1. REST 철학을 수용
  2. URI를 사용해 의도를 전달
  3. 요청과 응답에 JSON을 사용
  4. HTTP 상태코드 결과를 전달

 

 

반응형

 

 

3. 마이크로서비스를 사용하지 않아야 할 때

 

마이크로서비스가 만능은 아닙니다. 오히려 마이크로서비스를 적용하는것이 독이 될때가 있습니다.

 

아래는 마이크로서비스를 적용하지 않아야하는 할 때 입니다.

 

  1. 분산 시스템 구축의 복잡성
  2. 가상 서버/컨테이너의 스프롤
  3. 어플리케이션 유형
  4. 데이터 변환과 일관성

 

1) 분산 시스템 구축의 복잡성

 

마이크로서비스는 모놀리식에 비해 복잡성이 증가하게 됩니다.

 

때문에, 마이크로서비스에서 필요한 자동화와 운영작업에 투자할 의사가 없는 조직이라면 적용하지 않는데 낫습니다.

 

 

2) 가상 서버 / 컨테이너의 스프롤

 

마이크로 서비스는 클라우드 조합으로 많이 사용합니다.

 

클라우드에서 서비스들을 실행하는데 드는 비용은 저렴하지만 서버를 관리하고 모니터링하는 운영작업은 엄청나게 복잡해질수 있습니다.

 

 

3) 어플리케이션 유형

 

소수 사용자를 위한 어플리케이션을 개발할 때 마이크로서비스와 같은 분산 모델로 구축하는것은 더욱 복잡성을 증대시키며, 오버 스펙입니다.

 

 

4) 데이터 변환과 일관성

 

마이크로서비스 환경에서는 데이터를 변환하고 취합하는 작업이 분산환경으로 인해 어려울 수 있습니다.

 

또한, 각 서비스는 분리가 되어있어 업데이트한 데이터가 즉시 나타나지 않을 수 도 있습니다.

 

4. 개발자 이야기: 스프링 부트와 자바로 마이크로서비스 생성

마이크로서비스에서 구현단계에서는 일관된 방식으로 코드가 배치되도록 하는 것이 중요합니다.

 

아래는 간단히 위 EagleEye의 라이선스 서비스의 골격인 프로젝트 예제입니다.

 

https://github.com/klimtever/spmia-chapter2

 

5. 데브옵스 이야기: 혹독한 런타임 구축

 

마이크로서비스는 데브옵스 관점에서 관리할 프로젝트들이 늘어 힘들어 집니다.

 

때문에, 데브옵스 관점에서 아래 4가지 원칙을 사용하여 빌드 배포에 대해서 일반화를 시켜야합니다.

 

  1. 서비스 어셈블리
  2. 서비스 부트스트래핑
  3. 서비스 등록 및 디스커버리
  4. 서비스 모니터링

 

1) 서비스 어셈블리

 

서비스 어셈블리란 일관된 구축, 패키징 및 배포하는 과정을 의미합니다.

 

아래는 서비스 어셈블리를 도식화한 그림입니다.

 

 

 

기존에는 웹서버 사용시 외부 톰캣을 이용하기 때문에 스프링 버전과 톰캣버전등의 구성편차가 어쩔수 없이 존재했습니다.

하지만, 스프링 부트에서부터는 내장 톰캣이 있어 단일로 소스와 웹서버를 관리 및 배포가 가능해졌습니다.

 

 

2) 서비스 부트스트래핑

 

서비스 부트스트래핑은 마이크로서비스가 처음 가동할 때 시작하며 어플리케이션 구성 정보를 로드합니다.

 

아래는 서비스 부트스트래핑을 도식화한 그림입니다.

 

 

일반적으로, 구성 정보는 구조가 단순하며, 조회는 자주 있지만 변경은 자주 없는것이 특징입니다.

 

때문에, 구성 정보를 저장하기 위해 별도의 데이터베이스를 이용하는것은 오버스펙이 될 수 있습니다.

 

스프링 클라우드에서는 이를 위해 컨피그 서버라는 것을 제공하고 있습니다.

 

 

3) 서비스 등록 및 디스커버리

 

마이크로서비스는 위치 투명성을 가져야 합니다.

 

위치 투명성을 제공하기 위해서는 서비스 인스턴스들을 관리하고 자유롭게 추가 삭제가 되어야 합니다.

 

아래는 이러한 서비스 등록과 관리를 하는것을 도식화한 그림입니다.

 

 

그림에서 보는것과 같이 서비스 디스커버리 에이전트는 각 서비스 인스턴스들을 관리하며,

서비스 인스턴스는 시작 시 이 에이전트에게 자신을 등록시킵니다.

 

서비스 디스커버리는 URL 을 통해 서비스 인스턴스들의 그룹을 만들며, 클라이언트는 이 URL을 통해 서비스를 제공받을 수 있습니다.

 

 

4) 서비스 모니터링

 

서비스 인스턴스 중에 장애가 있는 것들이 있고, 클라이언트는 그로인해 응답을 받지 못할 수 있습니다.

때문에, 서비스 디스커버리는 각 서비스 인스턴스들을 모니터링 해야합니다.

 

아래는 서비스 모니터링을 도식화한 그림입니다.

 

 

그림에서 보는것처럼 서비스 디스커버리는 장애가 난 서비스 인스턴스를 자신의 라우팅 테이블에서 제거하여

클라이언트의 요청이 해당 인스턴스에 가지 않도록 합니다.

 

이러한 상태 관리 모니터링은 스프링에서 제공하는 스프링 액추에이터를 사용할 수 있습니다.

 

스프링 액추에이터는 기본적으로 /actuator URL 엔드포인트를 통해 상태를 확인 할 수 있습니다.

 

아래는 /actuator/health URL을 통해 위 4번 예제의 상태를 확인한 그림입니다.

 

 

6. 마무리

이번 포스팅에서는 스프링 부트로 마이크로서비스 구축에 대해 알아보았습니다.

다음에는 스프링 클라우드 컨피그 서버로 구성 관리에 대해 포스팅하겠습니다.

반응형
반응형

1. 서론

이번 포스팅에서는 스프링 마이크로서비스 코딩 공작소의 1장인 스프링, 클라우드와 만나다 에 대해 알아보도록 하겠습니다.

 

2. 마이크로 서비스란?

마이크로서비스 개념이 발전하기 전, 대부분의 프로젝트들은 모놀리식 아키텍처 형태였습니다.

 

모놀리식의 단점으로는 크고 복잡해질수록 프로젝트를 담당하는 각 팀의 의사소통과 조정 비용이 증가한다는 점이 있습니다.

 

이 단점을, 극복하기 위해 마이크로서비스라는 아키텍처 개념이 나왔고, 특징은 아래와 같습니다.

마이크로서비스는 느슨히 결합된 작은 분산 서비스라고 이해하시면 좋습니다.

 

  1. 어플리케이션 로직을 각자 작은 컴포넌트들로 분해하고 이들을 조합하여 사용
  2. 각 컴포넌트는 작은 책임 영역을 담당하고 완전히 상호 독립적으로 배포되며, 재사용이 가능.
  3. 각 컴포넌트간의 데이터 교환을 위해 HTTP와 JSON 같은 경량 통신 프로토콜을 사용.
  4. 마이크로서비스 기반의 어플리케이션을 다양한 언어와 기술로 구축 가능
  5. 명확히 정의된 책임 영역을 담당하는 조직 운영 가능

 

위 특징들로 인해 어플리케이션은 높은 확장성과 회복성을 얻을 수 있습니다.

 

3. 스프링은 무엇이고 마이크로서비스와 어떤 관련이 있을까?

스프링은 자바 객체간의 의존성 관리를 제공하는 프레임워크입니다.

 

스프링은 현재도 꾸준히 활발한 커뮤니티와 발전이 되고 있으며, 모놀리식에서 마이크로서비스라는 변화에도 맞춰

아래 2개의 프로젝트를 제공합니다.

 

  • 스프링 부트
  • 스프링 클라우드

 

이번 포스팅 책은 프로젝트를 마이크로서비스로 만들때, 위 스프링 부트와 스프링 클라우드를 기반으로 만드는 것을 소개합니다.

 

4. 스프링 부트로 마이크로서비스 구축

위에서 말씀드린것과 같이 스프링 부트와 스프링 클라우드를 기반으로 만들기 때문에 간단히 스프링 부트기반의 간단한 프로젝트를 예제로 살펴보겠습니다.

각자 ide를 통해 spring boot 프로젝트 생성까지 가능하다고 생각하고 진행하겠습니다.

 

예제는 간단하게 http 요청을 받아 문자열 응답을 주는 예제입니다.

 

먼저, spring boot 프로젝트를 생성하셨다면, 'org.springframework.boot:spring-boot-starter-web' 의존성을 추가합니다.

gradle 이라면 build,gradle 파일에 dependencies에 implementation 'org.springframework.boot:spring-boot-starter-web' 를 추가합니다.

 

아래와 같은 자바 클래스를 하나 만듭니다.

간단히, hello/{firstName}/{lastNam} URI로 엔드포인트를 하나 만들어 반환하는 어플리케이션입니다.

 

@SpringBootApplication
@RestController
@RequestMapping(value = "hello")
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @RequestMapping(value = "/{firstName}/{lastName}", method = RequestMethod.GET)
    public String hello(@PathVariable("firstName") String firstName, @PathVariable("lastName") String lastName) {
        return String.format("{\"message\":\"Hello %s %s\"}", firstName, lastName);
    }
}

 

  • @SpringBootApplication 은 스프링 부트 서비스의 진입점인 클래스에 지정합니다.
  • @RestController 는 엔드포인트를 만드는 클래스에 지정합니다.
  • @RequestMapping 은 http 요청 uri를 해당 클래스로 매핑하는 역할을 합니다.
  • @PathVariable 은 URL에 전달된 매개변수를 자바 타입으로 매핑해주는 역할을 합니다.

 

위 main을 수행하게되면, 8080 포트로 spring-boot-starter-web이 가지고 있는 내장 톰캣이 구동됩니다.

 

확인을 위해 browser를 열어 아래와 같이 URL을 입력하면 올바르게 응답이 오는것을 확인할 수 있습니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

5. 애플리케이션 구축 방식을 바꾸는 이유

요즈음 어플리케이션을 바라보는 방식이 아래 현상에 영향을 받고 있습니다.

 

  • 복잡성 증가: 어플리케이션은 여러 서비스와 데이터베이스 뿐만이 아닌 외부 서비스와도 통신이 필요합니다.
  • 고객은 더 빠른 출시를 원함
  • 성능 및 확장성 : 어플리케이션은 처리해야할 양에 따라 확장성있게 증가 및 감소해야합니다.
  • 고객은 어플리케이션을 항상 사용할 수 있길 기대: 어플리케이션은 항상 사용이 가능해야 하기 때문에 회복성이 높아야합니다.

 

위 현상으로 인해, 어플리케이션을 마이크로서비스로 구축 시 아래와 같은 장점을 취할 수 있습니다.

 

  1. 유연성 : 코드 단위가 작아져 변경에 대해서 복잡성이 낮아집니다.
  2. 회복성 : 여러 어플리케이션으로 운용되기 때문에 하나의 에러가 전체 어플리케이션에 영향이 가지 않습니다.
  3. 확장성 : 서비스들이 작아져 확장하기에 용이합니다.

 

때문에, 잘 만든 마이크로서비스 어플리케이션은 아래와 같이 정의할 수 있습니다.

 

마이크로 서비스는 확장가능하고 회복적이며 유연한 어플리케이션

 

6. 왜 클라우드와 마이크로서비스인가?

 

마이크로서비스 기반 아키텍처는 각 서비스를 독립된 개별 산출물로 패키징하고 배포함을 의미합니다.

 

때문에, 어플리케이션은 경량화되며 서로 독립적이고 이러한 장점을 극대화하기에는 클라우드와 찰떡궁합입니다.

 

왜냐하면, 클라우드를 통해 어플리케이션의 확장성이 용이해지고

경량화 된 어플리케이션이기 때문에 고성능의 물리적 서버까지는 필요가 없어졌기 때문입니다.

 

 

 

7. 마이크로 서비스는 코드 작성 이상을 의미

마이크로서비스 작성 시에는 아래와 같은 항목들을 고려해야합니다.

 

  1. 적정 크기 : 마이크로서비스가 과도한 책임을 맡지 않도록 적정한 크기로 만드는 방법
  2. 위치 투명성 : 서비스 클라이언트에 영향을 주지 않고 서비스 인스턴스를 추가/삭제할 수 있는 방법
  3. 회복성 : 서비스에 문제가 있을 때, 서비스 클라이언트는 '빨리 실패'하는 방법
  4. 반복성 : 새로운 서비스 인스턴스가 시작될 때마다 기존 인스턴스와 동일한 코드와 구성으로 유지할 수 있는 방법
  5. 확장성 : 서비스 간 의존성을 최소화하여 확장할 수 있는 방법

 

포스팅하는 책에서는 위 항목들을 아래와 같은 패턴 기반으로 접근하며, 사용하는 기술로는 스프링 부트와 스프링 클라우드입니다.

 

  • 핵심 개발 패턴
  • 라우팅 패턴
  • 클라이언트 회복성 패턴
  • 보안 패턴
  • 로깅 및 추적 패턴
  • 빌드 및 배포 패턴

 

1) 핵심 개발 패턴

 

핵심 개발 패턴은 아래 사항들을 중점적으로 다룹니다.

 

  1. 서비스 세분성 : 각 마이크로서비스가 정적 수준의 책임을 갖게 하는 방법
  2. 통신 프로토콜 : 마이크로서비스간의 데이터 교환
  3. 인터페이스 설계 : 개발자가 서비스 호출에 사용하는 실제 서비스 인터페이스를 설계
  4. 서비스간 이벤트 프로세싱 : 서비스간 의존성을 최소화하고 어플리케이션 회복성을 높이기 위한 방법

 

2) 라우팅 패턴

 

클라우드 기반 어플리케이션들은 수백 개의 마이크로 인스턴스가 실행 중일 수 있습니다.

때문에, 서비스의 물리적 IP 주소를 추상화하고 서비스 호출에 대한 단일 진입점을 만들어야 하는 필요성이 있습니다.

 

이를 위해, 서비스 디스커버리와 서비스 라우팅이라는 기능을 사용합니다.

 

  • 서비스 디스커버리 : 클라이언트에서 서비스의 물리적 위치를 추상화하여 투명하게 새 인스턴스를 추가하고 제거하는것을 제공
  • 서비스 라우팅 : 마이크로서비스 클라이언트에 인가 및 인증, 콘텐츠 검사 등 정책 시행 지점으로 사용되는 논리적 단일 URL을 제공

 

3) 클라이언트 회복성 패턴 

 

마이크로서비스 아키텍처는 한개의 서비스 문제가 서비스 소비자에게 연쇄적으로 발생하지 않도록 아래와 같은 4가지 클라이언트 회복성 패턴을 다룹니다.

 

  1. 클라이언트 측 부하 분산 : 서비스 클라이언트는 서비스 디스커버리에서 검색한 엔트포인트를 캐싱하고 인스턴스 간 서비스의 호출 부하를 분산합니다.
  2. 회로 차단기 패턴 : 서비스 클라이언트가 실패한 서비스를 반복적으로 호출하지 않게 하기 위해, 회로 차단기가 '빨리 실패'하도록 합니다.
  3. 폴백 패턴 : 클라이언트가 실패할 때, 데이터를 검색 등의 대체 방법을 제공합니다.
  4. 벌크헤드 패턴 : 오작동하는 서비스 하나가 클라이언트의 모든 리소스를 차지하지 않도록 클라이언트에서 다른 서비스 호출을 격리합니다.

 

4) 보안 패턴

 

해당 포스팅 책에서는 아래 3가지 보안패턴을 다룹니다.

 

  1. 인증
  2. 인가
  3. 자격 증명 관리와 전파 : 서비스 클라이언트가 여러 서비스 호출에서 자격 증명을 항상 제시하지 않아도 되는 방법

 

5) 로깅 및 추적 패턴

 

마이크로서비스의 단점은 어플리케이션과 서비스안에서 어떤일이 있어났는지 디버깅과 추적이 어렵다는 점입니다.

 

때문에, 아래와 같은 로깅 및 추적관련 패턴이 있습니다.

 

  • 로그 상관관계: 단일 트랜잭션에 대해 여러 서비스간 생성된 모든 로그를 상관관계를 매기는 방법
  • 로그 수집 : 개별 서비스 인스턴스에서 생성된 모든 로그를 질의 가능한 단일 데이터베이스로 취합하는 방법
  • 마이크로서비스 추적 : 트랜잭션과 연관된 모든 서비스에서 클라이언트 트랜잭션 흐름을 시각화하는 방법

 

6) 빌드 및 배포 패턴

 

마이크로서비스 아키텍처는 각 인스턴스가 모두 동일해야 한다는 점입니다.

이 말은, 서버가 배포된 이후의 변경으로 발생하는 '구성 편차'가 없어야 한다는 의미입니다.

 

8. 스프링 클라우드로 마이크로서비스 구축

 

위 패턴들에 사용할 기술들은 아래 그림으로 한눈에 볼 수 있습니다.

스프링 클라우드와 넷플릭스가 있습니다.

 

 

9. 마무리

 

이번 포스팅에서는 스프링, 클라우드와 만나다에 대해 간단한 소개와 설명을 했습니다.

다음에는 스프링 부트로 마이크로서비스 구축에 대해 포스팅하겠습니다.

반응형
반응형

1. 서론

 

이번 포스팅에서는 토비의 스프링 3.1의 1장인 오브젝트와 의존관계 에 대해 알아보도록 하겠습니다.

 

2. 초난감 DAO

스프링은 자바 언어 기반의 프레임워크입니다.

때문에, 객체지향 프로그래밍이 제공하는 폭넓은 혜택을 누릴 수 있도록 기본으로 돌아가자는 것이 스프링의 핵심입니다.

스프링은 객체지향 설계와 구현을 특정하도록 강요하지는 않습니다.

다만, 효과적으로 설계와 구현을 할 수 있도록 기준을 마련하여 개발자에게 편리함을 제공합니다.

 

아래는 책에 나오는 DB에 데이터를 추가 및 조회할 수 있는 예제입니다.

 

@Setter
@Getter
public class User {
    private String id;
    private String name;
    private String password;
}

 

public class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }

}

 

위 예제코드는 정상적으로 동작하나 사실은 문제가 많은 코드입니다.

개발자가 실제로 이러한 코드를 본다면... 경악을 금치 못할것입니다.

 

이제 아래 DAO의 분리와 DAO의 확장을 통해 위의 코드를 리팩토링하며 좀 더 나은 객체지향 설계로 변경하도록 하겠습니다.

 

3. DAO의 분리

위 예제를 리팩토링하기 전에, 먼저 관심사 분리에 대해서 설명하겠습니다.

 

자바 프로그램은 객체지향 장점으로 지속적인 변화에 매우 유용한 언어입니다.

그렇다면, 객체지향 장점이 그럼 무엇일까요?

바로, 관심사의 분리입니다.

 

즉, 관심이 같은 것끼리는 하나로 관심이 다른 것은 다르게 하여 실제 세계의 개념들을 객체라는 것으로 만들 수 있는 것을 의미합니다.

 

그렇다면, 위 초난감 DAO의 관심사항은 무엇일까요?

 

아마, 아래와 같은 관심사항으로 정의할 수 있습니다.

 

  1. DB와 연결을 위한 커넥션을 어떻게 가져올까라는 관심.
  2. 사용자 등록을 위해 DB에 보낼 SQL 문장을 담을 Statement를 만들고 실행하는 관심
  3. 작업이 끝나면 리소스인 Statement와 Connection 오브젝트를 닫아, 리소스 낭비를 일으키지 않는 관심

 

먼저, 중복 코드인 DB 연결 코드를 메소드로 추출하는 작업을 해보겠습니다.

현재는 2개의 메소드 밖에 없지만, 메소드가 10개 100개가 된다면 메소드를 만들 때 마다 Connection 소스가 중복으로 존재하게 됩니다.

 

아래는 getConnection이라는 메소드를 통해 DB 연결을 하는 중복코드를 제거한 코드입니다.

 

public class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
    
    private Connection getConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        return c;
    }

}

 

위와같이 변경함으로, 이제 DB 종류 혹은 접속 정보가 변경되더라도 getConnection 메소드만 수정하여 문제를 해결할 수 있게 되었습니다.

 

 

만약, UserDao를 소스를 외부에 제공하기 위해서는 메소드 추출만으로는 유연한 코드가 될 수 없습니다.

UserDao를 사용하는 클라이언트 입장에서 자신들이 사용하는 DB와 접속 정보에 따라 UserDao 코드를 수정해야 하기 때문입니다.

 

좀 더 유연한 UserDao를 만들기 위해 이번에는 자바에서 제공되는 상속을 사용하여 코드를 리팩토링 하겠습니다.

 

public abstract class UserDao {

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = getConnection();
        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }

    public abstract Connection getConnection() throws ClassNotFoundException, SQLException;
}

 

UserDao 클래스를 추상 클래스로, getConnection을 추상 메서드로 만들어 외부에서는 자신들이 사용하는 DB에 맞게 getConnection을 오버라이딩하여 사용할 수 있도록 만들었습니다.

 

위와 같인 슈퍼클래스에 기본적인 로직의 흐름을 만들고, 중간에 서브클래스가 제정의한 것을 사용하여 동작하도록 만드는 것이
템플릿 메소드 패턴입니다.

 

추가로, 위 예제에서는 Connection이라는 인터페이스를 통해 UserDao는 어떤 기능을 사용할지에 대한 관심만을 가지게 하였고.

서브 클래스들은 어떤 식으로 Connection 기능을 제공하는지에 관심을 가지도록 나누었습니다.

 

하지만, 위와 같은 코드도 문제점은 존재합니다. 바로 상속을 사용한 문제입니다.

자바는 기본적으로 다중 상속을 지원하지 않습니다.

 

때문에, UserDao의 서브 클래스는 UserDao가 아닌 클래스는 상속을 못받는 문제가 생깁니다.

 

4. DAO의 확장

위 getConnection은 DB 연결에 대한 관심으로 UserDao와는 관심이 다르기 때문에, 아예 별도의 클래스로 분리하겠습니다.

 

public class SimpleConnectionMaker {
    
    public Connection makeNewConnection() throws ClassNotFoundException, SQLException {
        Class.forName("com.mysql.jdbc.Driver");
        Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
        return c;
    }
}

 

public class UserDao {

    private SimpleConnectionMaker simpleConnectionMaker;

    public UserDao() {
        simpleConnectionMaker = new SimpleConnectionMaker();
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = simpleConnectionMaker.makeNewConnection();
        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
}

 

관심사 분리를 적용하여 클래스 분리까지는 좋았습니다.

다만, 위 코드는 외부제공 시 가져야하는 connection에 대한 유연성 문제를 해결하지 못한 코드입니다.

 

이런 경우, 인터페이스를 통하여 2개의 관심사를 느슨하게 연결할 수 있습니다.

 

public interface ConnectionMaker {
    
    Connection makeConnection() throws ClassNotFoundException, SQLException;
}

 

public class UserDao {

    private ConnectionMaker connectionMaker;

    public UserDao(ConnectionMaker connectionMaker) {
        this.connectionMaker = connectionMaker;
    }

    public void add(User user) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
        PreparedStatement ps = c.prepareStatement("insert into users(id, name, password) values(?, ?, ?)");
        ps.setString(1, user.getId());
        ps.setString(2, user.getName());
        ps.setString(3, user.getPassword());

        ps.executeUpdate();

        ps.close();
        c.close();
    }

    public User get(String id) throws ClassNotFoundException, SQLException {
        Connection c = connectionMaker.makeConnection();
        PreparedStatement ps = c.prepareStatement("select * from users where id = ?");
        ps.setString(1, id);

        ResultSet rs = ps.executeQuery();
        rs.next();
        User user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));

        rs.close();
        ps.close();
        c.close();

        return user;
    }
}

 

ConnectionMaker라는 인터페이스를 하나 만들어, UserDao가 인스턴스 시 해당 인터페이스의 구현체를 전달받아 사용하도록 하는 코드로 변경하였습니다.

 

사실상, 위 코드는 UserDao를 사용하는 제3의 클라이언트가 런타임 시 UserDao와 ConnectionMaker와의 관계를 갖도록 책임을 위임한 코드입니다.

 

그렇다면, 클라이언트는 ConnectionMaker의 구현체를 만들어 new UserDao(connectionImpl); 같은 코드로 프로그래밍해야 합니다.

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

5. 제어의 역전(IoC)

일반적으로, 팩토리는 객체의 생성 방법을 결정하고 그렇게 만들어진 오브젝트를 돌려주는 일을 합니다.

 

위 예제에서는 제 3의 클라이언트가 팩토리 기능을 구현해야 하는 것입니다.

이것 또한, UserDao, ConnectionMaker와는 별개로 객체의 관계 설정이라는 관심사로 분리되어 별도 클래스로 추출이 가능합니다.

 

public class DaoFactory {
    
    public UserDao userDao() {
        ConnectionMaker connectionMaker = () -> {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
            return c;
        };
        UserDao userDao = new UserDao(connectionMaker);
        return userDao;
    }
}

 

DaoFactory를 분리했을 때 얻을 수 있는 장점은 아래와 같습니다.

 

어플리케이션의 컴포넌트 역할을 하는 오브젝트와 어플리케이션의 구조를 결정하는 오브젝트를 분리.

 

이러한, 객체간의 관계를 제 3의 클라이언트 혹은 Factory 클래스에게 위임하는것이 바로 객체지향에서의 Ioc(제어의 역전)입니다.

 

제어의 역전에서는 아래와 같은 기능들을 제공해야 합니다.

 

  • 어플리케이션 컴포넌트의 생성과 관계설정
  • 컴포넌트 사용
  • 컴포넌트 생명주기 관리

 

6. 스프링의 IoC

 

이제 Ioc 역할인 DaoFactory를 스프링에서 사용하는 방식으로 변경하도록 하겠습니다.

 

스프링에서는 스프링이 제어권을 가지고 직접 만들고 관계를 부여하는 오브젝트를 빈(bean)이라고 부릅니다.

또한, 빈은 스프링 컨테이너가 생성과 관계설정, 사용 등을 제어해주는 제어의 역전이 적용된 오브젝트를 가리키는 말입니다.

 

스프링에서는 DaoFactory와 같이 IoC 오브젝트를 빈 팩토리라고 부르며, 동시에 어플리케이션 컨테스트라고도 일컫습니다.

실제 코드상에서는 ApplicationContext는 BeanFactory의 서브 인터페이스입니다.

 

이제 위 DaoFactory의 기능을 스프링으로 변경하겠습니다.

 

@Configuration
public class DaoFactory {

    @Bean
    public UserDao userDao() {
        UserDao userDao = new UserDao(connectionMaker());
        return userDao;
    }
    
    @Bean
    public ConnectionMaker connectionMaker() {
        return () -> {
            Class.forName("com.mysql.jdbc.Driver");
            Connection c = DriverManager.getConnection("jdbc:mysql://localhost/springbook", "spring", "book");
            return c;
        };
    }
}

 

위 코드의 @Configuration과 @Bean은 아래와 같습니다.

 

  1. @Configuration은 DaoFactory 클래스가 오브젝트 설정을 담당하는 클래스라는 것을 스프링이 인식할 수 있도록 하는 설정 표시.
  2. @Bean은 오브젝트 생성을 담당하는 IoC용 메소드라는 표시

 

이제 DaoFactory를 설정정보로 사용하는 어플리케이션 컨텍스트는 아래와 같습니다.

 

public static void main(String[] args) {
    ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
    UserDao userDao = context.getBean("userDao", UserDao.class);
    ...
}

 

DaoFactory라는 설정정보를 담고있는 클래스를 통하여 어플리케이션 컨텍스트를 생성하였습니다.

이러한, 어플리케이션 컨텍스트는 getBean 메서드를 통해 설정정보 클래스에 정의한 오브젝트를 가져올 수 있습니다.

 

getBean 메서드에서 사용한 "userDao"는 가져올 빈 이름을 의미합니다.

이 이름은 DaoFactory에서 @Bean 어노테이션을 붙인 메서드 명이 자동적으로 UserDao 빈의 이름으로 등록되어 집니다.

 

굳이 이름을 통하여 빈을 가져와야 하는 이유는, 스프링에서는 동일 타입의 인스턴스들을 빈으로 등록할 수 있기 때문입니다.

 

 

어플리케이션 컨텍스트는 DaoFactory와는 다르게 어플리케이션 전체에 IoC를 적용하여 모든 오브젝트에 대한 생성과 관계설정을 담당하는 것을 알았습니다.

 

이러한 스프링의 어플리케이션 컨텍스트 장점은 아래와 같습니다.

 

  1. 클라이언트는 구체적인 팩토리 클래스를 알 필요가 없습니다.
  2. 어플리케이션 컨텍스트는 종합 IoC 서비스를 제공해줍니다.
  3. 어플리케이션 컨텍스트는 빈을 검색하는 다양한 방법을 제공합니다.

 

7. 싱글톤 레지스트리와 오브젝트 스코프

 

스프링 어플리케이션 컨텍스트는 bean을 저장하고 관리하는 IoC 컨테이너라고 했습니다.

이때, bean은 싱글톤으로서 하나의 객체를 생성하여 bean으로 등록하여 재사용합니다.

 

싱글톤으로 만들어 사용하는 이유로는 스프링이 서버에서 주로 사용되는 프레임워크이기 때문입니다.

계속적인 객체가 생성되어 리소스 낭비가 심해지는것을 방지하기 위해서 입니다.

 

자바 디자인 패턴 중 싱글톤 패턴과는 한개의 객체를 사용하도록 한다는 개념은 같지만 약간 사용성이 다릅니다.

 

자바 싱글톤 패턴은 아래와 같습니다.

 

public class UserDao {

    private static UserDao INSTANCE;

    private UserDao() {
    }

    public static synchronized UserDao getInstance() {
        if (INSTANCE == null) INSTANCE = new UserDao();
        return INSTANCE;
    }
}

 

스프링의 bean의 싱글톤은 아래와 같이 정의할 수 있습니다.

 

public class UserDao {

    private static UserDao INSTANCE;

    public UserDao() {
    }

    public static UserDao getInstance() {
        if (INSTANCE == null) INSTANCE = new UserDao();
        return INSTANCE;
    }
}

 

차이점으로는 아래와 같습니다.

 

  1. 생성자 = bean의 경우 private 생성자가 아니기 때문에, 객체 생성에 대한 제약이 있지 않습니다.
  2. 동기화 코드 = bean의 경우 synchronized 선언이 되어 있지 않습니다. 이는 synchronized로 인해 발생하는 성능이슈를 방지하기 위함입니다.

 

차이점에서 두번째인 동기화 코드로 인해 bean 사용시 유의해야 할 점이 있습니다.

그건 바로, stateful 방식의 bean이 아닌 stateless 방식으로 빈을 생성해야 한다는 것입니다.

 

이유는 stateful 방식으로 bean을 사용하면 멀티쓰레드 환경에서 동시접근으로 인해 정확한 상태를 가질 수 없기 때문입니다.

 

 

8. 의존관계 주입(DI)

스프링에서 제공하는 또 하나의 큰 기능을 말하라고 한다면 DI(Dependency Injection) 를 말할 수 있습니다.

 

DI는 말 그대로 의존관계 주입으로 위에서 DaoFactory가 UserDao와 ConnectionMaker간의 의존성 관계를 주입하는 것을 의미합니다.

 

의존관계에는 방향성이 존재합니다.

 

위 UserDao와 ConnectionMaker의 의존관계를 표현한다면 UserDao -> ConnectionMaker로 표현할 수 있으며,

UserDao는 ConnectionMaker에 의존하고 있다고 말할 수 있습니다.

 

즉, 이 경우 UserDao는 ConnectionMaker에 의존하고 있기 때문에, ConnectionMaker 변경에 영향을 받습니다.

하지만, ConnectionMaker는 UserDao 에 의존하고 있지 않기 때문에 UserDao 변경에 영향을 받지 않습니다.

 

스프링에서는 의존관계 주입을 3가지의 방법을 통해 정의할 수 있습니다.

 

  1. 어노테이션 - @Autowired를 사용
  2. 생성자 주입
  3. setter 주입

 

위의 경우 UserDao는 생성자에 ConnectionMaker를 받고 있기 때문에 생성자 주입방식을 사용한 것을 알 수 있습니다.

 

스프링 Document에서는 DI 방식으로 생성자 주입을 권장하고 있습니다.
다만, 의존관계가 많은 클래스의 경우 생성자 코드가 커져 장황한 코드가 나올 수 있는 단점이 있습니다.
이는 lombok 과 같은 써드파티 라이브러리를 사용한다면 해결 가능합니다.

 

추가로 스프링은 DL(Dependency Lookup) 도 제공하고 있습니다.

DL은 의존관계 검색으로서 위에서 살펴본 getBean을 DL이라고 볼 수 있습니다.

 

DI는 객체간의 의존관계를 주입하기 위해 제어권을 스프링에게 넘겨야 하며, DI의 관련 객체들은 모두 bean으로 스프링 컨테이너에 등록되어야 합니다.

 

하지만 DL은 스프링 컨테이너에 등록된 bean을 검색하는 용도로,

DL을 사용하는 클래스는 bean으로 등록될 필요가 없다는 차이점을 가지고 있습니다.

 

9. 마무리

이번 포스팅에서는 오브젝트와 의존관계에 대해 간단한 소개와 설명을 했습니다.

다음에는 테스트에 대해 포스팅하겠습니다.

반응형

'Framework > Spring' 카테고리의 다른 글

(2) 테스트  (2) 2020.06.21
반응형

1. 서론

이번 포스팅에서는 Hbase가 제공하는 클라이언트 API : 관리 기능에 대해 알아보겠습니다.

 

Hbase는 데이터를 조작하는 API 뿐 만이 아닌 데이터 정의를 위한 클라이언트 API도 제공하고 있습니다.

 

2. 스키마 정의

Hbase에서는 테이블 생성 시 테이블 및 컬럼패밀리의 스키마를 정의해야 합니다.

 

1) 테이블

 

Hbase는 데이터를 저장하는 가장 상위의 개념으로 테이블을 사용합니다.

이런, 테이블의 특성을 클라이언트에서는 아래와 같은 지시자 클래스로 제공합니다.

 

TableDescriptorBuilder.newBuilder(TableName tableName).build();
TableDescriptorBuilder.newBuilder(TableDescriptor tableDescriptor).build();

TableName tableName = TableName.valueOf("tableName");
TableDescriptor tableDescriptor = new ModifyableTableDescriptor(tableName);

 

Hbase 의 테이블 명은 hdfs의 실제 디렉터리로 생성이되기 때문에 파일명 규칙에 따라 만들어야 합니다.

 

Hbase의 테이블은 논리적인 개념이며, 물리적으로는 여러개의 리전으로 분리가 되며 각 리전은 Hbase Region Server에 존재하게 됩니다.

 

아래는 위 설명을 간략하게 나타낸 그림입니다.

 

 

2) 테이블 속성

 

테이블 지시자 클래스에는 속성 설정을 위한 Getter, Setter를 제공합니다.

 

이름

 

아래와 같이 테이블 이름을 가져올 수 있습니다.

 

byte[] tableName1 = tableDescriptor.getTableName().getName();
String tableName2 = tableDescriptor.getTableName().getNameAsString();

 

수정의 경우 마이너 버전에서는 setName으로 가능했으나 현재는 테이블을 새로 생성하여 migration하는 방법을 권장하고 있습니다.

 

컬럼패밀리

 

테이블 정의 시 가장 중요한 부분은 컬럼패밀리입니다.

 

컬럼패밀리의 경우 아래와 같은 API를 통해 조작이 가능합니다.

 

tableDescriptor.getColumnFamilies();
tableDescriptor.getColumnFamily(byte[] column);
tableDescriptor.hasColumnFamily(byte[] column);
Admin admin = connection.getAdmin();
admin.addColumnFamily();
admin.deleteColumnFamily();

 

파일 최대 크기

 

테이블내의 리전이 커질 수 있는 최대 파일 크기 또한 조작이 가능합니다.

 

API 명세는 아래와 같습니다.

 

tableDescriptor.getMaxFileSize();
((ModifyableTableDescriptor) tableDescriptor).setMaxFileSize(1024000);

 

최대 크기 설정은 리전 크기가 해당 값에 도달했을때 시스템이 리전을 분할하는 기준이 됩니다.

 

 

읽기 전용

 

쓰기가 아닌 일기 전용 테이블을 만들어야 하는 경우 아래와 같은 API를 사용하면 됩니다.

 

tableDescriptor.isReadOnly();
((ModifyableTableDescriptor) tableDescriptor).setReadOnly(true);

 

 

 

멤스토어 플러시 크기

 

아래는 멤스토어에 있는 데이터를 HFile로 write하는 트리거링을 바이트 단위로 조작하고 싶은 경우 사용하는 API 입니다.

 

tableDescriptor.getMemStoreFlushSize();
((ModifyableTableDescriptor) tableDescriptor).setMemStoreFlushSize(102400);

 

 

3) 컬럼패밀리

 

TableDescriptor 와 같이 컬럼패밀리에 대한 설정을 담고 있는 클래스는 ColumnFamilyDescriptor 입니다.

 

컬럼패밀리 또한 저장소 계층의 디렉터리 이름으로 사용되기 때문에 파일명 규칙을 따라 생성해야 합니다.

 

아래는 컬럼패밀리 지시자 클래스 정의입니다.

 

ColumnFamilyDescriptor columnFamilyDescriptor1 = 
	ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes("")).build();
ColumnFamilyDescriptor columnFamilyDescriptor2 = 
	ColumnFamilyDescriptorBuilder.newBuilder(new ColumnFamilyDescriptorBuilder.ModifyableColumnFamilyDescriptor(Bytes.toBytes(""))).build();

 

이름

 

아래와 같이 이름을 읽을 수 있습니다.

 

byte[] name1 = columnFamilyDescriptor.getName();
String name2 = columnFamilyDescriptor.getNameAsString();

 

Setter를 통해 이름을 설정할 수 는 없습니다.

 

최대 버전 갯수

 

컬럼패밀리 별로 값의 버전을 몇 개까지 보유할 지 지정할 수 있습니다.

 

columnFamilyDescriptor.getMaxVersions();
((ModifyableColumnFamilyDescriptor)columnFamilyDescriptor).setMaxVersions(100);

 

압축

 

컬럼패밀리에 저장된 데이터에 특정 압축기법을 적용할 수 있습니다.

 

Algorithm algorithm1 = columnFamilyDescriptor.getCompactionCompressionType();
Algorithm algorithm2 = columnFamilyDescriptor.getCompressionType();
((ModifyableColumnFamilyDescriptor)columnFamilyDescriptor).setCompactionCompressionType(Algorithm.GZ);
((ModifyableColumnFamilyDescriptor)columnFamilyDescriptor).setCompressionType(Algorithm.GZ);

 

Algorithm 은 Hbase 클라이언트에서 제공하는 enum으로 아래 값들이 있습니다.

 

  • LZO
  • GZ
  • NONE
  • SNAPPY
  • LZ4
  • BZIP2
  • ZSTD
기본은 NONE으로 압축하지 않습니다.

 

블록크기

 

Hbase는 컬럼패밀리별로 HFile을 저장하게 됩니다.

그렇기 때문에, HFile의 블록 크기를 컬럼패밀리 지시자를 통해 조작이 가능합니다.

 

columnFamilyDescriptor.getBlocksize();
((ModifyableColumnFamilyDescriptor)columnFamilyDescriptor).setBlocksize(1024);

 

기본 HFile의 블록크기는 64KB로 기본 HDFS의 블록 크기인 64MB에 비교하여 1/1024 의 크기입니다.

 

블록 캐시

 

Hbase는 I/O 자원을 효율적으로 사용하기 위해 scan 연산 시 모든 블록을 읽어 메모리에 상주시켜놓고 재사용하게 합니다.

메모리에 올렸기 때문에 디스크에는 다시 접근하지 않아 I/O를 절약합니다.

 

columnFamilyDescriptor.isBlockCacheEnabled();
((ModifyableColumnFamilyDescriptor) columnFamilyDescriptor).setBlockCacheEnabled(false);

 

유효기간

 

Hbase 에서는 컬럼패밀리에 속한 데이터의 버전 뿐만이 아닌 유효기간도 설정도 가능합니다.

 

기본은 영원히 저장하도록 되어 있으며, 유효기간을 지정하게 되면 주 컴팩션시  유효기간이 지난 데이터들을 찾아 삭제합니다.

 

columnFamilyDescriptor.getTimeToLive();
((ModifyableColumnFamilyDescriptor) columnFamilyDescriptor).setTimeToLive(1000);

 

인메모리

 

위 블록캐시에서 말한 메모리에 대한 설정도 가능합니다.

 

columnFamilyDescriptor.isInMemory();
((ModifyableColumnFamilyDescriptor) columnFamilyDescriptor).setInMemory(true);

 

 

반응형

 

 

3. HBaseAdmin

Hbase에는 RDBMS의 DDL과 흡사한 기능을 제공하는 HBaseAdmin 클래스를 제공합니다.

 

HBaseAdmin 인스턴스는 아래와 같이 가져올 수 있습니다.

 

Configuration conf = HBaseConfiguration.create();
conf.set("hbase.zookeeper.quorum", "");

Connection connection = ConnectionFactory.createConnection(conf);
HBaseAdmin hBaseAdmin = (HBaseAdmin) connection.getAdmin();

 

1) 기본 기능

 

HBaseAdmin은 아래와 같은 기본 기능을 제공합니다.

 

1. getConnection()

 

연결 인스턴스를 반환하는 메소드입니다.

 

2. getConfiguration()

 

HBaseAdmin 인스턴스를 생성할 때 사용된 설정 인스턴스를 반환합니다.

 

3. close()

 

HBaseAdmin 인스턴스가 점유하고 있는 모든 자원을 해제합니다.

 

 

2) 테이블 관련 기능

 

HBaseAdmin은 DDL과 같이 테이블 생성을 위한 메서드를 제공합니다.

 

1. 생성

 

void createTable(TableDescriptor desc) throws IOException;
void createTable(TableDescriptor desc, byte[] startKey, byte[] endKey, int numRegions) throws IOException;
void createTable(final TableDescriptor desc, byte [][] splitKeys);

 

메서드 2번째를 보시면 테이블 생성 시 row key의 시작키와 끝키를 지정하여 특정 리전에 존재하는 테이블을 생성시킬 수 있습니다.

이때, 시작키는 끝키보다 작아야하며, 기본적으로 Hbase 의 리전은 시작키를 포함하고 끝키는 포함하지 않습니다.

numResions는 테이블이 확보해야하는 최소 리전 수를 의미하며
최소 3 이상이어야 합니다. 

 

아래는 테이블 생성하는 간단한 예제입니다.

 

Connection connection = ConnectionFactory.createConnection(HBaseConfiguration.create());
HBaseAdmin hBaseAdmin = (HBaseAdmin) connection.getAdmin();
TableDescriptor desc = TableDescriptorBuilder.newBuilder(TableName.valueOf("testTable")).build();
hBaseAdmin.createTable(desc);

 

 

2. 조회

 

테이블이 존재하는지, 혹은 지정한 테이블의 스키마 정의를 아래와 같은 메서드를 통해 가져올 수 있습니다.

 

boolean tableExists(final TableName tableName) throws IOException;
HTableDescriptor[] listTables() throws IOException;
HTableDescriptor[] listTables(Pattern pattern) throws IOException;
HTableDescriptor[] listTables(String regex) throws IOException;
HTableDescriptor getTableDescriptor(final TableName tableName) throws IOException;

 

3. 삭제

 

테이블 삭제는 아래 메서드를 사용하시면 됩니다.

 

void deleteTable(final TableName tableName) throws IOException;

 

4. 비활성

 

HBase에서는 테이블 삭제 전 필수로 비활성을 시켜야 합니다.

 

비활성 메서드는 아래와 같습니다.

 

void disableTable(final TableName tableName) throws IOException;

 

비활성화 메서드는 아래와 같은 동작을 HBase에게 수행하도록 합니다.

 

  1. 모든 리전 서버에서 아직 적용하지 않은 변경사항 처리
  2. 모든 리전 닫음
  3. 해당 테이블의 모든 리전이 어떤 서버에도 배치되어 있지 않다는 정보를 메타( .META. ) 테이블에 기록

 

5. 활성화

 

HBaseAdmin은 테이블의 활성화 메서드도 제공하고 있습니다.

 

void enableTable(final TableName tableName) throws IOException;

 

6. 확인

 

지정 테이블이 활성화되어 있는지, 비활성화 되어 있는지 등의 상태를 확인하는 메서드는 아래와 같습니다.

 

boolean isTableEnabled(final TableName tableName) throws IOException;
boolean isTableDisabled(TableName tableName) throws IOException;
boolean isTableAvailable(TableName tableName) throws IOException;

 

isTableAvailable 메서드의 경우에는 테이블의 활성화 상태가 아닌 단지 존재하는지를 체크하는 메서드입니다.

 

7. 변경

 

테이블에 대한 정의를 변경도 가능합니다.

 

void modifyTable(TableDescriptor td) throws IOException;

 

단, 변경도 삭제와 동일하게 먼저 테이블을 비활성화 시킨 후 수행하여야 합니다.

 

 

3) 스키마 관련 기능

 

HBaseAdmin은 테이블의 컬럼 패밀리에 대한 정의도 변경 가능합니다.

 

void addColumnFamily(final TableName tableName, final ColumnFamilyDescriptor columnFamily) throws IOException;
void deleteColumnFamily(final TableName tableName, final byte[] columnFamily) throws IOException;
void modifyColumnFamily(final TableName tableName, final ColumnFamilyDescriptor columnFamily) throws IOException;

 

4) 클러스터 관련 기능

 

HBaseAdmin 클래스는 클러스터에 관련된 정보를 확인 및 조작할 수 있는 메서드를 제공합니다.

 

1. void flush(final TableName tableName) throws IOException

 

멤스토어에 있는 정보들을 강제로 disk write 하도록 수행하는 메서드입니다.

 

2. void compact(final TableName tableName) throws IOException

 

지정한 테이블을 컴팩션 대기열에 넣는 메서드입니다.

 

3. void majorCompact(final TableName tableName) throws IOException

 

지정 테이블에 대해 주 컴팩션이 일어나도록 트리거링하는 메서드입니다.

 

4. void split(final TableName tableName) throws IOException

 

지정 테이블에 대해 분할 작업을 수행시키는 메서드입니다.

지정 테이블의 모든 리전을 이터레이트하여 분할 작업을 자동적으로 호출합니다.

 

5. void assign(final byte [] regionName) throws

MasterNotRunningException, ZooKeeperConnectionException, IOException

 

리전을 할당할 때 사용하는 메서드입니다.

 

6. void unassign(final byte [] regionName, final boolean force) throws IOException

 

assign과 반대로 리전을 해제할 때 사용하는 메서드입니다.

 

7. void move(final byte[] encodedRegionName, ServerName destServerName) throws IOException

 

지정 리전을 특정 서버로 이동시키는 메서드입니다.

 

즉, 클라이언트는 능동적으로 특정 리전을 현재 배치된 서버가 아닌 다른 서버로 옮길 수 있습니다.

 

8. boolean balancerSwitch(final boolean on, final boolean synchronous) throws IOException

 

리전 밸런서를 키거나 끄는 메서드 입니다.

리번 밸런서랑 해당 리전이 지정 크기보다 커지는 경우 분할 시켜 다른 리전 서버로 보내는 역할을 합니다.

 

9. synchronized void shutdown() throws IOException

 

클러스터를 중단할 때 사용합니다.

 

10. synchronized void stopMaster() throws IOException

 

마스터 서버를 중단할 때 사용합니다.

 

11. synchronized void stopRegionServer(final String hostnamePort) throws IOException

 

특정 리전 서버만을 중단 할 때 사용합니다.

 

4. 마무리

이번 포스팅에서는 클라이언트 API : 관리 기능에 대해 진행하였습니다.

 

다음 포스팅에서는 챕터 6장인 클라이언트 종류에 대해 진행하겠습니다.

 

반응형

'BigData > Hbase' 카테고리의 다른 글

(4) 클라이언트 API : 고급 기능  (0) 2020.04.19
(3) 클라이언트 API : 기본 기능  (0) 2020.04.09
(2) 설치  (0) 2020.04.08
(1) 소개  (0) 2020.04.07
반응형

1. 서론

이번 포스팅에서는 Chapter17의 리액티브 프로그래밍에 대해 진행하도록 하겠습니다.

 

2. 리액티브 매니패스토

리액티브 매니패스토란 리액티브 어플리케이션과 시스템 개발의 핵심 원칙을 공식적으로 정의한 내용입니다.

 

  1. 반응성 : 리액티브 시스템은 빠를 뿐 아니라 더 중요한 특징으로 일정하고 예상할 수 있는 반응 시간을 제공합니다.
  2. 회복성 : 장애가 발생해도 시스템은 반응해야 합니다.
  3. 탄력성 : 어플리케이션의 생명주기 동안 다양한 작업 부하를 받게 되는데 이 다양한 작업 부하로 어플리케이션의 반응성이 위협받을 수 있습니다. 이를 대비해, 자동으로 할당 된 자원 수를 늘립니다.
  4. 메시지 주도 : 회복성과 탄력성을 위해 메시지 기반의 통신으로 이루어 지도록 합니다.

아래는 위 4가지가 어떤 관계로 얽혀있는지 보여줍니다.

 

 

3. 리액티브 스트림과 플로 API

리액티브 프로그래밍은 리액티브 스트림을 사용하는 프로그래밍입니다.

 

리액티브 스트림은 무한의 비동기 데이터를 순서대로 블록하지 않은 역압력을 전제하여 처리하는 기술입니다.

스트림의 비동기가 기반이기 때문에 역압력 기술은 필수입니다.

 

역압력이란, 데이터 소모하는 쪽에서 데이터 발행자에게 요청한 경우에만 처리하도록 하는 기법입니다.

 

 

1) Flow 클래스 소개

 

자바 9 에서는 java.util.concurrent.Flow를 추가했습니다.

 

Flow 클래스는 내부적으로 4개의 인터페이스를 가지고 있습니다.

 

  • Publisher
  • Subscriber
  • Subscription
  • Processor

아래는 각 4개의 인터페이스 정의입니다.

 

@FunctionalInterface
public static interface Publisher<T> {
    public void subscribe(Subscriber<? super T> subscriber);
}

public static interface Subscriber<T> {
    public void onSubscribe(Subscription subscription);
    public void onNext(T item);
    public void onError(Throwable throwable);
    public void onComplete();
}

public static interface Subscription {
    public void request(long n);
    public void cancel();
}

public static interface Processor<T,R> extends Subscriber<T>, Publisher<R> {

}

 

여기서 Processor는 리액티브 스트림에서 처리하는 이벤트의 변환을 담당합니다.

 

 

아래는 이 4개의 인터페이스의 동작 원칙입니다.

 

  • Publisher는 반드시 Subscription의 request 메서드에 정의된 개수 이하의 요소만 Subscriber에 전달해야 합니다.
  • Subscriber 는 요소를 받아 처리할 수 있음을 Publisher에게 알려야 합니다.
  • Publisher 와 Subscriber 는 Subscription을 공유해야 합니다.

 

 

1) 첫번째 리액티브 어플리케이션 만들기

 

아래는 매초 온도를 보고하는 예제입니다.

 

public class TempInfo {

    public static final Random random = new Random();

    private final String town;
    private final int temp;

    public TempInfo(String town, int temp) {
        this.town = town;
        this.temp = temp;
    }
    
    public static TempInfo fetch(String town) {
        if(random.nextInt(10) == 0) {
            throw new RuntimeException("ERROR!!");
        }
        return new TempInfo(town, random.nextInt(100));
    }
}

 

public class TempSubscriber implements Flow.Subscriber<TempInfo> {

    private Flow.Subscription subscription;

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        this.subscription = subscription;
        subscription.request(1);
    }

    @Override
    public void onNext(TempInfo item) {
        System.out.println(item);
        subscription.request(1);
    }

    @Override
    public void onError(Throwable throwable) {
        System.err.println(throwable.getMessage());
    }

    @Override
    public void onComplete() {
        System.out.println("Done!");
    }
}

 

public class TempSubscription implements Flow.Subscription {

    private final Flow.Subscriber<? super TempInfo> subscriber;
    private final String town;

    public TempSubscription(Flow.Subscriber<? super TempInfo> subscriber, String town) {
        this.subscriber = subscriber;
        this.town = town;
    }

    @Override
    public void request(long n) {
        for(long i = 0L; i < n; i++) {
            try {
                subscriber.onNext(TempInfo.fetch(town));
            } catch (Exception e) {
                subscriber.onError(e);
                break;
            }
        }
    }

    @Override
    public void cancel() {
        subscriber.onComplete();
    }
}

 

public class Main {

    public static void main(String[] args) {
        getTemperatures("New York").subscribe(new TempSubscriber());
    }

    private static Flow.Publisher<TempInfo> getTemperatures(String town) {
        return subscriber -> subscriber.onSubscribe(
                new TempSubscription(subscriber, town)
        );
    }
}

 

 

위 예제에서는 Subscription의 request를 호출하면, Subscriber가 Subscription을 또 호출하는 재귀 문제가 있습니다.

이를 해결하기 위해서는 별도 Executor를 사용할 수 있습니다.

 

Executor 소스를 추가한 코드는 아래와 같습니다.

 

public class TempSubscription implements Flow.Subscription {
    
    private static final ExecutorService executor = Executors.newSingleThreadExecutor(); 
    private final Flow.Subscriber<? super TempInfo> subscriber;
    private final String town;

    public TempSubscription(Flow.Subscriber<? super TempInfo> subscriber, String town) {
        this.subscriber = subscriber;
        this.town = town;
    }

    @Override
    public void request(long n) {
        executor.submit(() -> {
            for(long i = 0L; i < n; i++) {
                try {
                    subscriber.onNext(TempInfo.fetch(town));
                } catch (Exception e) {
                    subscriber.onError(e);
                    break;
                }
            }    
        });
    }

    @Override
    public void cancel() {
        subscriber.onComplete();
    }
}

 

 

 

2) Processor로 데이터 변환하기

 

Processor의 목적은 Publisher를 구독한 다음 수신한 데이터를 가공해 다시 제공하는 것입니다.

 

아래는 위 예제에 Processor를 적용한 코드입니다.

 

public class TempSubscription implements Flow.Subscription {

    private static final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final Flow.Processor<? super TempInfo, ? super TempInfo> processor;
    private final String town;

    public TempSubscription(Flow.Processor<? super TempInfo, ? super TempInfo> processor, String town) {
        this.processor = processor;
        this.town = town;
    }

    @Override
    public void request(long n) {
        executor.submit(() -> {
            for(long i = 0L; i < n; i++) {
                try {
                    processor.onNext(TempInfo.fetch(town));
                } catch (Exception e) {
                    processor.onError(e);
                    break;
                }
            }
        });
    }

    @Override
    public void cancel() {
        processor.onComplete();
    }
}

 

public class TempProcessor implements Flow.Processor<TempInfo, TempInfo> {

    private Flow.Subscriber<? super TempInfo> subscriber;

    @Override
    public void subscribe(Flow.Subscriber<? super TempInfo> subscriber) {
        this.subscriber = subscriber;
    }

    @Override
    public void onSubscribe(Flow.Subscription subscription) {
        subscriber.onSubscribe(subscription);
    }

    @Override
    public void onNext(TempInfo item) {
        subscriber.onNext(
                new TempInfo(
                        item.getTown(),
                        (item.getTemp() -32) * 5 /9
                )
        );

    }

    @Override
    public void onError(Throwable throwable) {
        subscriber.onError(throwable);
    }

    @Override
    public void onComplete() {
        subscriber.onComplete();
    }
}

 

public class Main {

    public static void main(String[] args) {
        getTemperatures("New York").subscribe(new TempSubscriber());
    }

    private static Flow.Publisher<TempInfo> getTemperatures(String town) {
        return subscriber -> {
            TempProcessor processor = new TempProcessor();
            processor.subscribe(subscriber);
            processor.onSubscribe(new TempSubscription(processor, town));
        };
    }
}

 

Processor를 이용해 onNext 메서드에서 화씨 온도를 섭씨 온도로 변경하여 Subscriber 에게 전달하는 것을 볼 수 있습니다.

 

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

4. 리액티브 라이브러리 RxJava

RxJava는 자바로 리액티브 어플리케이션을 구현하는데 사용하는 라이브러리입니다.

compile group: 'io.reactivex.rxjava2', name: 'rxjava', version: '2.2.19'

 

RxJava는 io.reactivex 패키지하위의 Observable, Observer 를 사용할 수 있습니다.

 

간단히 자바 플로에서 Publisher 가 RxJava에서는 Observable, Subscriber가 Observer라고 보시면 됩니다.

 

 

1) Observable 만들고 사용하기

 

아래는 간단한 Observable 을 만드는 예제 코드입니다.

 

Observable<String> strings = Observable.just("fisrt", "second");

 

just 팩토리 메서드는 한개 이상의 요소를 이용해 방출하는 Observable 을 만듭니다.

 

Observable 구독자는 onNext("first"), onNext("second"), onComplete() 순으로 메시지를 받게 됩니다.

 

특정 시간단위로 상호작용할때는 아래와 같이 interval 팩토리 메서드를 사용할 수 있습니다.

 

Observable<Long> onePerSec = Observable.interval(1, TimeUnit.SECONDS);

 

추가로 RxJava는 역압력을 지원하지 않아, Observable에는 Subscription의 request 메서드 같은 기능이 없습니다.

 

Observer 인터페이스는 위의 Subscriber와 정의도 비슷합니다.

 

아래는 Observer 인터페이스 정의입니다.

 

public interface Observer<T> {
    void onSubscriber(Disposable d);
    void onNext(T t);
    void onCompleted();
    void onError(Throwable e);
}

 

RxJava는 위의 메서드를 모두 구현할 필요가 없고, onNext 만 구현해도 괜찮습니다.

 

예제는 아래와 같습니다.

 

onePerSec.subscribe(i -> System.out.println(TempInfo.fetch("New York")));

 

 

이 예제는 초마다 뉴욕의 온도를 출력하는 예제입니다.

 

하지만 실제로 동작시키면 아무런 출력이 되지 않습니다.

이유는, Observable이 RxJava의 연산 쓰레드 풀 즉 데몬 쓰레드에서 실행되기 때문입니다.

 

RxJava는 호출 쓰레드에서 값을 받는 메서드도 제공합니다.

 

예제는 아래와 같습니다.

 

onePerSec.blockingSubscribe(i -> System.out.println(TempInfo.fetch("New York")));

 

이제 위 예제에 RxJava를 적용해보겠습니다.

 

private static Observable<TempInfo> getTemperatures(String town) {
    return Observable.create(emitter -> {
        Observable.interval(1, TimeUnit.SECONDS)
                .subscribe(i -> {
                    if (!emitter.isDisposed()) {
                        if (i >= 5) {
                            emitter.onComplete();
                        } else {
                            try {
                                emitter.onNext(TempInfo.fetch(town));
                            } catch (Exception e) {
                                emitter.onError(e);
                            }

                        }
                    }
                });
    });
}

 

public class TempObserver implements Observer<TempInfo> {
    
    @Override
    public void onCompleted() {
        System.out.println("Done!");    
    }

    @Override
    public void onError(Throwable e) {
        System.out.println("Got problem : " + e.getMessage());

    }

    @Override
    public void onNext(TempInfo tempInfo) {
        System.out.println(tempInfo);
    }
}

 

public static void main(String[] args) {
    Observable<TempInfo> observable = getTemperatures("New York");
    observable.blockingSubscribe(new TempObserver());
}

 

 

2) Observable 변환하고 합치기

 

Observable에는 스트림과 비슷한 메서드들을 제공합니다.

 

  • map
  • filter
  • merge

 

map

 

map은 스트림의 map 과 같이 요소를 변환하는 메서드입니다.

자바 플로의 Processor라고 보시면 됩니다.

 

아래는 섭씨로 변환하는 작업을 Observable의 map을 사용하여 만드는 예제입니다.

 

private static Observable<TempInfo> getCelsiusTemperature(String town) {
    return getTemperatures(town).map(item -> new TempInfo(
            item.getTown(),
            (item.getTemp() -32) * 5 /9
    ));
}

 

filter

 

filter도 스트림과 동일하게 특정 조건의 데이터만을 추출하는 용도의 메서드입니다.

 

아래는 filter 예제 코드입니다.

 

private static Observable<TempInfo> getNegativeTemperature(String town) {
    return getCelsiusTemperature(town).filter(temp -> temp.getTemp() < 0);
}

 

merge

 

merge는 여러 Observable를 하나의 Observable로 만드는 메서드입니다.

 

아래는 merge 예제 코드입니다.

 

private static Observable<TempInfo> getCelsiusTemperatures(String... towns) {
    return Observable.merge(
            Arrays.asList(towns).stream()
            .map(Main::getCelsiusTemperature)
            .collect(Collectors.toList())
    );
}

 

public static void main(String[] args) {
    Observable<TempInfo> observable = getCelsiusTemperatures("New York", "Chicago", "San Francisco");
    observable.blockingSubscribe(new TempObserver());
}

 

merge 메서드는 Observable의 Iterator을 인수로 받아 한개의 Observable처럼 동작합니다.

 

5. 마무리

이번 포스팅에서는 Chapter17 리액티브 프로그래밍 대해 진행하였습니다.

이렇게, 모던 자바 인 액션에 대한 포스팅은 완료했습니다.

반응형
반응형

1. 서론

이번 포스팅에서는 Chapter16의 CompletableFuture : 안정적 비동기 프로그래밍에 대해 진행하도록 하겠습니다.

 

2. Future의 단순 활용

Future 인터페이스는 비동기 계산을 모델링 하는데 쓰이며, 계산이 끝났을 때 결과에 접근할 수 있는 참조를 제공합니다.

 

Future 이용시에는 시간이 오래걸리는 작업을 Callable 객체 내부로 감싼 다음 ExecutorService에 제출하면 됩니다.

 

아래는 예제 코드입니다.

 

ExecutorService executorService = Executors.newCachedThreadPool();
Future<Double> future = executorService.submit(new Callable<Double>() {
    Override
    public Double call() throws Exception {
        return doSomeThingLongComputation();
    }
});        
doSomeThingElse();        
try {
    double result = future.get(1, TimeUnit.SECONDS);
} catch (InterruptedException e) {
    // 현재 스레드에서 대기 중 인터럽트 발생
} catch (ExecutionException e) {
    // 계산 중 예외
} catch (TimeoutException e) {
    // Future가 완료되기 전에 타임아웃 발생 
}

 

위 예제에서 doSomeThingLongComputation 연산은 별도 쓰레드에서 수행하게 됩니다.

수행 후, 값을 얻기 위해 get 메서드를 호출해야합니다.

연산이 끝난 경우에는 바로 반환되지만 연산이 진행중이라면 get 메서드에서 블로킹 됩니다.

 

1) Future의 제한

 

Future는 일련의 동시 실행 코드를 구현하기에 충분하지 않습니다.

 

예를들어, '오래 걸리는 A라는 계산이 끝나면 그 결과를 다른 오래 걸리는 계산 B로 전달하시오 그리고 B의 결과가 나오면 다른 질의의 결과와 B의 결과를 조합하시오' 와 같은 요구사항을 쉽게 구현하기가 어렵다는 점입니다. 

 

이러한 제한적인 부분을 자바 8에서 제공하는 CompletableFuture로 해결할 수 있습니다.

 

3. 비동기 API 구현

CompletableFuture를 이용하여 최저가격 검색 어플리케이션 예제를 진행하겠습니다.

 

public class Shop {
    
    private static final Random random = new Random();
    
    private String name;

    public Shop(String name) {
        this.name = name;
    }
    
    public double getPrice(String product) {
        return calculatePrice(product);
    }
    
    private double calculatePrice(String product) {
        delay();
        return  random.nextDouble() * product.charAt(0) + product.charAt(1);
    }
    
    private static void delay() {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

 

위 예제에서는 상품명을 받아 1초의 작업 후 연산 결과를 반환하는 getPrice 메서드가 있습니다.

 

 

1) 동기 메서드를 비동기 메서드로 변환

 

위 예제의 경우 getPrice를 호출한 쓰레드는 1초 간 블로킹 되어집니다.

 

이 부분을 해소하기 위해 CompletableFuture를 이용한 getPriceAsync 메서드를 추가하겠습니다.

 

public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
       double price = calculatePrice(product);
       futurePrice.complete(price);
    }).start();
    
    return futurePrice;
}
Shop shop = new Shop("BestShop");
long start = System.nanoTime();
Future<Double> futurePrice = shop.getPriceAsync("my favorite product");
long invocationTime = ((System.nanoTime() - start) / 1000000);
System.out.println("Invocation returned after " + invocationTime + "msecs");

doSomeThingElse(); // 가격 정보를 가져오는 동안 다른 일 수행
        
try {
    double price = futurePrice.get();
    System.out.printf("Price is %.2f%n", price);
} catch (Exception e) {
    throw new RuntimeException(e);
}
        
long retrievalTime = ((System.nanoTime() - start) / 1000000);
System.out.println("Price returned after " + retrievalTime + " msecs");

 

위 코드의 getPriceAsync는 가격 계산이 끝나기 전에 return을 받습니다.

단, doSomeThingElse 의 작업이 먼저 끝난다면 get 메서드시 블로킹 되는 문제는 여전히 가지고 있습니다.

 

이 문제는, CompletableFuture의 기능을 활용하면 해결 가능하며 아래에서 더 살펴보겠습니다.

 

 

2) 에러 처리 방법

 

비동기로 처리시에는 별도의 쓰레드에서 동작하기에 에러 처리가 매우 까다롭습니다.

 

하지만, CompletableFuture 의 completeExceptionally 메서드를 사용하면 매우 간단해집니다.

 

아래는 예제 코드입니다.

 

public Future<Double> getPriceAsync(String product) {
    CompletableFuture<Double> futurePrice = new CompletableFuture<>();
    new Thread(() -> {
        try {
            double price = calculatePrice(product);
            futurePrice.complete(price);     
        } catch (Exception e) {
            futurePrice.completeExceptionally(e);
        }
    }).start();

    return futurePrice;
}

 

위와 같이 코드 추가 후 예외가 실제로 발생되면, 클라이언트는 ExecutionException을 받게 됩니다.

 

 

3) 팩토리 메서드 supplyAsync로 CompletableFuture 만들기

 

위 CompletableFuture를 만드는 코드가 매우 긴게 가독성이 좋지 않습니다.

 

이를 위해, CompletableFuture는 팩터리 메서드로 supplyAsync를 제공합니다.

 

위 getPriceAsync 메서드를 팩터리 메서드 supplyAsync를 적용한 코드는 아래와 같습니다.

 

public Future<Double> getPriceAsync(String product) {
   return CompletableFuture.supplyAsync(() -> calculatePrice(product));
}

 

이 방법은 위 에러처리 방법에서 본 completeExceptionally 까지 포함한 메서드입니다.

 

 

 

반응형

 

 

4. 비블록 코드 만들기

이제 위 예제를 좀 더 확장하여 여러 가게에서 가격정보를 가져와 노출하는 경우를 추가하겠습니다.

 

코드는 아래와 같습니다.

 

public class ShopTest {

    private static List<Shop> shopList = Arrays.asList(
            new Shop("BestPrice"),
            new Shop("LetsSaveBig"),
            new Shop("MyFavoriteShp["),
            new Shop("BuyItAll")
    );

    public static void main(String[] args) {
        long start = System.nanoTime();
        System.out.println(findPrices("myPhone27S"));
        long duration = (System.nanoTime() - start) / 1000000;
        System.out.println("Done in " + duration + "msecs");
    }

    public static List<String> findPrices(String product) {
        return shopList.stream()
                .map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
                .collect(Collectors.toList());
    }
}

 

위 예제는 각 shop 마다 delay가 있어 최소 4초 이상의 시간이 걸립니다.

 

 

1) 병렬 스트림으로 요청 병렬화 하기

 

 

위 예제를 앞장에서 배웠던 병렬 스트림을 통해서 성능을 개선시킬 수 있습니다.

 

코드는 아래와 같습니다.

 

public static List<String> findPrices(String product) {
    return shopList.parallelStream()
        .map(shop -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product)))
        .collect(Collectors.toList());
}

 

위와 같이 병렬로 변경 시, 작업은 대략 1/4로 줄어듭니다.

 

2) CompletableFuture로 비동기 호출 구현하기

 

위 병렬 스트림으로 성능이 개선되었지만 비동기를 입혀 블로킹까지 없애 성능을 더욱 올리는게 바람직합니다.

병렬스트림으로 처리하더라도, 위와 같은 경우 한 쓰레드가 shop을 2개 이상 가지게 된다면 블로킹은 여전히 존재합니다.

 

코드는 아래와 같습니다.

 

public static List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shopList.stream()
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))
            ))
            .collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
}

 

 

3) 커스텀 Executor 사용하기

 

위 병렬스트림과 CompletableFuture 를 적용한 성능은 내부적으로 Runtime.getRuntime().availableProcessors() 가 반환하는 스레드 수로 동작합니다.

 

따라서, 쓰레드 풀을 직접 생성하여 동작시킨다면 CompletableFuture를 이용한 비동기 프로그래밍은 더욱 유연해지고 성능 향상이 일어나게 됩니다.

 

아래는 예제입니다.

 

public static List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shopList.stream()
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> String.format("%s price is %.2f", shop.getName(), shop.getPrice(product))
			, executor
            ))
            .collect(Collectors.toList());
    return priceFutures.stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
}

 

간단히 비동기 처리에 쓸 Executor를 두번째 인자로 추가만 해주면 됩니다.

 

5. 비동기 작업 파이프라인 만들기

위 예제에서 할인율을 더해 출력하는 요구사항을 추가해보도록 하겠습니다.

 

아래는 getPrice 메서드를 shop 이름, 가격, DisCount 정보를 가진 문자열로 반환하도록 변경한 예제입니다.

 

public class Discount {
    
    public enum Code {
        NONE(0),
        SILVER(5),
        GOLD(10),
        PLATINUM(15),
        DIAMOND(20)
        ;
        
        private final int percentage;

        Code(int percentage) {
            this.percentage = percentage;
        }
    }
}

 

public String getPrice(String product) {
    double price = calculatePrice(product);
    Discount.Code code = Discount.Code.values()[random.nextInt(Discount.Code.values().length)];
    return String.format("%s:%.2f:%s", name, price, code);
}

 

1) 할인 서비스 구현

 

위 getPrice의 문자열을 파싱하여 사용하는 클래스는 아래 Quote로 정의하였습니다.

추가로, 이제 Quote 객체를 Discount에 넘겨 할인이 적용된 값을 반환하는 메서드를 추가했습니다.

 

public class Quote {

    private final String shopName;
    private final double price;
    private final Discount.Code code;

    public Quote(String shopName, double price, Discount.Code code) {
        this.shopName = shopName;
        this.price = price;
        this.code = code;
    }

    public static Quote parse(String s) {
        String[] split = s.split(":");
        
        String shopName = split[0];
        double price = Double.parseDouble(split[1]);
        Discount.Code code = Discount.Code.valueOf(split[2]);
        return new Quote(shopName, price, code);
    }

    public String getShopName() {
        return shopName;
    }

    public double getPrice() {
        return price;
    }

    public Discount.Code getCode() {
        return code;
    }
}

 

public class Discount {

    public enum Code {
        NONE(0),
        SILVER(5),
        GOLD(10),
        PLATINUM(15),
        DIAMOND(20)
        ;

        private final int percentage;

        Code(int percentage) {
            this.percentage = percentage;
        }
    }
    
    public static String applyDiscount(Quote quote) {
        return quote.getShopName() + "price is " + 
                Discount.apply(quote.getPrice(), quote.getCode());
    }
    
    private static double apply(double price, Code code) {
        delay();
        return price * (100 - code.percentage) / 100;
    } 
}

 

2) 할인 서비스 사용

 

이제 위 추가된 내용을 사용하기에 가장 쉬운 방법은 아래와 같이 stream으로 처리하는 것입니다.

 

public static List<String> findPrices(String product) {
    return shopList.stream()
        .map(shop -> shop.getPrice(product))
        .map(Quote::parse)
        .map(Discount::applyDiscount)
        .collect(Collectors.toList());
}

 

위 코드는 처리만 할 뿐 성능 최적화와는 거리가 멀게 됩니다.

 

 

2) 동기 작업과 비동기 작업 조합하기

 

위 코드를 비동기를 이용한 코드로 바꿔보겠습니다.

 

예제는 아래와 같습니다.

public static List<String> findPrices(String product) {
    List<CompletableFuture<String>> priceFutures = shopList.stream()
            .map(shop -> CompletableFuture.supplyAsync(
                    () -> shop.getPrice(product), executor)
            )
            .map(future -> future.thenApply(Quote::parse))
            .map(future -> future.thenCompose(
                    quote -> CompletableFuture.supplyAsync(
                            () -> Discount.applyDiscount(quote), executor
                    )
                )
            )
            .collect(Collectors.toList());
    
    return priceFutures.stream().map(CompletableFuture::join)
            .collect(Collectors.toList());
}

 

위 코드에서 thenApply 는 CompletableFuture가 끝날때까지 블록하지 않습니다.

 

따라서 블로킹 없이 CompletableFuture<String> 에서 CompletableFuture<Quote>로 변환됩니다.

 

추가로, thenCompose 메서드는 첫번째 연산의 결과를 두번째 연산으로 전달하는 메서드입니다.

위에서는 quote의 결과를 받으면 Discount.applyDiscount 메서드를 한번 더 비동기로 수행시키는 코드입니다.

 

 

3) 독립 CompletableFuture 와 비독립 CompletableFuture 합치기

 

개발을 하다보면 두개의 독립적인 비동기 작업을 합쳐야 하는 경우가 있습니다.

이런 경우 thenCombine 메서드를 활용하시면 됩니다.

 

예제는 아래와 같습니다.

 

Future<Double> futurePriceInUSD = 
        CompletableFuture.supplyAsync(() -> shop.getPrice(prduct))
        .thenCombine(CompletableFuture.supplyAsync(() -> exchangeService.getRate(Money.EUR, Money.USD)), (price, rate) -> price * rate);

 

아래는 두 Future를 합치는 작업을 도식화 한것입니다.

 

 

출처 : 모던 자바 인 액션

 

 

4) 타임아웃 효과적으로 사용하기

 

Future의 단점으로는 계산이 길어지는 경우 무한정 대기할 수도 있다는 부분입니다.

 

CompletableFuture에서는 이를 위해 orTimeout 메서드를 제공합니다.

 

아래는 예제입니다.

 

Future<Double> futurePriceInUSD = 
        CompletableFuture.supplyAsync(() -> shop.getPrice(prduct))
        .thenCombine(CompletableFuture.supplyAsync(() -> exchangeService.getRate(Money.EUR, Money.USD)), (price, rate) -> price * rate)
        .orTimeout(3, TimeUnit.SECONDS);

 

 

6. CompletableFuture의 종료에 대응하는 방법

CompletableFuture는 thenAccept라는 메서드를 제공합니다.

 

이 메서드는 연산 결과를 소비하는 Consumer를 인수로 받아 사용합니다.

 

아래는 예제입니다.

 

CompletableFuture[] futures = shopList.stream()
    .map(shop -> CompletableFuture.supplyAsync(
            () -> shop.getPrice(product), executor)
    )
    .map(future -> future.thenApply(Quote::parse))
    .map(future -> future.thenCompose(
            quote -> CompletableFuture.supplyAsync(
                    () -> Discount.applyDiscount(quote), executor
            )
        )
    )
    .map(f -> f.thenAccept(System.out::println))
    .toArray(size -> new CompletableFuture[size]);

 

7. 마무리

이번 포스팅에서는 Chapter16  CompletableFuture : 안정적 비동기 프로그래밍 대해 진행하였습니다.

다음에는 Chapter17 리액티브 프로그래밍에 대해 포스팅하겠습니다.

반응형
반응형

1. 서론

 

이번 포스팅에서는 Chapter15의 CompletableFuture와 리액티브 프로그래밍의 컨셉의 기초에 대해 진행하도록 하겠습니다.

 

2. 동시성을 구현하는 자바 지원의 진화

초기 자바는 Runnable과 Thread를 동기화된 클래스와 메서드를 이용해 잠갔습니다.

 

그 후, 자바 5에서는 ExecutorService 인터페이스를 제공하며 스레드 실행과 태스크를 분리하였고,

값을 반환하는 Callable도 ExecutorService로 사용할 수 있게 되었습니다.

ExecutorService는 Runnable, Callable 둘 다 인자로 받을 수 있습니다.

 

자바 8에서는 Future의 진화 단계인 CompletableFuture를 제공, 자바 9에서는 발행-구독 메커니즘을 위한 Flow를 제공하게 되었습니다.

 

CompletableFuture, Flow 의 제공 목표는 가능한한 동시에 블록킹 되지 않게 실행할 수 있도록 제공하기 위함입니다.

 

 

1) Executor와 쓰레드 풀

 

쓰레드의 문제

 

쓰레드는 자바 뿐만이 아닌 하드웨어, 운영체제에서도 최대 사용가능한 갯수가 있습니다.

 

만약 자바 어플리케이션의 쓰레드가 운영체제가 지원하는 쓰레드 수를 초과하면 에러가 발생 할 수 있습니다.

 

쓰레드 풀 그리고 쓰레드 풀이 더 좋은 이유

 

쓰레드 풀은 쓰레드를 계속 생성하는 것이 아니라 사용이 끝나면 반환하게 하여 재사용을 할 수 있도록 하는 방법입니다.

 

장점은 위의 쓰레드 문제같이 에러 나는 경우를 예방할 수 있으며, 쓰레드를 재사용할 수 있다는 점입니다.

 

쓰레드 풀 그리고 쓰레드 풀이 나쁜 이유

 

쓰레드 풀이 장점만 있는 것은 아닙니다.

 

쓰레드 풀을 사용 시 유휴상태의 쓰레드가 풀에 없다면 태스크는 블록킹되기 때문입니다.

 

3. 동기 API와 비동기 API

동기 API는 쓰레드가 CPU 자원을 점유한 상태로 아무런 일을 하지 않는 상황을 만듭니다.

 

이러한 동기 API의 문제점을 아래 예제 코드를 통해 보겠습니다.

 

public class ThreadExample {

    public static void main(String[] args) throws InterruptedException {
        
        int x = 1337;

        Result result = new Result();
        
        Thread t1 = new Thread(() -> {result.left = f(x);});
        Thread t2 = new Thread(() -> {result.right = g(x);});
        
        t1.start();
        t2.start();
        
        t1.join();
        t2.join();
        System.out.println(result.left + result.right);
    }
    
    private static class Result {
        private int left;
        private int right;
    }
}

 

위 예제는 f, g 라는 연산을 수행 후 두 결과를 합쳐 출력하는 예제입니다.

 

쓰레드를 2개 만들어 사용하지만 join 메서드로 인해 이들은 동시성이라고 할 수 없습니다.

 

 

1) Future 형식 API

 

위 예제를 Future 를 사용한다면 조금 개선이 될 수 있습니다.

 

Future는 태스크의 작업을 비동기로 쓰레드에서 수행하도록 합니다.

 

아래는 Future를 적용한 예제입니다.

 

int x = 1337;
ExecutorService executorService = Executors.newFixedThreadPool(2);
Future<Integer> y = executorService.submit(() -> f(x));
Future<Integer> z = executorService.submit(() -> g(x));

System.out.println(y.get()  + z.get());

 

2) 리액티브 형식 API

 

위 문제에서 Future 를 적용하였더라도 get() 메서드는 블로킹 되기 때문에 완벽한 해결책은 아닙니다.

 

리액티브 형식으로는 처리가 완료 되었을때의 콜백함수를 인자로 넣어 사용하는 것입니다.

 

아래는 예제입니다.

 

int x = 1337;

Result result = new Result();

f(x, (int y) -> {
    result.left = y;
    System.out.println((result.left + result.right));
});

g(x, (int z) -> {
    result.right = z;
    System.out.println((result.left + result.right));
});

 

 

위 같이 수행한다면 블록킹 되는 부분은 없을 겁니다.

하지만, f 와 g의 합계가 정확하게 출력되지 않으며 2번이나 출력이 될 것입니다.

 

이런 경우, 일반적으로 if-then-else 를 이용하여 해결 할 수 있습니다.

 

 

3) 잠자기(그리고 기타 블로캉 동작)는 해로운 것으로 간주

 

블로킹 코드가 있다면 쓰레드는 자원을 점유한 채 아무일도 하지 않을 것입니다.

 

이러한, 작업들이 조금씩 쌓이게되면 프로그램 전체에 영향이 가게 됩니다.

 

그러므로, 블로킹 동작은 최대한 배제해야하며 해로운 것으로 간주해야 합니다.

 

 

4) 비동기 API에서 예외는 어떻게 처리하는가?

 

비동기 API에서 호출된 메서드는 별도의 쓰레드에서 수행되기 때문에 호출자에서 예외를 핸들링 할 수 없습니다.

 

때문에, CompletableFuture에서는 런타임 get() 메서드에 예외를 처리할 수 있는 기능을 제공하였고,

exceptionally() 로 예외에서 회복할 수 있는 메서드도 제공하였습니다.

 

리액티브 형식에서는 값을 콜백형식으로 처리하기 때문에 예외 또한 콜백으로 처리하게됩니다.

 

플로를 예로 든다면 아래와 같이 예외에 대한 콜백 함수가 있습니다.

 

void onError(Throwable throwable)

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

4. CompletableFuture와 콤비네이터를 이용한 동시성

CompletableFuture는 complete 메서드를 통해 나중에 어떤 값을 이용해 다른 쓰레드가 이를 완료할 수 있게 허용합니다.

 

아래는 위 예제를  CompletableFuture를 이용한 예제로 바꾼 코드입니다.

 

ExecutorService executorService = Executors.newFixedThreadPool(2);
int x = 1337;

CompletableFuture<Integer> a = new CompletableFuture<>();
executorService.submit(() -> a.complete(f(x)));
int b = g(x);
System.out.println(a.get() + b);

 

위 예제의 경우 CompletableFuture를 사용하기만 했지 Future로 했을때처럼 블로킹은 존재합니다.

 

CompletableFuture에서는 이러한 문제를 해결하기 위해 thenCombine 메서드를 제공합니다.

 

아래는 예제입니다.

 

ExecutorService executorService = Executors.newFixedThreadPool(10);
int x = 1337;

CompletableFuture<Integer> a = new CompletableFuture<>();
CompletableFuture<Integer> b = new CompletableFuture<>();
CompletableFuture<Integer> c = a.thenCombine(b, (y, z) -> y + z);
executorService.submit(() -> a.complete(f(x)));
executorService.submit(() -> b.complete(g(x)));

System.out.println(c.get());

 

c의 경우 a와 b의 결과가 반환되기 전까지 쓰레드로 수행되지 않습니다.

 

때문에, 이제 블로킹이 없는 코드가 완성되었습니다.

 

5. 발행-구독 그리고 리액티브 프로그래밍

Future는 한번만 실행하여 결과를 제공합니다.

반면, 리액티브 프로그래밍은 시간이 흐르면서 여러 Future 같은 객체를 통해 여러 결과를 제공합니다.

 

자바 9에서는 java.util.concurrent.Flow 인터페이스에 발행-구독 모델을 적용하여 리액티브 프로그래밍을 제공합니다.

 

플로 API는 아래와 같이 3가지로 정리할 수 있습니다.

 

  1. 구독자가 구독할 수 있는 발행자
  2. 이 연결을 구독이라 합니다.
  3. 이 연결을 이용해 메시지 또는 이벤트를 전송합니다.

 

1) 발행 구독 예제

 

아래는 발행-구독의 간단한 예제 입니다.

 

public class SimpleCell implements Publisher<Integer>, Subscriber<Integer> {
    
    private int value = 0;
    private String name;
    private List<Subscriber> subscriberList = new ArrayList<>();

    public SimpleCell(String name) {
        this.name = name;
    }

    @Override
    public void subscriber(Subscriber<? super Integer> subscriber) {
        subscriberList.add(subscriber);
    }
    
    private void notifyAllSubscribers() {
        subscriberList.forEach(subscriber -> subscriber.onNext(this.value));
    }

    @Override
    public void onNext(Integer newValue) {
        this.value = newValue;
        System.out.println(this.name + ":" + this.value);
        notifyAllSubscribers();
    }
}

 

public class FlowTest {

    public static void main(String[] args) {
        SimpleCell c3 = new SimpleCell("C3");
        SimpleCell c2 = new SimpleCell("C2");
        SimpleCell c1 = new SimpleCell("C1");

        c1.subscriber(c3);

        c1.onNext(10);
        c1.onNext(20);
    }
}

 

위 예제의 결과는 아래와 같이 출력됩니다.

 

C1 : 10
C3 : 10
C2 : 20

 

2) 역압력

 

발행-구독 모델에서는 구독자가 처리할 수 있는 양에 비해 발행자가 무수히 많은 데이터를 전달할 수 있으며, 이러한 상황을 압력이라고 합니다.

 

압력 현상이 발생 시 구독자는 처리할 양만 늘어나며 나중에는 많은 작업으로 어떤 문제가 발생할 수 있을지 모릅니다.

때문에, 구독자가 처리할 수 있을때만 발행자에게 데이터를 전달받도록 조절하는 것이 필요하며, 이를 역압력이라고 합니다.

 

플로 API에서는 Subscription을 통해 이 역압력을 지원합니다.

 

아래는 Subscription 입니다.

 

public static interface Subscription {
    public void request(long n);
    public void cancel();
}

 

Subscription 은 발행자와 구독자의 중간에서 소통을 해주는 역할로 이해하시면 됩니다.

 

아래는 Publisher, Subscriber, Subscription에 대해 그림으로 나타낸 것입니다.

 

 

6. 마무리

 

이번 포스팅에서는 Chapter15 CompletableFuture와 리액티브 프로그래밍의 컨셉의 기초에 대해 진행하였습니다.

다음에는 Chapter16 CompletableFuture : 안정적 비동기 프로그래밍에  대해 포스팅하겠습니다.

반응형
반응형

1. 서론

이번 포스팅에서는 Chapter14의 자바 모듈 시스템에 대해 진행하도록 하겠습니다.

 

2. 압력 : 소프트웨어 유추

일반적으로 자바 언어는 유지보수가 쉬운 언어라고 합니다.

 

그 이유는 관심사 분리, 정보 은닉의 특징을 제공하기 때문입니다.

 

1) 관심사 분리

 

관심사 분리란 프로그램을 고유의 기능으로 나누는 동작을 권장하는 원칙입니다.

 

관심사 분리의 장점은 아래와 같습니다.

 

  1. 개별 기능을 따로 작업할 수 있으므로 팀이 쉽게 협업할 수 있습니다.
  2. 개별 부분을 재사용하기 쉽습니다.
  3. 전체 시스템을 쉽게 유지보수할 수 있습니다.

이러한 관심사 분리를 도울도록 자바 8에서는 모듈 시스템을 제공하였습니다.

 

2) 정보 은닉

 

정보 은닉은 어떤 부분을 변경하였을 때, 다른 부분까지 영향을 미칠 가능성을 줄일 수 있게 합니다.

 

3. 자바 모듈 시스템을 설계한 이유

 

1) 모듈화의 한계

 

자바 9 이전에는 모듈화된 소프트웨어 프로젝트를 만드는데 한계가 있었습니다.

 

자바는 클래스, 패키지, jar 세 가지 수준의 코드 그룹화를 제공합니다.

하지만 패키지와 jar 수준에서 캡슐화를 거의 지원하지 못했습니다.

 

제한된 가시성 제어

 

자바는 클래스 단위에서는 private, protected, public 으로 가시성을 제공합니다.

하지만 패키지 단위에서는 제공하고 있지 않는것이 문제가 되었습니다.

클래스는 다르고, 같은 패키지에서만 공개하여 사용하고 싶은 경우
다른 패키지에서도 사용이 가능한 public으로 해야하기 때문입니다.

 

클래스 경로

 

자바에는 태생적으로 클래스 경로와 JAR 조합에 약점을 가지고 있습니다.

 

  1. 클래스 경로에는 같은 클래스를 구분하는 버전 개념이 없습니다.
  2. 클래스 경로는 명시적인 의존성을 지원하지 않습니다.
    1. 한 JAR가 다른 JAR에 포함된 클래스 집합을 사용하라는 명시적인 의존성을 지원하지 않는다는 의미입니다.

 

 

반응형

 

 

 

4. 자바 모듈 큰 그림

자바 8에서는 모듈이라는 새로운 구조 단위를 제공합니다.

 

모듈 디스크립터는 module-info.java 라는 특별한 파일에 저장되고, 보통 패키지와 같은 폴더에 위치합니다.

 

아래는 자바 모듈 디스크립터의 구조를 도식화 한것입니다.

 

 

5. 여러 모듈 활용하기

 

1) exports 구문

 

exports는 패키지 단위로 다른 모듈에서도 사용할 수 있도록 공개하는 키워드입니다.

 

아래는 한 모듈의 exports 예제입니다.

 

module expenses.readers {
    exports com.example.expenses.readers;
    exports com.example.expenses.readers.file;
    exports com.example.expenses.readers.http;
}

 

2) requires 구문

 

requires는 의존하고 있는 모듈을 지정하는 키워드입니다.

 

기본적으로 모든 모듈은 java.base 라는 모듈을 의존하고 있습니다.

때문에, 이 java.base는 생략가능하며, java.base 가 아닌 모듈을 의존하는 경우에 requires를 사용하면 됩니다.

 

3) 이름 정하기

 

모듈명의 경우 자바 8에서 새롭게 나온 개념으로 이름을 정하는 것에 대한 정확한 규칙이 없습니다.

다만, 오라클에서는 패키지명처럼 인터넷 도메인명을 역순으로 모듈의 이름을 정하도록 추천하고 있습니다.

 

6. 모듈 정의와 구문들

모듈 정의는 module-info.java 파일에 해야하며, 이 파일은 src/main/java 디렉터리에 있어야 합니다.

 

정의할때 사용하는 구문들은 위에서 살펴본 requires, exports 말고도,  requires-transitive, exports-to, open, opens, uses, provides 구문들이 있습니다.

 

1)  requires-transitive

 

이 구문은 다른 모듈이 제공하는 공개 형식을 한 모듈에서 사용할 수 있다고 지정할 수 있습니다.

 

아래는 예제입니다.

 

module com.iteratrlearning.ui {
    requires transitive com.iteratrlearning.core;

    exports com.iteratrlearning.ui.panels;
    exports com.iteratrlearning.ui.widgets;
}

module com.iteratrlearning.application {
    requires com.iteratrlearning.ui
}

 

결과적으로 application 모듈은 core에서 제공하는 공개 형식에 접근할 수 있습니다.

 

 

2) exports to

 

exports to 구문은 사용자에게 공개할 기능을 제한함으로 가시성을 좀 더 정교하게 제어할 수 있습니다.

 

아래는 예제 입니다.

 

module com.iteratrlearning.ui {
    requires com.iteratrlearning.core;

    exports com.iteratrlearning.ui.panels;
    exports com.iteratrlearning.ui.widgets to
            com.iteratrlearning.ui.widgetuser;
}

 

위 예제는 com.iteratrlearning.ui.widgets 에 접근 권한을 가진 사용지의 권한을 com.iteratrlearning.ui.widgetuser로 제한합니다.

 

 

3) open 과 opens

 

모듈 선언에 open 키워드를 사용하면 모든 패키지를 다른 모듈에 반사적으로 접근을 허용할 수 있습니다.

 

전체 모듈을 개발하지 않고도 opens 구문을 모듈 선언에 이용해 필요한 개별 패키지만 개방할 수 있습니다.

 

7. 마무리

 

이번 포스팅에서는 Chapter14 자바 모듈 시스템에 대해 진행하였습니다.

다음에는 Chapter15 CompletableFuture와 리액티브 프로그래밍의 컨셉의 기초에  대해 포스팅하겠습니다.

반응형
반응형

1. 서론

이번 포스팅에서는 Chapter13의 디폴트 메서드에 대해 진행하도록 하겠습니다.

 

2. 변화하는 API

인터페이스에 새로운 메서드를 추가하는 등 인터페이스를 바꾸고 싶을때는 문제가 발생합니다. 

 

변경 인터페이스를 상속하고 있는 클래스들에 모두 영향이 가기 때문이죠.

 

이러한 문제점을 java에서는 디폴트 메서드라는 개념을 도입하여  해결하였습니다.

 

디폴트 메서드가 왜 필요한지 예제를 통해 알아보겠습니다.

 

처음 아래처럼 Resizable 인터페이스를 만들어 제공하였습니다.

 

public interface Resizable {
    int getWidth();
    int getHeight();
    void setWidth();
    void setHeight();
    void setAbsoluteSize(int width, int height);
}

public class Ellipse implements Resizable {
	...
}

 

시간이 지나 요구사항을 처리하다보니 Resizable에 setRelativeSize(int wFactor, int hFactor) 가 필요함을 깨달았습니다.

 

이때 바로 문제가 생깁니다.

 

Resizable를 구현한 클래스들은 모두 setRelativeSize 메서드를 구현해야 하기 때문입니다.

 

3. 디폴트 메서드란 무엇인가?

디폴트 메서드는 인터페이스에 구현 메서드를 놓을 수 있는 새로운 시그니쳐입니다.

 

이를 통해, 위와같은 문제점을 해결할 수 있습니다.

 

사용법으로는 default 라는 키워드로 메서드를 정의하면 됩니다.

 

아래는 디폴트 메서드를 사용한 예제입니다.

 

public interface Sized {
    int size();
    default boolean isEmpty() {
        return size() == 0;
    }
}

 

 

 

 

 

 

 

반응형

 

 

 

 

 

 

 

4. 디폴트 메서드 활용 패턴

디폴트 메서드를 활용하는 방식으로는 두가지가 있습니다.

 

  • 선택형 메서드
  • 동작 다중 상속

 

1) 선택형 메서드

 

인터페이스에는 간혹 구현 클래스에서 크게 중요하지 않은 메서드를 정의할때가 있습니다.

 

그로인해, 구현 클래스들은 메서드 오버라이드만 할 뿐 실제 바디에는 내용이 없게 하는 코드들이 생기게 됩니다.

 

하지만 디폴트 메서드를 사용하면 기본 메서드 구현을 인터페이스에 상주할 수 있어 이러한 문제를 해결하게 됩니다.

 

2) 동작 다중 상속

 

디폴트 메서드를 통해 여러 인터페이스를 상속하고 있는 클래스들은 사용할 수 있는 동작이 풍부해집니다.

 

아래는 코드로 해당 이점을 보여주는 예제입니다.

 

public interface Rotatable {
    void setRotationAngle(int angleInDegrees);
    int getRotationAngle();
    default void rotateBy(int angleInDegrees) {
        setRotationAngle(getRotationAngle() + angleInDegrees);
    }
}

public interface Resizable {
    int getWidth();
    int getHeight();
    void setWidth();
    void setHeight();
    void setAbsoluteSize(int width, int height);
    default void setRelativeSize(int wFactor, int hFactor) {
        setAbsoluteSize(getWidth() / wFactor, getHeight() / hFactor);
    }
}

public interface Moveable {
    int getX();
    int getY();
    void setX(int x);
    void setY(int y);
    default void moveHorizontally(int distance) {
        setX(getX() + distance);
    }

    default void moveVertically(int distance) {
        setY(getY() + distance);
    }
}

public class Moster implements Rotatable, Moveable, Resizable {

}

 

5. 해석 규칙

디폴트 메서드로 인해 개발자에게 장점만 제공된것은 아닙니다.

 

단점으로는 같은 시그니처를 갖는 디폴트 메서드를 상속 받는 상황이 생길 수 있다는 것입니다.

 

자바에서는 이러한 경우를 대비해 아래와 같은 3가지 규칙을 세워 메서드의 상속 우선순위를 가지도록 했습니다.

 

  1. 클래스가 항상 이긴다. 클래스나 슈퍼클래스에서 정의한 메서드가 디폴트 메서드보다 우선권을 갖습니다.
  2. 1번 다음으로는 서브 인터페이스가 이깁니다.
  3. 1번과 2번과 같은 상황이 없고, 디폴트 메서드에 대해 순위가 정해지지 않은 경우에는 상속 클래스에서 명시적으로 디폴트 메서드를 오버라이드하여 호출해야 합니다.

 

6. 마무리

이번 포스팅에서는 Chapter13 디폴트 메서드 대해 진행하였습니다.

다음에는 Chapter14 자바 모듈 시스템에 대해 포스팅하겠습니다.

반응형
반응형

1. 서론

 

이번 포스팅에서는 Chapter12의 새로운 날짜와 시간 API에 대해 진행하도록 하겠습니다.

 

2. LocalDate, LocalTime, Instant, Duration, Period 클래스

java.time 패키지에는 LocalDate, LocalTime, LocalDateTime, Instant, Duration, Period 등의 새로운 클래스를 제공합니다.

 

1) LocalDate 와 LocalTime 사용

 

LocalDate 인스턴스는 시간을 제외한 날짜를 표현하는 불변 객체입니다.

 

LocalDate는 팩토리 메서드 of를 통해 생성이 가능합니다.

 

아래는 2017년 9월 21일의 LocalDate를 만들어 사용하는 예제입니다.

 

LocalDate date = LocalDate.of(2017, 9, 21);
int year = date.getYear(); // 2017
Month month = date.getMonth(); // 9월
int day = date.getDayOfMonth(); // 21
DayOfWeek dow = date.getDayOfWeek(); // 목요일 
int len = date.lengthOfMonth(); // 31 (월의 일 수)
boolean deal = date.isLeapYear(); // false (윤년 유무)

 

현재 날짜를 구할때는 팩토리 메서드 now를 사용하면 됩니다.

아래는 예제입니다.

 

LocalDate today = LocalDate.now();

 

아래는 LocalDate 객체에서 값을 얻는 또 다른 방법입니다.

 

int year = date.get(ChronoField.YEAR);
int month = date.get(ChronoField.MONTH_OF_YEAR);
int day = date.get(ChronoField.DAY_OF_MONTH);

 

 

LocalTime은 날짜가 아닌 시간을 제공하는 클래스입니다.

 

LocalTime 역시 of 메서드를 통해 생성이 가능하며, (시, 분), (시, 분, 초) 각각 인수로 받아 생성 가능하도록 오버로드 되어 있습니다.

 

아래는 13시 24분 50초 를 만드는 예제 입니다.

 

LocalTime time = LocalTime.of(13, 45, 20);
int hour = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();

 

날짜 혹은 시간의 정보를 담고있는 문자열에서 LocalDate, LocalTime으로 만들고 싶은 경우가 있습니다.

이를 위해, LocalDate, LocalTime 은 parse 메서드를 제공합니다.

 

아래는 예제 입니다.

 

LocalDate date = LocalDate.parse("2017-09-21");
LocalTime time = LocalTime.parse("13:45:20");

 

2) 날짜와 시간 조합

 

LocalDateTime은 날짜와 시간 정보를 모두 가지고 있는 클래스입니다.

 

아래는 LocalDateTime 을 만드는 예제입니다.

 

LocalDateTime dt1 = LocalDateTime.of(2017, Month.SEPTEMBER, 21, 13, 45, 20);
LocalDateTime dt2 = LocalDateTime.of(date, time);
LocalDateTime dt3 = date.atTime(13 ,45 ,20);
LocalDateTime dt4 = date.atTime(time);
LocalDateTime dt5 = time.atDate(date);

 

반대로 LocalDateTime 에서 LocalDate, LocalTime 만을 빼내고 싶은 경우에는 아래와 같이 toLocalDate, toLocalTime 메서드를 사용하면 됩니다.

 

LocalDate date1 = dt1.toLocalDate();
LocalTime time1 = dt1.toLocalTime();

 

 

3) Instant 클래스 : 기계의 날짜와 시간

 

Instant 클래스는 유닉스 에포크 시간을 기준으로 특정 지점까지의 시간을 초로 표현합니다.

또한, 이 클래스는 나노초의 정밀도를 제공합니다.

 

아래는 Instant 인스턴스를 만드는 예제입니다.

 

Instant.ofEpochSecond(3);
Instant.ofEpochSecond(3, 0);
Instant.ofEpochSecond(2, 1000000000);
Instant.ofEpochSecond(4, -1000000000);

 

Instant 클래스 역시 정적 메서드로 now를 제공합니다.

 

Instant는 초, 나노초 정보를 가지고 있기 때문에 LocalDate, LocalTime와 같이 사람이 읽을 수 있는 정보를 제공하지 않습니다.

 

대신 Period와 Duration와는 함께 활용할 수 있습니다.

 

4) Duration 과 Period 정의

 

Duration은 두 시간 차의 정보를 가지고 있는 클래스입니다.

 

아래는 Duration을 생성하는 예제입니다. 생성시에는 LocalTime, LocalDateTime, Instant를 활용할 수 있습니다.

 

Duration d1 = Duration.between(time1, time2);
Duration d2 = Duration.between(dateTime1, dateTime2);
Duration d3 = Duration.between(instant1, instant2);

 

Period는 시간이 아닌 두 날짜 차의 정보를 가지고 있는 클래스입니다.

 

아래는 Period를 생성하는 예제입니다.

 

Period tenDays = Period.between(LocalDate.of(2017, 9, 11), LocalDate.of(2017, 9, 21));

 

Duration과 Period는 자체적으로도 생성이 가능하도록 팩토리 메서드들을 제공합니다.

 

아래는 예제입니다.

 

Period tenDays = Period.ofDays(10);
Period threeWeeks = Period.ofWeeks(3);
Period twoYearsSixMonthsOneDay = Period.of(2, 6, 1);

 

지금까지 알아본 클래스들은 모두 불변 객체로 생성하여 제공합니다.

 

 

 

반응형

 

 

 

3. 날짜 조정, 파싱, 포매팅

기존 날짜, 시간 데이터를 절대적으로 변경하고 싶은 경우가 있습니다.

 

이런 경우 with 관련 메서드를 통해 가능합니다.

with 메서드를 사용한다고 기존 객체의 값이 변하는것이 아니며, 새로운 객체에 값만을 바꿔 제공합니다.
이는 불변 객체라는 특징을 제공하기 위해서 입니다.

 

아래는 예제입니다.

 

LocalDate date1 = LocalDate.of(2017, 9 , 21);
LocalDate date2 = date1.withYear(2011); // 2011-09-21
LocalDate date3 = date2.withDayOfMonth(25); // 2011-09-25
LocalDate date4 = date3.with(ChronoField.MONTH_OF_YEAR, 2); // 2011-02-25

 

 

상대적인 방법으로도 변경도 가능합니다.

 

아래는 예제입니다.

 

LocalDate date1 = LocalDate.of(2017, 9, 21);
LocalDate date2 = date1.plusWeeks(1);
LocalDate date3 = date2.minusYears(6);
LocalDate date4 = date3.plus(6, ChronoUnit.MONTHS);

 

1) TemporalAdjusters 사용하기

 

복잡한 날짜 조정기능이 필요한 경우 with 메서드에 TemporalAdjusters 를 사용하여 해결할 수 있습니다.

 

아래는 TemporalAdjusters를 사용하는 한 예제입니다.

 

LocalDate date1 = LocalDate.of(2014, 3, 18); // 2014-03-18
LocalDate date2 = date1.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)); // 2014-03-23
LocalDate date3 = date2.with(TemporalAdjusters.lastDayOfMonth()); // 2014-03-31

 

TemporalAdjusters 에서 제공하는 메서드가 없는 경우에는 TemporalAdjuster 함수형 인터페이스를 구현하여 사용하면 됩니다.

 

아래는 TemporalAdjuster 입니다.

 

@FunctionalInterface
public interface TemporalAdjuster {
    Temporal adjustInto(Temporal temporal);
}

 

 

2) 날짜와 시간 객체 출력과 파싱

 

날짜와 시간 관련에서는 포매팅과 파싱은 서로 떨어질 수 없는 관계입니다.

 

때문에, java에서는 DateTimeFormatter를 제공하여 손쉽게 날짜나 시간을 특정 형식의 문자열로 만들어 줍니다.

 

아래는 예제입니다.

 

LocalDate date = LocalDate.of(2014, 3, 18);
String s1 = date.format(DateTimeFormatter.BASIC_ISO_DATE);
String s2 = date.format(DateTimeFormatter.ISO_LOCAL_DATE);

 

반대로 문자열에서 날짜나 시간으로도 변환시 사용할 수도 있습니다.

 

아래는 예제입니다.

 

LocalDate date1 = LocalDate.parse("20140318", DateTimeFormatter.BASIC_ISO_DATE);
LocalDate date2 = LocalDate.parse("2014-03-18", DateTimeFormatter.ISO_LOCAL_DATE);

 

DateTimeFormatter 은 기존의 java.util.DateFormat과 달리 쓰레드에 안전합니다.

 

추가로 DateTimeFormatter 은 특정 패턴으로도 포매팅을 제공합니다.

 

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
LocalDate date1 = LocalDate.of(2014, 3, 18);
String formattedDate = date1.format(formatter);
LocalDate date2 = LocalDate.parse(formattedDate, formatter);

 

좀 더 복합적인 포매팅을 원할시에는 DateTimeFormatterBuilder 를 사용하면 됩니다.

 

4. 마무리

이번 포스팅에서는 Chapter12 새로운 날짜와 시간 API에 대해 진행하였습니다.

다음에는 Chapter13 디폴트 메서드에 대해 포스팅하겠습니다.

반응형

+ Recent posts