[Project design] 프로그램 생산과 작성(코딩) - 클래스 작성법


Study/Project design  2020. 12. 16. 11:50

안녕하세요. 명월입니다.


이 글은 프로그램 생산과 작성(코딩) - 클래스 작성법에 대한 글입니다.


이전 글에서 코딩할 때 함수를 작성하는 방법에 설명했습니다. 작성법에 대해서는 규약이 정해진 것이나 꼭 이렇게 해야 한다라고 정해진 법칙은 아닙니다.

그냥 제 경험에 의해서 이렇게 작성하면 후에 관리하기도 편하고 좀 더 직관적으로 코딩을 할 수 있지 않을까하는 방법을 적은 것입니다.


클래스를 작성하는 방법에도 저 나름대로의 규칙이 있습니다.

기준은 웹 프로젝트의 기준으로 작성한 것이기 때문에 다른 분야는 약간 다를 수도 있습니다.


객체 지향의 4대 특성인 추상화, 캡슐화, 상속성, 다형성이 있습니다. 저는 나름대로 이 4대 특성이 무엇일까 생각을 많이 한 적이 있습니다.

사실 우리가 프로그램을 작성할 때, 저 4대 특성을 사용하지 않아도 개발하는 데 아무런 문제가 없습니다. 추상화를 안한다고 해서 프로그램을 못만드는 것도 아니고 캡슐화, 즉 변수를 다 public으로 하고 다른 영역에도 참조 가능하게 만든다고 해서 프로그램이 실행이 안되는 것도 아닙니다.

물론 상속과 다형성도 마찬가지입니다.

이 4대 특성이 무엇이냐면 프로그램 작성할 때, 좀 더 가독성을 높이기 위해서,즉 최종적으로 우리가 문서로 따로 설계서를 만들지 않아도 코드 자체가 설계도가 되게끔 만들기 위한 코딩 기법이라고 생각합니다.


예를 들어, 우리가 사람 이름과 출생연도를 넣으면 나이 계산까지 되는 간단한 프로그램을 작성합니다.

using NPOI.OpenXmlFormats.Dml;
using System;
using System.Collections.Generic;
using System.Linq;
// 실행 클래스
class Program
{
  // 사람 클래스
  class People
  {
    // 이름 변수
    private string name;
    // 생년 변수
    private int birthYear;
    // 나이
    private int olds;
    // 생성자로 이름과 생년을 받고 나이를 계산한다.
    public People(string name, int birthYear)
    {
      // 이름 설정
      this.name = name;
      // 생년 설정
      this.birthYear = birthYear;
      // 나이 계산
      this.olds = CalcOld();
    }
    // 이름 프로퍼티 (Java에서는 get함수)
    public string Name
    {
      get { return this.name; }
    }
    // 나이 프로퍼티 (Java에서는 get함수)
    public int Olds
    {
      get { return this.olds; }
    }
    // 나이 계산
    private int CalcOld()
    {
      return DateTime.Now.Year - birthYear;
    }
    // 출력
    public override string ToString()
    {
      return $"{this.name} - BirthYear: {this.birthYear}, Old: {this.olds}";
    }
  }

  // 실행 함수
  static void Main(string[] args)
  {
    // 리스트를 생성해서 사람을 입력한다.
    var peoples = new List<People>()
    {
      // 2000년생의 A
      new People("A", 2000),
      // 1990년생의 B
      new People("B", 1990),
      // 1980년생의 C
      new People("C", 1980),
      // 1970년생의 D
      new People("D", 1970)
    };
    // 20살 이상의 나이를 출력한다.
    foreach (var p in peoples.Where(x => x.Olds > 20))
    {
      // 이름과 나이 출력
      Console.WriteLine(p);
    }

    // 콘솔 출력
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

위 소스를 보시면 제가 People이라는 클래스를 만들었습니다. 보시면 생성자에서 이름과 생년만 입력을 받고 나이는 계산하게 합니다. 여기서 나이 변수가 public이면 어떻게 될까요? CalcOld라는 함수가 의미가 없어집니다.

즉, 클래스의 변수를 임의로 수정이 가능해져 버리기 때문에 무결성의 보장할 수 없어집니다. 이 변수를 readonly만 하려고 하려해도 변수 자체는 그런 제어가 되지 않기 때문에 함수를 통해서 이를 제어하는 것입니다. 이게 캡슐화가 되겠습니다.

여기서 저는 ToString을 재정의했기 때문에 그냥 p를 Console.WriteLine함수에 넣어도 ToString을 출력하게 되어 있습니다.


이렇게 클래스의 특성을 활용하여 프로그램을 작성하게 되면 Main함수에서는 Facade 패턴을 따라서 설계가 없어도 프로그램을 이해하기가 쉬워집니다.

제가 이 클래스의 특성은 따로 자세히 설명한 글이 있으니 참고하세요.

링크 - [C#] 19. 객체 지향(OOP) 프로그래밍의 4대 원칙(캡슐화, 추상화, 상속, 다형성)

링크 - [Java] 14. 객체 지향(OOP) 프로그래밍의 4대 원칙(캡슐화, 추상화, 상속, 다형성)


클래스를 작성하는 건 이렇게 만들면 되고 구성은 어떻게 할까에 대해 설명하겠습니다.

우리가 웹 프로그램을 봤을 때 주요 데이터가 주고 받는 것은 Client(브라우저)에서 Server(WAS)로 데이터를 주고 받는 부분과 Server(WAS)에서 데이터 베이스로 데이터를 주고 받는 부분이 있습니다.

물론 사양에 따라 다른 웹서버 혹은 응용 프로그램과 소켓 통신이나 Http 통신을 할 수도 있고 다른 데이터를 주고 받는 영역이 있을 수 있으나 우리는 일단 Client - Server와 Server - DB만 살펴 보겠습니다.


이 흐름은 Client에서 Server 그리고 DB로 나아가는 형태가 일반적입니다. 여기서 우리는 하나의 데이터 클래스로 Client에서 Server로 데이터를 받고 그대로 DB로 입력해도 됩니다.

그러나 Browser에서 사용하던 변수 이름과 DB에서 사용하는 변수 이름을 같은 것으로 지정을 해버리면 보안적으로 문제가 생길 요지가 있습니다. 그래서 이 두 클래스를 분리하는 게 좋습니다.

그리고 하나의 페이지 값이 하나의 DB 테이블에 다 들어가는 것은 아니기 때문이라도 분리하는게 프로그램을 작성하는 데 편하다고 할 수 있습니다.


그래서 저는 크게 클래스 구성을 Client - Server로 오는 데이터를 Bean이라고 이야기하고 Server - DB로 주고 받는 데이터를 Model이라고 표현합니다.

즉, 클래스의 큰 구조는 Bean 클래스 집합과 Model 클래스 집합, 그리고 Bean에서 Model로 변환, Model에서 Bean으로 데이터를 변환 시켜주는 Contoller 집합으로 나눕니다.

using System;
// 실행 클래스
class Program
{
  // 사람 빈 클래스
  class PeopleBean
  {
    // 이름 변수
    private string name;
    // 생년 변수
    private int birthYear;
    // 생성자로 이름과 생년을 받는다.
    public PeopleBean(string name, int birthYear)
    {
      // 이름 설정
      this.name = name;
      // 생년 설정
      this.birthYear = birthYear;
    }
    // 이름 프로퍼티 (Java에서는 get함수)
    public string Name
    {
      get { return this.name; }
    }
    // 생년 프로퍼티 (Java에서는 get함수)
    public int BirthYear
    {
      get { return this.birthYear; }
    }
    // 사람 모델 클래스 생성
    public PeopleModel ToModel()
    {
      return new PeopleModel(this.name, DateTime.Now.Year - birthYear);
    }
  }

  // 사람 모델 클래스
  class PeopleModel
  {
    // 이름 변수
    private string name;
    // 나이
    private int olds;
    // 생성자로 이름과 나이을 받는다
    public PeopleModel(string name, int olds)
    {
      // 이름 설정
      this.name = name;
      // 나이 설정
      this.olds = olds;
    }
    // 이름 프로퍼티 (Java에서는 get함수)
    public string Name
    {
      get { return this.name; }
    }
    // 나이 프로퍼티 (Java에서는 get함수)
    public int Olds
    {
      get { return this.olds; }
    }
    // 사람 빈 클래스 생성
    public PeopleBean ToBean()
    {
      return new PeopleBean(this.name, DateTime.Now.Year - olds);
    }
  }

  // Json을 사람 Bean클래스로 생성
  static PeopleBean CreateBean(string json)
  {
    return Newtonsoft.Json.JsonConvert.DeserializeObject<PeopleBean>(json);
  }
  // 데이터 베이스 입력
  static bool InsertPeopleModel(PeopleModel model)
  {
    return true;
  }
  // 실행 함수
  static void Main(string[] args)
  {
    // PostData로 Json 타입을 받았다고 가정
    string postData = "{\"name\":\"A\", \"birthYear\": 1990}";
    // 빈 클래스 생성
    var bean = CreateBean(postData);
    // 빈 클래스로 모델 클래스를 생성
    var model = bean.ToModel();
    // 데이터 베이스 입력
    if (InsertPeopleModel(model))
    {
      // 결과 출력
      Console.WriteLine($"OK - {model.Name} : {model.Olds}");
    }

    // 콘솔 출력
    Console.WriteLine("Press any key...");
    Console.ReadKey();
  }
}

위 예제는 이해하기 쉽게 콘솔 프로그램으로 작성했습니다.

먼저 Get방식의 QueryString나 Post방식의 PostData를 Json타입으로 받았다고 가정합니다.

Json String 자체로는 데이터를 다루기가 어렵기 때문에 우리는 Bean이라는 클래스로 변환합니다. 다시 이 데이터를 데이터 베이스에 넣기 위해서 Model이라는 클래스로 변환합니다.

우리가 데이터 베이스에 들어가는 값는 나이이지만 웹 브라우저에서 받는 값은 생년입니다. 즉 생년에서 나이로 변환하는게 필요하다는 것입니다.

또, 만약 브라우저에 DB의 값을 받는다고 했을 때 Model로 데이터를 받아 Bean으로 변환하고 화면에 표시하면 됩니다.


Bean과 Model를 하나의 클래스로 묶어서 사용해도 좋습니다. 하지만 경우에 따라서 이게 Inject 버그 발생으로 보안에 취약해지는 문제점이 발생할 수 있기 때문에 가능하면 Bean과 Model를 분리해서 사용합니다.

분리해서 사용하게 되면 문제가 이 Bean, Model 클래스가 엄청나게 늘어난다는 문제점이 있네요. 그걸 최소화하기 위해서는 Bean과 Model를 연결하는 적절한 추상화가 필요하겠네요.


개인적으로 이렇게 프로그램을 잘 구성합니다만 저는 이게 작성할 때 수고는 있을지언정 정말 소스 보기가 편해집니다. Main 함수에 Facade 패턴과 Strategy 패턴을 구성하기도 쉬워지는 효과도 있기 때문에 소스 자체가 설계도가 되는 형태가 됩니다.


여기까지 프로그램 생산과 작성(코딩) - 클래스 작성법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.