포스트

소프트웨어 설계 - 애플리케이션 설계

소프트웨어 설계 - 애플리케이션 설계

애플리케이션 설계

소프트웨어 아키텍처

소프트웨어를 구성하는 요소들간의 관계를 표현하는 시스템의 구조 또는 구조체

기본 원리

  • 모듈화
  • 추상화
  • 단계적 분해
  • 정보은닉

소프트웨어 아키텍처의 설계 과정

  1. 설계 목표 설정
  2. 시스템 타입 결정
  3. 아키텍쳐 패턴 적용
  4. 서브시스템 구체화
  5. 검토

객체지향(Object-Oriented)

유지보수 용이, 멀티미디어 데이터 및 병렬 처리를 지원

클래스

  • 객체들이 갖는 속성과 연산을 정의하고 있는 틀이자 데이터를 추상화 하는 단위
  • 클래스에 속한 각각의 객체는 인스턴스라고 부르며 새로운 객체를 생성하는 것을 인스턴스화 라고 함
  • 인스턴스화 : 클래스를 사용하여 메모리에 실제 구체적인 실체를 만드는 행위

객체

  • 데이터와 데이터를 처리하는 함수를 묶어놓은 하나의 소프트웨어 모듈
  • 클래스의 정의를 따라 메모리에 만들어진 구체적인 실체
  • 데이터와 함수를 한 몸에 지닌 독립적인 소프트웨어 모듈

  • 데이터 : 속성, 상태 변수, 상수, 자료 구조
  • 함수 : 메소드, 서비스, 동작, 연산
    • 객체는 독립적으로 식별 가능한 이름을 가진다
    • 객체가 가질 수 있는 상태는 시간에 따라 변화한다
    • 객체와 객체는 상호 연관성에 의한 관계가 형성된다
    • 객체가 반응하는 메세지의 집합 = 행위, 객체는 행위의 특징을 나타낸다
    • 객체는 일정한 기억장소를 가진다

캡슐화

  • 인터페이스를 제외한 세부 내용이 은폐되어 외부의 접근이 제한되고, 외부 모듈에 영향을 적게 받음
  • 재사용이 용이, 객체간의 결합도를 낮게함

상속

  • 하위클래스는 상위클래스의 모든 속성과 연산을 자신의 속성으로 사용 가능
  • 하위클래스는 상속받은 이외의 새로운 속성과 연산을 첨가 가능
  • 소프트웨어 재사용을 높이는 중요한 개념이다

다형성

  • 응용 프로그램 상에서 하나의 함수나 연산자가 두 개 이상의 서로 다른 클래스의 인스턴스들을 같은 클래스에 속한 인스턴스처럼 수행할 수 있게함

연관성

객체지향 분석 및 설계

객체지향 분석의 방법론

  • 럼바우: 가장 일반적으로 사용 / 객체, 동적, 기능 모델로 분석
    • SW 구성요소를 그래픽 표기법으로 모델링
    • 객체 모델링 표기법(OMT, Object-Modeling Technique)
    • 순서: 객체 모델링 → 동적 모델링 → 기능 모델링
      • 객체 모델링: 정보 모델링, 객체를 ERD로
      • 동적 모델링: 상태다이어그램으로 시간에 따른 객체 행위 모델링
      • 기능 모델링: 자료흐름도(DFD)로 자료 흐름/처리과정 모델링
  • 부치: 미시, 거시 개발 프로세스
  • 제이콥슨: 유스케이스
  • Coad/Yourdon: ERD
  • Wirfs/Brock: 분석, 설계 구분 X, 명세서 중심

객체지향 설계 원칙(SOLID)

단일 책임(SRP, Single Responsibility Principle)
  • 원칙 : 클래스는 단 하나의 변경 이유만 가져야한다(책임을 하나로 한정)

SRP 위반

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Employee {
  private String name;
  private int baseSalary;
  
  // 책임 1 : 급여 계산 로직
  public double calculatePay() {
    return this.baseSalary * 1.1;
  }
  
  // 책임 2 : 데이터 저장 로직
  public void saveToDatebase() {
    // 데이터 베이스 저장 로직
  }
}

SRP 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 책임 1: 데이터 모델 (DTO/Entity)
class EmployeeData {
  private String name;
  private String baseSalary;
}

// 책임 2: 급여 계산 로직만 담당
class PayCalculator {
  public double calculatePay(EmployeeData employee) {
    // 오직 급여 계산 로직만 존재
    System.out.println(employee.name + "의 급여를 계산합니다.");
    return employee.baseSalary * 1.1;
  }
}

// 책임 3: 데이터 저장 로직만 담당
class EmployeeRepository {
  public void save(EmployeeData employee) {
    // 오직 DB 저장 로직만 존재
    System.out.println(employee.name + "의 데이터를 저장소에 저장합니다.");
  }
}
개방 폐쇄(OCP, Open-Closed P)
  • 원칙: 확장에 대해 열려 있어야 하고, 변경에 대해 닫혀 있어야 한다. (기존 코드 수정 없이 기능 추가 가능)

OCP 위반

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// OCP 위반: 새로운 도형이 추가될 때마다 이 클래스 내부의 drawShape 메소드를 수정해야 함
class ShapeDrawer {
    public void drawShape(Object shape) {
        if (shape instanceof Circle) {
            System.out.println("원을 그립니다.");
        } else if (shape instanceof Rectangle) {
            System.out.println("사각형을 그립니다.");
        } 
        // 새로운 도형(Triangle)이 추가되면 'else if (shape instanceof Triangle)' 코드를 추가해야 함 (변경에 닫혀 있지 않음)
    }
}

class Circle {}
class Rectangle {}

OCP 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 추상화: Shape 인터페이스를 정의 (확장의 기준)
interface Shape {
    void draw(); // 모든 도형은 draw 행위를 가져야 함
}

// 확장 1: Circle은 Shape을 구현 (확장에 열려 있음)
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

// 확장 2: Rectangle도 Shape을 구현
class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("사각형을 그립니다.");
    }
}

// OCP 준수: 새로운 도형이 추가되어도 이 클래스는 수정할 필요 없음 (변경에 닫혀 있음)
class ShapeDrawer {
    public void drawShape(Shape shape) { // Shape 인터페이스에 의존
        shape.draw(); // 다형성(Polymorphism)을 이용해 실제 객체의 draw 메소드 호출
    }
}
리스코프 치환(LSP, Liskov Substitution)
  • 원칙 : 자식 클래스는 최소한 부모가 할 수 있는 일은 할 줄 알아야 함

LSP 위반

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 부모 클래스: Rectangle
class Rectangle {
    protected int width;
    protected int height;

    public void setWidth(int width) { this.width = width; }
    public void setHeight(int height) { this.height = height; }
    public int getArea() { return width * height; }
}

// LSP 위반: Square는 Rectangle의 setWidth를 재정의하여 부모의 의도를 망가뜨림
class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // 높이도 같이 변경 (부모와 다른 동작)
    }
    @Override
    public void setHeight(int height) {
        this.width = height; // 너비도 같이 변경
        this.height = height;
    }
}

// LSP 위반 테스트 코드
class LspViolationDemo {
    public static void printArea(Rectangle r) {
        r.setWidth(5); // 여기서 setWidth의 동작이 달라짐
        r.setHeight(4);
        // 사용자(클라이언트)는 항상 면적이 20이라고 기대함 (5 * 4)
        System.out.println("예상 면적: 20, 실제 면적: " + r.getArea());
    }

    public static void main(String[] args) {
        Rectangle rect = new Rectangle();
        printArea(rect); // 출력: 예상 면적: 20, 실제 면적: 20

        // 자식 객체(Square)로 치환했더니 프로그램 정확성이 깨짐!
        Rectangle square = new Square(); // LSP 위반 발생
        printArea(square); // 출력: 예상 면적: 20, 실제 면적: 16 (4*4, Square의 setWidth에 의해 높이도 4로 변경됨)
    }
}

LSP 적용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
abstract class Shape {
    public abstract int getArea();
}

class Rectangle extends Shape {
  private int width;
  private int height;

  public Rectangle(int width, int height) {
    this.width = width;
    this.height = height;
  }

  // Rectangle은 width와 height를 독립적으로 변경할 수 있음
  public void setWidth(int width) { this.width = width; }
  public void setHeight(int height) { this.height = height; }

  @Override
  public int getArea() {
    return width * height;
  }
}

class Square extends Shape {
  private int side;

  public Square(int side) {
    this.side = side;
  }

  // Square는 setWidth나 setHeight를 가지지 않거나, 
  // 가지더라도 side를 변경하는 방식으로 구현하여 Rectangle과 충돌을 피함
  public void setSide(int side) {
    this.side = side;
  }

  @Override
  public int getArea() {
    return side * side;
  }
}

class LspCleanDemo {
  // 클라이언트 코드는 오직 Shape이 할 수 있는 일(getArea)만 요청
  public static void printArea(Shape s) {
    // setWidth/setHeight와 같은, 치환 시 문제가 될 수 있는 메소드를 사용하지 않음
    System.out.println("도형의 면적: " + s.getArea());
  }

  public static void main(String[] args) {
    Shape rect = new Rectangle(5, 4);
    printArea(rect); // 출력: 도형의 면적: 20

    // Square 객체로 치환되어도 프로그램은 문제 없이 작동 (LSP 준수)
    Shape square = new Square(5);
    printArea(square); // 출력: 도형의 면적: 25 
  }
}

인터페이스 분리(ISP, Interface Segregation): 사용X 인터페이스와 관계, 영향X
  • 원칙 : 자신이 사용하지 않은 인터페이스에 의존하지 않아야 한다.

ISP 위반

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
interface Worker {
  void work();
  void eat();
  void sleep();
}

class human implements Worker {
  @Override
  void work(){
    
  }
  @Override
  void eat() {
    
  }
  @Override
  void sleep() {
    
  }
}

class Robot implements Worker {
  @Override
  void work() {
    
  }
  
  // 로봇 클래스는 사용하지 않은 인터페이스
  @Override
  void eat() {}
  @Override
  void sleep() {}
}

ISP 개선

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Workable {
  void work();
}

interface EatableSleepable {
  void eat();
  void sleep();
}

class Human implements Workable, EatableSleepable {
  @Override public void work() { /* ... */ }
  @Override public void eat() { /* ... */ }
  @Override public void sleep() { /* ... */ }
}

class Robot implements Workable {
  @Override public void work() {
    System.out.println("로봇이 작업합니다.");
  }
}
의존역전(DIP, Dependency Inversion): 추상성 높은 클래스와 의존

원칙 : 상위 모듈은 하위 모듈에 의존해서는 안된다, 추상화는 세부사항에 의존해서는 안된다

DIP 위반

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class GasolineEngine { // 구체적인 하위 모듈
    public void start() {
        System.out.println("휘발유 엔진 시동!");
    }
}

class Car { // 상위 모듈
    private GasolineEngine engine; // 구체적인 클래스에 직접 의존

    public Car() {
        this.engine = new GasolineEngine(); // Car 내부에서 구체적인 엔진을 생성 (강한 결합)
    }

    public void startEngine() {
        engine.start();
    }
}

DIP 개선

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 1. 추상화(인터페이스): 엔진의 공통 기능 정의
interface Engine {
    void start();
}

// 2. 하위 모듈: 추상화에 의존 (세부 사항이 추상화에 의존)
class GasolineEngine implements Engine { // 구체적인 하위 모듈
    @Override
    public void start() {
        System.out.println("휘발유 엔진 시동!");
    }
}

class ElectricEngine implements Engine {
    @Override
    public void start() {
        System.out.println("전기 모터 시동!");
    }
}

// 3. 상위 모듈: 추상화에 의존
class Car {
    private Engine engine; // 추상화(Engine 인터페이스)에 의존

    // 외부에서 Engine의 구현체를 주입받음 (의존성 주입, DIP의 핵심 구현 방식)
    public Car(Engine engine) { 
        this.engine = engine;
    }

    public void startEngine() {
        engine.start();
    }
}

// DIP 준수 데모
class DipDemo {
    public static void main(String[] args) {
        Engine gasoline = new GasolineEngine();
        Engine electric = new ElectricEngine();

        // Car는 어떤 Engine이 주입되든 관계없이 동작
        Car gasCar = new Car(gasoline); // 휘발유 엔진 장착
        gasCar.startEngine();

        Car ev = new Car(electric); // 전기 엔진 장착
        ev.startEngine();
    }
}

디자인 패턴

  • 개요
    • 설계 시 참조할만한 전형적인 해결방식
    • 문제/배경, 사례, 샘플코드 (Don’t reinvent the wheel - 바퀴를 다시 발명하지 마라)
  • GoF의 디자인 패턴: 생성(5), 구조(7), 행위(11) → 총 23
    • 장단점 범용성 높고 구조 파악 용이 객체지향 설계, 생산력 증대 이미 검증됨 → 개발 시간, 비용 단축 초기 투자비용 높음 개발자간 의사소통 용이 설계 변경 요청 대처 가능 객체지향 아니면 곤란

GoF 디자인 패턴 23가지 요약 정리

GoF(Gang of Four) 디자인 패턴은 소프트웨어 설계에서 반복적으로 발생하는 문제에 대한 해결책을 3가지 유형(생성, 구조, 행위)으로 분류한 것입니다.


1. 생성 패턴 (Creational Patterns) - 5가지

패턴 이름핵심 정의 및 목적
Abstract Factory (추상 팩토리)구체적인 클래스에 의존하지 않고, 서로 관련되거나 의존하는 객체들의 집합을 생성하기 위한 인터페이스를 제공합니다. (팩토리들의 팩토리)
Builder (빌더)복잡한 객체를 생성하는 방법과 그 객체의 표현(Representation)을 분리하여, 동일한 생성 절차로 다양한 형태의 객체를 만들 수 있게 합니다.
Factory Method (팩토리 메서드)객체를 생성할 인터페이스는 미리 정의하되, 실제 인스턴스를 만들 클래스의 결정은 서브 클래스에게 위임합니다.
Prototype (프로토타입)기존 객체를 복제(Clone)하여 새 객체를 생성함으로써, 객체를 직접 생성하는 비용을 줄이고 객체 타입에 구애받지 않고 복제가 가능하게 합니다.
Singleton (싱글톤)클래스의 인스턴스를 오직 하나만 생성하도록 보장하고, 어디서든 이 유일한 인스턴스에 접근할 수 있는 전역적인 접근점을 제공합니다.

2. 구조 패턴 (Structural Patterns) - 7가지

패턴 이름핵심 정의 및 목적
Adapter (어댑터)클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환하여, 호환되지 않는 인터페이스를 가진 클래스들이 함께 작동할 수 있도록 합니다. (접속기 역할)
Bridge (브리지)추상화(Abstraction)와 구현(Implementation)을 분리하여, 각각을 독립적으로 확장할 수 있도록 연결합니다.
Composite (컴포지트)객체들의 부분-전체 계층 구조를 정의하여, 클라이언트가 개별 객체와 복합 객체(그룹)를 동일하게 다룰 수 있도록 합니다.
Decorator (데코레이터)객체의 결합을 통해 기능을 동적으로 확장할 수 있도록 하며, 기존 클래스를 상속하는 대신 필요한 기능을 감싸(Wrap) 추가합니다.
Facade (퍼사드)복잡한 서브 시스템의 인터페이스 집합에 대해 통합된 하나의 단순한 인터페이스를 제공합니다.
Flyweight (플라이웨이트)다수의 유사한 객체들을 공유하여 메모리 사용량을 줄이고, 객체 생성 비용을 절감하는 데 사용됩니다. (객체의 공통 속성 공유)
Proxy (프록시)접근을 제어하거나, 복잡한 처리를 대신 수행하는 등 다른 객체에 대한 대리인(대리 객체)을 제공합니다.

3. 행위 패턴 (Behavioral Patterns) - 11가지

패턴 이름핵심 정의 및 목적
Chain of Responsibility (책임 연쇄)요청을 처리할 수 있는 기회를 여러 객체에게 부여하여, 객체들이 체인 형태로 연결되어 요청을 순차적으로 처리하게 합니다.
Command (커맨드)요청 자체를 객체로 캡슐화하여, 요청을 발행하는 객체와 수신하는 객체를 분리하고, 명령을 기록, 취소, 매개변수화할 수 있게 합니다.
Interpreter (인터프리터)언어의 문법(Grammar)을 정의하고, 그 문법에 따라 문장을 해석하는 인터프리터(해석기)를 만드는 방법을 제공합니다.
Iterator (반복자)집합 객체(컬렉션)의 내부 구조를 노출하지 않고 그 요소들에 순차적으로 접근할 수 있는 방법을 제공합니다.
Mediator (중재자)객체들 간의 상호작용을 캡슐화하는 중재자 객체를 정의하여, 객체들이 직접 통신하지 않고 중재자를 통해 통신함으로써 결합도를 낮춥니다.
Memento (메멘토)객체의 내부 상태를 외부에 노출하지 않고 이전 상태로 되돌릴 수 있는 기능을 제공합니다. (되돌리기 기능 구현)
Observer (옵서버)한 객체의 상태가 변하면, 그 객체에 의존하는 모든 객체들에게 변경 사항이 통보되고 자동으로 업데이트되도록 합니다. (일대다 의존성)
State (상태)객체의 내부 상태가 바뀔 때마다 객체의 행위가 바뀌도록 하며, 마치 클래스가 달라지는 것처럼 보이게 합니다.
Strategy (전략)동일 계열의 알고리즘들을 정의하고 각각을 캡슐화하여, 이들을 상호 교환할 수 있게 합니다. (런타임에 알고리즘 선택)
Template Method (템플릿 메서드)알고리즘의 골격(템플릿)을 상위 클래스에 정의하고, 구체적인 단계별 처리는 하위 클래스에 위임하여 구현합니다.
Visitor (방문자)객체 구조로부터 처리 기능(Operation)을 분리하여, 기능이 필요한 구조를 돌아다니며 작업을 수행하는 객체(Visitor)를 정의합니다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.