조립을 이용한 재사용
- 객체 조립은 여러 객체를 묶어서 더 복잡한 기능을 제공하는 객체를 만들어 내는 것이다.
- 객체지향 언어에서 객체 조립은 보통 필드에서 다른 객체를 참조하는 방식으로 구현된다.
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 원칙은 사용자 입장에서의 기능 사용을 중시한다.
배운점 : 항상 사용자 입장에서 기능을 생각, 큰모듈을 먼저 생각하고 작은 모듈을 생각, 이러한 생각을 갖도록 해주었다.