Chapter 2 이후로는 멈췄었지만, 책은 다 읽었다. 현생에 치이다 보니 이제야 정리를 올린다.
책이 정말 좋아서 배운 것도 많기에, 그냥 넘어가면 조만간 까먹을 거 같은 이슈로 꾸역꾸역 정리를 해본다. (내 머리는 RAM이다. 휘발성 메모리..)
Chapter 3. 행동
객체는 단순한 데이터 덩어리가 아니며 마치 자아를 가진 것처럼 동작해야 한다. 즉, 객체가 데이터로서 존재하는 것이 아니라 '행동'하는 것이 중요하다는 것을 의미한다. 그렇다면 객체 행동해야 한다는 것은 어떤 의미이며, 어떻게 해야 객체를 행동하게 만들 수 있을까?
가장 쉽게 객체를 행동하게 만드는 방법은 TDA 원칙을 적용하는 것이다. TDA 원칙은 객체를 행동하게 만든다. TDA의 묻지 말고 시켜라 라는 말 자체가 객체에 어떤 행동을 하라고 조언하는 것이기 때문이다. 따라서 속성을 가진 객체가 있을 때 사용하기 좋은 원칙이다. 그런데 TDA 원칙은 수동적인 객체를 능동적인 객체로 바꾸는 방법이다. 따라서 객체가 이미 존재한다는 전제로 사용한다.
자동차 클래스를 만들어 줄 수 있나요?
1. 속성을 떠올린다. (private Frame frame, privaet List <Wheel weels>...)
2. 행동을 떠올린다. (public void drive(), public void accelerate(long speed)...)
1번은 데이터 위주의 사고방식으로 클래스에 필요한 속성부터 정의한다. 2번은 행동 위주의 사고방식으로 객체가 어떤 동작을 하는지 알 수 있다. 분명 클래스를 만드는 목적에는 둘 다 포함된다. 따라서 뭐가 더 좋은 코드라고 단정 지을 수는 없다. 하지만 객체지향 관점에서 객체는 서로 협력해야 하기 때문에 적어도 2번이 1번보다 더 객체지향적으로 나은 코드이다. 협력을 하려면 상대방에게 행동을 요구할 수 있어야 한다.
행동과 역할
class ??? {
private float speed;
private float direction;
}
class ??? {
public void ride() {
// do something
}
public void run() {
// do something
}
public void stop() {
// do something
}
}
위 코드처럼 데이터를 보고 클래스 이름을 지을 때, 속도와 방향만을 가지고 이름을 짓는 것은 어렵다. (사람, 새, 자동차.. 무수히 많다.) 반면 행동을 보고 클래스 이름을 생각해 보자면 Vehicle과 같이 타는 것 정도로 정의할 수 있다.
이처럼 행동을 고민했더니 Car, Bicycle 같은 구체적인 이름보다 Vehicle(탈 것)이라는 역할이 나왔다. 행동은 자연스럽게 역할을 고민하게 만든다. 따라서 어떤 행동을 할 수 있을지가 곧 역할을 만든다. 그리고 이러한 여러 역할이 모여 객체를 정의한다.
3.1 덕 타이핑
덕 테스트: 만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
덕 타이핑의 유래는 덕 테스트에서 유래했다. 개발자 관점에서 보면 행동이 같다면 같은 클래스로 부르겠다는 의미이다.
실제 타입스크립트에서 Duck와 UnknownBird라는 두 클래스가 속성은 다르지만 모두 같은 메서드를 가지고 있을 때, 두 클래스가 따로 상속 관계를 맺지 않아도 서로 할당할 수 있다. const duck: Duck = new UnknownBird(); 이는 타입스크립트가 행동이 같은 두 클래스를 같은 클래스로 보겠다는 덕 타이핑의 개념을 지원하기 때문이다. 행동이 곧 역할을 정의하고 역할이 곧 객체를 정의한다.
3.2 행동과 구현
행동이 중요하고 행동 위주의 사고를 해야 한다는 사실을 알았다. 하지만 이게 전부는 아니다.
public class Car {
private int degree; // 자동차의 각도 -> 구현을 고민하니 속성이 생김
public void drive() {}
public void changeDirection(float amount) {
float result = (degree+amount) % 360;
if (result<0) {
result += 360
}
return result;
}
}
이처럼 메서드를 구현하려고 하니 데이터 위주의 사고로 다시 돌아왔다. 행동의 구현을 고민하면 결국 이 클래스가 어떤 값을 가지고 있어야 하는지를 고민하게 된다. 그리고 이것은 데이터 위주의 사고로 되돌아가는 것이다.
이런 현상이 일어난 이유는 구현을 고민했기 때문이다. 행동을 고민하면서 구현을 고민해서는 안된다. 행동을 고민하는 순간에는 순수하게 이 클래스에 어떤 동작을 시킬 수 있을 것인지만 고민하는 것이 좋다.
그렇다면 어떻게 해야 구현에 구애받지 않고 행동을 고민할 수 있을까?
바로 인터페이스이다. 자바의 interface를 활용하면 구현 없이도 메서드를 정의할 수 있다. 인터페이스에는 오롯이 어떤 행동을 어떻게 시킬 수 있는지만 선언할 수 있다. (물론 default, static의 경우 구현이 있을 수는 있다.)
정말 구현은 신경 쓰지 않아도 되나?
초기 설계 단계에서는 상세한 구현은 무시해도 괜찮다. 상세한 구현은 구현체를 개발하는 사람이 협의한 요구사항에 맞춰 알아서 개발할 것이다. 우리는 하기로 한 것을 제대로 했는지만 평가하면 된다. 구현에 욕심을 부린다면 처음부터 끝까지 결국 모든 프로그램을 다 만들어야 한다. 남이 할 거니 상관없다는 자세가 무책임한 것이 아니다. 그걸 구현해 줄 사람을 믿는 것이다. 그 사람이 인터페이스를 잘 지켜오지 않은 경우엔 문제가 발생하지만 그럴 경우는 어떻게 해야 하나 가 아니라 그래서는 안된다. 그래서 인터페이스는 곧 계약이다라는 말이 있는 것이다. 계약이 무너지면 시스템이 무너지기에 계약을 잘 지켜왔는지 확인할 필요가 있고 감시할 필요가 있다. 그리고 이럴 때 사용할 수 있는 것이 바로 테스트이다.
인터페이스와 메서드 구현
자바 인터페이스에 본문이 들어간 메서드를 넣는 것은 인터페이스 탄생 목적에 반하는 행동이다. 자바의 인터페이스는 구현과는 독립된 역할에 집중하겠다는 선언이다. 따라서 인터페이스에 본문이 들어가는 메서드가 있는 것은 설계가 무언가 잘못됐다는 신호이다.
3.3 인터페이스
인터페이스와 행동은 다르다. 행동에 집중하다 보니 인터페이스에 관한 설명으로 이어졌지만 행동이 곧 인터페이스는 아니다. 이 둘은 구분할 필요가 있다. 인터페이스는 어떤 행동을 지시하는 방법의 집합 정도로 표현할 수 있다.
인터페이스란 '나를 조작하고 싶다면 이런 메시지를 보내면 된다'라고 외부에 알려주는 수단이다.
말 그대로 인터페이스는 외부에 나를 사용하는 방법을 알려주는 것과 같다. 여기서 '나'는 객체가 될 수도 있고, 시스템이 될 수도 있다.
대표적인 인터페이스로 API(Application Programming Interface)가 있다. API는 애플리케이션을 조작하고 싶을 때 어떻게 메시지를 보내면 되는지 알려주는 것이다. UI(User Interface)는 사용자가 프로그램을 조작하고 싶을 때 어떻게 메시지를 보내면 되는지 알려주는 것이다. 이와 마찬가지로 자바의 인터페이스는 어떤 객체를 어떻게 사용하면 되는지 외부 객체에게 알려주는 것이다.
즉, 인터페이스는 어떤 행동을 지시하기 위해 사용할 수 있는 행동들의 집합이다. 객체들끼리 서로 협력하려면 행동을 지시해야 하고, 필요에 따라 물어보기도 해야 한다. 인터페이스는 협력을 위한 창구이며, 협력을 위해 객체들은 인터페이스를 통해 메시지를 주고받는다.
여기서 두 가지 사실이 이해될 것이다.
1. 자바 인터페이스는 private 선언이 불가능하다.
2. 인터페이스의 메서드에 public을 지정하면 IDE에서 public으로 선언할 필요가 없다고 안내한다. 왜냐하면 인터페이스는 지시자가 없어도 public이 기본이다.
당연하다. interface는 외부 세계에 나를 다루는 방법을 알려주는 것이기 때문에 private인 게 이상하다.
객체들이 인터페이스를 통해 통신하면 객체 간의 결합도를 낮출 수 있다. 더불어 유연성과 확장성을 얻을 수 있다. 인터페이스를 사용하는 코드는 재사용성도 높아지고 유지보수성도 올라간다. 모듈화도 가능해진다. 이외에도 인터페이스를 사용하는 코드의 이론적인 장점은 많다. 하지만 가장 중요한 것은 인터페이스는 행동과 역할을 고민할 수 있게 도와준다는 것이다. 그러므로 인터페이스는 중요하다.
3.4 행동과 역할
사실 '자동차 클래스를 만들어 줄 수 있나'라는 요청은 데이터 중심적 사고를 유도하는 질문이다. 이런 유형의 질문은 요청부터 적절하지 않다. 자동차라는 용어 자체가 데이터 위주의 사고를 유발하기 때문이다. 자동차는 역할보다는 구현에 가까운 용어이다.
탈것 클래스를 만들어 줄 수 있나요?
탈 것이라 하면 필요한 데이터를 생각하기보다 탑승하고, 달리고, 멈추고 와 같은 행동에 대한 정의를 먼저 할 수 있다. 왜냐하면 자동차는 실체이고 탈것은 역할이기 때문이다. 우리는 실체에 집중할 때 데이터 위주의 사고를 하고 역할에 집중할 때 행동 위주의 사고를 한다. 실체는 곧 구현이다. 구현을 고민하는 순간 데이터 위주의 사고를 촉진한다.
또한, 실체만으로는 어떤 역할을 하는 클래스인지 알 수 없다. 누군가에게 자동차는 탈것이지만, 누군가에겐 관상용, 누군가에겐 집이 될 수도 있다. 즉, 역학을 고민하지 않고 구현체에 집중하면 해당 클래스를 개발하는 개발자만의 생각이 반영된다.
따라서 자동차 클래스를 만들어 줄 수 있나요? 와 같은 질문엔
- 자동차는 어떤 행동을 하는 객체인가요?
- 꼭 자동차이어야 하나요?
- 자동차라는 클래스를 만들어서 달성하려는 목표가 뭔가요?
와 같은 질문을 한다면 좋을 것이다.
탑승할 수 있고, 달릴 수 있으면 좋겠어요.
와 같은 대답이 온다면 필요한 것이 Vehicle이라는 것을 알게 된다. 이처럼 역할에 집중하면 훨씬 유연한 설계를 할 수 있다. 따라서 역할과 구현은 반드시 구분해야 하며, 구분을 위한 출발점은 어떤 질문을 하느냐이다. 우리 사고 흐름은 사소한 단어에도 영향을 받기 때문에 이름 짓는 것 역시 역할과 구현을 구분하는 데 큰 영향을 준다.
구현에 집중한 코드는 확장되는 요구사항에 유연하게 대처할 수 없다. 역할에 집중해야 유연한 설계를 얻을 수 있다. 하지만 우린 구현체를 먼저 상상하는 데 익숙하다. 따라서 역할에 집중하는 사고방식을 익히기 위해서는 꾸준한 노력이 필요하다. 반복적이고 의식적으로 객체가 어떤 행동을 해야 하는지 고민해야 한다. 더 나아가 어떤 행동들을 모아 구조적으로 타당한 역할을 만들 수 있을지 계속해서 고민해야 한다.
행동과 역할에 집중하라는 것은 추상화를 많이 하라는 뜻은 아니다. 역할과 추상은 같은 말이 아니기 때문이다.
3.5 메서드
메서드는 왜 메서드 일까? '방법'을 뜻하는 메서드와 함수(function)에는 어떤 차이점이 있을까?
인터페이스란 '나를 조작하고 싶다면 이런 메시지를 보내면 된다'라고 외부에 알려주는 수단이다.
즉, 어떤 객체나 시스템을 다루는 외부 세계는 협력 대상과 소통하기 위해 메시지를 이용한다. 객체는 협력 객체에 메시지를 건네고, 협력 객체는 메시지를 수신해서 행동한다. 우리는 어떤 객체가 협력 객체에 요청을 보낼 때 어떤 특정 메서드나 함수를 실행한다고 생각한다. 하지만 그렇지 않다. 실제로는 인터페이스를 이용한 통신에서는 메시지를 보내면서도 어떤 메서드가 실행될지 모른다.
class Car implements Vehicle {
void ride() {}
}
class Bicycle implements Vehicle {
void ride() {}
}
class User {
void ride(Vehicle vehicle) {
vehicle.ride();
// 실제 코드가 실행되기 전까지 Car인지 Bicycle인지 모른다.
}
}
위 코드에서도 User의 ride()가 실행되기 전까지 어떤 메서드가 호출될지 모른다.
함수는 입력값(input)과 출력값(output) 사이의 대응 관계를 나타낸다. 함수의 입력값은 정확히 하나의 출력값으로 대응된다.
여기서 함수의 특징은 '함수의 각 입력값은 정확히 하나의 출력값으로 대응된다'이다. 함수는 같은 입력에 대해 항상 같은 출력을 해야 한다. 즉, 같은 입력에 대해 두 개의 출력을 갖는 함수가 있어서는 안 된다.
따라서 함수를 실행하면서도 실제로 어떤 함수가 실행될지 모른다는 말은 부적절하다. 함수를 실행한다는 것은 고정적이고 특정하게 지정된 절차를 수행한다는 의미라서 그렇다. 따라서 어떤 클래스의 어떤 메서드가 실행될지 모르면서 함수를 실행한다고 표현하는 것은 불가능하다. 그리고 이는 객체지향에서 추구하는 방향과 반대된다. 객체지향에서는 특정한 구현에 의존하는 상황을 피하고자 한다. 그래서 객체지향에서는 협력 객체에 어떤 일을 요청할 때 '함수를 실행한다'라는 말 보다 '메시지를 전달한다'라고 표현한다.
객체지향에서 객체들은 메시지를 통해 소통한다. 즉. 객체는 협력 객체에 메시지만 보낼 뿐 실제로 어떤 방법(method)으로 일을 어떻게 처리할지는 객체가 정한다. 그래서 객체지향에서는 객체가 수행하는 함수를 메서드라고 부른다. 메서드란 어떤 메시지를 처리해 달라는 요청을 받았을 때 이를 어떻게 처리하는지 방법(method)을 서술하는 것이다. 따라서 메서드를 어떻게 구현할지에 대해 집중하는 것은 좋은 방법이 아니다. 메시지란 결국 어떤 메시지를 어떻게 처리할지를 서술하는 것이므로 알고리즘에 가까운 것이다. 알고리즘을 고민하면 결국 구현에 집착하고, 그럼 메서드를 작성하는 것이 함수를 작성하는 것과 다를 바가 없어지고, 절차지향적인 코드가 나오게 된다.
객체지향에서 정말 중요한 것은 책임을 나누고 메시지를 통해 협력 관계를 구축하는 것이다.
명시적 입력과 암묵적 입력
함수의 입력에는 명시적 입력과 암묵적 입력이 있다. 명시적 입력은 매개변수를 통해 전달된 값이고, 암묵적 입력은 전역 변수, 클래스의 인스턴스 변수를 참조할 때, 네트워크 상황처럼 코드 밖에 있지만 함수 동작에 영향을 주는 요소다.
암묵적 입력이 많을수록 예측이 어려워지고 테스트도 복잡해진다. 그래서 같은 입력에 항상 같은 출력을 주고, 외부 상태에 의존하지 않는 순수 함수가 이상적인 형태로 여겨진다. 이 원칙은 객체에도 적용된다. 상태를 바꾸지 않는 불변 객체와 순수 함수 중심의 사고방식이 바로 함수형 프로그래밍이다.
Chapter3을 마치며
Chapter1에서는 객체는 책임을 어떻게 나누고, 어디에 할당하는지가 중요했다면, 그게 이어져서 결국 객체가 그 책임을 어떻게 행동으로 실현할 것인가, 그리고 행동이 어떻게 모여서 역할이 되는가를 생각해 보는 것이 중요하다. (여러 역할이 모여 객체를 정의하기 때문이다.)
'객체는 자아를 가진 존재처럼 행동해야 한다'는 말은 단순히 감성적인 표현이 아니라, 객체가 데이터를 가지는 것에서 끝나지 않고, 그 데이터를 바탕으로 스스로 로직을 실행해야 한다는 설계적 요구였다.
이러한 사고의 전환은 결국 TDA(Tell, Don’t Ask), 인터페이스, 역할과 구현 분리, 메서드와 메시지의 구분으로 이어지고, 객체 간의 유연하고 책임 있는 협력을 중심으로 한 설계로 발전하게 도와준다.
돌이켜보면 나도 '어떤 행동을 하는 객체인지', '달성하려는 목표가 무엇인지'와 같은 질문을 하면서 개발을 해왔던 것 같지만, 결국엔 내 머릿속은 철저히 데이터 중심의 사고에 박혀 있던 사람이었다.
아마도 ERD를 먼저 설계하며, 데이터를 먼저 다루는 방식에 익숙해지다 보니 편리함을 우선시하며 데이터 중심이 자연스럽게 굳어져 버린 게 아닐까 싶다. 혹은 객체지향을 '제대로' 배우지 못했던 것이 근본적인 원인일지도 모르겠다.
사실, 데이터 중심으로 개발하면 작은 규모의 프로젝트에서는 빠르고 효율적으로 기능을 구현할 수 있다. 하지만 그 익숙한 방식에 머물러 있던 나는 유연성도 없고, 확장성도 고려되지 않은 절차지향적인 코드만 계속 반복해 왔던 것 같다.
이제는 행동을 기반으로 객체를 바라보고, 객체의 책임과 역할을 고려하며, 협력을 통해 시스템을 설계하려 노력해야겠다. 그동안 익숙했던 사고방식에서 벗어나 단순히 유지보수성과 확장성에 강한 구조만을 쫓는 것이 아니라, 객체의 본질과 책임을 중심으로 사고하는 개발자가 목표이다^^ 물론, 그전까지도 그렇게 해왔다고 생각해서 고민도 많이 해봐야 할 듯..ㅎㅎ
실제로 최근 새로운 프로젝트 리팩토링을 진행하면서 좀 고민하게 된 것들을 조만간 포스팅 공유하러 다시 돌아오겠다,, 그전에 Part 14까지 남았는데 열심히 써볼게여🫨
'책책책 책을 읽어요 > 자바,스프링 개발자를 위한 실용주의 프로그래밍' 카테고리의 다른 글
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 5. 순환 참조 (0) | 2025.04.15 |
---|---|
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 4. SOLID (0) | 2025.04.08 |
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 2. 객체의 종류 (0) | 2025.03.16 |
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 1. 절차지향과 비교하기 (2) | 2025.03.15 |
Chapter 2 이후로는 멈췄었지만, 책은 다 읽었다. 현생에 치이다 보니 이제야 정리를 올린다.
책이 정말 좋아서 배운 것도 많기에, 그냥 넘어가면 조만간 까먹을 거 같은 이슈로 꾸역꾸역 정리를 해본다. (내 머리는 RAM이다. 휘발성 메모리..)
Chapter 3. 행동
객체는 단순한 데이터 덩어리가 아니며 마치 자아를 가진 것처럼 동작해야 한다. 즉, 객체가 데이터로서 존재하는 것이 아니라 '행동'하는 것이 중요하다는 것을 의미한다. 그렇다면 객체 행동해야 한다는 것은 어떤 의미이며, 어떻게 해야 객체를 행동하게 만들 수 있을까?
가장 쉽게 객체를 행동하게 만드는 방법은 TDA 원칙을 적용하는 것이다. TDA 원칙은 객체를 행동하게 만든다. TDA의 묻지 말고 시켜라 라는 말 자체가 객체에 어떤 행동을 하라고 조언하는 것이기 때문이다. 따라서 속성을 가진 객체가 있을 때 사용하기 좋은 원칙이다. 그런데 TDA 원칙은 수동적인 객체를 능동적인 객체로 바꾸는 방법이다. 따라서 객체가 이미 존재한다는 전제로 사용한다.
자동차 클래스를 만들어 줄 수 있나요?
1. 속성을 떠올린다. (private Frame frame, privaet List <Wheel weels>...)
2. 행동을 떠올린다. (public void drive(), public void accelerate(long speed)...)
1번은 데이터 위주의 사고방식으로 클래스에 필요한 속성부터 정의한다. 2번은 행동 위주의 사고방식으로 객체가 어떤 동작을 하는지 알 수 있다. 분명 클래스를 만드는 목적에는 둘 다 포함된다. 따라서 뭐가 더 좋은 코드라고 단정 지을 수는 없다. 하지만 객체지향 관점에서 객체는 서로 협력해야 하기 때문에 적어도 2번이 1번보다 더 객체지향적으로 나은 코드이다. 협력을 하려면 상대방에게 행동을 요구할 수 있어야 한다.
행동과 역할
class ??? {
private float speed;
private float direction;
}
class ??? {
public void ride() {
// do something
}
public void run() {
// do something
}
public void stop() {
// do something
}
}
위 코드처럼 데이터를 보고 클래스 이름을 지을 때, 속도와 방향만을 가지고 이름을 짓는 것은 어렵다. (사람, 새, 자동차.. 무수히 많다.) 반면 행동을 보고 클래스 이름을 생각해 보자면 Vehicle과 같이 타는 것 정도로 정의할 수 있다.
이처럼 행동을 고민했더니 Car, Bicycle 같은 구체적인 이름보다 Vehicle(탈 것)이라는 역할이 나왔다. 행동은 자연스럽게 역할을 고민하게 만든다. 따라서 어떤 행동을 할 수 있을지가 곧 역할을 만든다. 그리고 이러한 여러 역할이 모여 객체를 정의한다.
3.1 덕 타이핑
덕 테스트: 만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
덕 타이핑의 유래는 덕 테스트에서 유래했다. 개발자 관점에서 보면 행동이 같다면 같은 클래스로 부르겠다는 의미이다.
실제 타입스크립트에서 Duck와 UnknownBird라는 두 클래스가 속성은 다르지만 모두 같은 메서드를 가지고 있을 때, 두 클래스가 따로 상속 관계를 맺지 않아도 서로 할당할 수 있다. const duck: Duck = new UnknownBird(); 이는 타입스크립트가 행동이 같은 두 클래스를 같은 클래스로 보겠다는 덕 타이핑의 개념을 지원하기 때문이다. 행동이 곧 역할을 정의하고 역할이 곧 객체를 정의한다.
3.2 행동과 구현
행동이 중요하고 행동 위주의 사고를 해야 한다는 사실을 알았다. 하지만 이게 전부는 아니다.
public class Car {
private int degree; // 자동차의 각도 -> 구현을 고민하니 속성이 생김
public void drive() {}
public void changeDirection(float amount) {
float result = (degree+amount) % 360;
if (result<0) {
result += 360
}
return result;
}
}
이처럼 메서드를 구현하려고 하니 데이터 위주의 사고로 다시 돌아왔다. 행동의 구현을 고민하면 결국 이 클래스가 어떤 값을 가지고 있어야 하는지를 고민하게 된다. 그리고 이것은 데이터 위주의 사고로 되돌아가는 것이다.
이런 현상이 일어난 이유는 구현을 고민했기 때문이다. 행동을 고민하면서 구현을 고민해서는 안된다. 행동을 고민하는 순간에는 순수하게 이 클래스에 어떤 동작을 시킬 수 있을 것인지만 고민하는 것이 좋다.
그렇다면 어떻게 해야 구현에 구애받지 않고 행동을 고민할 수 있을까?
바로 인터페이스이다. 자바의 interface를 활용하면 구현 없이도 메서드를 정의할 수 있다. 인터페이스에는 오롯이 어떤 행동을 어떻게 시킬 수 있는지만 선언할 수 있다. (물론 default, static의 경우 구현이 있을 수는 있다.)
정말 구현은 신경 쓰지 않아도 되나?
초기 설계 단계에서는 상세한 구현은 무시해도 괜찮다. 상세한 구현은 구현체를 개발하는 사람이 협의한 요구사항에 맞춰 알아서 개발할 것이다. 우리는 하기로 한 것을 제대로 했는지만 평가하면 된다. 구현에 욕심을 부린다면 처음부터 끝까지 결국 모든 프로그램을 다 만들어야 한다. 남이 할 거니 상관없다는 자세가 무책임한 것이 아니다. 그걸 구현해 줄 사람을 믿는 것이다. 그 사람이 인터페이스를 잘 지켜오지 않은 경우엔 문제가 발생하지만 그럴 경우는 어떻게 해야 하나 가 아니라 그래서는 안된다. 그래서 인터페이스는 곧 계약이다라는 말이 있는 것이다. 계약이 무너지면 시스템이 무너지기에 계약을 잘 지켜왔는지 확인할 필요가 있고 감시할 필요가 있다. 그리고 이럴 때 사용할 수 있는 것이 바로 테스트이다.
인터페이스와 메서드 구현
자바 인터페이스에 본문이 들어간 메서드를 넣는 것은 인터페이스 탄생 목적에 반하는 행동이다. 자바의 인터페이스는 구현과는 독립된 역할에 집중하겠다는 선언이다. 따라서 인터페이스에 본문이 들어가는 메서드가 있는 것은 설계가 무언가 잘못됐다는 신호이다.
3.3 인터페이스
인터페이스와 행동은 다르다. 행동에 집중하다 보니 인터페이스에 관한 설명으로 이어졌지만 행동이 곧 인터페이스는 아니다. 이 둘은 구분할 필요가 있다. 인터페이스는 어떤 행동을 지시하는 방법의 집합 정도로 표현할 수 있다.
인터페이스란 '나를 조작하고 싶다면 이런 메시지를 보내면 된다'라고 외부에 알려주는 수단이다.
말 그대로 인터페이스는 외부에 나를 사용하는 방법을 알려주는 것과 같다. 여기서 '나'는 객체가 될 수도 있고, 시스템이 될 수도 있다.
대표적인 인터페이스로 API(Application Programming Interface)가 있다. API는 애플리케이션을 조작하고 싶을 때 어떻게 메시지를 보내면 되는지 알려주는 것이다. UI(User Interface)는 사용자가 프로그램을 조작하고 싶을 때 어떻게 메시지를 보내면 되는지 알려주는 것이다. 이와 마찬가지로 자바의 인터페이스는 어떤 객체를 어떻게 사용하면 되는지 외부 객체에게 알려주는 것이다.
즉, 인터페이스는 어떤 행동을 지시하기 위해 사용할 수 있는 행동들의 집합이다. 객체들끼리 서로 협력하려면 행동을 지시해야 하고, 필요에 따라 물어보기도 해야 한다. 인터페이스는 협력을 위한 창구이며, 협력을 위해 객체들은 인터페이스를 통해 메시지를 주고받는다.
여기서 두 가지 사실이 이해될 것이다.
1. 자바 인터페이스는 private 선언이 불가능하다.
2. 인터페이스의 메서드에 public을 지정하면 IDE에서 public으로 선언할 필요가 없다고 안내한다. 왜냐하면 인터페이스는 지시자가 없어도 public이 기본이다.
당연하다. interface는 외부 세계에 나를 다루는 방법을 알려주는 것이기 때문에 private인 게 이상하다.
객체들이 인터페이스를 통해 통신하면 객체 간의 결합도를 낮출 수 있다. 더불어 유연성과 확장성을 얻을 수 있다. 인터페이스를 사용하는 코드는 재사용성도 높아지고 유지보수성도 올라간다. 모듈화도 가능해진다. 이외에도 인터페이스를 사용하는 코드의 이론적인 장점은 많다. 하지만 가장 중요한 것은 인터페이스는 행동과 역할을 고민할 수 있게 도와준다는 것이다. 그러므로 인터페이스는 중요하다.
3.4 행동과 역할
사실 '자동차 클래스를 만들어 줄 수 있나'라는 요청은 데이터 중심적 사고를 유도하는 질문이다. 이런 유형의 질문은 요청부터 적절하지 않다. 자동차라는 용어 자체가 데이터 위주의 사고를 유발하기 때문이다. 자동차는 역할보다는 구현에 가까운 용어이다.
탈것 클래스를 만들어 줄 수 있나요?
탈 것이라 하면 필요한 데이터를 생각하기보다 탑승하고, 달리고, 멈추고 와 같은 행동에 대한 정의를 먼저 할 수 있다. 왜냐하면 자동차는 실체이고 탈것은 역할이기 때문이다. 우리는 실체에 집중할 때 데이터 위주의 사고를 하고 역할에 집중할 때 행동 위주의 사고를 한다. 실체는 곧 구현이다. 구현을 고민하는 순간 데이터 위주의 사고를 촉진한다.
또한, 실체만으로는 어떤 역할을 하는 클래스인지 알 수 없다. 누군가에게 자동차는 탈것이지만, 누군가에겐 관상용, 누군가에겐 집이 될 수도 있다. 즉, 역학을 고민하지 않고 구현체에 집중하면 해당 클래스를 개발하는 개발자만의 생각이 반영된다.
따라서 자동차 클래스를 만들어 줄 수 있나요? 와 같은 질문엔
- 자동차는 어떤 행동을 하는 객체인가요?
- 꼭 자동차이어야 하나요?
- 자동차라는 클래스를 만들어서 달성하려는 목표가 뭔가요?
와 같은 질문을 한다면 좋을 것이다.
탑승할 수 있고, 달릴 수 있으면 좋겠어요.
와 같은 대답이 온다면 필요한 것이 Vehicle이라는 것을 알게 된다. 이처럼 역할에 집중하면 훨씬 유연한 설계를 할 수 있다. 따라서 역할과 구현은 반드시 구분해야 하며, 구분을 위한 출발점은 어떤 질문을 하느냐이다. 우리 사고 흐름은 사소한 단어에도 영향을 받기 때문에 이름 짓는 것 역시 역할과 구현을 구분하는 데 큰 영향을 준다.
구현에 집중한 코드는 확장되는 요구사항에 유연하게 대처할 수 없다. 역할에 집중해야 유연한 설계를 얻을 수 있다. 하지만 우린 구현체를 먼저 상상하는 데 익숙하다. 따라서 역할에 집중하는 사고방식을 익히기 위해서는 꾸준한 노력이 필요하다. 반복적이고 의식적으로 객체가 어떤 행동을 해야 하는지 고민해야 한다. 더 나아가 어떤 행동들을 모아 구조적으로 타당한 역할을 만들 수 있을지 계속해서 고민해야 한다.
행동과 역할에 집중하라는 것은 추상화를 많이 하라는 뜻은 아니다. 역할과 추상은 같은 말이 아니기 때문이다.
3.5 메서드
메서드는 왜 메서드 일까? '방법'을 뜻하는 메서드와 함수(function)에는 어떤 차이점이 있을까?
인터페이스란 '나를 조작하고 싶다면 이런 메시지를 보내면 된다'라고 외부에 알려주는 수단이다.
즉, 어떤 객체나 시스템을 다루는 외부 세계는 협력 대상과 소통하기 위해 메시지를 이용한다. 객체는 협력 객체에 메시지를 건네고, 협력 객체는 메시지를 수신해서 행동한다. 우리는 어떤 객체가 협력 객체에 요청을 보낼 때 어떤 특정 메서드나 함수를 실행한다고 생각한다. 하지만 그렇지 않다. 실제로는 인터페이스를 이용한 통신에서는 메시지를 보내면서도 어떤 메서드가 실행될지 모른다.
class Car implements Vehicle {
void ride() {}
}
class Bicycle implements Vehicle {
void ride() {}
}
class User {
void ride(Vehicle vehicle) {
vehicle.ride();
// 실제 코드가 실행되기 전까지 Car인지 Bicycle인지 모른다.
}
}
위 코드에서도 User의 ride()가 실행되기 전까지 어떤 메서드가 호출될지 모른다.
함수는 입력값(input)과 출력값(output) 사이의 대응 관계를 나타낸다. 함수의 입력값은 정확히 하나의 출력값으로 대응된다.
여기서 함수의 특징은 '함수의 각 입력값은 정확히 하나의 출력값으로 대응된다'이다. 함수는 같은 입력에 대해 항상 같은 출력을 해야 한다. 즉, 같은 입력에 대해 두 개의 출력을 갖는 함수가 있어서는 안 된다.
따라서 함수를 실행하면서도 실제로 어떤 함수가 실행될지 모른다는 말은 부적절하다. 함수를 실행한다는 것은 고정적이고 특정하게 지정된 절차를 수행한다는 의미라서 그렇다. 따라서 어떤 클래스의 어떤 메서드가 실행될지 모르면서 함수를 실행한다고 표현하는 것은 불가능하다. 그리고 이는 객체지향에서 추구하는 방향과 반대된다. 객체지향에서는 특정한 구현에 의존하는 상황을 피하고자 한다. 그래서 객체지향에서는 협력 객체에 어떤 일을 요청할 때 '함수를 실행한다'라는 말 보다 '메시지를 전달한다'라고 표현한다.
객체지향에서 객체들은 메시지를 통해 소통한다. 즉. 객체는 협력 객체에 메시지만 보낼 뿐 실제로 어떤 방법(method)으로 일을 어떻게 처리할지는 객체가 정한다. 그래서 객체지향에서는 객체가 수행하는 함수를 메서드라고 부른다. 메서드란 어떤 메시지를 처리해 달라는 요청을 받았을 때 이를 어떻게 처리하는지 방법(method)을 서술하는 것이다. 따라서 메서드를 어떻게 구현할지에 대해 집중하는 것은 좋은 방법이 아니다. 메시지란 결국 어떤 메시지를 어떻게 처리할지를 서술하는 것이므로 알고리즘에 가까운 것이다. 알고리즘을 고민하면 결국 구현에 집착하고, 그럼 메서드를 작성하는 것이 함수를 작성하는 것과 다를 바가 없어지고, 절차지향적인 코드가 나오게 된다.
객체지향에서 정말 중요한 것은 책임을 나누고 메시지를 통해 협력 관계를 구축하는 것이다.
명시적 입력과 암묵적 입력
함수의 입력에는 명시적 입력과 암묵적 입력이 있다. 명시적 입력은 매개변수를 통해 전달된 값이고, 암묵적 입력은 전역 변수, 클래스의 인스턴스 변수를 참조할 때, 네트워크 상황처럼 코드 밖에 있지만 함수 동작에 영향을 주는 요소다.
암묵적 입력이 많을수록 예측이 어려워지고 테스트도 복잡해진다. 그래서 같은 입력에 항상 같은 출력을 주고, 외부 상태에 의존하지 않는 순수 함수가 이상적인 형태로 여겨진다. 이 원칙은 객체에도 적용된다. 상태를 바꾸지 않는 불변 객체와 순수 함수 중심의 사고방식이 바로 함수형 프로그래밍이다.
Chapter3을 마치며
Chapter1에서는 객체는 책임을 어떻게 나누고, 어디에 할당하는지가 중요했다면, 그게 이어져서 결국 객체가 그 책임을 어떻게 행동으로 실현할 것인가, 그리고 행동이 어떻게 모여서 역할이 되는가를 생각해 보는 것이 중요하다. (여러 역할이 모여 객체를 정의하기 때문이다.)
'객체는 자아를 가진 존재처럼 행동해야 한다'는 말은 단순히 감성적인 표현이 아니라, 객체가 데이터를 가지는 것에서 끝나지 않고, 그 데이터를 바탕으로 스스로 로직을 실행해야 한다는 설계적 요구였다.
이러한 사고의 전환은 결국 TDA(Tell, Don’t Ask), 인터페이스, 역할과 구현 분리, 메서드와 메시지의 구분으로 이어지고, 객체 간의 유연하고 책임 있는 협력을 중심으로 한 설계로 발전하게 도와준다.
돌이켜보면 나도 '어떤 행동을 하는 객체인지', '달성하려는 목표가 무엇인지'와 같은 질문을 하면서 개발을 해왔던 것 같지만, 결국엔 내 머릿속은 철저히 데이터 중심의 사고에 박혀 있던 사람이었다.
아마도 ERD를 먼저 설계하며, 데이터를 먼저 다루는 방식에 익숙해지다 보니 편리함을 우선시하며 데이터 중심이 자연스럽게 굳어져 버린 게 아닐까 싶다. 혹은 객체지향을 '제대로' 배우지 못했던 것이 근본적인 원인일지도 모르겠다.
사실, 데이터 중심으로 개발하면 작은 규모의 프로젝트에서는 빠르고 효율적으로 기능을 구현할 수 있다. 하지만 그 익숙한 방식에 머물러 있던 나는 유연성도 없고, 확장성도 고려되지 않은 절차지향적인 코드만 계속 반복해 왔던 것 같다.
이제는 행동을 기반으로 객체를 바라보고, 객체의 책임과 역할을 고려하며, 협력을 통해 시스템을 설계하려 노력해야겠다. 그동안 익숙했던 사고방식에서 벗어나 단순히 유지보수성과 확장성에 강한 구조만을 쫓는 것이 아니라, 객체의 본질과 책임을 중심으로 사고하는 개발자가 목표이다^^ 물론, 그전까지도 그렇게 해왔다고 생각해서 고민도 많이 해봐야 할 듯..ㅎㅎ
실제로 최근 새로운 프로젝트 리팩토링을 진행하면서 좀 고민하게 된 것들을 조만간 포스팅 공유하러 다시 돌아오겠다,, 그전에 Part 14까지 남았는데 열심히 써볼게여🫨
'책책책 책을 읽어요 > 자바,스프링 개발자를 위한 실용주의 프로그래밍' 카테고리의 다른 글
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 5. 순환 참조 (0) | 2025.04.15 |
---|---|
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 4. SOLID (0) | 2025.04.08 |
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 2. 객체의 종류 (0) | 2025.03.16 |
[자바/스프링 개발자를 위한 실용주의 프로그래밍] Chapter 1. 절차지향과 비교하기 (2) | 2025.03.15 |