"응집도와 결합도"
응집도 : 하나의 클래스(또는 모듈)이 어람나 하나의 목적을 위해 긴밀하게 뭉쳐있는지 나타내는 척도,
클래스 내의 메서드와 데이터들이 얼마나 서로 관련있는 작업을 수행하는지를 의미.
"좋은 소프트웨어를 위해서는 응집도를 높여주어야 한다."
- 응집도를 높이기 위해서는?
- 하나의 클래스는 하나의 책임만 가져야한다.
- 예를 들어, 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");
}
}