9장 : 유연한 설계

8장 복습

8장에서는 유연하고 재사용 가능한 설계를 만들기 위해 적용할 수 있는 다양한 의존성 관리 기법들을 소개한다.

1. 개방-폐쇄 원칙

로버트 마틴은 확장 가능하고 변화에 유연하게 대응할 수 있는 설계를 만들 수 있는 원칙 중 하나로 개방-폐괘 원칙(Open-Closed Principle, OCP)을 고안했다.

  • 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.

여기서 키워드는 ‘확장’과 ‘수정’이다. 이 둘은 순서대로 애플리케이션의 ‘동작’과 ‘코드’의 관점을 반영한다.

  • 확장에 대해 열려 있다. 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 ‘동작’을 추가해서 애플리케이션의 기능을 확장할 수 있다.

  • 수정에 대해 닫혀 있다. 기존의 ‘코드’를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있따.

✔️ 컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라

사실 개방-폐쇄 원칙은 런타임 의존성과 컴파일타임 의존성에 관한 이야기다. 런타임 의존성은 실행시에 협력에 참여하는 객체들 사이의 관계다. 컴파일타임 의존성은 코드에서 드러나는 클래스들 사이의 관계다.

✔️ 추상화가 핵심이다

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다.

추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법이다.

public abstract class DiscountPolicy {
	private List<DiscountCondition> conditions = new ArrayList<>();

	public DiscountPolicy(DiscountCondition... conditions) {
		this.conditions = Arrays.asList(conditions);
	}

	public Money calculateDiscountAmount(Screening screening) {
		for (DiscountCondition each : conditions) {
			if (each.isSatisfiedBy(screening)) {
				return getDiscountAmount(screening);
			}
		}

		return screening.getMovieFee();
	}

	abstract protected Money getDiscountAmount(Screening screening);
}

여기서 변하지 않는 부분은 할인 여부를 판단하는 로직이고 변하는 부분은 할인된 요금을 계산하는 방법이다. 따라서 DiscountPolicy는 추상화다. 추상화 과정을 통해 생략된 부분은 할인 요금을 계산하는 방법이다. 우리는 상속을 통해 생략된 부분을 구체화함으로써 할인 정책을 확장할 수 있는 것이다.

단순히 어떤 개념을 추상화했다고 해서 수정에 대해 닫혀 있는 설계를 만들 수 있는 것은 아니다. 개방-폐쇄 원칙에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다. 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다.

public class Movie {
	...
	private DiscountPolicy discountPolicy;

	public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
		...
		this.discountPolicy = discountPolicy;
	}

	public Money calculateMovieFee(Screening screening) {
		return fee.minus(discountPolicy.calculateDiscountAmount(screening));
	}
}

Movie는 할인 정책을 추상화한 DiscountPolicy에 대해서만 의존한다. 의존성은 변경의 영향을 의미하고 DiscountPolicy는 변하지 않는 추상화라는 사실에 주목하라.

먕시적 의존성과 의존성 해결 방법을 통해 컴파일타임 의존성을 런타임 의존성으로 대체함으로써 실행 시에 객체의 행동을 확장할 수 있다. 추상화가 수정에 대해 닫혀있을 수 있는 이유는 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 주의 깊게 선택했기 때문이다.

2. 생성 사용 분리

Movie가 오직 DiscountPolicy라는 추상화에만 의존하기 위해서는 Movie 내부에서 AmountDiscountPolicy같은 구체 클래스의 인스턴스를 생성해서는 안 된다. 아래 코드에서 Movie의 할인 정책을 비율 할인 정택으로 변경할 수 있는 방법은 단 한 가지밖에 없다. 바로 AmountDiscountPolicy의 인스턴스를 생성하는 부분을 PercentDiscountPolicy의 인스턴스를 생성하도록 직접 코드를 수정하는 것뿐이다. 이것은 동작을 추가하거나 변경하기 위해 기존의 코드를 수정하도록 만들기 때문에 개방-폐쇄 원칙을 위반한다.

public class Movie {
	...
	private DiscountPolicy discountPolicy;

	public Movie(String title, Duration runningTime, Money fee) {
		...
		this.discountPolicy = new AmountDiscountPolicy(...);
	}

	public Money calculateMovieFee(Screening screening) {
		return fee.minus(discountPolicy.calculateDiscountAmount(screening));
	}
}

결합도가 높아질수록 개방-폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다.

메시지를 전송하지 않고 객페를 생성하기만 한다면 아무런 문제가 없었을 것이다. 또는 객체를 생성하지 않고 메시지를 전송하기만 했다면 괜찮았을 것이다. 동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것이 문제인 것이다.

유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다. 한 마디로 말해서 객체에 대한 생성과 사용을 분리(separating use from createion)해야 한다.

  • 소프트웨어 시스템은 시작 단계와 실행 단계를 분리해야 한다.

사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.

Movie에게 금액 할인 정책을 적용할지, 비율 할인 정책을 적용할지를 알고 있는 것은 그 시점에 Movie와 협력할 클라이언트이기 때문이다. 현재의 컨텍스트에 관한 결정권을 가지고 있는 클라이언트로 컨텍스트에 대한 지식을 옮김으로써 Movie는 특정한 클라이언트에 결합되지 않고 독립적일 수 있다.

public class Client {
	public Money getAvatorFee() {
		Movie avatar = new Movie("아바타", 
												Duration.ofMinutes(120),
												Money.wons(10000),
												new AmountDiscountPolicy(...));
		return avatar.getFee();
	}
}

✔️ FACTORY 추가하기

생성 책임을 Client로 옮긴 배경에는 Movie는 특정 컨텍스트에 묶여서는 안 되지만 Client는 묶여도 상관이 없다는 전제가 깔려 있다. 하지만 Movie를 사용하는 Client도 특정한 컨텍스트에 묶이지 않기를 바란다고 가정해보자.

Client의 코드를 다시 살펴보면 Movie의 인스턴스를 생성하는 동시에 getFee 메시지도 함께 전송한다는 것을 알 수 있다. Client 역시 생성과 사용의 책임을 함께 지니고 있는 것이다.

Movie를 생성하는 책임을 Client의 인스턴스를 사용할 문맥을 결정할 클라이언트로 옮기는 것이다. 하지만 객체 생성과 관련된 지식이 Client와 협력하는 클라이언트에게까지 새어나가기를 원하지 않는다고 가정해보자.

이 경우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를 사용하도록 만들 수 있다. 이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다.

public class Factory {
	public Movie createAvatarMovie() {
		return new Movie("아바타", 
											Duration.ofMinutes(120),
											Money.wons(10000),
											new AmountDiscountPolicy(...));
	}

}

이제 Client는 Factory를 사용해서 생성된 Movie의 인스턴스를 반환받아 사용하기만 하면 된다.

public class Client {
	private Factory factory;

	public Client(Factory factory) {
		this.factory = factory;
	}

	public Money getAvatarFee() {
		Movie avatar = factory.createAvatarMovie();
		return avatar.getFee();
	}
}

FACTORY를 사용하면 Movie와 AmountDiscountPolicy를 생성하는 책임 모두를 FACTORY를 통해 생성된 Movie 객체를 얻기 위한 것이고 다른 하나는 Movie를 통해 가격을 계산하기 위한 것이다. Client는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도 가지지 않을 수 있다.

✔️ 순수한 가공물에게 책임 할당하기

5장에서 책임 할당 원칙을 패턴의 형태로 기술한 GRASP 패턴에 관해 살펴봤다. 책임 할당의 가장 기본이 되는 원칙은 책임을 수행하는 데 필요한 정보를 가장 많이 알고 있는 INFORMATION EXPERT에게 책임을 할당하는 것이다.

FACTORY를 추가한 이유는 전체적으로 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념에게 할당돼 있던 객체 생성 책임을 도메인 개념과는 아무런 상관이 없는 가공의 객체로 이동시킨 것이다.

모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다. 이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다. 크레이그 라만은 이처럼 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공문)이라고 부른다.

어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는다면 PURE FABRICATION을 추가하고 이 객체에게 책임을 할당하라. 그 결과로 추가된 PURE FACRICATION은 보통 특정한 행동을 표현하는 것이 일반적이다. 따라서 PURE FABRICATION은 표현적 분해보다는 행위적 분해에 의해 생성되는 것이 일반적이다.

설계자로서의 우리의 역할은 도메인 추상화를 기반으로 애플리케이션 로직을 설계하는 동시에 품질의 측면에 균형을 맞추는 데 필요한 객체들을 창조하는 것이다. 레베카 워프스브록의 말을 빌리자면 “애플리케이션 모델은 사용자에게 반응하고, 실행을 제어하며, 외부 리소스에 연결하는 컴퓨터 객체를 이용해 도메인 모델을 보충한다. ” 도메인 개념을 표현하는 객체와 순수하게 창조된 가공의 객체들이 모여 자신의 역할과 책임을 다하고 조화롭게 협력하는 애플리케이션을 설계하는 것이 목표여야 한다

먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하기 시작하라. 만약 도메인 개념이 만족스럽지 못하다면 주저하지 말고 인공적인 객체를 창조하라.

도메인 모델에서 출발해서 설계에 유연성을 추가하기 위해 책임을 이리저리 옮기다 보면 많은 PURE FABRICATION을 추가하게 된다는 사실을 알게 될 것이다. FACTORY는 객체의 생성 책임을 할당할만한 도메인 객체가 존재하지 않을 때 선택할 수 있는 PURE FABRICATION이다.

3. 의존성 주입

생성과 사용을 분리하면 Movie에는 오로지 인스턴스를 사용하는 책임만 남게 된다. 이것은 외부의 다른 객체가 Movie에게 생성된 인스턴스를 전달해야 한다는 것을 의미한다. 이처럼 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입(Dependency Injection)이라고 부른다. 이 기법을 의존성 주입이라고 부르는 이유는 외부에서 의존성의 대상을 해결한 후 이를 사용하는 객체 쪽으로 주입하기 떄문이다.

의존성 주입은 의존성 해결 방법과 관련이 깊다. 의존성 해결은 컴파일러 타임 의존성과 런타임 의존성의 차이점을 해소하기 위한 다양한 메커니즘을 포괄한다. 의존성 주입은 의존성을 해결하기 위해 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내서 외부에서 필요한 런타인 의존성을 전달할 수 있도록 만든느 방법을 포괄하는 명칭이다. 따라서 의존성 주입에서는 의존성을 해결하는 세 가지 방법을 가리키는 별도의 용어를 정의한다.

  • 생성자 주입 (constructor injection) : 객체를 생성하는 시점에 생성자를 통한 의존성 해결

  • setter 주입 (setter injection) : 객체 생성 후 setter 메서드를 통한 의존성 해결

  • 메서드 주입 (method injection) : 메서드 실행 시 인자를 이용한 의존성 해결

다음은 Movie 생성자의 인자로 AmountDiscountPolicy의 인스턴스를 전달해서 DiscountPolicy 클래스에 대한 컴파일타임 의존성을 런타임 의존성으로 대체하는 예를 나타낸 것이다. Movie의 생성자를 이용해 의존성을 주입하기 때문에 생성자 주입이라고 부른다.

Movie avatar = new Movie("아바타", 
			Duration.ofMinutes(120),
			Money.wons(10000),
			new AmountDiscountPolicy(...));

setter 주입은 이미 생성된 Movie에 대해 setter 메서드를 이용해 의존성을 해결한다. setter 주입의 장점은 의존성의 대상을 런타임에 변경할 수 있다는 것이다. 생성자 주입을 통해 설정된 인스턴스는 객체의 생명주기 전체에 걸쳐 관계를 유지하는 반면, setter 주입을 사용하면 언제라도 의존 대성을 교체할 수 있다.

avatar.setDiscountPolicy(new AmountDiscountPolicy(...));

setter 주입의 단점은 객체가 올바로 생성되기 위해 어떤 의존성이 필수적인지를 명시적으로 표현할 수 없다는 것이다. setter 메서드는 객체가 생성된 후에 호출돼야 하기 때문에 setter 메서드 호출을 누락한다면 객체는 비정상적인 상태로 생성될 것이다.

메서드 주입은 메서드 호출 주입(method call injection) 이라고도 부르며 메서드가 의존성을 필요로 하는 유일한 경우일 때 사용할 수 있다. 생성자 주입을 통해 의존성을 전달받으면 객체가 올바른 상태로 생성되는 데 필요한 의존성을 명확하게 표현할 수 있다는 장점이 있지만 주입된 의존성이 한 두 개의 메서드에서만 사용된다면 각 메서드의 인자로 전달하는 것이 더 나은 방법일 수 있다.

avatar.calculateDiscountAmount(screening, new AmountDiscountPolicy(...));

메서드 주입을 의존성 주입의 한 종류로 볼 것인가에 대해서는 논란의 여지가 있다. 개인적으로 외부에서 객체가 필요로 하는 의존성을 해결한다는 측면에서 의존성 주입의 한 종류로 간주한다.

  • 프로퍼티 주입과 인터페이스 주입

✔️ 숨겨진 의존성은 나쁘다

의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. 그중에서 가장 널리 사용되는 대표적인 방법은 SERVICE LOCATOR 패턴이다. SERVICE LOCATOR는 의존성을 해결할 객체들을 보관하는 일종의 저장소다. 외부에서 객체에게 의존성을 전달하는 의존성 주입과 달리 SERVICE LOCATOR의 경우 객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청한다.

의존성을 구현 내부로 감출 경우 의존성과 관련된 문제가 컴파일 타임이 아닌 런타임에 가서야 발견된다. 숨겨진 의존성이 이해하기 어렵고 디버깅하기 어려운 이유는 문제점을 발견할 수 있는 시점을 코드 작성 시점이 아니라 실행 시점으로 미루기 때문이다.

의존성을 숨기는 코드는 단위 테스트 작성도 어렵다.

숨겨진 의존성을 캡슐화를 위반한다. 단순히 인스턴스 변수의 가시성을 private으로 선언하고 변경되는 내용을 숨겼다고 해서 캡슐화가 지켜지는 것은 아니다.

캡슐화는 코드를 읽고 이해하는 행위와 관련이 있다. 클래스의 퍼블릭 인터페이스만으로 사용 방법을 이해할 수 있는 코드가 캡슐화의 관점에서 훌륭한 코드다. 클래스의 사용법을 익히기 위해 구현 내부를 샅샅이 뒤져야 한다면 그 클래스의 캡슐화는 무너진 것이다.

가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라. 의존성을 구현 내부에 숨기면 숨길수록 코드를 이해하기도, 수정하기도 어려워진다.

요점은 명시적인 의존성에 추점을 맞추는 것이다. 그리고 이 방법이 유연성을 향상시키는 가장 효과적인 방법이다.

4. 의존성 역전 원칙

✔️ 추상화와 의존성 역전

public class Movie {
	private AmountDiscountPolicy discountPolicy;
}

이 설계가 변경에 취약한 이유는 요금을 계산하는 상위 정책이 요금을 계산하는 데 필요한 구체적인 방법에 의존하기 때문이다. Movie는 가격 계산이라는 더 높은 수준의 개념을 구현한다. 그에 비해 AmountDiscountPolicy는 영화의 가격에서 특정한 금액만큼을 할인해주는 더 구체적인 수준의 매커니즘을 담당하고 있다. 다시 말해서 상위 수준 클래스인 Movie가 하위 수준 클래스인 AmountDiscountPolicy에 의존하는 것이다.

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. Movie와 AmountDiscountPolicy 사이의 협력이 가지는 본질은 영화의 가격을 계산하는 것이다. 어떻게 할인 금액을 계산할 것인지는 협력의 본질이 아니다. 다시 말해서 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스다.

그러니 이런 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다. 하위 수준의 AMountDiscountPolicy를 PercentDiscountPolicy로 변경한다고 해서 상위 수준의 Movie가 영향을 받아서는 안 된다. 상위 수준의 Movie의 변경으로 인해 하위 수준의 AmountDiscountPolicy가 영향을 받아야 한다.

의존성은 변경의 전파와 관련된 것이기 때문에 설계는 변경의 영향을 최소화하도록 의존성을 관리해야 한다. 의존성은 Movie에서 AmountDIscountPolicy로 흘러서는 안 된다. AmountDiscountPolicy에서 Movie로 흘러야 한다. 상위 수준의 클래스는 어떤 식으로든 하위 수준의 클래스에 의존해서는 안 되는 것이다.

중요한 것은 상위 수준의 클래스다. 상위 수준의 변경에 의해 하위 수준이 변경되는 것은 납득할 수 있지만 하위 수준의 변경으로 인해 상위 수준이 변경돼서는 곤란하다. 하위 수준의 이슈로 인해 상위 수준에 위치하는 클래스들을 재사용하는 것이 어렵다면 이것 역시 문제가 된다.

이 경우에도 해결사는 추상화다. Movie와 AmountDIscountPolicy 사이에 추상 클래스인 DiscountPolicy가 자리 잡고 있는 이유다.

가장 중요한 조언은 추상화에 의존하라는 것이다. 유연하고 재사용 가능한 설계를 원한다면 모든 의존성의 방향이 추상 클래스나 인터페이스와 같은 추상화를 따라야 한다. 구체 클래스는 의존성의 시작점이어야 한다. 의존성의 목적지가 돼서는 안된다.

1. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다 .
2. 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다. 

⇒ 이를 **의존성 역전 원칙(Dependency Inversion Principle, DIP)**이라고 부른다.

✔️ 의존성 역전 원칙과 패키지

역전은 의존성의 방향뿐만 아니라 인터페이스의 소유권에도 적용된다는 것이다. 개체지향 프로그래밍 언어에서 어떤 구성 요소의 소유권을 결정하는 것은 모듈이다. 자바는 패키지를 이용해 모듈을 구현하고, C#이나 C++는 네임스페이스를 이용해 모듈을 구현한다.

코드의 컴파일이 성공하기 위해 함께 존재해야 하는 코드를 정의하는 것이 바로 컴파일타임 의존성이다.

불필요한 클래스들을 같은 패키지에 두는 것은 전체적인 빌드 시간을 가파르게 상승시킨다.

Movie의 재사용을 위해 필요한 것이 DiscountPolicy 뿐이라면 DiscountPolicy를 Movie와 같은 패키지로 모으고 AmountDiscountPolicy롸 PercentDiscountPolicy를 별도의 패키지에 위치시켜 의존성 문제를 해결할 수 있다.

추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 그리고 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다. 마틴 파울러는 이 기법을 가리켜 SEPARATED INTERFACE 패턴이라고 부른다.

의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.

유연하고 재사용 가능하며 컨텍스트에 독립적인 설계는 전통적인 패러다임이 고수하는 의존성의 방향을 역전시킨다. 전통적인 패러다임에서는 상위 수준 모듈이 하위 수준 모듈에 의존했다면 객체지향 패러다임에서는 상위 수준 모듈과 하위 수준 모듈이 모두 추상화에 의존한다. 전통적인 패러다임에서는 인터페이스가 하위 수준 모듈에 속했다면 객체지향 패러다임에서는 인터페이스가 상위 수준 모듈에 속한다.

훌륭한 객체지향 설계를 위해서는 의존성을 역전시켜야 한다. 그리고 의존성을 역전시켜야만 유연하고 재사용 가능한 설계를 얻을 수 있다.

5. 유연성에 대한 조언

✔️ 유연한 설계는 유연성이 필요할 때만 옳다

유연하고 재사용 가능한 설게란 런타임 의존성과 컴파일타임 의존성의 차이를 인식하고 동일한 컴파일 타임 의존성으로부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지는 설계를 의미한다. 하지만 유연하고 재사용 가능한 설계가 항상 좋은 것은 아니다. 설계의 미덕은 단순함과 명확함으로부터 나온다. 단순하고 명확한 설계를 가진 코드는 읽기 쉽고 이해하기도 편하다. 유연한 설계는 이와는 다른 길을 걷는다. 변경하기 쉽고 확장하기 쉬운 구조를 만들기 위해서는 단순함과 명확함의 미덕을 버리게 될 가능성이 높다.

유연성은 항상 복잡성을 수반하다.

✔️ 협력과 책임이 중요하다

객체의 협력과 책임이 중요하다. 지금까지 클래스를 중심으로 구현 매커니즘 관점에서 의존성을 설명했지만 설계를 유연하게 만들기 위해서는 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요하다.

설계를 유연하게 만들기 위해서는 먼저 역할, 책임, 협력에 초점을 맞춰야 한다.

책임의 불균형이 심화되고 있는 상태에서 객체의 생성 책임을 지우는 것은 설계를 하부의 특정한 매커니즘에 종속적으로 만들 확률이 높다. 불필요한 SINGLETON 패턴은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있다. 핵심은 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절하다는 것이다.

역할, 책임, 협력의 모습이 선명하게 그려지지 않는다면 의존성을 관리하는 데 들이는 모든 노력이 물거품이 될 수도 있다는 사실을 명심하라.

Last updated