<개요>

1. Aurora

Amazon에서 Full Managed Service로 제공하고 있는 RDB이다. MySQL, PostgreSQL과 호환되며 속도도 기존 MySQL, PostgreSQL보다 빠르도록 개선되었다. 

일반적인 CRUD용도로는 크게 부족함이 없으나 현재 다루고 있는 데이터의 건수가 너무나도 많아서 분석쿼리 수행시 엄청난 시간이 소요된다. (대략 2000만건 x 수십개)

 

2. 트랜잭션 처리 OLTP

Aurora는 관계형 데이터베이스이며 그중에서 OLTP에 적합한 Row기반의 데이터베이스이다. 간단히 설명하자면 transaction처리를 최우선으로 하는 것이 목적이다. 

<트랜잭션이란?>

작업수행의 논리적 단위를 말한다.

Database의 관점에서 시스템 트랜잭션, 사용자관점에서 비지니스 트랜잭션의 개념으로 나눌 수 있으며 일반적으로 비지니스 트랜잭션은 1개이상의 시스템 트랜잭션으로 이루어진다.

예를 들어서 계좌이체는 출금-> 입금의 단계를 거쳐야 하는데 이를 하나의 트랜잭션으로 묶어야 할 필요가 있으며 Spring과 같은 Framework에서는 Transaction Manager를 통해서 이를 Support한다.

트랜잭션의 성질 ACID를 살펴보면 그 특징을 더 자세히 알수 있다.

 

<Row-Oriented vs Column-Oriented>

 이러한 요건을 만족시키기 위해서 일반적으로 Row-Oriented storage방식으로 접근한다. 각 Row를 기반으로 데이터를 바라보며 데이터의 정합성, 이를 위한 데이터 정규화 등을 수행한다.

Column-Oriented방식은 이와는 다르게 하나의 속성의 값들에 빠르게 접근하는 것을 목표로한다. 주로 OLAP환경에서 많이 선호한다. 대규모 분석쿼리를 수행하는 것이 주목적이다.

Row-oriented : 하나의 Row가 정합성을 가지도록 효과적으로 정규화하는데 목표가 있다.

 

Column-oriented : aggregation에 유리하고 효율적인 압축이 가능하다.

 

이부분에 대해서만도 책 한권의 분량이 나오기 때문에 간단히만 언급하고 지나갑니다.

 

3. OLAP, Big Data, 분산병렬처리

OLAP의 영역에서 BigData 기술들이 사용되고 있다.

 

 대부분의 Big Data, 그 중에서도 DataLake를 처리하는 오픈소스들은 트랜잭션을 보장하는 것이 목적이 아니라 일단 빠르게 쌓아놓고 나중에 병렬처리를 통해서 대규모 분석쿼리를 수행하여 Insight를 도출하는 것이 일반적인 목표이다.

 

 

그 특성이 OLAP과 유사하며 실제로 아키텍처도 비슷하다.

빅데이터의 출현배경과 활용방법등을 보면 과거 DatawareHouse, BI등과 99%동일하다는 것을 알수 있다.

 최근에는 트랜잭션 처리에 대한 요구사항이 꾸준히 증가하여 transaction을 지원하는 기능들이 추가되고 있으나 최초 설계된 목적이 다르기 때문에 한계점이 있다. (Apache Hive)
<참고:https://icthuman.tistory.com/m/entry/Apache-hive-transaction>

 

Apache hive - transaction

<개요> Apache Hive는 HDFS에 저장되어 있는 파일데이터를 SQL 기반으로 처리할 수 있도록 하는 오픈소스이다. (모든 SQL을 지원하는 것은 아니며, 파일시스템 특성상 UPDATE, DELETE는 권장하지 않는다. ) 그러나..

icthuman.tistory.com

현재 BigData영역에서는 Apache Spark Eco System이 상당부분을 커버하고 있다.

(Spark Core, Streaming, SQL, MLlib) - 추후에 Aurora에서 Redshift로 마이그를 위해서 AWS에서 제공하는 Glue를 살펴볼텐데 이 역시 SparkContext를 기반으로 동작한다. 

 

4. AWS Redshift

AWS에서 제공하는 OLAP 데이터베이스이다. 기존에 AWS에서 제공하는 Aurora, S3, RDS등의 데이터 소스와 쉽게 호환될 수 있는 것이 가장 큰 장점이다.

OLTP 대비 분석쿼리의 성능을 끌어올리기 위해서 다음과 같은 구조를 적용하였다. (일반적인 OLAP DB특성)

MPP : 대용량처리를 위해서 쿼리를 분산병렬처리 한다. 데이터를 저장할때 Distribution Key를 사용하여 적절히 분배하고 이를 기반으로 각 노드간의 데이터 이동을 최소화하여 병렬처리 성능을 극대화 한다.

Columnar data storage : Disk I/O 비용을 감소시키고 분석쿼리의 성능을 향상시킨다. 

Data compression : 데이터를 압축하여 메모리에 적재후 쿼리 수행시에만 해제한다. 이를 통해서 분석쿼리 수행시 메모리를 조금더 효과적으로 사용할 수 있다. 

Result caching : 수행되는 쿼리들에 대해서 캐싱한다. 

 

5. 쿼리 수행속도 향상

기존에 약 8~10분정도 소요되던 쿼리가 30초 정도 걸리는 것을 확인하였다.

 

<참고사이트>

https://francois-encrenaz.net/what-is-a-dbms-a-rdbms-olap-and-oltp/

https://victorydntmd.tistory.com/129

https://mariadb.com/resources/blog/why-is-columnstore-important/
https://databricks.com/

https://diffzi.com/oltp-vs-olap/

https://www.dbbest.com/blog/column-oriented-database-technologies/

http://www.javachain.com/big-data-hadoop-in-data-warehouse/

 

<개요>

Azure WebApp을 다음과 같은이유로 잘 사용해왔다.

- Azure Devops와 연계하여 배포가 쉽다.

- 다양한 Runtime환경을 기본으로 제공한다. (node, java, C# 등)

- 통합 로깅, 모니터링, 대시보드 환경을 제공한다.

- Auto Scaling이 편리하다. (VM 단위보다 더욱 유동적이다.)

- 접속을 위한 싱글포인트를 제공한다.

 

- 이번에 AWS에서 시스템을 구축하게 되어서 비슷한 PaaS를 찾던 중 Elastic Beanstalk를 발견하고 사용하던 중 발생한 내용이다.

 

<현상>

- Elastic Beanstalk는 내부적으로 nginx 를 사용하고 있다.

- Spring Boot배포 후 접속하면 502 Bad gateway 발생

 

<상세오류 - 로그확인>

2020/03/12 06:12:07 [error] 3114#0: *3 connect() failed (111: Connection refused) while connecting to upstream, client: 100.10.0.208, server: , request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:5000/", host: "userservice-env.eba-yqakdkhm.ap-northeast-2.elasticbeanstalk.com"


<해결과정>

- 구글링을 통해서 나오는 대표적인 해결방법은 포트변경이다. (AWS Beanstalk가 기본적으로 5000포트를 사용한다고 한다.)

Beanstalk내의 환경설정에서 SERVER_PORT를 5000으로 변경

하지만 나에게는 해당하지 않는 내용이었다. 과거 connection reset을 해결할때가 떠오르기 시작한다;;

 

조금 더 관련내용들을 찾아보았다.

https://stackoverflow.com/questions/52912663/aws-elastic-beanstalk-nginx-connect-failed-111-connection-refused

 

AWS elastic Beanstalk / nginx : connect() failed (111: Connection refused

I got this message connect() failed (111: Connection refused Here is my log: ------------------------------------- /var/log/nginx/error.log ------------------------------------- 2018/10/21 06:16...

stackoverflow.com

내용을 종합해보면 결국 application이 적절하게 기동되지 않거나 하여 nginx 가 해석할 수 없는 메시지를 돌려받았을 경우 발생한다.

- Beanstalk의 설정을 천천히 검토해보았다.

 구성, 환경변수, 네트워크, 로드 밸런서 등 이상이 없음을 확인하였다.

- 그렇다면 결국 application이 제대로 기동되지 않았을 확률이 높으나 해당 로그를 확인할 수 없는 것이 아쉽다.  (로컬에서는 정상적으로 기동하니까)

 

- AWS Beanstalk의 spring-boot application 기동방식이 궁금해지기 시작한다.

샘플어플리케이션으로 사용되는 예제는 다음과 같다.

https://github.com/spring-guides/gs-accessing-data-rest.git

 

해당 프로젝트의 pom.xml을 살펴보고 문제점을 찾았다. 아주 사소한....

<build> 태그의 <spring-boot-maven-plugin> 을 빼먹고 작성하지 않았다. 

 

 

<정리>

- Azure WebApp을 사용할때는 azure-webapp-maven-plugin을 사용했었다.

- 신규 프로젝트 구성시 initializer없이 구성하다 Public Cloud관련요소 작업 중 Build plugin실수로 삭제;;

- 정확한 로그를 확인할 수 없어서 약간 힘들었음

 

<Azure WebApp과 비교>

- Azure WebApp이 생성시 매우 간단하다. 클릭 몇번으로 끝

- 배포 역시 azure-webapp-maven-plugin을 사용하여 IDE에서 바로 배포하는 것도 쉽다.

  다만 version에 따른 사용법이 매우 다르기 때문에 주의해서 사용해야 한다. 

  예전에 사용했던 버전이 1.4.0 과 1.5.3 이었는데 세부속성과 사용법이 다르고, 설정 오류시 상세한 메시지를 확인하기가 어렵다.)

 

- AWS Beanstalk역시 기본설정은 간단하지만 상세화면으로 들아가면 항목이 매우 많다. 

( Azure의 경우 후발주자이다보니 PaaS를 중심으로 발전시켜 나갔고, 

  AWS의 경우 VM기반의 선발주자이다보니 제품 여기저기에서 VM중심의 구성이 많이 보인다.)

 

 

<참고>

https://aws.amazon.com/ko/blogs/devops/deploying-a-spring-boot-application-on-aws-using-aws-elastic-beanstalk/

https://github.com/spring-guides/gs-accessing-data-rest/

https://docs.microsoft.com/ko-kr/azure/java/spring-framework/deploy-containerized-spring-boot-java-app-with-maven-plugin

 

Maven을 사용하여 Azure에 Spring Boot 앱 배포

Azure Web Apps의 Maven 플러그 인을 사용하여 Spring Boot 앱을 Azure에 배포하는 방법을 알아봅니다.

docs.microsoft.com

 

다음에는 Azure Devops와 AWS Code Pipeline의 비교사용기를 ...

1. 시간 복잡도 vs 공간 복잡도

일반적인 용어 정리에 따르면 다음과 같습니다.

- 시간 복잡도(Time Complexity) : 알고리즘의 수행시간

- 공간 복잡도(Space Complexity) : 알고리즘의 메모리 사용량

 

시간복잡도와 공간복잡도는 소프트웨어를 만드는 사람이라면 빼놓을 수 없는 문제입니다.

 

위의 그래프는 각 알고리즘 간의 수행속도에 대해서 비교하고 있습니다. 데이터가 적으면 큰 차이가 나지 않지만 (오히려 전후처리 때문에 시간복잡도가 더 좋지만 느려지는 경우도 있습니다.) 많아질 수록 수행시간은 어마어마한 차이가 발생합니다.

수천건의 데이터의 경우 N log N 과 N^2 상에 큰 차이가 발생하지 않지만 수십만 단위로 넘어가게 되면 응답을 받을 수 없게 됩니다.

 

공간복잡도는 쉽게 메모리의 사용량이며 시간복잡도와는 trade-off 의 관계로 알려져있지만 잘 짜여져 있는 알고리즘에서는 시간복잡도와 공간복잡도를 모두 잡는 경우도 간혹 볼 수 있습니다.

 

시간복잡도가 높은 연산은 최소화 하는 것이 사용자 응답속도를 빠르게 하는데 도움이 됩니다.

 

2. 문자열 처리

어떤 문자열의 길이를 체크하는 알고리즘은 시간복잡도가 얼마일까요? 

O(N) 입니다. 끝까지 가보면 압니다.

 

그렇다면 어떤 문자열이 허용하는 문자로 구성되어 있는지, 예를 들어서 영문소문자 + 특수문자 ('-',',' 등) 로 이루어져있는지 체크하려면 어떻게 해야할까요? 

 

우리는 일반적으로 정규표현식을 사용해서 문제를 해결합니다. 정규표현식의 시간복잡도는 얼마일까요?

음.. 자세히는 몰라도 O(N)보다는 클 것 같습니다. (정규표현식은 기본적으로 모든 케이스를 시도해보는 백트래킹에 기반하고 있습니다.)

 

사용자로부터 입력을 받았을때 매번 정규표현식으로 문자열을 판단한다면 시간이 더 걸릴 것 같습니다.

(사실... 문자열은 대부분 길지 않기 때문에 큰 영향은 없습니다.^^;; ) 

 

3. 알고리즘 문제해결 접근법

알고리즘 문제를 많이 풀어보신 분들은 느껴보셨을 겁니다. 주어진 문제상황을 그대로 구현하면 100% Time Over가 발생하는 것을...

제약조건과 주어진 문제상황을 보다 간결하게 정리하게 크게 분류할 수 있는 기본로직을 세운 뒤, 최적화 알고리즘을 적절히 사용하여 구현하는 것이 일반적인 풀이입니다. 정리해보면 다음과 같습니다.

 

- 주어진 제약, 조건, 로직 이해

- 시간복잡도 / 공간복잡도 분석

- 새로운 문제로 (재분류, 대전제, 기본로직) 재정의

- 해당 문제에 적합한 알고리즘 사용 및 구현

- 결과에 대한 검증

 

4. 비지니스 요건에 접근할 때

다음과 같은 상황이 주어진다면?

 

<요건>

- 사용자가 서비스를 등록한다.

- 서비스는 고유 Id가 존재한다.

- 각 서비스를 구분할 수 있는 서비스명이 존재한다.

- 서비스명은 영문소문자로만 20자이하로 구성된다.

- 각 서비스를 구분할 수 있는 서비스코드가 존재한다.

- 서비스 코드는 영문소문자 + '-'  20자이하로 구성된다.

- 서비스에 대한 설명을 100자 이내로 작성할 수 있다.

 

<제약사항>

- 서비스명과 서비스코드는 반드시 입력되어야 한다.

- 서비스명과 서비스코드는 공백을 허용하지 않는다.

- 서비스명과 서비스코드는 트림처리가 되어야 한다.

- 서비스설명은 공백을 허용한다.

- 서비스명과 서비스코드는 반드시 영문소문자로 시작한다.

 

이와 같이 단순하게 요건을 나열된 그대로 구현하게 되면 소스코드는 상당히 지저분하게 됩니다. 그리고 변경이 발생했을 때 유지보수성도 떨어지며 속도에도 영향을 미치게 됩니다.

 

(번외로 알고리즘 공부하다보면 발견하게 되는 것 중에... 소스가 지저분하고 라인이 점점 길어진다면?  매우 높은 확률로 오답입니다 ㅜㅜ )

 

5. 접근방법

<예시코드>

if(StringUtils.isEmpty(serviceDto.getServiceName())){
            // error
}else{
      if(StringUtils.containsWhitespace(serviceDto.getServiceName())){
            serviceDto.setServiceName(StringUtils.replace(serviceDto.getServiceName()," ",""));
            serviceDto.setServiceName(serviceDto.getServiceName().trim());
      }
}

if(StringUtils.isEmpty(serviceDto.getServiceCode())){
            // error
}else {
      if(StringUtils.containsWhitespace(serviceDto.getServiceCode())){
            serviceDto.setServiceCode(StringUtils.replace(serviceDto.getServiceCode()," ",""));
            serviceDto.setServiceCode(serviceDto.getServiceCode().trim());
      }
}

새로운 컬럼이 늘어날때마다 if-else가...;; 계속 추가됩니다..

임시방편으로 trim에 대한 로직을 DTO내의 setter로 옮길 수 있지만, 기본적인 로직이 정리되지 않은 상태라 근원적인 해결이 되지는 않습니다. 결국 해당 로직은 여러 DTO로 각각 흩어져서 유지보수가 어려워 지는 것은 똑같습니다.

 

요구사항을 바로 구현하는 습관을 버려야 합니다.

오랜 시간동안 요구사항을 읽고 이해한 뒤, 다시 재구성해야 합니다.

또한 로직을 구성할 때에는 반드시 대, 중, 소 로 접근하여 최대한 간결하게 (중복없이, 누락없이) 정리해야 합니다.

 

 

위의 상황을 제가 이해한 모양으로 다시 정리해보면 다음과 같습니다.

- 컬럼은 필수 / 선택으로 나누어진다. 필수컬럼의 경우 허용하는 문자에 대해서 패턴이 존재한다.

- 트림은 문자의 패턴에 따라서 처음이나 마지막에 공백이 올수 없음을 의미한다.

- 반드시 입력되어야 하는 값이라면 문자의 패턴은 최소길이는 1로 표현가능하다.

- 서비스명 과 서비스코드는 필수컬럼이다.

- 서비스명의 문자패턴은 영문소문자(1글자) + 영문소문자(1 ~ 19글자)

- 서비스코드의 문자패턴은 영문소문자(1글자) + 영문소문자와 '-' (1~19글자)

- 패턴은 컬럼별로 다를 수 있다.

- 선택컬럼의 경우 허용하는 문자에 대한 패턴은 없다. 그러나 문자의 길이에 대해서는 조건이 존재할 수 있다.

- 서비스설명은 선택컬럼이다.

 

뭔가 훨씬 깔끔해진 느낌이 듭니다.

 

 

이부분이 제가 생각할 때 소프트웨어 구현에서 가장 중요한 단계입니다. 일반적으로 시니어 소프트웨어 개발자, 혹은 아키텍트들이 같이 해야하는 일입니다.

 

일반인이 이야기한 비지니스 요건을 본인이 이해한바로 잘 정리하여 문제를 재구성하여 소프트웨어로 만들 수 있도록 정의 하는 것이 핵심입니다. 또한 문제해결을 위한 기본적인 알고리즘 (시간복잡도, 공간복잡도) 과 아키텍처가 설계되는 시점입니다.

 

6. 구현

 각 Layer에서 어떠한 Validation을 할것인지 정의합니다.

 

Repository에서는 Data의 무결성을 보장합니다. 일반적으로 Data의 무결성은 DBMS에서 담당하며 Network, File 등을 거쳐서 가야하기 때문에 비용이 가장 많이 듭니다.

 

역으로 Contoller 가 사용자와 가장 가까운 곳에 위치하고 있기 때문에 기본적인 것들은 여기서 걸러주는 것이 많이 도움이 됩니다.

 

기본적인 Validation 체크는 Controller 에서 수행하고 나머지 비지니스 로직에 대한 처리는 Service에서 합니다.

 

(이번 글에서는 편의상 Pattern이 비지니스의 의미를 가지고 있다고 가정했습니다만 Controller에서 처리하는 경우도 많습니다.)

 

일반적인 MVC 구조

 

 Controller Layer에서는 값의 표면적 형태에 대해서만 검증을 수행합니다.

기본적인 Length 체크만을 수행하며 위에서 언급한 것처럼 정규표현식은 시간복잡도가 높기 때문에 Contoller Layer를 정상적으로 통과한 경우에 대해서만 검증을 수행하는 것이 수행속도에도 유리하다고 판단했습니다.

@RestController
@RequestMapping(path="/api")
public class ServiceController {

    private Logger logger = LoggerFactory.getLogger(ServiceController.class);

    @Autowired
    private ServiceService serviceService;


    @RequestMapping(value="/services", method= RequestMethod.POST)
    public @ResponseBody
    ServiceDto createService (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                              @RequestBody @Valid ServiceDto service, HttpServletResponse response) throws DataFormatViolationException, ServiceAlreadyExistsException {

        // serviceCode 중복 체크 수행
        ServiceDto serviceDto = serviceService.findServiceByServiceCode(service.getServiceCode());
        if(serviceDto.checkAvailablePrimaryKey()) {
            throw new ServiceAlreadyExistsException(String.valueOf(service.getServiceCode()));
        }

        ServiceDto ret = serviceService.createService(service);
        response.setStatus(HttpServletResponse.SC_CREATED);
        return ret;
    }

 

체크로직을 구성할 때에도 컬럼별로 비교로직을 Contoller 내에서 구현하는 것보다는 Spring @Valid를 활용하였습니다.

어노테이션 기반으로 Dto검증을 수행하기 때문에 로직이 훨씬 간결해 집니다.

@Data
@Getter
@Setter
public class ServiceDto extends AbstractCommonTable implements ServiceInformation, UserInformation {

    private Integer serviceId;

    @Size(min=1,max=20)
    private String serviceName;

    @Size(min=1,max=20)
    private String serviceCode;

    @Size(min=0,max=100)
    private String description;

 

다음은 Service Layer입니다.

 

공통적인 로직을 Service Layer 에 두면 다른 REST API 나 Controller 활용할 때에도 별도로 검증로직을 추가하지 않아도 됩니다.

또한 값의 의미를 검증하는 부분은 비지니스와 연관성이 있다고 판단하였습니다.

 

먼저 서비스명과 서비스코드의 문자패턴을 정규표현식으로 나타냅니다.

문자패턴을 정규표현식으로 나타내면서 (시작문자,종료문자,트림,길이 등) 에 대한 처리를 합니다.

public class ValidationPattern {
    public final static Pattern serviceNamePattern = Pattern.compile("(^[a-z][a-z0-9]{1,19}$)");
    public final static Pattern serviceCodePattern = Pattern.compile("(^[a-z][a-z0-9-]{1,19}$)");
}

 

이후 해당 패턴을 이용하여 검증하는 로직을 구현합니다.

@Service
public class ServiceService {
    @Autowired
    private ServiceRepository serviceRepository;

    @Autowired
    private ModelMapper modelMapper;
    
	 public ServiceDto createService(ServiceDto serviceDto) throws DataFormatViolationException {

        String serviceCode = serviceDto.getServiceCode();
        checkServiceCode(serviceCode);

        ServiceEntity serviceEntity =modelMapper.map(serviceDto, ServiceEntity.class);
        serviceRepository.save(serviceEntity);
        return modelMapper.map(serviceEntity, ServiceDto.class);
    }
    
    private void checkServiceCode(String serviceCode) throws DataFormatViolationException {

        if(serviceCode == null){
            throw new DataFormatViolationException("Code value should be not null");
        }else{
            Pattern codePattern = ValidationPattern.serviceCodePattern;
            Matcher matcher = codePattern.matcher(serviceCode);

            if(!matcher.matches()){
                throw new DataFormatViolationException("Code value should be consist of alphabet lowercase, number and '-', (length is from 2 to 20)");
            }
        }
    }

 

7. 결론

두서없이 쓰다보니 글의 요점이 모호해진 것 같습니다.

정리해보면...

 

- 어떠한 문제를 해결하기 위해서 바로 코딩으로 뛰어들어서는 안된다.

- 문제를 재해석하여 나만의 방식으로 표현하고 시간복잡도 / 공간복잡도 를 정리한다.

- 로직은 최대한 간결하게!  대/중/소, 중복없이, 누락없이!

- MVC 의 경우 각 Layer의 하는 일들이 나누어져 있다.

- 만약 시간복잡도가 높은 연산이 있다면 이러한 연산은 최소한으로 해주는 것이 좋고, 이러한 필터링을 각 Layer별로 해주면 효과적이다.

- 유지보수성유연성은 반드시 따라오는 덤! 

 

 

<참고사이트>

https://ledgku.tistory.com/33

http://www.secmem.org/blog/2019/02/10/a-comprehensive-guide-to-regex/

1. Message Queue

Message Queue 는 기존 레거시부터 최근까지 계속 사용되고 있는 방식입니다.

지원하는 프로토콜(MQTT, AMQP, JMS)에 따라서 분류되기도 하며 메모리 기반/디스크 기반으로 나누어지기도 합니다.

또한 Message를 처리하는 방식에 따라서 Push / Pull 방식으로 나누기도 합니다.

그리고 Queue 의 정상적인 동작을 돕기 위해서 Broker가 개입하는 것이 일반적입니다.

 

그러나 다양한 제품 가운데 공통적인 것이 있으니 결국은 Queue 라는 것입니다.

FIFO(First In First Out)의 메시지 처리가 필요하다는 전제로 각 시스템의 특징과 요구사항에 따라 맞춰서 솔루션을 선택하면 됩니다.

 

2. Message Queue 를 사용하는 이유는?

특히 Queue 는 비동기식 메시지 처리에서 많이 사용되는데 그 이유는 다음과 같습니다.

 

- 어플리케이션을 구현할 때 메시지가 어디서 오는지(Source),  어디로 보내야 하는지(Target) 신경쓸 필요가 없다.

- Producer는 메시지를 작성하는 데에만 집중하고 메시지 전달에 관련된 (목적지, 순서, QoS 등) 항목들에 대해서는 MQ에 모두 위임한다.

- Consumer 역시 해당 메시지를 어떻게 소모하여 처리하는 지에 대해서만 집중하면 된다.

 

- Producer 와 Consumer가 보다 각자의 역할에 집중할 수 있게 도와준다.

 

3. 자료구조 Queue

특정 자료구조에 대한 정리나 코드연습은 Algorithm 카테고리에서 자세히 소개할 예정이기 때문에 간단히 살펴봅니다.

Queue

이해를 돕기 위해서 array를 이용해서 작성한 Queue Sample입니다.

class Queue { 
	private static int front, rear, capacity; 
	private static int queue[]; 

	Queue(int c) 
	{ 
		front = rear = 0; 
		capacity = c; 
		queue = new int[capacity]; 
	} 

	static void queueEnqueue(int data) 
	{ 
		// check queue is full or not 
		if (capacity == rear) {  
			return; 
		} 
		// insert element at the rear 
		else { 
			queue[rear] = data; 
			rear++; 
		} 
		return; 
	} 
 
	static void queueDequeue() 
	{ 
		// if queue is empty 
		if (front == rear) { 
			return; 
		} 

		else { 
			for (int i = 0; i < rear - 1; i++) { 
				queue[i] = queue[i + 1]; 
			} 
			if (rear < capacity) 
				queue[rear] = 0; 

			rear--; 
		} 
		return; 
	} 
} 

-queueEnqueue 

 rear 는 현재 데이터를 넣어야하는 위치의 index 입니다.

 만약에 capacity 만큼 꽉 차있다면 더 이상 데이터를 넣을 수 없습니다.

 데이터를 넣는 것이 가능하다면 삽입 후 index를 변경합니다.

 

-queueDequeue

 front는 데이터가 삭제되는 위치의 index입니다.

 front == rear 라면 비어있는 상태입니다.

 데이터를 제거하는 것이 가능하다면 제거 후 index를 변경합니다.

 

4. Queue 사용시 주의점

위의 sample소스를 통해서 Queue의 특징을 이해하셨다면 주의점도 파악하실 수 있습니다.

- Queue는 무한한 용량이 아니다. 적절히 소모가 이루어지지 않는다면 Queue의 모든 용량을 소모하게 되고 유실이 발생한다.

- Queue에는 많은 쓰레드들이 동시에 접근할 수 있다. enqueue/dequeue 동작은 thread-safe 해야 합니다.

  Java에서는 이를 위해서 concurrent 패키지를 제공합니다.

- 서로 다른 Queue에서는 순서보장이 되지 않습니다. (당연한 말씀)

 

Computer Science의 기초인 DS & Algorithm은 반드시 알고 있는 것이 좋습니다.

 

5. Apache Kafka

 많은 Message Queue 중에서 Apache Kafka는 대용량 실시간 처리에 특화된 아키텍처 설계를 통하여 우수한 TPS를 보장합니다.

기본적으로 pub-sub 구조로 동작하며, scale out과 high availability를 확보하는 것으로 유명합니다.

아키텍처상 다양한 특징이 있지만 이번 글에서는 자료구조 & 알고리즘 측면에서 살펴보겠습니다.

 

 

5-A. Multi Partition

 Kafka가 분산처리가 가능한 것은 Multi Partition이 가능하기 때문입니다.

대용량의 데이터가 들어올 수록 병렬처리를 통해서 속도를 확보할 수 있는 구조를 가지고 있습니다.

 

그렇다면 단점은 없을까요? 

서로 다른 Partition에 대해서는 순서를 보장하지 않습니다.(다른 큐로 보시면 됩니다.)


예를 들어서 Partition 0 내의 메시지들끼리, Partition 1 내의 메시지들끼리 순서를 보장하지만 Partition 0과 1사이에는 순서를 보장할 수 없습니다.

 

반드시 순서를 보장해야 하는 케이스가 존재한다면?

 - Partition 을 하나만 사용하던지

 - Partitioner를 Custom 하게 작성하여 순서가 보장되어야 하는 메시지들은 같은 Partition으로 보내도록 합니다.

 

Topic과 혼동하시면 안됩니다. 특정 Topic내에 여러 Partition이 존재하며 Broker를 통해서 각 Topic별 Parition 의 저장정보와 복제 등이 이루어집니다. 

 

5-B. Partitioner

Kafka의 Default Partitioner는 modulo 연산을 사용하여 파티션을 나누게 되어 있습니다.

Hash 알고리즘을 잘 작성하여 데이터를 고르게 분산하면 병렬처리 성능을 더 끌어올릴 수 있습니다.

간단한 Hash Function의 예제입니다.

private int hashFunction(K key){
    int val=0;
    for(int i=0; i<key.toString().length();i++){
        val+= (key.toString().charAt(i)) % size;
    }
    return val % size;
}

일반적으로 우리가 사용하는 HashMap에서도 같은 Hash값이 자주 발생할 경우 특정 row가 길어지게 되어 chaining hash table을 구현하게 되는데, 이 때 worst case로 특정 bucket에 데이터가 집중되어 메모리를 효율적으로 사용하지 못하는 경우가 발생합니다.

 

Kafka에서도 partition을 적절하게 분산하지 못할 경우 이러한 문제가 발생할 수 있습니다.

 

6. 정리

- Apach Kafka는 자료구조 Queue와 동일합니다.

- 병렬처리를 위해서 multi partiton을 지원합니다. 여러 개의 Queue 를 동시에 사용한다고 생각하면 됩니다.

- Partitioner를 어떻게 구현하는가에 따라서 partiton이 결정됩니다.

- FIFO보장이 필요한 단위에 대해서는 같은 partiton 이 적용되도록 구현합니다.

 

0. 참조

https://sarc.io/index.php/miscellaneous/1615-message-queue-mq

https://ko.wikipedia.org/wiki/%ED%81%90_(%EC%9E%90%EB%A3%8C_%EA%B5%AC%EC%A1%B0)

https://monsieursongsong.tistory.com/

https://epicdevs.com/17

https://en.wikipedia.org/wiki/Hash_table

https://www.geeksforgeeks.org/array-implementation-of-queue-simple/

소프트웨어 개발이라는 분야는 오래했다고 잘한다는 것이 보장되는 곳이 아닙니다.

끊임없이 Why 와 How 에 대해서 스스로 질문하고 공부해야 성장하는 분야인 것 같습니다.

 

1. 아키텍트 vs 개발자

 

요즘 들어서 "개발자"라는 직업이 아주 핫합니다.

그런데 IT시장에서 오래 일하신분들이 "개발자" 라는 호칭을 들었을 때 반응이 좀 다릅니다.

 

"언제까지 개발만 할거냐?" 라는 질문을 던지며 전체적인 그림을 그리는 아키텍트가 되어야 한다는 사람들이 많이 있습니다.

이런분들은 개발=코딩의 의미로 바라보는 것 같습니다.

 

그러나 "우리는 아키텍트라는 표현을 사용하지 않아. 전부 다 개발자이지" 라고 이야기하는 분들도 상당히 많습니다.

보통 이런회사의 개발자분들은 아키텍처, 설계, 개발, 테스트를 모두 합쳐서 개발로 인식하여 수행합니다.

 

비율을 정확히 따져보지는 않았지만 두 부류 모두 부르는 명칭만 다를 뿐이지 사실 해야하는 일은 비슷합니다.

 

고객과 시장의 요구사항을 만났을 때, 기술적 관점으로 그것을 재해석하여 소프트웨어로 구현해야 하는 것이 궁극적인 목표입니다.

 

 

- 아키텍처 그림만 PPT로 열심히 그리는 사람은 아키텍트가 아닙니다. 그림은 아무나 그릴 수 있습니다.

- 자기가 만드는 프로그램 하나의 개발에만 집중하는 사람은 좋은 개발자가 아닙니다.

   전체적인 구성요소와 관계를 파악하여 기능적/비기능적 요구사항을 파악하고 이에 따른 제약사항과 구현방안을 설계/개발 할 수있는 사람이 좋은 개발자입니다.

 

2. 기술 vs 기본

 

시장의 많은 개발자들끼리 만났을때 심심치 않게 벌어지는 논쟁입니다.

 

"Spring 3.0에서는 어떠했고, 4.0에와서는 어떤 것이 바뀌었으며 ..."

"그럴때는 Spring 의 PostProcessor를 사용해서 구현하고"

등등 시장에서 사용되는 최신기술들에 관심이 많은 분들입니다.

 

이런 분들의 특징은 여러가지 기술등을 효과적으로 활용하여 원하는 바를 빠르게 만들어냅니다.

이른바 "손이 빠릅니다".

 

"이렇게 하면 시간복잡도, 공간복잡도가 이렇게 되고.."
"O(N^2)을 O(NlogN)으로 줄일 수 있고..

"건별 Data가 몇 byte이니, 초당 건수를 고려하면 메모리를 bytes만큼 사용하고"

 

이런 분들은 뭔가 하나에 꽂히면 파고들어서 최적의 구현을 하는 것이 목표입니다.

계속 들여다보고 최적화 / 효율화를 반복합니다. 

그리고 "흐뭇해 하십니다"

 

결론부터 말씀드리면 둘 다 중요합니다!

 

오픈 소스의 홍수 속에서 너무나도 많은 "바퀴"들이 존재합니다. 바퀴를 다시 만들필요는 없습니다.

망치가 있는데 돌멩이로 못질을 할필요는 없죠.

그런데 핵심이 되는 원리는 비슷합니다.

 

- 내가 사용하는 모듈의 시간복잡도, 공간복잡도가 어떠한지

- 내부에서 어느부분이 동기/비동기 처리가 일어나는지 

- Input 이 100건일때와 100만건일때 속도차이는 어떠한지

 

이러한 내용을 파악하지 않고 바퀴를 가져다 만드는데만 집중하면 밸런스가 안맞습니다.

시스템이 작을때는 문제가 없지만 데이터가 늘어나고 사용자가 늘어날 수록 버티기가 힘들어집니다.

 

이름있는 Tech기업들이 개발자(소프트웨어 인력)을 채용할때 알고리즘 테스트를 하는 것은 다 이유가 있습니다.

새로운 기술이 나왔을때 빨리 습득하고, 논리적 문제해결을 바탕으로 접근하여 그 기술을 사용할줄 아는 사람이 필요합니다.

 

 

현장에서 오래 일하다보면 고객의 요구사항을 만족시키는게 급급하여 기본을 무시하는 사람들을 많이 봅니다.

"알고리즘 같은건 그냥 시간있을때나 공부하는거지"

"그게 뭐가 중요해. 내가 하고 있는 업무랑 연관 없는데"

시스템이 커지고 복잡해지면 반드시 문제가 생깁니다.

 

한편으로는 기술을 무시하는 사람도 많습니다.

"남에 만든거 가져다 쓰는게 뭐 대단하다고"

"내가 만들면 그거보다 잘 만드는데"

바퀴를 만드는 것도 중요하지만 자동차를 만드는 것도 중요합니다. 사용자가 없는 소프트웨어는 죽은 겁니다.

 

이번 카테고리에서는 이 두가지를 섞어서 글을 정리해보려고 합니다.

개발과 알고리즘은 떼려야 뗄 수 없는 관계임을 우리는 이미 알고 있습니다.

이러한 주제를 아키텍처 레벨로 확장시켜서 살펴보려고 합니다.

 

우리가 알고있는 자료구조와 알고리즘들이 아키텍처에서는 어떻게 적용되고 있는지를 자세히 공부한다면

위와 같은 논쟁도 많이 없어지지 않을까 하는 생각입니다.

 

 

ps)

3. Design Pattern & Refactoring

위의 주제와는 약간 다른 주제입니다.

급변하는 요구사항을 어떻게 하면 최소한의 비용으로 빠르고 정확하게 만족시킬 수 있을지에 대한 해결방안으로 보면됩니다.

 

<개요>

https://icthuman.tistory.com/entry/%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EB%8A%94-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C%A8%ED%84%B4-2-%EB%B9%84%EC%A7%80%EB%8B%88%EC%8A%A4-%EB%A1%9C%EC%A7%81%EC%9D%84-%EB%8B%B4%EC%9E%90

 

사례로 배워보는 디자인패턴 #2 - 비지니스 로직을 담자

<개요> - 일전에는 간단히 MVC Layer로 조회 API를 만들어 봤습니다. https://icthuman.tistory.com/entry/%EC%82%AC%EB%A1%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EB%8A%94-%EB%94%94%EC%9E%90%EC%9D%B8%ED%8C..

icthuman.tistory.com

- 신규 서비스를 등록하는 API를 작성하였습니다.

- 비지니스 로직이 담긴 serviceCode라는 필드가 추가 되었습니다.

 

<내용>

- 지난 시간에는 신규 서비스를 생성하였습니다. 

- 이번시간에는 serviceId외에 입력하였던 serviceCode로 조회하는 API를 추가해보겠습니다.

반복되는 소스는 제외하고 살펴보도록 하겠습니다.

@RestController
@RequestMapping(path="/api")
public class ServiceController {

	@RequestMapping(value="/services/{serviceCode}", method= RequestMethod.GET)
    public @ResponseBody
    List<DeviceModelDto> findServiceByServiceCode (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                                          @PathVariable("serviceCode") String serviceCode) throws ServiceNotFoundException {
        // @ResponseBody means the returned String is the response, not a view name
        // @RequestParam means it is a parameter from the GET or POST request
        ServiceDto serviceDto =  serviceService.findServiceByServiceCode(serviceCode);
        if((serviceDto == null) ||
                loginUserDetails.checkNotAvailableService( serviceDto.getServiceId()) ||
                StringUtils.isEmpty(serviceDto.getServiceId())
        ){
            throw new ServiceNotFoundException(String.valueOf(serviceCode));
}

기존에 만들었던 getServiceByServiceId는 사용할 수가 없습니다. serviceId를 인자값으로 하고 있었는데 이제는 serviceCode를 사용해야하기 때문입니다.

 

Service Layer에도 작업이 필요합니다.

public ServiceDto findServiceByServiceCode(String serviceCode) throws DataFormatViolationException {

        Pattern codePattern = ValidationPattern.serviceCodePattern;
        Matcher matcher = codePattern.matcher(serviceCode);

        if(!matcher.matches()){
            throw new DataFormatViolationException("Code value should be consist of alphabet lowercase, number and '-', (length is from 2 to 20)");
        }

        ServiceEntity serviceEntity = serviceRepository.findByServiceCode(serviceCode).orElse(new ServiceEntity());
        return modelMapper.map(serviceEntity, ServiceDto.class);
    }

이때 주의해야 할 점은 입력으로 받는 serviceCode에 대해서도 기존과 동일한 검증로직을 적용해주는 것이 좋습니다. 없어도 상관은 없습니다. 그러나 불필요한 요청이 Repository Layer까지 전달될 필요는 없을 것 같습니다. (Repository Layer는 언제나 비용이 가장 비쌉니다.)

기본적으로 MVC는 Layerd Architecture이기 때문입니다.

 

다시 Controller 로 돌아가서 윗 부분을 로직도 간결하게 만들어 보겠습니다.

@RestController
@RequestMapping(path="/api")
public class ServiceController {
	
     @RequestMapping(value="/services/{serviceCode}/devices", method= RequestMethod.GET)
    public @ResponseBody
    List<DeviceDto> findServiceByServiceCode (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                                                 @PathVariable("serviceCode") String serviceCode) throws ServiceNotFoundException, DataFormatViolationException {
        
        ServiceDto serviceDto = getServiceByServiceCode(serviceCode, loginUserDetails);
		return serviceDto;
    }


	private ServiceDto getServiceByServiceCode(String serviceCode, LoginUserDetails loginUserDetails) throws DataFormatViolationException, ServiceNotFoundException {
        ServiceDto serviceDto =  serviceService.findServiceByServiceCode(serviceCode);

        if((loginUserDetails.checkNotAvailableService( serviceDto.getServiceId()) ||
            StringUtils.isEmpty(serviceDto.getServiceId())
        ){
            throw new ServiceNotFoundException(String.valueOf(serviceCode));
        }

        return serviceDto;
    }

- Null 처리를 Service Layer에서 해주었기 때문에 Controller Layer에서는 삭제가 가능합니다.

- serviceCode로 조회되는 경우도 재사용을 할 수 있도록 getServiceByServiceCode로 묶어서 private 메소드로 구현하였습니다.

 

기본적인 내용이지만 잠깐 짚고 넘어가야할 부분이 있습니다. 많은 분들이 개발을 할때 습관적으로 메소드의 기본을 public 으로 작성합니다.

왜그럴까요? 일단 다 사용할 수 있게 해주는 것이 편리하기 때문입니다. getter, setter 역시 습관적으로 모든 필드값에 만들어 놓고 시작하는 경우를 많이 봅니다.

 

하지만 이러한 습관은 설계의 기본원칙을 무시하는 위험한 행동입니다. 저는 개인적으로 private을 기본으로 하고 필요한 경우에만 public 메소드를 통해서 열어주는 것을 권장합니다. 메소드와 필드값 모두 동일한 원칙으로 적용합니다.

 

첫번째 시간에 LoginUserDetails내에서 service Id를 외부로 노출하지 않았던 것을 기억하시기 바랍니다. 현재 사용자의 serviceId를 가지고 작업해야 경우가 생긴다면 해당 객체의 method call을 하는 것이 맞습니다. 교과서적인 용어로는 객체간의 Interaction이라고 합니다.

 

최종 작업을 통해서 아래와 같은 코드가 작성되었습니다.

@RestController
@RequestMapping(path="/api")
public class ServiceController {

	@RequestMapping(value="/services/{serviceId}", method= RequestMethod.GET)
    public @ResponseBody
    ServiceDto findServiceById (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                            @PathVariable("serviceId") int serviceId) throws ServiceNotFoundException {
        
        ServiceDto serviceDto = getServiceByServiceId(serviceId, loginUserDetails);
        return serviceDto;
    }
    
    @RequestMapping(value="/services/{serviceCode}", method= RequestMethod.GET)
    public @ResponseBody
    ServiceDto findServiceByCode (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                            @PathVariable("serviceCode") String serviceCode) throws ServiceNotFoundException {
        
        ServiceDto serviceDto = getServiceByServiceCode(serviceId, loginUserDetails);
        return serviceDto;
    }


	private ServiceDto getServiceByServiceId(int serviceId, LoginUserDetails loginUserDetails) throws ServiceNotFoundException {
        ServiceDto serviceDto =  serviceService.findServiceById(serviceId);
        if(loginUserDetails.checkNotAvailableService( serviceDto.getServiceId()) ||
                StringUtils.isEmpty(serviceDto.getServiceId())){
            throw new ServiceNotFoundException(String.valueOf(serviceId));
        }
        return serviceDto;
    }


    private ServiceDto getServiceByServiceCode(String serviceCode, LoginUserDetails loginUserDetails) throws DataFormatViolationException, ServiceNotFoundException {
        ServiceDto serviceDto =  serviceService.findServiceByServiceCode(serviceCode);

        if((loginUserDetails.checkNotAvailableService( serviceDto.getServiceId()) ||
                StringUtils.isEmpty(serviceDto.getServiceId()) ){
            throw new ServiceNotFoundException(String.valueOf(serviceCode));
        }

        return serviceDto;
    }

똑같은 로직이 반복되는 것이 눈에 보입니다.

serviceId , serviceCode의 차이만 있고 나머지는 거의 유사합니다.

 

호출되는 Service Layer의 메소드명, 인자값만 약간 다른 것을 보니 여전히 통합할 수 있는 부분들이 보입니다.

조회하는 조건값을 serviceId, serviceCode로 나누어서 동작하면 service 호출외의 부분은 정리할 수 있을 것 같습니다.

 

과거에는 이러한 분기조건에서 int, char, boolean을 쓰는 경우가 많았지만 적어도 Java에서는 enum type이라는 좋은 대안이 있습니다.

public enum SearchConditionType {
    ID,CODE;
}
@RestController
@RequestMapping(path="/api")
public class ServiceController {

	@RequestMapping(value="/services/{serviceId}", method= RequestMethod.GET)
    public @ResponseBody
    ServiceDto findServiceById (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                            @PathVariable("serviceId") int serviceId) throws ServiceNotFoundException {
        
        ServiceDto serviceDto = getServiceByCondition(SearchConditionType.ID, serviceId, loginUserDetails);
        return serviceDto;
    }
    
    @RequestMapping(value="/services/{serviceCode}", method= RequestMethod.GET)
    public @ResponseBody
    ServiceDto findServiceByCode (@AuthenticationPrincipal LoginUserDetails loginUserDetails,
                            @PathVariable("serviceCode") String serviceCode) throws ServiceNotFoundException {
        
        ServiceDto serviceDto = getServiceByCondition(SearchConditionType.CODE, serviceCode, loginUserDetails);
        return serviceDto;
    }


private ServiceDto getServiceByCondition(SearchConditionType searchConditionType, Object condition, LoginUserDetails loginUserDetails) throws DataFormatViolationException, ServiceNotFoundException {
        ServiceDto serviceDto = null;
        switch (searchConditionType){
            case ID:
                serviceDto = serviceService.findServiceById((Integer)condition);
                break;
            case CODE:
                serviceDto = serviceService.findServiceByServiceCode((String)condition);
                break;
        }
        if(loginUserDetails.checkNotAvailableService( serviceDto.getServiceId()) ||
                StringUtils.isEmpty(serviceDto.getServiceId())){
            throw new ServiceNotFoundException(condition.toString());
        }

- 이와 같은 방법을 통해서 Validation Logic 을 하나로 통합하여 재사용할 수 있습니다.

  재사용을 고려하지 않고 ctrl + c, v를 무작정 사용해서 개발할 경우 나중에 수정사항이 발생했을 때의 여파는 생각보다 큽니다!

 매번 수작업으로 find usages를 해서 소스를 고쳐야 하고, 다 고치고 나서도 불안하며, 테스트코드도 모든 케이스 개별로 다시 해야합니다.

 

- enum을 사용할 경우 유효하지 않은 값의 입력을 막을 수 있습니다. 그 외에도 enum의 활용도는 무궁무진합니다.

  완벽한 싱글톤 객체로 사용되기도 하고(effective java), 코드 테이블용도로 사용되기도 합니다.

  제 블로그에 Status 및 Operation 을 담는 객체로 활용한 예제도 있으니 참고하시기 바랍니다.

 

- Object 를 특정 타입으로 캐스팅하는 것은 권장하고 싶지 않은 방법이지만 간단한 예제를 위해서 사용했습니다.

 

- switch문으로 분기문을 쭉 나열하는 것도 좋은 방법은 아닙니다..

 

<정리>

- 비지니스 검증은 되도록 Service Layer에서, Repository Layer는 접근 비용이 비쌉니다.

- 필드값, 메소드의 접근자는 생각하면서 사용합니다.

- ctrl + c, v는 없도록 합니다.

- Java enum은 다용도로 활용이 가능합니다.

 

 

 

지금까지는 어찌보면 간단한 구현이었습니다.

하지만 실제 비지니스는 더욱 복잡합니다.

 

- Join데이터들을 조회할 때 문제점

- 늘어나는 케이스마다 동일 변수 사용시 (e.g. serviceCode, serviceId) 공통화 할 수 있는 부분은?

- 테이블의 종류는 점점 늘어날 것인데, 조회된 값이 유효한지 (e.g. Id가 비어있지는 않은지, 0이 오지는 않는지) 매번 검증할 것인가

- 테이블 객체에서 검증할 것인가, 별도 객체에게 위임할 것인가.

- 만약 primary key의 타입이 달라지는 경우는 어떻게 비교할지

 

등등, 시간이 될때 마다 정리해서 올려보도록 하겠습니다.

+ Recent posts