728x90
반응형
Structural Typing
- 타입을 판단할 때 객체나 타입이 같은 구조를 가지고 있다면, 같은 타입으로 간주합니다. 즉, 이름이나 선언 위치가 달라도 구조만 같다면 호환됩니다.
- Typescript, Go에서 사용합니다.
- 장점
- 구조만 맞으면 타입 간 변환이 자유로워 유연한 타입을 가지며 코드 재사용성이 높습니다.
- 여러 객체가 같은 구조를 가진다면 추가 타입 선언 없이도 인터페이스 확장이 가능합니다.
- JSON 응답이나 dynamic 타입과 같은 동적으로 정의된 객체 대응이 용이합니다.
- 단점
- 구조는 같지만 의미가 전혀 다른 데이터가 타입 검사를 통과할 수 있어, 논리적 오류가 발생할 수 있습니다.
- 구조만 검사하기 때문에 타입 이름이 바뀌거나 재사용되어도 문제를 인식하지 못할 수 있습니다.
- 다양한 타입이 비슷한 구조를 가질 경우, 명확한 이름 없이 흘러 다니는 구조적 타입이 많아져 중복 구조 선언의 가능성이 높습니다.
// 객체 구조가 같으면 호환 가능합니다.
interface Point {
x: number;
y: number;
}
function printPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
const myPoint = { x: 10, y: 20, z: 30 }; // z 속성이 더 있음
printPoint(myPoint);
// ✅ 허용됨 (x와 y 속성이 있으므로 Point로 취급됨)
// myPoint는 Point에 명시적으로 선언되지는 않았지만, 필요한 속성(x, y)을 갖고 있으므로 printPoint의 매개변수로 사용 가능
// 타입 간 호환성도 구조로 판단합니다.
interface Person {
name: string;
age: number;
}
interface Employee {
name: string;
age: number;
employeeId: number;
}
const emp: Employee = { name: "Alice", age: 30, employeeId: 123 };
// Employee는 Person보다 더 많은 속성을 가짐
const p: Person = emp; // ✅ 허용됨: 구조적으로 Person을 포함하므로 호환
논리적 오류
- 구조만 맞추면 타입이 통과되므로, 의도하지 않은 타입 간 호환이 생길 수 있는 상황
interface UserId {
id: number;
}
interface ProductId {
id: number;
}
function deleteUser(user: UserId) {
console.log(`Deleting user with id ${user.id}`);
}
const product: ProductId = { id: 42 };
// ❗ 구조가 같기 때문에 오류 없이 컴파일됨
deleteUser(product);
// 🚨 논리적 오류: product를 사용자로 착각함
// UserId와 ProductId는 실제로 다른 의미를 갖는 타입이지만, 둘 다 { id: number } 형태로 구조가 같아서 TypeScript가 타입 호환성을 인정합니다. 결과적으로 실행 시 논리적 오류가 발생할 수 있음
논리적 오류를 피하는 방법
- 런타임에서 타입을 확실하게 식별해야 할 경우, type guard를 사용하여 의도를 명확히 할 수 있습니다.
type UserId = { id: number; kind: 'user' };
type ProductId = { id: number; kind: 'product' };
function isUserId(obj: any): obj is UserId { // ✅ Type Guards
return obj?.kind === 'user';
}
function deleteUser(user: UserId) {
console.log(`Deleting user with id ${user.id}`);
}
const unknownId: UserId | ProductId = { id: 10, kind: 'product' };
if (isUserId(unknownId)) {
deleteUser(unknownId); // ✅ 타입 안전
} else {
console.log('Not a user ID!');
}
Nominal Typing
- Structural Typing과 다르게 타입을 판단할 때 타입의 명칭(Name)으로 판단합니다. 즉, 구조가 달라도 이름만 같다면 호환됩니다.
- Java, C#, Swift, Rust에서 사용합니다.
- 장점
- 이름이 다르면 절대적으로 다른 타입으로 간주되므로, 명확한 타입 구분에 용이합니다.
- 의미적으로 불일치한 값이 할당되면 컴파일 단계에서 오류가 발생되기 때문에, 더 강력한 컴파일러 검사가 가능합니다. 실수 가능성을 줄일 수 있습니다.
- UserId, ProductId, OrderId 등 같은 숫자 구조라도 다른 개념임을 명확히 구분 가능하므로 도메인 주도 설계(Domain-Driven Design)에 유리합니다.
- 단점
- 구조가 같아도 타입 변환이 번거로움이 있습니다.
- 인터페이스 적응이 어렵고 구조 재정의를 위한 많은 보일러 플레이트를 생성합니다.
class UserId {
int value;
}
class ProductId {
int value;
}
public class Main {
static void deleteUser(UserId id) {
System.out.println("Deleting user with ID: " + id.value);
}
public static void main(String[] args) {
ProductId pid = new ProductId();
pid.value = 100;
// deleteUser(pid);
// ❌ 컴파일 에러 - 타입이 다름
// UserId와 ProductId는 구조가 같지만, 이름이 다르기 때문에 절대 호환되지 않습니다.
// 컴파일 타임에 실수(예: 잘못된 ID 전달)를 잡을 수 있어 논리 오류를 방지할 수 있습니다.
// 의미 구분이 명확하므로, 도메인 중심 설계(DDD)에 적합합니다.
}
}
// 같은 구조지만 별개의 타입이라 재사용 불가한 경우
class Point2D {
int x;
int y;
}
class Size {
int x;
int y;
}
public class Main {
static void draw(Point2D point) {
System.out.println("Drawing at: " + point.x + ", " + point.y);
}
public static void main(String[] args) {
Size s = new Size();
s.x = 10;
s.y = 20;
// draw(s);
// ❌ 컴파일 에러 - 구조는 같아도 타입 이름이 다릅니다.
// 코드를 재사용하려면 별도의 변환 로직 또는 타입 추상화가 필요하고 이는 보일러플레이트 증가시킵니다.
}
}
정리
Structural Typing | Nominal Typing | |
타입 일치 기준 | 타입의 구조 | 타입의 이름 |
유연성 | 높음 (이름이 달라도 구조만 같으면 허용) | 낮음 (정확히 같은 타입 선언이 필요) |
안전성 | 구조가 같아도 의미가 다를 수 있어 안전성이 상대적으로 낮음 | 정해진 사실에 기반으로 의도된 타입만을 상용하므로 안전성이 높음 |
타입 추론 방식 | 암시적 | 명시적 (정해진 타입 이름을 사용해야 한다.) |
코드 간결성 | 높음 | 낮음 |
재사용성 | 높음 | 제한적 |
디버깅 및 유지보수 | 의도치 않은 타입이 통과할 수 있어 주의해야 함 | 명확한 타입 관계가 존재하여 디버깅 및 유지보수에 용이하다. |
대표 언어 | typescript, go | java, c#, kotlin, swift, rust |
728x90
반응형
'typescript' 카테고리의 다른 글
Typescript ( CFA ) (1) | 2025.05.15 |
---|---|
Typescript ( type vs interface ) (0) | 2025.05.11 |
Typescript ( tsconfig.json ) (0) | 2025.05.11 |