"깊이"가 다른 게임개발자 허민영

유저에서 게임까지, 철학에서 코딩까지, 본질을 보는 게임개발

소프트웨어 공학/아키텍처 및 구조론

제1형 정규화(1NF)와 단일책임원칙을 활용한 리팩토링

허민영 2025. 4. 4. 22:39

제1형 정규화(1NF)와 단일책임원칙을 활용한 리팩토링

데이터베이스 정규화는 중복을 줄이고 데이터 무결성을 확보하기 위해 테이블 구조를 체계화하는 과정이다. 그 중 **제1형 정규화(1NF, First Normal Form)**는 가장 기본적인 정규화 단계로, **테이블의 각 컬럼이 원자값(Atomic value)**을 갖도록 강제한다. 즉, 하나의 셀에는 하나의 값만 존재해야 하며, 리스트나 배열 같은 다중 값을 허용하지 않는다.

예를 들어 다음과 같은 테이블은 1NF를 위반하고 있다.

IDNamePhoneNumbers
1 Alice 010-1111-1111, 010-2222-2222

위 예시에서 PhoneNumbers 컬럼은 쉼표로 구분된 다중 값을 가지므로 원자성이 깨진 상태이다. 이를 1NF로 정규화하면 다음과 같이 분해할 수 있다:

IDNamePhoneNumber
1 Alice 010-1111-1111
1 Alice 010-2222-2222

이처럼 복합된 데이터를 개별적인 단위로 분리함으로써 정보의 독립성과 유지보수성이 향상된다. 이 원칙은 비단 DB 설계뿐 아니라 객체지향 프로그래밍에서의 **리팩토링과 SRP(Single Responsibility Principle, 단일 책임 원칙)**에도 직접적으로 연관된다.


1NF와 단일 책임 원칙의 연결점

1NF는 "각 필드는 하나의 정보만을 가져야 한다"는 원칙이고, SRP는 "각 클래스는 하나의 책임만 가져야 한다"는 원칙이다. 여러 책임이 혼합된 클래스나 함수는 마치 하나의 셀에 여러 데이터를 넣은 것과 마찬가지로, 코드의 가독성과 유지보수를 저해한다.

예를 들어 아래와 같은 C# 코드를 보자. Unity 프로젝트 내에서 흔히 발생할 수 있는 구조이다.

public class EnemyManager : MonoBehaviour
{
    public void SpawnEnemies()
    {
        // 스폰 위치 계산
        Vector3 spawnPos = GetRandomSpawnPoint();

        // 프리팹 인스턴스 생성
        GameObject enemy = Instantiate(enemyPrefab, spawnPos, Quaternion.identity);

        // 로그 기록
        Debug.Log("Enemy spawned at " + spawnPos);
    }

    private Vector3 GetRandomSpawnPoint()
    {
        return new Vector3(Random.Range(-10, 10), 0, Random.Range(-10, 10));
    }
}

이 클래스는 "스폰 위치 계산", "적 인스턴스 생성", "로그 기록"이라는 세 가지 책임을 갖고 있다. 이 구조는 단일 책임 원칙에 위배되며, 마치 한 셀에 여러 값을 담은 것과 유사한 문제를 낳는다. 유지보수가 어려워지고, 각 책임에 대한 테스트나 확장도 어려워진다.


리팩토링: 단일 책임 원칙 적용

1NF의 아이디어를 따라 각 책임을 분리하면 다음과 같이 리팩토링할 수 있다.

public class EnemySpawner
{
    private readonly ISpawnPointProvider _spawnPointProvider;
    private readonly ILogger _logger;

    public EnemySpawner(ISpawnPointProvider spawnPointProvider, ILogger logger)
    {
        _spawnPointProvider = spawnPointProvider;
        _logger = logger;
    }

    public void Spawn(GameObject prefab)
    {
        Vector3 pos = _spawnPointProvider.GetSpawnPoint();
        GameObject enemy = UnityEngine.Object.Instantiate(prefab, pos, Quaternion.identity);
        _logger.Log("Enemy spawned at " + pos);
    }
}

public interface ISpawnPointProvider
{
    Vector3 GetSpawnPoint();
}

public class RandomSpawnPointProvider : ISpawnPointProvider
{
    public Vector3 GetSpawnPoint()
    {
        return new Vector3(Random.Range(-10, 10), 0, Random.Range(-10, 10));
    }
}

public interface ILogger
{
    void Log(string message);
}

public class DebugLogger : ILogger
{
    public void Log(string message)
    {
        Debug.Log(message);
    }
}

이 구조에서는 각 클래스가 명확히 하나의 책임만을 가지며, 테스트나 기능 확장(예: 로그 출력 방식 변경, 스폰 포인트 전략 변경)도 용이하다. 이는 1NF가 테이블을 정규화하여 구조를 명확히 하듯, 클래스 간 책임 분리를 통해 코드의 결합도를 낮추고 유연성을 높이는 효과를 가져온다.


결론

제1형 정규화는 데이터를 원자적으로 분해하여 명확한 구조를 만드는 원칙이며, 단일 책임 원칙은 코드를 책임 단위로 나누어 유지보수성을 높이는 설계 지침이다. 두 개념은 본질적으로 "혼합을 피하고 명확히 나누라"는 동일한 철학을 공유하며, 이를 리팩토링에 적용하면 보다 견고하고 변화에 강한 시스템을 만들 수 있다. 데이터든, 코드든 한 요소에 한 책임이라는 원칙은 변하지 않는다.