AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까
버즈빌은 2023년 한 해 동안 월간 약 1.2억, 연 기준으로 14억에 달하는 AWS 비용을 절약하였습니다. 그 경험과 팁을 여러 차례에 걸쳐 공유합니다. AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까 (준비중) …
Read Article안녕하세요. 버즈빌 신입 개발자 Damon입니다. 해당 게시글에서는 버즈빌의 신입 개발자가 마주한 개발 설계 과정에서의 문제점을 설명합니다. 그리고, 개발자 JD가 저를 가이드하여 설계가 개선된 경험을 공유하고자 합니다.
Robert C. Martin의 Clean Architecture에서 “좋은 설계란 시스템을 개발하고 유지 보수하는 데 필요한 인력, 비용, 시간을 줄이는 것”이라고 정의했습니다. 시스템은 개발하는 과정도 중요하지만 개발 이후 유지 보수의 과정도 매우 중요합니다. 소프트웨어에 대한 요구사항은 계속해서 변경되고, 새로운 기능이 추가되기도 합니다.
위 그래프를 보면 소프트웨어의 유지보수 비용은 소프트웨어의 개발 비용과 비슷하거나 그 이상입니다. 그만큼 소프트웨어의 유지 보수는 중요합니다. ‘Clean Architecture’, ‘DDD’, ‘SOLID’ 등과 같은 설계 개념은 좋은 설계를 통해 개발 및 유지 보수 과정에서 소요되는 물리적인 비용을 줄일 수 있습니다.
SOLID 원칙은 좋은 설계를 돕는 대표적인 원칙 중 하나입니다. 그중 SRP, OCP, DIP를 예시로 들어 어떻게 좋은 디자인을 할 수 있는지 소개합니다.
SRP, OCP, DIP는 다음과 같습니다.
저는 Unit/UI 테스팅 라이브러리를 개발하고 있었습니다. 당시 다음과 같은 소스 코드를 작성했습니다.
TestRunner
클래스는 테스트에 필요한 유틸리티 클래스를 주입받는다.RUN_TEST
는 인자로 전달받은 테스트 블록(block)을 수행하는 함수이다.TestUtil
클래스는 RUN_TEST
에서 테스트 시작 전과 테스트 종료 후에 수행된다.TestUtils.kt
interface TestUtils {
fun runBeforeTest()
fun runAfterTest()
}
TestRunner.kt
class TestRunner(
private val testUtils : List<TestUtils>
) {
fun RUN_TEST(block : () -> Unit) {
testUtils.forEach(TestUtil::runBeforeTest)
//RUN TEST
testUtils.forEach(TestUtil::runAfterTest)
}
}
ExecutionTimeSystemLogger.kt
class ExecutionTimeSystemLogger() : TestUtil {
private var startTime : Long = 0
override fun runBeforeTest() {
startTime = System.nanoTime()
}
override fun runAfterTest() {
val endTime = System.nanoTime()
val executionTIme = endTime - startTime
println("TIME: $executionTime")
}
}
해당 설계는 클라이언트의 요구사항을 완벽하게 반영하고 있습니다. 하지만, 개발자 JD는 저의 코드를 보고 다음과 같은 리뷰를 남깁니다.
ExecutionTimeSystemLogger
라는 구상 클래스에 의존한다.위와 같은 문제는 소프트웨어의 변화를 매우 어렵게 만듭니다. 예를 들어, 위 코드가 실제 코드에 적용된 후, 클라이언트가 시스템 로그가 아닌 파일을 통해 로그를 남기고 싶다고 요청하는 경우에 어떻게 될까요? 아마 저는 ExecutionTimeFileLogger
를 만들게 될 것입니다. 이 클래스는 기존의 ExecutionTimeSystemLogger
의 많은 내용이 중복됩니다. 또한, 시간을 계산하거나, 출력하는 포맷이 변경된다면 두 클래스 모두를 수정해야 될 것입니다. 만약, 시스템, 파일 외 다른 형태로 출력하는 기능이 계속해서 추가된다면 이런 시스템을 유지 보수하는 것은 매우 어려워질 것입니다.
이러한 상황을 대응하고 유연하게 소프트웨어를 설계하는 대표적인 원칙 중 하나가 ‘SOLID’입니다. 다음은 개발자 JD가 저를 가이드 하여 SOLID 원칙을 준수하게끔 코드를 개선한 결과입니다.
Logger.kt
interface Logger {
fun log(message: String)
}
SystemLogger.kt
class SystemLogger() : Logger {
override fun log(message: String) {
println(message)
}
}
ExecutionTimer.kt
class ExecutionTimer(
private val logger: Logger
) : TestUtil {
private var startTime : Long = 0
override fun runBeforeTest() {
startTime = System.nanoTime()
}
override fun runAfterTest() {
val endTime = System.nanoTime()
val executionTIme = endTime - startTime
val message = "TIME: $executionTIme"
logger.log(message)
}
}
위를 통해 다음과 같은 사항이 개선되었습니다.
ExecutionTimeSystemLogger
는 ExecutionTimer
, Logger
로 분리되어 각각이 할 일에 대해 책임집니다.
SystemLogger
는 Logger
를 구현한다. 만약, 클라이언트가 System 로그가 아닌 File로 로그를 남기고 싶을 때는 단순히 FileSystemLogger
를 구현하여 대응하면 됩니다.
ExecutionTimer
는 Logger
에 의존합니다. Logger
는 바뀔 가능성이 매우 적기 때문에 그로 인해 ExecutionTimer
가 변경될 가능성도 적어집니다.
이제 아래와 같은 클라이언트의 요구에 다음과 같이 FileSystemLogger
를 추가하여 대응할 수 있습니다.
“File로 실행 시간을 출력하는 기능을 추가해 주세요.”
class FileSystemLogger() : Logger {
override fun log(message: String) {
//File System Logging..
}
}
이렇게 설계하면 실행 시간을 계산하는 로직이나 출력의 포맷이 변하더라도 영향을 받지 않게 됩니다. 그리고, 하나 더 중요한 점은 ExecutionTimeSystemLogger
가 ExecutionTimer
와 Logger
로 분리 덕분에 다른 곳에서도 Logger
를 재활용할 수 있게 되었습니다.
버즈빌의 신입 개발자가 가이드를 통해 코드를 개선해 나아가는 과정을 설명드렸습니다. 다음엔 더 재밌는 포스팅으로 찾아오겠습니다. 추가로, 버즈빌의 신입 개발자로 채용되시면, 많은 개발자들이 코드 리뷰를 통해 어떻게 소프트웨어를 유연하게 설계할 수 있는지 자세하게 설명해 드립니다!
버즈빌은 2023년 한 해 동안 월간 약 1.2억, 연 기준으로 14억에 달하는 AWS 비용을 절약하였습니다. 그 경험과 팁을 여러 차례에 걸쳐 공유합니다. AWS 비용 최적화 Part 1: 버즈빌은 어떻게 월 1억 이상의 AWS 비용을 절약할 수 있었을까 (준비중) …
Read Article들어가며 안녕하세요, 버즈빌 데이터 엔지니어 Abel 입니다. 이번 포스팅에서는 데이터 파이프라인 CI 테스트에 소요되는 시간을 어떻게 7분대에서 3분대로 개선하였는지에 대해 소개하려 합니다. 배경 이전에 버즈빌의 데이터 플랫폼 팀에서 ‘셀프 서빙 데이터 …
Read Article