클린 코드
📌 클린 코드는 단순히 잘 동작하는 코드가 아니라, 가독성, 유지보수성, 확장성이 뛰어난 코드를 의미합니다. 이는 협업과 장기적인 코드 품질을 유지하기 위한 필수적인 개발 철학 입니다.
- Dirty Code는 폭탄이다. A를 수정하니 B가 터지는 연쇄반응이 수시로 나온다.
- 협업의 필수 조건으로, 사실 본인이 혼자해도 이 규칙을 어느정도 지켜야한다.
- 기술 부채가 쌓이면 나중에는 결국 리펙토링이 아니라 재개발하는게 더 좋을 수도 있다.
- 프로그래머는 가뜩이나 이직이 많아 에일리언코드(담당자가 없어져서든 버전의 변화로든 알 수 없는 코드를 말한다.)가 많이 생성될 가능이 높다.
리팩터링
📌 결과의 변경 없이 코드의 구조를 재조정하는 것을 말합니다. 주로 가독성을 높이고 유지보수를 편하게 하기 위해 추후 수정하는 것을 말합니다. 버그를 없애거나 새로운 기능을 추가하는 행위는 아닙니다.
- 코드가 읽기 어려운 순간 (코드를 읽으면 바로 이해가 되어야한다.)
- 수정할 때마다 오류가 발생하는 경우
- 같은 코드가 여기저기 복붙되어 있는 경우
- 확장하려는데 코드 구조가 방해되는 경우
- 단위 테스트 작성이 어려운 경우
보통 위와 같은 상황에서 진행하게 된다.
클린 코드의 기본 원칙
의미 있는 이름 짓기 |
|
함수 분리 하기 |
|
불필요한 주석 제거 |
|
코드 중복 제거 | DRY (Don’t Repeat Yourself) 원칙을 준수 |
복잡한 코드를 단순화하기 |
|
부정 표현을 긍정 표현으로 바꾸기 |
|
else 문 사용 지양하기 |
|
의미 있는 이름 짓기
// 나쁜 예
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);
}
}
}
- 구체적이고 의도를 담은 이름을 사용
- 매직 넘버를 피하라
- 데이터의 의미를 이름에 반영
함수 분리 하기
// 나쁜 예
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.");
}
- ★ 하나의 함수는 하나의 역할만 수행 (sendEmail은 이메일 발송의 흐름만 관리)
- 복잡한 작업은 작은 함수로 분리
- 함수 이름은 동작과 목적을 명확히 표현
불필요한 주석 제거
// 나쁜 예
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 + "원 전송됨.");
}
- 주석은 코드가 아닌 의도를 설명
- “어떻게”가 아닌 “왜”를 설명
- 주석 대신 명확한 변수와 함수 이름으로 의도를 드러냄
- 불필요한 주석은 제거하고, 코드는 가능한 자체적으로 읽히게 작성
- 사실 주석은 함부로 쓰지 않는게 좋다. 추후 관리가 어렵고 주석은 전부 개발자가 직접 달아야한다보니 더 문제가 많이 생긴다.
코드 중복 제거
// 나쁜 예
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();
}
- 조건문이 복잡하거나 여러 논리를 포함한다면 메서드로 분리
- isActiveAdultUser가 중요한 것 = 왜 했는지
부정 표현을 긍정 표현으로 바꾸기
// 나쁜 예
if (!user.isInActive()) {
return "Inactive User";
}
return "Active User";
// 좋은 예
if (user.isActive()) {
return "Active User";
}
return "Inactive User";
- 긍정적 변수명 사용 - 이거 상당히 중요하다
- 긍정적 조건문 작성
- 이중 부정 지양 = 이건 반의어를 생각해봐라, 진짜 웬만해서는 피해야한다.
- 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";
}
- else 문을 피하고 기본 동작을 명시
- 전처리와 핵심 로직을 분리
- 각 조건은 독립적으로 처리
클린 코드 이론 원칙
클린 코드를 작성하는 데 있어 추가적으로 고려할 수 있는 원칙과 실천 사항은 다음과 같습니다:
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)
📌 객체 지향 설계의 핵심 원칙 중 하나로, 소프트웨어 모듈, 클래스, 또는 함수가 다음과 같은 두 가지 상태를 만족해야 한다는 개념
- 확장에는 열려 있어야 한다 (Open for extension):
- 새로운 기능을 추가하거나 요구사항의 변화에 따라 시스템의 동작을 확장할 수 있어야 한다.
- 기존 코드를 수정하지 않고도 기능 추가가 가능해야 한다.
- 수정에는 닫혀 있어야 한다 (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) 중 하나로, 상위 수준의 모듈(비즈니스 로직이나 주요 흐름을 결정하는 코드)과 하위 수준의 모듈(구체적인 구현 코드) 간의 의존성을 분리하고, 둘 다 추상화에 의존하도록 만드는 원칙입니다.
= 말이 어렵지만, 추상화=공통의 기능,주제는 따로 분리하는 것 / 의존성=특정 불변의 기능 혹은 상수에 의해 코드가 유연성이 떨어지는것
= 코드는 구체적인 클래스를 구성하기 전, 공통의 기능을 가진 요소에 대해 인터페이스로 미리 추상화를 거쳐야, 추후 응용이나 변형이 쉽다는 것, 더 쉽게 말하면 그냥 추상화를 잘 유념해라
핵심 개념
- 상위 모듈은 하위 모듈에 의존해서는 안 된다:
- 상위 모듈은 구체적인 구현이 아닌, 추상화(인터페이스 또는 추상 클래스)에 의존해야 한다.
- 추상화는 세부사항에 의존하지 않는다:
- 구체적인 구현(세부사항)이 추상화에 의존해야 하며, 그 반대는 아니다.
목표
- 시스템을 더 유연하고 확장 가능하게 설계.
- 변경이 필요한 경우, 한 모듈의 수정이 다른 모듈에 영향을 미치는 것을 방지.
- 고수준의 로직과 저수준의 세부사항을 분리하여 시스템의 안정성과 재사용성 향상.
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)**은 이를 구현하는 방법론 중 하나입니다.
- 의존성 주입 방식:
- 생성자 주입(Constructor Injection)
- 세터 주입(Setter Injection)
- 인터페이스 주입(Interface Injection)
적용 사례
- DI 프레임워크: 스프링(Spring), 구아바(Guice) 등에서 제공하는 의존성 관리 기능은 DIP를 기반으로 설계되었습니다.
- 디자인 패턴: 전략 패턴(Strategy Pattern), 팩토리 패턴(Factory Pattern) 등에서 활용됩니다.
실무에서는 추상화 과정에서 비용(시간)이 발생하기 때문에 기능을 확장할 가능성이 없다면 구현 클래스를 직접 사용하고 추후 변경된다면 인터페이스로 리팩토링 하면된다.