💡 첫 번째 단상: 버전이라는 이름의 괴물
오늘 UnityEngine.Input 관련 에러를 마주했다. 새 Input System을 쓰면서 구버전 클래스를 호출한, 금방 해결될 작은 문제였다. 하지만 이 사소한 충돌은 내게 '버전 관리'라는, IT 업계의 거대한 괴물을 떠올리게 했다. 현업에서 버전업은 단순한 기능 추가가 아니라 거대한 수술에 가깝다.
🧐 오늘의 에러: Input System 충돌
우선 마주한 에러 메시지는 다음과 같았다.
InvalidOperationException: You are trying to read Input using the UnityEngine.Input class, but you have switched active Input handling to Input System package in Player Settings.
원인을 파고들어 보니, 유니티의 두 가지 입력 처리 방식 간의 충돌이었다.
기존 방식 Input Manager (Old): Input.GetKey()처럼 Input 클래스를 직접 사용하는 전통적이고 직관적인 방식.
새로운 방식 Input System (New): 패키지 기반의 최신 방식으로, 이벤트 기반으로 동작하며 다양한 컨트롤러를 더 유연하게 관리할 수 있다.
내 프로젝트 설정은 '새로운 방식'을 사용하도록 되어 있는데, 정작 내 코드에서는 '기존 방식'으로 입력을 받으려 했으니 충돌이 날 수밖에 없었던 것이다.
여기서 흥미로웠던 점은 유니티가 제시하는 세 가지 해결책이었다. 이는 단순한 코드 수정을 넘어, 개발자의 '선택'을 요구했다.
code
C#
// 기존 방식의 코드 (Error 발생!)
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
Debug.Log("Jump! (Old Input)");
}
}
C#
// 새로운 방식에 맞춰 수정한 코드 (권장)
using UnityEngine;
using UnityEngine.InputSystem; // 네임스페이스 추가
public class Player : MonoBehaviour
{
// 점프 액션이 수행되었을 때 호출될 함수
public void OnJump(InputAction.CallbackContext context)
{
if (context.performed)
{
Debug.Log("Jump! (New Input System)");
}
}
}
Project Settings에서 Active Input Handling을 Input Manager (Old)로 변경하는 것. 레거시 튜토리얼을 따라 하거나 빠른 프로토타이핑에 유용하다.
설정을 Both로 변경하여 두 방식의 코드가 공존할 수 있게 만드는 것. 점진적인 마이그레이션 과정에 적합하다.
이 세 가지 선택지, 즉 '완전한 전환', '현상 유지(회귀)', '점진적 타협'을 마주하니, 과거 APNS 프로젝트의 기억이 자연스럽게 떠올랐다.
과거 APNS(Apple Push Notification Service) 마이그레이션 프로젝트가 그랬다. APNS가 HTTP/2 프로토콜 기반의 APNS Provider API로 전환되면서, 우리 시스템도 대대적인 업데이트가 필요했다.
문제는 그 요구사항이 단순히 라이브러리 교체 수준이 아니었다는 점이다.
OpenSSL은 1.1.1 이상 버전이 필요했다.
Java는 나름 1.8을 사용하고 있었지만 Http2 전환을위한 alpns사용을 위해 특정 이상의 마이너 버전이 필요했다.
가장 큰 장벽은 OS였다. CentOS 7에서는 필요한 커널과 라이브러리 지원이 힘들어 Rocky Linux 9 같은 최신 배포판으로의 업그레이드가 불가피했다.
OS부터 JVM까지, 인프라의 근간을 뒤흔드는 작업이었다. 수많은 서비스가 얽힌 운영 서버에서 이 수술을 감행했을 때 터져 나올 사이드 이펙트를 아무도 감당할 자신이 없었다.
결국 우리의 선택은 '회피'였다. 아무도 쓰지 않는 낡은 서버를 찾아, 그곳에만 격리된 APNS 전용 프로젝트를 새로 구축하는 것. 기술 부채를 끌어안는 한이 있더라도, 서비스 전체의 안정성을 지키는 것이 우선이었다.
오늘 유니티의 구/신 Input System을 보며 그때의 기억이 선명해졌다. 더 나은 기술의 등장은 언제나 개발자를 설레게 하지만, 그 설렘 뒤에는 '마이그레이션'이라는 현실의 벽이 서 있다.
🤔 두 번째 단상: 싱글톤과 의존성 주입 사이에서의 고민
게임 매니저 클래스를 만들 때 싱글톤 패턴이 거의 표준처럼 쓰이는 것을 보았다. 전역 어디서든 접근 가능한 단 하나의 인스턴스라는 개념은 확실히 편리해 보인다. 문득 스프링 프레임워크의 빈(Bean)이 떠올랐는데, 같은 싱글톤 개념을 구현하는 방식에 있어 흥미로운 차이점이 보였다.
스프링에서 객체(Bean)는 스스로를 드러내지 않는다. IoC 컨테이너가 @Autowired 같은 어노테이션을 통해 필요한 곳에 의존성을 '주입(DI)' 해주는, 철저히 관리받는 구조다. 객체들은 자신이 누구에게 쓰일지 알 필요 없이 수동적으로 주입받아 자신의 역할만 수행하면 된다.
반면, 유니티에서 흔히 접한 public static Instance 방식의 싱글톤은 클래스 스스로가 자신을 외부에 적극적으로 노출시킨다. "내가 여기 있으니 필요하면 누구든 가져다 쓰세요" 하는 식이다.
이 방식이 가져오는 직접적인 편리함은 분명하지만, 몇 가지 생각해 볼 만한 지점들이 있었다.
결합(Coupling): 코드가 특정 싱글톤 클래스의 존재를 직접 알고 호출해야 하므로, 자연스럽게 해당 싱글톤에 대한 의존성이 생긴다.
테스트의 격리: 단위 테스트를 진행할 때, 여러 테스트 케이스가 하나의 전역 인스턴스 상태를 공유하게 되면 테스트 간 격리가 어려워질 수 있다.
의존성의 명확성: 메서드 시그니처만 봐서는 이 메서드가 내부적으로 어떤 전역 상태를 참조하고 변경하는지 파악하기 어렵다.
코드의 흐름을 따라가 봐야 비로소 숨겨진 의존성을 발견하게 되는 경우가 생긴다.
스프링이 왜 그토록 객체의 생성과 사용을 분리하고, 의존성 주입(DI)이라는 방식을 통해 객체 간의 관계를 느슨하게 유지하려 했는지 그 이유를 되새겨 보게 된다.
물론 게임 개발 환경의 특수성이 있겠지만, 싱글톤의 즉각적인 편리함과 DI가 추구하는 구조적 유연성 사이에서 어떤 균형을 잡는 것이 좋을지 앞으로 계속 고민해 봐야 할 주제인 것 같다.
🚀 세 번째 단상: 규칙이라는 이상, 현업이라는 현실
유니티 C#에서는 private 필드에 _ 접두사를 붙이는 등, 웹 개발보다 엄격한 코딩 컨벤션(Coding Convention)을 권장하는 것처럼 보였다.
이런 규칙들은 코드의 가독성을 높이고, 인스펙터에 노출될 변수와 내부 변수를 구분해 실수를 줄여주는 훌륭한 '이상'이다.
하지만 현업에서 이런 '이상'은 얼마나 지켜질까?
3년간의 웹 개발 경험을 돌이켜보면, '이상'과 '현실' 사이의 괴리는 언제나 존재했다.
잘 짜인 구조, 클린 코드, 일관된 컨벤션. 그 중요성을 모르는 개발자는 없다.
그럼에도 불구하고 현실의 코드는 왜 그렇게 되지 못할까?
그것은 '몰라서'가 아니라 '알면서도 타협하기' 때문이다.
시간이 없어서: "일단 되게 만들자"는 생각으로 public을 남발하고, 리팩토링은 다음으로 미룬다. (그리고 그 '다음'은 오지 않는다.)
협업 때문에: 다른 팀과 얽힌 복잡한 일정 속에서 나 혼자 이상적인 구조를 외치는 것은 공허한 메아리가 되기 십상이다.
그냥 그게 편해서: 당장 눈앞의 버그를 잡기 위해 컨벤션을 무시한 코드를 한 줄 추가하는 그 순간의 편리함은 너무나 달콤하다.
결국 작명 규칙을 지키고 좋은 구조를 고민하는 행위는, 단순히 코드를 예쁘게 꾸미는 것이 아니다.
그것은 "미래의 나, 그리고 내 동료들이 고통받지 않도록 현재의 내가 기꺼이 감수하는 약간의 불편함" 에 가깝다.
이 불편함을 감수할 여유와 문화가 없는 조직에서 클린 코드는 그저 책 속에나 존재하는 판타지일 뿐이다.
게임 개발 현장이라고 다를까? 아마 아닐 것이다.
이 이상과 현실의 줄다리기 속에서 나만의 중심을 잡는 것. 그것이 앞으로 내가 마주할 가장 큰 숙제일지도 모른다.