SoC(Seperation of Concern): 관심사의 분리

mmmn0616·2026년 8월 18일

배경

개인적으로 진행하고 있는 DSA(Distributed Spacecraft Autonomy Testbed) 프로젝트를 진행하고 있다. 우주선이 임무를 수행할지 말지 결정하고, 어떤 우주선이 이 임무를 수행할 것인가? 를 결정하는 로직을 짜고 있는데 이 과정에서 SoC를 적용해보았다.

SoC란?

SoC(Separation of Concerns, 관심사의 분리)는 하나의 함수나 모듈이 하나의 관심사만 가지도록 나누는 설계 원칙이다.

여기서 "관심사"는 프로그램이 신경 써야 하는 문제 하나하나를 의미한다. 예를 들어 "이 작업이 수행 가능한가?"와 "이 작업의 우선순위 점수는 몇 점인가?"는 서로 다른 질문이고, 서로 다른 관심사다.

SoC가 필요한 이유

관심사를 나누지 않으면 이런 함수가 나올 수 있다.

def evaluate(spacecraft, task):
    if not spacecraft.sensor_availability:
        return -1  # 불가능
    if spacecraft.battery < task.required_energy:
        return -1  # 불가능
    score = task.priority - task.required_energy
    return score

이 함수는 "가능/불가능 판단"과 "점수 계산"을 동시에 하고 있다.
센서 상태가 사용 가능한 상태인지, 임무 수행에 필요한 에너지가 충분한지는 임무의 우선순위를 결정하기 이전에 임무 수행이 가능한지를 우선적으로 판단하는 조건이다. 이렇게 관심사가 나눠지지 않은 상태라면 조건이 추가될 때마다 점수 계산 로직 옆에 끼워 넣어야 한다.
그리고 점수 계산 공식만 따로 테스트하고 싶어도, 불가능 케이스(-1)까지 같이 처리해줘야 해서 테스트 케이스가 지저분해질 여지가 있다.

관심사를 나누면 각 함수는 자기 역할만 신경 쓰면 되고, 한쪽을 바꿔도 다른 쪽 로직을 건드릴 필요가 없다.

내 DSA 프로젝트에서 SoC를 구현한 방법

이 프로젝트에서는 각 spacecraft의 상태, 그리고 주어진 임무의 상태를 보고 "어떤 spacecraft가 이 임무를 맡게 될 것인가?"를 결정한다.
그렇게 되려면 각 spacecraft는 다음 순서로 판단해야한다.
1. 이 임무가 현재 상태에서 수행가능한가?(is_feasible 함수)
2. 이 임무를 내가 수행하게 된다면 얼마만큼의 가치를 가지는가?(bid 함수, 클수록 우선순위가 큼)

1. "가능한가"와 "얼마나 우선순위 있는가"를 분리

def is_feasible(spacecraft: Spacecraft, task: Task, current_time: float) -> bool:
    if not spacecraft.sensor_availability:
        return False
    if spacecraft.health_status == HealthStatus.FAILED:
        return False
    if spacecraft.storage < task.required_storage:
        return False
    if current_time + task.observation_duration > task.deadline:
        return False
    if task.id in spacecraft.current_tasks:
        return False
    if _battery_margin(spacecraft, task) < SAFE_SOC_RESERVE:
        return False
    return True


def bid(spacecraft: Spacecraft, task: Task, link: Link | None = None,
        weights: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)) -> float:
    w1, w2, w3, w4 = weights
    return (
        w1 * task.priority
        - w2 * _energy_cost(spacecraft, task)
        - w3 * _slew_cost_proxy(spacecraft, task)
        + w4 * _battery_margin(spacecraft, task)
    )

is_feasible()은 "이 우주선이 이 작업을 할 수 있는가?" (bool)를 판단한다. 여기에서 작업 불가능한 경우를 미리 거른 후, 작업 가능하다고 판단된 spacecraft에 대해서만 bid 함수로 가치를 계산한다.
bid()는 가능하다는 전제 하에 "이 작업을 얼마나 하고 싶은가?"를 계산한다 (숫자 점수)

이렇게 나눠두면 두 가지가 좋아지는데,

  • is_feasible()만 따로 테스트할 수 있다. (배터리 부족일 때, 센서 고장일 때, 마감 못 맞출 때 등 각 조건을 독립적으로 검증)
  • 나중에 하드 제약 조건(예: 통신 링크 필요 여부)이 추가되어도 bid()의 점수 공식은 건드릴 필요가 없다.

2. 점수 계산 안에서도 계산 단위를 쪼갬

bid() 내부의 각 항목(_energy_cost, _battery_margin, _slew_cost_proxy)도 헬퍼 함수로 분리해뒀다.

def _energy_cost(spacecraft: Spacecraft, task: Task) -> float:
    return task.required_energy

def _battery_margin(spacecraft: Spacecraft, task: Task) -> float:
    return spacecraft.battery - task.required_energy

def _slew_cost_proxy(spacecraft: Spacecraft, task: Task) -> float:
    raw = _distance(spacecraft.position, task.target_location)
    return min(raw, SLEW_PROXY_DISTANCE_SCALE)

bid()는 이 조각들을 조합만 하고, 각 계산식이 어떻게 구현되는지는 신경 쓰지 않는다. 나중에 슬루 비용 계산 방식을 바꾸고 싶으면 _slew_cost_proxy() 하나만 고치면 되고, bid()나 is_feasible()은 그대로 둬도 된다.

0개의 댓글