티스토리 뷰

Backend

객체는 어떻게 식별하고 구현해야 할까?

지마켓 권우석 2022. 8. 17. 16:57

 안녕하세요 Post-tx & Accounting 팀 권우석입니다.

 

 회사에 처음 입사했을 때 저에게 '객체 지향 프로그래밍'은 인터넷에 검색하면 나오는 몇 가지 특성과 설계 원칙으로 대변되는 단어였습니다. 하지만 당장에 프로젝트를 진행한다고 상상했을 때 "어떤 객체가 필요하지?", "객체는 어떻게 구현해야 하지?"와 같은 질문을 마주할 것 같았고, 질문에 대한 답을 명확히 할 수 없는 느낌에 공부를 했었는데요.

 

 이때 공부했던 내용을 아래의 순서로 간단히 정리해보려 합니다.

 

(1) 객체를 식별하는 방법 

(2) 객체를 구현하는 방법 

(3) 객체들이 협력할 수 있는 구조

 

 

객체 지향은 현실의 모방이다?

 위 사진을 보면 "배달원이 고객에게 택배를 전달하는 상황"이라는 사실을 어렵지 않게 파악할 수 있습니다. 아마 사진 속 상황을 파악하지 못한 사람은 아무도 없을 것이라 생각합니다. 그런데 사람들은 어떻게 이 사진을 보고 상황을 이해할 수 있는 걸까요?

 

 인간은 선천적인 인지 능력을 이용해 독립적인 객체들을 식별하고 이를 통해 세상을 이해한다고 합니다. 저의 인지 과정을 뒤늦게 돌이켜보면 물건을 건네는 사람, 지마켓 로고가 보이는 상자, 이를 건네 받으려는 사람이라는 독립적인 객체와 이들이 가진 각각의 특성과 행동을 바탕으로 사진 속 상황을 이해한 것 같은데요.

 

 객체지향 패러다임은 제가 위 사진 속 상황을 이해한 것처럼 인간이 세상을 객체들의 집합으로 바라본다는 믿음에서 출발합니다. 이 생각에 더해 소프트웨어도 현실과 마찬가지로 독립적인 객체의 집합이라 여기는 것이죠. 이러한 관점은 인간의 선천적인 인지 능력에 바탕을 둡니다. 때문에 직관적이고 이해하기 쉬운 패러다임이라 여겨지기도 하죠.

 

 하지만 객체지향 패러다임이 추구하는 소프트웨어 세계와 현실 세계 사이의 교집합은 각 세계를 객체들의 집합으로 여긴다는 것 까지입니다. 객체 지향이 실세계의 모방이라는 비유는 객체 지향의 많은 부분을 쉽게 설명해주지만, 실제로 객체 지향 패러디임이 추구하는 소프트웨어 객체는 실세계의 객체와는 사뭇 다릅니다.

 

 차이점을 한가지만 들라면 '자율성'을 꼽을 수 있을 텐데요. 현실 세계의 택배 상품은 사람에 의해 전달되고 배송 상태를 가지고 있지도, 배송 상태를 바꾸지도 못합니다. 하지만 SW 객체로 택배 상품을 구현하고 배송 상태를 가지게 한다면 택배 상품 객체는 스스로의 배송 상태를 바꿀 수 있는 자율성을 가진 존재로 재탄생하게 됩니다.

 

 

(1) 소프트웨어 객체를 어떻게 찾아야 할까?

 소프트웨어 객체는 현실 객체와 다르다고 설명했는데요. 그렇다면 한 가지 의문이 생기는데요. 현실 속 객체는 사진을 관찰함과 동시에 직관적으로 식별해낼 수 있었습니다. 그렇다면 소프트웨어 객체는 어떻게 찾아낼 수 있는 걸까요?

 

객체들의 관계에서 출발하자

 「객체지향의 사실과 오해」 (조영호 저)에서는 소프트웨어를 "공동의 목표를 달성하기 위해 협력하는 객체들의 공동체"라 표현합니다. 또한 「객체지향 프로그래밍」 (김동헌 저)에서는 객체들을 설계할 때 "점보다는 선으로 접근하라"라고 조언하는데요. 두 책은 모두 "객체들의 관계에 주목하라"라고 힘주어 말하고 있습니다.

 

 객체도 찾지 않았는데 객체들의 관계에 주목하자니 의아할 수 있지만 예시를 통해 설명드리겠습니다. 우선 사진 속 상황에서 목표를 찾자면 "배송"일 것입니다. 이러한 목표가 달성되기 위해서는 많은 행동들이 필요합니다. 고객은 판매자의 상품을 구매해야 하고, 판매자는 해당 상품을 배달원에게 배송 요청해야 하고, 배달원은 고객에게 해당 상품을 전달해야 합니다. 정말 단순하게 표현해봤지만, 어떠한 목표가 생기면 목표를 달성하기 위한 행동들이 생기고 자연스럽게 행동을 수행할 주체인 객체들이 식별됩니다.

 

 

(2) 객체는 어떻게 구현해야 할까?

 지금까지 "배송"이라는 목표를 잡고, 이를 위한 행동을 정의함과 동시에 행동을 수행할 객체들을 찾아냈습니다. 이제는 찾아낸 소프트웨어 객체를 어떻게 구현하면 좋을지 고민해보겠습니다.

 

행동과 상태 그리고 식별자

 객체를 구현하기 위해서는 먼저 객체의 구성 요소를 알아야 할텐데요. 객체는 크게 "행동", "상태", "식별자"를 가지고 있습니다.

 

 먼저 행동은 위에서 정의한 3가지를 떠올릴 수 있겠죠. 고객 객체는 판매자의 상품을 구매해야 하고, 판매자는 고객에게 배송을 요청할 수 있어야 합니다.

  

 또한 배달원은 고객에게 판매자로부터 받은 상품을 배달하는 행동을 할 수 있어야 하죠. 객체는 관계들 속에서 자신에게 주어진 행동을 수행할 수 있어야 하고 이러한 행동은 객체 자신의 상태를 변하게 합니다. (다른 객체의 상태를 변화시키는 것이 아닌 객체 자신의 상태만 변화시킬 수 있습니다.)

 

 그렇다면 상태는 무엇일까요? 객체를 구성하는 상태는 값일 수도 혹은 또 다른 객체일 수도 있습니다. 하지만 상태의 핵심은 객체의 행동을 위해 존재한다는 점입니다. 행동은 객체 자신의 상태를 변하게 하고 변화한 상태는 이후 객체의 행동에 영향을 끼치죠.  

 

 간단한 예시를 들어보겠습니다. 판매자가 '상품에 대해 배송 요청을 했는지 여부'를 상태로 가지고 있다고 가정하겠습니다. 판매자가 배달원에게 배송을 요청하면 해당 상태값은 변경될 것입니다. 그리고 변경된 상태값은 객체의 이전 행동의 결과물이 됩니다. 만약 판매자가 같은 상품에 대해 배송 요청을 한다면 상태값을 통해 이전에 배송 요청을 했다는 사실을 알 수 있고 배송 요청을 반복하지 않게 되겠죠.

 

 마지막 객체의 구성요소는 식별자입니다. 객체는 두 인스턴스가 동일하거나 다르다고 판단할 수 있는 식별자(값 혹은 객체)를 가지고 있어야 합니다. 이때 중요한 점은 식별자가 불변해야 한다는 점입니다. 만약 식별자가 변경된다면 해당 인스턴스는 이전과는 다른 인스턴스로 식별되야 합니다.

 

 Java를 기준으로 생각해보면 모든 객체는 각기 다른 주소값을 가지고 이를 바탕으로 동일성을 비교가 가능합니다. 또한 객체에 따라 특별한 값 혹은 객체를 식별자로 가지고 동등성 비교를 통해 논리적인 동일성을 비교하기도 하죠.

 

행동이 상태를 결정한다

 객체지향 언어에서 객체는 클래스로 구현할 수 있습니다. 또한 객체가 상태와 행동을 가지고 있다면 클래스는 필드와 메서드로 상태와 행동을 구현하게 되죠. 클래스를 통해 객체를 구현할 때 유념해야할 점은 "행동이 상태를 결정한다"는 점입니다. 이러한 원칙을 생각하며 판매자 객체를 구현해보겠습니다.

 

public class Seller {
​
    // 식별자
    private String id;
    
    // 배송할 상품
    private Item item;
    
    // 배송을 위한 고객 정보
    private CustomerInfo customerInfo;
​
    // 실수로 반복해서 상품을 보내지 않기 위해
    private boolean isReqTransport;
​
​
    // 1. 목표를 위해 필요한 행동을 정의했고, 행동을 수행할 객체로 Seller를 선택했다.
    //    -> 배송을 요청하기 위해 필요한 상태는 무엇이 있을까?
    public void requestTransport(){
        
        // 상품 배송 요청 로직...
    }
}

 위 예시를 통해 객체를 구현할 때 어떤 방향으로 생각하며 만들어 나가야 하는지를 살펴보겠습니다.(정말 간단한..) 먼저 객체를 식별할 때 정의한 "배송 요청"이라는 행동을 메서드로 구현했습니다. 이후 배송 요청에 필요한 상태를 필드로 추가했고 마지막으로 식별자 id를 추가하여 객체를 구현한 클래스를 완성했습니다.

 

 이처럼 객체에서 행동을 바탕으로 상태를 결정해 나가면 보다 깔끔한 객체를 만들 수 있을 것입니다. 추가적으로 객체 내부에는 행동(프로세스)과 상태(데이터) 그리고 식별자가 존재하게 되는데, 객체가 3가지 요소를 감싼다는 의미에서 객체 지향 프로그래밍의 특성 중 하나를 캡슐화라고 부르기도 합니다.

 

 

(3) 어떻게 객체들이 협력하기 편한 구조를 만들 수 있을까?

 지금까지 목표를 이루기 위해 필요한 객체들을 식별하고 구현하는 방법에 대해 알아봤습니다. 문장에서도 들어나지만 이 과정에서 핵심은 "행동"이었습니다.

 

 하지만 한 가지 아쉬운 점은 같은 행동을 수행해야 하는 객체가 여럿 생성될 수 있다는 점입니다. 이렇게 된다면 객체들은 같은 행동을 하는 새로운 객체가 생성될 때마다 이를 고려해 변경이 일어나야 하는데요.

 

 예를 들어 일반 상품 판매자와 스마일 배송 상품 판매자가 있다고 가정하겠습니다. 두 판매자는 물건을 판매하는 똑같은 행동을 하지만 그 방식이 다르기에 각각의 객체로 분리되었죠.

 

행동의 추상화: 역할

  만약 각각의 객체를 있는 그대로 구현하게 된다면 판매자에게 요청해야하는 고객의 입장에서는 일반 상품을 구매하는 로직과 스마일 상품을 구매하는 로직을 모두 가지고 있어야 합니다. 상품을 구매한다는 행동은 똑같은데 말이죠. 이러한 문제점을 해결할 수 있는 방법이 추상화입니다. 일반 상품 판매자가 수행해야 하는 행동과 스마일 상품 판매자가 수행해야 하는 행동을 공통적으로 묶어 하나의 역할을 만들 수 있습니다.

 

  위 그림처럼 구매 요청에 대해 응답하고, 상품 배송을 요청하는 행동을 하는 판매자 역할을 두고 이를 일반 상품 판매자와 스마일 상품 판매자가 구현 하도록 만들면 문제를 해결할 수 있습니다. 즉, 일반 상품 구매에 대한 응답과 스마일 상품 구매에 대한 응답에서 "상품 구매에 대한 응답"만을 역할에 담고 "일반 상품", "스마일 상품"에 대한 구체적인 로직은 객체 각각에게 맡기는 것입니다.

 

 이렇게 구현하게 되면 고객 객체 입장에서 일반 상품 판매자에게 구매를 요청할지, 스마일 상품을 판매자에게 구매를 요청할지 고민할 필요 없이 상품을 요청할 수 있게 됩니다. 이러한 추상화 개념은 각각의 객체에게 행동의 구체적인 구현 방식을 위임하여 각 객체에게 자율성을 부여고, 요청자에게는 구체적인 동작 방식을 알 필요가 없게 만들어 객체들이 협력하기 좋은 토대를 마련해줍니다. 코드를 예시로 확인해보겠습니다.

 

public class Member {
​
​
    public void buyNormalItem(NormalSeller normalSeller){
        // 일반 상품 구매 로직
        // request To SmileSeller
        normalSeller.responseToMember();
    }
​
    public void buySmileItem(SmileSeller smileSeller){
        // 스마일 상품 구매 로직
        // request To SmileSeller
        smileSeller.responseToMember();
    }
}

 추상화된 역할을 두지 않고 각각의 객체와 소통해야 한다면 Member 객체는 각각의 상품을 사는 로직을 모두 가지고 있어야 합니다. 즉, 요청 시에 요청을 받을 객체를 고려해야 하는 것이죠.

 

// 판매자 역할
​
public interface Seller {
​
    abstract void requestTransport();
​
    abstract void responseToMember();
}

 

// 판매자 역할을 구현한 스마일 상품 판매자

public class SmileSeller implements Seller{

    @Override
    public void requestTransport() {
        // 스마일 상품 배송 요청 로직
    }

    @Override
    public void responseToMember() {
        // 스마일 상품 고객 구매 요청 처리 로직
    }
}

 

// 판매자 역할을 구현한 일반 상품 판매자

public class NormalSeller implements Seller{


    @Override
    public void requestTransport() {
        // 일반 상품 배송 로직
    }

    @Override
    public void responseToMember() {
        // 일반 상품 고객 구매 요청 처리 로직
    }
}

 반면 위와 같이 일반/스마일 상품 판매자가 수행해야 할 행동을 추상화한 역할을 만들고, 이를 각 객체가 구현하게 된다면 아래 코드와 같이 Member 객체는 더이상 일반/스마일 상품 판매자를 고려할 필요가 사라집니다. 즉, 객체들간의 협력이 보다 수월해질 수 있다는 뜻이죠.

 

public class Member {
​
​
    public void buyItem(Seller seller){
        // 상품(일반 / 스마일) 구매 로직
        // request To Seller(Normal / Smile)
        seller.responseToMember();
    }
}

 요약하자면 역할(인터페이스)을 둔다는 건 객체가 외부로부터 받을 수 있는 요청 목록을 만드는 것입니다. 요청을 하는 객체는 어떤 방식을 거쳐 응답을 반환하는지 관심이 없습니다. 단지 객체에 요청을 보내고 구체적인 로직은 요청을 받은 객체가 자율적으로 수행한 뒤 응답을 받는 것이죠. 이처럼 추상화된 역할과 자율적인 객체로 구성된 구조는 공동의 목표를 성취하기 위해 객체들이 보다 효율적으로 협력할 수 있는 토대가 됩니다.

 

 지금까지 객체를 식별하는 방법, 객체를 구현하는 방법, 객체들이 협력하는 구조 순으로 간략히 정리해봤습니다. 개인적으로 책을 읽고 객체지향에 대해 조금 이해하고 나니 새롭게 깨닫게 된 것들이 있었는데요. 그 중 하나가 3계층 구조를 구현하는데에서도 객체들이 협력하기 편한 구조를 찾아볼 수 있다는 점입니다. 글의 내용을 바탕으로 3계층 구조의 API를 분석해본다면 생각할 거리들이 많이 생겨날 거라 생각합니다. 

 

 글을 쓰는 과정에서 기존의 API 구조에 대해 의문도 생겼고, 객체 자체에 대해 고민해볼 수 있는 기회여서 좋았던 것 같은데요. 어떤 객체를 만들지, 객체를 어떻게 구현할지 혹은 어떤 구조에서 객체를 사용할지를 고민하시는 분들에게 이 글이 조금이나마 도움이 되었기를 바랍니다.

 

 

References

조용호 저, 『객체지향의 사실과 오해』, 위키북스 (2015)

김동헌 저, 『객체지향 프로그래밍』, e비즈 북스 (2019)

댓글