카테고리 없음

응집도, 결합도, SOLID원칙

hojin98 2026. 2. 5. 22:02
"응집도와 결합도"

 

 

응집도 : 하나의 클래스(또는 모듈)이 어람나 하나의 목적을 위해 긴밀하게 뭉쳐있는지 나타내는 척도,
클래스 내의 메서드와 데이터들이 얼마나 서로 관련있는 작업을 수행하는지를 의미.

 

 

 

"좋은 소프트웨어를 위해서는 응집도를 높여주어야 한다."

 

 

 

  • 응집도를 높이기 위해서는?
    • 하나의 클래스는 하나의 책임만 가져야한다.
    • 예를 들어, MovieController라면 Movie와 관련된 요청 처리(조회, 생성, 수정, 삭제)만을 담당해야 한다.

 

 

 

결합도 : 하나의 클래스(또는 모듈)가 다른 클래스와 얼마나 많이 엮여있는지, 즉 얼마나 의존적인지를 나타내는 척도

 

 

"좋은 소프트웨어를 위해서는 결합도를 느슨하게 만들어줘야한다."

 

 


 

 

"SOLID 원칙"

 

  • Sing Responsibility Principle(단일 책임 원칙)
클래스는 단 하나의 책임만 가져야 한다!

 

 


잘못된 코드 예시

// 너무 많은 책임을 가진 클래스
public class Employee {
    private String name;
    private double salary;
    
    // 책임 1: 직원 정보 관리
    public void setName(String name) {
        this.name = name;
    }
    
    // 책임 2: 급여 계산
    public double calculatePay() {
        return salary * 1.1;
    }
    
    // 책임 3: 데이터베이스 저장
    public void saveToDatabase() {
        // DB 저장 로직
        System.out.println("Saving to database...");
    }
    
    // 책임 4: 리포트 생성
    public void generateReport() {
        System.out.println("Generating report...");
    }
}

 

 


올바른 코드 예시

// 직원 정보만 관리
public class Employee {
    private String name;
    private double salary;
    
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getSalary() { return salary; }
}

// 급여 계산 담당
public class PayrollCalculator {
    public double calculatePay(Employee employee) {
        return employee.getSalary() * 1.1;
    }
}

// 데이터베이스 관리 담당
public class EmployeeRepository {
    public void save(Employee employee) {
        System.out.println("Saving " + employee.getName() + " to database");
    }
}

// 리포트 생성 담당
public class ReportGenerator {
    public void generateReport(Employee employee) {
        System.out.println("Generating report for " + employee.getName());
    }
}

 

 

 

  • Open / Closed Principle(개방 / 폐쇄 원칙)
클래스는 확정에는 열려있고, 수정에는 닫혀있어야 한다!

 

 


잘못된 코드 예시

public class DiscountCalculator {
    public double calculateDiscount(String customerType, double amount) {
        if (customerType.equals("REGULAR")) {
            return amount * 0.05;
        } else if (customerType.equals("VIP")) {
            return amount * 0.15;
        } else if (customerType.equals("PREMIUM")) {  // 새 타입 추가시 코드 수정 필요
            return amount * 0.20;
        }
        return 0;
    }
}

 

 


올바른 코드 예시

// 추상화를 통한 확장 가능한 설계
public interface DiscountStrategy {
    double calculateDiscount(double amount);
}

public class RegularCustomerDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.05;
    }
}

public class VIPCustomerDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.15;
    }
}

// 새로운 할인 정책 추가시 기존 코드 수정 없이 확장 가능
public class PremiumCustomerDiscount implements DiscountStrategy {
    @Override
    public double calculateDiscount(double amount) {
        return amount * 0.20;
    }
}

public class DiscountCalculator {
    public double calculateDiscount(DiscountStrategy strategy, double amount) {
        return strategy.calculateDiscount(amount);
    }
}

 

 

 

  • Liskov Substitution Principle (리스코프 치환 원칙)
자식 클래스는 부모 클래스를 대체할 수 있어야 한다!

 

 


잘못된 코드 예시

public class Rectangle {
    protected int width;
    protected int height;
    
    public void setWidth(int width) {
        this.width = width;
    }
    
    public void setHeight(int height) {
        this.height = height;
    }
    
    public int getArea() {
        return width * height;
    }
}

// 정사각형은 가로와 세로가 같아야 하는데...
public class Square extends Rectangle {
    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width;  // 높이도 같이 변경
    }
    
    @Override
    public void setHeight(int height) {
        this.width = height;  // 너비도 같이 변경
        this.height = height;
    }
}

// 문제 상황
public class TestLSP {
    public static void testRectangle(Rectangle rect) {
        rect.setWidth(5);
        rect.setHeight(10);
        // Rectangle이라면 50이어야 하지만, Square라면 100이 됨
        System.out.println("Area: " + rect.getArea()); 
    }
}

 

 


올바른 코드 예시

// 공통 인터페이스 사용
public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private int width;
    private int height;
    
    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }
    
    @Override
    public int getArea() {
        return width * height;
    }
}

public class Square implements Shape {
    private int side;
    
    public Square(int side) {
        this.side = side;
    }
    
    @Override
    public int getArea() {
        return side * side;
    }
}

 

 

  • Interface Segregation Principle(인터페이스 분리 원칙)
클라이언트는 자신이 사용하지 않는 메서드에 의존하면 안 된다!
여기서 클라이언트 -> "이미 구현된 코드를 사용하는 코드"를 의미한다.

 

 


잘못된 코드 예시

// 너무 많은 기능을 가진 인터페이스
public interface Worker {
    void work();
    void eat();
    void sleep();
    void receiveSalary();
}

// 로봇은 먹거나 자지 않는데도 구현해야 함
public class Robot implements Worker {
    @Override
    public void work() {
        System.out.println("Robot working");
    }
    
    @Override
    public void eat() {
        // 로봇은 먹지 않음 - 불필요한 구현
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void sleep() {
        // 로봇은 자지 않음 - 불필요한 구현
        throw new UnsupportedOperationException();
    }
    
    @Override
    public void receiveSalary() {
        // 로봇은 급여를 받지 않음
        throw new UnsupportedOperationException();
    }
}

 

 


올바른 코드 예시

 

// 인터페이스를 작은 단위로 분리
public interface Workable {
    void work();
}

public interface Eatable {
    void eat();
}

public interface Sleepable {
    void sleep();
}

public interface Payable {
    void receiveSalary();
}

// 필요한 인터페이스만 구현
public class Human implements Workable, Eatable, Sleepable, Payable {
    @Override
    public void work() {
        System.out.println("Human working");
    }
    
    @Override
    public void eat() {
        System.out.println("Human eating");
    }
    
    @Override
    public void sleep() {
        System.out.println("Human sleeping");
    }
    
    @Override
    public void receiveSalary() {
        System.out.println("Human receiving salary");
    }
}

public class Robot implements Workable {
    @Override
    public void work() {
        System.out.println("Robot working");
    }
    // 불필요한 메서드 구현 없음!
}

 

 

  • Dependency Inversion Principle(의존 역전 원칙)
고수준의 모듈은 저수준 모듈에 의존하면 안 된다. 둘 다 추상화에 의존해야 한다!

 


잘못된 코드 예시

// 저수준 클래스 (구체적인 구현)
public class EmailSender {
    public void sendEmail(String message) {
        System.out.println("Sending email: " + message);
    }
}

// 고수준 클래스가 저수준 클래스에 직접 의존
public class NotificationService {
    private EmailSender emailSender = new EmailSender();
    
    public void notify(String message) {
        emailSender.sendEmail(message);
        // SMS로 바꾸려면? 코드를 수정해야 함!
    }
}

 

 


올바른 코드 예시

 

// 추상화 (인터페이스)
public interface MessageSender {
    void sendMessage(String message);
}

// 저수준 클래스들이 인터페이스 구현
public class EmailSender implements MessageSender {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending email: " + message);
    }
}

public class SMSSender implements MessageSender {
    @Override
    public void sendMessage(String message) {
        System.out.println("Sending SMS: " + message);
    }
}

// 고수준 클래스는 추상화에 의존
public class NotificationService {
    private MessageSender messageSender;
    
    // 의존성 주입을 통해 유연성 확보
    public NotificationService(MessageSender messageSender) {
        this.messageSender = messageSender;
    }
    
    public void notify(String message) {
        messageSender.sendMessage(message);
    }
}

// 사용 예제
public class Main {
    public static void main(String[] args) {
        // 이메일로 알림
        NotificationService emailNotification = 
            new NotificationService(new EmailSender());
        emailNotification.notify("Hello via Email");
        
        // SMS로 알림 - 코드 변경 없이 교체 가능!
        NotificationService smsNotification = 
            new NotificationService(new SMSSender());
        smsNotification.notify("Hello via SMS");
    }
}