임시 저장소 계산기

윤뿔소·2024년 7월 2일
0

Agorithm 문제풀이

목록 보기
5/7
post-thumbnail

임시 저장소 계산기

임시 저장소를 만들어 쓰려고 합니다. 길이가 8인 스택과 레지스터 A, B가 있습니다. 산술 연산, 스택 조작 등의 명령어에 의해 스택 및 레지스터가 조작되는 스택 계산기를 구현하는 문제입니다.

+ 레지스터는 CPU 같은 프로세서에서 데이터 처리를 위한 임시 저장공간이 있는 장치입니다.

image

상세 요구사항 파악

이것만 보더라도 구현이 될 수 있게끔 전체 글을 읽고 정리해보겠습니다. 요구사항을 파악하며 입출력 값도 자연스레 정의해보겠습니다.

스택

레지스터와 통신하며 데이터를 주고 받는 데이터 저장 장치입니다. 정수형으로 값을 받습니다. 요구 사항은 아래와 같습니다.

  • 최대 8칸.
  • 스택에서 값을 레지스터로 옮기거나 출력 시 값이 없으면 "EMPTY"를 출력.
  • 스택에 값을 추가할 때, 8칸이 차있다면 "OVERFLOW"를 출력에 추가.

레지스터

스택에서 받은 값을 저장하고, 조작 및 계산하는 장치입니다. 정수형으로 값을 받습니다.

  • 초기 상태일 때는 값이 없음.
  • A, B 2칸. 각 1개의 데이터만 받을 수 있음.
  • 레지스터의 값을 사용하는 명령어를 사용할 때, 레지스터 어느 하나라도 값이 없다면 "ERROR"를 출력.

명령어 입력 조건

  • 명령어는 각각의 개별 함수로 만들어야함.
  • 명령어는 최대 100개임.
  • 입력된 명령어는 문자열로 이루어진 배열이며, 입력한 명령 중에 처리할 수 없는 명령의 경우는 "UNKNOWN"을 출력.
  • 입력된 명령어가 끝나면 return함.

명령어 처리 (출력)

  • POPA : 스택에서 값 하나를 꺼내서 A 레지스터로 복사.
  • POPB : 스택에서 값 하나를 꺼내서 B 레지스터로 복사.
  • ADD : A와 B 레지스터 값을 더해서 스택에 PUSH.
  • SUB : A 레지스터 값에서 B 레지스터 값을 빼서 스택에 PUSH.
  • PUSH0 : 스택에 0 값을 PUSH.
  • PUSH1 : 스택에 1 값을 PUSH.
  • PUSH2 : 스택에 2 값을 PUSH.
  • SWAP : A 레지스터 값과 B 레지스터 값을 맞교환.
  • PRINT : 스택 마지막 값을 꺼내서 출력. 이 때 스택은 빼낸 값을 없애 하나가 줄어듦.

+ "EMPTY", "OVERFLOW", "ERROR" 출력 조건 추가

출력 조건이 해당돼 출력이 필요하다면 return의 배열에 추가됩니다.

  • "EMPTY" : POPA, POPB, PRINT 실행 시 스택에 값이 없다면 출력 추가.
  • "OVERFLOW" : ADD, SUB, PUSH0, PUSH1, PUSH2 실행 시 스택 길이가 8이라면 출력 추가.
  • "ERROR" : ADD, SUB, SWAP 실행 시 레지스터 안에 실행할 값 = 초깃값이라면 출력 추가.

그 외에도 PUSH${N}, POPN을 구현할 때, 메소드를 사용하는 것이 아닌 개별 함수/메소드로 작성해서 만들어야 합니다.

의사코드 작성

요구사항을 보고 분석해 작성해보겠습니다.

스택 배열 초기화
레지스터 A null 초기화
레지스터 B null 초기화
결과값 배열 초기화

반복문: 입력값 분석

  IF ( 커맨드 = POP${N} )
    IF ( 스택 길이 = 0 )
      결과값 배열에 "EMPTY" 추가
      다음 커맨드로 이동
    ${N} 레지스터에 스택 값 복사
    복사한 값 하나 제거

  IF ( 커맨드 = ADD )
    IF ( 스택 길이 = 8 )
      결과값 배열에 "OVERFLOW" 추가
      다음 커맨드로 이동
    IF ( 레지스터 A = null || 레지스터 B = null )
      결과값 배열에 "ERROR" 추가
      다음 커맨드로 이동
    A 레지스터 + B 레지스터 값을 스택에 push

  IF ( 커맨드 = SUB )
    IF ( 스택 길이 = 8 )
      결과값 배열에 "OVERFLOW" 추가
      다음 커맨드로 이동
    IF ( 레지스터 A = null || 레지스터 B = null )
      결과값 배열에 "ERROR" 추가
      다음 커맨드로 이동
    A 레지스터 - B 레지스터 값을 스택에 push

  IF ( 커맨드 = PUSH${N} )
    IF ( 스택 길이 = 8 )
      결과값 배열에 "OVERFLOW" 추가
      다음 커맨드로 이동
    ${N} 값을 스택에 push

  IF ( 커맨드 = SWAP )
    IF ( 레지스터 A = null || 레지스터 B = null )
      결과값 배열에 "ERROR" 추가
      다음 커맨드로 이동
    임시 변수 : 레지스터 A 값을 따로 초기화
    레지스터 A = 레지스터 B
    레지스터 B = 임시 변수

  IF ( 커맨드 = PRINT )
    IF ( 스택 길이 = 0 )
      결과값 배열에 "EMPTY" 추가
      다음 커맨드로 이동
    스택에서 꺼낸 값을 결과값에 추가
    결과값에 추가된 값 제거

  IF (정의되지 않는 명령어?)
    결과값에 'UNKNOWN' 추가

조건문을 잘 써줘야하는 구조를 가진 반복문입니다. 이제 구현 단계로 넘어가겠습니다.

구현

우선 어떻게 구현할 것이냐는 구체적인 형태를 먼저 정해보겠습니다. 바로 클래스형 - 객체지향으로 개발하겠습니다. 왜 이렇게 개발할지에 대해서 설명하겠습니다.

스택과 레지스터를 클래스 내부에서 상태로 선언해 독립적으로 관리하기 쉬울 것이고, 각 요구사항에 맞게 명령어를 구현하는 데에 되게 많고 복잡해질 수도 있기 때문에 캡슐화를 통해 재사용성과 확장성을 올리려고 합니다. 그래서 클래스형으로 개발해보겠습니다.

스택, 레지스터 클래스 구현

명령어를 받아줄 상태와 메소드를 구현해보겠습니다. 메소드 부분은 재사용이 많이 되게끔, 사칙연산 느낌으로 구현했습니다.

// 스택 클래스 : stack 변수
class Stack {
  constructor() {
    this.stackArr = [];
  }

  // stack을 직접 사용할 메소드 push, pop 정의
  push(value) {
    if (this.stackArr.length >= 8) {
      return 'OVERFLOW';
    }
    this.stackArr[this.stackArr.length] = value;
    return null;
  }
  pop() {
    if (this.stackArr.length === 0) {
      return 'EMPTY';
    }
    const popValue = this.stackArr[this.stackArr.length - 1];
    this.stackArr.length -= 1;
    return popValue;
  }
}

// 레지스터 클래스 : register 변수
class Register {
  constructor() {
    this.register = null;
  }

  // stack을 직접 사용할 메소드 get, set 정의
  get() {
    if (this.register === null) {
      return 'ERROR';
    }
    return this.register;
  }
  set(value) {
    this.register = value;
  }
}
  1. 스택과 레지스터의 데이터를 담는 상태-변수를 생성자로 생성.
  2. 상태를 조작할 메소드 구현.
    • 스택 : push, pop을 선언. 각각의 에러 처리도 포함.
    • 레지스터 : get, set을 선언. get에 해당하는 에러 처리 구현.

명령어 처리 함수 구현

명령어를 처리하는 함수이자 스택 계산기인 stackCalculator를 작성해보겠습니다. 각 명령어에 따른 동작을 의사코드를 반영해 조건문을 통해 구현합니다.

function stackCalculator(commands) {
  const stack = new Stack();
  const registerA = new Register();
  const registerB = new Register();
  const result = [];

  commands.forEach((command) => {
    // POPA, POPB
    if (command.startsWith('POP')) {
      if (command.slice(-1) === 'A') {
        const popValue = stack.pop();
        popValue === 'EMPTY' ? result.push(popValue) : registerA.set(popValue);
      }
      if (command.slice(-1) === 'B') {
        const popValue = stack.pop();
        popValue === 'EMPTY' ? result.push(popValue) : registerB.set(popValue);
      }
    }

    // ADD, SUB
    else if (command === 'ADD' || command === 'SUB') {
      if (stack.stackArr.length >= 8) {
        result.push('OVERFLOW');
      }
      if (stack.stackArr.length < 8) {
        const valueA = registerA.get();
        const valueB = registerB.get();
        if (valueA === 'ERROR' || valueB === 'ERROR') {
          result.push('ERROR');
        } else {
          // ADD 일때
          command === 'ADD'
            ? stack.push(String(Number(valueA) + Number(valueB)))
            : // SUB 일때
              stack.push(String(Number(valueA) - Number(valueB)));
        }
      }
    }

    // PUSH0, PUSH1, PUSH2
    else if (
      command.startsWith('PUSH') &&
      command.slice(-1) >= 0 &&
      command.slice(-1) <= 2
    ) {
      const pushValue = stack.push(command.slice(-1));
      if (pushValue === 'OVERFLOW') {
        result.push(pushValue);
      }
    }

    // SWAP
    else if (command === 'SWAP') {
      const valueA = registerA.get();
      const valueB = registerB.get();
      if (valueA === 'ERROR' || valueB === 'ERROR') {
        result.push('ERROR');
      }
      if (!(valueA === 'ERROR' || valueB === 'ERROR')) {
        const temp = valueA;
        registerA.set(valueB);
        registerB.set(temp);
      }
    }

    // PRINT
    else if (command === 'PRINT') {
      const popValue = stack.pop();
      result.push(popValue);
    }

    // 정의되지 않는 명령어
    else {
      result.push('UNKNOWN');
    }
  });

  // 결과값 반환
  return result;
}
  1. 만든 클래스로 인스턴스를 만들어 변수를 초기화.
  2. if - else if - else로 조건문을 연결해 해당 명령어 뿐만 아니라 다른 input에도(UNKNOWN) 대응.
  3. 성격이 비슷하거나 반복되는 명령어는 묶어서 처리.
    • POP이나 PUSH 같은 경우 마지막 문자열에 의해 조건이 나뉘어져 조건 묶음.
    • ADDSUB는 +, -만 차이가 있고 나머진 구조가 같아 묶음.
  4. 반복이 끝나고 결과값 반환

테스트 케이스

이전에 세웠던 테스트 케이스 원칙에 따라 테스트 케이스를 작성해보겠습니다.

  • 경계값 입력 : 최소/최대 길이 입력
  • 같은 조건 및 다른 세부 정보, 수열, 난수 반복 등
  • 결과를 예측한 테스트 케이스
  • 그 외 특정한 상황 정의 케이스
console.log('1.', stackCalculator(['PRINT', 'PUSH0', 'PRINT', 'POPA']));
// 예상 결과: ["EMPTY", "0", "EMPTY"]
console.log(
  '2.',
  stackCalculator([
    'PUSH1',
    'PUSH1',
    'PUSH2',
    'POPA',
    'POPB',
    'SWAP',
    'ADD',
    'PRINT',
    'PRINT',
  ]),
);
// 예상 결과: ["3", "1"]
console.log(
  '3.',
  stackCalculator([
    'PUSH2',
    'PUSH2',
    'PUSH1',
    'POPA',
    'POPB',
    'SWAP',
    'SUB',
    'POPA',
    'POPB',
    'ADD',
    'PRINT',
  ]),
);
// 예상 결과: ["3"]
console.log(
  '4.',
  stackCalculator([
    'ADD',
    'PUSH2',
    'PUSH1',
    'PUSH0',
    'PUSH2',
    'PUSH1',
    'PUSH2',
    'PUSH2',
    'PUSH0',
    'PUSH2',
    'PUSH3',
  ]),
);
// 예상 결과: ["ERROR", "OVERFLOW", "UNKNOWN"]
console.log('5.', stackCalculator(['PUSH0']));
// 예상 결과: []
const maxLengthCommands = Array(100).fill('PUSH1');
console.log('6.', stackCalculator(maxLengthCommands));
// 예상 결과: 8번째 PUSH부터 "OVERFLOW"
console.log(
  '7.',
  stackCalculator(['PUSH1', 'PUSH2', 'PUSH1', 'POPB', 'POPA', 'ADD', 'PRINT']),
);
// 예상 결과: ["3"]
console.log('8.', stackCalculator(['PUSH1', 'POPA', 'POPB', 'ADD']));
// 예상 결과: ["EMPTY", "ERROR"]
console.log('9.', stackCalculator(['ADD', 'PUSH1', 'POPA', 'ADD']));
// 예상 결과: ["ERROR", "ERROR"]

결과

image
/Users/taeyeon/.nvm/versions/node/v20.11.0/bin/node ./stack-calculator.js
1. (3) ['EMPTY', '0', 'EMPTY']
2. (2) ['3', '1']
3. (1) ['3']
4. (3) ['ERROR', 'OVERFLOW', 'UNKNOWN']
5. (0) []
6. (92) ['OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW', 'OVERFLOW']
7. (1) ['3']
8. (2) ['EMPTY', 'ERROR']
9. (2) ['ERROR', 'ERROR']

아주 잘 나옵니다!

아주 잘 나옵니다!

디버깅 / 리팩토링

만들고 코드를 검토해보니 에러 처리 하나를 빼먹어서 디버깅? 리팩토링?을 해보려고 합니다.

문제: input 배열 개수가 100개가 넘어가도 실행된다.

제목이 곧 내용입니다. 명령어가 100개를 넘어가지 않게 실행돼야하는데 그러지 못하고 있습니다. 바로 수정에 들어갔습니다.

function stackCalculator(commands) {
  if (commands.length > 100)
    return '명령어의 총 개수가 100개가 넘어 갑니다! 100개 이하로 입력해주세요.';
  ...
}

이런 식으로 했다. 에러 메세지와 이후 지시문을 넣어 UX도 향상되게끔 작성했다.

image

명령어의 개수가 101개로 넘어가지마자 에러메시지가 잘 나온다. 대응 끝!

리팩토링: switch로 바꾸기

다른 분들의 코드를 참고해보니 switch도 많이 썼고, 또 되게 가독성이 좋아보여서 리팩토링 해봤습니다.

function stackCalculator(commands) {
  if (commands.length > 100)
    return '명령어의 총 개수가 100개가 넘어 갑니다! 100개 이하로 입력해주세요.';

  const stack = new Stack();
  const registerA = new Register();
  const registerB = new Register();
  const result = [];

  commands.forEach((command) => {
    switch (true) {
      case command.startsWith('POP'):
        if (command.slice(-1) === 'A') {
          const popValue = stack.pop();
          popValue === 'EMPTY'
            ? result.push(popValue)
            : registerA.set(popValue);
        }
        if (command.slice(-1) === 'B') {
          const popValue = stack.pop();
          popValue === 'EMPTY'
            ? result.push(popValue)
            : registerB.set(popValue);
        }
        break;

      case command === 'ADD':
      case command === 'SUB':
        if (stack.stackArr.length >= 8) {
          result.push('OVERFLOW');
        } else {
          const valueA = registerA.get();
          const valueB = registerB.get();
          if (valueA === 'ERROR' || valueB === 'ERROR') {
            result.push('ERROR');
          } else {
            command === 'ADD'
              ? stack.push(String(Number(valueA) + Number(valueB)))
              : stack.push(String(Number(valueA) - Number(valueB)));
          }
        }
        break;

      case command.startsWith('PUSH') && !isNaN(command.slice(-1)):
        const pushValue = stack.push(command.slice(-1));
        if (pushValue === 'OVERFLOW') {
          result.push(pushValue);
        }
        break;

      case command === 'SWAP':
        const valueA = registerA.get();
        const valueB = registerB.get();
        if (valueA === 'ERROR' || valueB === 'ERROR') {
          result.push('ERROR');
        } else {
          const temp = valueA;
          registerA.set(valueB);
          registerB.set(temp);
        }
        break;

      case command === 'PRINT':
        const popValue = stack.pop();
        result.push(popValue);
        break;

      default:
        result.push('UNKNOWN');
    }
  });

  // 결과값 반환
  return result;
}

이렇게 switch를 써보니 if문 중첩이 됐고, 같은 레벨의 조건이 반복된다면 switch도 괜찮은 생각이라고 생각했습니다.

새로 배운 건 case를 연달아 써서 2개 이상의 조건을 중첩할 수도 있구나하고 배웠습니다.

결론

해결한점

  1. 요구사항 분석 : 스택 계산기의 전체 요구사항 분석 및 기능, 명령어와 그에 따른 동작 정의
  2. 클래스 설계 및 구현 : Stack, Register 클래스 및 push, pop, get, set 메소드 구현
  3. 명령어 처리 함수 구현 : stackCalculator, 각 명령어, 에러 처리 등 구현
  4. 테스트 케이스 작성 및 실행 : 경계값 등의 다양한 시나리오를 포함한 테스트 케이스 작성.

배운 점

  1. 클래스 + 객체 지향 프로그래밍 : 특정 구조는 클래스 설계와 객체 지향 프로그래밍의 간편하다는 것을 실감함. 상태, 메소드 등을 캡슐화해 잘 정의하면 재사용, 가독성 등이 증가한다는 것을 앎

신경 쓴 부분

  • 어제 배운 설계 과정을 다시금 복습하는 과정으로 Actor 등의 이해 관계자와 동작을 구분. 요구사항 및 의사코드에 적용.
  • 코드가 복잡해지지 않도록 줄바꿈 마저 신경 씀. 반복되는 로직을 함수로 분리해 재사용성 높임.

비교 코드 보고 느낀점

저는 원래 if - else if - else 순으로 해서 명령문 처리 및 예외 처리까지 했습니다. 그런데 동료 분들의 코드와 수료생님의 코드를 보고 switch를 많이들 사용하셔서 '이런 방식의 구조라면 switch를 쓰는 것이 더 효율적이겠구나' 했습니다.
곧이어 리팩토링도 해보고 논리 연산자가 아닌 case를 중첩해 조건을 세운다는 것도 알았습니다. 다음에는 이런 구조라면 switch를 이용해보는 등의 더 가독성이 좋고 효율적인 방법이 뭔지 고민을 항상 해야겠다 생각했습니다.

리팩토링 후

디버깅과 리팩토링 과정에서 switch 문으로 전환해봤습니다.
원래 if-else 문을 사용해 명령어를 처리했지만, 동료와 수료생들의 코드를 보고 switch 문을 사용해 리팩토링했습니다.
switch 문을 사용해서 코드의 가독성이 올라가고, 특정 명령어에 대한 처리를 더 명확하게 할 수 있었습니다. 특히 비슷한 구조로 많은 조건 = 명령어를 처리해야 하는 경우, switch 문이 더 간결하고 이해하기 쉬운 코드를 작성하는 데 도움이 됐다는 걸 알게 됐습니다.

데일리회고

요구사항 및 의사코드를 그래도 타겟팅을 해서 조금 순조롭게 작성했던 거 같습니다. 또한 재사용을 많이 하기 위해 클래스 선정할 때, 조금 더 신경을 써서 만들어서 클래스 장점을 최대한 활용했던 것 같습니다. 저번에 작성한 테스트 케이스 원칙을 보고 작성해 코드의 신뢰성을 조금 높였고, 동료 분들과 수료생님의 코드를 참고해 switch 문으로 리팩토링함으로써 가독성과 유지보수성을 향상했습니다.
다만 초기 설계에서 더 효율적인 구조를 선택하지 못한 점과 요구사항 분석글과 의사코드를 보고 개발 해도 의도대로 되지 않아 아쉬웠습니다. 앞으로는 초기 설계 단계에서 다양한 구조를 검토하고, 의사코드를 조금 더 쉽게, 중요한 요구사항을 잘 입력하도록 해야겠다고 생각했습니다.

profile
코뿔소처럼 저돌적으로

0개의 댓글