객체지향의 오해와 진실

송현진·2025년 6월 17일
0

Java

목록 보기
8/11

자바 백엔드 개발자로 공부하면서 “객체지향 설계”, “OOP 원칙”, “유지보수 가능한 코드” 같은 말은 익숙했지만 실제로 어떤 코드를 짜야 좋은 객체지향인지는 항상 고민이었다. 예제를 보면 다들 클래스를 쪼개고 상속도 쓰고 있었지만 나중엔 코드가 더 복잡해지기도 하고 오히려 바꾸기 어려워지는 경우도 많았다. 무엇이 진짜 객체지향인지 오해하고 있던 부분이 무엇이었는지를 정리하고 실무나 프로젝트에서 “객체지향적으로 설계한다는 건 무엇을 해야 하는가”에 대한 나만의 기준을 갖기 위해 작성하게 되었다.

흔히 하는 오해들

클래스(class)를 쓰면 객체지향이다?

클래스는 객체지향의 "도구"일 뿐 본질이 아니다.
많은 사람들이 class 키워드를 쓰면 객체지향이라고 착각한다. 하지만 클래스는 단순한 문법적 도구일 뿐 객체지향의 본질은 아니다. 클래스 안에 모든 로직을 쑤셔 넣는다면 그건 객체지향이 아니라 절차지향적인 방식일 뿐이다.

예를 들어 아래 코드는 겉보기엔 객체처럼 보이지만 실제론 모든 처리를 하나의 서비스가 독점하고 있는 절차 중심 구조다.

public class OrderService {
    public void processOrder(String orderId) {
        // 상품 조회, 결제, 포인트 적립 등 모든 로직을 이 안에서 처리
        // 즉, 모든 로직이 몰려있음
    }
}

이렇게 되면 하나의 변경이 전체 코드에 영향을 주고 테스트도 어렵고 협업도 힘들다. 진짜 객체지향은 “서로 다른 객체들이 책임을 나눠서 협력”하도록 설계해야 한다.

상속을 쓰면 객체지향이다?

객체지향을 상속과 동의어처럼 여기는 경우도 많다. 특히 초기에 "객체지향 = 상속 + 다형성"이라고 배우는 경우가 많은데 이는 절반만 맞는 말이다. 상속은 잘 쓰면 강력한 도구지만 잘못 쓰면 유연성과 유지보수성을 해치는 원인이 된다. 실제로 상속보다 훨씬 더 유연하고 유지보수에 강한 패턴은 합성(composition)이다.

잘못된 상속 사용 예시

class Bird {
    void fly() { ... }
}

class Ostrich extends Bird {
    // 타조는 날 수 없음. fly()를 비워두거나 예외 던짐?
}

이 경우, is-a 관계가 성립하지 않기 때문에 상속 구조 자체가 잘못된 것이다. 이런 식의 오용은 결국 유지보수가 어려운 코드로 이어진다. 진짜 객체지향은 “행위에 따라 역할을 나누고 그 역할에 따라 객체가 협력”하는 구조를 만드는 것이다.

객체지향의 본질

핵심: "현실을 그대로 옮기는 게 아니라 책임과 협력에 집중하는 것"

객체지향은 현실 세계의 사물을 클래스화 하는게 아니라 소프트웨어 관점에서 역할과 책임을 분리하고 협력하는 구조를 만드는 것이다. 진짜 중요한 건 그 객체들이 어떤 책임을 지고 서로 어떻게 협력하는지다. 실제로 OOP의 본질은 다음과 같은 흐름에 있다.

  • 객체는 자신의 데이터를 책임지고 다룬다
  • 다른 객체에게 메시지를 전달하며 협력한다
  • 서로의 내부를 모르고도 일할 수 있게 한다 (캡슐화)

“객체는 행동을 중심으로 바라봐야 한다. 속성보다 더 중요한 건 무엇을 할 수 있느냐다.”

객체지향의 4대 특성은 수단일 뿐이다.

특성핵심 개념오해하지 말아야 할 점
캡슐화내부 구현을 감추고 메시지만 노출getter/setter 남발은 캡슐화 아님
상속코드 재사용 + 인터페이스 다형성재사용만을 위한 상속은 해로움
다형성같은 메시지에 다른 반응인터페이스 기반 설계가 핵심
추상화공통된 개념을 묶기무조건 일반화하려다 오히려 복잡해지기도

객체지향은 어떻게 써야 할까? 실전에서의 고민과 해결

문제 1. 모든 로직이 서비스 클래스에 몰리는 문제

초기에는 서비스 하나에 모든 비즈니스 로직을 넣는 것이 편하다. 하지만 프로젝트가 커질수록 UserService, OrderService 같은 클래스가 "만능 처리기"가 되면서 비대해지고 테스트도 어렵고 변경 시 영향 범위도 넓어진다.

해결 방향

도메인 모델에게 책임을 넘기자.
예: 포인트 적립, 결제 가능 여부 판단 등은 Order, Point 같은 객체에서 처리하도록 하자.

문제 2. 중복을 줄이려다 무분별하게 상속 구조를 만드는 경우

코드 중복을 줄이겠다는 명분으로 상위 클래스에 공통 로직을 넣고 무조건 상속하게 만든다. 하지만 시간이 지나면 하위 클래스마다 예외 상황이 생기고 결국 상위 클래스를 수정해야 하며 SRP(단일 책임 원칙)가 깨진다.

해결 방향

상속보다 합성(composition)을 우선 고려하자.
필요한 동작을 인터페이스로 정의하고 상황에 맞게 구현체를 주입해서 유연하게 처리하자.

문제 3. 캡슐화를 한다며 getter/setter만 제공하는 객체

모든 필드에 getX(), setX()를 만들면 캡슐화했다고 착각하는 경우가 많다. 하지만 외부에서 모든 필드를 바꿀 수 있으면, 객체가 스스로 상태를 지키지 못하고 의미가 없다.

해결 방향

외부에서 필드를 바꾸지 말고 행위를 통해 간접적으로 조작하도록 설계하자.

예시

  • point.setAmount(100) -> ❌
  • point.add(100) -> ✅

문제 4. 객체의 역할이 불분명해지고, 책임이 이리저리 흩어지는 문제

처음에는 User, Order, Product 등 객체가 잘 나눠져 있어 보이지만 점점 기능이 추가되면서 어디에 어떤 책임이 있는지 알 수 없는 코드가 된다.

해결 방향

객체 하나에는 하나의 책임만 부여하자. (SRP: 단일 책임 원칙)
변경 이유가 여러 개라면 객체가 잘못 설계된 것일 가능성이 높다.

📝 느낀점

객체지향은 단순히 “클래스를 나누고 상속을 쓰는 기술”이 아니라 “책임을 나누고, 객체들이 협력하는 구조를 만들어내는 설계 철학”이라는 것을 알 수 있었다. 좋은 객체지향 설계는 유지보수가 쉽고 확장이 유연하며 테스트가 쉬운 코드를 만든다. 앞으로 코드를 짤 때는, "이 책임은 누가 져야 할까?", "이 객체는 어떤 메시지를 주고받을까?" 같은 질문을 먼저 해보려 한다.

profile
개발자가 되고 싶은 취준생

0개의 댓글