본문 바로가기
이슈와해결

입사 0년차 주니어의 첫 운영 배포와 실수 경험

by Renechoi 2023. 12. 14.

목차

  • 개요 및 사건 요약
  • 첫 번째 문제
  • 두 번째 문제
  • 세 번째 문제
  • 결론
  • 회고 및 제안

 

개요 및 사건 요약

 

한 서비스에 기능 고도화를 개발하고 배포한 일이 있었습니다. 서비스 배포 과정에서 발생한 에러에 대해 원인을 파악하고 해결한 경험을 기록하여 공유해보려고 합니다.

 

용어나 코드는 실제의 것으로 작성하지 않고 컨셉으로 대체하였습니다.

  • 대상 서비스: 서비스A
  • 배포 내용: 유니버설 템플릿 적용을 통한 주요 기능 변경 및 추가 로직 도입
  • 일시: 최근 날짜 (예: 2023년 11월 8일 10:30부터 12:30까지)
  • 발생한 문제: 실제 운영 환경에서의 스프링 부트 어플리케이션 실행 실패, 주요 API 기능 제공 불가
  • 결론: 3가지 주요 실패 원인 분석

 

첫 번째 문제

첫 번째 시도에서 만난 에러 메시지는 다음과 같습니다. 이 에러로 빌드 자체가 안되었는데요. 

에러 메시지

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/JpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build JPA SessionFactory; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: missing column [column_name] in table [table_name]

 

원인 및 해결

 

해당 에러의 원인은 생성 쿼리의 문제였습니다. 스테이지 db의 생성 쿼리를 다음과 같이 가져왔는데 해당 쿼리에서 케이스 컨벤션을 확인하지 못한 부분이 문제였습니다.

 

ADD COLUMN isDeleted BOOLEAN DEFAULT FALSE COMMENT '삭제 여부';

 

따라서 해당 부분은 isDeleted 로 작성된 칼럼명을 스네이크 케이스로 변경하여 해결할 수 있었습니다.

 

두 번째 문제

가벼운 문제인 줄 알고 안도했는데 문제는 해당 부분을 변경한 이후에도 다음과 같은 에러가 발생합니다.

 

에러 메시지

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaConfiguration.class]: Invocation of init method failed; nested exception is javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is org.hibernate.tool.schema.spi.SchemaManagementException: Schema-validation: wrong column type encountered in column [is_deleted] in table [sample_table]; found [bit (Types#BIT)], but expecting [boolean comment '삭제 여부' (Types#BOOLEAN)]

 

원인 및 해결

에러의 내용은 칼럼 스키마 유효성 검증에서 발생한 에러로, 메시지의 내용에 따르면 칼럼이 데이터베이스에서 bit로 발견되었지만 Hibernateboolean 타입을 기대한다는 점입니다. isDelted 필드의 @Column 어노테이션에서 정의된 columnDefinition이 다음과 같이Boolean으로 설정되어 있었기 때문입니다.

 

@Column(columnDefinition = "BOOLEAN COMMENT '삭제 여부'")
private Boolean isDeleted;

 

해결 방법으로 수행한 내용은

1) 필드의 타입을 Boolean -> booleanprimitive 타입으로 변경하고
2) @Column 어노테이션에서 Boolean타입을 명시하는 내용을 지운 뒤
3) 로컬에서 ddl-auto 설정을 update를 통해 Hibernate가 테이블 및 칼럼을 생성하도록 하여 db의 isDeleted 칼럼의 타입 속성이 TINYINT가 아닌 bit로 생성되는 것을 확인한 뒤, ddl-auto 설정을 다시 validate로 바꾸어

 

배포하였습니다.

 

이와 같은 방식으로 이번에는 어플리케이션 정상 구동에 성공합니다.

 

세 번째 문제

위에서 변경한 방식으로 문제가 해결되고 정상 배포될 것을 기대했는데 또 다른 문제가 발생합니다.

에러 메시지

11:14:24.832 [ INFO] [Thread-1] [com.example.log.impl.LogTrace] - [111f77f6] |   |<GenericFacade.search(..) time=253ms ex=org.springframework.orm.jpa.JpaSystemException: Null value was assigned to a property [class com.example.domain.model.Entity.isFlag] of primitive type setter of com.example.domain.model.Entity.isFlag; nested exception is org.hibernate.PropertyAccessException: Null value was assigned to a property [class com.example.domain.model.Entity.isFlag] of primitive type setter of com.example.domain.model.Entity.isFlag

 

원인 및 해결

이번의 문제는 정상 배포는 되었는데 api 요청 발생시 정상 처리를 하지 못하는 문제였습니다.

 

해당 메시지에 따르면 org.hibernate.PropertyAccessExceptionSample엔티티의 isDeleted 속성에 null 값이 할당되면서 발생한 에러입니다.

 

위의 두 번째 문제를 해결하는 과정에서 속성을 원시 타입으로 변경하였는데 원시 타입인 필드에 기존의 저장되어 있던 null 값이 할당되려고 하면서 발생한 문제였습니다.

 

따라서 위에서 해결책으로 커밋한 원시타입 변경을 다시 래퍼 클래스인 Boolean으로 변경해주어 해결할 수 있었습니다.

결론

첫 번째 문제는 단순한 휴먼 에러로 쿼리를 작성하는 부분에서 실수한 문제였습니다.

 

두 번째 문제와 세 번째 문제는 결론적으로는 엔티티의 불리언 타입의 속성에 대해 @ColumncolumnDefinitionBoolean으로 명시하는 데서 발생한 문제였습니다. 문제를 해결하면서 추론했던 부분 중 하나는 MySql에서 사용하는 boolean 속성으로 TINYINT(1)이 아니라 bit여야 한다는 부분이었는데(에러 메시지를 그대로 해석한 내용이면서 두 번째 해결 방식으로 처리한 부분), 좀 더 리서치를 해보니 bit 혹은 TINYINT(1) 이 문제가 아니라 Hibernate가 매핑하는 과정에서 columnDefinitionBoolean으로 설정한 것이 문제였던 것입니다.

 

해당 부분이 문제가 되는 부분은 Hibernate는 MySQL에서 boolean 타입을 TINYINT(1)로 다루지만, columnDefinition에서 "Boolean"으로 지정하면, Hibernate가 이를 매핑하지 못하기 때문이었습니다.

 

이에 대해 Hibernate에서는 별도의 Boolean Converters를 사용하는 방식으로 해결하기도 합니다.

 

@Convert(converter = TrueFalseConverter.class)
private Boolean shouldBeAsked;

 

아무튼 불리언 타입의 속성을 설정할 때는 columnDefinition을 아예 정의를 하지 않거나 정의를 할 것이라면 다음과 같이 TINYINT 혹은 bit으로 생성된 칼럼 타입에 맞게 정의해주어야 합니다.

 

@Column(columnDefinition = "TINYINT(1) COMMENT '수정자 아이디'")
private Boolean isOk;

 

회고 및 제안

위와 같은 에러를 겪으며 느낀점을 다음과 같이 정리해볼 수 있었습니다.

1. 운영 배포시 쿼리 및 db 부분 체크에 보다 섬세함 & 꼼꼼함이 필요하겠다.

두 번째 문제는 차치하더라도 첫 번째 문제는 쿼리를 조금만 더 꼼꼼히 봤더라면 발생하지 않았을 문제이기 때문입니다.

 

2. 왜 단위 테스트에서 잡아내지 못했을까 ?

세 번째 문제는 두 번째 문제보다 당황스러웠는데, 급한 와중에 고쳤다 하더라도 테스트 코드가 돌 때 어째서 해당 문제를 잡아내지 못했느냐에 대한 의심과 이에 대한 좌절감을 느꼈습니다. 예를 들어 다음과 같은 컨셉의 영속 계층 IO 테스트가 있었다면 해당 문제를 컴파일 시점에 발견할 수 있었을 것입니다.

 

@Test
public void whenAssignNullToPrimitive_thenThrowException() {
    // 추상화된 템플릿 객체 생성
    GenericTemplate template = new GenericTemplate();
    Exception exception = assertDoesNotThrows(GenericDataAccessException.class, () -> {
    // 추상화된 객체의 필드를 의미 있는 값으로 설정
        template.setSomeField(null);
        genericRepository.save(template);
    });
}

 

운영 상황에서의 여러가지 케이스를 고려한 보다 더 꼼꼼한 테스트 코드 작성이 필요하다는 생각을 했습니다.

 

3. Stage에서 자체 테스트를 해볼 때 실제 운영상황과 최대한 비슷한 환경을 만들고 진행할 필요가 있겠다.

예를 들어, hibernate.ddl-autoStage에서는 update로 사용하였는데 운영 환경에서와 같이 validate로 했다면 배포 전에 문제를 확인할 수 있었을 것입니다.

 

데이터베이스에 영속화되어 있는 데이터들 그리고 테스트 해볼 때 사용하는 이름 등 (e.g. 일반 배달료: 배민 회원 - 3000.0원) 최대한 운영환경과 비슷한 환경을 조성하고 테스트 케이스를 검증해보는 것이 필요하겠다는 생각을 했습니다.

 

 

 

 


 

 

개발에서 문제라는 게 막상 부딪힐 때는 커보이는데 막상 지나고 나면 별 거 아닌 경우가 많은 것 같습니다. 개발 중 만나는 오류는 무심코 지나가며 그만인데 배포시엔 그게 아니다 보니 긴장되고 그러다보니 정말 별 것 아닌데도 되게 크게 다가왔던 기억이 있습니다. 물론 방어 장치들이 있지만 혹여라도 잘못되면 비즈니스 장애로 이어지고 그게 금전적인 손실로 나타날 수 있으니까요. 배포시에 겪은 문제들을 또 다시 겪지 않는 것이 중요하다고 생각해 기록해둔 내용을 공유해보았습니다.

 

 


 

reference

반응형