JAVA

자바 객체지향 설계를 망치는 7가지 이해 부족 실수와 올바른 접근법

CodeByJin 2025. 9. 12. 08:07
반응형

객체지향 프로그래밍(OOP)은 자바의 핵심 철학입니다. 하지만 객체지향 개념을 제대로 이해하지 못하면 코드가 지저분해지고 유지보수가 어려워집니다. 이 글에서는 자바 개발자가 흔히 저지르는 객체지향 설계 오류 7가지를 실제 사례와 함께 살펴보고, 어떻게 올바르게 접근해야 하는지 함께 공부해 나가겠습니다.

1. 단일 책임 원칙(SRP, Single Responsibility Principle)을 무시하기

오류 사례

public class UserService {
    public void registerUser(User user) {
        // 1) 사용자 등록 로직
        // 2) 이메일 인증 링크 생성
        // 3) 이메일 발송
        // 4) 사용자 로그인 세션 생성
    }
}

 
위 코드에서는 유저 등록, 링크 생성, 이메일 발송, 세션 생성까지 한 클래스가 너무 많은 역할을 수행합니다. 변경 지점이 여러 군데 얽혀 있어 유지보수가 취약합니다.
 
올바른 접근법

  • UserRegistrationService: 사용자 등록에 집중
  • EmailService: 이메일 생성 및 발송 담당
  • SessionService: 로그인 세션 관리
public class UserRegistrationService {
    private EmailService emailService;
    private SessionService sessionService;

    public void registerUser(User user) {
        saveUser(user);
        String link = createVerificationLink(user);
        emailService.sendVerificationEmail(user, link);
        sessionService.createUserSession(user);
    }
    // saveUser, createVerificationLink 내부 구현 생략
}

 
이렇게 책임을 분리하면 클래스 간 결합도를 낮출 수 있고, 수정이 필요할 때도 특정 서비스만 손보면 됩니다.

2. 상속(Inheritance) 오용

오류 사례

public class ElectricCar extends Car {
    public void chargeBattery() {
        // 충전 로직
    }
}
public class SportsCar extends Car {
    public void turboBoost() {
        // 터보 부스트 로직
    }
}

 

각 자동차가 상속으로 구분되었지만, 만약 FlyingCar라는 신기술 차량이 등장해 Car, ElectricCar, FlyingCar까지 단일 상속 트리가 복잡해집니다.

 
올바른 접근법: 컴포지션 활용

public interface Chargeable {
    void chargeBattery();
}
public interface Flyable {
    void fly();
}

public class ElectricCar implements Chargeable, Driveable {
    // Driveable 은 주행 인터페이스
    public void chargeBattery() { /*...*/ }
    public void drive() { /*...*/ }
}

public class FlyingCar implements Chargeable, Flyable, Driveable {
    public void chargeBattery() { /*...*/ }
    public void fly() { /*...*/ }
    public void drive() { /*...*/ }
}

 
필요한 기능을 인터페이스로 분리하고 컴포지션을 통해 조합하면 클래스 계층이 유연해집니다.

3. 인터페이스 분리 원칙(ISP, Interface Segregation Principle) 위배

오류 사례

public interface AllInOnePrinter {
    void print(Document doc);
    void scan(Document doc);
    void fax(Document doc);
}
public class OldPrinter implements AllInOnePrinter {
    @Override
    public void print(Document doc) { /*...*/ }
    @Override
    public void scan(Document doc) { throw new UnsupportedOperationException(); }
    @Override
    public void fax(Document doc) { throw new UnsupportedOperationException(); }
}

 

사용하지도 않는 scan(), fax() 메서드를 구현해야 하고, UnsupportedOperationException으로 예외처리를 남발합니다.

 
올바른 접근법

public interface Printer {
    void print(Document doc);
}
public interface Scanner {
    void scan(Document doc);
}
public interface Fax {
    void fax(Document doc);
}

public class OldPrinter implements Printer {
    public void print(Document doc) { /*...*/ }
}

 
필요한 기능만 인터페이스로 분리해 구현하면 불필요한 메서드를 제거할 수 있습니다.

4. 의존성 주입(Dependency Injection) 없이 강한 결합

오류 사례

public class OrderService {
    private final PaymentService paymentService = new PaymentService();
    public void placeOrder(Order order) {
        paymentService.process(order);
    }
}

 

OrderServicePaymentService 구현에 강하게 결합되어 확장성이 떨어집니다.

 
올바른 접근법

public class OrderService {
    private final PaymentService paymentService;
    
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder(Order order) {
        paymentService.process(order);
    }
}

 
생성자 주입으로 결합도를 낮추고, 다양한 결제 구현체를 주입할 수 있게 만듭니다.

5. 데이터와 로직의 분리 실패

오류 사례

public class User {
    private String name;
    private String email;
    // Getter, Setter 생략

    public boolean isEmailValid() {
        // 이메일 유효성 검사 로직
    }

    public void sendWelcomeEmail() {
        // 이메일 발송 로직
    }
}

 
도메인 모델에 비즈니스 로직과 이메일 발송 같은 외부 연동 로직이 뒤섞여 있습니다.
 
올바른 접근법: 도메인 모델과 서비스 분리

public class User {
    private String name;
    private String email;
    // Getter, Setter
}

public class UserService {
    public boolean isEmailValid(User user) { /*...*/ }
    public void sendWelcomeEmail(User user) { /*...*/ }
}

 
도메인 객체에는 순수한 데이터만 담고, 서비스 계층에서 외부 연동과 검증을 담당하게 합니다.

6. 과도한 캡슐화(Capsulation) 또는 캡슐화 무시

오류 사례 1: 모든 필드를 public으로 선언

public class Product {
    public String name;
    public double price;
}

 
외부에서 무분별하게 접근·수정되어 객체 상태가 예측 불가능해집니다.
 
오류 사례 2: 지나치게 private

public class BankAccount {
    private double balance;
    private void deductFee() { /*...*/ }
    // 너무 많은 private 메서드로 테스트가 어려움
}

 
올바른 접근법

  • 필드는 private로 선언하고, 필요한 getter/setter를 제공
  • 내부 구현 로직도 적절히 분리해 서비스나 유틸 클래스로 이동
public class Product {
    private String name;
    private double price;

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public double getPrice() { return price; }
    public void setPrice(double price) { 
        if(price < 0) {
            throw new IllegalArgumentException("가격은 0 이상이어야 합니다.");
        }
        this.price = price;
    }
}

 
데이터 무결성을 보장하면서 외부에 필요한 정보만 노출합니다.

7. 다형성(Polymorphism) 미활용

오류 사례

public class PaymentService {
    public void processCreditCard(CreditCardPayment payment) { /*...*/ }
    public void processPaypal(PaypalPayment payment) { /*...*/ }
}

 
새로운 결제 수단마다 메서드를 추가해야 해 유지보수가 곤란합니다.
 
올바른 접근법: 공통 인터페이스 활용

public interface Payment {
    void process();
}

public class CreditCardPayment implements Payment {
    public void process() { /* 크레딧카드 결제 로직 */ }
}

public class PaypalPayment implements Payment {
    public void process() { /* 페이팔 결제 로직 */ }
}

public class PaymentService {
    public void processPayment(Payment payment) {
        payment.process();
    }
}

 

다형성으로 결제 수단이 확장될 때마다 Payment 구현체만 추가하면 돼 설계가 유연해집니다.

 
위 7가지 설계 오류는 객체지향 개념을 잘못 이해했을 때 자주 발생하는 함정입니다. 설계를 개선하려면 다음을 실천해 보세요.

  1. 책임 분리: 하나의 클래스나 메서드는 한 가지 역할에만 집중합니다.
  2. 컴포지션 우선: 상속보다 인터페이스와 컴포지션을 적극 활용해 유연성을 확보합니다.
  3. 인터페이스 적절히 쪼개기: 필요한 기능만 잘라낸 작고 명확한 인터페이스를 정의합니다.
  4. 의존성 주입: 외부 구현체와 약한 결합을 유지해 변경에 유리하게 만듭니다.
  5. 계층 분리: 도메인 모델과 비즈니스 로직, 외부 연동은 각기 다른 레이어에 배치합니다.
  6. 캡슐화 균형: 데이터 무결성을 지키되, 필요한 부분만 공개하도록 접근 제어자를 조정합니다.
  7. 다형성 활용: 공통 인터페이스로 여러 구현체를 처리해 코드 중복을 줄입니다.

이 글을 참고하며 작은 프로젝트에 직접 적용해 보고, 설계 원칙을 체득해 보세요. 자바 객체지향 설계를 깊이 이해하고 나면 유지보수가 쉬워지고, 팀 협업도 원활해집니다. 함께 공부하며 성장해 나가길 바랍니다!

Java 개발자를 위한 필수 가이드: main 메소드 의존 습관을 버리고 깔끔한 코드로 나아가는 길

많은 Java 초보 개발자들이 범하는 가장 흔하면서도 심각한 실수 중 하나는 모든 코드를 main 메소드 안에 몰아넣는 것입니다. 이는 단순히 '나쁜 습관'을 넘어서 프로젝트의 확장성과 유지보수성

byteandbit.tistory.com

반응형