Java/객체지향

객체 지향과 디자인 패턴 Chapter 03 (1~3) - 상속, 다형성, 추상화

_Jin_ 2024. 8. 29.
더보기
'개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴'을 읽고 학습합니다. ( 최범균 저 )

 

객체 지향의 패러다임을 통한 프로그래밍으로 얻을 수 있는 이점은 구현 변경에 대한 유연성이며. Chapter 02에서는 캡슐화를 통해 객체를 사용하는 다른 코드에서 변경에 대한 영향을 최소화하며 객체 자체의 코드 변경에 대한 유연함을 얻는 방법에 대해서 알아보았다. 유연함을 얻을 수 있게 만들어 주는 방법에는 ' 추상화 '에 있는데, 이에 대해서 학습한 내용을 정리해보겠다. 

 

그 이전에 추상화를 가능하게 만들어주는 ' 다형성 '과 ' 상속 ' 대해서 알아보겠다. 

 

상속

다형성을 알아보기 이전에 상속에 대한 내용 파악이 필요하므로 가볍게 알아보자. ( 아마 객체 지향 언어를 학습했다면, 상속에 대한 내용은 기본적인 문법으로 대게 알고 있고 있으며 필자도 알지만, 내용의 연속성과 복습을 위해서 간단하게 정리해보겠다. )  

 

상속은 하나의 타입을 그대로 사용하면서도 구현을 추가할 수 있도록 해주는 방법을 제공하는 것이다. 

 

예를 들어서 금액 할인에 사용되는 쿠폰을 표현하기 위해서 Coupon이라는 클래스를 작성했다고 생각해보자.

public void Coupon {
	
    private int discountAmount;
    // 할인 금액 초기 세팅 생성자 
    public Coupon(int discountAmount) {
    	this.discountAmount = discount;
    }
    // 할인 금액 반환
    public int getDiscountAmount() {
    	return discountAmount;
    }
    // 할인 금액 계산
    public int calculateDiscountedPrice(int price){
    	if(price < discountAmount)
        	return 0;
        return price - discountAmount;
    }
}

 

그리고 위의 객체를 사용해서 할인 금액을 구하는 코드는 다음과 같을 것이다. 

Coupon coupon = new Coupon(3000);
int price = coupon.calculateDiscountedPrice(product.getPrice());

 

요구 사항이 추가되어서, 새로운 쿠폰 기능을 제공하는 경우가 생겼다고 고려해보자.

이제 상속을 사용해서 새로운 쿠폰 기능이 있는 클래스를 만들면 되겠다.

 

public class LimitPriceCoupon extends Coupon {
	private int limitPrice;
    
    public LimitPriceCoupon( int limitPrice, int dicountAmount ) {
    	super(dicountAmount);
        this.limitPrice = limitPrice;
    }
    
    public int getLimitPrice() {
    	return limitPrice;
    }
    
    @Override
    public int calculatedDiscountedPrice(int price) {
    	if(price < limitPrice)
        	return	price;
            
        return super.claculatedDiscountedPrice(price);
    }
}


이 코드는 앞서 Coupon 클래스의 상속을 받고 있으며, 따라서 부모의 기능들(메서드들)을 사용할 수 있다.

아래의 코드를 통해 보자.

LimitPriceCoupon lpCoupon = new LimitPriceCoupon(5000, 1000);
int discountAmount = lpCoupon.getDiscountAmount(); // Coupon(부모클래스)에서 물려 받음
int limitPrice = lpCoupon.getLimitPrice(); // LimitPriceCoupon 클래스에서 정의

 

여기서 부모 클래스를 상속받은 자식 클래스는 부모가 지닌 속성(private 제외)을 공유하고 사용할 수 있다는 점이다. 

 

 

다형성과 상속

다형성은 하나의 객체가 여러 모습(타입)을 가질 수 있도록 구성되는 것을 의미한다. 이는 하나의 객체가 A,B,C 등등의 다양한 타입에 따른 사용이 가능함을 의미한다. 

 

코드 수준에서 살펴보자.

public class Plane{
	public void fly(){
    	// 비행 코드
    }
}

public interface Turbo {
	public void boost();
}

public class TurboPlane extends Plane implements Turbo {
	public void boost() {
    	// 가속 코드
    }
}

 

위의 코드는 부모 - 자식 관계인 두 개의 클래스와 인터페이스가 있다. 

이런 경우, 자식 클래스인 TurboPlane은 Plane 타입( 클래스 ) 와 Turbo타입( 인터페이스 )에 정의된 메서드의 실행을 요청할 수 있다. 

 

TurboPlane tp = new TurboPlane();
tp.fly(); // Plane에 정의 및 구현된 메서드 실행
tp.boost(); // Turbo에 정의되고 TurboPlane에 구현된 메서드 실행

 

 

그리고, TurboPlane 타입의 객체를 Plane 타입이나 Turbo 타입에 할당하는 것도 가능하다. 

TurboPlane tp = new TurboPlane();

Plane p = tp; // TurboPlane 객체는 Plane 타입도 된다. 
p.fly();

Turbo t = tp; // TurboPlane 객체는 Turbo 타입도 된다. 
t.boost();

 

즉, 자식 클래스는 상속 받은 클래스나 인터페이스의 타입이 될 수 있으며, 해당 기능들을 사용할 수 있다. 

 

 

인터페이스 상속과 구현 상속

객체 지향 언어는 다형성의 구현을 위해서 타입을 상속받는 방법을 사용하고, 크게 구현 상속과 인터페이스 상속으로 구분된다. 먼저 자바에서 인터페이스 상속은 타입 정의만을 상속받는 것으로 클래스는 다중 상속을 지원하지 않기에 인터페이스를 이용해서 객체가 다형성을 가질 수 있다. 만약 객체가 인터페이스를 상속받으면, 인터페이스에 정의된 메서드를 구현하여 사용하게 된다.

한편, 구현 상속은 클래스 상속을 통해서 이루어지며 보통 상위 클래스에 정의된 기능을 재사용하기 위한 목적으로 사용된다. 단 위의 코드처럼 override( 재정의 )를 통해서 자식 객체는 부모 객체에 정의된 기능을 자신에 맞게 수정할 수도 있다.

 

구현 상속을 통해 매서드를 재정의하고, 사용하는 경우에 대해서 보자.

public class TurboPlane extends Plane {
	
    @override
    public void fly() {
    	// Plane에 정의된 fly() 구현을 오버라이딩
        // TurboPlane의 구현
    }
}
Plane p = new TurboPlane();
p.fly(); // TurboPlane의 fly() 실행

 

이처럼 부모 클래스를 상속받고 메서드를 재정의하고 

p 변수의 타입을 부모 타입으로 정의하면서 TurboPlane 객체로 생성했다. 

그리고 fly()를 실행하면, 이는 p 타입이 Plane이기에 Plane 클래스의 fly()가 실행되었다고 고려할 수도 있지만, p가 가르키는 실제 객체는 TurboPlane이기에 TurboPlane 클래스의 fly() 메서가 호출된다. 

 

 

추상 타입과 유연함

추상화란 데이터나 프로세스 등에서 의미가 비슷한 개념이나 표현으로 정의하는 과정이다. 아래 그림을 통해 이해해보자.

 

출처 :&amp;amp;amp;nbsp;https://m.blog.naver.com/songintae92/221374047225 출처: https://overcome-the-limits.tistory.com/363

 

특정 프로그램을 만듦에 있어, 위의 기능 3가지가 필요하다. 그리고 세 기능 모두 공통적으로 '로그 수집'이라는 프로세스가 요구되었다. 이에 세 기능에서 공통적으로 필요로하는 기능을 로그 수집이라는 추상화를 통해 정의할 수 있는 것이다.

 

이를 실제 코드의 클래스에 적용하면 아래와 같을 것이다. 

class FtpLogFileDownloader implements LogCollector{
	...
}

class SocketLogReader implements LogCollector {
	...
}

class DbTableLogGateway implements LogCollector {
	...
}

// 로그 수집의 추상화
interface LogCollector {
	public collect();
}

 

이처럼 인터페이스를 통해 로그 수집에 대한 부분을 추상화하였다. 이 추상화 된 타입(인터페이스)은 실제 구현을 제공하지 않는다. 다만, 로그 정보를 수집한다는 의미만 제공할 뿐인 것이다. 

 

 

추상 타입과 실제 구현의 연결

추상 타입(인터페이스)와 실제 구현 클래스(클래스)는 상속을 통해 연결될 수 있음을 앞서 살펴보았다. 즉, 구현 클래스가 인터페이스를 상속받는 방법으로 연결될 수 있다는 것이다. 

 

출처 :&amp;amp;amp;nbsp;https://m.blog.naver.com/songintae92/221374047225 출처: https://overcome-the-limits.tistory.com/363

// createLogCollector ()는 알맞은 구현 클래스의 객체를 생성
LogCollector collector = createLogCollector();
collector.collect();

 

위와 같은 코드에서 collector.collect(); 는 실제 collector 객체 타입의 collect() 메서드를 호출한다. 

 

만약 createLogCollector()가 SocketLogReader 클래스의 객체를 생성하면, collector.collect()는 SocketLogReader 타입의 collect() 메서드를 실행하고 다른 클래스의 객체를 생성했다면, 해당 객체에 맞는 collect() 메서드를 실행하는 것이다.

 

 

추상 타입을 이요한 구현 교체의 유연함

그럼, 추상 타입을 사용하는 이유는 무엇일까?

아래와 같은 콘크리트( 구상 ) 클래스를 사용해도 문제가 되지 않음에도 불구하고 말이다.

SocketLogReader reader = new SocketLogReader();
reader.collect();

 

물론, 구현 자체에는 큰 문제가 발생하지 않는다. 

그럼에도 불구하고 객체 지향에서 추상화가 필요한 이유가 분명 있을 것이다. 

이를 파악하기 위해, 파일을 읽고 암호화하는 프로그램을 만드는 과정 예시로 살펴보겠다. 

 

 

파일 암호화 프로그램의 클래스 구조

public class FlowContoller {
	
    public void process() {
    
    	// 파일 읽기 객체 생성 및 데이터 읽기
    	FileDataReader reader = new FileDataReader();
        byte[] data = reader.read();
        
        // 데이터 암호화
        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);
        
        // 파일 쓰기 
        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}

 

이처럼 크게 파일 데이터를 읽고 암호화하여 쓰는데 아무런 문제가 발생하지 않는다. 

그런데, 소켓을 통해서 데이터를 읽고 암호화할 수 있도록 해달라는 추가 요구 사항이 들어왔다. 

 

이에 아래의 코드처럼 소켓을 통해 데이터를 읽는 기능을 추가하였다.

public class FlowContoller {
	
    
    public void process() {
    
    	byte[] data = null;
        
        // 파일 읽기 객체 생성 및 데이터 읽기
        if(useFile) {
        	FileDataReader fileReader = new FileDataReader();
            data = filReader.read();
        } else {
        	SockectDataReader socketReader = new SocketDataReader();
            data = SocketReader.read();
        }        
        
        // 데이터 암호화
        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);
        
        // 파일 쓰기 
        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}

그런데, 코드의 형태가 좋지 않아 보인다.

if - else 문을 사용해서 필요한 기능을 처리하는 코드 구성이 만약 기능이 추가된다면, 앞서 유지 보수의 어려움을 주는 절차 지향의 코드 형식을 따를 것이다.

이는 FlowController 본연의 책임인 흐름 제어와 상관없이 기능에 따른 데이터 읽기 구현의 변경에 따른 책임을 할당(구현)하기에 생기는 문제이다. 

 

그럼 FlowController와 상관없는 책임을 해당 객체에서 제거해주는 것이 바람직한데, 이를 위해 추상화를 사용하자.

 

두 기능 사이에는 ' 어떤 곳으로 바이트 데이터 읽기 '라는 공통점이 있다.

이를 추상 타입으로 만들어보자. ( 인터페이스 )

public interface ByteSource {
	public byte[] read();
}

 

그리고 이를 바이트 타입을 읽는 객체가 상속받도록 설계할 수 있다.

 

public class FileDataReader implements ByteSource {
	pubilc byte[] read(){
    ...
    }
}

public class SockectDataReader implements ByteSource {
	...
}

바이트로 데이터를 읽어오는 과정에서 필요한 기능마다 객체로 생성한 뒤, 인터페이스인 ByteSource 타입을 상속받고 두 객체는 ByteSource 타입으로 동작할 수 있게 되었다. 

이는 FlowController의 코드에서 ByteSource를 사용하도록 수정이 가능함을 의미한다. 

 

...
ByteSource source = null;

if(useFile)
	source = new FileDataReader();
else
	source = new SocketDataReader();
    
byte[] data = source.read();

 

이전의 코드보다 약간 단순해졌지만, 여전히 if-else 블록이 남아있으며, 새로운 구현이 필요한 경우 if-else문으로 처리하는 구조이다. 

 

이를 더욱 추상화하여 해결하려면, ' ByteSource 타입의 객체를 생성한다. ' 는 공통적인 내용이 있으며 위의 코드에서 if-else로 처리되고 있다.

 

이처럼 ByteSource의 종류가 변경되더라도 FlowController가 바뀌지 않도록 하는 방법에는 아래의 두 가지 방법이 존재한다.

 

▶ ByteSource 타입의 객체를 생성하는 기능을 별도 객체로 분리하고, 그 객체로 생성

▶ 생성자(또는 다른 메서드)를 이용하여 생성할 객체 생성

 

이 중 첫 번째 방법을 사용해서 문제를 해결해보자( 두 번째는 DI로 추후에 다룬다. )

 

public class ByteSourceFactory {

	public ByteSource create() {
    	if(useFile()) {
        	return new FileDataReader();
        } else {
        	return new SocketDataReader();
        }
    }
    
    private boolean useFile() {
    	String useFileVal = System.getProperty("useFile");
        return useFileVal == null && Boolean.valueOf(useFileVal);
    }
    
    // 싱글톤
    private static ByteSourceFactory instance = new ByteSourceFactory();
    public static ByteSourceFactory getInstance() {
    	return instance;
    }
    
    private ByteSourceFactory() {}
}

 

ByteSourceFactory 클랙스는 ByteSource 타입의 객체를 생성하는 과정을 추상화한 코드이다. 

 

public class FlowContoller {
	
    
    public void process() {
    
    	ByteSource source = ByteSourceFactory.getInstance().create();
        byte[] data = source.read();
        
        // 데이터 암호화
        Encryptor encryptor = new Encryptor();
        byte[] encryptedData = encryptor.encrypt(data);
        
        // 파일 쓰기 
        FileDataWriter writer = new FileDataWriter();
        writer.write(encryptedData);
    }
}

그리고 FlowController 클래스에 적용하면 위와 같다. 

 

이를 통해 

▶ ByteSource의 종류 변경에도, FlowController 클래스의 변경이 발생하지 않는다.

▶ FlowController의 제어 흐름의 변경에 따라 ByteSource 객체 생성하는 부분은 영향 받지 않는다.

와 같이 객체 지향적 흐름으로 수정에 유연함을 얻을 수 있다. 

 

이는 ' 책임의 크기 ' 문제와도 관련이 깊다. 책임의 크기가 작을수록 변경에 따른 유연함을 얻을 수 있다고 했는데, 최초의 FlowController는 process()에서 데이터를 읽는 객체를 직접 생성하고,  데이터를 읽어왔다. 즉, 생성과 흐름 제어의 책임을 동시에 지니고 있던 것이다. 

 

하나의 클래스에서 동시에 두 개의 책임을 지니고 있다보니, 하나의 책임 변경에 따른 다른 책임도 이에 맞춰 변경이 필요한 구조로, 각 책임을 분리할 필요가 있었다. 

 

그리고 아래와 같은 순서로 추상화하여 이를 분리하였다. 

 

  1) 바이트 데이터 읽기 : ByteSource 인터페이스 도출
  2) ByteSource 객체 생성 : ByteSourceFactory 도출

 

이처럼, 추상화는 공통된 개념을 도출하여 추상 타입을 정의하면서, 많은 책임을 지닌 객체의 책임을 분리하는 촉매제 역할을 담당한다. 

 

 

변화되는 부분을 추상화하기 

추상화를 통해서 변경의 유연함을 얻을 수 있다는 점과 점진적인 과정을 살펴보았다.

하지만 너무나도 많은 경우와 상황이 있기에 미리 변경 지점을 알고, 추상화를 통해서 유연한 설계를 만드는 것은 쉬운 것이 아니다. 

 

그럼에도 추상화할 수 있는 방법이 하나 있는데, 이는 변화하는 부분에 대해 추상화를 하는 것이다. 

요구 사항에 따라 변경되는 지속적으로 변경될 소지가 많기에 추상 타입으로 교체하면 향후 변경에 유연한 구조를 가질 가능성이 높아진다. 

 

앞서 추상화한 FlowController는 요구 사항의 변경에 따라 변화하는 부분에 있어 추가적 요구 사항이 생겨도 FlowController의 코드는 변경하지 않으며 새로운 요구를 적용할 수 있게 되었다. 

 

public class FlowController {
	
    public void process() {
    
    	FileDataReader reader = new FileDataReader(); // 필요하면 구현체만 바꾸면 된다.
        byte[] data = reader.read();
        
        ... 암호화
        
        ... 파일 쓰기
    }
}

 

그리고, 추상화 이전의 코드는 if-else 블록이 계속 추가되는 모습으로 요구 사항의 변경에 따라서 함께 변경되는 문제를 가지고 있었다. 이는 제어 흐름 이외의 추가적인 책임을 하나의 객체가 가지고 있어서 생기는 문제로 흐름 제어 이외에 변화하는 두 가지를 추상화했다.

 

요구 사항의 변경에 맞춰 변경되는 부분을 ByteSource와 ByteSourceFactory로 추상화

 

 

인터페이스에 대고 프로그래밍하기 

' 인터페이스에 대고 프로그래밍하기 ' 는 객체 지향의 유명한 규칙 중 하나이다. 

 

이 말은 실제 구현을 제공하는 콘크리트 클래스를 사용하여 프로그래밍하지 말고, 기능을 정의한 인터페이스를 사용해서 프로그래밍하라는 의미이다. 

 

인터페이스는 최초 설계에서 바로 도출되는 경우가 적으며 요구 사항의 변경과 함께 점진적으로 도출이 된다. 즉, 인터페이스는 새롭게 발견된 추상 개념을 통해서 도출되는 것이다. 

위의 FlowController의 경우도 파일이 아닌 소켓에서도 파일을 읽는 것과 같은 추가 요구 사항에 맞추어 인터페이스를 도출하였다. 

 

추상 타입을 사용하면, 기존 코드를 사용하지 않으면서도 콘크리트 클래스를 교체할 수 있는 유연함을 얻을 수 있는데, ' 인터페이스에 대고 프로그래밍하기 ' 규칙은 이 추상화를 통한 유연함을 얻기 위한 규칙인 것이다.

 

그런데, 유연함을 얻기 위해서 타입이 증가하고 구조가 복잡해지는 점도 있기에 모든 곳에서 인테페이스를 남용해선 안된다. 변화 가능성이 높은 경우에 한하여 사용하는 것이 바람직하다.

 

 

인터페이스는 인터페이스 사용자 입장에서 만들기 

인터페이스를 활용하여 변경에 따른 유연한 대응이 가능한 코드를 설계하기 위해 FileDataReader만 필요한 상황에서 인터페이스를 도입했다고 고려해보자.

 

기존의 코드 구성
인터페이스를 사용한 구성 ( <<Annotation>> 이 아니라 <<Interface>> 이다. )

 

그리고 소켓을 이용해 데이터를 읽어오는 추가 요구사항에 따라 기능을 추가해야한다면?

도입한 인터페이스를 통해 아래와 같은 구조로 유연하게 대응하며, 인터페이스를 상속받은 클래스의 Byte 데이터를 읽어 올 수 있다.

 

그러나, 실제로 이 인터페이스를 사용하는 코드는 FlowController이며, 후에 정확한 코드 로직을 잊어버리면 FileDataReaderIF라는 이름은 그 기능을 명확히 알기 어렵다. 직관적으로는 모두 '파일'로 데이터를 읽어 온다고 생각되기 때문이다.( 실상은 Byte 데이터를 읽어오는 것인데..) 

이 경우에는 인터페이스 이름을 ByteSource로 사용하는 것이 의미를 더 명확하게 드러낸다.

 

즉, 인터페이스를 작성할 때는 그 인터페이스를 사용하는 코드 입장에서 작성하는 것이 좋다.

새로운 요구사항에 따른 구현체 추가 ( <<Annotation>> 이 아니라 <<Interface>> 이다. )

 

 

인터페이스와 테스트

지금까지 '추상화' 과정을 통해서 ByteSource 인터페이스를 도출하여 변경에 유연한 구성의 코드 설계를 알아보았다. 

이와 같은 인터페이스를 통한 유연성은 테스트하기 좋은 코드와도 일맥상통하는 부분이 있다.

 

예를 들어 ByteSource 인터페이스를 도입하기 이전의 상황에서 코드를 다시 살펴보자.

FlowController 클래스 코드는 FileDataReader 클래스를 직접 사용했었다.

public class FlowController {
	public void process() {
    
    	FileDataReader reader = new FileDataReader();
    	byte[] data = reader.read();
        ...
    }
}

 

그리고 두 명의 서로 다른 개발자가 FlowController 클래스와 FileDataReader 클래스를 개발한다고 고려해보자.

 

만약, FlowController 클래스를 개발하던 사람이 해당 클래스의 로직 작성을 완료하여서, 정상적으로 동작하는지 테스트해 보고 싶다. 이에 아래와 같은 테스트 코드를 작성했다.

public void testProcess() {
	
    FlowController tc = new FlowController();
    tc.process();
    // 결과가 정상적으로 작동하는지 확인하는 코드
    ...
}

 

그러나. 아직 FileDateReader 클래스의 구현이 완료되지 않아서 

FileDataReader.read() 메서드가 정상적으로 데이터를 제공하지 않아서 FlowController 클래스에 대한 테스트가 불가능하다.

 

반면에 FlowController 클래스가 ByteSource 인터페이스를 사용하여 프로그래밍 되어있고 

생성자를 통해서 사용할 ByteSource 타입 객체를 받는 방식으로 구현되었다면??

일단 코드를 통해서 살펴보자.

public class FlowController {

    private ByteSource byteSource;
	// 생성자를 통한 주입
    public FlowController(ByteSource byteSource) {
    	this.byteSource = byteSource;
    }
    
    public void process() {
    	byte[] data = byteSource.read();
        ...
    }
}

 

이에 해당하는 테스트 코드는 아래와 같을 것이다.

public void testProcess(){

    ByteSource fileSource = new FileDataReader();
    FlowController tc = new FlowController(fileSource);
    tc.process();
    
    // 테스트 코드
    ...
}

 

지금의 테스트 코드에서도 FileDataReader 클래스의 구현이 완료되지 않았기에 정상적으로 작동하지는 않을 것이다. 

하지만, ByteSource 인터페이스를 사용하고 있기에 FileDataReader 클래스의 구현이 완료되지 않았더라도 FlowController 클래스를 테스트가 가능해진다.

 

아래의 코드를 통해서 방법을 확인해보자.

public void testProcess() {
	
    ByteSource mockSource = new MockByteSource();
    FlowController tc = new FlowController(mockSource);
    tc.process();
    // 결과가 정상적으로 작동하는지 확인하는 코드
    ... 
    
}

class MockByteSource implements ByteSource {
	
    public byte[] read() {
    
    	byte[] data = new byte[128];
        // data를 테스트 목적에 따른 데이터로 초기화
        
        return data;
    }
}

 

MockByteSource 클래스는 ByteSource 인터페이스를 상속받아 구현하고 있는데, 

이 클래스의 read() 메서드는 테스트에 필요한 byte 데이터를 직접 생성하는 하드 코딩의 로직이다. 

그리고 테스트에서 이 MockByteSource를 사용하면 정상적인 로직을 확인할 수 있으며, FileDataReader 클래스 없이도 테스트가 가능해진다. 

 

이처럼 실제 콘크리트 클래스를 사용하는 대신 진짜처럼 작동하는 객체를 Mock 객체라고 부른다. 

Mock 객체를 만드는 방법은 다양하지만, 사용할 대상을 인터페이스로 추상화하면, 더욱 쉽게 Mock 객체를 만들 수 있으며, 이는 사용할 코드의 완성을 기다리지 않고도 내가 만든 코드를 먼저 빠르게 테스트 할 수 있도록 만든다.

 

이와 같이 테스트를 쉽게 만들어주는 코드를 고려하여 작성하다보면, 테스트 대상이 되는 클래스와 구분되는 책임을 지닌 객체를 따로 도출하게 된다.

객체 지향 설계가 객체마다 알맞는 책임을 할당하고 각 객체가 주고받는 메세지를 정의하는 과정임을 고려하면 결국 테스트하기 좋은 구조는 객체 지향 설계를 유도하게 된다. 그리고 이러한 프로그래밍 방식을 TDD라고 부른다. 

댓글