객체란 무엇인가?

Nochi·2023년 3월 1일
2

20'm

목록 보기
1/1

 나는 '여우와 두루미' 예시를 참 좋아한다. 너무 오래되서 내용의 전체 줄거리는 기억이 안나지만 여우와 두루미가 자기만의 표현 방식으로 상대를 자신의 퍼즐에 끼워 맞추려고 했을 때, 서로 기분만 안좋아진다는 그런 내용 말이다.

 이번에 회사에서 객체 지향에 대해서 설명하는 내 자신이 그러했다. 20분만에 담기 어려운 내용을 끼워 맞추려다 보니 더 어려웠다. 뭔가 블로그를 쓰는 이유도 이 스피치를 하는 이유도 상대방이 이해가 잘 되었으면 하는 바람이 있고, 무엇보다 소프트웨어 장인 정신에

"Not only individuals and interactions, but also a community of professionals"
(개별적으로 협력하는 것 뿐만 아니라, 프로페셔널 커뮤니티를 조성하는 것을)

내용에 의거하여 쉽게 설명하여 수준을 높이는 일에 일조하고 싶었기 때문이다. 주관적이지만 쉽게 풀어 쓰고자 하지만 더 나은 표현이 있다면 도움을 주길 바란다.

아 그리고 나는 프론트엔드 개발자여서 Typescript, React 기준으로 설명을 하니 참고 바란다.


서론.

 객체에 대해서 자신만의 주관이 있어야 한다.(객체는 @@다. 하는 주관.) 객체지향 프로그래밍을 처음 배웠을 때, 마치 현실 세계를 모방한 것과 같이 자신만의 주관이 필요하다. 그렇지 않으면 사실 객체란 무엇인가에 대해 의미론적으로만 배워가는 시간이 될 것이다.

  나는 객체지향의 사실과 오해 : 역할, 책임, 협력 관점에서 본 객체지향에서 말하는 객체에 대해 설명하고자 한다. 여기서 가장 훌륭한 객체는 자율적인 객체 라는 표현이 있다. 자율적인 객체를 이해하기 전에 객체란 무엇인지에 대해 알아보자.

객체

  흔히 객체는 상태(state)행동(behavior)을 함께 지닌 실체라고 정의한다. 현실 세계의 객체와 차이점이 있다면 객체는 스스로 상태를 관리한다는 것이다.

예를 들면, 물이 담긴 '물컵'이라는 객체에 물이 줄어들려면 '사람'에 의해서 '물을 마시거나', '물을 버리거나' 할 것이다. 이렇게 보면 현실 세계에선 사람이 물을 줄어들게 하지만, 객체 지향 세계에선 물컵이 물을 스스로 줄어들게 한다.

객체 세계로 들어가보자. 여기 간단하게 컵이 있다. 이 컵은 안에 내용 내용물이 얼마나 남아있는지만 확인하는 객체이다. 아래 코드를 보면서 객체의 상태와 행동이 무엇인지 알아보자.

class 물컵 {
  물양: number;
	constructor(담겨_있던_물양:number){
      this.물양 = 담겨_있던_물양;	
 	}
  
  	// 물 얼마나 남았지?
	  getAmount() {
      return this.물양; 
    }
  
  	// 물 줄이기
  	lossAmount(뺄_물양: number): void {
      this.물양 = this.물양 - 뺄_물양;
    }
  
  	// 물 늘리기
  	addAmount(더할_물양: number): void {
      this.물양 = this.물양 + 더할_물양;
    }
}

여기서 상태는 this.물양이며, 물컵에 담겨 있는 물의 양이다. 여기서 행동은 무엇일까? getAmount, lossAmount, addAmount이다. 우리는 이렇게 상태와 행동에 대해서 알아보았는데 객체는 이 상태와 행동을 함께 지닌 실체이다. 우린 저 코드 보면서 해당 행동들이 어떤 역할을 하는지 알고 있을 것이다. 자 이제 저 행동을 수행할 사람을 만들어보자.

class 사람 {
  이름: string;
	constructor(이름:string){
      this.이름 = 이름;	
 	} 
  
  	키미노_나마에와() {
      return console.log('나의 이름은 ', this.이름);
    }
  
  	물_젠부_쏟아버리기(cup: 물컵): void {
    	cup.lossAmount(cup.getAmount());
  	}

	물_마시기(cup: 물컵, amount: number): void {
    	cup.lossAmount(amount);
  	}
    
    물_채우기(cup: 물컵, amount: number): void {
      	cup.addAmount(amount);
    }
}

여기서 상태는 이름 밖에 없는 사람 클래스가 있다. 그러나 객체의 행동은 물컵과 연관이 되어있다. 이제 우리는 객체가 어떻게 연결되어 행동하는지 알아보자. 복사해서 확인하기 쉽도록 전체 코드를 넣었다.

class 물컵 {
  물양: number;
	constructor(담겨_있던_물양:number){
      this.물양 = 담겨_있던_물양;	
 	}
  
  	// 물 얼마나 남았지?
	  getAmount() {
      return this.물양; 
    }
  
  	// 물 줄이기
  	lossAmount(뺄_물양: number): void {
      this.물양 = this.물양 - 뺄_물양;
    }
  
  	// 물 늘리기
  	addAmount(더할_물양: number): void {
      this.물양 = this.물양 + 더할_물양;
    }
}

class 사람 {
  이름: string;
	constructor(이름:string){
      this.이름 = 이름;	
 	} 
  
  	키미노_나마에와() {
      return console.log('나의 이름은 ', this.이름);
    }
  
  	물_젠부_쏟아버리기(cup: 물컵): void {
    	cup.lossAmount(cup.getAmount());
  	}

	물_마시기(cup: 물컵, amount: number): void {
    	cup.lossAmount(amount);
  	}
    
    물_채우기(cup: 물컵, amount: number): void {
      	cup.addAmount(amount);
    }
}

// 스토리 텔링
console.log("물컵이라는 객체를 만들었다. 이 안에는 500ml의 물이 있다. (꽤 많이 들어가네..?)")
const myCup = new 물컵(500);

console.log("'노치'는 물을 엎지를거다 그러기 위해선 '노치'를 만들어야지.")
const nochi = new 사람("Nochi");

// 키미노_나마에와
nochi.키미노_나마에와()
console.log("물의 양: ", myCup.getAmount())

console.log("'노치'는 물을 전부 엎질러버린다.")
nochi.물_젠부_쏟아버리기(myCup)
console.log("물의 양: ", myCup.getAmount())

console.log("'노치'는 물은 알아서 마를테니 다시 물을 채운다.")
nochi.물_채우기(myCup, 300)
console.log("물의 양: ", myCup.getAmount())

 이렇게 보면 사실 현실 세계와 뭐가 다른지 모를 것이다. 내가 보기엔 물컵을 엎지른 주체도, 물을 채운 주체도 사람이란 객체가 했기 때문이다. 그러나 여긴 객체 지향의 세계이다. 이해가 되지 않는다면, 이 모든 것이 우리는 무의식적으로 현실 세계를 투영시켰기 때문에 물을 넣은 것은 '노치'가 했을 것이라고 생각한다. 객체 지향의 세계에서 저 물컵이라는 객체가 스스로 추가한다. 바로 addAmount 메서드로 말이다.

다르게 설명하면 노치는 물컵에게 메시지를 보낸다. "nochi.물_채우기(myCup, 300)" 는 노치가 물컵에게 "너 '물양' 상태의 값에 300을 더해줘"라는 의미이다. 그럼 물컵은 '물양'이라는 변수에 300을 더한 것이지 현실 세계를 그대로 투영하면 안된다.

표현을 다르게 하면 '해당 변수에 300을 증가시켜줘.'라고 한 것이다. 이 같은 시선으로 객체를 바라봐야 한다. 이것이 객체 지향적인 세계에서 바라보는 객체의 관점이다.

어느 유튜버가 한 말인데, 리그오브레전드 이즈리얼 강의에서 "이즈리얼 Q의 모양을 보지 말고 네모난 크기의 데이터 조각이 날아간다고 생각해보세요"

자율적인 객체

이제 우리는 객체적인 시각에서 객체를 바라볼 수 있는 눈이 생겼을 것이라 믿는다. 서론에서 나는 훌륭한 객체자율적인 객체라고 했다. 객체가 자율적이다 라는 말은 무엇일까?

사전적인 자율적

'자기 스스로의 원칙에 따라 어떤 일을 하거나 자기 스스로 통제하여 절제하는 것'

우리 프로그래밍 객체도 자기 맘대로 산다.(그 마음을 코딩하는 건 우리의 몫이다.) 물컵의 코드를 잠깐 수정하겠다.


class 물컵 {
  물양: number;
  용량: number;
	constructor(담겨_있던_물양:number, 최대_용량: number){
      this.물양 = 담겨_있던_물양;
      this.용량 = 최대_용량;
 	}
  
  	// 물 얼마나 남았지?
	getAmount() {
      return this.물양; 
    }
  
  	// 물 줄이기
  	lossAmount(뺄_물양: number): void {
      if(this.물양 < 뺄_물양 ) {
        throw new Error('아주 그냥 바닥까지 긁어버릴 기세네');
      } 
      this.물양 =- 뺄_물양;
    }
  
  	// 물 늘리기
  	addAmount(더할_물양: number): void {
      let total = this.물양 + 더할_물양;
      if(total > this.용량) {
        console.log('물이 넘쳤지만 최고 용량까지는 담았다.');
        total = this.용량;
      }
      this.물양 = total;
    }
}

이 객체는 이제 지 마음대로 행동한다. 아래 코드를 보라

// 물컵의 내용물의 양은 500ml,  용량은 1024ml
const myCup = new 물컵(500,1024);
const nochi = new 사람("Nochi");


// '노치'는 600ml의 물을 마시고 싶다.
nochi.물_마시기(myCup, 600);

하지만 에러를 발생 시키면서 물컵은 노치의 메세지를 거부했다. 이 얼마나 자율적인 객체인가. 이런 의미에서 자율적이란 의미는 아니지만 객체와 소통은 했지만 반드시 그 임무를 수행하진 않는다는 것을 보여주기 위한 예시로 넣었다.

const myCup = new 물컵(500,1024);
const nochi = new 사람("Nochi");


// '노치'는 10000ml의 물을 물컵에 채우고 싶다.
nochi.물_채우기(myCup, 10000);
console.log(`물의 양: ${myCup.getAmount()}ml`)

자율성

여기서 근데 웃긴건 물의 양을 볼 땐, 코드에선 myCup 객체를 이용하여 물의 양을 확인한다. 왜냐면 nochi 객체는 물의 양을 모른다. 다른 말로는 물의 양 데이터를 가지고 있지 않다. 또 다른 말로는 nochi 객체myCup 객체를 잘 모른다.

객체는 다른 객체가 '무엇(what)'을 수행하는지 알 수 있지만 '어떻게(how)' 수행하는지에 대해서는 알 수 없다.
- 객체지향의 사실과 오해 : 역할, 책임, 협력 관점에서 본 객체지향

다른 말로 '무엇을 수행' 어떤 함수가 실행되는지는 알 수 없지만 그 내부적으로 어떻게 동작하는지에 대해서는 알 수가 없음을 의미한다.

객체의 관점에서 자율성이란 자신의 상태를 직접 관리하고 상태를 기반으로 스스로 판단하고 행동할 수 있음을 의미한다. 객체는 행동을 위해 필요한 상태를 포함하는 동시에 특정한 행동을 수행하는 방법을 스스로 결정할 수 있어야 한다. 따라서 객체는 상태와 행위를 하나의 단위로 묶는 자율적인 존재이다.

자율적인 객체로 구성된 공동체는 유지보수가 쉽고 재사용이 용이한 시스템을 구축할 수 있는 가능성을 제시한다.
- 객체지향의 사실과 오해 : 역할, 책임, 협력 관점에서 본 객체지향

여기서 "자신의 상태를 직접 관리" 한다는 말을 기억해야 한다. 객체지향적이지 못한 프로그래밍은 주로 여기서 발생한다. react에서 주로 이러한 패턴을 쓰는 경우가 많은데 바로 useState의 set함수를 다른 컴포넌트로 넘겨버리는 행위이다. 아래의 코드로 살펴보자.

const Mother: React.FC = () => {
	const [분노, set분노] = useState(false);
  
  	return {
      <div> 
      	<Child set분노={set분노} />
      </div>
    }
  
}


const Child: React.FC = ({set분노} : {set분노: 대충 리액트 Type임}) => {
  
	const 장난감_어지르기 = () => {
     	/* 대충 장난감 어지르는 내용 */
      	set분노(false);
    }
    
    const 낙서하기 = () => {
     	/* 대충 낙서하는 내용 */
      	set분노(false);
    }
    
    return <div>미운 5</div>
}

이제 아이는 장난감을 어질러도, 낙서를 해도 엄마는 분노는 아이의 손에 있다. 이렇게 부모의 set함수의 결정권을 양도하게 되면 Mother 컴포넌트는 관리가 안된다. 그러면 어떻게 해야하는가?

우리는 컴포넌트를 구성하는 상태를 결정하는 함수를 넘겨서는 안된다.

const Mother: React.FC = () => {
	const [분노, set분노] = useState(false);
  	const [회초리, set회초리] = useState(0);
  
  	const handle용서 = ({이유}: {이유: string}) => {
      	if(용서의_이유들.includes(이유)) {
          set분노(false)
        } else {
          set회초리((prev회초리) => prev회초리 + 1)
        }
    }
  
  	return {
      <div> 
      	<Child handle용서={handle용서} />
      </div>
    }
  
}


const Child: React.FC = ({handle용서} : {handle용서: (이유: string)=>void}) => {
  
	const 장난감_어지르기 = () => {
     	/* 대충 장난감 어지르는 내용 */
      	handle용서('...');
    }
    
    const 낙서하기 = () => {
     	/* 대충 낙서하는 내용 */
      	handle용서('...');
    }
    
    return <div>미운 5</div>
}

이제 아이는 엄마의 화를 제어할 수 없다. 오히려 더 혼날 수 있다. 이렇듯 객체가 소통은 하지만 자신의 상태는 자율적으로 관리하는 형태를 지녀야 우리는 자율적인 객체라고 하고 코드에 녹여낼 수 있으면 해당 객체에 대해 관리가 편해진다.


정리

이때까지 자율적인 객체에 대해서 알아보았다. 정리하자면 결국 '캡슐화'이다.

캡슐화

캡슐화 - 위키백과

  • 객체의 속성(data fields)과 행위(메서드, methods)를 하나로 묶고,
  • 실제 구현 내용 일부를 내부에 감추어 은닉한다.

시작부터 OOP를 장황하게 녹여내면 이해하기 힘든 부분이 있으니 차근차근 적어보려 한다. 우리가 자율적인 객체를 만들어야 하는 이유는 완벽한 기획이란 없고, 요구사항은 항상 변하기 때문이다. 그렇기 때문에 수정이 용이하고 유지보수가 간편한 프로그래밍을 하기 위해서 자율적인 객체를 알아야 한다. 이 내용을 통해 자율적인 객체에 대해서 이해하고 코드에 녹여낼 수 있는 개발자가 되었으면 좋겠다.

1개의 댓글

comment-user-thumbnail
2023년 3월 8일

생각을 많이 하고 적은거 같아서 하트 드렸습니다.

답글 달기