행동에 집중하는 객체 설계

옹심이·2024년 12월 24일
0
post-thumbnail

시작하며

TDA에 대해 다룬 포스트에서 객체는 자아를 가진 것처럼 동작해야 한다고 하였다. 하지만 TDA 원칙은 객체가 이미 존재하고 있을 때, 수동적인 객체를 능동적으로 바꾸는 방법이다. 그렇다면 아직 정의되지 않은, 설계 단계에서는 어떻게 행동에 집중하는 것이 좋을까

자동차 클래스 만들기

@AllArgsConstructor
class Car1{
	private Frame frame;
	private Engine engine;
	private List<Wheel> wheels;
	...
}

자동차에는 여러 가지 부품이 필요할 것이다. 누군가는 부품에 중점을 두고 다음과 같이 클래스를 설계한다.

이 클래스는 데이터 위주의 사고방식으로 설계되어 필요한 속성들이 정의되어 있다. 이는 절차 지향 언어의 구조체와 다를 것이 없다.

@AllArgsConstructor
class Car2{
	public void drive(){};
	public void changeDirection(float amount){}
	...
}

누군가는 자동차가 어떤 행동을 하는지를 떠올려 클래스를 설계한다. 객체는 객체 간에 행동을 요구하여 협력할 수 있어야한다.

데이터를 객체지향적 사고로 설계하는 것이 목표라면 이 코드가 더 나은 설계가 될 것이다.

반대로 Car1과 Car2의 클래스 이름이 없다고 가정하고 클래스의 설계만 보고 이름을 짓는다면 어떤게 더 자연스러울까? 두 번째 코드의 메서드 이름을 보면 분명 탈 것이라는 생각이 든다.

이처럼 객체가 갖고 있는 속성이 세부 구현보다 행동에 집중할 때 더 자연스러운 모델링이 될 수 있다.

행동과 구현

이전의 자동차 클래스는 미완성된 코드이다. 메서드를 구현하며 클래스를 완성시키자.

class Car{
	private int drive;
	
	public void drive(){};
	
	public void changeDirection(float amount){
		float result = (degree + amount) %360;
		if(result<0)result += 360;
		return result;
	}
	...
}

changeDirection 메서드를 구현하였다. 로직은 크게 어렵지 않다.

그런데 drive라는 못보던 멤버 변수가 생겼다. 메서드를 구현하니 데이터 위주의 사고로 돌아왔다.

이처럼 행동의 구현을 고민하면 클래스가 어떤 속성을 가지고 있어야 하는지 고민할 수 밖에 없게된다.

하지만 데이터 위주의 사고로 돌아오는 것은 좋지 않은 결과이며 이는 구현에 중점을 두고 고민한 결과이다.

행동을 고민하면서 구현이나 알고리즘을 고민하는 것이 아니라 순수하게 클래스에 어떤 동작을 시킬지만 고민하는 것이 좋다. ‘어떻게’는 그 다음 문제이다.

순수한 동작만 생각하라고? 행동을 구현해야하는데 구현을 생각하지 말라니 이게 무슨 소리인가 싶다. 이럴 때 반갑게 등장하는 것이 ‘인터페이스’이다.

인터페이스는 구현 없이도 메서드를 정의할 수 있다. 인터페이스는 오롯이 어떤 행동을 어떻게 시킬 수 있는지만 고민 하면 된다.

그래도 코드가 동작을 해야 프로그램이 완성되는데 정말 구현을 1도 생각 안해도 되는걸까? 초기 설계 단계에서는 상세한 구현은 무시해도 된다. 예를 들면, 자동자는 원하는 곳으로 굴러가기만 하면 되지 내부가 어떻게 구현되어서 어떻게 앞으로 가는지 알 필요 없다.

인터페이스

행동에 집중하여 설계하니 인터페이스로 이어졌지만 행동과 인터페이스는 다르다. 인터페이스는 외부에서 어떤 객체에 행동을 시키고자할 때 메시지를 보낼 수 있는 창구이다.

인터페이스란 ‘나를 조작하고 싶으면 이렇게 메시지를 보내’ 라고 외부에 알려주는 수단이다(따라서 정보 은닉을 위한 private은 사용 불가능하다). 여기서 ‘나’는 객체 또는 시스템이 될 수 있다.

API와 UI 통해 인터페이스의 정의를 이해해보자. API는 어플리케이션을 조작할 때 어떤 메시지를 보내면 되는지 알려주는 것이다. UI는 사용자가 프로그램을 조작하고 싶을 때 어떻게 메시지를 보내면 되는지 알려주는 것이다.

정리하면, 인터페이스는 어떤 행동을 지시하기 위한 행동들의 집합인 것이다. 그리고 객체들은 협력을 할 때 인터페이스를 통해 메시지를 주고 받는다.

이 덕분에 객체 간의 결합도를 낮추고 그 결과 유연성과 확장성을 얻을 수 있다. 그리고 가장 중요한 장점은 행동에 대해 고민할 수 있게 도와준다는 것이다.

행동과 역할

만약 ‘자동차 클래스 만들어줘’라는 부탁을 듣게 되면 자동차라는 단어에 집중하게 되고 이 때문에 데이터 위주의 사고를 하게된다.

따라서 이러한 방식의 요청보다는, ‘탈것 클래스 만들어줘’라는 요청이 맞다. 클래스가 어떤 행동을 가져야 하는지 상상이 가능하도록 요청을 하는 것이 좋다.

이렇게 요청에 따라 다른 사고 방식을 가지게 되는 이유는 전자는 실체이고 후자는 역할이기 때문이다. 실체를 접하면 데이터와 구현에 집중하게 되고 어떤 역할을 요청하는지는 알 수 없기 때문에 개발자 마음대로 구현할 수 밖에 없다.

만약 전자와 같은 질문을 받는다면 역으로 질문하자.

  • 자동차는 어떤 행동을 하는 객체야?
  • 꼭 자동차여야 해?
  • 자동차라는 클래스를 만드는 목표가 뭐야?

이러한 질문들의 대답을 들으면 클라이언트가 어떤 것을 요청하는지 알 수 있게 된다. 그럼 인터페이스를 만들 수 있게된다.

public interface Vehicle{
	void ride();
	void run();
	void stop();
}

우선 역할에 집중하게 되면 유연한 설계를 얻게 된다. 그래서 역할과 구현은 반드시 구분해야 하며, 이 같은 구분을 위한 출발점은 어떤 질문을 하느냐에 달려있다.

만약 전자의 질문대로 개발자가 고민하고 자동차 클래스를 만든다면 어떻게 될지 생각해보자.

  1. 자동차에 필요한 데이터를 위주로 클래스를 구현한다.
  2. 이후에 클라이언트가 원하는 자동차는 탑승이 가능한 자동차임을 알게 된다. 따라서 ride 메서드를 추가한다.
  3. 한참 후에 자전거 클래스를 요청 받는다.
  4. 자동차를 타는 사용자는 자전거도 탈 수 있도록 자동차를 타는 코드를 찾아 자전거에 대한 코드도 추가한다.
class User{
	public ride(String type, Object object){
		if(type=="CAR"){
			((Car)object).ride();
		}
		if(type=="BICYCLE"){
			((Bicycle)object).ride();
		}
	}
}

결과적으로 이렇게 끔직한 코드가 완성된다. 이처럼 구현에 집중한 코드는 확장되는 요구사항에 유연하게 대처할 수 없다. 따라서 역할에 집중하는 사고 방식을 기르고 설계하도록 하자.

메시지를 실행하는 메서드

메서드와 함수는 뭐가 다르길래 클래스에 함수가 들어오면 메서드가 되는 것일까?

우리는 협력 객체에 요청을 보낼 때 메서드나 함수를 실행시킨다고 생각하지만, 인터페이스를 이용한 통신에서는 어떤 메서드가 실행될지 모른다.

class Car implements Vehicle{
	void ride(){};
	void run(){};
	void stop(){};
}

class Bicycle implements Vehicle{
	void ride();
	void run();
	void stop();
}

class User{
	public ride(Vehicle vehicle){
		vehicle.ride(); 
	}
}

User클래스는 어떤 Vehicle 객체에게 ride라는 메시지를 전달할지 알 수 없다. 메서드에서는 실제로 어떤 ride()가 실행될지 알 수 없으며 Vehicle 구현 객체의 종류에 따라 그 결과 값이 다르다. 이러한 부분에서 메서드는 각 입력 값에 정확히 하나의 출력 값으로 대응되는 함수와 다르다고 할 수 있다.

이처럼 객체들은 메시지를 통해 어떠한 행동을 요청한다. 그리고 이를 어떤 메서드(방법)으로 어떻게 처리할지는 객체가 결정한다. 따라서 메서드는 어떤 메시지를 처리해달라는 요청을 받았을 때 이를 어떻게 처리하는지 방법을 서술하는 것이다.

따라서 메서드의 구현에 집중하다보면 어떤 알고리즘으로 문제를 해결할지에 대해 고민하게 되고 이는 결국 어떤 행위를 위한 함수를 정의하는 것으로 이어진다. 그러면 함수에 중심이 되는 절차 지향적 코드가 나온다.

객체 지향에서 가장 중요한 것은 책임을 나눠 정리하고 메시지를 통해 협력 관계를 구축하는 것이다.

정리

개요

이전까지는 객체에게 일을 시키라는 TDA를 공부했지만 이는 객체가 이미 정의된 상태에서 더 유리한 방법이었다. 따라서 도메인을 설계할 때 어떤 사고 방식을 가져야 하는지 공부했다.

키워드 정리

  • 역할 : 객체가 시스템에서 담당하는 책임 또는 역할을 의미한다. 예를 들어, Vehicle이라는 역할을 맡은 객체는 run, ride 등의 요구 사항을 만족 시켜야 한다. 보통 인터페이스로 표현된다.
  • 메서드 : 객체가 메시지를 받았을 때 어떻게 처리할지 정의이다. 메서드 오버라이딩, 인터페이스 구현 등다형성과 연관되어 있다.
  • 행동 : 역할을 수행하기 위해 실제 수행하는 동작. 메서드를 통해 구체화 가능하다.
  • 인터페이스 : 외부에서 어떤 역할을 가지는 객체에게 일을 시키고자 할 때 메시지를 보낼 수 있는 창구이다. 우리는 특정한 역할을 위한 일을 시킬 뿐 객체가 어떤 행동으로 역할을 수행할지 알 수 없다.
  • 메시지 : 객체 간의 협력을 위한 행동 요청이다. 객체는 메시지를 받아 메서드를 수행한다.

설계 절차

  1. 도메인에서 문제를 해결하기 위해 핵심적인 행동을 정리한다.

  2. 누가 무엇을 해야 하는가? 즉 행동에 초점을 맞춰 필요한 역할을 정의한다.

  3. 누가 누구에게 메시지를 보낼 것인가? 즉 객체 간의 협력을 구체화한다.

  4. 정의된 역할에 대해 인터페이스를 설계한다.

    예를 들어, Car라면 구현 세부 사항은 뒤로 미루고 ‘달린다’, ‘멈춘다’, ‘탑승한다’와 같은 행동을 정의한다.

  5. 구현 클래스를 작성한다. 이 때부터 Car 객체가 부여 받은 역할을 하기 위해 필요한 상태(멤버 변수)를 결정한다.

0개의 댓글