객체지향 패러다임이란 ??

tony·2024년 5월 18일

코드 패러다임

목록 보기
4/6

패러다임


패러다임이란 본디 과학분야에서 나온 용어로,

“이미 달성된 성취를 기반으로 새로운 발견을 누적시키거나, 기존의 오류를 수정하면서 단계적으로 진보해나가는 과정”을 의미한다.

프로그래밍이란 문제해결을 목적으로 하는 로직 설계이다.

그렇다면 프로그래밍 패러다임이라는 것은 “문제해결을 목적으로 하는 진보적인 로직 설계에 대한 과정 및 방법론” 쯤 된다는 말이다.

왜 프로그래밍 패러다임을 알아야하는가?


프로그래밍 패러다임은 개발자 공동체가 동일한 프로그래밍 스타일과 모델을 공유할 수 있게 함으로써 불필요한 부분에 대한 의견 충돌을 방지한다.

우리는 혼자 코딩하지 않는다.

우리는 혼자 설계하지 않는다.

모두가 함께 공통의 목적을 위해 설계자로서, 각자의 모듈 구현자로서 맡은 바를 최선을 다할 뿐이다.

같은 배를 몰려면 올바른 방향으로 나아가려해야한다.

올바른 판단을 내리려면, 트레이드오프를 고려하여 상황에 따라 적합한 패러다임 적용이 무엇인지를 알아야한다.

여러 패러다임을 경험해보지 못 한다면, 상황에 적합한 패러다임이 무엇인지를 모를 것이다.

설계에 있어서 이론은 실무보다 중요치 않다.


설계에 관해 설명할 때 가장 유용한 도구는 이론으로 덕지덕지 치장된 개념과 용어가 아니라 ‘코드’ 그 자체다.

“적합한 패러다임”은 “적용된 패러다임이 주어진 상황에 적합하다”가 기저에 깔려있다.

우리는 “주어진 상황” 이라는 단어에 주목해야한다.

왜냐하면 “주어진 상황”은 이론에 등장하지 않기 때문이다. -- 아니, 등장할 수가 없다.

상황이란 100만, 아니 어쩌면 변인에 따라 몇 억만가지의 경우가 발생할 수있다.

이론에서는 이를 다룰 수가 없다.

따라서 실무만큼, 즉 코드 그 자체만큼 중요한 것이 없다.

누구나, 입으로는 훌륭한 설계를 할 수 있다.

하지만 아무나 좋은 코드를 짤 수는 없다.

설계를 제대로 했다면,,,


설계를 제대로 했다면 아래 체크리스트를 만족해야한다.

모듈 설계 체크리스트


  • 모듈은 제대로 실행되어야 하고
  • 변경이 용이해야하고
  • 이해하기 쉬워야 한다

구현 체크리스트


  • 요구사항을 지켜야하고
  • 내일 쉽게 변경할 수 있는 코드

객체지향 패러다임


3장 역할, 책임, 협력 | Incheol's TECH BLOG

객체지향 패러다임에서는 자율적인 객체를 설계하여 변경이 용이한 구현을 지향한다.

그렇다면 자율적인 객체란 무엇일까?

자율적인 객체란,,,

  • 자신의 상태를 직접 관리하고
  • 스스로의 결정에 따라 행동하는 객체이다.

그렇다면 아래와 같은 의문점이 든다.

  • 어떻게 자신의 상태를 직접 관리한다는 걸까?
  • 어떻게 스스로의 결정에 따라 행동한다는 걸까?

각 객체에 자율성을 부여한다.


  • 여기서 자신의 상태라하면, 객체 자신의 내부 멤버를 말한다. 상태를 직접 관리한다는 것은 내부 멤버의 값을 객체만이 접근하겠다는 것이다. 직접, 그러니까 외부의 조작을 허용하지 않겠다는 소리이다. 이는 캡슐화의 내용으로 이어진다.
    public class Bag {
        private Long amount;
        private final Invitation invitation;
        private Ticket ticket;
    }
  • 스스로의 결정에 따른 어떤 행동이라하면, 스스로의 결정이라하면, 어떤 행동(비즈니스 로직)을 할지는 나에 따라(객체 본인) 달려있다는 소리다. 즉, 상태에 따라 로직A를 실행할지, 로직B를 실행할지는 본인 메서드의 구현부 안에 정해져있다는 소리다. ***더 나아가면 팩토리 메서드에 따라 정책을 갈아끼우는 상황도 상상할 수 있다. 😊
    public class Bag {
        private Long amount;
        private final Invitation invitation;
        private Ticket ticket;
    
        public Bag(long amount) {
            this(null, amount);
        }
    		
        public Bag(Invitation invitation, long amount) {
            this.invitation = invitation;
            this.amount = amount;
        }
    
    		/** 내부 멤버에 대한 조작을 "내부적으로만" 처리한다. */
        public Long hold(Ticket ticket) {
            if (hasInvitation()) {
                setTicket(ticket);
                return 0L;
            } else {
                setTicket(ticket);
                minusAmount(ticket.getFee());
                return ticket.getFee();
            }
        }
    
        private boolean hasInvitation() {
            return invitation != null;
        }
        private boolean hasTicket() {
            return ticket != null;
        }
    
        private void setTicket(Ticket ticket) {
            this.ticket = ticket;
        }
        private void minusAmount(long amount) {
            this.amount -= amount;
        }
        private void plusAmount(long amount) {
            this.amount += amount;
        }
    }
    여기서 hold는 Ticket의 상태에 따라 행동A,행동B로 나뉜다. 이 행동을 바꾸는 결정권자는 Bag 본인이다. Ticket에 따라 로직이 바뀌는 hold는 Bag의 메서드이기 때문이다. 따라서 “스스로 결정한다”라고 말할 수 있을 것이다.

캡슐화를 지향한 클래스 설계


클래스를 설계할 때는, 어디까지 외부 객체의 간섭이 들어갈 수 있는지에 대해 주의해야한다.

지나친 허용은 객체를 수동적이게 만들고, 이러한 의존관계는 결합도가 높아 변경 용이하지 못 하기 때문이다.

따라서 메세지에 따라 객체가 상태를 변경할 수 있도록 캡슐화를 해주어야한다.

B객체가 A객체의 인스턴스 변수에 직접 변경해서 값을 변경 ❌

B객체에서 메세지를 날려주면 A객체가 스스로 상태를 변경 ✅

// 캡슐화 ❌
public class Theater {
	private final TicketSeller ticketSeller;
	

	// 잘못된 코드 Case#1
	// Theater가 TicketSeller의 인스턴스 변수 ticketOffice에 직접 접근
	public void enter(Audience audience){
		ticketSeller.getTicketOffice().getTicket();
        
		...
    }
}

public class TicketSeller {
	public TicketOffice ticketOffice;
	...
	private TicketOffice getTicketOffice(){
        return this.ticketOffice;
    }
}
// 캡슐화 ✅ 
public class Theater {
	private final TicketSeller ticketSeller;

	public void enter(Audience audience){
		ticketSeller.sellTo(audience);

		...
    }
}

public class TicketSeller {
	private TicketOffice ticketOffice;
	...
	
	public void sellTo(Audience audience){
		// 티켓 구매 프로세스
		
	}
}

객체의 의인화


그런데 현실 세계의 객체가 가상 세계의 객체로 넘어오면 수동적 존재였던 객체들은 모두 능동적으로 바뀌게 되고 그 객체에 생명이 깃든다. 그래서 현실 세계에서는 인간이라는 행동주체가 있어야 동작할 수 있던 객체들이 행동주체가 없이도 혼자서 동작할 수 있다.

객체지향의 목표; 오늘은 오류 없이 잘 동작하고 내일은 쉽게 변경할 수 있는 코드

앞서 언급했다싶이, 적절한 패러다임은 구현에 충실함과 동시에 변경이 용이해야한다.

객체지향 패러다임에서는 “책임/역할 부여” 라는 방법을 통해 이를 처리한다.

핵심은 객체에 대해 자율성을 부여하는 것이다.

즉, 객체,모듈에 적절한 책임/역할을 할당하고 변경하기 용이하게끔한다.

  • 책임이란 객체가 무엇을 알고있는가, 무엇을 할 수 있는가를 부여하는 것이다.

  • 협력이란 이렇게 정해진 객체들 간의 상호작용이다.

  • 역할이란, 각 객체에게 주어진 책임을 이행하는 것이다.

각각의 단어와 정의만을 보고만 있자니 머리가 아프고 이해가 안 될 수 있다.

설계는 이론보다 실무다.

실제 코드를 살펴보자.

가령 매표소, 직원, 고객, 가방, 티켓이 있다고 하자.

이에 대해 현실세계에서는 이렇게 동작할 것이다.

  1. 매표소에 도착한다.
  2. 고객은 가방에서 티켓을 꺼낸다.
  3. 고객은 꺼낸 티켓을 매표소에 건낸다.
  4. 매표소에서는 티켓을 받아 이를 직원이 확인한다.

이에 따라 아래와 같이 구현될 수 있다.

public class Theater {
	private TicketSeller ticketSeller;

	public Theater(TicketSeller ticketSeller) {
			this.ticketSeller = ticketSeller;
	}

	public void enter(Audience audience) {
			if (audience.getBag().hasInvitation()) {
					Ticket ticket = ticketSeller.getTicketOffice().getTicket();
					audience.getBag().setTicket(ticket);
			} else {
					Ticket ticket = ticketSeller.getTicketOffice().getTicket();
					audience.getBag().minusAmount(ticket.getFee());
					ticketSeller.getTicketOffice().plusAmount(ticket.getFee());
					audience.getBag().setTicket(ticket);
			}
	}
}

하지만 이렇게 설계하게 된다면 의존성의 영역이 범잡을 수 없이 커지게 된다.

요컨대, Bag와 Ticket 에 대해 Theater 와 Audience, 어쩌면 나중에 구현될 직원 객체인 TheaterManager 또한 알게된다.

그렇다면 구현로직이 파편화될 것이고, 수정하기 굉장히 어렵게 된다.

그렇다면 어떻게 바꾸어 주어야 할까?

이렇게 바꾸어준다면 의존성을 낮추어 변경하기 용이하게 바꿀 수 있다.

public class Theater {
    private TicketSeller ticketSeller;

    public Theater(TicketSeller ticketSeller) {
        this.ticketSeller = ticketSeller;
    }

    public void enter(Audience audience) {
        ticketSeller.sellTo(audience);
    }
}
public class TicketSeller {
    private TicketOffice ticketOffice;

    public TicketSeller(TicketOffice ticketOffice) {
        this.ticketOffice = ticketOffice;
    }

    public void sellTo(Audience audience) {
        ticketOffice.sellTicketTo(audience);
    }
}
public class Audience {
    private Bag bag;

    public Audience(Bag bag) {
        this.bag = bag;
    }

    public Long buy(Ticket ticket) {
        return bag.hold(ticket);
    }
}
public class Bag {
    private Long amount;
    private Ticket ticket;
    private Invitation invitation;

    public Long hold(Ticket ticket) {
        if (hasInvitation()) {
            setTicket(ticket);
            return 0L;
        } else {
            setTicket(ticket);
            minusAmount(ticket.getFee());
            return ticket.getFee();
        }
    }

    private void setTicket(Ticket ticket) {
        this.ticket = ticket;
    }

    private boolean hasInvitation() {
        return invitation != null;
    }

    private void minusAmount(Long amount) {
        this.amount -= amount;
    }
}

위 코드를 살펴보면 아래와 같은 질문이 생길 수 있다.

“왜 Bag 객체가 티켓이 있는지를 확인할까?? 현실과는 부합하지 않는 로직 아닌가”

이 질문을 바꾸자면 이렇게 바꿀 수 있다.

“왜 현실에서는 수동적인-움직일 수 없는 사물인-Bag 객체가 티켓을 확인하게끔 할까”

왜냐하면 Bag 는 본인의 내부상태값인 Ticket 을 관리할 책임과 역할이 있기 때문이다.

그렇기에 Bag 는 자율적으로 이에 대한 로직 수행을 이행할 역할을 수행하는 것이다.

정리하자면 Bag, Ticket, Audience 에 대한 책임,협력 구조도를 아래와 그릴 수 있을 것이다.

객체지향 패러다임을 지향한다면, 현실세계에서 우리가 생각하는 프로세스와 달리 구현되어야할 때가 있다.

하지만 자율성을 부여함으로써 아래와 같은 프로세스를 통해 확장성을 높일 수 있다.

  • 적절한 책임,역할 분리 ⬆️
    • 적절한 캡슐화 ⬆️
      • 적절한 응집도 ⬆️
        • 코드 변경성 & 확장성 ⬆️

Reference


23년 9월 원티드 유연한 코드 설계 세션 내용 정리를 많이 참고하였다.

그 때 굉장히 재밌게 들었는데,,,,

유연한 코드 설계 2강 :: 자율적인 객체

GitHub - eternity-oop/object: 오브젝트: 코드로 이해하는 객체지향 설계 예제

profile
블로그 이전했습니다 -- https://vanillacake369.github.io

2개의 댓글

comment-user-thumbnail
2024년 5월 18일

잘 읽었습니다. 좋은 글이네요.

답글 달기
comment-user-thumbnail
2024년 5월 18일

너무 좋은 글입니다 잘읽었습니다

답글 달기