개발자가 정복해야 할 객체지향-4

조립을 이용한 재사용

  • 객체 조립은 여러 객체를 묶어서 더 복잡한 기능을 제공하는 객체를 만들어 내는 것이다.
  • 객체지향 언어에서 객체 조립은 보통 필드에서 다른 객체를 참조하는 방식으로 구현된다.
    public class FlowController {
      //필드로 조립
      private Encryptor encrytor = new Ecrytor();
        
      public void process() { 
    		//....
    		byte[] encrytedData = encryptor.encrytir(data);
      }
    }
    
  • FlowController 클래스은 Encrytor클래스의 암호화 기능을 사용하고 있다.
  • 한 객체가 다른 객체를 조립해사 필드로 갖는다는 것은 다른 객체의 기능을 사용한다는 의미이다.
  • 조립을 이용하면 불필요한 클래스 증가를 방지할 수 있다.
  • 런타임에 조립 대상 객체를 교체할 수 있다.
  • 상속보다는 객체 조립을 사용할 것.
  • 변경의 관점에서 객체 조립을 먼저 고민하라.

위임

  • 내가 할 일을 다른 객체에게 넘긴다는 의미
  • 객체를 새로 생성해서 요청을 전달한다 해도 위임이란 의미에서 벗어나지 않는다.

상속은 언제 사용하나?

  • 재사용이라는 관점이 아닌 기능의 확장이라는 관점에서 상속을 적용해야 한다.
  • 명확한 IS-A 관계에서 점진적으로 상위 클래스의 기능을 확장해 나갈 때 사용할 수 있다.

설계원칙 : SOLID

  • 단일 책임 원칙(SRP)
  • 개방-폐쇄 원칙(OCP)
  • 리스코프 치환원칙(LSP)
  • 인터페이스 분리 원칙(ISP)
  • 의존 역전 원칙(DIP)

단일 책임 원칙

  • 클래스는 단 한 개의 책임을 가져야 한다.
  • 책임의 개수가 많아질수록 한 책임의 기능 변화가 다른 책임에 주는 영향은 비례해서 증가한다.
  • 단일 책임 원칙을 어길 시 재사용을 어렵게 한다.
  • 메서드를 실행하는 것이 누구인지 확인해 보는 것으로 단일 책임 원칙을 지킬수 있다.

개방 폐쇄 원칙

  • 확장은 열려있어야 하고, 변경에는 닫혀 있어야 한다.
  • 기능을 변경하거나 확장할 수 있으면서, 그 기능을 사용하는 코드는 수정하지 않는다.
  • 확장되는 부분을 추상화해서 표현한다.
  • 상속을 이용하는 방법 : 상위 클래스의 기능을 그대로 사용하면서 하위 클래스에서 일부 구현을 오버라이딩 할 수 있는 방법을 제공한다.
개방 폐쇄 원칙이 깨질 때의 주요 증상
  • 추상화와 다형성이 제대로 지켜지지 않은 코드는 개방 폐쇄 원칙을 어기게 된다.
  • 다운 캐스팅을 할 경우 개방 폐쇄 원칙이 깨진다.
    public void drawCharacter(Character character) {
      public void drawCharacter(Character character) {
    	//타입확인
    	if(character instanced Missile) {
      	//타입 다운 캐스팅
      	Missile missile = (Missile)character;
      	missile.drawSpecifice();
    	} else {
      	character.draw();
    	}
    }
    
  • 특정 타입인 경우에 별도 처리를 하도록 drawCharacter() 메서드를 구현한다면 drawCharacter() 메서드는 Character 클래스가 확정될 때 함께 수정된다. 변경에 닫혀있지 않다.
  • 비슷한 if-else 블록이 존재할 경우 개방 폐쇄 원칙이 깨진다.

개방 폐쇄 원칙은 유연함에 대한 것

  • 변화가 예상되는 것을 추상화해서 변경의 유연함을 얻도록 해준다.
  • 코드에 대한 변화 요구가 발생하면, 변화와 관련된 구현을 추상화해서 개방 폐쇄 원칙을 맞게 수정할 수 있는지 확인하는 습관을 갖도록 해야한다.

리스코프 치환 원칙

  • 개방 폐쇄 원칙을 받쳐 주는 다형성에 관한 원칙을 제공한다.
  • 상위 타입의 객체를 하위 타입의 객체로 지환해도 상위 타입을 사용하는 프로그램은 정상적으로 작동해야 한다.
    //상위 타입인 SuperCl;ass를 이용하는 메서드
    public void someMethod(superClass sc) {
      sc.someMethod();
    }
    //하위 타입의 객체를 전달해도 정상적으로 동작해야한다.
    someMethod( new SubClass() );
    

리스크포 치환원칙을 지키지 않을 때의 문제

  • 대표적인 예가 직사각형-정사각형 문제이다.
    public class Rectangle {
      private int width;
      private int height;
    
      public void setWidth(int width) {
      	this.width = width;
    	}
      public void setHeight(int height) {
          this.height= height;
    	}
    	public int getWidth() {
      	return width;
    	}
    	public int getHeight() {
      	return height;
    	}
    }
    
    //정사각형을 직사각형의 특수한 경우로 보고 정사각형을 표현하기 위한 Square 클래스가 Rectangle클래스를 상속받도록 구현
    public class Square extends Rectangle {
      //가로 세로가 모두 동일한 값을 가져야 하므로, setWidth() 메서드와 setHeight()메서드를 재정의하여 
    	//가로, 세로값이 일치되도록 구현
    	@Override
    	puboic void setWidth(int width) {
      	super.setWidth(width);
      	super.setHeight(width);
    	}
    
    	@Override
    	puboic void setHeight(int height) {
      	super.setWidth(height);
      	super.setHeight(height);
    	}
    }
    
    //Rectangle 클래스를 사용하는 코드 ( 높이와 폭을 비교해서 높이를 더 길게 만들어 주는 기능 )
    public void increaseHeight(Rectangle rec) {
    	if(rec.getHeight() <= rec.getWidh()) {
      	rec.setHeight(rec.getWidth() + 10);
    	}
    }
    
  • increaseHeight() 메서드의 rec파라미터로 Square 객체가 전달되면 Square의 setHeight()메서드는 높이와 폭을 모두 같은 값으로 만들기 때문에 increaseheight() 메서드를 실행하더라도 높이가 폭보다 길어지지 않게 된다.
  • 직사각형-정사각형 문제는 개념적으로 상속 관계에 있는 것처럼 보일지라도 실제 구현에서는 상속 관계가 아닐 수도 있다.
  • 다른 예는 상위 타입에서 지정한 리턴 값의 범위에 해당되지 않는 값을 리턴하는 것이다.
    //입력 스트림으로부터 데이터를 읽어 와 출력 스트림에 복사해주는 기능
    public class CopyUtil {
    	public static void copy(InputStream is, OutputStream out) {
      	byte[] data = new byte[512];
      	int len = -1;
    
      	//InputStream.read() 메서드는 스트림의 끝에 도달하면 -1을 리턴
      	while( (len = isread(data)) != -1 ) {
        	out.write(data, 0 ,len);
      	}
    	}
    }
    //InputStream을 상속한 하위 타입에서 read() 메서드를 구현 
    public class satanInputStream implements InputStream { 
      public int read(byte[] data) { 
      	//.... return 0; 
          //데이터가 없을 때 0을 리턴 
      } 
    }
    //SataInputStream 객체로부터 데이터를 읽어와서 파일에 저장하기 위해 구현 
    InputStram is = new satanInputStream(someData); 
    OutputStream out = new FileOutputStream(filePath); CopyItil.copy(is, out);
    
  • CopyUtil.copy() 메서드는 무한로프를 돈다.
  • SatanInputStream 타압의 객체가 상위 타입인 InputStream을 올바르게 대체하지 않기 때문에 발생한 문제이다.

리스코프 치환 원칙은 계약과 확장에 대한 것

    기능 실행의 계약과 관련해서 흔히 발생하는 위반 사례
    • 1. 명시된 명세에서 벗어난 값을 리턴한다.
    • 2. 명시된 명세에서 벗어난 익셉션을 발생한다.
    • 3. 명시된 명세에서 벗어난 기능을 수행한다.
  • 리스코프 치환 원칙은 확장에 대한 것이다.
  • 리스코프 치환 원칙이 지켜지지 않으면 개방 폐쇄 원칙을 지킬 수 없게 된다. 그러면 기능 확장하기가 어렵게 된다.

인터페이스 분리 원칙

  • 인터페이스는 그 인터페이스를 사용하는 클라이언트를 기준으로 분리해야 한다.
  • 자신이 사용하는 메서드에만 의존해야 한다.
  • 용도에 맞게 인터페이스를 분리하는 것은 단일 책임 원칙과도 연결된다.

인터페이스 분리 원칙은 클라이언트에 대한 것

  • 인터페이스 분리 원칙은 클라이언트 입장에서 인터페이스를 분리하라는 원칙.
  • 각 클라이언트가 사용하는 기능을 중심으로 인터페이스를 분리함으로써, 클라이언트로부터 발생하는 인터페이스 변경의 여파가 다른 클라이언트에 미치는 영향을 최소화할 수 있다.

의존 역전 원칙

  • 고수준 모듈은 저수준 모듈의 구현에 의존해서는 안된다. 저수준 모듈이 고수준 모듈에서 정의한 추상 타입에 의존해야한다.
- 고수준 모듈 : 바이트 데이터를 읽어와 암호화하고 결과 바이트 데이터를 쓴다. - 저수준 모듈 : 파일에서 바이트 데이터를 읽어온다. / AES 알고리즘으로 암호화한다. / 파일에 바이트 데이터를 쓴다.
  • 고수준 모듈은 상대적으로 클 틀에서 프로그램을 다룬다면, 저수준 모듈은 각 개별 요소가 어떻게 구현될지에 대해서 다룬다.

의존 역전 원칙을 통한 변경의 유연함 확보

  • 저수준 모듈이 고수준 모듈을 의존하게 만들어서 해결한다. 방법은 추상화에 있다.
  • 의존 역전 원칙은 앞서 리스코프 치환 원칙과 함게 개방 폐쇄 원칙을 따르는 설계를 만들어 주는 기반이 된다.

소스 코드 의존과 런타임 의존

public class FlowController {
  	public void process() {
    	//소스 코드에서 FileDataReader에 대한 의존 발생
    	FileDataReader reader = new FileDataReader();
  	}
}
//의존역전 원칙 적용 : 상세 구현에서 추상 타입에 의존
public class FileDataReader implements ByteSource {
	//....
}
  • 런타임의 의존이 아닌 소스 코드의 의존을 역전시킴으로써 변경의 유연함을 확보할 수 있도록 만들어 주는 원칙.

의존 역전 원칙과 패키지

  • 의존 역전 원칙은 타입의 소유도 역전시킨다.
  • 타입의 소유 역전은 각 패키지를 독립적으로 배포할 수 있도록 만들어 준다.

SOLID 정리

  • SOLID 원칙은 변화에 유연하게 대처할 수 있는 설계 원칙이다.
  • 단일 책임 원칙과 인터페이스 분리 원칙은 객체가 커지지 않도록 막아준다.
  • 리스코프 치환 원칙과 의존 역전 원칙은 개방 폐쇄 원칙을 지원한다.
  • 개방 폐쇄 원칙은 변화되는 부분을 추상화하고 다형성을 이용함으로써 기능 확장을 하면서도 기존 코드를 수정하지 않도록 만들어 준다.
  • 변화되는 부분을 추상화할 수 있도록 도와주는 원칙이 의존 역전 원칙이고, 다형성을 도와주는 원칙이 리스코프 치환 원칙이다.

  • SOLID 원칙은 사용자 입장에서의 기능 사용을 중시한다.
배운점 : 항상 사용자 입장에서 기능을 생각, 큰모듈을 먼저 생각하고 작은 모듈을 생각, 이러한 생각을 갖도록 해주었다.