임시 저장소를 만들어 쓰려고 합니다. 길이가 8인 스택과 레지스터 A, B가 있습니다. 산술 연산, 스택 조작 등의 명령어에 의해 스택 및 레지스터가 조작되는 스택 계산기를 구현하는 문제입니다.
+ 레지스터는 CPU 같은 프로세서에서 데이터 처리를 위한 임시 저장공간이 있는 장치입니다.
이것만 보더라도 구현이 될 수 있게끔 전체 글을 읽고 정리해보겠습니다. 요구사항을 파악하며 입출력 값도 자연스레 정의해보겠습니다.
레지스터와 통신하며 데이터를 주고 받는 데이터 저장 장치입니다. 정수형으로 값을 받습니다. 요구 사항은 아래와 같습니다.
"EMPTY"
를 출력."OVERFLOW"
를 출력에 추가.스택에서 받은 값을 저장하고, 조작 및 계산하는 장치입니다. 정수형으로 값을 받습니다.
"ERROR"
를 출력.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;
}
}
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;
}
if
- else if
- else
로 조건문을 연결해 해당 명령어 뿐만 아니라 다른 input
에도(UNKNOWN
) 대응.POP
이나 PUSH
같은 경우 마지막 문자열에 의해 조건이 나뉘어져 조건 묶음.ADD
와 SUB
는 +, -만 차이가 있고 나머진 구조가 같아 묶음.이전에 세웠던 테스트 케이스 원칙에 따라 테스트 케이스를 작성해보겠습니다.
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"]
/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']
아주 잘 나옵니다!
아주 잘 나옵니다!
만들고 코드를 검토해보니 에러 처리 하나를 빼먹어서 디버깅? 리팩토링?을 해보려고 합니다.
제목이 곧 내용입니다. 명령어가 100개를 넘어가지 않게 실행돼야하는데 그러지 못하고 있습니다. 바로 수정에 들어갔습니다.
function stackCalculator(commands) {
if (commands.length > 100)
return '명령어의 총 개수가 100개가 넘어 갑니다! 100개 이하로 입력해주세요.';
...
}
이런 식으로 했다. 에러 메세지와 이후 지시문을 넣어 UX도 향상되게끔 작성했다.
명령어의 개수가 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개 이상의 조건을 중첩할 수도 있구나하고 배웠습니다.
저는 원래 if - else if - else 순으로 해서 명령문 처리 및 예외 처리까지 했습니다. 그런데 동료 분들의 코드와 수료생님의 코드를 보고 switch를 많이들 사용하셔서 '이런 방식의 구조라면 switch를 쓰는 것이 더 효율적이겠구나' 했습니다.
곧이어 리팩토링도 해보고 논리 연산자가 아닌 case를 중첩해 조건을 세운다는 것도 알았습니다. 다음에는 이런 구조라면 switch를 이용해보는 등의 더 가독성이 좋고 효율적인 방법이 뭔지 고민을 항상 해야겠다 생각했습니다.
디버깅과 리팩토링 과정에서 switch 문으로 전환해봤습니다.
원래 if-else 문을 사용해 명령어를 처리했지만, 동료와 수료생들의 코드를 보고 switch 문을 사용해 리팩토링했습니다.
switch 문을 사용해서 코드의 가독성이 올라가고, 특정 명령어에 대한 처리를 더 명확하게 할 수 있었습니다. 특히 비슷한 구조로 많은 조건 = 명령어를 처리해야 하는 경우, switch 문이 더 간결하고 이해하기 쉬운 코드를 작성하는 데 도움이 됐다는 걸 알게 됐습니다.
요구사항 및 의사코드를 그래도 타겟팅을 해서 조금 순조롭게 작성했던 거 같습니다. 또한 재사용을 많이 하기 위해 클래스 선정할 때, 조금 더 신경을 써서 만들어서 클래스 장점을 최대한 활용했던 것 같습니다. 저번에 작성한 테스트 케이스 원칙을 보고 작성해 코드의 신뢰성을 조금 높였고, 동료 분들과 수료생님의 코드를 참고해 switch
문으로 리팩토링함으로써 가독성과 유지보수성을 향상했습니다.
다만 초기 설계에서 더 효율적인 구조를 선택하지 못한 점과 요구사항 분석글과 의사코드를 보고 개발 해도 의도대로 되지 않아 아쉬웠습니다. 앞으로는 초기 설계 단계에서 다양한 구조를 검토하고, 의사코드를 조금 더 쉽게, 중요한 요구사항을 잘 입력하도록 해야겠다고 생각했습니다.