SW Engineering - 설계원리

윤형·2025년 4월 20일

Sofrware Engineering

목록 보기
5/9
post-thumbnail

서론

지난시간 까지는 UML, 클래스 다이어그램등을 배웠다. 그렇다면 이제는 본격적인 설계를 해볼 차례이다.


전통적인 설계

전통적인 설계는 개발 과정에서 요구사항 분석, 설계, 구현, 테스트, 유지보수등 과정을 단계별로 체계적으로 진행하는 고전적인 접근 방법을 의미한다. 전통적인 설계원칙에는 크게 3가지가 있다.

  • 추상화
  • 캡슐화
  • 모듈화

1. 추상화 : Abstaction

복잡한 시스템이나 객체에서 핵심적인 내용만 남겨서 단순화 한다.
: 불필요한 내용은 감추고, 필요한 부분만 노출시킨다.

장점 : 복잡성을 줄이고 시스템 전체 구조를 보다 쉽게 파악할 수 있다.

2. 캡슐화 : Encapsultion

내부 정보를 외부에서 접근할 수 없게 만드는 보호 기법이다.

이런식으로 설계에서 +,- 를 이용해 보호 수준을 정하는 걸 의미한다.

3. 모듈화 : Modularization

시스템의 전체 기능을 여러 개의 독립적인 모듈로 분할하여 설계하는 원칙이다.

  • 각 모듈은 명확한 역할과 책임을 가지며, 다른 모듈들과 최소한의 인터페이스를 사용하여 상호작용한다.

장점 : 각각의 모듈을 독립적으로 수정할 수 있다. 재사용 가능하다.
단점 : 너무 세분화 하면 모듈간 상호작용을 이해하기 어려워진다. (복잡성 증가)

Q. 어떤 것이 적절한 모듈일까?
A. 결합도는 낮추고, 응집도는 높이는 모듈

결합도

모듈간의 서로 의존하는 정도.

결합도가 높다는 의미 : 모듈간 상호작용이 많다 -> 이해하기 어려워짐 -> 버그 수정하기 어려움, 예상하지 못한곳에서 버그 발생 가능.

그렇다면 어떤 요소들이 결합도에 영향을 미치게 될까?

  1. 모듈 외부로 노출되는 인터페이스 수
    • public class, public method...
  2. 모듈 간 결합도 타입
    • 내용 결합
    • 공통 결합
    • 제어 결합
    • 스탬프 결합
    • 데이터 결합

내용 결합 (Content Coupling)

다른 모듈이 내용에 직접적인 의존성이 있는 경우
-> 모듈을 사용하기 위해 그 모듈의 내용을 알고 있어야 하는 경우

해결법: 단순한 getter/setter보다는 논리적으로 적절한 메서드를 고려한다.

public class User {
  public int age;
}

…
user.age = user.age + 1;

이렇게 나이를 하나 더하는 클래스가 있다면,

public class User {
  private int age;
  public void getOld() {age += 1;}
}
user.getOld();

이런식으로 필요한 부분을 메서드로 논리적으로 고려한다. (단순 getter, setter말고)

공통 결합 ( Common Coupling )

모듈간 전역 변수를 공유하는 경우.

public class Item {
  public static int PRICE = 20;
}

이런식으로 static을 하면 문제가

Item a = new Item();
a.PRICE = 100;
Item b = new Item();
System.out.println(b.PRICE);

객체별 변수인줄 알고 a를 수정해버리면 이 클래스를 이용한 객체들 즉, b까지 값이 바뀌게 된다.

해결법 : 전역 변수 혹은 static사용을 피한다.
해결법2 : 혹은 싱글톤 객체로 만든다. (싱글톤 = 하나만 존재하는 객체)

class Config {
  public int PRICE;
};
Config globalConfig = new Config();

제어 결합 (Control Coupling)

매개변수 등으로 다른 모듈의 제어 흐름 경로를 바꿀 수 있는 경우

public class Store {
  public void onPurchase(boolean isPromotion) {
    if (isPromotion) {
      // 할인된 가격
    } else {
      // 정가
    }
  }
}

위 코드는 bool 값을 받아서 흐름을 제어하고 있는 모습이다.
하지만 단일성의 원칙을 위반한 것이다. 왜냐하면 한 메서드는 한가지의 기능을 하는 것이 바람직 하기 때문이다.

따라서 그냥 bool값을 넘겨주지 말고 처음부터 다른 기능을 하는 메서드나 클래스를 만들어 필요한 순간에 다른 기능을 호출하는게 더 효율적이다.

스탬프 결합 (Stamp Coupling)

모듈간 자료구조 전체를 인터페이스로 전달하여 상호작용하는 결합 형태를 의미한다.

// 스탬프 결합 예시: Record라는 객체 전체를 전달
public static void calculateFee(Record record) {
    int defaultMoney = 1000;
    record.setFee(defaultMoney * record.useTime);
}

이런식으로 Recore라는 클래스나 자료구조를 전달받아 사용하는 형태를 스탬프 결합이라고 생각하면 된다.
사실 괜찮은 형태이긴 하지만 추후에 발생할 수 있는 문제는 구조를 다 사용하지 않는데 전체를 넘기기 때문에 자원 낭비가 된다는 것이다.

해결법: 당연하겠지만 사용하는 부분만 인수로 넘기거나 활용하는 식으로 하면 된다. 하지만 개인적으로 그냥 전체를 넘기는게 뭔가 깔끔....하다.ㅎㅎ

데이터 결합 (Data Coupling)

모듈 간에 오직 필요한 데이터(기본 타입 변수, 값 등)만을 파라미터로 전달하는 결합 방식입니다.

// 데이터 결합 예시
public static void calculateFee(int useTime) {
    int defaultMoney = 1000;
    int fee = defaultMoney * useTime;
}

위에서 말했던 스탬프 해결책이 데이터 결합이다. 근데! 만약에 넘길 인자가 많아진다면 스탬프로 넘기는게 이득이다. 따라서 적절하게 스탬프-데이터 결합을 사용하는 것이 중요하다.


응집도 (Cohesion)

하나의 모듈 안에서 수행되는 작업들이 서로 관련된 정도를 의미한다.

  • 우연적 응집 (coicidental) : 아무 의미 없이 그냥 우연히 모아진 것.

  • 논리적 응집 (logical): 큰 범주에서 볼 때 비슷한 기능들이라 묶은 경우

    • ex) 마우스를 다루는 함수와 키보드를 다루는 함수가 모여있음.
  • 시간적 응집 (temporal) : 내부 요소들이 비슷한 시기에 호출될 수 있어서 묶어둔 경우

    • ex) 파일을 여는 함수와, 거기서 발생할 수 있는 예외를 같이 묶음.
  • 절차적 응집 (procedural) : 내부 요소들의 수행 순서에 따라 묶은 경우.

    • ex) 파일을 열고, 단어 수를 세고, 화면에 출력하는 기능을 묶어둠.
  • 교환적 응집 (communicational) : 같은 데이터 타입을 다루기 때문에 묶은 경우.

    • ex) 유저 로그인, 로그 아웃, 유저 정보 출력은 모두 유저를 다루는 내용이므로 같이 묶는다.
  • 기능적 응집 (functional) : 모듈이 한 기능에 모두 기여하는 것들을 묶음

    • ex) 유저 로그인을 위해 필요한 모든 함수를 묶어둠.

아래로 내려갈 수록 높은 응집성을 보여서 추구해야하는 방향에 가깝다.

결합도, 응집도가 미치는 영향

  • 결합도는 모듈 간 의존성에 영향을 미치게 된다.

    • 낮은 결합도 : 모듈간 낮은 의존성
    • 높은 결합도 : 모듈간 높은 의존성
  • 응집도는 한 모듈이 얼마나 연관된 기능을 담고 있는지에 대한 내용이므로 높은 응집도를 갖게 만들면 불필요한 모듈 분리에 따른 의존성을 없앨 수 있다.
    -> 응집도가 높을 수록 의존도는 낮아짐

  • 모듈 간 의존성은 빌드에 영향을 미친다.
    -> B가 A에 의존적이면 A에 변경이 생기면 B도 처리를 해야한다.
    -> 일정 규모 이상이 되면 빌드 시간이 10~60분 정도 된다. 따라서 빌드 시간을 줄이는 것은 생산성을 높이는 것이다.

  • 두 사진중에서 어떤게 깔끔해 보이는가? 당연히 첫번째 사진이다. 만약 B에서 문제가 생기면 B만 고치면 된다. 하지만 밑에 그림경우에는 B에서 문제가 생기면 이와 연결되어있는 다른 모듈들도 전부 수정해야 한다.

아키텍쳐 기반 설계

아키텍쳐를 먼저 정하는 설계이다.

  • 소프트웨어가 커지면 커질수록 복잡도가 무조건 높아진다.
  • 그래서 처음부터 전체 시스템이 어떻게 생겼는지, 어떤 컴포넌트들이 있고 서로 어떻게 소통하는지 먼저 결정한다.

예시 : 배달앱 개발

  1. 아키텍처 단계 (큰 구조 설계) → 어떤 구성 요소(컴포넌트)가 있는지 정의해
  • 사용자 앱 (Client)
  • 배달 기사 앱
  • 가게 관리자 웹
  • 서버 (API 서버)
  • 데이터베이스
  • 이 요소들이 어떻게 연결되는지도 정한다.
    (예: 클라이언트는 API 서버에 요청을 보냄 → 서버는 DB에서 데이터 가져옴)
  1. 서브시스템으로 나누기 (복잡도 줄이기)
    : 서버를 다시 나누기
  • 사용자 관리 시스템
  • 주문 처리 시스템
  • 결제 시스템
  • 알림 시스템 등
  1. 관점에 따른 아키텍처 표현
관점설명예시
모듈 관점코드 기준으로 기능을 어떻게 나눌지로그인 모듈, 결제 모듈 등
컴포넌트 관점실행되는 컴포넌트들의 연결 구조클라이언트-서버, pub-sub
할당 관점물리적 자원에 어떻게 배치할지API 서버는 AWS EC2, DB는 RDS 등

소프트웨어 품질 목표

품질 제약의 우선순위에 따라 설계가 달라질 수 있다.
예를들어 보안이 가장 중요하면 보안 알고리즘등을 우선적으로 설계해야하고
속도가 가장 중요하다고 하면 캐시 서버나 데이터베이스 성능 위주로 개발하게 된다.

  • 따라서 요구의 품질을 구체적으로 명시하고 합의한 후 개발을 시작해야 한다.

  • 효율성

    • 시간적 관점 : 응답시간, 단위 시간당 처리량, CPU사용량
    • 공간적 관점 : 메모리 사용량
  • 단순성

    • 단순한 설계, 단순한 구현이 더 선호됨
    • 소프트웨어 유지보수 편의성에 직접적인 영향을 미치기 때문

SOLID 디자인 원칙

Design Principles and Design Patterns이라는 책에서 나온 객체지향 설계를 위한 5가지 원칙을 의미한다.

Single-responsibility principle (단일 책임 원칙)

클래스, 모듈, 함수는 오직 하나의 책임(기능) 만 가져야 한다.

이런식으로 하나의 클래스에는 거기에 맞는 기능만 넣는 것이다. 또 필요한 기능은 외부에서 상속하거나 가져다가 쓴다.

+) 참고로 내가 게임개발할때 단일성의 원칙을 지키지않아서 Player코드에서 몬스터 소환까지 했다... 절대 귀찮다고 한번에 넣지 말자. 나중에 수많은 버그를 뱉어낸다....

Open-closed principle (개방 폐쇄 원칙)

확장에 대해서는 개발적이고, 수정에 대해서는 폐쇠적이여야 한다.
-> 확장은 쉽게, 확장에 따른 변셩은 최소화
-> 기존코드를 수정하지 않고 확장해야함 이라는 뜻.

void makeNoise() {
  if (type.equals("dog")) {
    System.out.println("멍멍");
  } else if (type.equals("cat")) {
    System.out.println("야옹");
  }
}

이런 코드가 있다면, 새로운 동물이 추가 될 때마다 이걸 추가해야 한다.

따라서 이런식으로 분리를 시켜두면, 새로운 동물이 추가될 때마다 동물만 추가하면 된다. 이렇게 확장은 쉽고 기존 코드는 최대한 건드리지 않는 것이 개발 폐쇠 원칙이다.

참고로 이는 객체지향의 "다형성" 과 연관이 있다는 사실을 알 수 있다.

Liskov substitution principle (리스코프 교체 원칙)

"자식클래스는 언제나 부모 클래스를 대신할 수 있어야 한다."라는 의미이다.

Parent p = new Child();

이런식으로 부모 클래스 위치에 자식이 올 수 있어야 한다는 의미이다.

class Bird {
    void fly() {
        System.out.println("I can fly!");
    }
}

class Ostrich extends Bird {
    void fly() {
        throw new UnsupportedOperationException("I can't fly!");
    }
}

이런식으로 자식 클래스가 부모클래스를 대신하지 못한다면 리스코프 교체 원칙을 위반한 것이다.

또다른 대표적인 위반 사례는 타원과 원이 있다.

class Ellipse {
    protected double radiusX;
    protected double radiusY;

    public void setRadiusX(double r) { radiusX = r; }
    public void setRadiusY(double r) { radiusY = r; }
}

class Circle extends Ellipse {
    @Override
    public void setRadiusX(double r) {
        radiusX = r;
        radiusY = r; // 원은 항상 두 반지름이 같아야 하니까!
    }

    @Override
    public void setRadiusY(double r) {
        radiusX = r;
        radiusY = r;
    }
}
Ellipse e = new Circle();
e.setRadiusX(10);
e.setRadiusY(5); // 타원에서는 가능한 행동

System.out.println(e.radiusX); // 5? 10? 뭔가 이상함

이런식으로 하면 원의 기능이 정상적으로 작동하지 않고 문제가 생긴다. 이것이 자식이 부모를 완전히 대체하지 못하기때문에 발생하는 문제이다.

Interface segregation principle (인터페이스 분이 원칙)

클라이언트는 자신이 쓰지도 않을 인터페이스를 받으면 안된다.
= 범용성 있는 인터페이스를 만드는게 아니라 한 기능당 한 인터페이스를 만들어야함.

  • 단일 책임 원칙과의 차이점? : 클래스를 의미하냐 인터페이스를 의미하냐 차이
  • 인터페이스 설계는 신중히 해야함 : 나중에 인터페이스를 수정하게 되면 많은 코드를 수정해야 한다.

Dependency inversion principle (의존관계 역전 원칙)

클래스를 참조할 때는 추상 클래스/인터페이스를 사용해야 한다.

public class Client {
  public void doSomething(Service service) {
    service.f();
    service.g();
  }
}
public class Service {
  public void f() {
    // 이 코드가 약 1천 줄 된다고 합시다.
  }
  public void g() {
    // 이 코드가 약 1천 줄 된다고 합시다.
  }
}

만약 이렇게 코드가 존재한다고 해보자.
만약 h()라는 새로운 코드를 추가한다고 하면, client.java, service.java둘다 새로 빌드를 해야한다.

public interface Service {
  public void f();
  public void g();
}
public class ServiceImpl implements Service {
  public void f() {
    // 이 코드가 약 1천 줄 된다고 합시다.
  }
  public void g() {
    // 이 코드가 약 1천 줄 된다고 합시다.
  }
}
,,,
Service s = new ServiceImpI();
doSomething(s);
,,,
  • Service를 인터페이스로 변경하고, 실제 구현은 ServiceImpI에서 한다.
  • 이렇게 하고 h()를 serviceImpI에 추가하게 되면 client는 빌드 X
  • 꼭 인터페이스 말고 Abstract로도 똑같이 할 수 있다.
  • Q. 구상 클래스를 바로 사용하지 않고 ImpI클래스는 두는 경우 장단점.
  • 장점: 유연하다. 구조를 쉽게 바꿀 수 있다. 빌드 시간이 줄어든다. 확장성과 유지보수가 좋아진다.
  • 단점: 코드 수가 증가한다. 복잡성이 올라간다.
profile
제가 관심있고 공부하고 싶은걸 정리하는 저만의 노트입니다.

0개의 댓글