Published on

고급컴퓨터그래픽스 | Software Design

Software Design

UML Class Diagram

UML(Unified Modeling Language), 소프트웨어 개념을 다이어그램으로 그리기 위해 사용하는 시각적인 표기법. Class, Sequence, Usecase, Object, State diagram 등이 있다.

여기서 알아볼 것은 Class Diagram!

가장 많이 사용되는 다이어그램이며 코드의 구조를 표현한다. 크게 아래와 같이 나눈다

  • 클래스
    • 추상클래스: 메서드 구현이 제공하지 않는 클래스. 흔히들 아는 순수 가상함수를 포함하는 클래스이다. 순수 가상 함수의 경우 반드시 상속받는 파생 클래스에서 메서드가 구현이 되어야한다.
  • 관계
    • 연관: 다른 객체들을 연결. 방향은 알고 있는 방향으로 표현
      • 합성: 수명에 관한 책임이 있는 것이 특징이다. c++의 경우 pointer나 reference의 경우 가리키는 데이터(주인)이 소멸/복사될 경우 가리키는 피보험자도 소멸/복사가 되는 특징이 있음.
      • 스테리오 타입: 연관의 의미를 바꿈. 예를 들어 <<create>>의 경우 연관의 원본이 연관의 대상을 생성하는 의미를 지님.
    • 상속/일반화: extends
  • 인터페이스: 추상 클래스와 달리 순수하게 가상함수로 이루어진 클래스이다. 인스턴스 변수가 없는 것이 특징이다.
  • 다수성: 파생 클래스의 갯수를 지정함.

UML이 물론 코드의 모든 것을 담아낼 수 있는 것은 아니다! 필요에 따라 적재적소에 사용하는 것을 권하는 것 같다. 다이어그램을 만듦으로써 협업자에게 효과적인 의사소통의 수단으로써 재역할을 할 수 있다. 그러니 명료하게, 간단하게 그리는 것이 좋은 방향이라고 하는 것 같다(?)

설계에 대한 의사소통을 위해 본 수업에서는 다이어그램을 그렸고, 프로젝트를 함으로써 설계가 개선되어 가는 과정을 느끼는 것이 목표였을 것이다.

사실 본인은 과제할 때 각기 다르게 코딩을 해서 과제가 연결되지 않아 다이어그램 그리는 것이 사실 의미가 없었다..ㅠ 좋은 코딩이 아님을 여기서 깨닫긴 했다.

Good Design? Why and when?

소프트웨어 개발에 있어서 제대로 된 설계 기반하여 좋은 코드를 작성해야 하는 건 프로그래밍하는 개발자라면 누구나 알 것이다. 한 번쯤 다들 경험해서 아는 것이지만, 코드가 꼬이다 못해 버리는? 경우가 있었을 것이다. 아주 초반에..ㅋㅋ

좋은 코드는 따라서 변경에 유연하게 대응할 수 있어야 한다. 좋은 설계도 마찬가지로 변화하는 요구사항에 대처할 수 있어야 하고, 지속적으로 새로운 기능의 추가와 기존 기능의 수정/삭제가 이루어질 수 있어야 한다. 따라서 이와 같은 변경 작업을 빠르고, 효율적으로 버그의 발생을 최소화하는 설계라고 볼 수 있다.

하지만 첫술에 배부를 수 없듯이 초기 설계는 100% 완벽할 수 없다. 설령 탄탄한 설계 위에서 작성하더라도 유연하게 대응하지 못할 경우 새로운 기능만 쌓이게 되어 결국에는 치명적인 버그로 이어질 수 있다. 유연한 설계라도 예상하지 못한 변경사항은 언제라도 발생할 수 있다.

설계는 따라서 한 번만 정해진 건물 블루프린트가 아닌, 항상 설계가 되어야한다. 오히려 정원일에 가깝다고 할 수 있다.

Refactoring

겉으로 드러나는 기능을 그대로 둔채, 소프트웨어 내부를 수정하는 작업이다. 코드를 다시 작성하고, 작업하고 다시 설계하기라고 볼 수 있다. 리팩토링 시에는 단위 테스트가 반드시 준비되어 있어야 한다. 사실 많이 애먹은 부분이기도 하다..

Principles of good design

  • 낮은 결합도(Low Coupling): 하나의 모듈을 수정할 때 얼마나 많은 모듈을 함께 수정해야 하는지 나타냄. 결합도가 낮을 수록 함께 변경하는 모듈 수가 최소화가 되므로 변경이 수월하다. 결합도 자체가 의존성 정도를 나타내며 다른 모듈에 대한 지식의 정도를 나타낸다. 필요한 지식이 낮을 수록 낮은 결합도를 가진다. 인터페이스와 캡슐화가 이와 관련되어 있다. 내 생각엔 모듈과의 "관계"라고 생각한다.
  • 높은 응집도(High Cohesion): 모듈 내부에서 발생하는 변경의 정도 혹은 연과되어 있는 정도를 나타낸다. 모듈 전체가 함께 변경이 되거나 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 해당 모듈은 응집도가 높은 것이다.
  • 코드의 중복 제거: 어떤 코드가 중복인지 찬아야하며 일관되게 수정하는 작업이 필요하다.
    • 한 클래스에서 두 개의 메서드가 중복: 별도의 메서드로 빼서 두 메서드를 호출해본다.
    • 한 클래스의 두 하위 클래스에서 중복: 별도의 메서드를 상위 클래스에 추가
    • 서로 상관없는 클래스에서 중복: 제 3 클래스로 때어낸 후 다른 클래스에서 이용한다.
  • 의미 있는 이름: 의도가 분명하게 드러나도록! 검색하기 쉽게, camelCase, snake_case
    • 클래스: 명사/명사구
    • 메서드: 동사/동사구

Code smells

프로그램에서 심오한 문제를 일으킬 가능성 있는 소스코드. 좋은 설계를 위한 원칙에 어긋나는 하나의 신호이며, code smell이 느껴진다면, 리팩토링을 해야 한다.

  • 중복 코드
  • 긴 메서드: 함수가 여러 기능을 수행할 경우, 기능간의 의존성이 생겨 하나의 기능을 수정할 때 다른 기능도 영향을 받기 쉽다. 높은 결합도인 경우가 이에 해당한다. 하나의 메서드에서는 하나의 기능만 수정하도록 수정해야한다.
  • 큰 클래스: 낮은 응집도, 하나의 모둘에서 기능이 지나치게 많음을 의미. 기능들 간의 의존성으로 다른 기능도 영향을 받을 수 있다. 이에 경우 다른 클래스로 분할해야 한다. 클래스가 방대하면 중복 코드가 존재할 가능성이 있다.
  • switch-case: 분기문의 사용을 최소화해야 한다. 같은 switch 조건이 코드에 존재할 수 있기 때문이다. 다형성을 이용하여 재정의로 대체하는 것이 바람직하다.
  • 주석: 코드의 의도를 기록으로 남기자!
  • 긴 매개변수 리스트: 인자를 최대한 객체로 넘기자! 하지만 추가적인 종속성을 만들고 싶지 않을 경우, 여러개의 파라미터로 풀어서 리스트로 넘기는 것이 바람직하다.
  • 확산적 변경: 하나의 클래스에서 자주 변경된다면 낮은 응집도를 의심해보아야 한다..! 하나의 클래스를 여러 개로 분리하는 것이 좋다.
  • 산탄총 수술: 총 수술에서 산탄총의 수술의 경우 몸에서 파편이 다양하게 박혀서 수술 집도가 어려운 수술이라고 한다. 여기서 쓰이는 의미로 응집도가 낮고 결합도가 높은 결합도의 상황을 의미한다. 확산적 변경와 달리 하나의 기능 수정할 부분들은 모두 하나의 클래스로 모으는 것이 바람직하다.
  • 기능 욕심: 자신이 속한 클래스보다 다른 클래스에 더 많이 접근하는 경우 (높은 결합도) 자주 접근하는 클래스로 메서드를 이동하는 것이 바람직하다.
  • 기본 타입에 대한 강박: 항상 함께 쓰이는 데이터들을 묶은 클래스, 별도의 객체로 만드는 것이 바람직하다.
  • 부적절한 친밀함: 지나치게 관여하는 클래스(높은 결합도)의 경우 메서드나 필드를 적절한 쪽으로 옮기거나 별도의 클래스로 빼내는 것이 적절하다. 상속이 원인이라면 상속 대신 합성을 사용한다.

Object oriented design

객체 지향 프로그래밍은 의존성을 효율적으로 통제할 수 있는 다양한 방법을 제공함으로써 요구사항 변경에 좀 더 수월하게 대응할 수 있는 가능성을 높여준다.

클래스? 인터페이스?

  • 클래스: 객체의 구현, 내부 상태와 오퍼레이션 구현 방법을 정의
  • 인터페이스: 오퍼레이션의 집합, 하나의 객체가 여러 인터페이스를 지원할 수 있고 서로 다른 클래스의 객체들이 동일한 인터페이스를 지원할 수 있다(다형성). type은 특정 인터페이스를 상징할 때 쓰는 용어다. 객체의 인터페이스를 정의함. 예를 들어 window 타입을 갖는 객체는 window 인터페이스에 정의한 오퍼레이션을 모두 처리할 수 있다.

상속에 있어서도 서로 다른 의미를 지닌다.

  • 클래스 상속: 코드 재사용 ➡ 결합도가 높음(컴파일시 부모와 자식이 강하게 결합)
  • 인터페이스 상속: 객체가 다른 곳에서 사용될 수 있음. ➡ 결합도가 낮음
    • 타입 계층의 구현은 부모 클래스에서는 일반화(generalization)를, 자식 클래스는 특수한(specialization) 개념을 상징.
    • 클라이언트 입장에서는 객체 타입에 대해 굳이 알 필요는 없다. 단지 인터페이스를 정의하는 추상 클래스가 무엇인지만 알고 있을 뿐.

하지만 언어별로 상속에 의미는 다른 의미를 지닌다.

  • C++: 가상함수를 갖는 클래스(추상 클래스)를 상속, 인터페이스 상속과 구현 상속을 모두 의미
  • Java: extends, implements로 클래스와 인터페이스 상속을 구분한다.

웬만해서는 인터페이스 상속을 하자!

런타임시 캡슐화를 유지하면서 참조자를 얻는 방식은 클래스 상속보다 동적이다. 따라서 상속보단 합성 을 이용하자.

Wrapup & Conclusion

첫 번째 과제로 지난 프로젝트를 토대로 UML 다이어그램을 작성하는 것이었는데, 당시 피드백은 잘 기억나지 않는 걸 봐선 무난하게 넘어간 것 같다.

아래는 그 당시 제작하던(지금은 실패한 ㅋ큐) 앱 프로젝트였는데, 그에 관한 다이어그램을 작성해서 발표하고 과제를 마쳤었다.

BeforeAfter

다음에는 Refactoring, Unit testing에 관해 다루어 보겠다..!

Authors