클린 코드

📌 클린 코드는 단순히 잘 동작하는 코드가 아니라, 가독성, 유지보수성, 확장성이 뛰어난 코드를 의미합니다. 이는 협업과 장기적인 코드 품질을 유지하기 위한 필수적인 개발 철학 입니다.

  • Dirty Code는 폭탄이다. A를 수정하니 B가 터지는 연쇄반응이 수시로 나온다.
  • 협업의 필수 조건으로, 사실 본인이 혼자해도 이 규칙을 어느정도 지켜야한다.
  • 기술 부채가 쌓이면 나중에는 결국 리펙토링이 아니라 재개발하는게 더 좋을 수도 있다.
  • 프로그래머는 가뜩이나 이직이 많아 에일리언코드(담당자가 없어져서든 버전의 변화로든 알 수 없는 코드를 말한다.)가 많이 생성될 가능이 높다.

감당 못함

 

리팩터링

📌 결과의 변경 없이 코드의 구조를 재조정하는 것을 말합니다. 주로 가독성을 높이고 유지보수를 편하게 하기 위해 추후 수정하는 것을 말합니다. 버그를 없애거나 새로운 기능을 추가하는 행위는 아닙니다.

  • 코드가 읽기 어려운 순간 (코드를 읽으면 바로 이해가 되어야한다.)
  • 수정할 때마다 오류가 발생하는 경우
  • 같은 코드가 여기저기 복붙되어 있는 경우
  • 확장하려는데 코드 구조가 방해되는 경우
  • 단위 테스트 작성이 어려운 경우

보통 위와 같은 상황에서 진행하게 된다.

 

 

 

클린 코드의 기본 원칙

의미 있는 이름 짓기
  1. 구체적이고 의도를 담은 이름을 사용
  2. 매직 넘버를 피하라
  3. 데이터의 의미를 이름에 반영
함수 분리 하기
  1. 하나의 함수는 하나의 역할만 수행 (sendEmail은 이메일 발송의 흐름만 관리)
  2. 복잡한 작업은 작은 함수로 분리
  3. 함수 이름은 동작과 목적을 명확히 표현
불필요한 주석 제거
  1. 주석은 코드가 아닌 의도를 설명
    • “어떻게”가 아닌 “왜”를 설명
  2. 주석 대신 명확한 변수와 함수 이름으로 의도를 드러냄
  3. 불필요한 주석은 제거하고, 코드는 가능한 자체적으로 읽히게 작성
코드 중복 제거
DRY (Don’t Repeat Yourself) 원칙을 준수

복잡한 코드를 단순화하기
  1. 조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리
부정 표현을 긍정 표현으로 바꾸기
  1. 긍정적 변수명 사용
  2. 긍정적 조건문 작성
  3. 이중 부정 지양
else 문 사용 지양하기
  1. else 문을 피하고 기본 동작을 명시
  2. 전처리와 핵심 로직을 분리
  3. 각 조건은 독립적으로 처리

 

의미 있는 이름 짓기

// 나쁜 예
public void processData(List<String> data) {
    for (String item : data) {
        if (item.length() > 5) {
            System.out.println(item);
        }
    }
}

// 좋은 예
public void printLongUserNames(List<String> userNames) {
    final int MIN_NAME_LENGTH = 5;
    for (String userName : userNames) {
        if (userName.length() > MIN_NAME_LENGTH) {
            System.out.println(userName);
        }
    }
}
  1. 구체적이고 의도를 담은 이름을 사용
  2. 매직 넘버를 피하라
  3. 데이터의 의미를 이름에 반영

 

 

함수 분리 하기

// 나쁜 예
public void sendEmail(String recipient, String subject, String body) {
    if (recipient == null || recipient.isEmpty()) {
        throw new IllegalArgumentException("Recipient cannot be null or empty");
    }
    System.out.println("Connecting to SMTP server...");
    System.out.println("Authenticating...");
    System.out.println("Sending email to: " + recipient);
    System.out.println("Subject: " + subject);
    System.out.println("Body: " + body);
    System.out.println("Email sent successfully.");
}

// 좋은 예
public void sendEmail(String recipient, String subject, String body) {
    validateRecipient(recipient);
    connectToSmtpServer();
    authenticate();
    deliverEmail(recipient, subject, body);
}

private void validateRecipient(String recipient) {
    if (recipient == null || recipient.isEmpty()) {
        throw new IllegalArgumentException("Recipient cannot be null or empty");
    }
}

private void connectToSmtpServer() {
    System.out.println("Connecting to SMTP server...");
}

private void authenticate() {
    System.out.println("Authenticating...");
}

private void deliverEmail(String recipient, String subject, String body) {
    System.out.println("Sending email to: " + recipient);
    System.out.println("Subject: " + subject);
    System.out.println("Body: " + body);
    System.out.println("Email sent successfully.");
}
  1. ★  하나의 함수는 하나의 역할만 수행   (sendEmail은 이메일 발송의 흐름만 관리)
  2. 복잡한 작업은 작은 함수로 분리
  3. 함수 이름은 동작과 목적을 명확히 표현

 

 

불필요한 주석 제거

// 나쁜 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
    // 송금 금액이 0보다 커야 합니다.
    if (amount <= 0) {
        throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
    }

    // 잔액 확인
    if (fromAccount.getBalance() < amount) {
        throw new IllegalStateException("계좌 잔액이 부족합니다.");
    }

    // 같은 계좌인지 확인
    if (fromAccount.equals(toAccount)) {
        throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
    }

    // 송금 실행
    fromAccount.withdraw(amount); // 돈을 출금합니다.
    toAccount.deposit(amount);    // 돈을 입금합니다.

    // 송금 로그
    System.out.println("송금 성공: " + amount + "원 전송됨.");
}

// 좋은 예
public void processTransaction(Account fromAccount, Account toAccount, double amount) {
    // 비즈니스 규칙: 송금 금액은 0보다 커야 함
    if (amount <= 0) {
        throw new IllegalArgumentException("송금 금액은 0보다 커야 합니다.");
    }

    // 비즈니스 규칙: 송금 계좌 잔액이 부족하면 송금 불가
    if (fromAccount.getBalance() < amount) {
        throw new IllegalStateException("계좌 잔액이 부족합니다.");
    }

    // 비즈니스 규칙: 동일 계좌 간 송금 금지 (실수 방지 목적)
    if (fromAccount.equals(toAccount)) {
        throw new IllegalArgumentException("같은 계좌로 송금할 수 없습니다.");
    }

    // 송금 실행
    fromAccount.withdraw(amount);
    toAccount.deposit(amount);

    // 로그 기록: 성공적인 송금을 기록 (보안 및 추적 목적)
    System.out.println("송금 성공: " + amount + "원 전송됨.");
}
  1. 주석은 코드가 아닌 의도를 설명
    • “어떻게”가 아닌 “왜”를 설명
  2. 주석 대신 명확한 변수와 함수 이름으로 의도를 드러냄
  3. 불필요한 주석은 제거하고, 코드는 가능한 자체적으로 읽히게 작성
  4. 사실 주석은 함부로 쓰지 않는게 좋다. 추후 관리가 어렵고 주석은 전부 개발자가 직접 달아야한다보니 더 문제가 많이 생긴다.

 

코드 중복 제거

// 나쁜 예
public void printUserName(String name) {
    System.out.println("User: " + name);
}

public void printAdminName(String name) {
    System.out.println("Admin: " + name);
}

// 좋은 예
public void printName(String role, String name) {
    System.out.println(role + ": " + name);
}

DRY (Don’t Repeat Yourself) 원칙을 준수

 

 

복잡한 코드를 단순화하기

// 나쁜 예
if (user != null && user.getAge() > 18 && user.isActive()) {
    // do something
}

// 좋은 예
if (isActiveAdultUser(user)) {
    // do something
}

private boolean isActiveAdultUser(User user) {
    return user != null && user.getAge() > 18 && user.isActive();
}
  1. 조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리
  2. isActiveAdultUser가 중요한 것 = 왜 했는지

 

 

부정 표현을 긍정 표현으로 바꾸기

// 나쁜 예
if (!user.isInActive()) {
    return "Inactive User";
}
return "Active User";

// 좋은 예
if (user.isActive()) {
    return "Active User";
}
return "Inactive User";
  1. 긍정적 변수명 사용   -   이거 상당히 중요하다
  2. 긍정적 조건문 작성
  3. 이중 부정 지양 = 이건 반의어를 생각해봐라, 진짜 웬만해서는 피해야한다.
  4. isinactive 라면 긍정인 isactive로 수정한다. 부정은 결국 한번 더 생각을 해야한다..

 

 

else 문 사용 지양하기

// 나쁜 예
public void login(User user) {
    if (user != null) {
        if (user.isActive()) {
            if (user.isVerified()) {
                System.out.println("Login successful");
            } else {
                System.out.println("User is not verified");
            }
        } else {
            System.out.println("User is inactive");
        }
    } else {
        System.out.println("Invalid user");
    }
}

// 좋은 예
public void login(User user) {
    if (user == null) {
        System.out.println("Invalid user");
        return;
    }
    if (!user.isActive()) {
        System.out.println("User is inactive");
        return;
    }
    if (!user.isVerified()) {
        System.out.println("User is not verified");
        return;
    }

    // 모든 조건을 통과한 경우
    System.out.println("Login successful");
}

// 더 좋은 예
public void login(User user) {
    String validationResult = validateUser(user);

    if (!validationResult.equals("Valid")) {
        System.out.println(validationResult);
        return;
    }

    // 모든 조건을 통과한 경우
    System.out.println("Login successful");
}

private String validateUser(User user) {
    if (user == null) {
        return "Invalid user";
    }
    if (!user.isActive()) {
        return "User is inactive";
    }
    if (!user.isVerified()) {
        return "User is not verified";
    }

    return "Valid";
}
  1. else 문을 피하고 기본 동작을 명시
  2. 전처리와 핵심 로직을 분리
  3. 각 조건은 독립적으로 처리

 

클린 코드 이론 원칙

 

클린 코드를 작성하는 데 있어 추가적으로 고려할 수 있는 원칙과 실천 사항은 다음과 같습니다:


1. 코드의 가독성 향상

  • 일관성 유지: 코드 스타일(들여쓰기, 괄호 배치, 공백 등)을 일관되게 유지합니다.
  • 짧은 함수: 함수는 가능한 짧게 유지하여 한눈에 이해할 수 있도록 합니다.
  • 코드 정렬: 논리적 흐름을 고려하여 코드를 정렬합니다(예: 관련된 부분끼리 묶기).

2. 객체 지향 설계 원칙(SOLID)

  • S - 단일 책임 원칙(Single Responsibility Principle)
    클래스나 모듈은 하나의 책임만 가져야 합니다.
  • O - 개방-폐쇄 원칙(Open/Closed Principle)
    코드는 확장에는 열려 있고, 수정에는 닫혀 있어야 합니다.
  • L - 리스코프 치환 원칙(Liskov Substitution Principle)
    서브클래스는 언제나 기반 클래스의 역할을 대체할 수 있어야 합니다.
  • I - 인터페이스 분리 원칙(Interface Segregation Principle)
    클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 합니다.
  • D - 의존성 역전 원칙(Dependency Inversion Principle)
    상위 모듈은 하위 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.

3. 에러 처리

  • 명확한 예외 처리: 예외를 포괄적으로 잡기보다는 특정한 경우에만 잡아야 합니다.
  • 사전 조건 체크: 함수나 메서드의 입력값 검증을 통해 에러를 방지합니다.
  • null 사용 지양: null 대신 Optional, Maybe 같은 명시적 표현을 사용합니다.

4. 의존성 관리

  • 의존성 주입(DI): 객체 생성과 사용을 분리하여 의존성을 줄입니다.
  • 전역 상태 지양: 전역 변수를 사용하지 않고 명시적으로 데이터를 전달합니다.

5. 성능 및 최적화

  • 불필요한 작업 최소화: 반복 작업이나 연산을 줄이고 캐싱을 활용합니다.
  • 지연 계산(lazy evaluation): 필요할 때만 계산하도록 설계합니다.
  • 효율적인 자료구조 선택: 목적에 맞는 자료구조를 사용하여 성능을 개선합니다.

6. 테스트 가능 코드

  • 단위 테스트 작성: 각 함수와 모듈의 동작을 검증합니다.
  • 테스트 자동화: CI/CD 환경에서 자동화된 테스트를 실행합니다.
  • 모의 객체(Mock Object) 활용: 외부 의존성을 분리하여 테스트를 단순화합니다.

7. 유지보수성을 고려한 설계

  • 코드 중복 제거: 유틸리티 함수나 상수로 공통된 로직을 추출합니다.
  • 확장성 있는 설계: 변경 사항이 최소한의 영향으로 처리되도록 설계합니다.
  • 의도를 드러내는 코드: 코드만 읽어도 설계 의도를 알 수 있게 작성합니다.

8. 클린 아키텍처

  • 계층 분리: 비즈니스 로직, 프레젠테이션, 데이터 접근 계층을 분리합니다.
  • 엔티티 독립성: 엔티티는 프레임워크나 데이터베이스에 의존하지 않도록 설계합니다.
  • 경계(Interface) 설정: 외부 의존성과 내부 로직 사이에 명확한 경계를 만듭니다.

9. 적절한 캡슐화

  • 정보 은닉: 클래스 내부 상태를 외부에서 직접 접근하지 못하도록 합니다.
  • Getter/Setter 최소화: 단순히 데이터를 노출하는 Getter/Setter보다는 행동 중심의 메서드 사용.

10. 명확한 의사소통

  • 코드 리뷰: 동료와의 코드 리뷰를 통해 더 나은 코드를 작성합니다.
  • 컨벤션 문서화: 팀에서 사용하는 코딩 스타일을 문서로 명시합니다.

개방-폐쇄 원칙 (Open-Closed Principle)

📌 객체 지향 설계의 핵심 원칙 중 하나로, 소프트웨어 모듈, 클래스, 또는 함수가 다음과 같은 두 가지 상태를 만족해야 한다는 개념

  1. 확장에는 열려 있어야 한다 (Open for extension):
    • 새로운 기능을 추가하거나 요구사항의 변화에 따라 시스템의 동작을 확장할 수 있어야 한다.
    • 기존 코드를 수정하지 않고도 기능 추가가 가능해야 한다.
  2. 수정에는 닫혀 있어야 한다 (Closed for modification):
    • 기존의 잘 검증된 코드는 수정되지 않아야 한다.
    • 코드 변경이 없기 때문에 기존 시스템의 안정성을 유지할 수 있다.

목표

  • 코드를 재사용 가능하고 유지보수성이 높은 상태로 유지.
  • 새로운 요구사항이 생기더라도 기존 코드의 변경 없이 대응 가능.
class Notification {
    public void send(String type) {
        if (type.equals("Email")) {
            // 이메일 전송 코드
        } else if (type.equals("SMS")) {
            // SMS 전송 코드
        }
    }
}

--------------------------------------------

interface Notification {
    void send();
}

class EmailNotification implements Notification {
    public void send() {
        // 이메일 전송 코드
    }
}

class SMSNotification implements Notification {
    public void send() {
        // SMS 전송 코드
    }
}

class PushNotification implements Notification {
    public void send() {
        // 푸시 알림 전송 코드
    }
}

class NotificationService {
    public void sendNotification(Notification notification) {
        notification.send();
    }
}

새로운 알림 방식(예: 푸시 알림)을 추가하려면 send 메서드에 조건문을 추가해야 하므로 기존 코드를 수정해야 함.

=> 새로운 알림 방식을 추가하려면 새로운 클래스를 구현하기만 하면 된다. 기존 코드는 수정하지 않는다.

 

 

 

OOP (Object-Oriented Programming)

📌 객체 지향 프로그래밍(OOP)은 현실 세계의 사물이나 개념을 객체(Object)로 모델링하여 소프트웨어를 개발하는 방식

  • 객체를 사용해 데이터를 묶고, 이를 다루는 기능을 추가합니다.

[1] 캡슐화 (Encapsulation)

  • 내부 데이터를 보호하고, 외부에서 접근할 방법을 제한하는 것.
    • 약병은 뚜껑으로 약을 보호하지만, 우리가 약을 꺼낼 수 있는 기능(뚜껑 열기)을 제공해요.

[2] 상속 (Inheritance)

  • 부모가 가진 것을 자식이 물려받는 것.
    • 부모가 물려준 성격(코드)을 자식이 사용하거나 더 발전시킬 수 있어요!

[3] 다형성 (Polymorphism)

  • 같은 동작을 다양한 방식으로 처리할 수 있는 것.
    • “달려!“라는 명령에 강아지는 뛰고, 자동차는 굴러가요. 하지만 둘 다 “달리는 동작”이에요!

[4] 추상화 (Abstraction)

  • 복잡한 내부 내용은 숨기고, 필요한 부분만 드러내는 것.
    • 자동차의 내부 작동 방식은 몰라도, 운전대와 페달로 운전할 수 있잖아요!

 

SOLID 원칙 - 면접에서 자주 나온다

📌 OOP를 더 잘 설계하기 위한 5가지 규칙.

  • 쉽게 고치고, 확장할 수 있는 코드를 만드는 지침.

[1]  S - 단일 책임 원칙

  • 클래스는 하나의 역할만 가져야 한다.
    • 주방은 요리만, 침실은 잠만. (하나의 역할만!)
  • 왜?
    • 역할이 여러 개면 고칠 때 어디를 수정해야 할지 복잡해 짐.
public class User {
		private String name; // 사용자 정보
    public void login() { /* 로그인 기능 */ }
    public void saveUser() { /* 데이터베이스 저장 기능 */ }
}

--------------------------

public class User { /* 사용자 정보 관리 */ }

public class AuthService {
    public void login(User user) { /* 로그인 기능 */ }
}

public class UserRepository {
    public void saveUser(User user) { /* 데이터베이스 저장 */ }
}

[2]  O - 개방/폐쇄 원칙

  • 코드는 확장에 열려 있고, 수정에는 닫혀 있어야 한다.
    • 옥상에 방을 추가해도 기존 방은 그대로. (확장 가능!)
  • 왜?
    • 기존 코드를 수정하면 예기치 못한 문제가 생길 수 있음.

원래라면 새로운 도형이 추가될 때마다 AreaCalculator 클래스를 수정해야 한다. 하지만 개방 폐쇄 원칙을 적용하면 새로운 도형이 추가되더라도 shape 인터페이스만 구현하면 된다.

 

  • 다형성을 활용하여 해결한다.
    • 인터페이스를 implements 하여 구현한 새로운 클래스를 만들어서 새로운 기능을 구현한다.
    • 역할(도형)과 구현(원, 사각형, 삼각형 등)을 분리하면 된다.
public class Shape {
    public String type;
}

public class AreaCalculator {
    public double calculate(Shape shape) {
        if (shape.type.equals("circle")) {
            return /* 원의 넓이 계산 */;
        } else if (shape.type.equals("square")) {
            return /* 사각형의 넓이 계산 */;
        }
    }
}

------------------------

public interface Shape {
    double calculateArea();
}

public class Circle implements Shape {
    public double calculateArea() { return /* 원의 넓이 계산 */; }
}

public class Square implements Shape {
    public double calculateArea() { return /* 사각형의 넓이 계산 */; }
}

public class AreaCalculator {
    public double calculate(Shape shape) {
        return shape.calculateArea();
    }
}

문제점

// Circle을 계산하는 경우
public class Main {
		public static void main(String[]) {
		
				AreaCalculator areaCalculator = new AreaCalculator();
				Circle circle = new Circle();
				
				areaCalculator.calculate(circle);
			
		}
}

// Square를 계산하는 경우
public class Main {
		public static void main(String[]) {
		
				AreaCalculator areaCalculator = new AreaCalculator();
				// Circle circle = new Circle();
				Square square = new Square();
				
				areaCalculator.calculate(square);
			
		}
}
  • 구현 객체를 변경하기 위해서는 해당 코드를 사용하는 클라이언트측의 코드를 변경해야 한다.
  • 객체의 생성, 사용 등을 자동으로 설정해주는 무엇인가가 필요하다.
    • Spring Container의 역할

 

[3]  L - 리스코프 치환 원칙

  • 자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
    • 새가 날 수 있는 것처럼, 모든 새는 ‘날기’ 동작을 가져야 한다. (일관성 유지!)
    • 부모 클래스를 사용하는 곳에서 자식 클래스를 사용해도 프로그램의 동작에 문제가 없어야 한다.
  • 왜?
    • 부모 클래스처럼 동작하지 않으면, 프로그램이 예외를 일으킴.

 

  • 예시
    • ElectricCar는 Car 클래스를 상속 받았지만, accelerate() 를 사용할 수 없다. LSP 위반

 

리스코프 치환 원칙 적용

  • 인터페이스를 구현한 구현체를 믿고 사용할 수 있도록 만들어준다.
  • 엑셀은 앞으로 가는 기능이다. 만약 뒤로 간다면 LSP를 위반한다.
class Car {
    public void accelerate() {
        System.out.println("자동차가 휘발유로 가속합니다.");
    }
}

class ElectricCar extends Car {
    @Override
    public void accelerate() {
        throw new UnsupportedOperationException("전기차는 이 방식으로 가속하지 않습니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.accelerate(); // "자동차가 가속합니다."

        Car electricCar = new ElectricCar();
        electricCar.accelerate(); // UnsupportedOperationException 발생
    }
}

---------------------------------

// 가속 기능(역할)을 인터페이스로 분리
interface Acceleratable {
    void accelerate();
}

class Car implements Acceleratable {
    @Override
    public void accelerate() {
        System.out.println("내연기관 자동차가 가속합니다.");
    }
}

class ElectricCar implements Acceleratable {
    @Override
    public void accelerate() {
        System.out.println("전기차가 배터리로 가속합니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Acceleratable car = new Car();
        car.accelerate(); // "내연기관 자동차가 가속합니다."

        Acceleratable electricCar = new ElectricCar();
        electricCar.accelerate(); // "전기차가 배터리로 가속합니다."
    }
}

[4]  I - 인터페이스 분리 원칙

  • 클래스는 필요 없는 기능을 강요받지 말아야 한다.
    • 로봇은 먹지 않는데, ‘먹기’ 기능을 구현해야 한다면 비 효율적.
  • 왜?
    • 쓰지 않는 기능 때문에 코드가 불필요하게 복잡해 짐.
  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
    • 클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다.
    • 즉, 하나의 큰 인터페이스보다는 여러 개의 작은 인터페이스로 분리해야 한다.
public interface Animal {
    void fly();
    void run();
    void swim();
}

public class Dog implements Animal {
    public void fly() { /* 사용하지 않음 */ }
    public void run() { /* 달리기 */ }
    public void swim() { /* 수영 */ }
}


------------------------------


public interface Runnable {
    void run();
}

public interface Swimmable {
    void swim();
}

public class Dog implements Runnable, Swimmable {
    public void run() { /* 달리기 */ }
    public void swim() { /* 수영 */ }
}

[5]  D - 의존 역전 원칙

  • “구체적인 것보다, 추상적인 것에 의존하라.”
    • 예: 모든 가전제품은 콘센트만 꽂으면 작동. (표준 인터페이스!)
  • 왜?
    • 변경에 유연해지고, 재사용성이 높아짐.
  • 예시
    • NotificationService는 EmailNotifier 클래스를 의존한다.
// Email 알림 클래스
class EmailNotifier {
    public void sendEmail(String message) {
        System.out.println("Email 알림: " + message);
    }
}

// 알림 시스템
class NotificationService {
    private EmailNotifier emailNotifier;

    public NotificationService() {
		    // 구체적인 클래스인 EmailNotifier에 의존
        this.emailNotifier = new EmailNotifier();
    }

    public void sendNotification(String message) {
        emailNotifier.sendEmail(message);
    }
}

public class Main {
    public static void main(String[] args) {
        NotificationService service = new NotificationService();
        service.sendNotification("안녕하세요! 이메일 알림입니다.");
    }
}

[이메일 알림이 아닌 SMS 알림과 같은 기능이 추가되면 NotificationService 는 수정되어야 한다. DIP 위반]
-----------------------------------


// 알림 인터페이스(추상화)
interface Notifier {
    void send(String message);
}

// Email 알림 클래스
class EmailNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("Email 알림: " + message);
    }
}

// SMS 알림 클래스
class SMSNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("SMS 알림: " + message);
    }
}

// 알림 서비스 (높은 수준 모듈)
class NotificationService {
		// 추상화된 인터페이스에 의존
    private Notifier notifier;

    // 의존성 주입 (생성자를 통해 주입)
    public NotificationService(Notifier notifier) {
        this.notifier = notifier;
    }

    public void sendNotification(String message) {
		    // notifier가 어떤 구현체인지 상관하지 않음
        notifier.send(message);
    }
}

public class Main {
    public static void main(String[] args) {
        // Email 알림을 사용
        Notifier emailNotifier = new EmailNotifier();
        NotificationService emailService = new NotificationService(emailNotifier);
        emailService.sendNotification("안녕하세요! 이메일 알림입니다.");

        // SMS 알림을 사용
        Notifier smsNotifier = new SMSNotifier();
        NotificationService smsService = new NotificationService(smsNotifier);
        smsService.sendNotification("안녕하세요! SMS 알림입니다.");
    }
}
  • 추상화된 Notifier 인터페이스에만 의존한다.
    • 새로운 알림 방식이 추가되어도 NotificationService 는 변경되지 않아도 된다.
  • 필요한 Notifier 객체를 외부에서 주입받는다.
    • NotificationService는 어떤 알림 방식을 사용할지에 대한 세부 사항을 몰라도 되므로, 의존성이 약해진다.
  • 모듈간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.
  • 서로의 변경 사항에 독립적이어서 변경에 유연하다.

 

 

의존 역전 원칙 (Dependency Inversion Principle, DIP)

📌 객체 지향 설계의 5가지 원칙(SOLID) 중 하나로, 상위 수준의 모듈(비즈니스 로직이나 주요 흐름을 결정하는 코드)과 하위 수준의 모듈(구체적인 구현 코드) 간의 의존성을 분리하고, 둘 다 추상화에 의존하도록 만드는 원칙입니다.

 

= 말이 어렵지만, 추상화=공통의 기능,주제는 따로 분리하는 것 / 의존성=특정 불변의 기능 혹은 상수에 의해 코드가 유연성이 떨어지는것

= 코드는 구체적인 클래스를 구성하기 전, 공통의 기능을 가진 요소에 대해 인터페이스로 미리 추상화를 거쳐야, 추후 응용이나 변형이 쉽다는 것, 더 쉽게 말하면 그냥 추상화를 잘 유념해라

핵심 개념

  1. 상위 모듈은 하위 모듈에 의존해서는 안 된다:
    • 상위 모듈은 구체적인 구현이 아닌, 추상화(인터페이스 또는 추상 클래스)에 의존해야 한다.
  2. 추상화는 세부사항에 의존하지 않는다:
    • 구체적인 구현(세부사항)이 추상화에 의존해야 하며, 그 반대는 아니다.

 

목표

  • 시스템을 더 유연하고 확장 가능하게 설계.
  • 변경이 필요한 경우, 한 모듈의 수정이 다른 모듈에 영향을 미치는 것을 방지.
  • 고수준의 로직과 저수준의 세부사항을 분리하여 시스템의 안정성과 재사용성 향상.

 

class FileLogger {
    public void log(String message) {
        System.out.println("Logging to a file: " + message);
    }
}

class Application {
    private FileLogger logger;

    public Application() {
        logger = new FileLogger(); // 구체적 구현에 의존
    }

    public void run() {
        logger.log("Application started.");
    }
}


-----------------------------------

interface Logger {
    void log(String message);
}

class FileLogger implements Logger {
    public void log(String message) {
        System.out.println("Logging to a file: " + message);
    }
}

class DatabaseLogger implements Logger {
    public void log(String message) {
        System.out.println("Logging to a database: " + message);
    }
}

class Application {
    private Logger logger;

    public Application(Logger logger) {
        this.logger = logger; // 추상화에 의존
    }

    public void run() {
        logger.log("Application started.");
    }
}

// 사용:
Logger logger = new FileLogger();
Application app = new Application(logger);
app.run();

Application 클래스는 FileLogger의 구체적인 구현에 의존하기 때문에 다른 로거로 변경하려면 Application 코드를 수정해야 합니다.

 

=> 이제 로깅 구현을 교체하려면 Logger를 구현하는 다른 클래스를 제공하기만 하면 된다. Application은 수정이 필요하지 않다.

 

의존성 역전 원칙과 의존성 주입

  • 의존성 역전 원칙은 설계 원칙이고, **의존성 주입(Dependency Injection)**은 이를 구현하는 방법론 중 하나입니다.
  • 의존성 주입 방식:
    1. 생성자 주입(Constructor Injection)
    2. 세터 주입(Setter Injection)
    3. 인터페이스 주입(Interface Injection)

적용 사례

  • DI 프레임워크: 스프링(Spring), 구아바(Guice) 등에서 제공하는 의존성 관리 기능은 DIP를 기반으로 설계되었습니다.
  • 디자인 패턴: 전략 패턴(Strategy Pattern), 팩토리 패턴(Factory Pattern) 등에서 활용됩니다.

 

실무에서는 추상화 과정에서 비용(시간)이 발생하기 때문에 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 추후 변경된다면 인터페이스로 리팩토링 하면된다.

'기본기' 카테고리의 다른 글

클린 아키텍처 (Clean Architecture)  (1) 2025.02.23
테스트와 개발  (0) 2025.02.21
리팩토링  (0) 2025.02.15
백엔드 로드맵  (0) 2025.01.06
이름 짓기 규칙  (0) 2024.12.05

+ Recent posts