2023. 1. 19. 15:30

EF(Entity Framework)에서 FK(foreign key)를 걸면 자동으로 List 타입을 관리해줍니다.

FK가 개발 중에는 좋은데 서비스 중에는 좀 단점이 있고(특정 데이터를 수동으로 지우려면 연결된 FK를 순서대로 뒤에서부터 지워야 함.)

자동으로 바인딩 되다 보니 무분별하게 난발하게 돼서 속도를 다 까먹는 문제가 있습니다. (잘 관리하면 좋긴 합니다. ㅎㅎㅎ)

EF는 FK로 묶인 데이터에 접근하면 인덱스로 전체 선택(select)해서 처리하기 때문입니다. ㅎㄷㄷ

 

그래서 저는 가급적 FK를 안 쓰고 수동으로 선택(select)해서 사용하는 방법을 사용합니다.

그러다 보니 EF에서 리스트형 데이터들은 어떻게 처리되는지 궁금해졌습니다.

 

 

1. 배열과 리스트

모델을 아래와 같이 선언하고 마이그레이션을 해보았습니다.

public class EfList_Test1
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int idEfList_Test1 { get; set; }

    public int[] ListInt1{ get; set; } = new int[0];
    
    public string[] ListString1 { get; set; } = new string[0];
    public List<string> ListString2 { get; set; } = new List<string>();
}

 

아래와 같은 에러가 납니다.

System.InvalidOperationException: The property 'EfList_Test1.ListInt1' could not be mapped because it is of type 'int[]', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.

System.InvalidOperationException: 'EfList_Test1.ListInt1' 속성은 지원되는 기본 형식 또는 유효한 엔터티 형식이 아닌 'int[]' 형식이므로 매핑할 수 없습니다. 이 속성을 명시적으로 매핑하거나 '[NotMapped]' 특성을 사용하거나 'OnModelCreating'에서 'EntityTypeBuilder.Ignore'를 사용하여 무시합니다.

 

 

문자열 배열도 같은 에러가 납니다.

 

리스트의 경우 기본 키를 지정하라는 에러가 나는데

이것은 기본 키가 있는 별도의 모델을 선언하여 사용하라는 의미입니다.

 

여기서 기본 키가 있는 별도의 모델로 리스트를 선언하면 자동으로 FK가 연결됩니다.

System.InvalidOperationException: The entity type 'List<string>' requires a primary key to be defined. If you intended to use a keyless entity type, call 'HasNoKey' in 'OnModelCreating'. For more information on keyless entity types, see https://go.microsoft.com/fwlink/?linkid=2141943.

System.InvalidOperationException: 엔터티 형식 'List<string>'에는 기본 키를 정의해야 합니다. 키 없는 엔터티 유형을 사용하려는 경우 'OnModelCreating'에서 'HasNoKey'를 호출합니다. 키 없는 엔터티 유형에 대한 자세한 내용은 https://go.microsoft.com/fwlink/?linkid=2141943을 참조하세요.

 

결국 FK없이는 필드를 리스트로 선언할 수 없다는 의미입니다.

 

 

2. 직렬화/ 역 직렬화

별도의 테이블을 만들고 싶지 않다면 저장은 문자열로 하고 모델에서만 리스트나 배열로 보이도록

자동으로 직렬화/역 직렬화 하도록 구성할 수도 있습니다.

 

2-1. 배열과 리스트를 직렬화/역 직렬화

1) 필드를 배열로 선언합니다.

public string[] ListString1 { get; set; } = new string[0];

 

2) DbContext > OnModelCreating에서 자동화 코드를 아래와 같이 넣습니다.

아래 코드는 배열을 콤마(,)로 구분하는 문자열로 바꿔주는 코드입니다.

필요에 따라 다른 구분자를 이용할 수 있습니다.

//문자열 배열
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListString1)
    .HasConversion(
        //배열을 콤마(,)로 구분하는 문자열로 바꾸기
        c => string.Join(',', c),
        //콤마(,)로 구분된 문자열을 배열로 변환
        c => c.Split(',', StringSplitOptions.RemoveEmptyEntries));

//문자열 리스트
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListString2)
    .HasConversion(
        //배열을 콤마(,)로 구분하는 문자열로 바꾸기
        c => string.Join(',', c),
        //콤마(,)로 구분된 문자열을 리스트로 변환
        c => c.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList());

문자열과 리스트는 출력 형식이 달라 약간의 코드 차이가 있습니다.

 

 

변환돼서 잘 들어가는군요.

 

불러오는 것도 배열과 리스트 문제없이 불러와집니다.

 

'값 비교자'를 설정해달라는 경고

위 코드를 마이그레이션을 하면 아래와 같은 경고가 표시될 수 있습니다.

The property '[테이블].[필드]' is a collection or enumeration type with a value converter but with no value comparer. Set a value comparer to ensure the collection/enumeration elements are compared correctly.

이 경우 명시적으로 '값 비교자'를 지정해야 합니다.

참고 : MS Learn - 값 비교자

 

아래 코드로 수정합니다.

//문자열 배열
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListString1)
    .HasConversion(
        //배열을 콤마(,)로 구분하는 문자열로 바꾸기
        c => string.Join(',', c)
        //콤마(,)로 구분된 문자열을 배열로 변환
        ,c => c.Split(',', StringSplitOptions.RemoveEmptyEntries)
        , new ValueComparer<string[]>(
            (c1, c2) => c1!.SequenceEqual(c2!)
            , c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
            , c => c.ToArray()));


//문자열 리스트
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListString2)
    .HasConversion(
        //배열을 콤마(,)로 구분하는 문자열로 바꾸기
        c => string.Join(',', c)
        //콤마(,)로 구분된 문자열을 리스트로 변환
        , c => c.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
        , new ValueComparer<List<string>>(
            (c1, c2) => c1!.SequenceEqual(c2!)
            , c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
            , c => c.ToList()));

 

9번 줄, 23번 줄 : 필드로 사용할 데이터 형식을 지정합니다.

 

10번 줄, 24번 줄 : 인수가 널(null)을 허용하지 않으므로 null이 온다면 에러가능성이 있습니다.

 

12번 줄, 26번 줄 : 해당 프로퍼티가 사용할 데이터 형에 맞게 출력해야 합니다.

 

 

2-2. JSON구조로 직렬화/역 직렬화

이곳에 구조화된 데이터(모델, model)을 넣고 싶다면 JSON과 같이 구조화된 데이터를 문자열로 처리할 수 있는 방법을 이용해야 합니다.

이 글에서는 'System.Text.Json'를 이용하여 따로 선언된 모델을 JSON 문자열로 바꿔

저장하고 불러오도록 하겠습니다.

 

저장할 모델을 아래와 같습니다.

public class JsonTestModel
{
	public int id { get; set; }
	public string Name { get; set; } = string.Empty;
	public int Age { get; set; }
}

 

필드를 아래와 같이 선언합니다.

public List<JsonTestModel> ListJson1 { get; set; } = new List<JsonTestModel>();

 

DbContext > OnModelCreating

에 아래 코드를 넣습니다.

//json 리스트
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListJson1)
    .HasConversion(
        //JSON 모델을 리스트로 사용하는 문자열로 변환
        c => JsonSerializer.Serialize(c, new JsonSerializerOptions())
        //JSON 모델을 리스트를 리스트 모델로 전환
        , c => JsonSerializer.Deserialize<List<JsonTestModel>>(c, new JsonSerializerOptions())!
        , new ValueComparer<List<JsonTestModel>>(
            (c1, c2) => c1!.SequenceEqual(c2!)
            , c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
            , c => c.ToList()));

6번 줄 : JSON 모델 리스트를 문자열로 변환합니다.

 

8번 줄 : 문자열을 JSON 모델 리스트fh qusghks gkqslek.

 

9번 줄 : '값 비교자' 설정해줍니다.

 

JSON 문자열로 잘 변환되는군요.

 

 

'메서드 또는 속성 간 호출이 모호 합니다.' 에러
'JsonSerializer.Deserialize<TValue>(string, JsonSerializerOptions?)' 및 'JsonSerializer.Deserialize<TValue>(string, JsonTypeInfo<TValue>)'의 메서드 또는 속성 간 호출이 모호합니다.

이전 버전 'System.Text.Json.JsonSerializer.Serialize'과 'System.Text.Json.JsonSerializer.Deserialize'의 매개변수 변화 때문에 발생하는 에러입니다.

두 번째 인자를 'JsonSerializerOptions'개체로 넣어 줘야 합니다.

( 참고 : MS Learn - 새 JsonSerializer 소스 생성기 오버로드 )

//에러
c => JsonSerializer.Serialize(c, default)

//수정
c => JsonSerializer.Serialize(c, new JsonSerializerOptions())

 

'null을 허용하지 않는 형식으로 변환하는 중입니다.' 경고
null 리터럴 또는 가능한 null 값을 null을 허용하지 않는 형식으로 변환하는 중입니다.
혹은
가능한 null 참조 반환입니다.

 

'JsonSerializer.Deserialize'는 'null'을 반환할 가능성이 있으므로 이것에 대한 처리해야 합니다.

필드에 'null'을 허용하거나(필드에 물음표(?) 추가) 

public List<JsonTestModel>? ListJson1 { get; set; } = new List<JsonTestModel>();

 

변환할 때 'null'이 아님을 명시해줍니다. (변환된 결과 리턴에 느낌표(!) 추가)

c => JsonSerializer.Deserialize<List<JsonTestModel>>(c, new JsonSerializerOptions())!

 

 

3. 전체 코드

프로젝트 소스 : github - dang-gun/EntityFrameworkSample/ListField

 

테스트에 사용한

DbContext > OnConfiguring

의 전체 코드입니다.

//문자열 배열
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListString1)
    .HasConversion(
        //배열을 콤마(,)로 구분하는 문자열로 바꾸기
        c => string.Join(',', c)
        //콤마(,)로 구분된 문자열을 배열로 변환
        ,c => c.Split(',', StringSplitOptions.RemoveEmptyEntries)
        , new ValueComparer<string[]>(
            (c1, c2) => c1!.SequenceEqual(c2!)
            , c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
            , c => c.ToArray()));


//문자열 리스트
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListString2)
    .HasConversion(
        //배열을 콤마(,)로 구분하는 문자열로 바꾸기
        c => string.Join(',', c)
        //콤마(,)로 구분된 문자열을 리스트로 변환
        , c => c.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList()
        , new ValueComparer<List<string>>(
            (c1, c2) => c1!.SequenceEqual(c2!)
            , c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
            , c => c.ToList()));


//json 리스트
modelBuilder.Entity<EfList_Test2>()
    .Property(p => p.ListJson1)
    .HasConversion(
        //JSON 모델을 리스트로 사용하는 문자열로 변환
        c => JsonSerializer.Serialize(c, new JsonSerializerOptions())
        //JSON 모델을 리스트를 리스트 모델로 전환
        , c => JsonSerializer.Deserialize<List<JsonTestModel>>(c, new JsonSerializerOptions())!
        , new ValueComparer<List<JsonTestModel>>(
            (c1, c2) => c1!.SequenceEqual(c2!)
            , c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode()))
            , c => c.ToList()));

modelBuilder.Entity<EfList_Test2>().HasData(
    new EfList_Test2
    {
        idEfList_Test2 = 1,
        ListString1 = new string[] { "a", "b", "c" },
        ListString2 = new List<string> { "a", "b", "c" },
        ListJson1 = new List<JsonTestModel>() { new JsonTestModel() {id=1, Name="test", Age=10 } },
    });

modelBuilder.Entity<EfList_Test2>().HasData(
    new EfList_Test2
    {
        idEfList_Test2 = 2,
        ListString1 = new string[] { "a", "b", "c" },
        ListString2 = new List<string> { "a", "b", "c" },
        ListJson1 = new List<JsonTestModel>() { new JsonTestModel() { id = 1, Name = "test", Age = 10 } },
    });

modelBuilder.Entity<EfList_Test2>().HasData(
    new EfList_Test2
    {
        idEfList_Test2 = 3,
        ListString1 = new string[] { "a", "b", "c" },
        ListString2 = new List<string> { "a", "b", "c" },
        ListJson1 = new List<JsonTestModel>() { new JsonTestModel() { id = 3, Name = "test", Age = 10 } },
    });

 

 

마무리

참고 : stackoverflow - Entity Framework - Code First - Can't Store List<String> - Sasan님 답변, Mathieu VIALES님 답변

 

원래를 가벼운 마음으로 작성한 건데.....

내용이 내용인지라 가볍지 않네요 ㅎㅎㅎ