공통 주문과 전용 실험 데이터를 나눴습니다
유럽법인에 Nanopore Sequencing 서비스를 새로 만들 때 본사에는 그대로 옮길 수 있는 운영 사례가 없었습니다. 비슷한 실험 흐름을 가진 Fragment Analysis가 참고점은 될 수 있었지만, Whole Plasmid와 16S 서비스의 입력 항목, Sample Condition, 부가서비스, 결과 제공 방식은 달랐습니다.
기존 테이블과 화면에 조건문만 계속 추가하면 새 서비스의 상태가 다른 서비스 로직에 섞입니다. 주문부터 결과까지 모두 새로 만들면 주문번호, 법인 구분, 등록·수정 이력처럼 이미 검증된 공통 기능까지 중복됩니다. 다음 원칙으로 재사용할 데이터와 분리할 데이터를 정했습니다.
- 주문의 공통 식별자와 서비스 선택값은 기존 주문 구조를 확장해 사용합니다.
- 샘플, 반응, 플레이트와 실험 배열은 Nanopore 전용 모델로 분리합니다.
- 임시저장 데이터와 접수 이후의 생산 데이터를 구분합니다.
- 현재 상태만 남기지 않고 반응 상태와 샘플 변경 이력을 별도로 적재합니다.
전용 테이블 8개의 책임
실제 구현에서는 전용 테이블 8개를 사용했습니다. 아래 이름은 내부 식별자를 드러내지 않도록 줄였지만, 테이블을 나눈 기준과 관계는 실제 설계를 따랐습니다.
| 영역 | 테이블 | 맡은 데이터 |
|---|---|---|
| 주문 작성 | sample_temp | 접수 전 샘플 입력값 |
| 주문 작성 | reaction_temp | 접수 전 반응과 보류·취소 정보 |
| 생산 기준 | sample | 접수된 샘플의 이름, 상태, 농도, 보관 정보 |
| 생산 기준 | reaction | 주문별 반응 번호, 진행 상태, 보류·취소 사유 |
| 실험 묶음 | plate | 플레이트 상태, 작업자, 시작·완료 시점 |
| 실험 배치 | reaction_array | 반응과 플레이트 Well의 연결 및 실험 순서 |
| 상태 감사 | reaction_status_history | 반응 상태와 보류 상태가 바뀐 시점 |
| 변경 감사 | sample_change_history | 샘플 값의 변경 전·후 값과 변경 화면 |
샘플과 반응을 한 행에 합치지 않은 이유는 재사용 단위가 달랐기 때문입니다. 샘플은 물리적인 시료의 속성과 보관 정보를 가지지만, 반응은 특정 주문 안에서 진행 상태와 보류·취소 사유를 가집니다. 플레이트도 반응의 단순 속성이 아닙니다. 여러 반응을 한 작업 단위로 묶고 각 반응의 Well 좌표와 순서를 따로 배치해야 했습니다.
임시저장과 생산 데이터를 분리했습니다
주문 작성 중인 값은 아직 생산 기준 데이터가 아닙니다. Excel 일괄 업로드 뒤 사용자가 값을 수정하거나 주문 자체를 취소할 수 있고, 접수되지 않은 행이 Plating 대상에 섞여서도 안 됩니다.
그래서 임시저장 단계에서는 sample_temp와 reaction_temp를 조회하고, 접수 시점에 검증을 통과한 값만 본 테이블로 옮겼습니다. 실제 코드에도 조회 대상을 TEMP, MAIN, BOTH로 구분하는 값이 있었습니다. 보류 화면처럼 접수 전후의 반응을 함께 찾아야 하는 기능만 두 영역을 합쳐 조회했습니다.
public enum ReactionDataScope {
BOTH,
TEMP,
MAIN
}
List<Reaction> findReactions(OrderKey order, ReactionDataScope scope);이 코드는 실제 구현의 조회 구분을 공개용 이름으로 축약한 예시입니다. 임시 테이블은 주문 접수 전에 수정하거나 취소할 수 있는 데이터를 맡았습니다.
주문 상태와 반응 상태는 같은 값이 아닙니다
주문 하나에는 여러 반응이 들어갈 수 있습니다. 일부 반응이 보류돼도 나머지는 Plating으로 진행할 수 있고, 모든 반응이 취소되면 주문 상태도 취소로 바뀌어야 합니다. 주문 상태 하나만으로 이 차이를 표현하면 부분 진행과 예외 처리를 잃게 됩니다.
반응 상태는 실제 구현에서 다음 순서로 바뀌었습니다. 코드값은 제외하고 의미가 드러나는 이름만 남겼습니다.
public enum ReactionStatus {
PENDING,
WAITING_PLATING,
PLATING_COMPLETE,
FINAL_COMPLETE,
HOLD,
CANCEL
}Plating이 끝났다고 실험까지 끝난 것은 아니므로 PLATING_COMPLETE와 FINAL_COMPLETE를 나눴습니다. HOLD와 CANCEL도 진행 단계와 별도로 다뤘습니다. 보류 해제 시에는 화면이 기억한 이전 상태가 아니라 저장된 반응 상태를 기준으로 복구하고, 그 결과를 이력에 남기도록 했습니다.
플레이트 완료와 반응 완료를 함께 기록했습니다
Rxn Sheet에서 실험 완료를 저장할 때는 플레이트만 완료 처리하면 안 됩니다. 플레이트에 배치된 각 반응 상태와 실험 배열의 입력값도 함께 바뀌어야 합니다. 실제 서비스의 책임을 줄이면 다음 순서입니다.
void completeExperiment(PlateWork work, List<ArrayInput> inputs) {
Plate plate = plateRepository.get(work.plateId());
validateCompletable(plate);
for (Reaction reaction : reactionRepository.findByPlate(plate.id())) {
reaction.changeStatus(ReactionStatus.FINAL_COMPLETE);
reactionRepository.updateStatus(reaction);
reactionHistoryRepository.insert(reaction.snapshot());
}
for (ArrayInput input : inputs) {
reactionArrayRepository.updateResult(input);
}
plate.complete(work.operator(), work.completedAt());
plateRepository.update(plate);
}내부 클래스와 메서드명은 일반화했지만 반응 상태 변경 → 상태 이력 적재 → 실험 배열 갱신 → 플레이트 완료라는 동작 순서는 실제 구현에 맞췄습니다. 이 메서드는 한 번의 저장에서 무엇을 바꿔야 하는지 보여 주며, 트랜잭션 설정처럼 공개 근거가 부족한 부분까지 확대해 설명하지 않았습니다.
현재값과 이력을 함께 둔 이유
이력 테이블만 조회해 현재 상태를 매번 계산하는 방식도 가능합니다. 하지만 주문 조회, Plating 대상 조회와 완료 조건 검사는 현재 상태를 자주 사용합니다. 그래서 반응 테이블에는 현재 상태를 두고, 변경할 때 같은 상태를 이력에도 추가했습니다.
샘플 변경 이력은 상태 이력과 목적이 달랐습니다. 농도나 보관 정보처럼 값 자체가 바뀌는 항목은 변경 화면, 변경 종류, 변경 전·후 값을 기록했습니다. 반응 상태 이력은 주문·반응·이력 순번과 당시 상태를 저장했습니다. 두 이력을 하나의 범용 문자열 로그에 합치지 않아 조회 목적과 업무 의미를 유지했습니다.
검증과 직접 수행 범위
구현 뒤에는 주문등록, Excel 업로드, 접수, 보류·해제·취소, Plating, Rxn Sheet, Worksheet, 결과 URL과 메일 발송, 주문완료까지 확인했습니다. 영어·프랑스어·이탈리아어 메시지와 유럽법인 QA 피드백도 반영했고, LIMS와 COMS 주문 연계를 함께 테스트한 뒤 2024년 7월 운영에 반영했습니다.
제가 직접 맡은 범위는 요구사항과 데이터·화면 설계, LIMS의 주문·생산·실험·결과 처리, 통합 테스트와 운영 반영입니다. COMS 주문등록은 다른 개발자가 담당했고 서로 주고받는 데이터와 처리 순서를 맞춰 연계했습니다. 이 글은 신규 LIMS 업무 모델과 제가 담당한 구현 범위를 설명합니다.