구현을 숨기고 추상적인 인터페이스로 자료를 표현하라.
public class Point { public double x; public double y; }
public interface IPoint { double getX(); double getY(); void setCartesian(double x, double y); double getR(); double getTheta(); void setPolar(double r, double theta); }
| 특성 | 설명 |
|---|---|
| 구현 캡슐화 | 사용자는 내부가 직각좌표인지 극좌표인지 모름 |
| 변경 유연성 | 추후에 PolarPoint, SphericalPoint로 교체 가능 |
| 추상적 의미 | "좌표"라는 의미로 다룸, 수학적 개념과 맞음 |
| SRP/OCP 만족 | 구현은 바꿀 수 있으나 인터페이스는 안정적 유지 |
public class CartesianPoint : IPoint { private double _x, _y; public double GetX() => _x; public double GetY() => _y; public void SetCartesian(double x, double y) { _x = x; _y = y; } public double GetR() => Math.Sqrt(_x * _x + _y * _y); public double GetTheta() => Math.Atan2(_y, _x); public void SetPolar(double r, double theta) { _x = r * Math.Cos(theta); _y = r * Math.Sin(theta); } }
런타임에 알고리즘(또는 내부 구조)을 바꾸고 싶을 때 사용하는 패턴.
public interface ICoordinateStrategy { double GetX(); double GetY(); void SetCartesian(double x, double y); void SetPolar(double r, double theta); }
public class CartesianCoordinate : ICoordinateStrategy { private double _x, _y; public double GetX() => _x; public double GetY() => _y; public void SetCartesian(double x, double y) { _x = x; _y = y; } public void SetPolar(double r, double theta) { _x = r * Math.Cos(theta); _y = r * Math.Sin(theta); } }
public class PolarCoordinate : ICoordinateStrategy { private double _r, _theta; public double GetX() => _r * Math.Cos(_theta); public double GetY() => _r * Math.Sin(_theta); public void SetCartesian(double x, double y) { _r = Math.Sqrt(x * x + y * y); _theta = Math.Atan2(y, x); } public void SetPolar(double r, double theta) { _r = r; _theta = theta; } }
public class Point { private ICoordinateStrategy _coordinate; public Point(ICoordinateStrategy coordinate) { _coordinate = coordinate; } public double X => _coordinate.GetX(); public double Y => _coordinate.GetY(); public void SetCartesian(double x, double y) => _coordinate.SetCartesian(x, y); public void SetPolar(double r, double theta) => _coordinate.SetPolar(r, theta); }
내부가 Cartesian인지 Polar인지 모른 채 사용 가능 — 자료 추상화 완벽 달성.
런타임에 전략만 바꿔 끼우면 됨 → 유연성, 테스트성 향상
어느 쪽이 옳은가가 아니라, 언제 무엇을 쓸지의 문제다.
public class Square { public Point TopLeft; public double Side; } public class Rectangle { public Point TopLeft; public double Height; public double Width; } public class Circle { public Point Center; public double Radius; }
public class Geometry { public double Area(object shape) { return shape switch { Square s => s.Side * s.Side, Rectangle r => r.Height * r.Width, Circle c => Math.PI * c.Radius * c.Radius, _ => throw new ArgumentException("Unknown shape") }; } }
자료구조 특성 — 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다. 반면, 새로운 자료 구조를 추가하려면 모든 함수를 고쳐야 한다.
public interface IShape { double Area(); } public class Square : IShape { private double _side; public Square(Point topLeft, double side) { _side = side; } public double Area() => _side * _side; } public class Circle : IShape { private double _radius; public Circle(Point center, double radius) { _radius = radius; } public double Area() => Math.PI * _radius * _radius; }
객체 특성 — 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다. 반면, 새로운 함수(동작)를 추가하려면 모든 클래스를 고쳐야 한다.
| 항목 | 절차적 (자료 중심) | 객체 지향 (다형성) |
|---|---|---|
| 새 함수 추가 | ✅ 쉬움 — Geometry에만 추가 | ❌ 어려움 — 모든 클래스 수정 |
| 새 타입 추가 | ❌ 어려움 — 모든 함수 수정 | ✅ 쉬움 — 새 클래스만 추가 |
| 적합한 경우 | 함수가 자주 추가되는 경우 | 타입이 자주 추가되는 경우 |
객체 구조를 변경하지 않고 새로운 연산(동작)을 추가하는 패턴.
public interface IShape { double Accept(IShapeVisitor visitor); } public interface IShapeVisitor { double Visit(Square square); double Visit(Rectangle rectangle); double Visit(Circle circle); }
public class Square : IShape { public double Side { get; } public Square(double side) => Side = side; public double Accept(IShapeVisitor visitor) => visitor.Visit(this); } public class Circle : IShape { public double Radius { get; } public Circle(double radius) => Radius = radius; public double Accept(IShapeVisitor visitor) => visitor.Visit(this); }
public class AreaVisitor : IShapeVisitor { public double Visit(Square s) => s.Side * s.Side; public double Visit(Rectangle r) => r.Width * r.Height; public double Visit(Circle c) => Math.PI * c.Radius * c.Radius; }
public class PerimeterVisitor : IShapeVisitor { public double Visit(Square s) => 4 * s.Side; public double Visit(Rectangle r) => 2 * (r.Width + r.Height); public double Visit(Circle c) => 2 * Math.PI * c.Radius; }
var shapes = new List<IShape> { new Square(5), new Rectangle(4, 6), new Circle(3) }; var areaVisitor = new AreaVisitor(); foreach (var shape in shapes) Console.WriteLine($"Area: {shape.Accept(areaVisitor)}");
| 항목 | 설명 |
|---|---|
| 새 도형 추가 | Visitor에 Visit(NewShape) 추가 필요 → 약점 |
| 새 동작 추가 | Visitor 클래스 하나 추가로 끝 (기존 도형 손대지 않음) ✅ |
| OCP 만족 | 동작(방문자)은 열려 있고, 도형 구조는 닫혀 있음 |
모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다. 낯선 사람은 경계하고 친구랑만 놀아라.
readonly String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();
여러 객차가 한 줄로 이어진 기차처럼 보인다. ctxt의 내부 구조를 속속들이 알아야 코드가 작동한다.
Options opts = ctxt.getOptions(); File scratchDir = opts.getScratchDir(); readonly String outputDir = scratchDir.getAbsolutePath();
final String outputDir = ctxt.options.scratchDir.absolutePath;
이것이 자료 구조라면 내부 구조를 직접 노출하는 것이 자연스럽다. 디미터 법칙은 객체에 적용되는 법칙이다.
데이터 전달용 객체(DTO)에 비즈니스 로직을 넣으면 잡종 객체가 된다.
public class User { public int Id { get; set; } public string Email { get; set; } public string PasswordHash { get; set; } public void Save() { Database.Save(this); } public static User? Find(int id) { return Database.FindById<User>(id); } }
public class User { public int Id { get; set; } public string Email { get; set; } public void Save() => Database.Save(this); // ❌ 문제: 비즈니스 로직이 섞임 public bool CanLogin(DateTime now) { return Email.Contains("@") && now.Hour < 22; } }
잡종 객체의 문제 — 비즈니스 로직과 DB 저장 책임이 섞여 있다. SRP를 위반하고, 테스트하기 어렵고, 두 가지 이유로 변경된다.