2026년 9월 6일 일요일

Python CSV KeyError: 'id' — Diagnose and Fix a UTF-8 BOM Header

If Python raises KeyError: 'id' even though the CSV appears to start with id, inspect the header before changing your data. A UTF-8 byte order mark (BOM) can turn that first key into '\ufeffid'. For that specific case, read the file with encoding="utf-8-sig".

UTF-8 decoding leaves a BOM in the first CSV header; UTF-8-sig removes the initial signature.
The same bytes produce different first-header keys. Diagram based on the verified Python example.

This walkthrough reproduces the failure and the fix using Python 3.12.13 on macOS, tested on September 6, 2026. The example uses synthetic data and only the standard library.

Match the symptom to the fix

Inspect reader.fieldnames before changing the file
Observed headerNext step
\ufeffidRead with encoding="utf-8-sig".
id;nameConfirm the separator, then set delimiter=";".
id or IDCheck spaces and case against the expected schema.
No headersCheck for an empty or failed export.

1. See the actual key, including invisible characters

Run this against your CSV. The repr() output makes the invisible prefix visible. Reading the first three bytes is a separate check: ef bb bf identifies the UTF-8 signature at the start of this file.

from pathlib import Path
import csv

path = Path("input.csv")
with path.open("rb") as f:
    print("First bytes:", f.read(3).hex(" "))
with path.open(encoding="utf-8", newline="") as f:
    reader = csv.DictReader(f)
    print("Headers:", [repr(x) for x in (reader.fieldnames or [])])

2. Reproduce the error without downloading a file

import csv
import io

raw = "id,name\r\n00123,서울\r\n".encode("utf-8-sig")
with io.TextIOWrapper(io.BytesIO(raw), encoding="utf-8", newline="") as f:
    row = next(csv.DictReader(f))
    print(repr(next(iter(row))))
    try:
        print(row["id"])
    except KeyError as error:
        print(type(error).__name__, str(error))

Observed output:

'\ufeffid'
KeyError 'id'

The row exists. The lookup fails because id and \ufeffid are different strings. In this reproduction, neither a missing row nor damaged Korean text causes the error.

3. Read the BOM-aware way and validate the header

import csv

with open("input.csv", encoding="utf-8-sig", newline="") as f:
    reader = csv.DictReader(f)
    headers = reader.fieldnames or []
    if "id" not in headers:
        raise ValueError(f"Expected an id column; got {headers!r}")
    for row in reader:
        print(row["id"])

For the sample above, the value is 00123. Our test also confirmed that the same reader handles the UTF-8 sample without a BOM. It does not turn the identifier into a number.

Python documents how utf-8-sig skips an initial UTF-8 signature. Its CSV documentation explains header-based dictionaries and the recommended newline="" file setting.

4. If it still fails, follow the header evidence

  • ['id;name']: your comma reader sees one column. If the file uses semicolons, pass delimiter=";".
  • [' id', 'name']: the first header has a space. Confirm the intended schema before trimming or renaming columns.
  • ['ID', 'name']: the spelling differs by case. Use the actual key or an explicit, checked mapping.
  • An empty list: there is no header to map. Check whether the file is empty or whether the export failed.

The wrong-delimiter and leading-space cases were included in our tests: switching to utf-8-sig did not fix either one. Avoid silently falling back to row.get("id", "") before diagnosing the schema; that would hide the failed lookup.

What this fix does not do

It does not detect every encoding, repair invalid UTF-8 bytes, or remove a U+FEFF character embedded in a cell. We tested invalid UTF-8 and an interior U+FEFF separately: decoding failed for the former and preserved the latter. Keep the original export while diagnosing the problem.

For choosing the encoding of a new export, see UTF-8 vs UTF-8 BOM for CSV: a tested Python read/write matrix.

2026년 1월 7일 수요일

어고 커플링 구매후기!!

최근에 압구정 현대백화점에서 진행 중인 어고 팝업 스토어에 다녀왔습니다. 온라인으로만 보던 브랜드였는데, 직접 보고 착용해볼 수 있는 기회라 방문하게 되었어요. 팝업 공간은 전체적으로 깔끔하고 정돈된 분위기였고, 브랜드 특유의 차분한 무드가 잘 느껴졌습니다.

가장 인상 깊었던 점은 직원분의 응대였습니다. 단순히 제품을 설명하는 데 그치지 않고, 반지의 디자인 의도나 착용 시 느낌 등을 차분하게 안내해주셔서 편하게 둘러볼 수 있었어요. 부담스럽게 구매를 권하는 분위기가 아니라, 궁금한 점을 물어보면 하나하나 친절하게 설명해주셔서 팝업 방문 자체에 대한 만족도가 높았습니다.

어고의 반지들은 전반적으로 과하지 않고, 일상에서 자연스럽게 착용하기 좋은 디자인이 많았습니다. 직접 착용해보니 사진으로 봤을 때보다 손에 올라갔을 때의 분위기가 더 잘 느껴졌고, 은은한 디테일들이 눈에 들어오더라고요. 이런 부분은 역시 오프라인에서 직접 보는 게 좋다는 생각이 들었습니다.

기념일이나 특별한 날을 앞두고 있다면, 압구정 현대백화점 팝업을 한 번 방문해보는 것도 괜찮을 것 같아요. 직접 착용해보고 비교해볼 수 있어서 여자친구선물을 고민 중인 분들께 특히 도움이 될 것 같았습니다. 전체적으로 편안한 분위기에서 브랜드를 경험할 수 있었던 방문이었어요.

조용하지만 탄탄한 인상을 남긴 팝업이었고, 디자인뿐 아니라 응대까지 포함해 브랜드 이미지가 잘 전달된 공간이었습니다. 의미 있는 여자친구선물이나 기념 아이템을 찾고 있다면, 이런 팝업 방문도 좋은 선택지가 될 수 있을 것 같습니다.







https://www.agojewelry.com/

2024년 1월 13일 토요일

2023년 4월 16일 일요일

테스트 관련 메모

테스트에 대해 헷갈리는 내용이 많음. 테스트 환경을 잘 만드는 것도 중요할 듯 하다.

  • chatGPT

    Java Spring 환경에서 사용할 수 있는 대표적인 테스트 프레임워크는 다음과 같습니다.
    
    JUnit: Java 언어로 작성된 단위 테스트 프레임워크입니다. Spring에서도 가장 많이 사용되는 테스트 프레임워크 중 하나입니다.
    
    Mockito: Mock 객체를 생성하고 관리하는 라이브러리로, Spring에서 유닛 테스트를 작성할 때 자주 사용됩니다.
    
    Spring Test: Spring Framework에서 제공하는 테스트 프레임워크입니다. Spring 애플리케이션의 통합 테스트, 웹 애플리케이션의 테스트 등을 작성할 때 사용됩니다.
    
    TestNG: JUnit과 유사한 기능을 제공하는 테스트 프레임워크로, Spring에서도 사용 가능합니다. 테스트 그룹, 의존성 설정 등 다양한 기능을 제공합니다.
    
    Selenium: 웹 애플리케이션의 UI 테스트를 자동화하는 데 사용되는 프레임워크입니다. Spring에서도 웹 애플리케이션의 UI 테스트를 작성할 때 사용됩니다.
    
    Cucumber: BDD(Behavior Driven Development)를 지원하는 테스트 프레임워크입니다. 비즈니스 요구사항을 기반으로 시나리오를 작성하고, 이를 테스트 코드로 변환하는 방식으로 작성합니다.
    
    Arquillian: Java EE 애플리케이션의 통합 테스트를 위한 프레임워크입니다. Spring과 함께 사용할 수 있습니다.
    
    이외에도 다양한 테스트 프레임워크들이 있지만, 위에 소개한 프레임워크들은 Spring 환경에서 가장 많이 사용되는 대표적인 프레임워크들입니다.

용어 정리

  • 자세한 사용법은 패스. 모두 다양한 사용방법을 제공함

Junit

  • 유닛 테스트 프레임워크

  • 흔히 사용하는 @Test

  • 스프링 부트 2.2.x 이상 부터는 Junit5

  • Junit5 는 세 가지 모듈로 구성

    • Junit Platform
    • Junit Jupiter
    • Junit Vintage : Junit4 등 이전 버전의 테스트를 실행할 수 있도록 지원
    • import org.junit.jupiter.api.Assertions;
      import org.junit.jupiter.api.Test;

      class JunitTest {

      @Test
      void test1() {
      String a = "1";
      int number = 1;

      Assertions.assertEquals(Integer.parseInt(a), number);
      }
      }

Mockito

  • 단위테스트를 작성할 때, 모든 의존성을 직접 만들지 않고도 코드를 테스트할 수 있도록 도와줌

    • Mockito 를 이용하여 실제 객체를 모방한 가짜 객체 생성이 가능함
    • Stubbing 을 통해 mock 객체의 메소드를 실행했을 때 어떤 리턴 값을 리턴할 지 정의 가능
    • import org.junit.jupiter.api.Assertions;
      import org.junit.jupiter.api.Test;

      import static org.mockito.ArgumentMatchers.any;
      import static org.mockito.Mockito.*;

      class MockitoTest {
      /**
      * 예시를 위한 테스트
      * 이 테스트에서는 db 에 저장되는 로직이 아니라 다른 로직이 동작하는 지 확인하고 싶음
      * service 의 save 가 호출되면 count 가 증가된다. 이 count 만 정상 동작하는 지 확인하고 싶음
      */
      @Test
      void memberService_count_test() {
      Member member = new Member("name");
      MemberRepository memberRepository = mock(MemberRepository.class);
      when(memberRepository.save(any())).thenReturn(member);

      // mock repository 주입
      MemberService memberService = new MemberService(memberRepository);

      MemberDto dto = new MemberDto("name");
      MemberDto dto2 = new MemberDto("name2");

      Member saveMember = memberService.save(dto);
      Member saveMember2 = memberService.save(dto2);

      // dto2 에 name2 를 저장했지만, mock repository 에 의해 name 가 됨
      Assertions.assertEquals(saveMember.getName(), dto.getName());
      Assertions.assertEquals(saveMember2.getName(), dto.getName());

      // mock 객체에 대해서 발생한 동작에 대해서 확인 가능
      verify(memberRepository, times(2)).save(any());

      // save 를 호출하였을 때 count 가 증가되는 지 확인한다
      Assertions.assertEquals(memberService.getCount(), 2);
      }
      }
    • // 어노테이션으로 사용
      @ExtendWith(MockitoExtension.class)
      class MockitoTest2 {

      @Mock
      private MemberRepository memberRepository;

      @InjectMocks
      private MemberService memberService;

      /**
      * 예시를 위한 테스트
      * 이 테스트에서는 db 에 저장되는 로직이 아니라 다른 로직이 동작하는 지 확인하고 싶음
      * service 의 save 가 호출되면 count 가 증가된다. 이 count 만 정상 동작하는 지 확인하고 싶음
      */
      @Test
      void memberService_count_test() {
      Member member = new Member("name");
      when(memberRepository.save(any())).thenReturn(member);

      MemberDto dto = new MemberDto("name");
      MemberDto dto2 = new MemberDto("name2");

      Member saveMember = memberService.save(dto);
      Member saveMember2 = memberService.save(dto2);

      // dto2 에 name2 를 저장했지만, mock repository 에 의해 name 가 됨
      Assertions.assertEquals(saveMember.getName(), dto.getName());
      Assertions.assertEquals(saveMember2.getName(), dto.getName());

      // mock 객체에 대해서 발생한 동작에 대해서 확인 가능
      verify(memberRepository, times(2)).save(any());

      // save 를 호출하였을 때 count 가 증가되는 지 확인한다
      Assertions.assertEquals(memberService.getCount(), 2);
      }
      }

@SpringBootTest / @WebMvcTest

  • @SpringBootTest

    • 통합테스트 느낌
    • 프로젝트 내부에 있는 스프링 빈을 모두 등록하여 테스트에 필요한 의존성 추가
    • 실제로 구동되는 애플리케이션과 거의 동일한 환경을 제공
    • 단위 테스트가 아닌 Spring Framework 에서 전체적으로 Flow가 제대로 동작하는지 검증하기 위해 사용
    • 애플리케이션의 모든 계층(데이터베이스, 서비스, 컨트롤러 등)을 테스트할 수 있음
    • 시간이 오래걸림
    • // 많이 보이는 테스트로 그냥 reference 예제 사용
      @SpringBootTest
      @AutoConfigureMockMvc
      class MyMockMvcTests {
      // If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient
      @Test
      fun testWithWebTestClient(@Autowired webClient: WebTestClient) {
      webClient
      .get().uri("/")
      .exchange()
      .expectStatus().isOk
      .expectBody<String>().isEqualTo("Hello World")
      }

      }
  • @WebMvcTest

    • Controller 클래스를 테스트하기 위한 어노테이션
    • 애플리케이션의 다른 구성요소(service, repository) 를 모두 로드하기 않고도 특정 컨트롤러를 테스트할 수 있도록 함
    • 시간이 짧고, 불필요한 의존성을 로드하지 않음
    • // 많이 보이는 테스트로 그냥 reference 예제 사용
      @WebMvcTest(UserVehicleController.class)
      class MyControllerTests {

      @Autowired
      private MockMvc mvc;

      @MockBean
      private UserVehicleService userVehicleService;

      @Test
      void testExample() throws Exception {
      given(this.userVehicleService.getVehicleDetails("sboot"))
      .willReturn(new VehicleDetails("Honda", "Civic"));
      this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
      .andExpect(status().isOk())
      .andExpect(content().string("Honda Civic"));
      }

      }

WireMock

  • HTTP 기반의 웹 서비스를 mock 하는 도구
    • 특정한 HTTP 요청에 대한 응답으로 미리 지정해둔 형태의 HTTP 응답이 반환됨
  • chatGPT : WireMock은 언제 사용하면 좋을까?

    WireMock은 주로 다음과 같은 상황에서 사용됩니다.
    
    1. 외부 API 호출 테스트
    API를 사용하는 애플리케이션을 개발할 때, 외부 API 호출을 테스트하기 위해서는 외부 API 서버가 필요합니다. 그러나 외부 API 서버가 불안정하거나, 인증이 필요한 경우에는 테스트를 진행하기 어렵습니다. 이런 경우에는 WireMock을 사용하여 가짜 응답을 생성할 수 있습니다.
    
    2. 마이크로서비스 아키텍처에서의 서비스 통합 테스트
    마이크로서비스 아키텍처에서는 각각의 서비스가 독립적으로 개발되고, 운영됩니다. 이때 서비스 간의 통합 테스트를 진행해야 합니다. WireMock은 마이크로서비스 아키텍처에서 서비스 통합 테스트를 위한 훌륭한 도구입니다.
    
    3. 데모 시스템 구축
    WireMock은 미리 정의된 응답을 가지고 있기 때문에, 데모 시스템을 구축할 때 유용합니다. 예를 들어, WireMock을 사용하여 가짜 결제 시스템을 만들고, 이를 사용하여 결제 처리 흐름을 시뮬레이션할 수 있습니다.
    
    4. 테스트 용이성 개선
    WireMock을 사용하면 테스트 코드를 더욱 쉽게 작성할 수 있습니다. 예를 들어, WireMock을 사용하여 가짜 서버를 띄우고, 이를 이용하여 HTTP 요청을 보내는 코드를 작성할 수 있습니다. 이렇게 작성된 테스트 코드는 외부 서버와의 의존성을 제거할 수 있습니다.
  • 이를 활용하여 외부 서버 통신이 필요한 로직의 테스트의 경우

    • 아래와 같이 url, request, response 를 미리 정의하여 해당 값으로 테스트할 수 있음
    • 외부데이터와의 의존도를 낮출 수 있음
  • json 형태를 미리 정의하거나 WireMock.stubFor 을 통해서도 정의 가능

  • // test/resources/mappings/test.json
    {
    "request" : {
    "url" : "/request",
    "method" : "GET"
    },
    "response" : {
    "status" : 200,
    "body" : "Hello Response1",
    "headers" : {
    "Content-Type" : "text/plain"
    }
    }
    }
  • import com.github.tomakehurst.wiremock.client.WireMock;
    import org.junit.jupiter.api.Assertions;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
    import org.springframework.http.HttpStatus;
    import org.springframework.test.context.ActiveProfiles;
    import org.springframework.web.client.RestTemplate;

    import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
    import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;


    @SpringBootTest
    @ActiveProfiles("test")
    @AutoConfigureWireMock(port = 9999)
    class WireMockTest {

    private RestTemplate restTemplate = new RestTemplate();

    @Test
    void test_json() {
    String body = "Hello Response1";

    String response = restTemplate.getForObject("http://localhost:9999/request", String.class);

    Assertions.assertEquals(response, body);
    System.out.println(response);
    }

    @Test
    void test_wireMock_method() {
    String body = "Hello Response2";
    stubFor(WireMock.get(urlEqualTo("/request2"))
    .willReturn(
    WireMock.aResponse()
    .withStatus(HttpStatus.OK.value())
    .withHeader("Content-Type", "application/json")
    .withBody("Hello Response2")
    ));

    String response = restTemplate.getForObject("http://localhost:9999/request2", String.class);

    Assertions.assertEquals(response, body);
    }
    }


Testcontainers

  • 테스트 시에 도커 컨테이너를 이용하여 필요한 외부 시스템을 쉽게 생성하고 테스트하는 라이브러리
    • 데이터베이스나 메시지 큐 같은 외부 시스템을 테스트할 때 활용 가능
    • docker-compose 도 사용 가능!
    • 테스트 시에만 도커 컨테이너가 띄워진다고 생각하면 될 듯
    • 하지만 띄우는 시간이 걸림. 테스트가 느려짐. 적절한 용도에 사용하자

// build.gradle
...
testImplementation "org.testcontainers:testcontainers:1.18.0"
testImplementation "org.testcontainers:junit-jupiter:1.18.0"
testImplementation "org.testcontainers:mariadb:1.18.0"
...

// application-test
...
datasource:
url: jdbc:tc:mariadb:10://to-do-list-test
username: admin
password: 1234
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
...

import org.testcontainers.containers.DockerComposeContainer;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.io.File;


@Testcontainers
public class TestContainer {

@Container
public static JdbcDatabaseContainer mariaDBContainer = new org.testcontainers.containers.MariaDBContainer("mariadb:10")
.withDatabaseName("to-do-list-test")
.withUsername("admin")
.withPassword("1234")
//.withConfigurationOverride("conf.d") // DB 서버 추가 설정
.withInitScript("testContainer/initData.sql") // 초기 데이터
;


@Container
static DockerComposeContainer dockerComposeContainer =
new DockerComposeContainer(new File("src/test/resources/testContainer/docker-compose.yml"));

}

(테스트 실행 시 docker ps / docker-compose 에 redis-latest 해놓음 )


참고