이번에 객체지향의 사실과 오해를 읽고, 내용이 너무 좋고 중요하다는 생각이 들었다.
따라서 좋은 동료들과 함께 객체지향의 사실과 오해를 스터디하며 중요한 부분을 복습하기로 하였다.
또한 이를 기록할 예정이다. 오늘의 1장의 내용 중 중요하다고 생각되는 부분을 정리하고 코드로 구현해 보자.
객체지향 프로그래밍이란?
우리는 흔히 현실 세계에 존재하는 사물에 대한 추상화를 객체라고 생각한다.
하지만 객체지향의 목표는 실세계를 모방하는 것이 아니다.
소프트웨어 개발자의 역할은 단순히 실세계를 소프트웨어 안으로 옮겨 담는 것이 아니다.
오히려 새로운 세계를 창조하는 것이다.
단순히 우리가 이해를 쉽게 하기 위해 이러한 비유를 계속해서 사용하는 것이다.
협력하는 사람들
커피 공화국의 아침
첫 비유는 한 개발자가 일을 시작하기 전 커피를 주문하는 것으로부터 시작된다.
이곳에는 손님(개발자), 주문을 받는(캐시어), 커피를 만드는(바리스타)가 나오게 된다.
손님은 캐시어에게 커피를 주문하고, 캐시어는 주문받은 커피를 바리스타에게 알린다.
바리스타는 커피를 만들고, 커피를 캐시어에게 전달한다.
캐시어는 알림을 통해 커피의 완성을 알리고, 커피를 손님에게 다시 전달해 준다.
위의 과정에서 서로 협력관계가 존재하고 각자의 역할이 존재한다. 또한 협력하는 과정에서 자신이 맡은 바 책임을 다한다.
요청과 응답으로 구성된 협력
요청(request)
- 문제 해결에 필요한 지식을 알고 있거나 서비스를 제공해 줄 수 있는 사람에게 도움을 요청한다.
- 다수의 사람 혹은 역할이 필요하다 -> 요청은 연쇄적으로 발생한다.
응답(response)
- 요청을 받은 사람이 주어진 책임을 다하면서 필요한 지식, 서비스를 제공한다.
- 요청 연쇄적 발생 -> 요청의 방향과 반대 방향으로 연쇄적으로 전달한다.
협력의 성공은 특정한 역할을 맡은 각 개인이 얼마나 요청을 성실히 이행하였는지에 따라 달려있다.
역할과 책임
역할(role)
특정한 사람이 어떤 협력 안에서 차지하는 책임이나 임무를 의미
- 손님: 커피를 주문하는 임무를 맡음
- 캐시어: 커피 주문을 처리하는 임무를 맡음
- 바리스타: 커피를 제조해야 하는 임무를 맡음
책임(responsibility)
역할이라는 단어는 의미적으로 책임이라는 개념을 내포
- 손님 역할: 커피 주문 책임
- 캐시어 역할: 주문 내용을 바리스타에게 전달 책임, 커피 준비 완료 사실을 손님에게 알릴 책임
- 바리스타 역할: 커피 제조 책임
역할과 책임은 협력이 원활하게 진행되는 필요한 핵심적인 구성요소
- 여러 사람이 동일한 역할 수행 가능 - 각 역할은 각 역할을 수행하는 사람이 누구인지 중요하지 않다.
- 역할은 대체 가능성을 의미 - 대체 가능으로 두 사람이 동일한 역할을 수행한다면 둘 중 어떤 사람이 역할을 수행하더라도 문제가 되지 않는다.
- 책임을 수행하는 방법은 자율적으로 선택 - 동일한 요청에 대해 서로 다른 방식으로 응답할 수 있는 능력
- 한 사람이 동시에 여러 역할을 수행할 수 있다 - 캐시어와 바리스타 역할을 동시에 수행하는 것도 가능
역할, 책임, 협력
기능을 구현하기 위해 협력하는 객체들
사람 -> 객체
에이전트의 요청 -> 메시지
에이전트가 요청을 처리하는 방법 -> 메서드
역할과 책임 수행하며 협력하는 객체들
협력의 특정한 책임을 수행하는 역할들 간 연쇄적인 요청과 응답을 통해 목표를 달성한다.
적절한 객체에게 적절한 책임을 할당하는 것에서 시작
책임 -> 객체지향 설계의 품질을 결정하는 가장 중요한 요소
역할 -> 유연하고 재사용 가능한 협력 관계를 구축하는데 중요한 설계 요소
협력 속에 사는 객체
협력적이어야 한다.
- 복종하는 것이 아닌 요청에 응답할 뿐이다.
- 어떤 방식으로 응답할지는 스스로 판단한다.
- 요청 응답 여부도 스스로 판단한다.
자율적이어야 한다.
- 협력에 참여하지만 스스로의 결정과 판단에 따라 행동해야 한다.
- 자기 스스로의 원칙에 따라 어떤 일을 하거나 자기 스스로를 통제하여 절제해야 한다. 이는 책임이 따른다.
상태와 행동을 함께 지닌 자율적인 객체
객체가 자율적인 존재로 남기 위해서는 필요한 행동과 상태를 지녀야 한다.
객체의 자율성
- 사적인 부분은 스스로 관리하고, 외부에서 간섭할 수 없게 차단해야 한다.
- 접근이 허락된 수단을 통해서만 의사소통 해야 한다.
- 다른 객체가 무엇(what)을 수행하는지는 알 수 있지만, 어떻게(how) 수행하는지는 몰라야 한다.
객체의 상태와 행동
- 행동을 위해 필요한 상태를 포함한다. - 바리스타는 커피 제조방법을 기억함
- 행동을 하되, 수행 방법은 스스로 결정한다. - 바리스타는 자신이 알고 있는 제조법에 따라 커피를 만든다.
과거 개발 -> 데이터와 프로세스 엄격하게 구분
객체 지향 -> 데이터와 프로세스 객체 하나의 틀 안에 함께 묶어 놓음으로써 객체의 자율성 보장
협력과 메시지
메시지(Message)
- 객체지향의 세계 한 가지 의사소통 수단
- 객체는 협력을 위해 다른 객체에서 메시지를 전송 + 수신
협력 -> 메시지를 전송하는 객체 + 메시지를 수신하는 객체의 관계로 구성
- 송신자(sender): 메시지를 전송하는 객체
- 수신자(receiver): 메시지를 수신하는 객체
메서드와 자율성
메서드
- 수신된 메시지를 처리하는 방법
- 메시지를 수신한 객체가 실행 시간에 메서드를 선택할 수 있다.
메시지와 메서드의 분리
- 메시지와 메서드를 분리하는 것은 객체의 자율성을 높이는 핵심 메커니즘이다.
- 객체의 협력에 참여하는 객체들 간의 자율성 증진
- 다른 객체가 무엇을 수행하는지는 알 수 있지만 어떻게 수행하는지에 대해 알 수는 다른 없다.
객체지향의 본질
객체지향
- 시스템을 상호작용하는 자율적인 객체들의 공동체로 바라보고, 객체를 이용해 시스템을 분할하는 방법
객체
- 자율적인 객체는 상태와 행위를 함께 지니며 스스로 자기 자신을 책임지는 객체를 의미
- 객체는 다른 객체와 협력한다. 협력 내에서 각자 정해진 역할을 수행하고, 역할은 책임의 집합이다.
메시지
- 다른 객체와 협력하기 위해 전송한다. 메서드는 자율적으로 선택한다.
객체를 지향하라
클래스를 지향하는 것이 아닌, 객체를 지향해야 한다.
클래스는 객체지향에서 중요한 구성요소이지만, 핵심이 아니다. 객체들의 협력 관계를 코드로 옮기는 도구일 뿐이다.
적절한 책임을 수행하는 역할 간의 유연하고 견고한 협력 관계를 구축하는 것이 핵심이다.
이를 적용해 간단하게 코드를 작성해 보았다.
public interface Barista {
String make(String order);
}
public interface Cashier {
String callWithOrder(String order);
}
public interface Customer {
void order(String order);
}
우선 각각의 역할을 인터페이스로 설정하였다.
즉 이를 구현하는 클래스는(역할을 수행하는) 누구든지 그 역할을 대신할 수 있다.
이후 구체적인 구현체를 만들었다.
public class BaristaA implements Barista {
@Override
public String make(String order) {
String result = makeCoffee(order);
return result;
}
private String makeCoffee(String order) {
if (order.equals("iceCoffee")) {
return "iceCoffee";
}
if (order.equals("latte")) {
return "latte";
}
throw new IllegalStateException("커피 제조의 책임을 다하지 못함");
}
}
바리스타는 커피를 만드는데, 오직 아이스커피와 라테의 조리법만 알고 있으며, 나머지는 제조법을 모른다고 생각하였다.
매우 단순하게 String을 통해 커피를 만든다고 생각하였다.
public class CashierA implements Cashier{
private Barista barista;
public CashierA(Barista barista) {
this.barista = barista;
}
@Override
public String callWithOrder(String order) {
String result = barista.make(order);
wait(1000);
return result;
}
private void wait(int millis){
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
캐시어의 경우 바리스타에게 커피를 요청하고 이를 반환하는 역할을 수행한다.
캐시어의 책임에 대해 정말 모호하다는 생각을 많이 하였다.
여러 가지 의견이 나왔는데, 그중 한 가지는 바리스타가 만든 커피가 손님이 주문한 커피와 일치한 지 판단하는 책임을 가져야 한다는 의견이었다.
wait의 경우 처리에 걸리는 시간이라 생각하였다.(생략가능)
public class CustomerA implements Customer{
List<String> menu = List.of("iceCoffee","latte");
private Cashier cashier;
public CustomerA(Cashier cashier) {
this.cashier = cashier;
}
@Override
public void order(String order) {
if (!menu.contains(order)){
throw new IllegalStateException("커피 주문의 책임을 임하지 못하였습니다");
}
String result = cashier.callWithOrder(order);
System.out.println(result);
}
}
손님의 경우 특정 메뉴에 대해서만 주문을 할 수 있는 책임이 있다.
위 코드를 활용한 Main 클래스는 다음과 같다.
public class CoffeeShop {
public static void main(String[] args) {
Barista baristaA = new BaristaA();
Cashier cashier = new CashierA(baristaB);
Customer customer = new CustomerA(cashier);
Scanner sc = new Scanner(System.in);
//iceCoffee , latte
String menu = sc.next();
customer.order(menu);
}
}
즉 손님은 캐시어 인터페이스를 의존하고, 캐시어는 바리스타를 의존하는 코드이다.
이후 손님이 커피를 주문하는 방식이다.
정말 단순하게 객체지향의 사실과 오해 1장의 내용을 활용해 적용해 보았다.
구현 코드에 대한 피드백
우선 인터페이스로 분리한 것에 대한 긍정적인 답변을 받았다. 하지만 계속해서 오버라이딩과 구현을 통한
번거로움이 있기에 이를 상속관계를 통해 풀어나가는 것은 어떠한지에 대한 피드백을 받았다.
또한 나는 손님은 캐시어를 의존하고, 캐시어는 바리스타를 의존해야 한다고 생각하고 코드를 작성하였다.
하지만 다른 사람들은 다른 객체를 의존하지 말고, 단독으로 쓰일 수 있게 하는 것에 대한 피드백을 해주었다.
스터디를 통해 내 코드에서도 여러 가지 의견이 나오며 피드백을 받을 수 있었다.
당연하게도 정답은 없다.
따라서 각각의 코드마다 장점과 단점을 명확하게 구분하고 이를 적절히 사용하는 능력을 길러야겠다.
처음 책을 읽었을 때는 당연하다고 생각하고 넘어갔는데, 다시 읽으면서 이를 코드에 옮기니 새롭게 와닿고, 더욱 학습에 도움이 되었다.
1장의 경우 내용이 매우 추상적이라고 생각한다. 그래서 쉬워 보이지만 어려웠다.
계속해서 코드에 적용하며 성장할 것이다.
'좋은 개발자가 되기' 카테고리의 다른 글
객체지향의 사실과 오해(4장) (0) | 2023.04.25 |
---|---|
객체지향의 사실과 오해(3장) (2) | 2023.04.17 |
객체지향 생활 체조 원칙 (0) | 2023.04.16 |
객체지향의 사실과 오해(2장) (0) | 2023.04.11 |
자바 코드 컨벤션 (2) | 2023.03.29 |