6장 객체와 자료 구조
1. 📌 핵심 개념 정리
✅ 요약하기
변수를 비공개로 정의하는 이유가 있다. 남들이 변수에 의존하지 않게 만들고 싶기 때문이다. 그렇다면 어째서 수많은 프로그래머가 조회 함수와 설정 함수를 당연하게 공개해 비공개 변수를 외부에 노출할까?
- 자료 추상화
- 변수 사이에 함수라는 계층을 넣는다고 구현이 저절로 감춰지지는 않는다. 구현을 감추려면 추상화가 필요하다.
- 그저 조회 함수와 설정 함수로 변수를 다룬다고 클래스가 되지는 않는다. 그보다는 추상 인터페이스를 제공해 사용자가 구현을 모른 채 자료의 핵심을 조작할 수 있어야 진정한 의미의 클래스다.
- 자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다.
- 인터페이스나 조회/설정 함수만으로는 추상화가 이뤄지지 않는다.
- 개발자는 객체가 포함하는 자료를 표현할 가자 좋은 방법을 심각하게 고민해야 한다.
- 아무 생각 없이 조회/설정 함수를 추가하는 방법이 가장 나쁘다.
- 변수를
private
으로 선언하더라도 각 변수마다getter/setter
를 제공한다면 구현을 외부로 노출하는 셈이다. - 구체적인 Point 클래스는 공개 변수를 가진다.
- 추상적인 Point 클래스는 내부 구조를 노출하고, 개별적으로 좌표값을 읽고 설정하게 강제한다. 일반적으로 변수를 private으로 많이 선언을 하는데, 각 값마다 get과 set 함수를 제공한다면 이는 결과적으로 내부 구조를 노출하는 구조가 된다.
- 자료/객체 비대칭
객체는 추상화 뒤로 자료를 숨긴 채 자료를 다루는 함수만 공개한다.
자료 구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.
- 위 두 정의는 본질적으로 상반된다.
- 절차 지향적인 구조의 클래스는 자료를 공개하며,
Geometry
클래스에서 도형의 동작 방식을 구현한다. 새 함수를 추가해도 도형 클래스는 영향을 받지 않지만, 새 도형을 추가하면Geometry
클래스의 함수를 모두 고쳐야 할 수 있다. - 객체 지향적인 구조의 클래스는 내부 자료를 숨기고 각 도형 클래스에서 자신의 동작(
area()
)을 구현한다. 새 도형을 추가해도 기존 함수에 영향을 미치지 않지만, 새 함수를 추가하려면 모든 도형 클래스를 고쳐야 한다. - 다시말해, 객체 지향 코드에서 어려운 변경은 절차적인 코드에서 쉬우며, 절차적인 코드에서 어려운 변경은 객체 지향 코드에서 쉽다.
- 복잡한 시스템을 짜다 보면 새로운 함수가 아니라 새로운 자료 타입이 필요한 경우가 생긴다. 이때는 클래스와 객체 지향 기법이 가장 적합하다.
- 반면, 새로운 자료 타입이 아니라 새로운 함수가 필요한 경우도 생긴다. 이때는 절차적인 코드와 자료 구조가 좀 더 적합하다.
- 객체는 동작을 공개하고 자료를 숨긴다. 그래서 기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기 쉽다. 반면, 기존 객체에 새 동작을 추가하기는 어렵다.
- 자료 구조는 별다른 동작 없이 자료를 노출한다. 그래서 기존 자료 구조에 새 동작을 추가하기는 쉬우나, 기존 함수에 새 자료 구조를 추가하기는 어렵다.
- 보통 모든 것을 객체로 바라보지만, 때로는 단순한 자료 구조가 적합한 상황도 있다.
- 디미터 법칙
- 잘 알려진 휴리스틱으로, 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
- 객체는 조회 함수로 내부 구조를 공개하면 안 된다는 의미다. 그러면 내부 구조를 노출하는 셈이기 때문이다.
- 기차 충돌
- 여러 객체가 한줄로 이어진 기차처럼 보이는 코드
- 일반적으로 조잡하다 여겨지는 방식이므로 피하는 편이 좋다.
- 클래스
C
의 메서드f
는 다음과 같은 객체의 메서드만 호출해야 한다:- 클래스
C
:f
는 자기 자신(this)의 메서드를 호출할 수 있다. f
가 생성한 객체: 메서드f
가 만든 객체의 메서드는 호출해도 된다.f
인수로 넘어온 객체:f
가 인수로 받은 객체의 메서드는 호출할 수 있다.C
인스턴스 변수에 저장된 객체: 멤버 변수로 저장된 객체는 사용 가능하다.
- 클래스
- 하지만 위 객체에서 허용된 메서드가 반환하는 객체의 메서드는 호출하면 안 된다. 다시 말해, 낯선 사람은 경계하고 친구랑만 놀라는 의미이다.
- 위반 사례:
class Engine {
public void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine = new Engine();
public Engine getEngine() {
return engine; // 내부 객체를 직접 반환
}
}
class Driver {
public void drive(Car car) {
car.getEngine().start(); // 🚨 디미터의 법칙 위반 (Car의 내부 객체(Engine)에 직접 접근)
}
}
- 준수 사례:
class Car {
private Engine engine = new Engine();
public void startEngine() { // 중간 계층을 제공
engine.start();
}
}
class Driver {
public void drive(Car car) {
car.startEngine(); // ✅ 디미터의 법칙 준수 (직접 관계된 객체만 사용)
}
}
- 잡종 구조
- 절반은 객체, 절반은 자료 구조인 잡종 구조가 나온다.
- 잡종 구조는 중요한 기능을 수행하는 함수도 있고, 공개 변수나 공개 조회/설정 함수도 있다.
- 공개 조회/설정 함수는 비공개 변수를 그대로 노출한다.
- 이러한 잡종 구조는 새로운 함수는 물론이고 새로운 자료 구조도 추가하기 어렵다.
- 잡종 구조는 최대한 피하자! 프로그래머가 함수나 타입을 보호할지 공개할지 확신하지 못해 어중간하게 내놓은 설계에 불과하다.
- 구조체 감추기
- 객체라면 내부 구조를 감춰야 하니, 줄줄이 사탕으로 엮어서는 안된다.
- 객체는 뭔가를 하라고 말해야지 속을 드러내라고 말하면 안된다.
- 내부 구조를 드러내지 않으며, 모듈에서 해당 함수는 자신이 몰라야 하는 여러 객체를 탐색할 필요가 없어야 한다. 이래야 디미터 법칙을 위반하지 않는 것이다.
- 기차 충돌 코드는 다음과 같이 나누는 편이 좋다.
- 자료 구조는 무조건 함수 없이 공개 변수만 포함하고 객체는 비공개 변수와 공개 함수를 포함한다면 디미터 법칙 어기지 않는다.
- 기차 충돌 예시:
final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
- 위의 기차 충돌 예시에서 반환하는 값이 필요한 이유가 무엇인가? 해당 이유에 맞는 동작(임시 파일 생성)을 구현하여 자료 구조의 내부를 감춘다.
BufferedOuputStream bos = ctxt.createScratchFileStream(classFileNmae);
- 기차 충돌 예시:
Car car = new Car();
car.getEngine().getBattery().checkBattery(); // 기차 충돌 발생 (Car → Engine → Battery)
car.getEngine().start(); // Car 내부 구조를 너무 깊이 탐색함
- 개선된 예시:
class Battery {
public void checkBattery() {
System.out.println("배터리 상태 확인: 정상");
}
}
class Engine {
private Battery battery = new Battery();
public void start() {
System.out.println("엔진 시동 걸림!");
}
public void checkBatteryStatus() { // 배터리 체크는 엔진이 처리
battery.checkBattery();
}
}
class Car {
private Engine engine = new Engine();
public void startEngine() { // 엔진을 직접 노출하지 않음
engine.start();
}
public void checkBattery() { // 배터리 체크도 Car가 담당
engine.checkBatteryStatus();
}
}
class Driver {
public static void main(String[] args) {
Car car = new Car();
car.checkBattery(); // Driver는 Car에게만 요청
car.startEngine(); // Engine을 직접 알 필요 없음
}
}
- 위 예제가 디미터 법칙을 위반하는지 여부는 위의 변수들이 객체인지 자료 구조인지에 달렸다. 객체라면 내부 구조를 숨겨야 하므로 확실히 디미터 법칙을 위반한다. 반면, 자료 구조라면 당연히 내부 구조를 노출하므로 문제되지 않는다.
- 자료 전달 객체
- 자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스이다.
- 이런 자료 구조체를 때로는 자료 전달 객체(DTO) 라고 한다.
- 흔히 DTO는 데이터베이스에 저장된 가공되지 않은 정보를 애플리케이션 코드에서 사용할 객체로 변환하는 일련의 단계에서 가장 처음으로 사용하는 구조체이다.
- 좀더 일반적인 형태는 빈 구조다.
- 빈은 비공개 변수를 조회/설정 함수로 조작한다.
- 활성 레코드
- DTO의 특수한 형태이다.
- 공개 변수가 있거나 비공개 변수에 조회/설정 함수가 있는 자료 구조지만, 대개 save나 find와 같은 탐색 함수도 제공한다.
- 활성 레코드는 데이터베이스 테이블이나 다른 소스에서 자료를 직접 변환한 결과다.
- 활성 레코드에 비즈니스 규칙 메서드를 추가해 이런 자료 구조를 객체로 취급하는 개발자가 흔한데, 이는 바람직하지 않다. 잡종 구조가 나오기 때문이다.
- 해결책: 활성 레코드는 자료 구조로 취급한다. 비즈니스 규칙을 담으면서 내부 자료를 숨기는 객체는 따로 생성한다.
- 활성 레코드로 구현 가능.
- 레코드는 자료 구조로 취급한다. 비지니스 규칙 메서드를 레코드에 구현하지 마라.
2. 🤔 이해가 어려운 부분
🔍 질문하기
객체는 뭔가를 하라고 말해야지 속을 드러내라고 말하면 안된다
- "객체에게 뭔가를 하라고 말해야 한다"
- 즉, 객체는 행동(메서드)을 수행해야 하며, 내부 데이터를 직접 노출해서는 안 된다.
- 객체가 스스로 데이터를 처리하도록 유도해야 한다.
- "속을 드러내라고 말하면 안 된다"
- 즉, 객체의 내부 상태(데이터)를 직접 노출시키지 말라는 의미.
- 데이터를 가져와서 외부에서 직접 조작하면, 캡슐화가 깨지고 코드의 결합도가 높아짐.
구조체 감추기
- ❌ 나쁜 코드: 모듈이 너무 많은 걸 알고 있음 → outputDir 직접 조작
- ✅ 좋은 코드: 필요한 기능(임시 파일 생성)을 객체에게 요청하고, 내부 구조는 감춤
- 디미터 법칙
- 어려웠던 부분
예시가 없어서 이해가 어렵다. 책에 나와있는 예제를 이해하기에는 코드에 대한 정보가 조금 부족해서 이해하는데 어려웠다.
- 궁금한 점
디미터 법칙을 어긴 예시 코드와 그것을 개선한 코드 살펴보기
대다수의 개발자들이 기차 충돌이 발생하는 코드를 많이 작성하는데 이를 방지할 수 있는 방법이 있을까?
- 디미터 법칙을 어긴 예시 코드:
class Address {
constructor(city) {
this.city = city;
}
getCity() {
return this.city;
}
}
class Person {
constructor(name, address) {
this.name = name;
this.address = address;
}
getAddress() {
return this.address;
}
}
class Company {
constructor(name, owner) {
this.name = name;
this.owner = owner;
}
getOwner() {
return this.owner;
}
}
// 사용 예시
const address = new Address("Seoul");
const person = new Person("John", address);
const company = new Company("TechCorp", person);
// 기차 충돌 문제점 발생
console.log(company.getOwner().getAddress().getCity()); // Seoul 출력
- 개선한 예시 코드:
class Address {
constructor(city) {
this.city = city;
}
getCity() {
return this.city;
}
}
class Person {
constructor(name, address) {
this.name = name;
this.address = address;
}
getCity() {
return this.address.getCity(); // 중간 단계 숨기기
}
}
class Company {
constructor(name, owner) {
this.name = name;
this.owner = owner;
}
getOwnerCity() {
return this.owner.getCity(); // 중간 객체와 직접 상호작용 X
}
}
// 사용 예시
const address = new Address("Seoul");
const person = new Person("John", address);
const company = new Company("TechCorp", person);
// 기차 충돌 문제 해결
console.log(company.getOwnerCity()); // "Seoul" 출력
3. 📚 참고 사항
📢 논의하기
관련 자료 공유
-
추가 자료