[Architecture] 1. 소프트웨어 아키텍처

KSK·2025년 3월 23일

Architecture

목록 보기
1/4

개요

  • 소프트웨어 아키텍처란
  • 주요 소프트웨어 아키텍처

이 시리즈의 최종목적은 클린 아키텍처에 대한 학습,
그리고 안드로이드 권장 아키텍처인 AAC에 대한 학습이다.

소프트웨어 아키텍처

소프트웨어 아키텍처란?

시스템의 기본구조이자, 시스템을 구성하는 요소와 각 요소간의 관계를 정의하는 청사진

즉, 소프트웨어 시스템을 구성하는 서브시스템, 컴포넌트 간의 관계를 정의하는 방식을 의미하는데, 두루뭉술한 설명에 여전히 이게 뭔데? 싶은 사람들도 있을 것이다.

좀 어렵다면, 하나의 서비스가 어떻게 구성되며 어떻게 동작이 된다 를 기술한 것이라고 이해하면 될 듯 하다. 실제로 어느정도 넓은 의미로 사용되는 단어이지 좀 더 특정된 개념을 나타내는 단어는 아니라 생각한다.

설계와의 차이

일반적으로

  • 설계는 저수준의 코드레벨에서의 구조와 패턴, 해결방법
  • 아키텍처는 더 고수준의, 시스템 단위에서의 구조와 데이터 흐름

라고 이야기 한다.

Clean Architecture에 따르면,

고수준의 결정사항과 저수준의 세부사항은 전체 설계의 구성요소일 뿐이다.
라고 하는데

두 개념의 차이는 어느 정도 인지하되, 명확한 구분에 목맬 필요 없이
좋은 설계는 좋은 아키텍처를 기반으로 하며, 좋은 아키텍처는 좋은 설계를 촉진하는 상호 보완적인 개념 이라고 이해하는 것이 좋을 것 같다.


왜 쓰는건데?

1. 개발자간의 약속

여러 개발자가 관여하는 프로젝트에서 상호간의 약속 없이 마구잡이로 파일을 추가하거나, 이름을 짓는다면 많은 혼란이 발생할 것 이다.

이 때 아키텍처 구조는 일종의 약속 역할을 하여, 파일이나 디렉토리 간 복잡한 의존관계를 가지거나 로직이 산발적으로 구성되는 일을 방지해 일관적인 코드 작성이 가능하다.

또한 코드를 파악하는데 있어서 전체적인 흐름과 설계를 이해하는데 도움이 될 수 있다.

이는 장기적으로 협업 능력을 향상 시켜 생산성 향상으로 이어질 수 있다.

2. 유지보수와 확장성

시스템의 전체 구조와 흐름을 정의해놓았기 때문에 유지보수를 위해 코드를 파악하는데 도움이 될 뿐만 아니라 코드가 체계적으로 구성되어 수정 및 확장하는데에도 용이하다.

또한 관심사 분리가 명확히 적용된 아키텍처를 사용한다면 추가/수정 시 기존 코드에 주는 영향을 최소화 할 수 있다.

실제로 초보일때 마구잡이로 기능개발을 하다가 이후 추가 기능 개발이나 수정 시 큰 벽을 느낀 사람이라면 이 점이 가장 큰 장점으로 다가오지 않을까 싶다.

실제로 SW 아키텍처 적용하는 이유의 핵심이라고 생각한다.

3. 테스트 용이

아키텍처 패턴을 적용해 시스템을 구성하는 각 컴포넌트 간 의존성을 최소화하고 그 관계를 명확히 정의해놓았다면 모듈 별 개별 테스트에 용이하다.

또한 테스트 결과에 따라 동작을 바꾸어도 서로에 미치는 영향을 최소화 할 수 있다.


위 그래프는 유명 엔지니어인 마틴 파울러가 언급한 내용으로,

  • 아키텍처 없는 개발은 초반에 빠른 생산성을 가져올 수 있지만, 개발이 지속될수록 점차 생산성이 떨어지게 된다.
  • 하지만 좋은 아키텍처를 사용하게 된다면, 초반 생산성은 떨어질지라도 개발이 지속될 수록 생산성이 향상된다는 것이다.

아키텍처 원칙

많은 소프트웨어 아키텍처 패턴에 적용되는 원칙으로, 아키텍처 사용의 목적인 개발 간 발생하는 복잡성을 관리하고, 유지보수성와 확장성을 향상시키기 위한 원칙이다.

1. 관심사 분리

Seperation of Concerns. 시스템을 독립된 부분으로 나누어 각 요소가 특정 관심사에만 집중하도록 하는 원칙이다. 코드가 단위별로 단일 목적에만 충실할 수 있도록 잘 분리하는 것을 의미한다.

관심사가 잘 분리된 코드는 곧 유지보수에도 큰 관계가 있다.

사용자 로그인 기능을 구현한다고 할 때,
로그인 기능은 다음과 같은 세가지 주요기능을 포함한다.

  • A: 사용자 입력 검증 (이메일/비밀번호 형식 확인)
  • B: 로그인 요청 처리 (서버 API 호출)
  • C: 로그인 결과 처리 (성공 시 홈 화면 이동, 실패 시 오류 메시지 표시)
// 예시는 Kotlin으로 작성되었습니다.

fun login(email: String, password: String) {
    // A: 이메일, 비밀번호 유효성 검사
    if (!isValidEmail(email) || password.length < 6) {
        showError("잘못된 입력입니다.")
        return
    }

    // B: 서버 API 요청
    val response = api.login(email, password)

    // C: 응답 처리 및 화면 이동
    if (response.isSuccessful) {
        navigateToHome()
    } else {
        showError("로그인 실패")
    }
}

위 처럼 로그인 함수에 각 A,B,C 모든 구현부가 모여있다면,
입력 검증 A 로직ㅇ을 변경하거나 추가할 때 전체 함수에 영향을 미칠 수 있다.

그러나 A,B,C 각 기능만을 담당하도록 코드를 분리하고, ABC의 결과값을 받아 합치는 기능만을 수행하는 D를 분리한다면, C 기능을 수정해야할 때 C만을 수정하면 될 일이다.

// 예시는 Kotlin으로 작성되었습니다.

fun validateInput(email: String, password: String): Boolean {
    return isValidEmail(email) && password.length >= 6
}

fun loginUser(email: String, password: String): Response {
    return api.login(email, password)
}

fun handleLoginResponse(response: Response) {
    if (response.isSuccessful) {
        navigateToHome()
    } else {
        showError("로그인 실패")
    }
}

fun login(email: String, password: String) {
    if (!validateInput(email, password)) {
        showError("잘못된 입력입니다.")
        return
    }
    
    val response = loginUser(email, password)
    handleLoginResponse(response)
}

위와 같이 관심사 분리를 통해 함수를 분리한다면,

  • 입력 검증 로직(A)을 변경해야 할 때, validateInput() 함수만 수정하면 됨
  • 로그인 요청 방식(B)이 변경될 경우 loginUser() 함수만 수정하면 됨
  • 결과 처리(C) 로직을 변경할 때도 handleLoginResponse()만 수정하면 됨

이처럼 유지보수성을 향상시킬 수 있다.

2. 모듈화

Modularity. 시스템을 여러 모듈로 분할하고 각 모듈은 특정 기능을 담당하도록 하는 방식.

각 모듈은 독립적으로 개발, 테스트, 배포 될 수 있기 때문에 시스템의 복잡성을 최소화 시킬 수 있는데, 이를 위해선 모듈 간 의존성을 최소화 시켜 결합도를 낮추고 모듈 내부의 응집도를 높여야한다.

3. 추상화, 캡슐화

Abstraction. 추상화는 복잡한 시스템의 불필요한 세부 구현 사항은 감추고, 중요 개념만 드러내어 표존화된 방법으로 시스템 각 요소가 상호작용 할 수 있도록 하는 개념이다.

쉽게 말해 사용자는 핵심 기능만 알면 되고 내부 동작 방식은 신경쓰지 않아도 되는 것이다.

인터페이스로 타입을 추상화 시켜 사용하는 이유와 일맥상통하다.

interface Car {
    fun start()
    fun drive()
    fun stop()
}

class Tesla : Car {
    override fun start() {
        println("Tesla 시동을 겁니다.")
    }

    override fun drive() {
        println("Tesla가 전기로 움직입니다.")
    }

    override fun stop() {
        println("Tesla가 멈춥니다.")
    }
}

fun main() {
    val myCar: Car = Tesla()
    myCar.start()  // 내부 구현을 몰라도 사용 가능
    myCar.drive()
    myCar.stop()
}

우리는 자동차를 운전할 때, 엔진 내부에서 연료가 어떻게 연소되는지 몰라도 된다.
단순히 엑셀을 밟으면 가고, 브레이크를 밟으면 멈춘다는 것만 알면 된다.

위 코드에서, 사용자는 Car라는 인터페이스를 통해 시동을 걸고, 움직이고, 멈추는 동작을 하기만 하면 된다. 유저는 세부적으로 각 기능이 어떻게 동작하는지는 알 필요 없으므로 이는 하위 클래스인 Tesla에 구현되어 있다.

이 역시 유지보수와 확장성을 높이는데 도움이 되는데, 만약 테슬라 차량에 오토파일럿 기능을 추가한다고 하면 사용부인 main()의 수정 없이 구현부인 Tesla 클래스의 drive()만 수정하면 된다.

여전히 유저는 세부 구현에 대해 알 필요 없다는 말이다.


Encapsulation. 캡슐화는 객체의 내부 데이터(속성)과 기능(메소드)를 숨기고, 필요한 기능만 외부에 공개하는 것이다.

쉽게 말해, 객체의 내부 상태를 보호하고, 외부에서는 허용된 방법으로만 접근할 수 있도록 하는 것 이다.

class Car {
    private var speed: Int = 0 // 🚨 외부에서 직접 접근 불가능 (캡슐화)

    fun accelerate(amount: Int) {
        if (speed + amount > 200) {
            println("🚨 속도는 200km/h를 넘을 수 없습니다.")
        } else {
            speed += amount
            println("🚗 현재 속도: $speed km/h")
        }
    }

    fun getSpeed(): Int { // ✅ 안전한 속도 확인 메서드 제공
        return speed
    }
}

fun main() {
    val myCar = Car()

    // myCar.speed = 500 🚨 오류! private 속성이라 직접 변경 불가능
    myCar.accelerate(50) // 🚗 현재 속도: 50 km/h
    myCar.accelerate(200) // 🚨 속도는 200km/h를 넘을 수 없습니다.
    println("현재 속도: ${myCar.getSpeed()} km/h") // ✅ 현재 속도: 50 km/h
}

자동차의 속도 제한 값을 직접 변경할 수 있다면 큰 문제가 발생할 것이다.
위 코드에선 accelerate() 함수로만 speed 값을 변경할 수 있게 은닉함으로써

  • 데이터의 직접 접근을 막아 보호하고
  • 잘못된 값이 입력되지 않도록 검증하여

객체의 일관성을 유지하고 오류를 방지한다.

주요 소프트웨어 아키텍처

대표적인 소프트웨어 아키텍처 종류를 몇개만 살펴봄으로써 위에서 설명한 내용들이 어떻게 적용되어 있는지 알아보자.

레이어드 아키텍처(Layered)

레이어드 아키텍처는 SW 시스템을 여러 계층으로 분리해,
각 계층이 특정 역할을 담당하도록 만든 아키텍처이다.

주요 특징은 단방향 의존성인데,
각 계층은 자신보다 아래에 있는 계층에만 의존하고, 위 계층과는 의존성이 없다는 것이다.

위의 그림에서

  • Presentation 계층은 UI를 담당하며, 사용자와 입출력 등 상호작용을 담당한다.
  • Business 계층은 주문 처리와 같은 핵심 비즈니스 로직을 처리한다.
  • Persistence 계층은 DB와의 연결을 담당한다.
  • Database 계층은 실제 DB

이를 코드 예시로 한번 더 살펴보면

/* Kotlin으로 작성되었습니다. */

// 추상화 적용 - 인터페이스를 통해 데이터 소스 분리
interface UserRepository {
    fun getUser(userId: String): User
}

// 캡슐화 적용 - 내부 구현을 숨기고, 인터페이스를 통해 접근
class RemoteUserRepository : UserRepository {
    override fun getUser(userId: String): User {
        // DB에서 사용자 정보 가져오는 로직 생략
        ...
        return User(userId, "John Doe")
    }
}

// 비즈니스 로직 계층 (Use Case)
class UserService(private val userRepository: UserRepository) {
    fun getUserInfo(userId: String): User {
        return userRepository.getUser(userId) // 내부 구현을 알 필요 없음
    }
}

// 프레젠테이션 계층
class UserViewModel(private val userService: UserService) {
    fun fetchUser(userId: String) {
        val user = userService.getUserInfo(userId)
        println("User: ${user.name}")
    }
}

코드에선 Database Layer는 빠져있다. (보통 외부에 위치하기 때문에)

Repository는 나중에 다룰테지만, 일단 DB에서 데이터를 가져오고 비즈니스 로직에 제공하는 모듈이라고 이해하자.

  • UI 계층인 UserViewModel은 print를 통해 유저의 인터페이스에 출력하는 역할을 담당하며, 오직 UserService라는 비즈니스 로직을 처리하는 클래스에만 의존성을 가진다.

  • UserService는 비즈니스 로직 처리 계층으로, Persistence계층인 UserRepository만을 참조한다. 이때 UserRepository는 인터페이스로, 추상화 원칙을 적용하여 이를 사용하는 UserService에서는 구현부에 대한 내용을 모르도록 한다.

  • UserRepository는 DB에서 데이터를 가져와 가공하여 비즈니스 로직에 가져오는 역할을 수행한다. 여기선 인터페이스이고, 이를 구현하는 RemoteUserRepository 클래스를 통해 세부적인 로직을 구현한다.
    이는 역시 추상화를 통해 RemoteUserRepository 로직이 변경되어도 이를 실질적으로 사용하는 비즈니스 계층에 영향을 끼치지 않도록 하기 위함이다.


모놀리식 아키텍처(Monolithic)

애플리케이션을 하나의 단일 코드베이스와 실행 가능한 단위로 개발하는 방식
즉, 모든 기능(UI, 비즈니스 로직, 데이터 접근 등)이 하나의 프로젝트 안에서 동작

하나의 코드 덩어리로 개발하기 때문에

  • 배포가 쉬움
  • 구조가 단순
  • 빠른 개발과 디버깅

이라는 장점을 가지지만 그와 동시에

  • 규모가 커질수록 유지보수가 어려움
  • 배포 리스크 증가: 작은 기능 변경이 전체에도 큰 영향을 끼쳐 다시 빌드+배포해야함
  • 확장성 문제: 한 기능의 확장으로 전체 코드 변경해야하는 일이 발생
  • 테스트 불편함: 부분 테스트가 힘들고 테스트를 위해 전체 코드를 실행

따라서 일반적으로 규모가 작고 빠른 출시가 필요한 프로젝트, 비즈니스적인 성장 예정이 없는 프로젝트에서 사용된다.



사실 이외에도 마이크로서비스, 마이크로커널 등 여러 소프트웨어 아키텍처에 대해서도 정리했었으나 정확하진 않지만 보통 백엔드에서 더 많이 쓰이는 개념이라 해서 과감히 빼버렸다. 일단은 안드로이드에 필요한 지식을 위주로 쓰는게 목표기 때문에...ㅎ



참고


위 영상은 우아콘에서 아키텍처의 목적, 그리고 클린 아키텍처에 대해서도 설명한 영상인데 참고하면 좋을듯 싶다.

https://f-lab.kr/insight/software-design-architecture-difference-20240605

https://6mini.github.io/software%20architecture%20pattern/2022/11/07/architecture/

https://yozm.wishket.com/magazine/detail/2743/

https://yozm.wishket.com/magazine/detail/1813/

profile
그런게어딨어그냥하는거지

0개의 댓글