| 구분 | 정적(Static) | 동적(Dynamic) |
|---|---|---|
| 시점 | 컴파일 시 결정 | 실행 시 결정 |
| 변화 여부 | 고정되어 있음 | 실행 중에 바뀔 수 있음 |
| 안정성 | 컴파일러가 미리 오류를 잡음 | 실행하면서 유연하게 처리 |
| 대표 언어 | Java, C, C++ (정적 타이핑) | Python, JavaScript (동적 타이핑) |
즉, "정적"은 프로그램 실행 전에 다 정해놓고 시작하는 방식이며 "동적"은 프로그램 실행 중에 바뀌거나 결정되는 방식이다.
| 구분 | 정적 | 동적 |
|---|---|---|
| 바인딩(Binding) | 어떤 함수/메서드를 호출할지 컴파일 시점에 결정 | 실행 시점에 결정 (예: 오버라이딩) |
| 메모리 할당 | 컴파일 시 고정된 크기 | 실행 중에 필요할 때 생성/할당 |
| 다형성 | 오버로딩(정적 다형성) | 오버라이딩(동적 다형성) |
객체지향 프로그래밍을 설계할 때 지켜야할 원칙
부모 타입의 객체를 자식 타입의 객체로 교체해도, 프로그램의 동작이 깨지지 않아야 한다
Parent p = new Child(); 처럼 부모 자리에 자식을 넣어도 Parent로서의 기능은 그대로 잘 작동해야 한다는 뜻이다.
"부모 객체에 자식을 넣는다"의 대표적인 자바 코드가 있다.
List<String> list = new ArrayList<>();
ArrayList는 List의 하위 타입이다.
그런데 List로 선언해도 ArrayList의 기능(추가, 삭제 등)은 정상 작동고 있다.
이게 바로 리스코프 치환 원칙이 잘 지켜진 경우로 자식 클래스가 부모의 “약속된 행위(계약)”를 그대로 지키고 있다면, 내부에 어떤 속성이나 기능이 더 있든 상관없다.
즉, 부모가 약속한 기능(메소드)은 그대로 작동해야하며 부모가 보장한 입력/출력 규칙도 깨면 안 된다.
class Bird {
void fly() { System.out.println("날 수 있다"); }
}
class Sparrow extends Bird {
// 부모의 규약을 그대로 지킴
}
Bird b = new Sparrow();
b.fly(); // "날 수 있다" → 정상
Sparrow는 Bird가 약속한 “fly 가능”을 그대로 지키고 있어 LSP를 만족하고 있다.
class Bird {
void fly() { System.out.println("날 수 있다"); }
}
class Penguin extends Bird {
@Override
void fly() {
throw new UnsupportedOperationException("펭귄은 날 수 없음");
}
}
Bird b = new Penguin();
b.fly(); // 예외 발생
문법상으로는 “자식이 부모를 대체”했지만, 부모가 약속한 “fly() 가능”이라는 계약을 깨버렸다.
즉, LSP 위반이다.
무조건 교체 가능이 아니라 “부모가 약속한 행위를 그대로 지킬 수 있다면 교체 가능”하다.
LSP의 진짜 의도는 “논리적 일관성 유지”이다. 서브타입은 진짜로 상위타입의 의미를 확장(extends)하는 관계여야 한다. 부모가 무엇을 할 수 있는가를 약속했으면 자식도 그 약속을 위반하지 않아야 한다.