달리기반 1일차 수업에서는 객체지향에 대해서 배웠다.
일단 오늘 수업 요약 전에 달리기반 첫 수업 소감을 말하자면 튜터님께서 설명을 해주실 때 찰떡 비유와 함께 진행해주셔서 이해가 정말 잘 되었다.
손님(Customer), 바리스타(Barista) 가 있다고 가정
손님이 커피를 주문하면 바리스타가 만들어줌
public class Barista {
// 세상에! 누구나 접근 가능한 public 변수라니요.
public int beans = 100; // 커피 원두 양 (그램)
// 커피 만드는 기술(메서드)도 없고, 단순히 재료만 들고 서 있습니다.
}
public class Customer {
public void order(String menuName, Barista barista) {
System.out.println("손님: " + menuName + " 주세요.");
// 여기가 문제입니다!
// 손님이 직접 바리스타의 원두통을 확인합니다. (???)
if (barista.beans >= 20) {
// 심지어 직접 원두를 퍼갑니다.
barista.beans -= 20;
System.out.println("손님: (원두를 직접 갈아서 커피를 만듦) -> " + new Coffee(menuName));
} else {
System.out.println("손님: 원두가 부족하네... 못 먹겠다.");
}
}
}
Barista bariKim = new Barista();
Customer sonnom = new Customer();
// 1. 정상적인(?) 손놈의 강탈 과정
sonnom.order("아이스 아메리카노", bariKim);
// 2. 대참사
// 어떤 나쁜 마음을 먹은 개발자가 나타나서...
bariKim.beans = 0;
System.out.println("누군가 원두를 다 훔쳐감: " + bariKim.beans);
객체에게 일을 시키세요!
객체지향의 진짜 핵심은 데이터를 가진 놈이 그 데이터를 처리하게 하는 것 (책임 주도 설계 혹은 캡슐화 라고 함)
- 바리스타의 원두(beans)는 바리스타만 만질 수 있어야함(private)
- 손님은 바리스타에게 "커피 줘!"라고 메시지만 보내야함(barista.makeCoffee())
위에 잘못된 코드를 제대로 고쳐보면
public class Barista {
// 1. private으로 외부 접근 차단 (원두는 나만의 거시야~)
private int beans = 100;
// 2. 상태를 변경하는 로직을 스스로 수행 (책임 수행)
public Coffee makeCoffee(String menuName) {
if (this.beans >= 20) {
this.beans -= 20;
System.out.println("바리스타: (쓱싹쓱싹) 커피 만드는 중...");
return new Coffee(menuName);
} else {
System.out.println("바리스타: 원두가 부족합니다!");
return null; // 간단한 예제를 위해 null 반환
}
}
}
public class Customer {
public void order(String menuName, Barista barista) {
System.out.println("손님: " + menuName + " 주세요.");
// 이제 손님은 원두가 몇 개인지, 어떻게 만드는지 알 필요가 없음
// 그냥 "만들어줘"라고 요청(Message)만 보냄.
Coffee coffee = barista.makeCoffee(menuName);
if (coffee != null) {
System.out.println("손님: 와! " + coffee + " 나왔다.");
} else {
System.out.println("손님: ㅠㅠ");
}
}
}
정리(무엇이 좋아졌는가?)
- 결합도가 낮아짐(Low Coupling) - 바리스타가 로직을 바꿔도 손님 코드는 수정할 필요가 없음
- 응집도가 높아짐(High Cohesion) - 원두에 관련된 로직은 모두 Barista 클래스 안에 모여 있음
- 데이터 무결성 - 외부에서 beans = 0처럼 악의적인 조작을 할 수 없음
1) public (=동네 놀이터(누구나))
: 모든 곳에서 접근 가능
2) protected (=우리 집(가족 + 자녀))
: 같은 폴더(패키지) + 자식 클래스까지
3) default (= 내 방(나 혼자))
: 같은 폴더(패키지)에서만 가능
4) private (= 내 비밀 일기장(오직 나만))
: 오직 이 클래스 안에서만
필드(변수)는 무조건 private으로 꼭꼭 숨긴다(Information Hiding)
외부에서 써야 하는 메서드만 Public으로 열어준다
데이터(필드)는 private으로
데이터 조작은 Public 메서드로만
메서드도 캡슐화할 수 있음
캡슐화는 데이터뿐만 아니라, 내부에서만 사용하는 복잡한 로직(메서드)도 숨기는 데 사용됨
복잡하거나 불필요한 내부 로직을 private으로 숨기는 것을 정보 은닉(Information Hiding)이라고 부르며 캡슐화의 핵심 목표
Step2에서 손님이 바리스타에게 직접 주문하였음
캐셔를 등장시켜서 협력과 메시징을 진행
손님 -> 캐셔 -> 바리스타
public class Cashier {
private Barista barista; // 협력(의존) 객체
// 캐셔는 바리스타를 알고 있어야만 주문을 전달할 수 있죠? (협력 관계)
public Cashier(Barista barista) {
this.barista = barista;
}
public Coffee takeOrder(String menuName) {
System.out.println("캐셔: 주문 확인했습니다. (" + menuName + ")");
// 중요! 캐셔가 직접 커피를 만드는 게 아닙니다.
// 전문가인 바리스타에게 "만들어줘"라고 메시지만 토스(Toss)합니다.
return barista.makeCoffee(menuName);
}
}
public class Customer {
// 이제 손님은 Barista를 알 필요가 없습니다. (느슨한 결합, Loose Coupling)
public void order(String menuName, Cashier cashier) {
System.out.println("손님: " + menuName + " 주세요!");
// 손님은 캐셔만 믿고 주문합니다.
Coffee coffee = cashier.takeOrder(menuName);
if (coffee != null) {
System.out.println("손님: (홀짝) 음~ " + coffee + " 맛있다.");
} else {
System.out.println("손님: (아쉽) 다음에 올게요.");
}
}
}
Barista chulsoo = new Barista();
Cashier younghee = new Cashier(chulsoo); // "영희야, 주문 들어오면 철수한테 말해줘"
Customer gildong = new Customer();
// 길동이는 영희(캐셔)에게만 말합니다.
gildong.order("따뜻한 라떼", younghee);
[실행결과]
손님: 따뜻한 라떼 주세요!
캐셔: 주문 확인했습니다. (따뜻한 라떼)
바리스타: (원두 갈갈갈) 맛있게 만들어 드릴게요!
손님: (홀짝) 음~ ☕ 따뜻한 라떼 맛있다.
만약 캐셔에게 이제부터는 오전엔 철수(사람)랑 일하고, 오후엔 이 로봇이랑 일해 라고 한다면 과연 캐셔는 이 지시를 따를 수 있을까?
public class Cashier {
// ❌ 문제점: 'Barista'(사람) 클래스에 꽉 묶여 있음 (Tight Coupling)
private Barista barista;
public Cashier(Barista barista) {
this.barista = barista;
}
}
// CoffeeMaker.java
public interface CoffeeMaker {
// "누구든 이 명찰을 달려면, 커피 만드는 기능은 꼭 있어야 해!"
Coffee makeCoffee(String menuName);
}
이 문제를 해결하기 위해 "커피를 만드는 존재"라는 공통점을 뽑아서 인터페이스(Interface)를 만듬
그리고 사람과 로봇이 이 명찰을 달게 함
// 사람
public class Barista implements CoffeeMaker{...}
// 로봇
public class RobotBarista implements CoffeeMaker{...}
public class Cashier {
// ⭕ 해결: 구체적인 클래스가 아니라 '인터페이스'에 의존함 (Loose Coupling)
private CoffeeMaker coffeeMaker;
public Cashier(CoffeeMaker coffeeMaker) {
this.coffeeMaker = coffeeMaker; // 사람도 OK, 로봇도 OK!
}
}
이제 캐셔는 상대가 사람인지 로봇인지 알 필요가 없음
그저 커피를 만들 줄 아는 무언가(CoffeeMaker)라면 누구든 환영
// 오전: 사람 투입
Cashier morning = new Cashier(new Barista());
morning.takeOrder("라떼");
// -> 🧔🏻♂️ 바리스타: (핸드드립) 뚝딱뚝딱...
// 오후: 로봇 투입
Cashier afternoon = new Cashier(new RobotBarista());
afternoon.takeOrder("라떼");
// -> 🤖 로봇: 삐리릭! 고압 추출 모드...
똑같은 takeOrder()를 줄 뿐인데 누가 일하느냐에 따라 결과가 달라짐 -> 이것이 다형성
부모 클래스 타입의 변수로, 자식 클래스 객체를 다룰 수 있다
코드의 유연성과 확장성이 올라감
다형성: 하나의 타입(부모)으로 여러 가지 형태(자식)의 객체를 다루는 기술
핵심 원리: 부모 타입의 변수에 자식 객체를 담을 수 있음
장점: 코드의 중복을 줄이고, 유연하고 확장성 있는 프로그램을 만들 수 있음
주요 활용처:

실무에서는 보통 인터페이스를 훨씬 더 많이 사용함. 인터페이스를 통해 객체의 역할을 정의하고, 그 역할에 맞는 구현체를 갈아 끼우는 방식(다형성)이 프로그램을 훨씬 더 유연하게 만들어주기 때문
// Step 4의 Main 코드
CoffeeMaker robot = new RobotBarista();
Cashier cashier = new Cashier(robot);
객체지향의 설계 원칙 중 하나: 쓰는 놈과 만드는 놈을 나누자
public class OrderContext {
// 오전반 세팅: 사람 + 캐셔 조립
public static Cashier configMorningShift() {
return new Cashier(new Barista()); // 여기서 '주입(Injection)' 해줌!
}
// 오후반 세팅: 로봇 + 캐셔 조립
public static Cashier configAfternoonShift() {
return new Cashier(new RobotBarista());
}
}
// Main은 이제 '누가' 오는지 전혀 몰라도 됩니다.
// 그냥 "오전반 세팅 줘!"라고 공장에다 주문만 하면 끝.
Cashier cashier = OrderContext.configMorningShift();
1) High Level(Cashier): 구체적인 것(Barista)에 의존하지 않고, 추상적인 것(CoffeeMaker)에 의존
2) Low Level(Barista): 추상적인 것(CoffeeMaker)을 구현
3) 조립가(OrderContext): 이 둘을 밖에서 연결(Injection) 해 줌.
-> 이전에는 캐셔가 바리스타를 직접 데려왔다면(new Barista), 이제는 외부(Context)에서 바리스타를 캐셔에게 주입(Injection)해줌.
이것을 의존성 주입(Dependency Injection, DI)이라고 부름
왜 좋을까?
1) 이해하기 쉬움 - 클래스가 하는 일이 명확해서 코드를 파악하기 편함
2) 수정이 안전 - 하나의 기능을 수정해도 다른 기능에 영향을 줄 확률이 크게 줄어듬
3) 재사용하기 좋음 - 필요한 기능만 쏙쏙 가져다 쓰기 편함
왜 좋을까?
1) 안정적 - 이미 잘 동작하던 기존 코드를 건드리지 않으니, 새로운 기능을 추가하다가 기존 기능이 고장날 위험이 없음
2) 유연함 - 새로운 요구사항이 생겨도 겁내지 않고 유연하게 대처할 수 있음
왜 좋을까?
1) 신뢰성이 높아짐 - 클래스들 간의 관계가 예측 가능해져서 프로그램 전체의 안정성이 올라감
2) 다형성을 잘 활용할 수 있음 - OCP에서 본 것처럼, 부모 타입 하나로 다양한 자식 타입을 믿고 다룰 수 있게 됨
왜 좋을까?
1) 결합도가 낮아짐 - 클래스가 자기에게 필요 없는 기능에 의존하지 않게 되어 시스템이 깔끔해짐
2) 수정이 쉬워짐 - 인터페이스가 작고 명확하면, 수정이 필요할 때 영향을 받는 범위를 최소화할 수 있음
왜 좋을까?
1) 유연성과 확장성이 극대화됨 - 부품(클래스)을 갈아 끼우기가 매우 쉬워짐
2) 테스트하기 쉬워짐 - 실제 객체 대신 테스트용 가짜 객체(Mock Object)를 쉽게 연결해서 테스트할 수 있음