이 글을 잘 이해하기 위해, 아래 사항들에 대한 지식이 필요합니다.
- 상속과 조합
- 추상화
(+ Kotlin의 interface와 abstract class)
많이들 헷갈려하는 개념인 라이브러리와 프레임워크가 어떤 차이가 있는지 알아보도록 하겠습니다.
개발을 하다 보면 다양한 도구들을 사용하게 됩니다. 그리고 우리는 종종 그것을 라이브러리 혹은 프레임워크라고 부르곤 합니다. 사용하는 입장에서는 이 둘이 크게 다르다고 느껴지지 않을 수도 있지만, 이 두 개념은 명확히 다른 개념입니다.
Android
framework: A group of Java classes, interfaces, and other precompiled code upon which apps are built.
Koin : The pragmatic Kotlin & Kotlin Multiplatform Dependency Injectionframework
React : Thelibraryfor web and native user interfaces
이 글을 끝까지 읽는다면, 라이브러리와 프레임워크의 대표적인 차이로 언급되는 아래의 문장을 이해할 수 있을 것입니다.
라이브러리는 개발자가 제어 흐름을 주도하지만,프레임워크는 자체적으로 제어 흐름을 관리한다.
In computing, a
libraryis a collection of resources that is leveraged during software development to implement a computer program. Commonly, a library consists of executable code such as compiled functions and classes, ... (후략) ...-위키피디아, Library (computing)
라이브러리는 개발 중 가져다 쓸 수 있는 자원 일체를 가리킵니다.
특정 지식을 얻기 위하여 도서관에서 어떤 자료든 가져다 쓰는 모습에 비유할 수 있습니다.
'가져다 쓸' 수 있으면 모두 라이브러리가 될 수 있기 때문에, 우리가 생각하는 것보다 많은 것들을 라이브러리로 부를 수 있습니다.
예를 들어 이미 우리에게 익숙한 함수나 클래스 같은 코드뿐만 아니라, 이미지나 텍스트 같은 데이터 역시 라이브러리의 구성원이 될 수 있습니다.
이 글에서는 '개발자들이 소프트웨어를 만들 때 사용할 수 있는 미리 작성된 코드의 모음'이라는 좁은 의미를 바탕으로 설명을 이어가보겠습니다.
'가져다 쓰인다'는 특성을 생각해보면, 높은 자유도를 라이브러리의 특징으로 생각해볼 수 있습니다. 개발자는 큰 제약 없이 자신의 필요에 맞게 라이브러리들을 선택적으로 사용할 수 있습니다.
이 때 얻을 수 있는 장점은 당연히 개발 과정의 효율성 증가인데, 특히 코드의 재사용성을 높이고 모듈화를 달성할 수 있습니다. 라이브러리를 사용해본 경험이 있다면, 직접 세부 로직을 작성할 필요 없이 라이브러리의 사용 방법만 숙지하고 비교적 쉽게 목적을 달성할 수 있었을 것입니다.
라이브러리 활용 예시 중 하나로, Kotlin의 컬렉션 라이브러리가 있습니다. 아래 코드에서는 특정 리스트에 대해 원하는 시점에 필터링을 해주는 작업만 호출하는 방식으로 컬렉션 라이브러리를 활용하고 있습니다. 즉 라이브러리는 특정 기능을 제공하는 도구로서 개발자가 원하는 대로 호출하여 사용할 수 있는 수단인 것입니다.
fun main() {
val numbers = listOf(1, 2, 3, 4, 5)
// 라이브러리 활용
val evenNumbers = numbers.filter { it % 2 == 0 }
println(evenNumbers) // [2, 4]
}
🤷 의문
지금까지 봤을 때, 라이브러리는 분명 편리한 수단입니다.
그럼 개발할 때 과연 라이브러리만으로 충분할까요?
It provides a
standardway to build and deploy applications and is a universal, reusable software environment ... (중략)...Software frameworks may include ... (중략) ... code libraries, ... (중략) ... that bring together all the different components to enable development of a project or system.
-위키피디아, Software framework
프레임워크는 라이브러리 혹은 클래스의 모임으로, 재사용 가능한 소프트웨어 시스템을 가리킵니다.
프레임워크에서 나오는 Frame 이라는 단어의 의미를 생각해보면, 애플리케이션의 전체적인 뼈대를 제공하는 것 같다는 느낌이 듭니다. 실제로 프레임워크는 소프트웨어의 기본적인 구조를 정의하여, 개발자가 그 구조 안에서 특정 기능을 구현할 수 있도록 도와주는 역할을 합니다. 즉 애플리케이션을 만드는 표준적인 방법을 제공해주는 소프트웨어 시스템이라 볼 수 있습니다.
프레임워크 역시 소프트웨어이기 때문에, 같은 목적을 달성할 수 있다면 이미 만들어진 코드를 재사용할 수 있는 방법을 사용하면 좋을 것입니다. 따라서 프레임워크를 제작하는 과정에서 라이브러리가 사용될 수도 있습니다.
아래와 같이 라이브러리와 프레임워크 간의 관계를 표현해볼 수 있습니다.

소프트웨어를 만들 수 있도록 하는 뼈대인 만큼, 프레임워크를 활용하여 개발을 할 때는 해당 프레임워크의 규칙을 따라야 합니다.
'로마에 가면 로마법을 따르라'는 표현이 떠오릅니다.
소프트웨어 개발을 건축에 빗대어보면, 건물의 뼈대를 프레임워크에 비유할 수 있습니다.
(더 넓게 생각해보면, 건축 설계도 등 건축의 틀을 잡아주는 다른 요소들 역시 프레임워크에 속할 것입니다.)
건축:뼈대=소프트웨어 개발:프레임워크
프레임워크를 사용한다면, 개발자는 프레임워크라는 뼈대를 따라 시스템을 구축해야 합니다. 만약 프레임워크가 정한 구조를 무시하고 임의로 변경하면, 프로그램이 정상적으로 동작하지 않을 것입니다.
그러나 프레임워크가 제공하는 구조를 따른다고 해서 모든 소프트웨어가 동일한 모습이 되는 것은 아닙니다. 프레임워크가 기본적으로는 공통적인 아키텍처 대로 구현하도록 유도하지만, 프로젝트의 목적과 요구 사항에 따라 다양한 기능을 추가할 수 있습니다. 같은 프레임워크를 사용하더라도 전자상거래 플랫폼을 만들 수도, 전혀 다른 성격의 레시피 앱을 만들 수도 있을 것입니다.
프레임워크를 활용하면 안정적인 틀을 바탕으로 효율적으로 개발을 진행하면서도, 유연하게 원하는 기능을 구현할 수 있습니다. 기본적인 역할을 생각해봤을 때, 프레임워크는 다소 경직되어있는 툴이라는 생각이 들 수 있지만 사실 그렇지만은 않다는 것입니다.
프레임워크가 제공하는 기본적인 틀을 따르면서도, 조합이나 상속 등의 방식을 통해 필요에 따라 확장할 수 있습니다. Kotlin에서는 이것을 달성하기 위해 프레임워크 제작 과정에서 abstract class나 interface를 사용할 수 있을 것입니다. 이를 통해 프로젝트의 목적과 요구 사항에 맞는 기능을 추가하고, 유연하게 커스터마이징된 애플리케이션을 개발하도록 유도할 수 있습니다.
아래 코드는 알림 기능을 구현하기 위한 간단한 프레임워크 예시입니다. 조합과 상속을 활용해 기존 프레임워크(BasicNotifier)의 기능을 확장하여 새로운 기능을 추가한 모습(NotifierDecorator, EmailNotifier)을 확인할 수 있습니다.
interface Notifier {
fun send(message: String)
}
class BasicNotifier : Notifier {
override fun send(message: String) {
println("기본 알림: $message")
}
}
abstract class NotifierDecorator(private val notifier: Notifier) : Notifier {
override fun send(message: String) {
notifier.send(message) // 기존 기능을 수행
}
}
class EmailNotifier(notifier: Notifier) : NotifierDecorator(notifier) {
override fun send(message: String) {
super.send(message)
println("📧 이메일 전송: $message")
}
}
fun main() {
// 기본 알림만 사용하는 경우
val basicNotifier = BasicNotifier()
// "Hello, World!"만 출력
basicNotifier.send("Hello, World!")
val emailNotifier = EmailNotifier(basicNotifier)
// "Hello World!", "📧 이메일 전송: Hello World!" 출력
emailNotifier.send("Hello, World!")
}
프레임워크는 단순히 정해진 규칙을 강제만 하는 도구가 아니고,
효율성과확장성을 동시에 제공하는 수단이다!
우리가 평소에 하는 안드로이드 개발 역시 프레임워크를 활용하여 개발을 하는 것입니다.

앞서 프레임워크는 내부적으로 라이브러리 코드를 활용할 수 있다고 설명하였습니다. 실제로도 안드로이드 프레임워크 아키텍처의 Libraries 계층을 살펴보면, 우리에게 익숙한 SQLite를 비롯한 여러 라이브러리가 사용되는 것을 확인할 수 있습니다.
한편 프로그램의 원활한 실행을 위해서는 개발 과정에서 수많은 규칙을 따라야만 합니다. 예를 들어, 안드로이드 개발에서 액티비티를 만들기 위해서는 AppCompatActivity라는 클래스를 상속받아야 하고, 앱의 정보와 컴포넌트들을 명시하기 위해 AndroidManifest.xml이라는 파일을 활용해야 한다는 규칙들이 있습니다.
이러한 규칙을 따르지 않으면 앱이 제대로 동작하지 않거나, 앱의 구성 요소들이 서로 연동되지 않아 예기치 못한 오류가 발생할 수 있습니다.
예를 들어, 액티비티 클래스가 AppCompatActivity를 상속받지 않거나 AndroidManifest.xml에서 액티비티를 등록하지 않는다면 액티비티를 원활하게 사용할 수 없습니다. 앞서 살펴보았던 프레임워크가 제공하는 규칙을 무시하면 소프트웨어가 예상대로 작동하지 않는다는 원칙에 대응되는 부분입니다.
또한 우리는 안드로이드 프레임워크를 활용하여 애플리케이션의 성격에 따라 세부 기능을 추가하고 확장할 수도 있습니다. 하나의 액티비티 클래스를 만들 때 필요한 클래스를 상속받고 Manifest에 등록하는 과정뿐만 아니라, 원하는 기능을 구현하기 위한 확장을 추가적으로 진행합니다.
아래와 같이 우리가 일상적으로 작성하는 코드 역시 프레임워크의 규칙을 따르면서도 조합 혹은 상속을 통해 컴포넌트를 확장하는 좋은 예시가 될 수 있습니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 버튼 객체 생성
val button = findViewById<Button>(R.id.btn_default)
button.setOnClickListener {
Toast.makeText(this, "버튼이 클릭되었습니다!", Toast.LENGTH_SHORT).show()
}
}
}
개념 상으로 보았을 때는 라이브러리와 프레임워크는 모두 개발자의 개발을 편리하게 해준다는 점을 알 수 있습니다. 하지만 편리하게 해주는 방법 측면에서는 그 결이 다릅니다. 코드와 함께 두 개념의 차이점을 살펴보도록 하겠습니다.
두 개념의 차이점을 알기 위해서는 제어 흐름의 이해가 필요합니다.
Control flow is the order in which individual statements, instructions or function calls of an imperative program are executed or evaluated.
-위키피디아, Control flow
제어 흐름은 쉽게 말해 코드가 실행되는 순서이며, 여기서 코드는 개별적인 구문(statement)을 가리킵니다. 제어 흐름은 흔히 함수 호출, 조건문, 반복문 등을 활용해 결정할 수 있습니다.
앞에서 간단히 살펴보았듯, 라이브러리와 프레임워크는 우리가 작성하는 코드의 제어 흐름 보유 주체에 차이를 보입니다. 이것이 두 개념의 가장 큰 차이입니다.
라이브러리 : 라이브러리를 사용하는 개발자가 보유프레임워크 : 프레임워크가 보유제어 흐름이라는 용어가 생소한데, 같은 기능을 수행하는 코드를 작성해보며 두 요소의 차이를 직접 느껴보도록 하겠습니다.
우선 라이브러리를 만들어보겠습니다. 라이브러리의 정의에 따라, 개발자가 원할 때 원하는 기능을 언제든 가져다 쓸 수 있도록 하는 방향으로 구현해보도록 하겠습니다.
class LoggerLibrary {
// 로그 출력 함수
fun log(message: String, level: LogLevel = LogLevel.INFO) {
val timestamp = LocalDateTime.now()
val logMessage = "[$timestamp] [${level.name}] $message"
// 콘솔에 로그 출력
println(logMessage)
}
}
enum class LogLevel {
INFO, WARNING, ERROR
}
LoggerLibrary는 출력하고자 하는 메시지를 log 함수 측으로부터 전달 받아, 로그의 레벨과 시간과 함께 콘솔에 출력하는 역할을 수행합니다.
이제 이 기능을 직접 가져다 써보도록 하겠습니다.
fun main() {
val logger = LoggerLibrary()
logger.log("This is an information message.")
logger.log("This is a warning message.", LogLevel.WARNING)
logger.log("This is an error message.", LogLevel.ERROR)
}
기능을 사용하는 우리가 직접 라이브러리의 함수를 호출 함으로써, 원하는 메시지를 콘솔에 출력할 수 있습니다.
앞서 프레임워크 측에서 라이브러리를 활용할 수 있다고 설명하였습니다. 프레임워크를 구현하며, 이미 만들어져 있는 LoggerLibrary를 가져다 써보도록 하겠습니다.
프레임워크의 특성을 반영하여, 로깅 프로그램의 전체적인 구조와 사용 방법을 정의했습니다. 프레임워크 차원에서 로깅의 기본 틀을 제공하면서도, 필요에 따라 기능을 확장할 수 있는 유연성을 갖추도록 하기 위해 추상 클래스를 활용하였습니다. 이를 통해 개발자는 특정 요구사항에 맞춰 로깅 기능을 확장할 수 있게 되었습니다.
abstract class LoggerFramework {
private val logger = LoggerLibrary()
abstract fun run()
protected fun log(message: String) {
// 상황에 맞는 로그 레벨을 자동으로 결정
val effectiveLevel = determineLogLevel(message)
logger.log(message, effectiveLevel)
}
private fun determineLogLevel(message: String): LogLevel {
return when {
message.contains("error", true) -> LogLevel.ERROR
message.contains("warning", true) -> LogLevel.WARNING
else -> LogLevel.INFO
}
}
fun start() {
log("Application started")
run() // ***주목!***
log("Application finished")
}
}
함수 호출에 별다른 제약이 없는 라이브러리와는 달리, 프레임워크에는 규칙이 존재함을 알 수 있습니다.
로깅 애플리케이션을 구현하기 위해 LoggerFramework를 상속받은 객체를 선언해야 한다.
원하는 메시지를 로깅하기 위해서는 run()을 구현해야 한다.
애플리케이션을 동작시키기 위해 start()를 호출해야 한다.
start() 구현부에서, 개발자가 구현한 run() 을 동작시킨다!이제 이러한 규칙에 따라, 애플리케이션을 만들어 실행시키는 로직을 다음과 같이 작성해볼 수 있습니다.
class MyApplication : LoggerFramework() {
override fun run() {
log("This is an error message")
// ... (애플리케이션 로직 확장 가능) ...
log("This is an informational message")
}
}
fun main() {
val app = MyApplication()
app.start()
}
LoggerFramework는 LoggerLibrary와는 달리 로깅 애플리케이션의 시작부터 종료까지의 전반적인 흐름을 관리하고 있음을 확인할 수 있습니다.
즉 제어 흐름의 측면에서 두 객체는 큰 차이를 보입니다.
LoggerLibrary 코드를 다시 살펴보겠습니다.
class LoggerLibrary {
fun log(message: String, level: LogLevel = LogLevel.INFO) {
val timestamp = LocalDateTime.now()
val logMessage = "[$timestamp] [${level.name}] $message"
println(logMessage)
}
}
클래스 내부에 개발자가 작성하는 코드를 실행하는 로직(매개변수로 주어지는 콜백이나, 추상 함수 등)은 전혀 존재하지 않고, 독립적으로 특정 기능을 실행하는 로직만이 존재합니다. 즉 단순히 로깅을 위한 함수만 제공할 뿐, 개발자가 작성한 코드의 제어 흐름을 관리하는 코드는 전혀 존재하지 않습니다.
fun main() {
val logger = LoggerLibrary()
logger.log("This is an information message.")
logger.log("This is a warning message.", LogLevel.WARNING)
logger.log("This is an error message.", LogLevel.ERROR)
}

라이브러리의 log()를 언제, 어디서, 어떤 방식으로 호출할지를 전적으로 개발자가 결정하고 있습니다. 라이브러리 기능 호출 시, 개발자가 직접 자신이 작성한 코드의 실행 흐름을 관리하고 있던 것입니다. 이것이 바로 라이브러리를 사용할 때의 제어 흐름입니다.
반면 LoggerFramework를 사용할 때의 모습을 살펴보면 라이브러리와는 사뭇 다름을 알 수 있습니다.
start() 메서드의 구현을 보면, 애플리케이션의 전체적인 실행 흐름을 관리하는 느낌이 듭니다. 단순히 개발자가 작성한 run() 로직을 실행할 뿐만 아니라, 프로그램의 진행을 이끌어주는 추가적인 작업을 함께 수행해주고 있습니다.
LoggerFramework 기준으로는 애플리케이션의 시작과 끝을 알리는 로그를 자동으로 출력하는 기능이 추가되어 있습니다. 개발자가 원하는 로직(run)을 작성하면, 프레임워크가 실행 흐름을 관리하면서 적절한 시점에 그 로직을 실행하고 필요한 부가 작업도 함께 처리해 줍니다.

개발자가 직접 실행 흐름을 관리하는 것이 아니라, 프레임워크 측에서 개발자가 작성한 코드의 실행 순서를 결정하여 적절한 시점에 호출하는 구조인 것입니다. 프레임워크는 이러한 방식으로 개발자가 작성한 로직을 적재적소에 스스로 활용하여 애플리케이션의 실행 흐름을 직접 관리하고 있음을 알 수 있습니다.
Inversion of control (IoC) is a design principle in which custom-written portions of a
computer programreceive the flow of control from an external source (e.g. a framework).
-위키피디아, Inversion of control
앞서 살펴본 프레임워크의 이러한 동작 방식을 제어의 역전(Inversion of Control, IoC)이라 부릅니다.

"Don't call us, we'll call you."
프레임워크를 나타내는 적절한 표현으로 사용되곤 합니다.
이제 초반에 나왔던 의문의 답을 얻을 수 있을 것입니다.
개발할 때 과연 라이브러리만으로 충분할까요?
👉 더 자유롭고 유연한 사용이 필요하다면, 라이브러리가 더 좋을 수도?
프레임워크와 달리, 라이브러리는 자유로운 사용이 가능하여 특정 기능만 필요할 때나 기존 시스템과의 통합 시 유리하게 활용할 수 있습니다. 라이브러리를 활용하면 정형화된 규칙 없이 필요한 기능만 간편하게 추가할 수 있으며, 자유롭게 코드를 조작할 수 있기 때문입니다. 앞에서 살펴보았던 예시를 다시 생각해보면, 사실 로깅이라는 비교적 간단한 기능 정도는 라이브러리로 충분할 것입니다.
👉 라이브러리가 정답이 아닐 수 있다!
가장 간단한 예시로, 여러 라이브러리를 조합하는 상황을 들 수 있습니다. 개발자가 직접 여러 라이브러리를 호출하는 방식으로 구현하면, 라이브러리 간 호환성 혹은 제어 흐름의 일관성을 유지하는 것이 어려워질 수 있습니다. 이런 상황에서는, 정형화된 프레임워크를 사용하는 것이 더 효율적일 수 있습니다. 프레임워크에 이미 구조와 규칙이 정의되어 있고, 제어 흐름을 개발자 대신 일관되게 관리해주기 때문입니다. 따라서 개발자는 복잡한 흐름을 걱정할 필요 없이 비즈니스 로직에 집중할 수 있습니다.
👉 당연히 판단은 개인의 몫!
더 이상의 자세한 설명은 생략합니다.
라이브러리는 개발자가 제어 흐름을 주도하지만,프레임워크는 자체적으로 제어 흐름을 관리한다.
라이브러리 : 개발자가 필요에 따라 직접 라이브러리의 코드를 호출 (함수 등)
프레임워크 : 프레임워크가 개발자의 코드를 호출 👉 제어의 역전(IoC)
Library - Wikipedia
Software framework - Wikipedia
Control Flow - Wikipedia
Inversion of control - Wikipedia
The difference between libraries and frameworks
라이브러리? 프레임워크? 차이점 아직도 모름? 5분 순삭.
km 잘 읽고 갑니다!!
저도 한 번 정리가 필요할 것 같네요