자바를 공부하는 개발자로서 객체지향에 대해 모르면 안된다고 생각해서, 객체지향과 객체지향 디자인 패턴에 대해 알아보고자 합니다.
만약 어떤 소프트웨어를 개발하고자 하는 일을 성을 쌓는다고 가정하고, 코딩은 인부들에게 시키는 작업이라고 생각해봅시다.

만약 인부들에게 지시사항을 전달할 때,
'나무를 베라',
'벤 나무를 다시 잘라라',
'자른 나무를 가공해서 목재를 만들어라',
'시멘트와 모래를 섞어라',
'거기에 물을 붓고 섞어서 회반죽을 만들어'
'이 공간에 회반죽을 사용해서 돌들을 쌓아'
이러한 지시사항들을 인부들에게 일일히 전달하면 끊임없이 이어지고, 비슷한 내용이 계속 반복되게 됩니다.
이러한 방식은 코드를 매우 장황하고 중복이 많아 비효율적이며, 이를 분석하고 수정하는데 많은 어려움을 겪게 합니다.

따라서 이러한 작업들을 결이 비슷한 작업들끼리 묶어 사용하는데, 이 묶음 단위를 함수라고 합니다. 예를 들어 나무를 베고 벤 나무를 다시 가공하여 목재로 만드는 일련의 과정들을 합쳐 목재 부품 만들기라는 함수로 정의할 수 있습니다. 그리고 나머지 석재를 만들어 쌓는 과정을 석재쌓기로 정의할 수 있다. 이러한 과정은 아직 절차지향 프로그래밍에 해당된다. 함수를 사용하여 재사용성과 효율성을 높였지만 소프트웨어의 규모가 커지면 이것만으로는 코드의 복잡성을 관리하는데 한계가 있습니다.
이제부터 우리는 '역할'들을 정의해볼겁니다. 이 역할은 객체지향 프로그래밍에서는 '클래스(class)'라 불립니다. 성을 건축하는 작업에는 목수, 석공, 대장장이, 기술자 같은 클래스가 동원됩니다.

| 클래스 | 목수(Carpenter) | 석공(Mason) | 대장장이(BlackSmith) | 엔지니어(Engineer) |
| 속성 | -tools -blacksmith |
-materials -mortarMix |
-forgeTemperature -anvilType |
-specialty |
| 메소드 | +makeWoodenComponent | +layStone() | +forgeTool() | +engineer() |
말하자면 인부들의 '직업'들을 정의한다고 생각하면 될 것 같습니다. 이를 '클래스', 그리고 '역할'이란 말로 부를겁니다. 이 클래스에는 이 '역할'을 맡은 인부들이 저마다 가질 데이터와, 그들이 구사할 수 있는 함수들이 정의되어 있습니다. 이 데이터들을 속성(attribute), property 또는 field라고 부릅니다. 여기선 속성이라고 부르겠습니다. 이 속성은 이 역할을 맡은 인부들 각각이 가지고 있는 '상태'입니다. 그리고 클래스에 속한 함수들을 메소드라고 부르는데, 이건 이 인부들이 할 수 있는 행동으로 표현할 수 있습니다. 예를 들어 이 목수라는 역할에는 목수가 사용하는 도구와 그 도구들을 만들어주는 전담 대장장이가 속성으로, 그리고 목수가 하는 일, 즉 '목재 부품 만들기'가 메소드로 정의되어 있습니다. 여기서 핵심이 되는 것은 이 메소드로, 각 역할을 맡은 인부들은 이 '행동'을 취할 책임을 갖게 됩니다. 속성들은 이 책임을 수행하기 위해 필요한 데이터들일 뿐입니다. 대장장이들에게는 도구들을 만들 책임, 기술자들에게는 설계/시공을 하는 책임이 메소드라 불리는 함수의 형태로 주어져 있습니다.
이처럼, 객체지향 프로그래밍에서는 성을 건축하는 일을 시작하기 전에 이러한 역할들과 그것들 각각의 '책임'을 먼저 설계하고 정의합니다. 그러고 나서 인부들에게 '너는 목수 1, 너는 목수 2야'. '너는 석공이야', '나머지는 모두 대장장이야' 이렇게 각자의 역할을 배정하고 일을 시작하게 됩니다.

만약 여러분이 직접 인부들을 통솔해서 성을 쌓는 일을 한다고 생각해시보면, 처음에 했던 방식처럼 그냥 명령들을 내려서 일을 시키는 것 보다, 이처럼 역할부터 정한 뒤 인부들에게 할당하는 방식이 훨씬 체계적이고 관리가 용이한 방법임을 쉽게 알 수 있을 것입니다. 객체지향 프로그래밍의 코드는 보통 이와 같이 각 클래스별로 모듈화, 즉 클래스란 단위로 나뉘어져 있습니다. 프로그램을 수정할 일이 있으면 그에 해당하는 클래스를 찾아 수정할 수 있고, 하나의 클래스를 여러 프로그램에서 재사용하는것도 가능해집니다. 성을 지을 때 사용했던 목수 클래스를 나중에 집이나 다리를 만드는 프로그램에도 가져다 쓸 수 있다는 얘기입니다. 우리가 '클래스'라 부르는 이 역할을 배정받은 인부들을 '인스턴스' 또는 '객체'라고 부릅니다. 이 객체들은 자신의 역할대로 부여받은 속성들과 메소드에 대한 데이터를 갖고 있는 데이터 덩어리입니다. 객체에 이런 데이터들이 담기기 때문에, 흔히 객체를 데이터를 넣는 용도의 주머니 같은 것으로 오해할 수 있는데, 객체에서 중요한 것은 메소드 즉 책임입니다. 객체는 일정한 메모리 자원에 클래스, 즉 역할이 부여되어 책임을 가진 존재로 만들어진 결과라고 생각됩니다.

이 클래스들은 보통 혼자 일하지 않고, 다른 클래스와의 '협력' 을 통해 복잡한 작업을 수행해냅니다. 예를 들어 목수 객체들은 지정된 전담 대장장이 객체를 가지고 있고, 이 대장장이 객체의 메소드를 호출하여 각자가 사용할 도구들을 주문합니다.
여기서 중요한 것은, 목수 객체는 대장장이 객체가 이 '도구제작'이란 책임을 '어떻게' 수행하는가에는 관심을 갖거나 참견하지 않는다는 것입니다.
이 속성들과 메소드 앞에 보면 이렇게 플러스 표시와 마이너스 표시가 있습니다. 이처럼 마이너스로 표시된 항목들은 'private'이란 상태로 외부로부터 감춰져 있기 때문에 다른 클래스의 객체가 이 데이터를 읽거나 수정하지 못하도록 되어 있습니다. 이것을 '캡슐화'라고 합니다. 대장장이가 어느 온도의 불을 사용하고 어떤 모루를 사용하는지는 절대 다른 누군가 간섭해서는 안되는겁니다. 목수는 단지 대장장이에게 필요한 도구를 주문하고, 대장장이는 자기 방식대로 이를 만들어 건내줄 뿐입니다. 이처럼 각 클래스가 자기가 맡은 책임에만 관심을 갖도록 함으로써 코드의 복잡성을 줄이고 유연성을 확보할 수 있습니다.
각 클래스는 보통 특정 메소드만을 외부에 공개해서 이를 다른 클래스와의 소통 창구로 삼는데, 목수 클래스의 코드를 작성할 때 개발자는 대장장이 클래스가 어떻게 구현되어 있는지 고려할 필요 없이 대장장이가 외부로 공개한 이 'forgeTool' 메소드를 호출하기만 하면 됩니다. 그리고 대장장이 클래스가 'forgeTool'을 실행하는 메커니즘이 이후 어떻게 바뀌든 다른 클래스가 호출하는데에는 아무 영향을 받지 않습니다. 이 외부 요소들은 공개된 이 메소드를 통해 작업의 결과물만을 받아갈 뿐입니다. 만약 이렇게 하지 않고 다른 클래스들이 서로의 매커니즘에 관여할 수 있게 했을 때, 코드가 얼마나 얽히고 설키게 될 지 생각해보면 캡슐화를 하는 이유를 생각하실 수 있을 겁니다.
다음은 상속에 대해 알아보고자 합니다. 목수에도 일반 목수들 말고도 이들을 활용하여 목재 구조물을 건설할 수 있는 '건설목수' 가 있을 수 있습니다. 또한 석공에도 일반 석공이 할 수 있는 일에 더하여 돌로 조각을 할 수 있는 '석조조각가', 일반 석재 대신 벽돌을 쌓는 '벽돌공' 들의 역할을 따로 둘 수 있습니다. 만약 이들을 목수와 석공을 클래스로 정의한 것 처럼 별개의 클래스로 정의한다면 어떨까요? 물론 그럴수도 있지만 이에는 두가지 단점이 존재합니다.

건설목수 클래스는 기존 목수 클래스의 속성과 메소드에 자신의 메소드를 더한 클래스이기 때문에, 이처럼 중복되는 코드가 생기게 됩니다. 이후 공통 기능에 수정사항이 있을 때 이를 역시 일일히 다 적용해주어야 하게 됩니다.

두 번째 단점은 공통분모를 가진 이 클래스들 간에 아무런 연관성이 생기지 않는다는 점입니다. 속성과 메소드들을 공유하는 클래스들이지만, 이러한 공통점이 프로그래밍에는 전혀 반영되지 못하게 됩니다. 이러한 단점들을 보안하기 위해서 객체지향 프로그래밍의 '상속'이란 개념이 유용하게 사용될 수 있습니다.

이처럼 클래스는 다른 클래스에게 속성과 메소드를 물려줄 수 있습니다. 건설 목수 클래스가 목수 클래스로부터 확장하도록 설계하면 자식 클래스인 건설목수 클래스는 부모 클래스인 목수 클래스의 속성과 메소드를 '상속'받게 됩니다. 개발자는 이 관계를 설정하고 건설목수 클래스가 추가로 할 일을 메소드로 작성해주면 됩니다.
석공 클래스 또한 자식 클래스들에게 속성과 메소드를 물려주는데 석조조각가에서는 layStone 메소드를 실행하면 석공 클래스와 동일하게 그냥 돌을 쌓는 메소드를 실행시키지만, 벽돌공에서 layStone 메소드는 조금 다르게 실행됩니다. 이름에서 알 수 있듯 벽돌을 쌓게 되는데, 이처럼 특정 메소드를 다른 방식으로 실행할 목적으로 자식 클래스를 만들기도 합니다. 이처럼 부모의 메소드를 자식의 방식으로 덮어씌우는 것을 '오버라이딩' 이라고 합니다. 상속은 특정 속성이나 메소드를 추가한 클래스를 만들거나, 특정 메소드의 실행방식을 수정한 클래스를 만들기 위해서도 할 수 있고, 물론 둘 다도 가능합니다. 이러한 상속을 통해 얻을 수 있는 이점은 단지 중복 제거를 통해 코드량을 줄이는 것 뿐만 아니라 서로 다른 클래스들을 하나의 카테고리로 묶을 수 있다는 점입니다.
건설목수는 부모인 목수의, 그리고 석조조각사와 벽돌공은 역시 그들의 부모인 석공의 한 종류가 됩니다. 이는 목수가 들어갈 수 있는 자리에는 건설목수도 들어갈 수 있고, 석공이 들어갈 수 있는 자리에는 자식 클래스인 석조조각사와 벽돌공 클래스도 들어갈 수 있습니다. 예를 들자면 석공의 객체들이 포함될 수 있는 배열이나 리스트에는 석조조각사와 벽돌공의 객체도 담길 수 있습니다. 즉 부모 클래스는 자식 클래스들을 한데 묶는 카테고리의 역할도 할 수 있다는 겁니다. 그리고 이 카테고리는 특정 자리에 어떤 클래스들이 들어가 사용될 수 있는지를 정하는 기준으로 작용할 수 있습니다.

이러한 카테고리의 역할을 하는 것만을 목적으로 하는 클래스도 있습니다. 위에서 정의했던 기술자 클래스인데, 이는 추상 클래스라고 불립니다. 추상 클래스는 그 자체로 인스턴스, 즉 객체를 만들 수 없습니다. 사람들이 그냥 '포유류' 또는 '파충류'라고만 부르는 동물이 없는 것처럼 기술자 또한 그냥 기술자라는 추상적인 역할을 인부들에게 부여하지 않습니다. 이 클래스에는 '설계/시공' 이라는 메소드가 있지만 이름만 있을 뿐 이게 뭘 하는지는 정의되어 있지 않습니다. '배수기술자', '방어시설기술자' 클래스는 이 '기술자' 추상 클래스의 자식 클래스 입니다. 인부들은 이 구체화된 자식 클래스들의 객체로서 역할을 부여받을 수 있습니다. 배수기술자나 방어시설 기술자가 될 수는 있지만 그냥 기술자가 될 수는 없는겁니다. 그리고 engineer() 메소드가 뭘 하는지는 자식클래스 대에서 결정됩니다. 배수기술자는 수로시설을 구축하고 방어시술기술자는 방어용 무기들을 제작하는 engineer() 메소드를 갖게 되죠. 추상 클래스인 '기술자' 클래스의 역할은 이 자식들을 한 데 묶는카테고리로 작용하는 것입니다. 위에서 말했듯, '기술자'가 들어갈 자리로 정의된 자리에는 배수기술자나, 방어시술기술자 모두 들어갈 수가 있습니다. 프로그램의 특정 부분에서 '기술자'에게 설계/시공을 시킨다는 코드를 작성했을 때, 그 자리에 배수기술자를 임명하면 수로가 구축되고 방어시술 기술자를 임명하면 방어시설이 건설되는 겁니다. '기술자'의 자식 클래스들은 반드시 '설계/시공'이라는 메소드를 물려받아 갖고 있기 때문에 가능한 것입니다.
이처럼 서로 다른 클래스를 한 카테고리로 묶고 같은 이름의 책임을 부여하는 또 다른 방법으로 인터페이스가 있습니다. 자바에서의 인터페이스를 설명하자면, 인터페이스는 서로 완전히 다른 부모에게 속한 클래스들도 하나의 카테고리로 묶을 수 있습니다. 상속이 마치 가문의 이름처럼 조상으로부터 물려받는 거라면, 인터페이스는 학위나 자격증처럼 혈통에 상관없이 부여받을 수 있습니다.

박쥐 날치 비둘기는 각각 포유류, 어류, 조류로서 서로 다른 클래스의 자식들이지만 날아다닐 수 있기 때문에 '날짐승'이란 인터페이스를 적용받은 겁니다. 인터페이스 또한 그 자체로 객체를 만들지는 못합니다. '마차운전수'라는 인터페이스는 마차를 이용해 '자제들을 운반' 할 수 있는 자격증으로 정의했을 때, 마차운전수라는 직업은 없지만 모든 목수, 벽돌공, 방어시술기술자는 업무상 마차로 자재들을 운반할 필요가 있습니다. 때문에 이 클래스들에 '마차운전수' 인터페이스를 적용하는 겁니다. 이 직업들의 필수 자격증으로요. 이제 이 클래스는 만드시 이 '자제들을 운반' 이라는 메소드를 각자 나름의 방식대로 구현해야 합니다. 예를 들어 '건설목수'의 경우에는 '목수'로부터 물려받은 속성과 메소드, 그리고 자신의 메소드에 더하여 '마차운전수' 인터페이스의 메소드까지 갖추게 되는 것입니다. 또한 이들은 모두 '마차운전수'라는 카테고리에 들어가게 되고 '마차운전수' 전용으로 만든 배열 등의 자료구조, '마차운전수'를 매개변수로 갖는 함수등에 이들의 객체가 들어갈 수 있고, 이는 '마차운전수'가 '자제들을 운반'해야 하는 상황에서, 이들 중 어떤 것의 객체든 동원될 수 있다는 겁니다. 그리고 어느 것의 객체가 동원되었는가에 따라, 어떻게 '자제들이 운반'될지도 달라지게 됩니다. 추상 클래스의 메소드처럼, 이를 적용하는 클래스들 각각 나름의 방식으로 이를 구현하기 때문입니다.
이런 방식으로 우리는 클래스 속성, 메서드, 상속, 인터페이스 등 객체지향 프로그래밍의 핵심 기능을 실제 예제를 통해 살펴보았습니다. 실제 시나리오를 반영하는 것은 제가 이해했다고 생각했던 개념을 명확히 하는 데 도움이 되었습니다. 또한 다른 사람들에게 객체지향에 대해 설명할 때 어떻게 설명하면 좋을지 생각해볼 수 있었습니다. 긴 글 읽어주셔서 감사합니다.
'개발 공부' 카테고리의 다른 글
| TPS(Transaction Per Second) (0) | 2025.03.28 |
|---|---|
| WebSocket과 STOMP (0) | 2025.03.26 |
| WebSocket 통신 방식이란? (0) | 2025.03.26 |
| 코딩테스트와 알고리즘 연습 (0) | 2024.11.28 |
| CORS가 뭐지 (0) | 2024.11.08 |