본문 바로가기
Back-end/Java 개념

지극히 개인적인 OOP에 대한 원초적인 접근과 해석

by devraphy 2023. 8. 6.

0. 개요

요즘 스터디에서 토비의 스프링을 읽고 있습니다. 

책을 읽다보니 내가 그동안 짜던 코드는 OOP적인 코드가 아니라는 것을 깨달았습니다.

머리로는 "이해하고 있다"라는 착각을 하고 있던 것이죠.

그래서 근본적인 질문과 관점으로 다시한번 OOP에 대해 배워보려고 합니다.

 

1. OOP란 무엇인가?

OOP는 객체지향 프로그래밍(Object Oriented Programming)의 약자입니다.

그렇다면 이 객체 지향이라는 것이 무엇일까 궁금증이 생깁니다. 

 

객체지향이라는 것은 한 묶음의 소스코드를 객체라는 하나의 단위로 보는 것입니다.

그리고 이 객체 간의 상호작용을 통해서 하나의 프로그램이 돌아가게 되는 것이죠.

마치 전대물에 나오는... 수십가지 서로 다른 로보트가 합체해서 새로운 로보트가 되는 것과 같은 개념입니다.

 

그러면 소스코드가 묶인다는 것은 무슨 뜻일까요? 무엇을 기준으로 소스코드가 묶이고 분리되는 것일까요?

그 기준은 역할과 기능에 있습니다. 

 

하나의 객체는 한가지 역할을 수행하는 소스코드의 집합이며, 그 한가지 역할을 수행하기 위해 필요한

여러 세부 기능들이 메서드라는 단위로 나뉘어집니다. 이 메서드 또한 하나의 역할을 기준으로 분리됩니다.

 

음... 뭔가 재귀함수처럼 계속 돌고 도는 이야기를 하는 것같은데...

 

여기서 말하는 역할과 기능에 의해 코드를 분리하고 묶는것이 무엇을 의미하는 것일까?

정확하게 어떤 방식으로 코드를 짜라는 것일까? 라는 궁금증이 생깁니다. 

 

여기에 대해서 이야기를 해보죠. 

 

2. 역할과 기능에 의한 분리와 구성

역할과 기능에 의한 분리와 구성은 굉장히 방대한 범주의 이야기입니다.

왜냐면 하나의 역할을 어떻게 정의하느냐에 따라 그 역할이 갖게되는 기능의 범주가 달라지고,

하나의 기능을 어떻게 정의하느냐에 따라 기능의 분리가 좌우되기 때문이죠.

 

더불어 이러한 설명 자체가 굉장히 추상적인 워딩이기 때문에

도대체가 뭔소린지 하나도 모르겠습니다.

 

좀 더 명확한, 구체적인 기준이 필요합니다.

 

그래서 "중복"이라는 키워드로 OOP를 설명해보려합니다. 

 

3. OOP는 중복을 제거하는 것

중복이 없는 설계 또는 중복을 최소화하는 설계가 OOP라고 할 수는 없습니다.

다만 OOP이기위한 필요조건 중 하나가 중복에 대한 제거이며,

이에 따른 유지보수의 간소화와 재사용성 증가의 발현이 있습니다.

 

중복을 최소화함으로써 소스코드를 분리하고 묶는 기준이 생기고 이에 의해 OOP가 발현되기 때문이죠.

즉, 중복된 코드를 공통부로 치환함으로써 역할과 기능에 대한 분리 기준이 생기는 것입니다. 

 

한가지 예시를 볼까요? 

위의 그림처럼 3개의 청소부 클래스 있다고 해봅시다.

각 클래스는 청소부라는 역할을 가지며, 그 역할을 수행하기 위해 출근(), 청소(), 퇴근()이라는 기능을 가집니다.

 

위처럼 코드를 작성한다면 동일한 내용의 클래스를 3개나 생성한 것이죠? 여기서 중복이 발생한 것입니다.

 

그렇다면 어떻게 중복을 제거할 수 있을까요?  아래 코드처럼 할 수 있지 않을까요?

@Getter @Setter
public class Cleaner {

    private String name;
    private String building;
    
    public Cleaner(){}
    
    public Cleaner(String name, String building) {
    	this.name = name;
        this.building = building;
    }
    
    public void goToWork() {
    	System.out.println(building + " 출근");
    }
    
    public void clean() {
    	System.out.println(building + " 청소");
    }
    
    public void goToHome(){
    	System.out.println(building + " 에서 퇴근");
    }
}

public class Main {
    public static void main(String[] args) {
    	Cleaner cleanerA = new Cleaner("A", "BuildingA");
    	Cleaner cleanerB = new Cleaner("B", "BuildingB");
        Cleaner cleanerC = new Cleaner("C", "BuildingC");
    }
}

 

a) 중복이란 정의에 대해서

여기서 한가지 생각해봅시다.

위의 코드는 어떤 중복에 대한 해결을 이뤄낸 것인가요? 

 

동일한 내용의 클래스 3개를 1개의 클래스로 구성하여 해소시켰으니,  

위의 예시는 생성에 대한 중복을 제거한 것이라고 볼 수 있겠네요.

 

이처럼 중복이라는 것은 단순히 동일한 코드의 등장을 이야기하는 것이 아니라는 것을 알 수 있죠.

 

내용이 같은 객체를 생성하는 것에 대한 중복일 수도,

동일한 기능의 구현을 반복하는 것에 대한 중복일 수도, 

동일한 구조를 사용하는 것에 대한 중복일 수도 있습니다.

 

결론적으로 중복을 제거한다는 것에는 다양한 의미가 내포되어 있다는 것을 알 수 있죠.

 

b) 행동에 대한 중복 제거

만약 청소부의 기본적인 역할은 똑같지만, 각 청소부마다 내용이 아예 다르다면 어떻게 해야할까요?

아래의 예시를 봅시다.

3가지 다른 종류의 청소부가 있다고 해봅시다.

동일한 행동을 수행하지만 그 내용은 너무나도 다르다고 가정해보죠.

그렇다면 위에서 사용했던 생성 중복에 대한 제거 방식이 적용될까요? 

 

그렇지 않을거에요. 왜냐면 동일한 행동이지만 그 내용이 전혀 다르기 때문이죠. 

그렇다면 이럴때에는 어떻게 해야할까요?

 

추상화를 통해서 공통된 역할에 대한 정의를 만들고, 그 내용은 각 클래스에 작성하도록 할 수 있죠.

아래의 코드처럼 말이에요. 

public interface Cleaner {
	
    public abstract void goToWork();
    public abstract void clean();
    public abstract void goToHome();
}

public class RocketCleaner implements Cleaner {
    @Override
    public void goToWork() {}
    
    @Override
    public void clean(){}
    
    @Override
    public void goToHome(){}
}

public class BattleShipCleaner implements Cleaner {
    @Override
    public void goToWork() {}
    
    @Override
    public void clean(){}
    
    @Override
    public void goToHome(){}
}

public class BuildingCleaner implements Cleaner {
    @Override
    public void goToWork() {}
    
    @Override
    public void clean(){}
    
    @Override
    public void goToHome(){}
}

위의 코드처럼 작성했을 때 발현되는 또다른 특징은 확장성이라는 겁니다.

청소부이기에 Cleaner 인터페이스를 공통적으로 상속받아 공통된 역할을 명시하면서, 

동시에 각 클래스가 갖는 별도의 기능 또는 행동을 메서드로 정의 및 추가할 수 있다는 것이죠. 

 

또한 Cleaner 인터페이스를 상속받았기에 해당 클래스가 청소부의 역할을 한다는 것을 Sementic하게 알 수 있죠.

 

c) 구조에 대한 중복 제거

마지막으로 구조(Structure)에 대한 중복 제거를 이야기해보려고 합니다.

구조라고 하니까 엄청 어려운것 같은데 사실은 그렇지 않습니다. 

 

구조라 함은 객체의 생성방식에 대한 이야기입니다. 

예를 들어서 매번 new 연산자를 이용해 객체를 계속해서 생성하면 메모리 효율이 매우 떨어지겠죠.

그래서 공통된 하나의 객체를 생성하고 싶다면 Singletone이라는 패턴을 이용하여 중복 객체 생성을 제거합니다.

 

또 다른 예시로 특정한 라이프사이클 안에서만 어떤 객체를 생성하고 사용하고싶다면, 

특정 라이프 사이클 안에서 매번 new 연산자로 객체를 생성하는 것이 아니라 애초에 객체의 생성방식 자체를 
특정 라이프 사이클로 종속시킬 수도 있겠죠? 

이러한 경우 Prototype이라는 패턴을 이용하여 객체 생성 방식에 대한 중복을 제거할 수 있습니다. 

 

4. 마무리하며

지금까지 중복 제거를 통한 OOP 구현에 대해 이야기해보았습니다.

사실 중복 제거 외에도 OOP를 구현하는데 필요한 여러가지 조건이나 특징들이 있지만,

중복을 제거함으로써 파생 및 발현되는 특징들이라는 생각에 중복 제거를 중점으로 OOP에 대해 풀어보았습니다.

 

개인적으로 OOP를 코드레벨의 어떤 작성 방식이라고 이해하고 있었습니다.

그렇기에 OOP를 짜는데 왜 디자인패턴이 중요하지? 왜 디자인 패턴 이야기가 매번 등장하지? 라는 의문이 

머릿속 한켠에 남아있었는데, 이번 기회를 통해서 OOP라는 것은 결국 코드레벨이 아닌 프로그램의 설계, 구조에 대한 이야기라는 것을 깨닫게 되었습니다. 

 

추가적로 왜 중복의 종류를 생성, 행동, 구조라는 3가지 테마로 나누어서 설명했는지 궁금하실 수도 있는데, 

그 이유는 디자인 패턴의 종류가 생성, 행동, 구조라는 3가지 목적으로 구분되어 설명되기 때문입니다. 

 

요약해보자면 OOP란 코드의 중복을 제거하는 설계이며, 중복을 제거함으로써 파생되는 다양한 개념들(재사용성, 확장성, 추상화, 상속, 다형성, 캡슐화, DI, IoC 등)이 있으며 중복은 생성, 행동, 구조라는 3가지 종류로 나뉘고 이를 구조적으로 해결하기 위한 디자인패턴이 등장하며, 그렇기에 디자인패턴은 생성, 행동, 구조라는 3가지 목적으로 나뉜다 라는 것이 저의 해석입니다.   

 

여러분은 어떻게 생각하시나요?

 

OOP라는 것이 한마디로 정의하거나 설명할 수 없을만큼 방대하고 복합적인 개념이라는 것은 잘 알고 있습니다.

다만, 이를 간소화 하기위해 지극히 개인적인 저만의 이해와 해석으로 풀어보았습니다. 

 

추가적인 질문이나 지적은 언제나 환영입니다.

 

감사합니다.

댓글