crossorigin="anonymous"> $(function(){ $('.article_view').find('table').each(function (idx, el) { $(el).wrap('
') }); $('img[alt="N"]').each(function(){ $(this).replaceWith('

N

') }); });

새소식

고수만/필드에서 써먹은 것들

클린 아키택처(Clean Architecture)의 개념과 사용법 -보통

  • -

클린 아키택처란?

 

클린 아키텍처(Clean Architecture)는 로버트 C. 마틴(Robert C. Martin : 엉클 밥 이라도 불리는 ) 이 제안한 소프트웨어 설계 원칙 중 하나이다. 클린 아키텍처의 주요 목표는 소프트웨어의 조직화, 유지 보수, 테스팅의 용이성을 향상시키는 것이다. 이 아키텍처는 여러 원리와 패턴(예: SOLID 원칙)을 기반으로 하며, 시스템을 더 유연하고, 독립적이며, 테스트 가능하게 만들려한다.

 

클린 아키텍처는 일반적으로 아래 네 개의 층으로 구성된다.:

1. 엔터티 (Entities): 비즈니스 규칙을 포함하는 중심적인 비즈니스 로직을 나타낸다. 
2. 유스 케이스 (Use Cases): 애플리케이션에 특화된 비즈니스 규칙을 나타낸다.
3. 인터페이스 어댑터 (Interface Adapters): 유스 케이스 계층과 외부 세계(웹, DB 등) 사이의 변환을 담당한다.
4. 프레임워크와 드라이버 (Frameworks and Drivers): 웹 프레임워크나 데이터베이스 등의 세부 구현을 포함한다 간단히 설명해 DB와의 연결 혹은 외부 api와의 연결을 하는 부분이다..

이렇게 구성함으로써, 클린 아키텍처는 의존성 규칙을 따르게 되며, 이는 코드의 결합도를 낮추고 모듈성을 향상시킨다. 이러한 원칙들은 소프트웨어가 변화하는 요구 사항에 더 유연하게 대응할 수 있도록 해둔다.

 

밥아저씨의 설명에 의하면 기존 설계의 문제점

  1. 우리가 해결해야 하는 문제에 대해 결정을 해야되는 시점은 종종 프로젝트의 처음에 내려진다.
  2. 한번 구조가 정해지면 새로운 요구사항에 대해 변경이 쉽지 않다. 그 이유로는
    1. 기존 로직이 프레임워크를 도구로만 사용하지 않고 프레임워크를 중심으로 설계가 이루어지기 때문
    2. 데이터 베이스를 중심으로 설계가 이루어지기 때문 
    3. 비즈니스 로직이 여러 계층에 분산되어 있기 때문이다.

 

간단히 말해 아키택처를 설계할 때 가장 중요한 부분을 처음에 정하고 진행하게 되는데 프로젝트가 설계를 100% 완벽하게 따라갈 수 없다. 개발 중간에 필요에 의해 사용하는 DB가 변경 되거나 데이터를 가져오는 방법이 바뀐다면 이미 복잡하게 얽혀있는 비즈니스 로직은 대다수가 재사용이 어렵거나 변경될 여지가 커진다. 

 

그렇기에 각각의 1. 레이어의 관심사를 확실하게 분리하고  2.종속성의 흐름을 하나의 방향으로 제어하는 것 으로 기존 로직의 문제점을 해결하려는 것이다. 

 

그럼 기존 로직에는 어떠한 문제가 있었길래 클린 아키택처를 사용하면 코드의 결합도를 낮추고 더욱 유연하게 설계가 변한다는 것일까? 아래에 예시코드를 들어 문제점을 파악해 보겠다.

 

기존 코드의 문제점 예시

 

import java.sql.*;

public class SimpleUserProfile {

    public void printUserName(int userId) {
        String url = "jdbc:mysql://localhost:3306/mydb";
        String username = "user";
        String password = "pass";

        Connection connection = null;
        Statement statement = null;
        ResultSet resultSet = null;

        try {
            // 데이터베이스에 연결
            connection = DriverManager.getConnection(url, username, password);

            // SQL 쿼리 실행
            statement = connection.createStatement();
            resultSet = statement.executeQuery("SELECT name FROM users WHERE id = " + userId);

            // 결과 출력
            if (resultSet.next()) {
                System.out.println("User name: " + resultSet.getString("name"));
            } else {
                System.out.println("User not found.");
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // 리소스 정리
            try {
                if (resultSet != null) resultSet.close();
                if (statement != null) statement.close();
                if (connection != null) connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        new SimpleUserProfile().printUserName(1);
    }
}

위의 코드는 데이터 베이스에 연결하여 User 의 정보를 가져오는 간단한 로직이다.

 

물론 위와 같은 코드로 실제 필드를 구성하는 경우는 거의 없다.

대부분 위와 같은 코드의 문제점을 해결하기 위해 DI도 사용하고 Repository 패턴과 같은 다양한 디자인 패턴으로

문제점을 해결하고 코드의 결합도를 낮추려 노력한다.

 

 

하지만 결국 우리가 마주하게 되는 가장 큰 문제는 2가지가 있다.

1. 만약 로컬 DB를 사용하지 않고 외부 API에서 데이터를 가져오게 된다면 어떻게 할 것인가. (결합도의 문제)

2. 현재 DB의 연결과 조회하는 로직과 예외를 잡는 등 비즈니스 로직들이 복잡하게 얽혀있고 이는 단일 테스트 코드를 짜기 어렵게 만든다.(유연성과 테스트의 용이성)

 

그 밖에도 여러 문제점이 있지만 이 두가지 문제점이 가장 크게 발생하게 되며 

우리는 이 문제를 해결하기 위해 관심사가 같은 레이어를 확실하게 나누고, 각 계층의 의존관계는 중심에서 외부로 퍼저나가도록 의존의 방향성을 컨트롤할 것이다.

 

각 레이어는 독립성을 유지하게 되며 비즈니스 로직을 가장 중심으로 놓고 외부로의 변화로 부터 보호하기 위해 캡슐화를 하는 것이다.

 

이는 아래와 같은 장점을 제공하게 된다.

 

클린아키택처를 적용한 코드 예시

1. 비즈니스 로직이 담겨있는 Entity 레이어 이다.

여기선 Entity의 이름을 가져오는 간단한 비즈니스 로직인 getName을 가지고 있다.

User의 필드 변수들은 private로 캡슐화 되어 getName으로만 entity의 정보를 가져올 수 있도록 하여 다른 레이어에서

entity를 오염시키는 문제를 해결한다.

// Entity
public class User {
    private int id;
    private String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

2. 인터페이스의 어답터 레이어이다.

인터페이스를 사용하여 코드의 유연성을 더해주며 Action 의 트리거에 대해 비즈니스 로직은 관심을 끌 수 있다.

entity를 외부 API든 DB든 가져와 구성하는 부분을 담당한다.

// Repository Interface
public interface UserRepository {
    User getUserById(int userId);
}
// Repository Implementation
public class SqlUserRepository implements UserRepository {
    // ... (database connection and other details)

    @Override
    public User getUserById(int userId) {
        // Implement the logic to fetch user from the database
        // ...
        return new User(userId, "exampleName");
    }
}

 

Use Case의 레이어이며 어플리케이션 비즈니스 로직이 되는 부분이다. 

인터페이스 어답터를 호출하여 사용해야 되는 entity를 불러오거나 비즈니스 로직을 수행 할 수 있도록 만들어주는 

부분이다.

참고로 user != null ? user.getName() : "User not found"; 이 부분은

조금 더 entity의 부분에 비즈니스 로직을 집중할 수 있다.

// Use Case
public class GetUserUseCase {
    private UserRepository userRepository;

    public GetUserUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String execute(int userId) {
        User user = userRepository.getUserById(userId);
        return user != null ? user.getName() : "User not found";
    }
}

 

프레임워크와 드라이버의 레이어이다. web api가 될수도 있고 mvc controller가 될 수도 있다. 비즈니스 로직의 Action이

발생할 수  있도록 Use Case 레이어를 호출하는 부분이다.

 

// Client Code
public class Main {
    public static void main(String[] args) {
        UserRepository userRepository = new SqlUserRepository();
        GetUserUseCase getUserUseCase = new GetUserUseCase(userRepository);

        System.out.println(getUserUseCase.execute(1));
    }
}

 

 

클린 아키택처의 장점

위와 같이 코드가 바뀌게 되면

 

1. 기존의 entity를 불러오는 방식이 바뀌더라도(예를 들어 DB-> 외부 API) adapter의 교체를 통하여 비즈니스 로직이 크게 바뀌지 않을 수 있다.

 

이를 증명할 수 있도록 코드를 보여주겠다.

 

// Repository Interface
public interface UserRepository {
    User getUserById(int userId);
}

 

기존의 인터페이스는 바뀔 필요가 없다.

그러나  SqlUserRepository 대신 ApiUserRepository 로 api에 접근하여 entity를 가져오는 인터페이스 구현 클래스를

만든다. 

// Repository Implementation for API
public class ApiUserRepository implements UserRepository {
    // ... (API client and other details)

    @Override
    public User getUserById(int userId) {
        // Implement the logic to fetch user from the API
        // For example, making an HTTP GET request to fetch user data
        // ...

        // Here is a mocked user data as an example
        return new User(userId, "exampleNameFromAPI");
    }
}

 

그리고 아래와 같이 userRepository만 갈아끼워 주면 기존 비즈니스 로직의 변경없이 데이터 로드 방법을 바꿀 수 있으며

코드의 유연성을 가져다 준다.

// Client Code
public class Main {
    public static void main(String[] args) {
        UserRepository userRepository = new ApiUserRepository();
        GetUserUseCase getUserUseCase = new GetUserUseCase(userRepository);

        System.out.println(getUserUseCase.execute(1));
    }
}

 

2. 테스트의 용이성을 가져다 준다.

 

위의 코드를 부면 UserRepository를 바꿔줄 수 있도록  interface로 구현하였는데 

// Client Code with Dependency Injection
public class Main {
    private final GetUserUseCase getUserUseCase;

    public Main(UserRepository userRepository) {
        this.getUserUseCase = new GetUserUseCase(userRepository);
    }

    public void execute() {
        System.out.println(getUserUseCase.execute(1));
    }

    public static void main(String[] args) {
        // Dependency Injection: Inject the actual UserRepository implementation here
        UserRepository userRepository = new ApiUserRepository(); // or new SqlUserRepository();
        Main main = new Main(userRepository);
        main.execute();
    }
}

 

위 코드를 보면 아까전 Main문에 DI를 사용한 코드이다.

이렇게 만들면 테스트 코드에 Mock 객체를 만들어 주입해 쉽게 테스트 할 수 있도록 만들 수 있다.

import org.junit.jupiter.api.Test;
import static org.mockito.Mockito.*;

public class MainTest {
    @Test
    public void testExecute() {
        // Arrange
        UserRepository mockUserRepository = mock(UserRepository.class);
        when(mockUserRepository.getUserById(1)).thenReturn(new User(1, "testUser"));

        Main main = new Main(mockUserRepository);

        // Act
        main.execute();

        // Assert
        // 여기서 원하는대로 검증 코드를 추가할 수 있습니다.
    }
}

 

3. 코드의 응집도가 높아진다.

 

기존 코드를 다시보면 DB의 연결, SQL 호출 , 예외처리가 모두 한 부분에 들어가 있다.

그러나 바뀐 코드를 보면 가장 중요한 entity의 데이터 정보를 가져오는 부분은 Entity로 가있는 것을 알수 있다.

지금 예시가 너무 쉽고 비즈니스로직이 덜 모인걸 볼수 있는데  

 

// Entity
public class User {
    private int id;
    private String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }
    
    public String getName() {
        return name;
    }

    public String getDisplayName() {
        return name != null ? name : "User not found";
    }
}

위와 같이 use case에서 담당하던

return user != null ? user.getName() : "User not found"; 부분을

캡슐화 하여 Entity로 옮긴다면 아래와 같이 use case 부분이 더욱 단순해지고

해당 로직이 다른 곳에서 쓰인다고 가정한다면 코드이 재사용률 까지 높일 수 있다.

// Use Case
public class GetUserUseCase {
    private UserRepository userRepository;

    public GetUserUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String execute(int userId) {
        User user = userRepository.getUserById(userId);
        return user.getDisplayName();
    }
}

 

결국 핵심 비즈니스는 Entity에 몰리게 되며 코드의 응집도가 높아지는 결과를 만든다.

이는 DDD(Domain-Driven Design)하고도 연결 된다.

 

DDD란  무엇인가?

 Eric Evans가 "Domain-Driven Design: Tackling Complexity in the Heart of Software"라는 책에서 소개했으며, 이 방법론은 비즈니스 도메인의 복잡성을 해결하기 위해 도메인 모델링에 중점을 둔다.

 

DDD는 도메인이라는 개념을 도입해 Entity의 정의를 더욱 확실하게 정리한다.

도메인을 기반으로 하여 비즈니스의 문제와 요구사항 영역을 명확하게 구분하여 의사소통을 개선하는데 중점을 둔다.

 

도메인이라는 속성을 명확하게 구분하고 그걸 중심으로 설계하는 방식인데 이는 클린아키택처와 함께 사용될 수 있으며

서로를 보완할 수 있기에 자주 같이 쓰인다.

클린 아키텍처는 소프트웨어의 아키택처에 중점을 둔다. 따라서 레이어의 분리 그리고 의존성의 흐름 컨트롤이 중점인데

여기에 비즈니스 문제 사항을 도메인이란 개념으로 모델링해 정리하면 비즈니스 로직이 더욱 응축될 수 있기 때문이다.

 

아까의 예시코드에서보면 DDD로 개발 하지 않으면 Entity 부분은 단순히 DTO처럼 데이터를 담는 그릇으로 전략되고

 

// Entity
public class User {
    public int id;
    public String name;   
}

 

// Use Case
public class GetUserUseCase {
    private UserRepository userRepository;

    public GetUserUseCase(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public String execute(int userId) {
        User user = userRepository.getUserById(userId);
        return user != null ? user.name : "User not found";
    }
}

과 같이 만들어 질 수있다.

이렇게 되면 application 레이어에 비즈니스 로직이 집중 될 수도 있다.

이렇게 되었을 때의 단점은 굳이 말하지 않아도 위 글들을 읽었다면 유추할 수 있을 것이라 생각한다.

 

4. 아키택처의 안정성

클린 아키택처는 비즈니스 로직이 Entity에 집중되고 캡슐화 되어 설계의 중심에 있을 수 있다. 이는 아키책처의 안정성을 가져온다. 중심부는 다른 계층에 의존가능성을 열어주며 외부로 갈 수록 내부에 의존하기 때문에 단뱡향 의존성을 가진다.

즉 Entity는 모든 곳에서 의존하여 사용되어질 수 있지만 Entity에선 외부 레이어의 참조가 불가능하기에 관심을 끌 수 있다.

이는 외부의 변화에 둔감해 질 수 있으며 핵심 비즈니스 로직의 안정성을 가질 수 있게 된다.

 

// 데이터베이스 연결을 담당하는 클래스
public class DatabaseConnector {
    public Connection connect() {
        // 데이터베이스 연결 로직
    }
}

// 고객 정보를 처리하는 비즈니스 로직 클래스
public class CustomerService {
    private DatabaseConnector databaseConnector;
    
    public CustomerService() {
        this.databaseConnector = new DatabaseConnector();
    }
    
    public Customer getCustomer(int customerId) {
        Connection connection = databaseConnector.connect();
        // 고객 정보를 데이터베이스에서 조회하여 반환하는 로직
    }
}

 

위의 코드는 단방향 의존성을 가지지 않은 간단한 예시 코드이다.

고객의 정보를 데이터 베이스에서 조회하는 어플리케이션이다.

 

기본적으로 클린아키택처는 저수준 모듈 -> 고수준 모듈에 의존을 하는 단방향을 이룬다.

고수준 모듈은 비즈니스 로직을 포함하고 있는 모듈을 의하는데 여기서 보면 

Service 로직은 비즈니스 로직을 가진 고수준 모델이지만 DatabaseConnector는 연결만을 하는 저수준 모듈이다.

그러나 Service 모듈은 DatabaseConnector를 직접 의존하고 있기 때문에 DatabaseConnector가 변경되었을 경우

영향을 받게 된다. 따라서 클린아키텍처에선 인터페이스를 두고 의존성의 역전을 사용한 것이다.

 

 

이렇듯 클린아키택처는 설계적으로 여러 장점을 가져다 주다.

무엇보다도 마이크로 서비스를 제공하게 될 때 큰 힘이 될 것이다.

클린 아키택처와 DDD는 비즈니스 로직의 응집과 테스트 코드의 용이함을 주기에

더 작은 필수 모듈로 나누고 테스트 코드 및 배포에 적합한 설계가 될 수 있다고 생각한다.

 

 

https://www.freecodecamp.org/news/a-quick-introduction-to-clean-architecture-990c014448d2/

 

A quick introduction to clean architecture

by Daniel Deutsch A quick introduction to clean architecture Photo by Rubén García on Unsplash — https://unsplash.com/photos/R-wQExeiGrcIn an open source project [https://github.com/Keep-Current] I started to contribute to, the concept of “clean arch

www.freecodecamp.org

 

Contents