728x90
반응형

Javascript Async (비동기 처리)

2023.12.06에 작성됨

 

Javascript에서 비동기처리를 하기 위해서는 Promise 객체를 이용한다.

Promise는 3가지 중 하나의 Status를 갖는다.

  • pending (작업이 진행중인 상태)
  • fulfilled (작업이 성공적으로 완료된 상태)
  • rejected (작업이 실패한 상태)

Javascript의 비동기는 두 부분으로 나누어 생각할 수 있는데 Promise를 생성하는 Producer 와 사용하는 Consumer 이다.

  1. Producer:

Promise 객체는 executor(resolve, reject) 라는 콜백 함수를 인자로 받아서 생성하며,

Promise 객체가 생성되는 순간 executor는 실행된다.


const promise = new Promise((resolve, reject) => {

// ... 어떤 작업 처리 ...

// 성공했을 경우

resolve();

// 실패했을 경우 

reject();

});
  1. Consumer:

Promise로부터 나온 결과를 처리하는 부분이다.


promise

.then((value) => {})      // promise가 resolve()를 호출할 경우 실행됨.

.catch((error) => {})     // promise가 reject()를 호출할 경우 실행됨.

.finally(() => {});       // promise의 성공여부와 상관 없이 완료된 경우 최종적으로 실행됨.

Async/Await

Async/Await은 Promise를 조금 더 편리하게 사용할 수 있도록 고안된 문법 이다. (Syntactic Sugar)

Promise를 직접 사용한 비동기 함수와 Async/Await을 사용하여 만든 함수를 아래 예제를 통해 비교해보자.


// Promise 를 사용한 경우 

function readFileAsync() {

return new Promise((resolve, reject) => {

// ...어떤 작업 처리...

resolve(value);

});

}



function logFile() {

const file = readFileAsync();

file.then((value) => console.log(value));

}



logFile();

// Async-Await을 사용한 경우

async function readFileAsync() {

// ...어떤 작업 처리...

return value;

}

// 설명: 

// 함수 앞에 async를 붙이면 내부적으로 Promise를 반환하는 함수로 변환해준다. 

// 함수에서 value를 바로 리턴하는 것 처럼 보이지만 실제로는 Promise 객체를 반환한다. 



async function logFile() {

const file = await readFileAsync();

console.log(file);

}

// await 은 async 함수 안에서만 사용이 가능하며, 

// .then으로 처리했던 것과 같이 비동기함수의 수행 완료를 기다렸다가 

// resolve() 함수에 의해 처리된 값을 반환해준다. 

// reject() 함수에 의해 처리된 값을 처리할 때는 try-catch문을 통해서 처리할 수 있다. 



logFile();



참고:

https://youtu.be/JB_yU6Oe2eE?si=9z4D5QRuaqy4XRkO

https://youtu.be/aoQSOZfz3vQ?si=RVdbsuqfNgtwG_wp

728x90
반응형
728x90
반응형

Typescript 세팅하기


 

2023.12.06에 작성됨 (node: v18.17.0, npm: 10.2.1)

시작하기

# 글로벌로 설치할 경우
npm i -g typescript   
tsc --init            # tsconfig.json 생성됨

# 개별 프로젝트에 설치 할 경우
npm i -D typescript   
npx tsc --init        # 프로젝트에 설치된 경우 앞에 npx를 붙여줘야한다.

NOTE: 많은 사람들이 ts-node를 같이 같이 설치 해주는 것 같은데 Typesciprt도 VS Code에서 디버깅 모드를 바로 사용할 수 있기 때문에 굳이 필요한지는 잘 모르겠다.

// tsconfig.json
{
    "compilerOptions": {
        // 일반적으로 사용하는 option
        "target": "ES2016",
        "module": "commonjs",
        "rootDir": "./src",
        "outDir": "./dist",
        "removeComments": true,
        "noEmitOnError": true,
        // 유용한 Type Checking Options
        "noUnusedParameters": true,
        "noImplicitReturns": true,
        "noUnusedLocals": true,
        // 디버깅 사용할 때
        "sourceMap": true,
    }
}

NOTE: “tsconfig.json” 이 생성되면 뒤에 파일이름을 붙이지 않아도 tsc만 입력해서 컴파일을 실행시킬 수 있음.

VS Code에서 디버깅하기

  1. tsconfig.json
    • compile 할 때 [filename].js.map 이라는 파일이 생성되고 디버거가 읽는 파일이라고 생각하면됨.
  2. “sourceMap”: true
  3. create launch.json
    • “preLaunchTask”: “tsc: build - typeconfig.json” 추가
    // launch.json
    // 기본적으로 생성되는 파일 안에 
    // “preLaunchTask”: “tsc: build - typeconfig.json” 추가
    {
      // Use IntelliSense to learn about possible attributes.
      // Hover to view descriptions of existing attributes.
      // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
      "version": "0.2.0",
      "configurations": [
        {
          "type": "node",
          "request": "launch",
          "name": "Launch Program",
          "skipFiles": [
            "<node_internals>/**"
          ],
          "program": "${file}",
          "preLaunchTask": "tsc: build - tsconfig.json",
          "outFiles": [
            "${workspaceFolder}/**/*.js"
          ]
        }
      ]
    }

Typescript CLI (Command Line Interface)

tsc --build (혹은 tsc -b)를 실행하면 다음의 작업을 합니다:

  • 참조된 모든 프로젝트를 찾습니다
  • 최신 상태인지 감지합니다
  • 올바른 순서로 최신 상태가 아닌 프로젝트를 빌드 합니다

만약 config 파일 이름이 tsconfig.json이라면 이름을 지정하지 않고 tsc --project(혹은 tsc -p)를 사용할 수 있다.

일반적으로는 package.json 파일에 스크립트로 저장하고 사용한다.

//package.json
"scripts": {
    "build": "tsc -p"
}
// terminal> npm run build

Common Errors

  1. Cannot find module 'xxx' or its corresponding type declarations.ts(2307)
import path from "path"; 
// Cannot find module 'path' or its corresponding type declarations.ts(2307)

NOTE: Typescript는 Typescript가 아닌 기본 Node 패키지를 바로 사용할 수 없다. (타입을 추론할 수 없기 때문에). 아래와 같이 Typescript 전용 패키지를 설치 해주어야 한다.

# npm i @types/<패키지>
npm i -D @types/node
# 일반적으로 -D 옵션을 붙여 Development Dependency 로 설치해준다.

참고:

https://youtu.be/d56mG7DezGs?si=UvvP1NosR6QV23mZ

 

728x90
반응형
728x90
반응형

이 글은 Head First에서 나온 Design Patterns 책을 보고 이해한 내용을 나름대로 정리한 내용입니다.

옵저버 패턴(Observer Pattern)은 출판사 와 구독자와 같은 관계를 갖는 패턴이다. 구독자들이 출판사에 구독을 신청하면 출판사는 신문이 출간될 때마다 구독자들에게 신문을 저달한다. 구독자가 구독을 해지하면 더이상 신문을 받지 않는다. 이렇게 옵저버 패턴은 일대다 의존성을 정의하는 패턴이다.

옵저버 패턴에서 일반적으로 출판사 열할을 하는 객체를 Subject 라고 부르고 구독자 객체를 Observer라고 부른다. 모든 Subject는 자신에게 "구독 추가", "구독 해지" 메소드가 있다는 것을 구독자들에게 알려줘야하고, 반드시 알림(신문을 전달하는 행동) 메소드가 필요하다. 이를 추상화 하여 인터페이스로 구현해보자.

public interface ISubject
{
    void AddObserver(IObserver observer);  // 구독자 추가
    void RemoveObserver(IObserver observer); // 구독자 삭제
    void NotifyObserver(); // 구독자들에게 알림
}

이번엔 구독자인 Observer를 보자. 모든 ObserverSubject에게 "나에게는 Update()라는 메소드가 있으니 필요하면 이 메소드를 통해 전달해주세요" 라고 알려줘야한다. 이를 추상화 하여 인터페이스로 만든다.

public interface IObserver
{
    void Update(Object obj); // Subject가 갱신된 내용을 전달하는 메소드
}

* 이 예제에서는 범용적으로 사용하기 위해 Update의 인자로 Object를 넣었는데 구체적인 객체로 받아도 상관 없다.

이제 구체적인 상황을 설정해보자. 나는 WebParser라는 클래스를 Subject로 만들어 3초마다 웹에서 파싱한 내용을 구독자들에게 저달하려고 한다. 시뮬레이션이기 실제 웹파싱을 하진 않고 3초마다 랜덤 String을 만들어 웹에서 긁어온 키워드라고 가정을했다.

public class WebParser : ISubject 
{
    private List<IObserver> _observers = new List<IObserver>();

    public string WebContents { get; set; }

    // 생성자에서 시뮬레이터를 동작시킴
    public WebParser()
    {
        Task.Run(() => SimulateWebParser());
    }

    // 웹 파싱 시뮬레이터 메소드
    private void SimulateWebParser()
    {            
        while(true)
        {
            Thread.Sleep(3000);
            WebContents = RandomStringGenerator();
            NotifyObserver();
        }
    }

    // 랜덤으로 String을 생성해주는 메소드
    private string RandomStringGenerator()
    {
        Random random = new Random();
        const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        return new string(Enumerable.Repeat(chars, 10)
            .Select(s => s[random.Next(s.Length)]).ToArray());
    }

    // Observer가 구독을 신청하는 메소드
    public void AddObserver(IObserver subscriber)
    {
        _observers.Add(subscriber);
    }

    // Observer가 구독을 해지하는 메소드
    public void RemoveObserver(IObserver subscriber)
    {
        if (_observers.Contains(subscriber))
            _observers.Remove(subscriber);
    }

    // 구독중인 Observer들에게 변경을 알려주는 메소드
    public void NotifyObserver()
    {
        foreach(var observer in _observers)
        {
            // Observer의 Update 메소드이용해 갱신해줌.
            observer.Update(this);
        }
    }
}

랜덤 String을 만드는 작업 때문에 코드가 조금 길어졌는데 중요한 부분은 ISubject 인터페이스의 AddObserver(), RemoveObserver(), NotifyObserver()를 어떻게 구현했는지만 보면 된다.

이제 마지막으로 간단한 Observer들을 만들어보자. 나는 WebParser로부터 전달받은 내용을 디스플레이해주는 객체와 이메일로 전달 해주는 두 가지 Observer를 만들었다.

public class DisplayWebContents : IObserver
{
    public DisplayWebContents(WebParser webParser)
    {
        webParser.AddObserver(this);
    }
    public void Update(object obj)
    {
        if (obj is WebParser)
        {
            var webParser = obj as WebParser;
            Console.WriteLine($"Display {webParser.WebContents}"); 
        }
    }
}

public class EmailWebContents : IObserver
{
    public EmailWebContents(WebParser webParser)
    {
        webParser.AddObserver(this);
    }
    public void Update(object obj)
    {
        if (obj is WebParser)
        {
            var webParser = obj as WebParser;
            Console.WriteLine($"Email {webParser.WebContents}");
        }
    }
}

Observer 구상클래스를 보면 알겠지만 구독을 신청하기 위해 Subject의 구상클래스를 반드시 포함하고 있다. 클래스 다이어그램을 보면서 다시 한 번 확인해보자.

이제 마지막으로 Main 함수와 실행 결과를 확인해보자.

class Program
{
    static void Main(string[] args)
    {
        WebParser wp = new WebParser();

        DisplayWebContents display = new DisplayWebContents(wp);
        EmailWebContents email = new EmailWebContents(wp);

        Thread.Sleep(100000);
    }
}

실행결과:

3초에 한번식 랜덤 String이 생성되고 디스플레이와 이메일이 업데이트됨.

이렇게 옵저버 패턴에서는 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체들한테 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다(one-to-many) 의존성을 정의한다.

참고 자료: Head First - Design Pattern

728x90
반응형
728x90
반응형

이 글은 Head First에서 나온 Design Patterns 책을 보고 이해한 내용을 나름대로 정리한 내용입니다.

Stratagy pattern은 알고리즘 집합을 정의하고 각각 캡슐화 하여 교환해서 사용할 수 있도록 한 것이다.

스타크래프트 예제로 두 개의 알고리즘 집합을 정의 하려고 한다. 하나는 '공격'에 대한 알고리즘 집합이고, 다른 하나는 '이동'에 대한 알고리즘 집합이다.

두 개의 알고리즘 집합 :

  • IAttackBehavior (인터페이스)
  • IMoveBehavior (인터페이스)
public abstract class Unit
{
    // 모든 유닛은 '공격'과 '이동'이라는 두가지 행동이 가능함.
    private IAttackBehavior _attackBehavior;
    private IMoveBehavior _moveBehavior;

    public void SetAttackBehavior(IAttackBehavior attackBehavior)
    {
        _attackBehavior = attackBehavior;
    }
    public void SetMoveBehavior(IMoveBehavior moveBehavior)
    {
        _moveBehavior = moveBehavior;
    }
    public void PerformAttack()
    {
        _attackBehavior.Attack();
    }
    public void PerformMove()
    {
        _moveBehavior.Move();
    }
}

public interface IAttackBehavior
{
    void Attack();
}

public interface IMoveBehavior
{
    void Move();
}

각 집합은 인터페이스로 추상화하여 나중에 구체적인 알고리즘을 만들때 이 인터페이스들을 상속받도록 한다.

구체적인 행동(알고리즘) :

// 공격에 대한 구체적인 알고리즘 1. 원거리 공격
public class AttackFar : IAttackBehavior
{
    public void Attack()
    {
        Console.WriteLine("원거리 공격!!");
    }
}

// 공격에 대한 구체적인 알고리즘 2. 근거리 공격
public class AttackNear : IAttackBehavior
{
    public void Attack()
    {
        Console.WriteLine("근거리 공격!!");
    }
}

// 이동에 대한 구체적인 알고리즘 1. 땅에서 이동
public class MoveGround : IMoveBehavior
{
    public void Move()
    {
        Console.WriteLine("땅에서 이동");
    }
}

// 이동에 대한 구체적인 알고리즘 2. 하늘에서 이동
public class MoveSky : IMoveBehavior
{
    public void Move()
    {
        Console.WriteLine("하늘에서 이동");
    }
}

이제 유닛을 만들 준비가 다 되었다.
저글링과 뮤탈리스크를 만들어보자.

public class Zergling : Unit
{
    public Zergling()
    {
        // 저글링은 근거리 공격을 하고 땅에서 이동함.
        SetAttackBehavior(new AttackNear());
        SetMoveBehavior(new MoveGround());
    }
}

public class Mutalisk : Unit
{
    public Mutalisk()
    {
        // 뮤탈리스크는 원거리공격을하고 하늘에서 이동함.
        SetAttackBehavior(new AttackFar());
        SetMoveBehavior(new MoveSky());
    }
}

생성자에 '공격'과 '이동'에 대한 구체적인 행동을 할당해주어 태어나면서 구체적인 행동을 갖도록 했다.

이제 Main 함수와 실행 결과를 확인해보자.

class Program
{
    static void Main(string[] args)
    {
        // 저글링
        Zergling myZergling = new Zergling();
        myZergling.PerformMove();
        myZergling.PerformAttack();


        // 뮤탈리스크
        Mutalisk myMutalisk = new Mutalisk();            
        myMutalisk.PerformMove();
        myMutalisk.PerformAttack();

        // 뮤탈리스크가 갑자기 땅으로 이동하고싶어짐.
        Console.WriteLine("\n땅으로 이동하고싶다~");
        myMutalisk.SetMoveBehavior(new MoveGround());
        myMutalisk.PerformMove();
        myMutalisk.PerformAttack();
    }
}

지금까지 만든 프로그램을 Class Diagram으로 다시한 번 확인해보자.

stratagy 패턴을 사용하면 이렇게 알고리즘 집합을 추상화 함으로써 알고리즘을 동적으로 교환할 수 있도록 한다.

참고 자료: Head First - Design Pattern

728x90
반응형
728x90
반응형

PlantUML은 프로그램언어처럼 코드로 다이어그램을 작성할 수 있도록 도와주는 오픈소스 툴이다. 코드로 작성하기 때문에 빠르게 작성할 수 있다는 장점도 있지만 중요한 키워드들은 외워놓던지 찾아가면서 작성해야한다는 단점도 있다.

VSCode 에도 plantUML을 지원하는 Extension이 있어 설치 및 사용 방법을 정리하고자 한다.

  1. 일반적인 Extension 설치와 마찬가지로 PlantUML을 검색하고 설치한다.
  2. VSCode 안에 새로운 파일을 생성한다. 확장자 명은 다음 중 하나로 해야함. (*.wsd, *.pu, *.puml, *.plantuml, *.iuml)
  3. 예제 코드를 작성해보고 Alt + D 를 눌러서 preview 가 잘 나오는지 확인한다.
example.pu preview (Alt + D)

  • 만약 preview가 보이지 않는다면 Java 또는 Graphviz가 설치되지 않았기 때문일 가능성이 크다. Documnet에는 PlantUML설치시 Graphviz가 같이 설치된다고 써있긴 하지만 이상하게 설치가 안되는 경우가 있는 것 같다. 공식 홈페이지에 가서 직접 설치해주자.
  1. 완성된 다이어그램을 Export 하려면 F1 키를 누르고 plantuml export를 검색하면 세 가지 옵션으로 Export 할 수 있다.

728x90
반응형
728x90
반응형

나는 프로그래밍을 처음 배울 때 컴파일(Compile) 또는 빌드(Build)라는 용어의 개념을 구분해본적이 없었다.
간혹(또는 자주) 주변 사람들도 두 용어를 같은 의미로 함께 사용하기도 하는 것 같았다.

하지만 굳이 따지자면 두 용어는 다른 의미를 지니고 있다.
C언어는 사실 컴퓨터가 이해할 수 있는 언어는 아니고 사람이 읽고 쓸 수 있는 언어이다. 이런 C 언어를 컴퓨터가 이해할 수 있는 언어(기계어)로 번역하는 것이 컴파일(Compile) 이고, 컴파일을 해주는 놈을 (Compiler) 라고 부른다.

그리고 이렇게 컴파일되어 기계어로 작성된 파일을 오브젝트(Object) 파일 이라고 부르는데, 이 오브젝트 파일들과 라이브러리 파일들을 연결시켜 최종적으로 실행파일을 만들게 된다.
이렇게 연결시키는 작업을 링크(Link), 연결시키는 작업을 해주는 놈을 링커(Linker) 라고 부른다. 이렇게 링크를 거치고 나면 최종적으로 실행파일이 생성되게 된다.

마지막으로 빌드(Build)는 이런 일련의 과정을 통칭하는 표현이다.
다시 말해 "빌드 해라" 라는 말을 풀어쓰면 "작성한 코드를 기계어로 번역하고 필요한 파일들을 연결시켜 실행파일을 만들어라" 라는 말이 되는 것이다.

이해를 돕기위해 Visual Studio를 사용하지 않고 간단한 프로그램을 하나 만들어 보았다.
보통 Mac에서는 Xcode를 설치할때 gcc라는 컴파일러가 함께 설치되는데 이것을 이용해 터미널에서 컴파일을 해보자.
터미널에 'gcc -v' 를 입력했을 때 아래와 같이 정보가 나오면 gcc가 설치되어있는 것이다.

$ gcc -v

Configured with: --prefix=/Library/Developer/CommandLineTools/usr --with-gxx-include-dir=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/4.2.1
Apple clang version 12.0.5 (clang-1205.0.22.11)
Target: x86_64-apple-darwin20.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

아무 텍스트 에디터나 열어 아래와 같이 코드를 작성하고 'CompileTest.c'라고 저장해보자.

#include <stdio.h>

int main()
{
  printf("Hello world!\n");
  return 0;
}

그리고 터미널에 아래와 같이 gcc 를 이용해서 작성한 파일을 컴파일하면
[fileName]에 입력한 이름으로 실행파일이 생성된다.

$ gcc -o [fileName] CompileTest.c

생성된 실행파일을 실행시키면 'Hello world!'가 잘 출력되는 것을 확인할 수 있다.
Visual Studio를 이용하면 마술처럼 완성되는 실행파일이 사실 이런 과정을 거쳐 만들어지게 되는 것이다.

728x90
반응형
728x90
반응형

정규식(Regular Expression)은 긴 문자열 안에서 특정한 패턴을 가진 문자열을 찾아내기 위해 사용된다.

예를 들어 웹페이지 안에서 이메일 주소를 추출한다거나 로그파일에서 특정 에러 메시지를 추출할때 매우 편리하게 사용할 수 있다.

우선 regex를 테스트하기 위한 여러 웹 어플리케이션들이 있지만 Regex101이라는 사이트가 가장 사용하기 편리했다. https://regex101.com/

사용 방법은 위에 캡쳐한 이미지처럼 'TEST STRING' 입력창 에 실험 대상이 들어갈 문자열들을 입력해놓고, 'REGULAR EXPRESSTION' 입력창에 찾고자하는 패턴을 입력하면 된다.

나는 이 실험 대상에 임의의 6개 단어를 참가시켰고, 찾고자 하는 패턴 'bo' 를 입력했더니 'MATCH INFORMATION'에 3군데 일치하는 곳이 있음을 알려주고 있다.

'bo'라는 패턴을 조금 더 정확히 해석하면
"b 다음에 o가 나오는 문자열이 있나요?" 라고 질문을 던진 것이다. 이처럼 정규식은 앞에서부터 한글자씩 일치하는지 여부를 확인한다.

문자집합 (Charecter Set)

대괄호('[ ]')를 이용하면 문자집합을 나타낼 수 있다. 문자집합은 대괄호 안에 오는 어떤 문자든 오면 일치시킨다.
같은 예제에 패턴 'bl[ao]'를 입력해보았다. 이를 해석하면 "b 다음에 l이 있고, 그 다음에 a 또는 o가 오는 문자열이 있나요?" 라고 풀이할 수 있다.

수량자 (Quantifier)

중괄호('{ }')를 이용하면 수량을 나타낼 수 있다.
같은 문자가 여러번 나올 때는 그 중괄호 안에 문자가 반복되는 횟수를 입력하여 더 간단하게 표현할 수 있다.
이번엔 기존 예제에 두개의 실험 대상을 더 추가하고 'bo{4}' 라고 패턴을 입력해보았다.
해석하자면 "b 다음에 o가 네번 반복되는 문자열이 있나요?" 라는 질문이다.

그룹화 (Group)

소괄호('( )')를 이용하면 그룹을 나타낸다. 지금까지 본 것 처럼 정규식은 기본적으로 한 단어씩 일치 여부를 확인하지만, 소괄호 안에 여러개의 문자를 넣으면 괄호안에 포함된 단어를 하나의 그룹으로 묶어서 판단한다. 예를 들어 '(bo){2}'라는 패턴을 입력해보자. 소괄호가 없었다면 o 가 두번 반복되는지 여부를 판단하겠지만 bo를 소괄호로 묶어주어 bo 가 두번 반복되는지 여부를 판단한다.

그룹화를 하게되면 기본적으로 캡쳐도 동시에 되는데,
캡쳐된 그룹은 'MATCH INFORMATION' 창을 보면 캡쳐된 그룹을 볼 수 있다. 아래 예제는 날짜 중에 특히 몇월인지 캡쳐하는 방법이다.
(참고로 '\d' 는 '아무 숫자'를 의미하고, '\w' 는 '특수문자를 제외한 아무문자'를 의미한다)

여기까지 잘 이해했다면, 그 다음부터는 특별한 설명 없이 아래 표만 참고해도 충분히 이해 할 수 있을 것 같다. 이 표는 Regex 에서 가장 많이 사용되는 패턴들을 정리한 표이다.

 

패턴 설명 예제
[abc] a,b,c 중에 아무거나
[^abc] a,b,c 를 제외하고 아무거나
[a-z] a 부터 z까지 문자중에 아무거나
[^a-z] a 부터 z까지 문자를 제외하고 아무거나
. 아무 문자 하나
a|b a 또는 b
\s 공백문자 중에 아무거나
\S 공백문자를 제외한 아무거나
\d 숫자 중에 아무거나
\D 숫자를 제외하고 아무거나
\w 알파벳이나 숫자 중에 아무거나
\W 알파벳이나 숫자를 제외하고 아무거나(특수문자)
(?:...) 그룹화하지만 캡쳐는 하지 않음
(...) 그룹화하고 캡쳐
a? a 가 없거나 한개 있거나
a* a 가 없거나 여러개 있거나
a{3} a가 3개
a{3,} a가 3개이거나 그 이상이거나
a{3,6} a가 3개 이상 6개 이하
^ 문자열 시작
$ 문자열 끝
\b 단어의 경계
\B 단어의 경계 아닌 아무거나
728x90
반응형
728x90
반응형

객체 지향 언어의 4대 기본 원칙 중 마지막 원칙인 다형성에 대해 알아보자.

객체 지향 언어의 4대 기본 원칙 :

  • 캡슐화 (Encapsulation)
  • 추상화 (Abstraction)
  • 상속 (Inheritance)
  • 다형성 (Polymorphism)

지난 포스트에서 다룬 상속은 사실 다형성을 위해서 사용되는 경우가 더 많다. 다형성은 "여러가지 형태를 갖는 것을 허용한다"는 뜻이다.

이전 포스트에서 사용했던 예제를 다시 한 번 들여다보자.

class Program
{
    static void Main(string[] args)
    {
        Dog myDog = new Puppy();
        myDog.Bark();
        //실행결과 --> 삑삑!
    }
}
public class Dog
{
    public virtual void Bark()
    {
        Console.WriteLine("멍멍!");
    }
}

public class Puppy : Dog
{
    public override void Bark()
    {
        Console.WriteLine("삑삑!");
    }
}

위 예제에서 Main함수를 보면 myDog라는 변수를 Dog로 선언했는데 실제로 생성한 객체는 Dog의 자식클래스인 Puppy이다.

이렇게 부모클래스로 선언된 변수에 자식클래스들도 할당될 수 있도록 허용한 것이 다형성이다. (반대로 자식클래스에 부모클래스를 할당하는 것은 불가능하다.)

다형성이 왜 필요한지 생각해보기 위해 간단한 예제를 만들어보자.

class Program
    {
    static void Main(string[] args)
    {
        List<Dog> myDogs = new List<Dog>();

        Dog dog1 = new Dog();
        myDogs.Add(dog1);
        Dog dog2 = new Puppy();
        myDogs.Add(dog2);
        Dog dog3 = new FakeDog();
        myDogs.Add(dog3);

        foreach(Dog dog in myDogs)
        {
            dog.Bark();
        }
    }
}

public class Dog
{
    public virtual void Bark()
    { 
        Console.WriteLine("멍멍!");
    }
}

public class Puppy : Dog
{
    public override void Bark()
    {
        Console.WriteLine("삑삑!");
    }
}

public class FakeDog : Dog
{
    public override void Bark()
    {
        Console.WriteLine("야옹~!");
    }
}

위 예제를 보면 Dog를 담을 수 있는 ListDog뿐만 아니라 Dog의 자식 클래스인 PuppyFakeDog도 들어가는 것을 볼 수 있다. 만약 다형성을 허용하지 않았다면. List<Dog>, List<Puppy>, List<FakeDog>를 모두 별도로 만들어 각각 따로 관리해야했을 것이다. 우리는 다형성을 이용해서 자료를 훨씬 체계적으로 관리 할 수 있다. 뿐만 아니라 클래스간의 의존성을 낮춰 느슨한 결합이 가능하게 한다. 느슨한 결합에 대해서는 디자인 패턴을 공부하면서 조금 더 알아보자.

마지막으로 지난 포스트 상속편에서 new키워드를 이용해서 부모클래스의 메소드를 변경했을 때 실행결과가 달라졌던 이유를 다시 한 번 생각해보자.

class Program
{
    static void Main(string[] args)
    {
        Puppy myPuppy = new Puppy();
        myPuppy.Bark();
        //실행결과 --> 삑삑!
        Dog myDog = new Puppy();
        myDog.Bark();
        //실행결과 --> 멍멍!
    }
}

public class Dog
{
    public void Bark()
    { 
        Console.WriteLine("멍멍!");
    }
}

public class Puppy : Dog
{
    public new void Bark() // new 키워드를 이용해 부모 메소드를 가린다.
    {
        Console.WriteLine("삑삑!");
    }
}

위 예제에서 보면 myPuppymyDog 변수 모두 Puppy 객체를 가지고 있지만 선언된 데이터 형태에 따라서 "멍멍"이라고 짖기도 하고 "삑삑"이라고 짖기도 한다. 이건 다형성의 원칙을 훼손하는 케이스다. 선언된 데이터 타입이 어떻든 Puppy는 "삑삑"하고 짖어야하지 않을까? 이러한 이유로 new키워드를 이용해 부모클래스의 함수를 변경하는 것은 지양해야한다.

728x90
반응형
728x90
반응형

지난 포스트에서 객체 지향 언어의 4대 기본 원칙 중 갭슐화와 추상화에 대해 알아보았다.

객체 지향 언어의 4대 기본 원칙 :

  • 캡슐화 (Encapsulation)
  • 추상화 (Abstraction)
  • 상속 (Inheritance)
  • 다형성 (Polymorphism)

이번 포스트에서는 상속에 대해 알아보려고 한다. 상속은 기본적으로 확장의 개념이다. 확장이라고 하면 스타크래프트 게임 확장팩 "블루드워"가 나왔을 때가 생각난다(너무 옛날 이야기인가...). 확장팩에는 럭커와 메딕 처럼 새로 추가되는 유닛들이 있었고, 골리앗의 대공 사거리 업그레이드 처럼 기존 유닛의 업데이트도 있었다. 상속은 이렇게 확장이라는 개념을 통해서 코드의 재사용성을 높일 수 있도록 하는 것이다.

class StarcraftUnits {
    public Unit marine;
    public Unit firebat;
    public Unit zerggling;
    .
    .
    .
}

// 확장팩인 BloodWar 는 기존 Starcraft 유닛을 상속받음.
class BloodWarUnits : StarcraftUnits {
    public Unit medic; 
    public Unit lurker;
    .
    .
    .
}

위 예제에서 BloodWarUnit 클래스는 StarcraftUnit 클래스를 상속받음으로써 기존에 이미 있었던 마린, 파이어뱃, 저글링들을 다시 구현하지 않고도 기존 유닛들을 사용할 수 있다.

기존 클래스에 있던 메소드를 자식클래스에서 변경할 수도 있다. 이 때 부모클래스는 변경을 허용하는 메소드 앞에 virtual 또는 abstract 키워드를 가지고 있어야 하고, 자식클래스에서는 override 키워드를 이용하여 부모클래스로부터 가져온 메소드에 변경이 있음을 알려줘야한다.

public class Dog
{
    public virtual void Bark()
    {
        Console.WriteLine("멍멍!");
    }
}

public class Puppy : Dog
{
    public override void Bark()
    {
        Console.WriteLine("삑삑!");
    }
}

위 예제는 Puppy 클래스가 Dog 클래스를 상속받지만 Bark() 메소드를 그대로 사용하지 않고, 변경하여 사용하는 예이다.

virtualabstract의 차이는 구현부가 있느냐 없느냐의 차이이다.

  • virtual는 부모클래스에 구현부가 있음 : 자식클래스에서 굳이 override하지 않아도 되며, 이때는 부모클래스의 메소드를 그대로 사용할 수 있음.
  • abstract는 부모클래스에 구현부가 없음 : 자식클래스에서 반드시 override 해줘야하고, 부모클래스 이름 앞에 abstract 키워드를 붙여서 추상클래스임을 명시해줘야함.

위에서 봤던 강아지 예제를 abstract를 사용해서 만들어보자.

public abstract class Dog // 클래스 명 앞에 abstract가 붙어있음.
{
    public abstract void Bark();
    // 구현부가 올 수 없음. 
    // 구현할 경우 에러 발생.
    // { 
    //    Console.WriteLine("멍멍!");
    // }
}

public class Puppy : Dog
{
    // Bark() 메소드를 반드시 만들어야함. 
    // 없을 경우 에러 발생.
    public override void Bark() 
    {
        Console.WriteLine("삑삑!");
    }
}

나는 이런 의문이 들었다.
부모클래스의 변경을 전혀 원치 않을 경우 virtual이나 abstract키워드를 사용하지 않고 자식클래스에서 변경하는 방법이 없을까?

방법이 없는 것은 아니다. new키워드를 사용하여 부모클래스의 메소드를 가릴 수 있지만 아니지만 중요한 문제점을 수반한다. 아래 두가지 예제를 보고 실행결과를 비교해보자.

new 키워드를 이용한 방법:

class Program
{
    static void Main(string[] args)
    {
        Dog myDog = new Puppy();
        myDog.Bark();
        //실행결과 --> 멍멍!
    }
}

public class Dog
{
    public void Bark()
    { 
        Console.WriteLine("멍멍!");
    }
}

public class Puppy : Dog
{
    public new void Bark() // new 키워드를 이용해 부모 메소드를 가린다.
    {
        Console.WriteLine("삑삑!");
    }
}

virtual 키워드를 이용한 방법:

class Program
{
    static void Main(string[] args)
    {
        Dog myDog = new Puppy();
        myDog.Bark();
        //실행결과 --> 삑삑!
    }
}
public class Dog
{
    public virtual void Bark()
    {
        Console.WriteLine("멍멍!");
    }
}

public class Puppy : Dog
{
    public override void Bark()
    {
        Console.WriteLine("삑삑!");
    }
}

실행 결과가 왜 달라졌을까?
다음 포스트에서 다룰 다형성에서 자세히 들어다보자.

728x90
반응형
728x90
반응형

지난 번 포스트에서 객체 지향 언어의 4대 기본 원칙 중 첫 번째 캡슐화를 알아보았다.

객체 지향 언어의 4대 기본 원칙 :

  • 캡슐화 (Encapsulation)
  • 추상화 (Abstraction)
  • 상속 (Inheritance)
  • 다형성 (Polymorphism)

이번 포스트는 추상화를 이야기해보려고 한다.
나는 사실 처음에 추상화라는 용어가 낯설게 느껴졌다. "이미 소프트웨어라는 것 자체가 추상적인데 뭘 더 추상화하라는 거지?" 라는 생각이 머리에 떠올랐던 것 같다.

사실 추상화 한다는 의미는 사용자의 입장에서 단순화 시켜준다는 의미이다. 사용자는 복잡한 구현에 대해서는 관심이 없다.
예를 들어 우리가 자동차의 엑셀을 밟을 때 몇 ml 의 휘발유를 엔진에 넣고 몇 어느정도의 세기로 압축할 것이며, 어느시점에 점화를 할 것인지 궁금하지 않은 것 처럼 말이다.

리모컨의 인터페이스를 생각해보자. 누가 일일이 알려주지 않아도 + 라고 적힌 버튼을 보면 음량이 높아질 것 같다. 리모콘이 어떤 과정을 거쳐 음량을 높이는지는 모르겠지만 사용자가 보기에 음량이 높아질 것 같다는 생각이 들면 그 자체로 좋은 디자인인 것이다.

그렇기 때문에 추상화라는 것은 반드시 interface를 사용한다거나 abstract클래스를 사용하는 것을 의미하지는 않는다. 이런 것들은 추상화를 도와주는 도구일 뿐이다.

그래도 이왕 이야기가 나왔으니 interface를 사용하면 어떻게 추상화가 되는지 예를 들어 보자. interface는 클래스와 비슷하게 생겼는데 구현부가 없고 메소드 이름과 형태만 존재한다.

interface IEncoder
{
    string Encode(Video video);

    Video Decode(string encodedString); 
}

위 예제에서 내가 설정한 상황은 이렇다.

우리 팀은 좋은 인코더를 만들 수 있는 기술이 없다. 그래서 영상팀에 인코더를 만들어달라고 요청하려고 하는데 그 팀에서 어떻게 구현하는지는 관심이 없고 Video를 넣으면 string으로 바꿔주는 인코더와 인코딩된 string을 다시 Video로 바꿔주는 디코더만 있으면 된다.

이렇게 함수의 형태와 이름을 약속하는 것이 interface의 역할이다. 이제 영상팀은 IEncoder 를 상속받아 구현할 것이다.

public class ExcelentEncoder : IEncoder
{
    public Video Decode(string encodedString)
    {
        Video vedeo = new Video();
        // ~~복잡한 구현~~
        return vedeo;
    }

    public string Encode(Video video)
    {
        string encodedString = "";
        // ~~복잡한 구현~~
        return encodedString;
    }
}

우리팀은 이제 영상팀에서 interface를 통해 약속한 계약만 지켜준다면 그들이 어떻게 구현하든 상관 없이 개발을 이어나갈 수 있다.

static void Main(string[] args)
{
    Video video = new Video();
    IEncoder encoder = new ExcelentEncoder(); 

    string encoded = encoder.Encode(video);
    // 데이터 베이스에 저장, 메일로 전송, 블라 블라~~
}

여기서 인터페이스를 사용했다는 것 보다 중요한 것은 "인코딩"이라는 작업을 추상화 했다는 것이다. 사용자인 나는 "인코딩"이 어떻게 이뤄지는지는 관심이 없다. 그저 Encode()라는 메소드를 이용하면 "인코딩"이 될 것이라는 약속이면 만족한다.

이렇게 interface를 사용하여 약속을 정하는 것은 추상화에 사용하는 일반적인 기법중에 하나이다. 꼭 interfaceabstract클래스를 사용하지 않더라도 사용자의 시점에서 어떻게 하면 기능들을 추상화(단순화)하고 사용하는데 편리하게 만들 수 있을까 고민하는 것이 중요하다.

다음 포스트 : 상속

728x90
반응형

+ Recent posts