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

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub
소프트웨어 프로그래밍에서 지속되는 도전 과제 중 하나는 이것입니다: "어떻게 보이지 않는 것을 전달할 것인가?" 로거, HTTP 요청 컨텍스트, 현재 로케일, I/O 핸들—이러한 정보는 프로그램 전체에서 필요하지만, 모든 함수 매개변수를 통해 명시적으로 전달하는 것은 감당할 수 없을 정도로 장황할 것입니다.
역사적으로 이 문제를 해결하기 위한 다양한 접근 방식이 등장했습니다. 동적 스코핑(Dynamic scoping), 관점 지향 프로그래밍(AOP), 컨텍스트 변수, 최신 이펙트 시스템 등... 일부는 연속적인 발전의 진화적 단계를 나타내고, 다른 일부는 독립적으로 발생했습니다. 그러나 우리는 이 모든 개념을 통합된 관점으로 볼 수 있습니다.
동적 스코핑
동적 스코핑은 1960년대 Lisp에서 시작되어 가장 순수한 형태의 해결책을 제공했습니다. "변수의 값은 그것이 정의된 위치가 아니라 호출된 위치에 의해 결정됩니다." 단순하고 강력하지만, 예측 불가능성 때문에 Common Lisp과 Perl 이후 주류 프로그래밍 언어에서는 인기를 잃었습니다. 그러나 우리는 여전히 JavaScript의 this
바인딩에서 그 계보를 추적할 수 있습니다.
;; Common Lisp example - logger bound dynamically
(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))
;; actual processing logic…
)
(defun main ()
(let ((*logger* (lambda (msg) (format t "[INFO] ~a~%" msg))))
(process-user-data "john@example.com"))) ; logger passed implicitly
관점 지향 프로그래밍
AOP는 "횡단 관심사를 모듈화하는" 핵심 아이디어를 구조화했습니다. 그 철학은 "규칙과 함께 컨텍스트를 주입하라"입니다. 로깅이나 트랜잭션과 같은 횡단 관심사를 애스펙트로 분리함으로써, 동적 스코핑의 유연성을 유지하면서도 더 예측 가능한 동작을 추구했습니다. 그러나 디버깅의 어려움과 성능 오버헤드로 인해 Java와 .NET 생태계를 넘어서는 확산이 제한되었습니다.
// Spring AOP example - logging separated as cross-cutting concern
@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 // logger implicitly injected through aspect
public User processUser(String userData) {
// actual processing logic…
return new User(userData);
}
}
*AOP: 관점 지향 프로그래밍
컨텍스트 변수
컨텍스트 변수는 현대적 요구사항—비동기 및 병렬 프로그래밍—을 위해 재설계된 동적 스코핑을 나타냅니다. Python의 contextvars
와 Java의 ThreadLocal
이 이 접근 방식을 보여줍니다. 그러나 이들은 여전히 런타임 의존성과 API 컨텍스트 요구사항이 문서를 통해서만 발견 가능하다는 사실로 인해 어려움을 겪습니다.
컨텍스트 변수의 또 다른 형태는 React의 contexts와 다른 UI 프레임워크의 유사한 개념에서 나타납니다. 사용법은 다양하지만, 모두 동일한 문제를 해결합니다: prop drilling. 컴포넌트 트리를 통한 암시적 전파는 함수 호출 스택을 통한 전파를 반영합니다.
# Python contextvars example - custom logger propagated through context
from contextvars import ContextVar
# Define custom logger function as context variable
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}") # logger implicitly propagated
def main():
# Set specific logger function in context
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))
와 같은 다루기 어려운 타입과 씨름해야 했습니다. 레이어 순서가 중요했고, 각 레이어는 명시적인 리프팅이 필요했으며, 사용성이 저하되었습니다. 결과적으로 모나딕한 아이디어는 주로 Haskell, Scala, F#과 같은 진지한 함수형 프로그래밍 언어에 국한되었습니다.
-- Haskell Logger monad example - custom Logger monad definition
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'
-- Logging functions
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 -- logger passed through monad
runLogger :: Logger a -> IO a
runLogger (Logger io) = io
main :: IO ()
main = runLogger $ processUserData "john@example.com"
이펙트 시스템
이펙트 시스템은 모나드의 구성적 복잡성을 해결하기 위해 등장했습니다. Koka와 Eff와 같은 언어에서 구현되었으며, 대수적 효과와 핸들러를 통해 작동합니다. 여러 효과 레이어는 순서 제약 없이 구성됩니다. 여러 중첩된 레이어는 명시적인 리프팅이 필요하지 않습니다. 효과 핸들러는 고정되어 있지 않으며 동적으로 대체될 수 있어 상당한 유연성을 제공합니다.
그러나 컴파일러 최적화는 아직 미성숙하고, 기존 생태계와의 상호 운용성은 도전 과제를 제시하며, 효과 추론의 복잡성과 타입 시스템에 미치는 영향은 지속적인 연구 질문을 제시합니다. 이펙트 시스템은 여기서 논의된 가장 새로운 접근 방식이며, 그 한계는 더 넓은 채택을 얻으면서 탐구될 것입니다.
// Koka effect system example - logging effects flexibly propagated
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) // logger effect implicitly propagated
if user-data == "" then
log-error("Invalid user data: empty string")
fun main()
// Different logger implementations can be chosen dynamically
with handler
fun log-info(msg) println("[INFO] " ++ msg)
fun log-error(msg) println("[ERROR] " ++ msg)
process-user-data("john@example.com")
보이지 않는 것을 전달하는 기술—이것은 여기서 논의된 모든 개념이 공유하는 본질이며, 소프트웨어 프로그래밍에서 영원한 주제로서 새로운 형태로 계속 진화할 것입니다.