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
반응형

객체 지향 언어의 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
반응형
728x90
반응형

코딩을 하다보면 내가 짠 코드가 좋은 코드인지 나쁜 코드인지 궁금할 때가 있다. 물론 목적에 부합하는 결과가 항상 나오고 아무도 불편없이 사용하고 있다면 굳이 좋은 코드인지 나쁜 코드인지 궁금하지도 않겠지만, 현실에서 그런 일은 거의 일어나지 않는다. 사용자의 요구에 따라 여러가지 기능이 추가되기도 하고 삭제되기도 하며 그 과정에서 수많은 에러가 추가적으로 발생하기도 한다. 이러한 변경에 빠르게 대응할 수 있고, 에러가 적어 신뢰성이 높은 프로그램을 만들었을 때 일반적으로 우리는 그것을 좋은 코드라고 부른다.

객체지향언어는 좋은 코드를 작성하기 위한 4대 기본 원칙이 있다. 이 원칙들은 각각 무엇을 의미하는지, 그리고 왜 중요한지 예제를 통해 알아보자.

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

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

캡슐화 (Encapsulation)

아래 예제들을 더 쉽게 이해하기 위해 Main 함수를 작성하는 사람이 사용자이고, 나머지 클래스의 작성자는 개발자인 '나'라고 상상해보자.

// 통장 계좌
public class Account 
{
    public int balance; // 잔액
}

// 사용자 영역
class Program 
{    
    static void Main(string[] args)
    {
        Account myAccount = new Account();
        myAccount.balance += 2000; // 2000원 입금
        myAccount.balance -= 1000; // 1000원 출금
        Console.WriteLine($"잔액은 {myAccount.balance}원입니다");
        // -> 잔액은 1000원입니다.
    }
}

위의 예제에서 나는 통장 계좌를 의미하는 Account라는 클래스를 만들어 사용자에게 제공하였다. 사용자는 내가 만든 Account 클래스를 이용해 통장을 새로 개설(myAccount)하여, 2000원을 입금하고 1000원을 출금했다. 잔액을 확인해보니 1000원이 남았다.

이 예제는 기능을 수행하는데 아무런 문제가 없다.
캡슐화는 기본적으로 관련된 속성과 행위를 별도의 클래스로 묶는 것을 말하는데 이 예제에서는 Account라는 클래스를 만들었을 뿐 속성과 행위를 묶어놓지 않고 사용자가 Main()함수에서 직접 사용하도록 모두 열어주었기 때문에 캡슐화가 잘 되어있지 않은 코드다.

문제점을 파악해보자.

  1. 사용자가 잔액에 직접 접근 가능하도록 하여 정보를 조작할 수 있도록 허용하고 있다.
  2. 잔액이 음수를 허용하고 있다.
// 통장 계좌
public class Account 
{
    private int balance; //접근 제한자를 private 으로 
                         //하여 외부에서 볼수 없도록 함.

    public void Deposit(int money)
    {
        if (money < 0)
        {
            Console.WriteLine("음수를 허용하지 않습니다.");
            return;
        }        
        balance += money;
    }

    public void Withdraw(int money)
    {
        if (money < 0)
        {
            Console.WriteLine("음수를 허용하지 않습니다.");
            return;
        }
        if (balance < money)
        {
            Console.WriteLine("잔액이 부족합니다.");
            return;
        }
        balance -= money;
    }

    public void ShowBalance()
    {
        Console.WriteLine($"잔액은 {balance}원입니다");
    }
}

// 사용자 영역
class Program 
{    
    static void Main(string[] args)
    {
        Account myAccount = new Account();
        myAccount.Deposit(2000);  // 2000원 입금
        myAccount.Withdraw(1000); // 1000원 출금
        myAccount.ShowBalance();
        // -> 잔액은 1000원입니다.
    }
}

두 번째 예제는 첫번째보다 캡슐화가 더 잘된 코드라고 볼 수 있다.

  1. 잔액에 대한 접근제한자를 private으로 변경해 사용자는 잔액에 직접 접근하지 못하게 하였고, DepositWithdraw함수를 이용해서만 접근 가능하도록 하였다. (정보 은닉화)
  2. DepositWithdraw 함수 안에서 음수를 허용하지 않고 잔액이 부족할 경우 출금도 불가하다. 이런 것을 유효성 검사라고 하는데, 유효성 검사를 Main 함수에 작성하지 않고 클래스 안에서 해결하고 있다는 점이 중요하다. (관련된 속성이나 행위를 묶는다)

다음 포스트: 추상화

728x90
반응형
728x90
반응형

객체 지향 언어 에서 말하는 객체란 우리 주변에 존재하는 모든 사물, 또는 추상적으로 존재하는 개념 등 우리가 상상할 수 있는 모든 것을 말한다.

객체에 대한 속성이나 행동, 특징들을 정의 하는 청사진(설계도)을 프로그래밍 언어에서는 클래스로 구현한다.

예를 들어 아래와 같이 Car 라는 클래스를 만들었다.
이 클래스는 speed 라는 속성을 가지고 있고, speed라는 속성을 Accel()Break() 라는 두 개의 메소드로 조절 할 수 있다.

public class Car
{
    // field
    int speed = 0;

    // method
    public void Accel()
    {
        speed++;
    }

    public void Break()
    {
        speed--;
    }

    public void DisplaySpeed()
    {
        Console.WriteLine($"현재속도: {speed}");
    }
} 

하지만 클래스는 어디까지나 청사진을 만든 것 뿐이지 내가 만든 소프트웨어 세계 안에 실제로 존재하는 것은 아니다. 내가 만든 세계에 실제로 존재하게 하기 위해선 new라는 키워드를 이용해서 실제 메모리에 할당해주어야 한다.

아래의 예제에서 처럼 Car 라는 클래스를 실제 내가 만든 소프트웨어 세계안에 실체화(인스턴스화) 하였다. 이제 myCar 라는 변수 안에는 Car 클래스의 인스턴스(Instance)가 들어있다.
이 인스턴스를 이용해 클래스 안에 있는 함수들을 사용할 수 있다.

static void Main(string[] args)
{
    Car myCar = new Car(); // 인스턴스화
    myCar.Accel();
    myCar.Accel();
    myCar.DisplaySpeed();
    myCar.Break();
    myCar.DisplaySpeed();
}

// 실행 결과:
// 현재속도: 2
// 현재속도: 1

new Car() 부분이 인스턴스를 생성하는 부분이다. 여기서 "myCar이라는 변수에 인스턴스가 할당되었다." 라고 할 수 있다.
그래서 클래스를 주로 "붕어빵 틀"에 비유하고, 인스턴스를 "붕어빵"에 비유하곤 한다.
누가 시작했는지는 잘 모르겠지만 이해하기 좋은 비유인 것 같다.

728x90
반응형

+ Recent posts