MS-SQL Replace ASCII control code

SQL 쿼리 결과를 XML 형태로 출력 할 때 아래와 같은 오류가 발생하는 경우가 있다.

FOR XML could not serialize the data for node ‘[ Column name]’ because it contains a character (0x001F) which is not allowed in XML. To retrieve this data using FOR XML, convert it to binary, varbinary or image data type and use the BINARY BASE64 directive.

XML로 변활 할 대상 데이터에 ASCII control code가 포함되어 있어서 발생하는 경우이다. 그래서 대상 데이터 안에 포함 되어 있는 해당 아래 스크립트 같이 XML로 변환 전에 모두 치환해 주어야 한다.

SELECT 
  REPLACE([Column name], char(0), '') 
FROM 
  [Table Name]

Control code를 삭제하는 Function을 만들어 두면 필요할 때 가져다 쓰면 좋다.

CREATE FUNCTION [dbo].[FN_ReplaceControlCharacter]  
(  
    @mString NVARCHAR(MAX)  
)  
RETURNS NVARCHAR(MAX)  
AS  
BEGIN      	
	SET @mString = REPLACE(@mString, char(0), '')
	SET @mString = REPLACE(@mString, char(1), '')
	SET @mString = REPLACE(@mString, char(2), '')
	SET @mString = REPLACE(@mString, char(3), '')
	SET @mString = REPLACE(@mString, char(4), '')
	SET @mString = REPLACE(@mString, char(5), '')
	SET @mString = REPLACE(@mString, char(6), '')
	SET @mString = REPLACE(@mString, char(7), '')
	SET @mString = REPLACE(@mString, char(8), '')
	SET @mString = REPLACE(@mString, char(9), '')
	SET @mString = REPLACE(@mString, char(10), '')
	SET @mString = REPLACE(@mString, char(11), '')
	SET @mString = REPLACE(@mString, char(12), '')
	SET @mString = REPLACE(@mString, char(13), '')
	SET @mString = REPLACE(@mString, char(14), '')
	SET @mString = REPLACE(@mString, char(15), '')
	SET @mString = REPLACE(@mString, char(16), '')
	SET @mString = REPLACE(@mString, char(17), '')
	SET @mString = REPLACE(@mString, char(18), '')
	SET @mString = REPLACE(@mString, char(19), '')
	SET @mString = REPLACE(@mString, char(20), '')
	SET @mString = REPLACE(@mString, char(21), '')
	SET @mString = REPLACE(@mString, char(22), '')
	SET @mString = REPLACE(@mString, char(23), '')
	SET @mString = REPLACE(@mString, char(24), '')
	SET @mString = REPLACE(@mString, char(25), '')
	SET @mString = REPLACE(@mString, char(26), '')
	SET @mString = REPLACE(@mString, char(27), '')
	SET @mString = REPLACE(@mString, char(28), '')
	SET @mString = REPLACE(@mString, char(29), '')
	SET @mString = REPLACE(@mString, char(30), '')
	SET @mString = REPLACE(@mString, char(31), '')

    RETURN @mString    
END  

0부터 31까지 순차적으로 있어서 While Loop를 사용하면 간결하게 보일 수 있긴 하지만, 데이터가 많은 경우(ex 100만개 이상의 row) 2배 이상의 속도 차이가 날 수 있다. 성능을 고려해야 한다면, 최대한 필요한 Control code를 특정 지어서 직접 치환하는 것을 권장한다.

Power BI Compare with previous row value

시간에 따라 변화하는 등의 시계열 데이터가 있을 때, 이전 값과 얼만큼 변화 했는지 확인하기 위해서 current row value와 previous row value를 비교 해볼 수 있다.

COVID19 Sample 데이터를 Power BI에 Load 해보자

COVID19-Sample

각 row 데이터를 식별하기 위해서 Index 열을 추가한다.
(참고, 별도의 설정 없이 기본으로 추가된 인덱스는 0부터 시작한다.)

Add Index

이제, 사용자 지정 열을 추가 하여서 Current row에 Previous row value를 추가한다.

사용자 지정 열

사용자 지정 열에는 Script를 추가 할 수 있는데 아래 스크립트를 넣어주면 Previous row에 있는 “인원[명]” 값을 Current row에 추가 할 수 있다.

try 
 #"추가된 인덱스" {[인덱스] + 1} [#"인원[명]"]
otherwise 0
Current row에 Previous row value가 추가됨.

두 개의 값의 비율을 계산하여 증감률을 계산해 낼 수 있다.
다시 한번 사용자 지정 열을 추가 하여 비율 계산 식을 추가한다.

사용자 지정 열
[#"인원[명]"] / [Previous value] * 100

다음과 같이 증감율에 대한 데이터를 확인해 볼 수 있다.

Rate(증감률) 열 추가

만든 증감율 데이터를 가지고 다음과 같이 시각화 할 수 있다.

코로나 차트

선 그래프는 2020년 7월부터 2021년 2월까지 신규 확진자 수를 나타내고 있으며 막대 그래프는 전일 대비 신규 확진자 증감률을 나타내고 있다. 막대 그래프의 색은 증감률 데이터가 100 이하로 감소하면 푸른 계열로 바뀌고, 증가하면 붉은 계열로 바뀌도록 되어 있다.

MS-SQL Stored procedure에 List(or Array) parameter 전달하기

Custom type을 정의하면 Stored procedure(이하 SP)에 List(or Array) 형태의 정보를 전달하여 처리 할 수 있다.

먼저, List로 전달할 정보를 담을 Custom table type을 정의한다.
객체 정보를 담는 Class를 정의한다고 생각하면 된다.

CREATE TYPE CodeList
AS TABLE
(
  Code varchar(32)  
);

SP에 변수를 type을 위에서 만든 Custom type으로 정의한다. 그러면, SP는 정의된 Table 정보를 받아 처리 할 수 있게 된다.

CREATE PROCEDURE GetInfoByCodes
	@pCodeList AS CodeList READONLY
AS 
BEGIN
	SET NOCOUNT ON;
	SELECT * FROM InfoTable
	WHERE Code in (SELECT * FROM @pCodeList)	
END
GO

임의로 Custom table type으로 만든 변수를 만들어 다음과 같이 SP를 실행해 볼 수 있다.

DECLARE @pCodes CodeList
INSERT INTO @pCodes VALUES('A40274208')
INSERT INTO @pCodes VALUES('A10028014') 
INSERT INTO @pCodes VALUES('A56087115') 

EXEC GetInfoByCodes @pCodeList = @pCodes

.NET Framework 4.X 에서 Option Pattern 사용하기

Option Pattern은 Ubiquitous design pattern 중 하나로 web.config와 같은 Application의 모든 환경 설정 넣어서 사용하는 파일을 목적에 맞는 클래스들로 분리 시키고 인터페이스로 추상화하여 사용할 수 있도록 해준다.

이러한 이유로, Option Pattern을 사용하면 소프트웨어 엔지니어링 원칙중 2가지를 따를 수 있게 된다. 하나의 기능에 대한 환경 설정을 위한 클래스로 분리해서 사용하기 때문에 SRP(Single Responsibility Principle)를 따르게 되며, 인터페이스를 통해서 클래스의 값을 주입 받기 때문에 ISP(Interface Segregation Principle)를 따르게 된다.

기존에 web.config에 설정된 환경 변수들을 사용할때는 다음과 같이 String 타입인 XML 값을 읽어와야 했다. 그리고 필요에 따라 String 타입을 필요한 타입에 맞추어 Cast 해서사용해야 했다.

string SecretName = ConfigurationManager.AppSettings["SecretName"]

하지만, Option Pattern을 사용하다면, 미리 정의된 클래스 속성에 맞추어 Injection 과정에서 Cast가 진행되기 때문에 바로 사용할 수 있는 편리함이 있다. 그리고 미리 정의된 클래스 속성 이름을 참조하여 사용하기 때문에 항상 동일한 이름을 일관되게 코드에 적용 할 수 있게 되며, 참조 추적을 통해 얼마나 많은 곳에 해당 환경 설정이 사용되고 있는지 영향도 파악을 할 수 있게 되는 장점을 얻을 수 있다.

Option Pattern은 .Net Core부터는 Option pattern이 기본 패키지로 제공되기 때문에 쉽게 사용할 수 있다. 하지만 점점 EOS(End Of Service)가 공지되고 있는 .Net Framework는 그렇지 않기 때문에 만들어 사용해야 한다.

아래 구현된 소스코드는 .Net Framework 4.8 LTS 기준으로 작성되었고 Injector는 Simple Injector를 사용하였다. 전체 소스는 Github를 참고하길 바란다.

Web.Config

<appSettings>    
  <!--Application Settings-->
  <add key="SecretName" value="Name" />
  <add key="SecretKey" value="p@ssWord!" />    
</appSettings>

Options Class

public class SecretOptions
{    
    [Option("SecretName")]
    public string secretName { get; set; }
               
    [Option("SecretKey")]
    public string secretKey { get; set; }             
}

Options Interface

public interface IOptions<T>
{
    T Value { get; }
}

Options Concrete

public class Options<T> : IOptions<T>
{
    private readonly T model;
    private readonly List<string> AppSettingNames;
    
    public Options()
    {      
        AppSettingNames = ConfigurationManager.AppSettings.AllKeys.ToList();           

        T instance = Activator.CreateInstance<T>();

        model = (T)SetConfig(instance.GetType());
    }

    private object SetConfig(Type type)
    {
        object instance = Activator.CreateInstance(type);

        var instanceProperties = instance.GetType().GetProperties();

        foreach (var property in instanceProperties)
        {
            //If it is a hierarchical option class, it is searched recursively.
            if (property.GetCustomAttribute<OptionObjectIgnoreAttribute>() != null)
            {
                property.SetValue(instance, SetConfig(property.PropertyType));
            }

            //If an attribute does not have an 'OptionAttribute', the value is searched by the attribute's original name. 
            //but if there is, the value is searched by the set name at 'OptionAttribute'
            var name = AppSettingNames.FirstOrDefault(m => property.GetCustomAttribute<OptionAttribute>() == null ?
                                                            property.Name.Equals(m, StringComparison.InvariantCultureIgnoreCase) :
                                                            property.GetCustomAttribute<OptionAttribute>().keyName.Any(n => n.Equals(m, StringComparison.InvariantCultureIgnoreCase)));

            var value = ConfigurationManager.AppSettings.Get(name);

            if (!value.Equals(String.Empty))
            {
                property.SetValue(instance, Convert.ChangeType(value, property.PropertyType));
            }
        }

        return instance;
    }

    public T Value
    {
        get
        {
            return model;
        }
    }
}

Global.asax

var container = new Container();

container.RegisterMvcControllers(Assembly.GetExecutingAssembly());

//Injection form Web.config settings to Option Model  
container.Register<IOptions<SecretOptions>, Options<SecretOptions>>(SimpleInjector.Lifestyle.Singleton);

DependencyResolver.SetResolver(new SimpleInjectorDependencyResolver(container));

Calculating DTU in AWS RDS for MSSQL

On-Prem이나 VM 형태로 사용하던 MSSQL Server를 Azure SQL로 Migration을 한다고 가정해보자. 기존에 사용하던 처리 성능에 맞춰 Cloud 상에 알맞은 수준의 리소스를 준비하고 옮겨야 할 것이다.

기존에 사용하던 SQL Server는 CPU와 Memory, Disk I/O등 Hardware적인 요소로 처리 성능을 나타낸다. 하지만 Azure SQL에서는 DTU(Database Throughput Unit)라는 단위를 사용하여 처리 성능을 나타내기 때문에 성능 비교가 쉽지가 않다.

DTU는 Hardware 성능 요소인 CPU, Disk I/O와 Database Log flush 발생양을 이용하여서 계산해낸 단위이다. 때문에 MSSQL Server에서 각 Metric들을 추출해 낼 수 있다면, DTU를 계산해 낼 수 있다. DTU 계산은 이미 Azure DTU Calculator라는 계산기가 제공되고 있기 때문에 추출해낸 Metric 값들을 설명대로 잘 넣어 주기만 하면 쉽게 확인해 볼 수 있다.

Metric을 추출하는 방법은 대상이 되는 SQL Server에서 DTU Calculator에서 제공하는 PowerShell Script를 관리자 권한으로 실행시켜 주기만 하면 된다. Script는 CPU, Disk I/O, Database Log flush에 대한 Metric들을 DTU Calculator에서 요구하는 형식으로 추출해준다. 때문에 일반적으로 On-Prem이나 VM 형태로 MSSQL Server를 사용하고 있다면, 계산해 내는데 별다른 어려움이 없을 것이다.

하지만 AWS RDS for MSSQL 같은 경우 MSSQL Server를 AWS에서 Cloud Service로 제공하기 위해 Wrapping한 서비스다보니 SQL Server에 대한 관리자 권한을 얻을 수가 없다. 이러한 이유 때문에 DTU Calculator에서 제공하는 PowerShell Script를 사용할 수 가 없다.

이런 경우, DTU 계산에 필요한 Metric들을 AWS에서 별도로 추출해야 한다. AWS에는 Cloudwatch라는 서비스를 제공하고 있는데, AWS 서비스들에 대한 Performance Metric을 기록하고 제공하는 역할을 한다.

Cloudwatch에서 metric을 추출하기 위해서는 AWS CLI를 사용하는 것이 편리하다. AWS Console의 우측 상단에 있는 CLI 실행 아이콘을 눌러 AWS CLI를 실행해 보자.

AWS CLI가 실행 되었다면, 아래 조회 명령어(list-metrics)를 실행시켜 제공되는 metric들에 대한 정보를 확인해 보자.

aws cloudwatch list-metrics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name]

잘 실행되었다면, cloudwatch에서 제공하는 Metric 리스트들을 확인 할 수 있다. 리스트 중 DTU계산에 필요한 CPU Processor Time과 Disk Reads/sec, Writes/sec에 값에 해당 하는 Metric 다음과 같다. (참고로, Log Bytes Flushed/sec에 해당하는 metric은 제공되지 않는다.)

{
    "Namespace": "AWS/RDS",
    "MetricName": "ReadIOPS",
    "Dimensions": [
        {
            "Name": "DBInstanceIdentifier",
            "Value": "[RDS for MSSQL Name]"
        }
    ]
},
{
    "Namespace": "AWS/RDS",
    "MetricName": "WriteIOPS",
    "Dimensions": [
        {
            "Name": "DBInstanceIdentifier",
            "Value": "[RDS for MSSQL Name]"
        }
    ]
},
{
    "Namespace": "AWS/RDS",
    "MetricName": "CPUUtilization",
    "Dimensions": [
        {
            "Name": "DBInstanceIdentifier",
            "Value": "[RDS for MSSQL Name]"
        }
    ]
}

이제, 필요한 metric 정보에 대해서 확인 하였으니 수집해보자. 아래 metric 수집 명령어(get-metric-statistics)를 사용하면 수집된 metric정보가 csv파일로 저장된다.

aws cloudwatch get-metric-statistics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name] --metric-name=CPUUtilization --period 3600 --statistics Average --start-time 2021-01-01T00:00:00.000Z --end-time 2021-02-01T00:00:00.000Z | jq -r '.Datapoints[] | [.Timestamp, .Average, .Unit] | @csv' | cat > cpu.csv

aws cloudwatch get-metric-statistics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name] --metric-name=ReadIOPS --period 3600 --statistics Average --start-time 2021-01-01T00:00:00.000Z --end-time 2021-02-01T00:00:00.000Z | jq -r '.Datapoints[] | [.Timestamp, .Average, .Unit] | @csv' | cat > read.csv

aws cloudwatch get-metric-statistics --namespace AWS/RDS --dimensions Name=DBInstanceIdentifier,Value=[RDS for MSSQL Name] --metric-name=WriteIOPS --period 3600 --statistics Average --start-time 2021-01-01T00:00:00.000Z --end-time 2021-02-01T00:00:00.000Z | jq -r '.Datapoints[] | [.Timestamp, .Average, .Unit] | @csv' | cat > write.csv

생성한 csv 파일은 AWS CLI Storage에 저장 되어있다. 우측 상단의 Actions > Download File 메뉴를 사용하면 로컬 환경으로 다운 받을 수 있다.

3개의 파일 모두 다운 받아서 DTU 계산에 필요한 형식으로 맞춰준다.

  • % Processor Time = CPUUtilization
  • Disk Reads/sec = ReadIOPS
  • Disk Writes/sec = WriteIOPS
  • Log Bytes Flushed/sec에 해당하는 데이터가 없기때문에 0으로 넣어준다.

그림과 같은 형태로 구성될 것이다.

RDS for MSSQL metrics for DTU Calculator

이제, 한땀 한땀 준비한 데이터를 Azure DTU Calculator에 넣어 주기만 하면 되는데 한가지 주의할 점이 있다. 우리가 얻은 데이터는 RDS for MSSQL Server에 대한 정보이다. 때문에 여러 DB Instance에 대한 계산을 하는 Elastic Database 메뉴를 선택하여 계산 하도록 해야한다.

계산이 끝나면 아래와 같이 나오는데 이 결과를 통해서 AWS RDS에서 사용하던 성능 수준을 Azure SQL에서 그대로 사용하기 위해서 구성 해야하는 Service Tire/Performance Level을 확인 할 수 있다.

DTU Result

[공유]모자이크 처리된 글자들을 복원하는 프로그램

민감 정보(예: 연락처, 비밀번호 등)가 있는 이미지를 게시할 때 해당 민감 정보가 있는 부분을 모자이크 처리(픽셀화)를 하여서 대상을 식별할 수 없게 한다. 그러나 사실 이런 모자이크 처리된 원본 이미지의 캐릭터 라인을 파악할 수 있다는 ‘함정’이 존재한다.

이런 함정을 잘 이용하여 모자이크 처리된 이미지를 Linear box filter 처리 알고리즘을 이용하여 간단히 복원하는 파이썬 프로그램이다.

[출처]Github – beurtschipper

이제는 이미지에서 민감정보는 모자이크 처리 보다는 해당 부분을 잘라내던가 완전히 색을 덮어 안보이게 하는 것이 안전할 것 같다.

Dependency Injection Service Lifetimes

DI(Dependency Injection)을 구성할때 lifetime을 정의해야한다. lifetime은 DI가 동작할때 서비스 인스턴스가 생성되는 조건을 정의 하는 것이다.
(참고로, DI에 대한 개념 설명은 “Dependency Injection 개념과 Ninject 사용법”이란 포스팅에 설명해 두었다. 추가적인 내용이 필요 하다면 참고하길 바란다.)

lifetime에는 다음과 같이 3가지가 있다.

  1. Transient – 모든 요청에 대해서 매번 새로 인스턴스를 생성한다.
  2. Scoped – scope 당 1회 인스턴스를 생성한다.
  3. Singleton – DI가 구성되고 특정 시점에 1회 인스턴스를 생성되고 나면, 모든 호출에 대해서 동일한 인스턴스를 재활용한다.

글로만 읽으면 잘 이해가 가지 않는다. 좀 더 쉽게 이해를 하기 위해서 간단히 코딩을 하여 알아보자.

.Net Core Console 프로젝트를 생성하고 Progrma.cs에 다음과 같이 코드를 넣어 보자.

namespace DILifetimesTest
{
    public interface IService
    {
        void Info();
    }

    public interface ISingleton : IService { }
    public interface IScoped : IService { }
    public interface ITransient : IService { }

    public abstract class Operation : ISingleton, IScoped, ITransient
    {
        private Guid _operationId;
        private string _lifeTime;

        public Operation(string lifeTime)
        {
            _operationId = Guid.NewGuid();
            _lifeTime = lifeTime;

            Console.WriteLine($"{_lifeTime} Service Created.");
        }

        public void Info()
        {
            Console.WriteLine($"{_lifeTime}: {_operationId}");
        }
    }

    public class SingletonOperation : Operation
    {
        public SingletonOperation() : base("Singleton") { }
    }
    public class ScopedOperation : Operation
    {
        public ScopedOperation() : base("Scoped") { }
    }
    public class TransientOperation : Operation
    {
        public TransientOperation() : base("Transient") { }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var serviceProvider = new ServiceCollection()
               .AddTransient<ITransient, TransientOperation>()
               .AddScoped<IScoped, ScopedOperation>()
               .AddSingleton<ISingleton, SingletonOperation>()
               .BuildServiceProvider();


            Console.WriteLine("========== Request 1 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");

            Console.WriteLine("========== Request 2 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");
                        
            using (var scope1 = serviceProvider.CreateScope())
            {
                Console.WriteLine("========== Request 3 (scope1)============");
                scope1.ServiceProvider.GetService<ITransient>().Info();
                scope1.ServiceProvider.GetService<IScoped>().Info();                
                scope1.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");

                Console.WriteLine("========== Request 4 (scope1)============");
                scope1.ServiceProvider.GetService<ITransient>().Info();
                scope1.ServiceProvider.GetService<IScoped>().Info();
                scope1.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");
            }

            using (var scope2 = serviceProvider.CreateScope())
            {
                Console.WriteLine("========== Request 5 (scope2)============");
                scope2.ServiceProvider.GetService<ITransient>().Info();
                scope2.ServiceProvider.GetService<IScoped>().Info();
                scope2.ServiceProvider.GetService<ISingleton>().Info();
                Console.WriteLine("========== ========= ============\r\n");
            }

            Console.WriteLine("========== Request 6 ============");
            serviceProvider.GetService<ITransient>().Info();
            serviceProvider.GetService<IScoped>().Info();
            serviceProvider.GetService<ISingleton>().Info();
            Console.WriteLine("========== ========= ============\r\n");

            Console.ReadKey();
        }
    }
}

코드를 실행해 보면 다음과 같이 결과를 얻을 수 있다.

========== Request 1 ============
Transient Service Created.
Transient: 2abfdf9f-5223-4a2c-953f-1c7269745073
Scoped Service Created.
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton Service Created.
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 2 ============
Transient Service Created.
Transient: 883343bd-0f24-4416-a56c-04b1a7cba89f
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 3 (scope1)============
Transient Service Created.
Transient: 5eba1fb5-f53b-49cc-bab4-5e1e103b4e17
Scoped Service Created.
Scoped: a0cdf863-e10b-4925-8731-3fe0f2a085ff
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 4 (scope1)============
Transient Service Created.
Transient: d6124182-1fbc-402d-997f-fd9024fa5187
Scoped: a0cdf863-e10b-4925-8731-3fe0f2a085ff
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 5 (scope2)============
Transient Service Created.
Transient: 8a200580-965e-4d77-bfad-08cc722e6a13
Scoped Service Created.
Scoped: 72c67b06-6296-49da-b162-853c6f16e1d0
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

========== Request 6 ============
Transient Service Created.
Transient: 0885afac-3b12-4c8a-8e60-0fb68b0f32a4
Scoped: ab819f16-7b39-4f61-b077-3f7c5e38025b
Singleton: 47e04473-f743-41ec-9f2a-e2b08e1a216e
========== ========= ============

출력된 결과를 살펴 보면 Transit은 매 서비스 요청마다 새로 인스턴스가 생성된 것을 확인 할 수 있다.

다음으로 Scoped는 1번 요청에서 생성된것이 2번 6번에서 재활용되었고, 그 외 Scope가 새로 할당 될 때는 새 인스턴스가 생성되고 해당 Scope안에서는 Scope가 종료될때까지 동일한 인스턴스가 재활용되는 것을 확인 할 수 있다.

마지막으로 Sigleton은 1회 인스턴스가 생성되면, 요청이나 Scope 할당과 상관없이 동일한 인스턴스가 재활용 되는 것을 확인 할 수 있다.

위와 같이 각각 lifetime 옵션에 따라 서비스 처리 특성이 다르기 때문에 서비스 요청 특성에 맞춰 알맞는 lifetime을 설정하는 것이 중요할 것 같다.

[출처] : https://www.c-sharpcorner.com/article/dependency-injection-service-lifetimes/

WSL(Windows Subsystem for Linux) Ubuntu CRON Job 사용하기

CRON(Command Run On)이란, Unix System에서 제공하는 Job scheduler를 말한다. CRON은 Shell 명령어들이 주어진 일정에 주기적으로 실행하기 위해서 crontab(CRON Table)이란 것을 참조하고, crontab에는 실행시킬 Job 목록과 일정이 기록되어 있다.

CRON Table

사실, Windows에서도 Task scheduler라는 기능을 사용하면 Scheduling을 얼마든지 할 수 있긴 하다. 하지만 간혹 이미 Linux shell script로 제공되는 경우 등 Linux 환경에서의 작업이 더 유리한 상황인 경우에는 해당 script를 Windows 용으로 변경해야 하는 번거로움이 있다.

이럴때 WSL을 이용하면 좋다. WSL은 Ubuntu환경을 거의 대부분 제공하기 때문에 별도의 Linux 환경을 구성하지 않아도 주어진 Shell Script를 그대로 이용하면 Scheduling이 가능하다.

예를 들어, Linux System에 대한 Health check를 주기적으로 매시간마다 기록해야하는 상황이라고 가정해보자. Linux System이기 때문에 Bash shell script로 작업하는 것이 유리 할 것이다.

먼저, CRON Scehduler가 Backgroud에서 동작하기 위해서 daemon service를 설치 및 실행해야한다.

# Install CRON package
$ sudo apt-get udpate
$ sudo apt-get install cron

# CRON Service start
$ sudo service cron start

CRON이 정상적으로 실행되었다면, 다음으로 Health check를 위한 Script를 준비해야한다. Script는 직접 작성하여도 좋지만, 미리 잘 만들어져서 공개된 Open Source Script를 사용하면 쉽게 작업 할 수 있다.

WSL(Ubuntu-18.04)를 실행시켜 미리 잘 준비된 Script를 가져와 보자.
(Linux health check script는 여기를 참조하였다. Script code는 Apache 2.0 License로 배포되었다.)

$ wget https://tecmint.com/wp-content/scripts/tecmint_monitor.sh

Shell Script가 준비가 되었다면, WSL에서 CRON에 등록하여 사용하기만 하면 된다.
다음 명령어를 실행하면 CRON Table을 편집할 수 있는 편집창이 나온다.

$ crontab -e

아래 Command Code를 그림과 같이 편집창에 실행될 Schedule과 함께 넣어준다.

# monitor script 실행 결과를 result text 파일에 출력한다.
0 * * * * /home/newth/CRON/tecmint_monitor.sh >> /home/newth/CRON/result.txt
CRON Table 편집창

저장하고 나면 CRON Table에 Job Schedule이 등록된 것이다. 등록된 Schedule로 인해서 CRON은 매 시간 정각마다 Command에 입력된 절대경로 shell script를 실행시킬 것이다.

정상적으로 실행 되는지 확인해 보고 싶다면 다음과 같이 syslog를 확인해 보면된다.

$ tail -f /var/log/syslog

CRON Table에 등록한 대로 동작한 결과를 확인 해 볼 수 있다.

$ /var/log$ tail -f syslog

Feb 14 22:45:28 DESKTOP-LM4ALRO crontab[331]: (newth) BEGIN EDIT (newth) --CRON Table 편집 
Feb 14 22:45:33 DESKTOP-LM4ALRO crontab[331]: (newth) END EDIT (newth) --CRON Table 편집 종료
Feb 14 22:45:35 DESKTOP-LM4ALRO crontab[342]: (newth) LIST (newth) --CRON Table 리스트 확인
... 생 략 ...
Feb 14 23:00:00 DESKTOP-LM4ALRO CRON[433]: (newth) CMD (/home/newth/CRON/tecmint_monitor.sh | cat > /home/newth/CRON/result.txt) --CRON Table 실행 Log


/bin/sh^M: bad interpreter 해결 방법

Linux에서 Shell Script를 실행 할때, “/bin/sh^M: bad interpreter”라는 오류가 발생할 때가 있다. 그리고 오류 내용은 입력하지도 않은 ‘^M’이란 문자때문에 오류가 발생한 것으로 나온다.

작성한 Shell Script를 열어 내용을 아무리 살펴보아도 ‘^M’이 입력된 부분이 있다거나 잘 못 작성된 부분이 있는 것을 찾기 힘들어 엄청 열 받을 것이다.

먼저, 이 오류에 대부분의 원인에 대해서 말하자면 Windows에서 작성한 Script를 Linux에서 바로 실행 하려고 하면 나타나는 현상인데, Windows와 Linux의 개행문자를 표현 하는 방식이 다르기 때문이다.

개행문자란 무엇인가?

개행문자(혹은 Enter)는 대부분의 편집 툴에서 편집 작업시 눈에 보이지는 않지만 줄바꿈이 있을 때마다 자동으로 입력되는 문자이다. 그리고 개행문자는 앞서 언급했듯이 운영체제 별로 표현법이 다르다.

Windows에서는 CRLF(\r\n)으로 표현하는데 그 의미는 다음과 같다.

CR(Carriage Return, \r)은 새로운 행을 추가하고 LF(Line Feed, \n)는 시작위치로 돌아간다는 의미이다.

반면에, Linux에서는 LF(\n)으로만 표현하도록 되어 있다.

때문에 Windows에서 작성한 문서를 Linux에서 보면 CR이 개행문자로 인식되지 못하고 ‘^M’으로 인식 되어 버린다. 그리고 이런한 이유로 오류가 발생하게 된다.

그럼 어떻게 보이지도 않는 “^M” 문자를 처리해야 하는가?

해결법은 간단하다. VI 편집기를 사용할때 옵션 [-b]를 다음과 같이 사용하여 실행하면 바이너리 모드로 문서를 볼 수 있는데 확인해 보면 모든 줄 끝에 ‘^M’문자가 들어간 것을 확인 할 수 있다.

$ vi -b [수정 할 문서 이름]
Windows에서 작성한 문서를 Linux에서 바이너리 모드로 오픈한 화면

모든 줄마다 붙어있는 ‘^M’ 문자를 모두 삭제해 하고 다시 실행해보면 “/bin/sh^M: bad interpreter” 오류는 해결 될 것이다.

Using the R programming language in Jupyter notebook

Jupyter notebook을 기본값으로 사용하다 보면 Python만 사용할 수 있게 되는 경우가 있다. 이런 경우 R programming으로 작업한 내용을 보려고 하면, R studio 등 별도의 Tool로 작업 내용을 열어 봐야 한다. 그런데 이렇게 여러개의 툴을 쓴다는게 번거롭고 귀찮을 수 있다.

이런 수고로움을 조금이나마 줄이기 위해서 Jupyter notebook에서 R을 실행하는 방법을 알아보겠다.

먼저, 터미널에서 대상이 되는 Jupyter notebook이 설치된 가상환경을 활성화 한다.

가상환경이 활성화된 터미널

다음으로 아래 명령문을 실행 시켜주면 Jupyter notebook에 R 패키지가 설치 된다.

# R 실행
$ R

# IRkernel  패키지 설치
$ install.packages(‘IRkernel’)

# 커널 설치 확인
$ IRkernel::installspec()

# Jupyter notebook 실행
$ jupyter notebook

실행된 Jupyter notebook에 접속하여 “New”버튼을 눌러 확인해보자. 다음과 같이 R programming을 할 수 있는 R Notebook을 생성할 수 있을 것이다.

R Notebook 생성

이제, Jupyter notebook에서 Python과 R작업을 모두 수행하면 된다.