객체지향 설계: 객체 모델링과 SOLID 원칙
객체지향 프로그래밍의 객체·클래스·캡슐화·상속·다형성·추상화와 SOLID 설계 원칙을 실무 관점에서 정리합니다.
2026-08-15 · 최초 발행 2025-05-23
객체로 문제 영역을 모델링하는 방식
객체지향(Object Oriented)은 실세계의 물체와 개체를 소프트웨어의 객체로 표현하는 프로그래밍 패러다임이다. 1960년대 시뮬라(Simula) 언어에서 개념이 도입됐고, 1980년대 이후 C++, Java 등을 거치며 널리 사용됐다.
객체는 상태와 행위를 함께 다룬다. 기능과 데이터를 분리해 절차를 중심으로 구성하는 대신, 문제 영역의 개체가 무엇을 알고 무엇을 수행하는지에 맞춰 소프트웨어를 구성한다.
객체를 이루는 상태와 행위
객체(Object)는 실세계 개체(Entity)를 소프트웨어적으로 나타낸 단위다. 속성으로 데이터를 보유하고 메소드로 행위를 제공하며, 독립적으로 식별되고 다른 객체와 상호작용한다.
클래스(Class)는 같은 유형의 객체가 공유할 속성과 메소드를 정의하는 템플릿 또는 청사진이다. 실제 객체는 클래스의 인스턴스(Instance)로 생성된다.
속성(Attribute)은 객체의 상태와 특성을 나타내는 데이터다. 학생 객체라면 이름, 학번, 성적이 속성이 될 수 있다. 메소드(Method)는 객체가 수행하는 기능이나 행위로, 내부 상태를 조작하거나 다른 객체와 통신한다. 계좌 객체의 입금(), 출금(), 이체()가 이에 해당한다.
객체의 경계를 설계하는 핵심 원칙
캡슐화로 접근 경로를 통제한다
캡슐화(Encapsulation)는 데이터와 메소드를 하나의 단위로 묶는 방식이다. 객체 내부 구현을 감추고, 외부에는 정의된 인터페이스를 통한 상호작용만 허용한다. 정보 은닉(Information Hiding)과 public·private·protected 같은 접근 제어자를 통해 내부 데이터를 직접 다루지 못하게 제한하며, getter/setter 메소드는 상태를 읽고 바꾸는 통제된 인터페이스가 된다.
public class BankAccount {
private double balance; // 외부에서 직접 접근 불가
public double getBalance() {
return balance; // 잔액 조회는 허용
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount; // 유효성 검증 후 입금 처리
}
}
}
이 구조는 데이터 무결성을 지키고, 내부 변경이 미치는 범위를 줄이며, 유지보수를 수월하게 한다.
상속으로 공통 특성을 재사용한다
상속(Inheritance)은 기존 클래스의 특성을 새 클래스가 이어받는 관계다. 공통 동작을 재사용하면서 계층적 구조를 만들 수 있다. 상속을 제공하는 클래스는 부모 클래스 또는 슈퍼 클래스, 이를 확장하는 클래스는 자식 클래스 또는 서브 클래스라고 한다. 자식 클래스가 부모 클래스의 한 종류라는 is-a 관계를 표현할 때 쓰인다.
class Vehicle:
def __init__(self, brand):
self.brand = brand
def start_engine(self):
return "Engine started!"
class Car(Vehicle): # Vehicle 클래스 상속
def __init__(self, brand, model):
super().__init__(brand) # 부모 클래스 생성자 호출
self.model = model
def drive(self):
return f"{self.brand} {self.model} is driving"
상속은 코드 재사용을 높이고 도메인을 계층 구조로 모델링하는 데 도움이 된다. 인사관리 모델에서도 직원이 공유하는 정보를 상위 클래스에 두고, 직무나 역할에 따른 특성을 하위 클래스에서 표현할 수 있다. 다만 계층이 실제 행위까지 호환하는지 확인해야 한다.
다형성은 같은 인터페이스에 다른 동작을 연결한다
다형성(Polymorphism)은 동일한 인터페이스를 통해 여러 객체가 각기 다르게 반응하도록 하는 특성이다. 부모 클래스 메소드를 자식 클래스가 재정의하는 오버라이딩(Overriding), 같은 이름의 메소드를 다른 매개변수로 다중 정의하는 오버로딩(Overloading), 구현체보다 인터페이스에 맞춰 동작하는 방식이 여기에 포함된다.
// 다형성 예시
public abstract class Shape {
public abstract double CalculateArea(); // 추상 메소드
}
public class Circle : Shape {
private double radius;
public Circle(double r) {
radius = r;
}
public override double CalculateArea() {
return Math.PI * radius * radius; // 원 면적 계산
}
}
public class Rectangle : Shape {
private double width, height;
public Rectangle(double w, double h) {
width = w;
height = h;
}
public override double CalculateArea() {
return width * height; // 사각형 면적 계산
}
}
호출하는 쪽은 구체 클래스별 분기를 늘리지 않고도 도형을 처리할 수 있다.
// 다양한 도형 객체를 동일한 방식으로
List<Shape> shapes = new List<Shape> {
new Circle(5),
new Rectangle(4, 6)
};
foreach (Shape shape in shapes) {
Console.WriteLine($"면적: {shape.CalculateArea()}"); // 각 도형의 구현에 따라 다른 결과
}
인사관리 모델에서도 직원 유형이 공통 인터페이스를 따르면서 각 역할에 맞는 동작을 제공하도록 설계할 수 있다. 호출하는 쪽은 구체적인 직원 유형을 구분하는 대신 공통 인터페이스를 통해 처리하므로, 새 유형을 추가할 때 기존 처리 코드를 크게 바꾸지 않아도 된다. 이 방식은 설계의 유연성과 확장성을 높이고 코드 결합도를 낮춘다.
추상화는 필요한 개념만 드러낸다
추상화(Abstraction)는 복잡한 시스템에서 핵심 개념과 기능을 추출하고, 불필요한 세부 사항을 감추는 방식이다. 인터페이스와 추상 클래스는 구현의 차이를 감춘 채 필요한 기능을 표현하는 데 사용된다. 일부 구현과 추상 메소드를 함께 둘 수 있는 추상 클래스(Abstract Class), 메소드 시그니처만 정의하는 계약인 인터페이스(Interface), 그리고 관심사의 분리가 주요 수단이다.
// 인터페이스를 통한 추상화
public interface DatabaseConnection {
void connect();
void disconnect();
ResultSet executeQuery(String query);
}
// 구체적인 구현
public class MySQLConnection implements DatabaseConnection {
@Override
public void connect() {
// MySQL 연결 로직
}
@Override
public void disconnect() {
// MySQL 연결 종료 로직
}
@Override
public ResultSet executeQuery(String query) {
// MySQL 쿼리 실행 로직
return result;
}
}
호출자는 연결의 세부 구현보다 DatabaseConnection이 제공하는 계약에 집중할 수 있다. 시스템 복잡성을 줄이고 구현 변경에 유연하게 대응하며 세부사항을 감추는 효과가 있다.
변경을 관리하는 SOLID 원칙
단일 책임 원칙(Single Responsibility Principle)은 클래스가 하나의 책임을 가져야 하며 변경 이유도 하나여야 한다는 기준이다. 사용자 인증과 사용자 프로필 관리를 별도 클래스로 분리하는 방식이 예시다.
// 단일 책임 원칙 위반 - 너무 많은 역할
public class UserService {
public User getUser(int id) { /* ... */ }
public void saveUser(User user) { /* ... */ }
public void sendEmail(User user, String message) { /* ... */ }
public void generateReport(User user) { /* ... */ }
}
public class UserRepository {
public User getUser(int id) { /* ... */ }
public void saveUser(User user) { /* ... */ }
}
public class EmailService {
public void sendEmail(User user, String message) { /* ... */ }
}
public class ReportService {
public void generateReport(User user) { /* ... */ }
}
개방-폐쇄 원칙(Open-Closed Principle)은 기존 코드 수정 없이 새 기능을 확장할 수 있어야 한다는 원칙이다. 인터페이스와 추상화를 통해 이를 구현한다.
// 인터페이스로 추상화
public interface PaymentProcessor {
void processPayment(double amount);
}
// 구현체들
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// 신용카드 결제 처리
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// 페이팔 결제 처리
}
}
// 새로운 결제 방식이 추가되어도 기존 코드 변경 없음
public class BitcoinProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// 비트코인 결제 처리
}
}
리스코프 치환 원칙(Liskov Substitution Principle)은 하위 클래스가 상위 클래스의 자리를 대체할 수 있어야 한다고 본다. Bird 클래스의 하위 클래스인 Penguin이 fly() 메소드를 제대로 구현할 수 없다면 계층 구조를 다시 설계할 필요가 있다.
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 getArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // 정사각형은 너비와 높이가 같아야 함
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
// 문제 상황
void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.getArea() == 20; // Square인 경우 실패
}
인터페이스 분리 원칙(Interface Segregation Principle)은 클라이언트가 사용하지 않는 인터페이스에 의존하지 않아야 한다는 원칙이다. 범용 인터페이스보다 특정 클라이언트에 맞춘 작은 인터페이스가 적합하다.
// 너무 큰 인터페이스
interface Worker {
void work();
void eat();
void sleep();
}
// 로봇은 eat, sleep이 필요 없음
class Robot implements Worker {
public void work() { /* ... */ }
public void eat() { throw new UnsupportedOperationException(); }
public void sleep() { throw new UnsupportedOperationException(); }
}
// 인터페이스 분리
interface Workable {
void work();
}
interface Eatable {
void eat();
}
interface Sleepable {
void sleep();
}
// 필요한 인터페이스만 구현
class Robot implements Workable {
public void work() { /* ... */ }
}
class Human implements Workable, Eatable, Sleepable {
public void work() { /* ... */ }
public void eat() { /* ... */ }
public void sleep() { /* ... */ }
}
의존성 역전 원칙(Dependency Inversion Principle)은 상위 모듈과 하위 모듈이 모두 추상화에 의존해야 한다는 기준이다. 추상화가 세부 사항에 의존하는 대신 세부 사항이 추상화에 의존하도록 구성하며, 의존성 주입(Dependency Injection)으로 구현할 수 있다.
// 추상화에 의존
public class UserService {
private UserRepository userRepository; // 인터페이스
// 의존성 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User getUser(int id) {
return userRepository.findById(id);
}
}
// 인터페이스
public interface UserRepository {
User findById(int id);
}
// 구현체
public class MySQLUserRepository implements UserRepository {
@Override
public User findById(int id) {
// MySQL DB 접근 로직
return user;
}
}
// 다른 구현체 추가 가능
public class MongoUserRepository implements UserRepository {
@Override
public User findById(int id) {
// MongoDB 접근 로직
return user;
}
}
도메인 관계를 객체로 표현하는 예
은행 시스템에서는 고객, 계좌, 거래를 별도 객체로 두고 소유 관계와 거래 관계를 표현할 수 있다.
온라인 쇼핑몰에서는 상품, 장바구니, 장바구니 항목, 주문을 구분해 재고와 주문 상태, 상품 참조 관계를 모델링할 수 있다.
얻는 점과 감수할 점
객체지향은 복잡한 시스템을 관리 가능한 모듈로 나누고, 기존 코드를 재활용할 수 있게 한다. 코드 변경을 국소화해 유지보수를 돕고, 기존 코드를 크게 바꾸지 않으면서 기능을 확장할 수 있다. 구현을 객체 내부에 감추고 공통 특성을 재사용하며, 추상화와 다형성으로 확장 지점을 설계하는 것이 이러한 장점과 연결된다. 현실의 문제 영역을 자연스럽게 모델링할 수 있다는 점도 장점이다.
반면 캡슐화와 다형성은 추가 처리를 요구할 수 있다. 좋은 설계를 위해서는 상당한 경험과 지식이 필요하며, 절차적 프로그래밍보다 개념을 익히는 데 시간이 들 수 있다. 필요 이상의 추상화는 오히려 복잡성을 키운다.
절차적·함수형 접근과의 관계
절차적 프로그래밍은 기능 중심으로 구성하며 데이터와 함수를 분리한다. 객체지향은 데이터와 함수를 객체 내부에 캡슐화한다.
함수형 프로그래밍은 불변성과 부작용 최소화, 함수를 중심에 둔다. 객체지향은 가변 상태 관리와 객체 간 상호작용을 중심으로 설계한다.
대부분의 기업용 소프트웨어는 객체지향 방식으로 개발되며, 마이크로서비스 아키텍처에도 객체지향 원칙이 적용된다. 도메인 주도 설계(DDD)는 객체지향 개념을 확장한 방법론이며, 객체지향과 함수형 프로그래밍의 장점을 통합하는 하이브리드 접근도 사용된다.