본문 바로가기

아키텍처와 관점

사용자 기능·코드 협력·실행 배치·요청 순서를 구별하고 품질 요구와 구조 선택을 연결합니다.

모듈의 책임을 잘 나누어도 그림만 보고는 프로그램이 어떻게 운영되는지 알 수 없습니다. ‘서비스 → 저장소’라는 그림은 호출 관계를 설명하지만 두 부분이 다른 컴퓨터에서 실행되는지는 알려 주지 않습니다. 아키텍처는 중요한 구성·관계·결정과 그 이유를 설명하며, 질문에 맞는 여러 관점이 필요합니다.

앞 장의 모듈·인터페이스와 테스트의 기본 흐름을 이해했다면 시작할 수 있습니다. 이번 목표는 같은 독서 모임 프로그램을 사용자 기능, 코드 구조, 실행 배치 관점으로 설명하고 하나의 구조 변경에서 무엇을 검증해야 하는지 정하는 것입니다.

그림이 답할 질문을 먼저 정한다

소프트웨어 아키텍처(architecture)는 시스템의 중요한 구성 요소·관계·외부에 드러나는 속성과 이를 정한 원칙을 다룹니다. 모든 함수의 모든 줄을 담는 상세 설계와 목적이 다릅니다. 무엇이 중요한지는 요구와 제약에 따라 달라집니다. 데이터 보존이 중요하면 저장 위치·실패 처리·복구가 주요 결정이 됩니다.

관점(viewpoint)은 어떤 관심사와 표현 규칙으로 볼 것인지이고, 뷰(view)는 그 관점으로 표현한 실제 설명입니다. 회원은 기능과 응답에, 개발자는 책임·의존에, 운영자는 실행 위치·장애에 관심을 가집니다. 같은 시스템이어도 한 장의 그림으로 모두 답하려 하면 선의 의미가 섞입니다.

질문이번 프로그램의 설명별도로 보아야 할 정보
무엇을 할 수 있는가회원 신청·취소, 운영자 모집 종료유스케이스·수락 조건
코드가 어떻게 협력하는가입력/표현 → 신청 서비스 → 저장소 계약모듈·공개 연산·의존 방향
어디에서 실행되는가현재는 한 Node 프로세스, 저장은 메모리프로세스·파일·배치
요청이 어떤 순서로 처리되는가load → 판정 → save → 성공 응답실행 순서·실패 지점

원자료의 여러 UML 뷰는 이 관심사를 구별하는 데 도움이 됩니다. 유스케이스 뷰는 외부 목표, 논리/설계 뷰는 내부 역할, 프로세스 뷰는 동시 실행과 상호작용, 구현/개발 뷰는 코드와 구성 단위, 배치 뷰는 실행 장치와 연결을 다룹니다. 배치의 영어 표기는 deployment이며 ‘개발’과 혼동하지 않습니다. 모든 작은 실습에 모든 종류의 그림을 강제로 만들 필요는 없습니다.

SOFTWARE ENGINEERING · CH7-1

논리 경계와 실행 위치를 구별한다

이 그림의 화살표는 기능 호출이며 세 책임은 현재 같은 Node 프로세스에서 실행됩니다.

논리 경계와 실행 위치를 구별한다입력과 표현은 신청 서비스를 호출하고 신청 서비스는 load/save 저장소 계약을 호출합니다. 별 컴퓨터나 네트워크 배치를 뜻하지 않습니다.입력 · 표현ID 전달 · 결과 안내신청 서비스규칙 확인 · 저장 성공 확인저장소 계약async load · save

입력과 표현은 신청 서비스를 호출하고 신청 서비스는 load/save 저장소 계약을 호출합니다. 별 컴퓨터나 네트워크 배치를 뜻하지 않습니다.

컴포넌트와 배치 단위를 구별한다

컴포넌트(component)는 계약을 통해 협력하는 구성 단위입니다. 이 교재에서는 신청 서비스와 저장소 구현을 분리해서 설명합니다. 프로세스(process)는 실행 중인 프로그램의 단위이며 모듈 파일 하나와 같은 뜻이 아닙니다. 파일 세 개를 같은 Node 프로세스에서 import하여 쓸 수도 있습니다.

현재 논리 경계를 네트워크 서비스로 나누면 호출 방식과 실패가 달라집니다. 원격 서비스는 연결 끊김·지연·인증·재시도 같은 문제가 추가됩니다. ‘모듈이 독립적’이라는 이유만으로 각 모듈을 별 서버로 배치하는 것은 자동 결정이 아닙니다. 필요한 품질·팀 운영 비용·데이터 일관성을 보고 선택합니다.

자주 쓰는 구조를 목적과 제약으로 읽는다

구조 방식유용한 상황생각할 비용·한계
계층(layered)입력·업무·저장 책임을 구분경계 우회·불필요한 전달 단계
클라이언트–서버여러 사용자가 공유 기능에 접근네트워크 실패·서버 운영
파이프–필터단계별 변환 결과를 다음 단계에 전달중간 데이터 형식·오류 전달
저장소 중심(repository)여러 기능이 공통 데이터를 사용공통 모델 변경·동시 수정
MVC입력 제어·화면·업무 데이터 역할 구분프레임워크별 실제 경계 차이

계층은 파일 이름 세 개를 정하는 규칙이 아니라 어떤 책임이 어떤 계약으로 접근하는지를 정합니다. 파이프–필터의 화살표는 데이터 변환 흐름이고 코드 의존 화살표와 다를 수 있습니다. 구조 이름만 붙이면 성능·유지보수·보안이 저절로 좋아진다고 말하지 않습니다.

품질 요구를 결정의 이유로 바꾼다

‘변경하기 쉬워야 한다’는 요구를 이번 범위에서는 ‘메모리 저장소를 JSON 저장소로 바꿔도 신청 허용 조건을 다시 쓰지 않는다’처럼 확인할 수 있습니다. ‘보존해야 한다’는 요구는 프로세스를 다시 시작해도 저장된 명단을 읽을 수 있는지로 연결합니다. 정확한 응답 시간이나 동시 사용자 목표를 검증하지 않고 수치를 만들어 품질을 주장하지 않습니다.

결정 기록에는 문제, 제약, 고려한 선택, 선택 이유, 남은 비용을 적습니다. 예를 들어 ‘로컬 자습에서 저장 경계를 이해하기 위해 파일 저장을 사용하지만 동시 다중 사용자 서버의 저장으로 간주하지 않는다’고 기록할 수 있습니다. 이후 요구가 달라지면 이유와 가정을 보고 결정을 다시 검토합니다.

구조를 작은 프로그램으로 실행한다

아래 저장소는 load/save 비동기 계약을 제공합니다. async 함수는 Promise를 반환하고, await는 그 작업이 성공하거나 실패할 때까지 다음 처리를 이어가지 않습니다. 파일 구현과 같은 형태를 쓰기 위해 메모리 구현도 async로 정의합니다.

저장소 최상위 폴더에서 다음 명령을 실행합니다. Node.js가 설치되어 있어야 하며 패키지를 추가로 설치하지 않습니다. 두 번째 인수를 생략하면 이 장의 예제를 모두 실행합니다.

node dev-note/scripts/software-engineering-lab/examples/ch7.mjs ch7-1
ch7-1 실행 예제
function createMemoryStore(seed) {
  let state = structuredClone(seed);
  return {
    async load() { return structuredClone(state); },
    async save(next) { state = structuredClone(next); },
  };
}
function createRegistrationService(store) {
  return {
    async register(memberId) {
      const session = await store.load();
      let code = "REGISTERED";
      if (typeof memberId !== "string" || !/^[a-z][a-z0-9-]{0,23}$/.test(memberId)) code = "INVALID_MEMBER";
      else if (session.status !== "OPEN") code = "CLOSED";
      else if (session.participants.includes(memberId)) code = "ALREADY_REGISTERED";
      else if (session.participants.length >= session.capacity) code = "FULL";
      if (code !== "REGISTERED") return { ok: false, code, state: session };
      const next = { ...session, participants: [...session.participants, memberId] };
      await store.save(next);
      return { ok: true, code, state: next };
    },
  };
}
const store = createMemoryStore({
  id: "book-07", title: "구조를 읽다",
  capacity: 1, status: "OPEN", participants: [],
});
const service = createRegistrationService(store);
console.log((await service.register("member-a")).code);
console.log((await service.register("member-b")).code);
console.log("저장된 명단:", (await store.load()).participants.join(","));

출력은 REGISTERED, FULL, 저장된 명단: member-a입니다. 서비스는 저장소 내부의 state에 직접 접근하지 않습니다. 입력과 출력 역할을 하는 마지막 호출부만 콘솔을 사용합니다. 서비스는 판정·새 상태 생성·저장 성공 확인을 연결합니다.

structuredClone은 이 예제의 단순 데이터 객체와 배열을 복사합니다. 저장소가 반환한 값을 호출자가 바꿔도 저장된 값은 직접 바뀌지 않도록 한 계약입니다. 임의 함수·클래스·외부 자원을 모두 같은 방법으로 복제할 수 있다는 뜻은 아닙니다. 이 모델의 데이터 범위에 한정합니다.

저장 실패는 await store.save(next)에서 예외가 되어 성공 결과를 반환하지 않습니다. 화면/입력 계층은 그 실패를 안내해야 합니다. 메모리 구현이라도 실패 저장소를 대신 넣어 이 경로를 검증할 수 있습니다. 실제 디스크 장애 전체를 검증한 것과는 구분합니다.

await는 이 호출의 다음 단계만 기다린다

Promise는 나중에 성공 값이나 실패를 받는 작업의 결과입니다. async 함수는 Promise를 반환하고 await는 자신이 쓰인 함수의 다음 줄 실행을 잠시 멈춥니다. 다른 요청·타이머·호출 전체를 잠그는 명령은 아닙니다. await store.save(next)가 실패하면 그 뒤 성공 반환으로 넘어가지 않고 예외가 호출자에게 전파됩니다.

코드/시점min 호출hana 호출
await service.register("min") 뒤 다음 호출읽기→판정→저장→성공까지 완료아직 시작하지 않음
두 호출을 먼저 만들고 Promise.allload를 기다릴 수 있음같은 시간에 load를 시작할 수 있음
min의 await가 완료min 다음 줄 재개hana는 자신의 진행 위치에서 실행

앞 예제의 두 줄은 첫 호출 완료 뒤 둘째 호출을 시작하므로 순차입니다. 다음처럼 호출부터 둘 다 만들면 교차가 가능합니다.

const first = service.register("min");
const second = service.register("hana");
const results = await Promise.all([first, second]);

이 코드는 ‘두 성공을 보장’하는 코드가 아닙니다. 저장 계약이 동시 읽기/쓰기에서 무엇을 보장하는지가 결과를 결정합니다. structuredClone은 별칭에 의한 직접 변경을 막는 복사이고 잠금이나 버전 충돌 검사는 아닙니다. 동시 저장과 복구 계약에서 같은 시간축을 실제 최소 모델로 확인합니다.

반례: 논리 계층을 운영 보장으로 읽는다

서비스와 저장소를 나눴다는 사실만으로 마지막 자리의 동시 신청 문제가 해결되지는 않습니다. 두 요청이 같은 상태를 load한 후 둘 다 통과해 save하면 한 명의 저장을 덮어쓸 수 있습니다. 이 예제는 순차 호출이며 단일 요청 흐름을 확인합니다. 아키텍처 그림의 경계와 실제 일관성 보장은 별도 검증이 필요합니다.

독립 문제: 사진 변환 프로그램의 세 관점

사진을 읽어 작은 미리보기와 원본 보관 파일을 만드는 프로그램이 있습니다. 처음에는 한 컴퓨터에서 실행하지만 나중에 여러 작업자가 같은 폴더를 처리할 수 있습니다.

사용자 기능·코드 구조·실행 배치 관점의 설명을 각각 두 문장으로 작성합니다. 변환 단계를 파이프–필터로 표현할지 검토하고, 작업자 수를 늘릴 때 새로 확인해야 할 실패 두 개를 적습니다.

힌트

함수의 호출 순서와 컴퓨터의 수는 다릅니다. 같은 원본에 두 작업자가 접근하거나 중간 파일에서 장애가 날 수 있습니다.

풀이

사용자 기능은 ‘사진을 지정하면 미리보기와 보관 결과를 얻는다’와 실패 원인을 알려 주는 응답입니다. 코드 구조는 읽기·변환·저장 계약과 입력/표현의 책임이고, 실행 배치는 현재 한 프로세스와 로컬 파일입니다.

읽기 결과가 변환 입력이고 변환 결과가 저장 입력이므로 파이프–필터 관점이 단계 추적에 유용합니다. 여러 작업자에서는 같은 파일의 중복 처리·동시 덮어쓰기와 중간 결과만 남는 장애를 확인해야 합니다. 컴포넌트 그림만 여러 개 복사했다고 작업 분배·실패 복구가 생기지는 않습니다.

다음 결정으로 연결

아키텍처 설명은 그림·계약·이유·검증이 함께 있을 때 읽는 사람에게 도움이 됩니다. 다음 본문은 반복되는 변경 문제를 디자인 패턴으로 설명하되, 패턴 이름을 먼저 고르는 대신 필요한 변화 지점을 찾습니다.


원자료와 더 읽기

장원사이버평생교육원 「소프트웨어공학」 PDF 물리 128–148쪽의 주제를 바탕으로 설명·도표·실습을 새로 구성했습니다.

모델 표현의 표준은 OMG UML 2.5.1, 실행 언어의 경계는 MDN JavaScript 모듈을 참고합니다.

아키텍처를 나누어도 회원 ID 형식은 공통 core와 동일합니다. 대문자·숫자 시작·공백·밑줄·25자 ID를 거절하는 검증을 포함하고, 저장 경계의 변화로 업무 입력 규칙을 바꾸지 않습니다.