본문 바로가기
공부/안드로이드

안드로이드 앱이 만들어지는 과정

by 화릿불 2025. 8. 21.

 

오늘은 안드로이드 앱이 만들어지는 과정에 대해 공부하고자 한다.

먼저 앱 개발에 있어 기본이 되는 OOP(Object Oriented Programming)빌드의 개념과 과정, 시스템까지 이해해 볼 것이다.

 

앱 설계하기, OOP란?

앱 개발에 앞서 가장 중요한 것은 앱을 설계하는 것이다. 물론 어떤 서비스를 만들지 어떤 기능을 제공할지 정하는 것도 설계의 일부겠지만 프로그래밍 관점에서의 설계 또한 매우 중요하다. 이에 자주 언급되는 것은 바로 OOP이다.

Object Oriented Programming 이름에서 보이는 것처럼 객체들의 상호작용으로 구조화하여 유연하고 유지보수가 용이하게 만드는 프로그래밍 방식을 말하며 주로 Java나 Kotlin 같은 객체 지향 언어를 사용해 구현한다. 그래서 사실 OOP는 객체 지향 언어를 다뤄봤다면 한 번쯤 들어봤을 듯하다.

특히 안드로이드 앱 개발에서는 UI 요소나 데이터 관리, 네트워크 통신 등 다양한 부분에서 활용된다.

추후에 공부하겠지만 안드로이드의 구성 요소인 액티비티(Activity)부터 그 외 요소들까지 대부분이 하나의 객체로 관리되고, UI 요소인 버튼이나 뷰 또한 독립적인 객체로 구현이 되어있다. 따라서 이들이 효과적으로 구성되고 연결되기 위해서는 객체 지향 방식으로 OOP는 필수인 셈이다.

 

OOP의 주요 개념

1. 클래스(Class)

클래스는 객체를 만들기 위한 설계도라 할 수 있다. 객체가 가질 수 있는 속성(데이터)동작(메서드)이 정의된다.

open class Person(val name: String, var age: Int = 0) {
    fun introduce() {
        println("안녕하세요, 저는 $name이고 $age살입니다.")
    }
    
    fun haveBirthday() {
        age++
        println("$name의 새로운 나이는 $age살입니다.")
    }
}

 

 

2. 객체(Object)

객체는 하나의 인스턴스(Instance)로 클래스에서 정의된 속성과 메서드를 사용할 수 있도록 실제로 만들어진 실체 그 자체이다. 클래스가 추상적인 개념이지만 객체는 실제 메모리에 할당된다.

fun main() {
    val person1 = Person("상빈", 27)

    person1.introduce()
    person1.haveBirthday()
}

클래스에 이어 객체까지 살펴보았을 때 살짝 어색함을 느꼈을 수도 있다.

기존 Java나 C++에서 매개변수가 필요한 클래스를 다룰 때는 개발자가 생성자에서 데이터들을 직접 할당하는 과정이 필요했다. 하지만 코틀린에서는 이 과정을 주 생성자(Primary Constructor)를 통해 간결하게 처리가 가능하며 기본값을 지정해 해당 매개변수를 선택값으로 만들 수도 있다.

따라서 위 코드는 처음 객체를 생성할 당시 age가 27로 저장되고 haveBirthday를 호출하면 저장된 age의 값을 1 늘리고 출력하게 된다.

 

3. 상속(Inheritance)

상속은 하나의 클래스가 다른 클래스의 속성과 메서드를 물려받아 사용하는 개념이다.

이를 통해 공통된 기능은 상위 클래스에서, 특화된 기능은 하위 클래스에서 사용하도록 하여 재사용성과 확장성을 갖출 수 있다.

class Student(name: String, age: Int = 0, val school: String) : Person(name, age) {
    fun study() {
        println("$name 은(는) $school 에서 공부합니다.")
    }
}

class Worker(name: String, age: Int = 0, val company: String) : Person(name, age) {
    fun work() {
        println("$name 은(는) $company 에서 일을 합니다.")
    }
}

Student 클래스와 Worker 클래스를 상속을 통해서 작성했다.

모두 부모 클래스인 Person의 데이터와 메서드는 동일하게 이용가능하며 각자 새로운 데이터와 메서드를 추가로 정의할 수 있다.

한 가지 주의할 점은 Kotlin의 클래스와 메서드는 기본적으로 final(상속 및 오버라이딩 불가)이다. 따라서 상속을 위해서는 반드시 open 키워드를 사용해야 한다.

안드로이드에서 모든 View 컴포넌트는 View 클래스를 상속받는 대표적인 예시다.

예를 들어 Button이나 TextView는 View의 기본적인 속성인 크기나 위치 등을 물려받고, 각자에게 특화된 텍스트 기능이나 클릭 이벤트 처리를 추가로 구현할 수 있다. 더 자세한 내용은 Android 공식 문서를 참고하자.

 

이렇게 상속은 강력하지만, 클래스 간의 결합도를 높여 유연성을 해친다.

그래서 최근 Kotlin에서는 상속보다 필요한 기능을 객체로 만들어 포함시키는 "컴포지션" 방식이 더 권장된다.

아래 코드는 상속이 아닌 컴포지션 방식의 예시이다.

class Student(name: String, age: Int = 0, val school: String) {
    private val person = Person(name, age)

    fun study() {
        println("${person.name} 은(는) $school 에서 공부합니다.")
    }

    fun introduce() {
        person.introduce()
    }
}

이처럼 Student는 Person 객체를 내부에 프로퍼티로 소유한다. name 같은 데이터가 필요하면 내부의 person 객체를 통해 접근하고, Person의 메서드가 필요할 땐 해당 메서드를 대신 호출해 주는 위임(delegation) 메서드를 만들어 사용한다.

 

4. 캡슐화(Encapsulation)

캡슐화는 데이터와 동작을 하나로 묶고, 외부에서 접근을 제한하는 개념이다.

이를 통해 데이터의 무결성을 유지할 수 있다.

class Person(private val name: String, age: Int = 0) {
    var age: Int = age
            set(value) {
                if (value >= field) {
                    field = value
                } else {
                    println("나이는 줄일 수 없습니다.")
                }
            }
}

 

위처럼 private이나 protected 같은 접근 제어자를 통해 캡슐화를 구현하며 Java에서는 보통 get, set 메서드를 명시적으로 만들었지만, 코틀린에서는 그렇지 않다.

또한, 위 코드는 age의 setter를 재정의한 모습을 보여주고 있다. 기본적으로 Kotlin에서는 get이나 set을 따로 정의하지 않아도 사용이 가능한데, 특정 데이터에 대해서 특별한 로직을 만들고 싶을 때 getter나 setter를 직접 구현해 외부에서의 직접적인 접근 및 데이터 변경을 방지할 수 있다.

 

5. 추상화(Abstraction)

추상화는 구체적인 구현, 세부 사항을 숨기고 핵심적인 내용만 정의하고 보여주는 개념이다.

복잡성은 줄이고 사용자에게 필요한 핵심만 제공할 수 있고 안드로이드에서 인터페이스나 추상 클래스를 통해 구현된다.

추상 클래스는 A는 B이다 관계처럼 공통된 상태나 구현이 일부 있을 때 사용하고, 인터페이스는 A는 B를 할 수 있다처럼 특정 메서드나 기능을 약속할 때 사용한다.

// 추상 클래스 정의
abstract class Person(val name: String, var age: Int = 0) {
    abstract fun work()

    fun introduce() {
        println("안녕하세요, 저는 $name이고 $age살입니다.")
    }
}

class Developer(name: String, age: Int = 0) : Person(name, age) {
    override fun work() {
        println("$name은 코드를 작성합니다.")
    }
}

class Designer(name: String, age: Int = 0) : Person(name, age) {
    override fun work() {
        println("$name은 디자인 작업을 합니다.")
    }
}

추상 클래스를 통해 모든 Person은 공통된 기능으로 work를 가지고 관리되지만, 구체적인 구현이나 동작 방식은 자식 클래스가 결정하도록 구성되어 있다.

안드로이드의 OnclickListener는 인터페이스의 적절한 예시다. 개발자는 해당 메서드 안에서 무엇을 할지 정의하기만 하면 된다.

 

더 자세한 내용은 Android 공식 문서 를 참고하자.

 

6. 다형성(Polymorphism)

다형성은 하나의 객체가 여러 타입(형태)을 가지는 개념이다.

상위 클래스 타입으로 서로 다른 타입의 하위 클래스 객체를 참조하거나 같은 메서드 호출이라도 객체 타입에 따라 다르게 동작하도록 구성할 수 있다.

fun main() {
    val people: List<Person> = listOf(
        Developer("철수", 25),
        Designer("영희", 28)
    )

    for (person in people) {
        person.introduce()
        person.work()
    }
}

추상화 클래스에 이어서 위와 같은 코드가 다형성을 나타낸다고 할 수 있다.

people 변수를 보면 Developer 클래스와 Designer 클래스가 하나의 리스트에 포함된 모습이며 아래 반복문을 통해 동일한 동작을 실행하는 것처럼 보이지만 실제 객체에 맞는 work가 실행된다.

즉, 다형성은 하나의 인터페이스나 부모 클래스를 통해 서로 다른 자식 클래스들을 일관되게 다루는 능력이다.

 

 

이렇게 OOP를 활용해 앱을 설계하고 구현했다면 이다음은 빌드가 필요하다.

 

안드로이드 빌드 및 빌드 시스템 (Gradle)

프로그램을 개발하면서 빌드에 대해 들어본 적이 있을 것이다.

기본적으로 빌드란 작성된 프로그램의 소스 코드를 사용자에게 배포할 수 있는 독립적인 실행 가능한 결과물로 바꾸는 과정을 말한다. 즉, 안드로이드에서 빌드는 앱 리소스 및 소스 코드를 컴파일하고 Android Package(APK)Android App Bundle(AAB) 패키징 하는 과정을 말하는 것이다.

 

빌드를 통해 만들 수 있는 결과물에는 사용자가 바로 사용 가능한 APK, AAB가 있고 다른 프로젝트에서 사용할 수 있도록 특정 기능만 담겨있는 Android Archive(AAR), Java Archive(JAR) 라이브러리가 있다는 정도만 알아두자.

 

이렇게 안드로이드 앱을 만드는 과정은 단순히 코드만 작성한다고 끝이 아니다. 작성한 코드와 리소스들을 실제 기기에서 실행 가능한 파일로 변환하고 패키징 하는 작업이 반드시 필요하다. 이 복잡한 과정을 자동화해 주는 시스템이 바로 빌드 시스템이다.

 

빌드 도구 : Gradle

안드로이드의 공식 빌드 시스템은 Gradle 이다. 빌드에 필요한 컴파일, 테스트, 최종 결과물 생성까지 모든 단계를 자동화하는 도구이다. 하지만, Gradle 자체는 앱을 만드는 구체적인 방법까지는 모르기에 이때 필요한 것이 안드로이드 Gradle 플로그인(AGP)이다.

개발자들은 build.gradle.kts 라는 설정 파일을 통해 gradle과 AGP에게 빌드에 필요한 라이브러리, 앱 버전 등의 내용을 전달하게 된다.

 

 

빌드 과정

개발 과정에서 Android Studio를 통해 실행하면 Gradle은 다음의 과정을 거쳐 앱을 빌드한다.

 

1. 빌드 준비

빌드를 위한 사전 준비 단계이다. build.gradle.kts 파일을 수정할 때마다 실행되며 dependencies에 적힌 외부 라이브러리들을 다운로드하고 빌드 시스템이 프로젝트 구조를 이해할 수 있게 준비한다.

 

2. 소스코드 컴파일

작성한 코틀린 및 자바 소스 코드를 자바 컴파일러를 통해 자바 바이트코드로 변환한다.

이 시점에서 Annotation Processor(주석 처리기)가 동작한다. Room이나 Hilt 같은 라이브러리들에 사용된 어노테이션을 보고 필요한 코드들이 이 시점에 자동 생성된다.

 

3. 리소스 컴파일

Android Asset Packing Tool(AAPT)이 res 폴더의 모든 리소스(레이아웃, 이미지)와 AndroidManifest 파일을 병합 처리한다.

이때 소스 코드에서 각 리소스를 쉽게 참조할 수 있도록 고유 ID를 담은 R.java 파일이 생성된다.

 

4. DEX 코드 변환

가장 핵심 단계라고 할 수 있다.

컴파일된 자바 바이트코드는 안드로이드 런타임에서 직접 실행할 수 없다. D8 컴파일러가 모든 자바 바이트코드를 안드로이드 런타임(ART)이 이해할 수 있는 DEX(Dalvik Executable) 바이트코드로 변환한다. 이때 코드 최적화도 이루어진다.

DEX 코드는 ART에서 실행된다. 과거에 Dalvik이라는 런타임을 사용했는데 앱 실행 시점에 코드를 기계어로 번역하는 JIT(Just In Time) 방식을 사용했다. 현재의 ART는 앱 설치 시점에 미리 기계어로 번역해두는 AOT(Ahead Of Time) 방식이기 때문에 앱 실행속도가 더 빠른 편이다.

최근에는 JIT와 AOT 방식의 장점을 결합해 설치 시점에는 JIT 실행 시점에는 AOT 방식을 활용하는 하이브리드 방식도 존재한다.

 

5. 패키징 및 서명

마지막으로 컴파일된 소스, DEX 파일, AndroidManfiest.xml 등 모든 결과물을 하나의 파일인 APK나 AAB로 압축한다. 그리고 앱이 신뢰할 수 있으며 위변조되지 않았음을 증명하기 위한 디지털 서명도 추가한다. 이 서명이 없다면 설치나 배포가 불가능하다.

현재로서는 AAB가 필수가 되었는데, APK는 모든 기기용 리소스와 코드가 담겨있지만 AAB는 Google Play가 사용자의 기기 사양(CPU, 화면 밀도 등)에 최적화된 APK를 동적으로 생성해 전달해 준다. 따라서 불필요한 리소스를 제외할 수 있기 때문에 AAB는 필수가 되었다.

 

6. 최적화(선택)

주로 배포용 빌드에만 적용되는 과정이다.

R8같은 도구를 사용해 최종 APK/AAB의 품질을 높이고 크기를 줄이는 데 크게 세 가지 역할을 한다.

코드 축소 : 사용되지 않는 클래스나 메서드를 제거해 용량을 줄인다.

난독화 : 리버스 엔지니어링을 어렵게 하기 위해 클래스나 메서드, 변수명 등을 의미 없게 변경한다.

최적화 : 코드를 분석해 더 효율적인 바이트코드로 작성한다.

 

더 자세한 내용은 Android 공식 문서를 참고하자.

 

 

'공부 > 안드로이드' 카테고리의 다른 글

안드로이드의 기본 요소  (0) 2025.08.27
안드로이드의 언어  (5) 2025.08.20
안드로이드 개발 공부  (0) 2025.08.19