Chap 05. 요구 모델링

윤희빈·2026년 7월 22일

요구 모델링이란?

  • 고객과 개발자가 무엇이 개발되고 있는지에 동의하는 것을 주된 목적으로 요구 명세를 생성
  • 시스템에 대한 형식적/준형식적 설명을 제공
  • 작업결과인 명세는 고객과 개발자가 같은 의미로 이해되어야 한다.

요구 분석과 모델링의 차이

  • 요구 모델링은 고객과 개발자가 무엇을 개발할 것인지를 같이 이해하고 동의하는 요구 명세를 생성하는 것이 목적이다.
  • 요구 분석의 주요 목표 (구체적으로 요구 사항 분석)는 구축중인 시스템에 대하여 정형적인 또는 준정형적인 설명을 제공하는 것이다.


1 모델링

  • 복잡한 시스템을 다루는 방법
    • 전체를 다루기에는 너무 복잡한 대상을 추상화 또는 단순화

모델링을 하는 이유

  1. 복잡함을 관리하기 위해
  2. 눈에 보이지 않는 SW 구조를 시각화하기 위해
  3. 커뮤니케이션을 위해
  4. 문제 도메인/제품 요구사항을 이해하기 위해
  5. 개발 중인 시스템을 이해하기 위해
  6. 구현 전에 잠재 솔루션을 실험하기 위해
  7. 기존 시스템을 문서화하기 위해

1.1 관점과 추상화 수준

  • 모델은 특정 관점(perspective)과 추상화 수준(abstraction level)에 따라 달라짐

1.2 소프트웨어와 모델링

  • 모델은 그래픽 기호 + 주석으로 구성된 시각적 다이어그램 형태로 표현
  • 소프트웨어를 모델링하는 관점에는 비즈니스 프로세스 ,구조, 동작, 세가지 측면이 있다.

5.3. 모델 사이의 관계


2 UML (Unified Modeling Language)

  • 객체지향 SW를 모델링하는 표준 그래픽 언어
  • 시스템의 여러 측면을 그림으로 모델링(회로도처럼) → 모델링의 공통 언어

UML의 역사

  • OMT + Booch + OOSE 방법을 통합해 만들어짐

2.1 UML 다이어그램

  • 기능적 관점 / 구조적 관점 / 동적 관점으로 구성

2.2 모델링 과정

  1. 요구를 유스케이스로 정리 + 유스케이스 다이어그램 작성
  2. 클래스 후보를 찾아 개념 객체 모형 작성
  3. 유스케이스 기반으로 순서(시퀀스) 다이어그램 작성
  4. 클래스 속성/오퍼레이션/관계를 찾아 객체 모형 완성
  5. 상태/액티비티 등 다른 다이어그램 추가해 UML 모델 완성
  6. 서브시스템 파악 + 전체 구조 설계
  7. 재사용/커스터마이징/신규 객체 설계


3 정적 모델링

  • 정적 모델: 객체들의 공통 구조와 동작을 추상화한 것
  • 대표 다이어그램: 클래스 다이어그램(클래스 + 클래스 간 관계, 도메인 개념/속성 표현)

3.1 객체지향 개념

  • 객체/속성, 연관, 집합, 상속, 다형성

객체와 클래스

  • 객체: 상태/동작/고유 식별자를 가진 실체
  • 클래스: 공통 속성을 공유하는 객체 집합의 정의

속성과 오퍼레이션

각 객체가 자료구조를 가진다는 것은 각 객체가 어떤 속성을 가지고 있따는 것을 의미

각 객체가 적용될 수 있는 연산을 갖는다는 것은 각 객체가 어떤 연산을 수행할 수 있는 능력을 갖고 있다는 의미

캡슐화

  • 속성과 오퍼레이션을 묶어 단위화 + 정보 은닉

연관

  • 연관: 서비스를 제공하는 객체와 요청하는 객체가 상호작용하는 관계

  • 가시성: 객체 접근 가능성 A와 B가 연관이 있다면 A에서 B객체를 알고 있고 접근이 가능하여야 한다. 연관을 맺은 두 객체가 서로를 알게 하고 접근하게 하는 방법은 여러 가지가 있다.

포함 관계

  • 한 객체가 다른 객체의 집합을 포함하는 것을 의미
  • 포함하는 객체는 전체 개념에 해당하며 포함되는 객체는 전체에 속한 부분 개념이다.
  • 포함 관계의 강동에 따라 집합 관계와 합성 관계로 구분
  • 집합 관계
    • 느슨한 결합

  • 합성 관계
    • 강한 결합

상속

  • 일반화 클래스의 속성과 연산을 하위 클래스가 물려받음

상속(Inheritance) & LSP(리스코프 치환 원칙) (수업 정리)

교수님 예시: Bird - fly() 문제

  • Bird 클래스에 fly()가 있다.
  • MockingBird가 Bird를 상속받으면 fly()를 그대로 물려받아 사용 가능.
  • 그런데 Penguin도 Bird를 상속받게 하면?
    • 펭귄은 fly()가 불가능 → 상속 구조가 어색해짐

“그럼 예외 던지면 되지 않나요?”

  • Penguin.fly()에서 매번 exception을 던지게 만들 수는 있다.
  • 하지만 이런 방식은 “펭귄은 Bird이다”라는 관계를 코드로 표현해놓고, 실제로는 Bird의 중요한 행위를 못 하게 만드는 거라서 설계 자체가 잘못된 신호다.

해결 방향: 역할을 분리해서 상속/구현 구조 재설계

  • fly()가 Bird의 “본질”이 아니라면, Bird에 억지로 넣지 말고 날 수 있는 역할을 분리한다.

예시 구조(개념)

  • Bird : 새의 공통 속성/행동(날기 “제외” 가능)
  • Flyable : fly()를 가진 ‘날 수 있는’ 역할(인터페이스/클래스)

적용:

  • MockingBird : Bird + Flyable (날 수 있음)
  • Penguin : Bird만 상속 (날 수 없음)

➡️ 결론: 상속은 무조건 하는 게 아니라, 더 면밀하게 분석하고 역할을 쪼개서 설계해야 한다.


LSP (Liskov Substitution Principle)

  • 부모 클래스를 자식 클래스가 상속받으면, 자식은 부모의 역할을 대체할 수 있어야 한다.
  • 즉, 부모 타입 자리에 자식 객체를 넣어도 프로그램이 깨지면 안 됨
  • 펭귄이 Bird를 상속받아도 fly()를 정상적으로 수행할 수 없다면
    • 부모 역할을 대체 못 함 → LSP 위반
  • 그래서 LSP가 지켜지도록 새 클래스/역할(Flyable)을 분리해서 상속 구조를 다시 잡는 것

LSP 위반 예: Rectangle <- Square 상속

상황

  • Square가 Rectangle을 상속한다고 가정하면, 정사각형은 항상 w = h 조건을 유지해야 함.
  • 그런데 Rectangle은 일반적으로 w와 h가 독립적으로 바뀔 수 있음(직사각형).

기대 동작(부모 타입 Rectangle 기준)

  • R.getArea(5, 10)이라면
    • 가로 5, 세로 10인 직사각형
    • 면적 = 5 × 10 = 50 이 나와야 정상

실제 문제(자식 타입 Square가 끼어들면)

  • 같은 자리에 S.getArea(5, 10)을 넣는다고 생각해보면,
  • Square는 w=h를 강제해야 하니까 내부적으로 이렇게 동작하게 됨(개념적으로)
  1. width를 5로 설정 → w = h = 5
  2. height를 10으로 설정 → w = h = 10
  3. 결과 면적 → 10 × 10 = 100

즉, Rectangle 자리에 Square를 넣었더니 결과가 바뀜

  • 원래 기대: 50
  • 실제: 100

결론: LSP 위반

  • 부모(Rectangle)가 기대하는 행동(가로/세로 독립 설정)을 자식(Square)이 만족하지 못함
  • 따라서 Square is-a Rectangle 상속은 설계적으로 문제가 있고, LSP(리스코프 치환 원칙)가 깨진다.

다형성

  • 같은 이름으로 여러 형태를 받아들일 수 있는 특징
  • 같은 메시지를 다른 객체/서브클래스에 호출 가능

3.2 클래스 다이어그램

클래스 다이어그램은 UML 다이어그램 중에서 가장 대표적인 정적 다이어그램.

클래스

  • 클래스는 3구획: 이름 / 속성 / 오퍼레이션
  • 추상클래스=이탤릭체, 인터페이스=<>

속성

  • 객체가 가지는 모든 필드를 포함
  • 가시성: public (+), private(-), protected(#)

오퍼레이션/메소드

  • get/set 같은 흔한 메소드는 생략 가능
  • 오퍼레이션: 동작에 대한 인터페이스
  • 메서드: 오퍼레이션의 구현
  • 가시성: public (+), private(-), protected(#)

관계

  • 연관 / 상속 / 의존 / 구현

사례

상속과 인터페이스만 잘 표현되어 있으면 된다.

Transcirpt는 여러 Registration의 객체이다. 방향을 조심하자. Transcript가 소스이다.

4 동적 모델링

  • 동적 측면: 실행 중 변경될 수 있는 뷰(시간의 함수)
  • 정적 다이어그램을 보완하며, 객체 간 상호작용 패턴을 표현

상호작용 다이어그램

  1. 시퀀스 다이어그램
  2. 협동(콜라보레이션) 다이어그램

4.1 시퀀스 다이어그램

  • 객체들의 메시지 교환을 울타리 형태로 시각화하여 시스템 동작을 정형화

  • 객체는 기본적으로 객체 이름: 클래스 이름 형식으로 표현한다.
  • 조건문, 반복문은 프레임에 엮어서 표현해주면 된다.
  • 프레임
    • 조건 : alt
    • 반복 실행 : loop

작성 과정

  1. 참여 객체 파악
  2. 객체를 X축에 나열 + 라이프라인 그림
  3. 유스케이스 이벤트 순서대로 메시지 호출을 화살표로 표현

4.2 협동 다이어그램 (커뮤니케이션 다이어그램)

  • 상호작용에 필요한 객체들 간 링크를 포함한 객체 다이어그램 + 객체 간 메시지

4.3 상태 다이어그램

  • 이벤트 수신과 응답을 기반으로 상태 전이로 동작을 모델링

도서관 시스템에서 책의 상태


5 제어 모델링

액티비티 다이어그램(Activity Diagram)

  • 액티비티 사이 제어 흐름을 보여주는 흐름도 성격

  • 구성 요소
    - 액티비티(계산/프로세스)
    - 전환(제어가 다른 액티비티로 넘어감)
    - 분기(진위 조건)

BPEL : 교수의 언급

Business Process Execution Language(정식으론 WS-BPEL)

  • 뭐 하는 거? 여러 웹서비스(예: 결제 서비스, 배송 서비스, 재고 서비스)를 순서/조건/병렬/예외처리 규칙에 따라 엮어서 “비즈니스 프로세스를 실행(오케스트레이션)”하도록 정의하는 언어.
  • 어디서 쓰였어? 예전 SOA(서비스 지향 아키텍처) 환경에서 기업 시스템 통합(EAI)할 때 많이 등장했어. 보통 BPEL 엔진이 프로세스를 실행함.
  • 핵심 키워드 오케스트레이션(orchestration), 웹서비스 조합, 워크플로우 실행, 예외/보상(compensation) 처리
  • BPMN이랑 차이 BPMN은 사람이 보기 좋은 모델링 그림(표기법) 쪽, BPEL은 그걸 실제로 실행 가능한 프로세스로 표현하는 쪽(코드/정의)이라고 보면 돼.

POP(Partial Order Planning, 부분 순서 계획) 통합 정리

1) POP가 뭐냐?

  • 로봇/AI가 목표를 달성하기 위해 어떤 액션을 어떤 순서로 할지 계획을 세우는 방법
  • 모든 순서를 처음부터 고정하는 게 아니라, 필요한 제약(선후관계)만 최소로 정해가며 계획을 완성한다. → 그래서 “부분 순서(Partial Order)” 계획

2) 상태 표현: Predicate(프레디케이트)

  • 로봇이 “현재 상태”를 알 수 있게 논리 변수로 표현
  • 예: SocksOn(Left) = 왼발에 양말이 신겨져 있는가? (True/False)

3) 액션의 조건: Pre-condition(사전조건) / Constraint(제약)

  • 인간은 상식으로 순서를 알지만, 로봇은 모를 수 있으므로 명시적으로 조건을 적어줘야 함
  • 예:
    • Shoe(Left)를 하려면 Pre-condition: SocksOn(Left)=True
    • 만약 SocksOn(Left)=False라면 Shoe(Left)가 아니라 먼저 Socks(Left)를 해야 함
  • 즉, 로봇은 현재 상태(predicate)를 보고 가능한 액션을 선택한다.

4) POP에서 중요한 문제: “이전 액션을 취소(깨뜨리는) 상황” = Threat(위협)

  • 계획을 세우다 보면 나중에 하는 액션이 이전 액션의 결과를 망가뜨리는 경우가 생김
  • 이런 간섭을 Threat(위협) 또는 “이전 액션을 취소한다”라고 표현할 수 있음
  • 예시 형태:
    • 어떤 액션이 SocksOn(Left)=True를 만들어놨는데
    • 다른 액션이 그 상태를 False로 바꿔버리면 → 계획이 깨짐

5) Threat 해결 방법(POP가 하는 일)

POP는 위협을 발견하면 제약을 추가해서 충돌을 해결한다.

  • Promotion(프로모션): 위협하는 액션을 더 뒤로 보내기(순서 제약 추가)
  • Demotion(디모션): 위협하는 액션을 더 앞으로 보내기(순서 제약 추가)
  • Separation(분리/바인딩 제약): 변수/대상을 다르게 해서 충돌 자체를 피하기

6) 한 줄 결론

  • POP는 상태(predicate) + 사전조건(pre-condition) + 부분 순서 제약으로 계획을 세우고, 계획 중 생기는 액션 간 위협(threat: 이전 액션 결과를 깨뜨림)을 추가 제약으로 해결하는 계획법이다.

6 모델 검증

모델 검증 방법

  • 리뷰(워크스루, 인스펙션)
  • 테스팅
  • 정형적 방법
  • 프로토타이핑
  • 요구 추적

일관성 체크

  • 유스케이스 다이어그램 ↔ 시퀀스 다이어그램: 유스케이스 명세가 있고 매칭되는 시퀀스가 있는지

  • 시퀀스 다이어그램 ↔ 클래스 다이어그램: 시퀀스에 등장한 클래스/메시지가 클래스 다이어그램에 누락되지 않았는지

  • 상태 다이어그램 ↔ 클래스 다이어그램: 두 모델 간 크로스체크


실습

내용

기본 미션: 다음의 모바일기기 기반 지하철 출입 및 결제 시스템의 기능을 모델링하는 클래스 다이어그램과 시퀀스 다이어그램을 작성하시오.

  1. MobileApp 클래스가 TicketGate 클래스에 사용자의 ID를 전달

  2. TicketGate 클래스가 SeoulMetro에 다음의 정보를 전달

  • 사용자 ID

  • 태그 시각

  • 역 이름

  • 들어가는 경우  위의 3가지 정보만 전달

  • 나가는 경우 사용자 ID, 태그 시각, 역 이름 전달하고, 요금 계산 요청

  • 입력 또는 하역인 경우에 따른 로직이 달라지므로 이는 StarUML toolbox -> Interaction (Advanced) 에서 Combined Fragment라는 프레임을 이용해서 표현하도록 한다. Combined Fragment에 Operand를 추가하여 두가지의 경우를 처리하는 로직을 표현한다. interactionOperator는 "alt"로 지정한다. Combined Fragment는 두개의 다른 로직이 일어나는 시퀀스 다이어그램의 부분들을 감싸도록 표현한다.

  1. SeoulMetro 클래스는 CardPayment 클래스에 정산 요청을 한다.
  • SeoulMetro는 사용자 ID, 요금 및 요청 시각을 보낸다

  • CardPayment는  정산 완료 후 사용자 ID, 요금 및 처리 시각을 DB에 저장한다.

산출물

  • 모델들이 담긴 PDF 파일 (File -> Print to (Save as) PDF) (하나의 파일로 제출)

  • (보너스) 생성형 AI에 의해 생성된 코드 (아무 선호하는 언어)

유의 사항:

  • 변수, 함수의 이름들이 의미있게 작성되었는지 여부
  • 함수의 매개변수들이 명확한지 여부
  • 캡슐화가 제대로 되어있는지 여부 (가시성을 신경써서 설정할 것)
  • 시퀀스 다이어그램에서 상호 작용에 참여하는 클래스의 설정이 클래스 다이어그램 정보에 기반했는지 여부 확인할 것
  • 동적 모델의 조건 또는 반복 행위는 프레임과 operator를 사용하여 표현할 수 있음.
  • StarUML 한시적인 무료 버전외에 draw.io를 활용하여도 무방함

추가 미션 1:

Interaction Overview Diagram 기능을 사용하여, State Chart Diagram의 Decision 등을 써서 들어갈 때와 나올때의  로직을 구분해서 모델링

(들어갈 때의 로직과 나올때의 로직을 Interaction (Inline) 을 이용하여 별도의 시퀀스 다이어그램으로 그리고) 초기 조건에서 들어갈때와 나올때를 구분하여 어떤 시퀀스 다이어그램을 선택할지를 결정.

이 경우 기본 미션에서 썼던 Combined Fragment는 쓰지 않는 것으로 함 (쓸 필요가 굳이 없음)

Interaction Overview Diagram을 통한 State Chart Diagram과 시퀀스 다이어그램의 혼용 방법은 다음의 메뉴얼에서 참고하도록 한다.

https://docs.staruml.io/working-with-uml-diagrams/interaction-overview-diagram

https://drawio-app.com/blog/uml-interaction-overview-diagrams-in-draw-io/

추가 미션2:

모델링된 것을 ChatGPT 등의 생성형 AI에 넣어서 모델을 구현한 코드를 생성해달라고 해보자. 그리고, 어떤 결과가 나오는지 분석해보자.

TA (강의조교)들이 여러분들 실습을 봐줄테니, 많은 도움을 청하시기 바람.

  • 클래스 다이어그램

  • 시퀀스 다이어그램

  • 자바코드
    import java.time.LocalDateTime;
    import java.util.HashMap;
    import java.util.Map;
    
    // MobileApp 클래스
    class MobileApp {
        private String userId;
    
        public MobileApp(String userId) {
            this.userId = userId;
        }
    
        public String getUserId() {
            return userId;
        }
    
        public void sendUserId(TicketGate gate, boolean isEntry) {
            LocalDateTime tagTime = LocalDateTime.now();
    
            if (isEntry) {
                gate.processTagEntry(userId, tagTime);
            } else {
                gate.processTagExit(userId, tagTime);
            }
        }
    }
    
    // TicketGate 클래스
    class TicketGate {
        private String stationName;
        private SeoulMetro seoulMetro;
    
        public TicketGate(String stationName, SeoulMetro seoulMetro) {
            this.stationName = stationName;
            this.seoulMetro = seoulMetro;
        }
    
        public void processTagEntry(String userId, LocalDateTime tagTime) {
            seoulMetro.recordEntry(userId, tagTime, stationName);
        }
    
        public void processTagExit(String userId, LocalDateTime tagTime) {
            int fare = seoulMetro.calculateFare(userId, tagTime, stationName);
            seoulMetro.requestPayment(userId, fare, tagTime);
        }
    }
    
    // SeoulMetro 클래스
    class SeoulMetro {
        // 사용자별 입장 기록 저장
        private Map<String, EntryRecord> entryRecords = new HashMap<>();
        private CardPayment cardPayment;
    
        public SeoulMetro(CardPayment cardPayment) {
            this.cardPayment = cardPayment;
        }
    
        public void recordEntry(String userId, LocalDateTime tagTime, String stationName) {
            entryRecords.put(userId, new EntryRecord(tagTime, stationName));
            System.out.println("[SeoulMetro] 승차 기록 저장");
            System.out.println("사용자 ID: " + userId);
            System.out.println("태그 시각: " + tagTime);
            System.out.println("역 이름: " + stationName);
            System.out.println();
        }
    
        public int calculateFare(String userId, LocalDateTime exitTime, String exitStation) {
            EntryRecord entry = entryRecords.get(userId);
    
            System.out.println("[SeoulMetro] 요금 계산 요청");
            System.out.println("사용자 ID: " + userId);
            System.out.println("하차 시각: " + exitTime);
            System.out.println("하차 역: " + exitStation);
    
            if (entry == null) {
                System.out.println("승차 기록이 없어 기본 요금 0원 처리");
                System.out.println();
                return 0;
            }
    
            // 예시용 고정 요금
            int fare = 1500;
    
            System.out.println("승차 역: " + entry.getStationName());
            System.out.println("계산된 요금: " + fare + "원");
            System.out.println();
    
            return fare;
        }
    
        public void requestPayment(String userId, int fare, LocalDateTime requestTime) {
            System.out.println("[SeoulMetro] 결제 요청");
            System.out.println("사용자 ID: " + userId);
            System.out.println("요금: " + fare + "원");
            System.out.println("요청 시각: " + requestTime);
            System.out.println();
    
            cardPayment.settlePayment(userId, fare, requestTime);
        }
    }
    
    // CardPayment 클래스
    class CardPayment {
        private DB db;
    
        public CardPayment(DB db) {
            this.db = db;
        }
    
        public void settlePayment(String userId, int fare, LocalDateTime requestTime) {
            LocalDateTime processedTime = LocalDateTime.now();
    
            System.out.println("[CardPayment] 결제 정산 완료");
            System.out.println("사용자 ID: " + userId);
            System.out.println("요금: " + fare + "원");
            System.out.println("요청 시각: " + requestTime);
            System.out.println("처리 시각: " + processedTime);
            System.out.println();
    
            savePaymentHistory(userId, fare, processedTime);
        }
    
        public void savePaymentHistory(String userId, int fare, LocalDateTime processedTime) {
            db.savePayment(userId, fare, processedTime);
        }
    }
    
    // DB 클래스
    class DB {
        public void savePayment(String userId, int fare, LocalDateTime processedTime) {
            System.out.println("[DB] 결제 내역 저장 완료");
            System.out.println("사용자 ID: " + userId);
            System.out.println("요금: " + fare + "원");
            System.out.println("처리 시각: " + processedTime);
            System.out.println();
        }
    }
    
    // 승차 기록 저장용 클래스
    class EntryRecord {
        private LocalDateTime tagTime;
        private String stationName;
    
        public EntryRecord(LocalDateTime tagTime, String stationName) {
            this.tagTime = tagTime;
            this.stationName = stationName;
        }
    
        public LocalDateTime getTagTime() {
            return tagTime;
        }
    
        public String getStationName() {
            return stationName;
        }
    }
    
    // 실행 예시
    public class SubwaySystemDemo {
        public static void main(String[] args) {
            DB db = new DB();
            CardPayment cardPayment = new CardPayment(db);
            SeoulMetro seoulMetro = new SeoulMetro(cardPayment);
    
            TicketGate gangnamGate = new TicketGate("Gangnam", seoulMetro);
            TicketGate cityHallGate = new TicketGate("CityHall", seoulMetro);
    
            MobileApp app = new MobileApp("user123");
    
            // 입장
            app.sendUserId(gangnamGate, true);
    
            // 퇴장
            app.sendUserId(cityHallGate, false);
        }
    }
profile
비니비니히비니의 정리블로그

0개의 댓글