객체지향 프로그래밍은 프로그램을 객체들의 협력으로 바라보는 프로그래밍 패러다임이다. 절차적으로 명령을 나열하는 방식이 아니라, 상태와 행동을 가진 객체를 만들고 객체끼리 메시지를 주고받으며 문제를 해결한다.

객체지향의 장점은 변경에 대응하기 쉽다는 데 있다. 역할과 책임이 잘 나뉜 객체는 필요한 부분만 수정하거나 확장할 수 있다. 반대로 책임이 뒤섞인 객체가 많아지면 작은 변경도 여러 곳으로 퍼지고, 객체지향을 사용했는데도 유지보수가 어려워진다. 결국 중요한 것은 클래스를 많이 만드는 것이 아니라 적절한 책임을 가진 객체를 설계하는 것이다.

OOP 설계 원칙

객체지향 설계 원칙은 변경에 강한 구조를 만들기 위한 기준이다. 대표적으로 SOLID 원칙이 있고, 각각은 객체의 책임, 확장 방식, 상속 관계, 의존 방향, 인터페이스 설계를 다룬다.

단일 책임 원칙(SRP)

단일 책임 원칙은 하나의 모듈이 하나의 변경 이유만 가져야 한다는 원칙이다. 여기서 책임은 단순히 기능 하나를 뜻하기보다, 어떤 액터나 요구사항 변화에 의해 변경되는 이유에 가깝다.

예를 들어 주문을 저장하는 코드와 주문 완료 이메일을 보내는 코드가 같은 클래스에 있다면, 저장 방식이 바뀔 때도 이메일 정책이 바뀔 때도 같은 클래스를 수정하게 된다. 이 경우 변경 이유가 둘이므로 책임을 분리하는 편이 좋다.

SRP를 지키면 수정해야 할 위치가 명확해진다. 한 변경이 다른 기능에 영향을 줄 가능성도 줄어들기 때문에, 애플리케이션 변화에 더 안전하게 대응할 수 있다.

개방 폐쇄 원칙(OCP)

개방 폐쇄 원칙은 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙이다. 요구사항이 추가될 때 기존 코드를 계속 고치는 대신, 새로운 구현을 추가해서 동작을 확장할 수 있어야 한다.

이 원칙을 지키려면 변하는 부분과 변하지 않는 부분을 분리해야 한다. 예를 들어 결제 수단이 카드, 계좌이체, 포인트로 늘어날 수 있다면 결제 흐름 자체는 인터페이스에 의존하고, 세부 결제 방식은 구현체로 분리할 수 있다. 그러면 새 결제 수단이 추가되어도 기존 흐름을 크게 수정하지 않고 기능을 확장할 수 있다.

객체가 너무 많은 구체 클래스를 직접 알고 있으면 결합도가 높아진다. 결합도가 높을수록 작은 변경도 기존 코드 수정으로 이어지고, OCP를 지키기 어려워진다.

리스코프 치환 원칙(LSP)

리스코프 치환 원칙은 하위 타입이 상위 타입을 대체할 수 있어야 한다는 원칙이다. 어떤 코드가 부모 타입을 사용하고 있다면, 그 자리에 자식 타입을 넣어도 기대한 동작이 깨지지 않아야 한다.

상속을 사용할 때는 단순히 코드 재사용만 생각하면 안 된다. 자식 클래스가 부모 클래스의 약속을 지키지 않으면 사용하는 쪽에서는 타입만 보고 안전하게 사용할 수 없다. 예를 들어 부모 타입의 메서드가 항상 성공한다고 기대되는데, 특정 자식 타입에서만 예외를 던지거나 전혀 다른 의미로 동작한다면 LSP를 위반할 수 있다.

LSP는 “상속 관계가 문법적으로 가능한가”보다 “행동적으로 대체 가능한가”를 보라는 원칙이다.

의존 역전 원칙(DIP)

의존 역전 원칙은 고수준 모듈이 저수준 구현에 직접 의존하지 말고, 둘 다 추상화에 의존해야 한다는 원칙이다. 고수준 모듈은 비즈니스 규칙이나 정책처럼 핵심 흐름을 담고, 저수준 모듈은 데이터베이스 접근, 외부 API 호출, 파일 저장처럼 세부 구현을 담당한다.

핵심 비즈니스 로직이 특정 데이터베이스 구현이나 외부 라이브러리에 직접 묶이면 변경이 어려워진다. 반대로 인터페이스를 사이에 두면 비즈니스 로직은 “저장한다”, “전송한다” 같은 추상적인 역할에만 의존하고, 실제 구현은 필요에 따라 교체할 수 있다.

DIP는 테스트에도 도움이 된다. 구체 구현 대신 인터페이스에 의존하면 테스트에서는 가짜 구현체를 넣어 핵심 로직만 검증할 수 있다.

인터페이스 분리 원칙(ISP)

인터페이스 분리 원칙은 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 작게 나누라는 원칙이다. 하나의 큰 인터페이스에 여러 기능을 몰아넣으면 구현체는 필요 없는 메서드까지 억지로 구현해야 하고, 클라이언트도 불필요한 변경의 영향을 받을 수 있다.

예를 들어 읽기만 필요한 객체가 읽기와 쓰기, 삭제 메서드가 모두 들어 있는 인터페이스에 의존한다면 과한 의존이다. 읽기 전용 인터페이스와 수정 가능한 인터페이스를 분리하면 각 클라이언트는 자신에게 필요한 기능만 알면 된다.

다만 인터페이스를 무조건 잘게 쪼개는 것이 정답은 아니다. 실제 사용 방식과 변경 가능성을 기준으로 나눠야 한다. 한 번 공개된 인터페이스를 다시 분리하면 기존 구현체와 클라이언트에 영향을 줄 수 있으므로 처음부터 역할을 기준으로 설계하는 것이 좋다.

객체지향 설계의 핵심은 변경을 어디에 가둘지 정하는 것이다. SOLID 원칙은 그 결정을 돕는 기준이며, 원칙 자체를 외우기보다 코드의 결합도와 변경 방향을 설명할 수 있어야 실무에서 의미가 있다.