개발을 하면서 발생하는 반복적인 문제에 대해 효과적으로 해결할 수 있는 모범 사례를 의미한다. 즉, 특정 문제를 해결할 수 있는 정해진 틀(설계)이다.
데이터와 데이터를 다루는 메소드를 하나로 묶어 독립적으로 이루는 것
(내부 데이터 접근 제한 , 코드 유지보수 향상)
자식 클래스는 부모 클래스의 모든 기능과 필드를 상속받을 수 있고 추가로 구현을 할 수 있다.
(코드의 재사용성 증가 , 계층적인 구조를 표현할 수 있음)
메서드를 재정의하거나(오버라이딩) , 파라미터 타입 및 개수를 변경해(오버로딩) 다른 기능을 구현할 수 있다.
(예를 들어 System.out.println() 은 int도 가능 , String도 가능 ..(오버로딩) )
복잡한 현실을 단순하게 추상화하여 핵심 기능에 집중하고 세부 구현은 숨기게 할 수 있다.
SRP(단일책임원칙) : 클래스는 하나의 책임만 가질 것 (클래스를 변경할 이유는 하나일 것)
OCP(개방폐쇄원칙) : 클래스는 확장에 열려있고 수정은 폐쇄적이어야 함 (확장시 기존 코드 변경없이)
LSP(리스코프치환원칙) : 자식 클래스는 언제든 부모 클래스 기능을 사용할 수 있어야 함
ISP(인터페이스분리원칙) : 인터페이스는 필요로 하는 작은 단위로 분리해 필요 이상의 메소드를 구현/의존 하도록 강요하면 안됨
DIP(의존역전원칙) : 의존성은 추상화에 의해 이루어지고, 구체적인 구현체에 의존하면 안됨
디자인 패턴이란, 개발을 하면서 발생하는 반복적인 문제를 효과적으로 해결하는 모범 사례를 의미한다. 즉, 특정 문제를 해결할 수 있는 정해진 설계도와 같다.
이런 디자인 패턴은 객체 지향의 4대 특성(캡슐화 , 상속, 추상화 , 다형성) 과 설계원칙(SOLID)를 기반으로 구현한다. (의미는 사전 단어 정리를 참조 )
클린 아키텍처는 이런 특징을 가진 디자인 패턴들 중에 하나로
"추상화개념"을 적용하여 관심사를 분리해 컴포넌트 간의 의존도를 낮추는 것이 목적이다.
의존도가 낮다는 의미는 독립적으로 테스트가 용이하고 유지보수 및 확장이 높아질 수 있다는 의미가 된다.
Enterprise Business Rules (Entities)
애플리케이션의 비즈니스 로직에 사용되는 핵심 클래스이다.
Application Business Rules (Use Cases)
애플리케이션의 특정 기능을 수행하는 비즈니스 규칙을 캡슐화한다.
Interface Adapters (Controller , Gateways , Presenters .. )
외부 프레임워크나 UI 등의 외부 장치와 내부 로직을 연결한다.
Frameworks & Drivers ( External Interfaces , DB , UI , Web ... )
DB, 프레임워크 등 외부시스템을 의미한다.
비즈니스 로직에 사용되는 핵심 클래스를 의미한다.
public class Book {
private String title;
public Book(String title, String author) {
this.title = title;
}
// getters and setters ...
}
애플리케이션의 특정 기능을 수행하는 비즈니스 규칙을 캡슐화한다.
public class CreateUserUseCase {
private final UserRepository userRepository;
public CreateUserUseCase(UserRepository userRepository) {
this.userRepository = userRepository;
}
//User 엔터티를 활용해서 새로운 사용자를 생성하고 저장하는 내부 로직 구현
public void execute(String id, String name, String email) {
User user = new User(id, name, email);
userRepository.save(user);
}
}
외부 프레임워크나 UI 등의 외부 장치와 내부 로직을 연결한다.
//Repository (DB와 통신을 위한 어댑터 역할)
public interface UserRepository {
void save(User user);
User findById(String id);
List<User> findAll();
}
//Repository를 실제로 구현한 구현체
//예) Oracle방식 , MySql방식 , H2방식 등
// 필요시 Repository에 위 구현체만 갈아끼우면 다양한 DB 환경을 사용할 수 있음
public class InMemoryUserRepository implements UserRepository {
private Map<String, User> database = new HashMap<>();
@Override
public void save(User user) {
database.put(user.getId(), user);
}
}
DB, 프레임워크 등 외부시스템과의 통합을 처리하는 곳
public class Main {
public static void main(String[] args) {
InMemoryUserRepository userRepository = new InMemoryUserRepository(); //다른 DB를 원한다면 이 구현체를 변경하면 됨
CreateUserUseCase createUserUseCase = new CreateUserUseCase(userRepository);
UserController userController = new UserController(createUserUseCase);
userController.createUser("1", "John Doe", "john.doe@example.com");
}
}
public class UserService {
private UserRepository userRepository = new InMemoryUserRepository();
// 비즈니스 로직과 데이터 접근 로직이 섞여 있음
public void createUser(String id, String name, String email) {
// 비즈니스 로직: User 객체 생성
User user = new User(id, name, email);
// 데이터 접근 로직: Repository에 User 저장
userRepository.save(user);
}
public User getUser(String id) {
// 데이터 접근 로직: Repository에서 User 조회
return userRepository.findById(id);
}
// Repository 인터페이스
interface UserRepository {
void save(User user);
User findById(String id);
}
// InMemoryUserRepository 클래스가 데이터 접근 로직을 구현
class InMemoryUserRepository implements UserRepository {
...
}
Entity와 UseCase가 내부 시스템이며 이를 외부 시스템(DB 등) 과 연계할 때 그 사이에 인터페이스와 구현체로 계층을 두어 통신을 담당한다.
예를 들면,
(1) User는 비즈니스에 중요한 내부 도메인 객체이다.
(2) UserManager는 위 User 엔터티를 사용해 내부 비즈니스 로직을 구현한다.
(3) UserManager는 외부 DB 접속을 위해 UserRepositry를 주입받고 있다.
중요한 점은 UserManager에서 직접 DB연결 설정등을 통해 실제 DB를 주입하는 것이 아니라 인터페이스를 주입받아 사용하고 있는 점이다
(4) 즉 DB라는 외부 시스템을 사용하기 위해 UserRepository 인터페이스와 실제 구현체인 DatabaseUserRepository를 이용해 외부 DB와 내부 시스템간의 소통을 하는 게이트 웨이인 셈이다.
//결제 인터페이스 (어댑터 역할)
public interface PaymentProcessor {
void processPayment(double amount);
}
public class CreditCardPaymentProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// 신용카드 결제 처리 로직
}
}
public class CashPaymentProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
// PayPal 결제 처리 로직
}
}
...
//나는 신용카드 결제 기능을 쓸거야
PaymentProcessor paymentProcessor = new CreditCardPaymentProcessor();
//OR
//나는 현금 결제 기능을 쓸거야
PaymentProcessor paymentProcessor = new CashPaymentProcessor();
PaymentProcessor 인터페이스를 정의하고 이를 구현한 구현체 클래스를 원하는 경우에 맞춰 주입하고 있다 (다형성 원리) . 특정 프레임워크에 종속되는 게 아니라 원하는 결제 처리 방식을 자유롭게 활용할 수 있다.
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void addUser(User user) {
userRepository.save(user);
}
}
UserService는 @Service라는 스프링 프레임워크에서 사용하는 어노테이션을 사용하고 있기 때문에 이를 다른 프레임워크에 적용할 수 없다. 즉 스프링이라는 특정 프레임워크이 제공하는 DI 기능에 의존하고 있다.
public class Calculator {
public int add(int a, int b) { return a + b; }
public int subtract(int a, int b) { return a - b; }
}
//비즈니스 로직을 각 메서드별로 독립적으로 실행할 수 있음
public class CalculatorTest {
@Test
public void testAdd() {
Calculator calculator = new Calculator();
assertEquals(5, calculator.add(2, 3));
}
@Test
public void testSubtract() {
Calculator calculator = new Calculator();
assertEquals(2, calculator.subtract(5, 3));
}
}
Calculator 클래스의 두 메서드를 각각 테스트로 확인하고 있다.
외부의 요소의 도움이 없어도 원하는 비즈니스 로직만 테스트를 할 수 있다.
// 외부 의존성이 강한 코드
public class Calculator {
public int add(int a, int b) {
// 외부 API 호출 등의 의존성이 있음
return ExternalService.add(a, b);
}
}
이 경우 테스트 환경에서 다른 Service의 메소드를 호출해 결과를 반환한다.
만약 외부API의 파라미터 개수등이 변경된다면 이는 에러가 발생한다.
위 소스코드 처럼 adapter 와 실제 구현체 개념을 사용하므로 adapter 역할을 하는 인터페이스를 알맞게 구현한 어떤 클래스도 변환하여 적용할 수 있다.
(DB 독립적 = 어떤 DB든 구애받지 않고 사용 가능)
외부 요소의 변경이 시스템에 영향을 미치지 않음
클린 아키텍처는 약간 체감을 해봐야 편리함을 알 수 있을 것 같다. 소스코드를 짜다보면 수정을 해야하는 경우가 발생하는데, 이 때 번거롭게 다른 소스도 건드려야 한다면 정말 귀찮은 일이다. 이런 문제를 사전에 해결할 수 있도록 정의된 방법론(아키텍처 등)을 사용해 개발의 편의성과 효율을 높일 수 있다고 생각한다.
https://daryeou.tistory.com/280
https://ittrue.tistory.com/550
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html