보이지 않는 것을 전달하는 방법

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
소프트웨어 프로그래밍에서 지속되는 도전 과제 중 하나는 이것입니다: "보이지 않는 것을 어떻게 전달할 것인가?" 로거, HTTP 요청 컨텍스트, 현재 로케일, I/O 핸들—이러한 정보는 프로그램 전체에서 필요하지만, 모든 함수 매개변수를 통해 명시적으로 전달하는 것은 감당할 수 없을 정도로 장황할 것입니다.
역사적으로 이 문제를 해결하기 위한 다양한 접근 방식이 등장했습니다. 동적 스코핑(Dynamic scoping), 관점 지향 프로그래밍(Aspect-oriented programming), 컨텍스트 변수(Context variables), 최신 이펙트 시스템(Effect systems)... 일부는 연속적인 발전의 진화적 단계를 나타내고, 다른 일부는 독립적으로 발생했습니다. 그러나 우리는 이 모든 개념을 통합된 렌즈를 통해 볼 수 있습니다.
동적 스코핑
동적 스코핑은 1960년대 Lisp에서 시작되어 가장 순수한 형태의 해결책을 제공했습니다. "변수의 값은 그것이 정의된 위치가 아니라 호출된 위치에 의해 결정됩니다." 단순하고 강력하지만, 예측 불가능성 때문에 Common Lisp과 Perl 이후 주류 프로그래밍 언어에서는 인기를 잃었습니다. 그러나 우리는 여전히 JavaScript의 this
바인딩에서 그 계보를 추적할 수 있습니다.
;; Common Lisp 예제 - 동적으로 바인딩된 로거
(defvar *logger* nil)
(defun log-message (message)
(when *logger*
(funcall *logger* message)))
(defun process-user-data (data)
(log-message (format nil "Processing user: ~a" data))
;; 실제 처리 로직...
)
(defun main ()
(let ((*logger* (lambda (msg) (format t "[INFO] ~a~%" msg))))
(process-user-data "john@example.com"))) ; 로거가 암묵적으로 전달됨
관점 지향 프로그래밍
AOP는 "횡단 관심사를 모듈화하는" 핵심 아이디어를 구조화했습니다. 그 철학은 "규칙과 함께 컨텍스트를 주입하라"입니다. 로깅이나 트랜잭션과 같은 횡단 관심사를 애스펙트로 분리함으로써, 동적 스코핑의 유연성을 유지하면서 더 예측 가능한 동작을 추구했습니다. 그러나 디버깅의 어려움과 성능 오버헤드로 인해 Java와 .NET 생태계를 넘어 확산되는 데 제한이 있었습니다.
// Spring AOP 예제 - 로깅이 횡단 관심사로 분리됨
@Aspect
public class LoggingAspect {
private Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Around("@annotation(Loggable)")
public Object logMethodCall(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
logger.info("Entering method: " + methodName);
Object result = joinPoint.proceed();
logger.info("Exiting method: " + methodName);
return result;
}
}
@Service
public class UserService {
@Loggable // 로거가 애스펙트를 통해 암묵적으로 주입됨
public User processUser(String userData) {
// 실제 처리 로직...
return new User(userData);
}
}
*AOP: aspect-oriented programming (관점 지향 프로그래밍)
컨텍스트 변수
컨텍스트 변수는 현대적 요구사항—비동기 및 병렬 프로그래밍—을 위해 재설계된 동적 스코핑을 나타냅니다. Python의 contextvars
와 Java의 ThreadLocal
이 이 접근 방식을 보여줍니다. 그러나 이들은 여전히 런타임 의존성과 API 컨텍스트 요구사항이 문서를 통해서만 발견 가능하다는 사실로 인해 어려움을 겪습니다.
컨텍스트 변수의 또 다른 형태는 React의 contexts와 다른 UI 프레임워크의 유사한 개념에서 나타납니다. 사용법은 다양하지만, 모두 동일한 문제를 해결합니다: prop drilling(속성 드릴링). 컴포넌트 트리를 통한 암묵적 전파는 함수 호출 스택을 통한 전파를 반영합니다.
# Python contextvars 예제 - 컨텍스트를 통해 전파되는 커스텀 로거
from contextvars import ContextVar
# 컨텍스트 변수로 커스텀 로거 함수 정의
logger_func = ContextVar('logger_func')
def log_info(message):
log_fn = logger_func.get()
if log_fn:
log_fn(f"[INFO] {message}")
def process_user_data(data):
log_info(f"Processing user: {data}")
validate_user_data(data)
def validate_user_data(data):
log_info(f"Validating user: {data}") # 로거가 암묵적으로 전파됨
def main():
# 컨텍스트에 특정 로거 함수 설정
def my_logger(msg):
print(f"CustomLogger: {msg}")
logger_func.set(my_logger)
process_user_data("john@example.com")
모나드
모나드는 이 문제를 다른 출발점에서 접근합니다. 암묵적인 컨텍스트 전달 대신, 모나드는 타입 시스템에 효과를 인코딩하려고 시도합니다—더 근본적인 문제를 해결하는 것입니다. 특히 Reader
모나드는 컨텍스트 변수에 해당합니다. 그러나 모나드 트랜스포머를 통해 여러 효과를 결합할 때 복잡성이 폭발적으로 증가했습니다. 개발자들은 ReaderT Config (StateT AppState (ExceptT Error IO))
와 같은 다루기 어려운 타입과 씨름해야 했습니다. 레이어 순서가 중요했고, 각 레이어는 명시적인 리프팅(lifting)이 필요했으며, 사용성이 저하되었습니다. 결과적으로 모나딕한 아이디어는 주로 Haskell, Scala, F#과 같은 진지한 함수형 프로그래밍 언어에 국한되었습니다.
-- Haskell Logger 모나드 예제 - 커스텀 Logger 모나드 정의
newtype Logger a = Logger (IO a)
instance Functor Logger where
fmap f (Logger io) = Logger (fmap f io)
instance Applicative Logger where
pure = Logger . pure
Logger f <*> Logger x = Logger (f <*> x)
instance Monad Logger where
Logger io >>= f = Logger $ do
a <- io
let Logger io' = f a
io'
-- 로깅 함수
logInfo :: String -> Logger ()
logInfo msg = Logger $ putStrLn $ "[INFO] " ++ msg
processUserData :: String -> Logger ()
processUserData userData = do
logInfo $ "Processing user: " ++ userData
validateUserData userData
validateUserData :: String -> Logger ()
validateUserData userData = do
logInfo $ "Validating user: " ++ userData -- 로거가 모나드를 통해 전달됨
runLogger :: Logger a -> IO a
runLogger (Logger io) = io
main :: IO ()
main = runLogger $ processUserData "john@example.com"
이펙트 시스템
이펙트 시스템은 모나드의 구성적 복잡성을 해결하기 위해 등장했습니다. Koka와 Eff와 같은 언어에서 구현되었으며, 대수적 효과(algebraic effects)와 핸들러를 통해 작동합니다. 여러 이펙트 레이어는 순서 제약 없이 구성됩니다. 여러 중첩된 레이어는 명시적인 리프팅이 필요하지 않습니다. 이펙트 핸들러는 고정되지 않습니다—동적으로 대체될 수 있어 상당한 유연성을 제공합니다.
그러나 컴파일러 최적화는 아직 미성숙하고, 기존 생태계와의 상호 운용성은 도전 과제를 제시하며, 이펙트 추론의 복잡성과 타입 시스템에 미치는 영향은 지속적인 연구 질문을 제시합니다. 이펙트 시스템은 여기서 논의된 가장 새로운 접근 방식이며, 그 한계는 더 넓은 채택을 얻으면서 탐구될 것입니다.
// Koka 이펙트 시스템 예제 - 유연하게 전파되는 로깅 이펙트
effect logger
fun log-info(message: string): ()
fun log-error(message: string): ()
fun process-user-data(user-data: string): logger ()
log-info("Processing user: " ++ user-data)
validate-user-data(user-data)
fun validate-user-data(user-data: string): logger ()
log-info("Validating user: " ++ user-data) // 로거 이펙트가 암묵적으로 전파됨
if user-data == "" then
log-error("Invalid user data: empty string")
fun main()
// 다른 로거 구현을 동적으로 선택할 수 있음
with handler
fun log-info(msg) println("[INFO] " ++ msg)
fun log-error(msg) println("[ERROR] " ++ msg)
process-user-data("john@example.com")
보이지 않는 것을 전달하는 예술—이것이 여기서 논의된 모든 개념이 공유하는 본질이며, 소프트웨어 프로그래밍에서 영원한 주제로서 새로운 형태로 계속 진화할 것입니다.