Home > Today I Learned > 2026_05_11~2026_05_15 > 2026-05-12 일지

2026-05-12 일지
web spring jpa hibernate TDD

테스트를 위한 환경구축


서론

이후 과정에서 배울 CI/CD 과정을 거치면 문제가 있는 코드를 메인 브랜치에 들어가지 못하게 하는 것이다. 또한 슬라이스 테스트를 하게 되면 DB가 필요한데 테스트는 가상의 컴퓨터에서 실행한다고 가정하기 때문에 DB가 없으면 무조건 실패를 하게 된다.

자바에서는 간단한 테스트, 메모리에서 실행, 설치 필요없음과 같은 이유로 H2를 테스트시 사용하는 데이터베이스로 많이 사용하게 된다. 스프링 부트 프로젝트에서는 운영용으로 MySQL, PostgreSQL등을 사용한다고 하면 테스트용으로 따로 H2를 설정해서 사용하는 것이 가능하다.

또한 테스트는 언제, 어디서 실행해도 같은 결과가 나와야 하기 때문에 운영DB에서 테스트를 실행하는 것보다는 테스트용DB, Mock객체등을 활용하여 구현하는 것이 조금 더 바람직한 방향이 될 것이다.

auto-configuration


Spring Boot가 개발자가 Bean을 하나하나 등록하지 않아도 프로젝트에 추가된 의존성과 설정파일을 보고 필요한 설정을 자동으로 구성해주는 기능이다.

Spring boot에서는 외부에서 주입되는 application.yml을 통해 DB, 서버 포트, JPA 옵션과 같은 설정을 관리할 수가 있다. 이처럼 외부에서 필요한 설정을 가져오는 것을 Externalized Configuration이라고 한다.

Spring Boot에서는 여러 설정소스를 정해진 우선순위에 따라 읽고, 더 높은 우선순위의 설정이 낮은 우선순위의 설정을 덮어쓴다.

hibernate ddl-auto 설정

Entity 클래스와 DB 스키마를 비교해서, 애플리케이션 실행시 DDL을 어떻게 처리할 지 정하는 옵션이다.

 jpa:
    hibernate:
      ddl-auto: create #update # create-drop #create #validate

validate

애플리케이션 실행 시 Entity 클래스와 DB 테이블 구조가 맞는지 검사만 한다.
Entity와 DB 스키마가 일치하는지 확인만 하고, DB 구조는 절대 변경하지 않는다.
운영 환경에서 사용할 수 있는 비교적 안전한 옵션이라고 볼 수가 있다.

update

Entity 클래스와 DB 스키마를 비교해서 부족한 테이블이나 컬럼을 자동으로 추가/수정하려고 시도한다. 우리 entity 클래스에 맞추는 쪽으로 구현된다.
예를 들어 Entity에 새로운 필드를 추가하면 DB에 컬럼이 추가될 수 있다.

다만 다음과 같은 단점이 존재한다.

  • 기존 컬럼을 지우지 않고 새 컬럼을 추가하면 default 설정을 해주지 않으면 불필요한 null 컬럼이 생길 수 있다.
  • 이때문에 데이터 마이그레이션을 세밀하게 제어하기 어렵다

따라서 로컬에서는 사용가능하나 개발서버 이상에서 사용하기는 힘들다

create

애플리케이션이 시작될 때 기존의 테이블을 삭제하고 다시 생성한다.
즉 실행할 때마다 DB 구조가 초기화된다.

create-drop

create와 비슷하지만 애플리케이션 종료시점에 테이블을 다시 삭제한다.

none

Hibernate가 자동으로 DDL을 처리하지 않게한다.

정리

개발서버에는 어디까지 허용될까?
create-drop, create를 사용하면 입력한 데이터가 날아가기 때문에 사용하면 안된다고 볼 수가 있다.
다만 개발서버와 로컬은 다르다. 개발서버는 내부에 배포가 되어있기 때문에 update가 적당한 것 같다. 다만 update를 하면 null 컬럼이 많이 생기므로 기본값을 넣은 상태로 진행하던가 서비스를 적용하기전에 스키마를 보고 적용하는 것을 하게 되는데 업데이트에서 보수적으로 진행하게 된다.

운영은 none이 적당하고 개발 서버는 update까지 적당하면 로컬에서는 다 해도 괜찮다고 볼 수 있다.

주의
spring boot starter 사이트에서 다운받아 압축을 풀어서 spring boot를 시작하는 경우 파일에 iso로 인코딩이 들어가서 yml 파일에 한글이 깨지는 경우가 있다.

entity 설정


저번 hibernate와 JPA를 배웠을 때와 유사하게 전개를 하게 된다.
먼저 필요한 접근설정, id에 관해 설정을 한뒤 필요한 attribute를 넣어주게 된다.

equals와 HashCode()사용 (동등성 검사)

자바에서는 기본적으로 메모리 주소를 비교하여 객체를 비교를 하게 되는데

동등성 검사를 위한 equals()
두 객체가 동일한 의미를 갖는지 여부를 나타낸다. 두 객체가 동등하다는 것은 그들의 상태가 동일하다는 것을 의미한다.
equal()를 사용하여 같은 DB데이터를 같은 객체로 판단하기 위해 사용한다.

java에서는 equals가 true이면 hashCode()로 나오는 해쉬값도 같아야 하기 때문에 같이 쓰는 것을 원칙으로 한다.
그리고 hashCode()에는 매개변수가 오면 안된다. Entity 저장전에 id가 없고 hashCode()가 변하면 collection이 깨질 수가 있기 때문이다.

외래키를 위한 별도의 attribute 설정

Pk인 id가 아닌 code라는 별도의 attribute를 통해 이번 실습에서 DB를 구현했다.

Order  
id code
1 ORD-20260512-001
OrderItem  
id order_code
10 ORD-20260512-001

이렇게 구현할 때의 장점

  • 비지니스 식별자가 명확하다
    코드를 보면 대략적인 정보를 알 수가 있다.

  • 외부 시스템 연동이 쉬워진다.
    시스템간 PK 충돌 가능성이 감소하고 로그 추적이 쉬위지는 장점이 존재한다.

  • Pk 전략 변경과 분리가능
    내부 DB의 전략이 바뀌더라도 외부에 보여지는 것은 code이기 때문에 관리하기 쉽다.

이는 외부에서 쉽게 ID를 유추하기 힘들게 하는 장점이 존재한다. 그리고 DDD(도메인 주도 설계)에 가까워졌다고 볼 수가 있다.

단점

  • FK성능이 PK보다 느릴 수가 있다.
    문자열로 구성되어서 인덱스 비용 증가, join 비용 증가등 이슈가 존재한다.
  • code의 변경이 위험해진다.
    연관된 FK에 전부 영향이 가고 데이터 무결성이 위험해진다.
  • cascadeType.all을 @ManyToOne에서 하는 경우 위험해 질 수도 있다.
    연결된 다른 테이블의 row까지 삭제할 위험이 존재한다. cascade를 사용하는 경우 OneToMany일 때 두도록 하자.

TDD 실습


통합테스트

여러 객체가 실제로 함께 동작하는지 검증하는 테스트
여러 계층을 실제로 연결해서 테스트를 한다.

  • @SpringBootTest
    실제 Spring Container을 띄운다는 의미
    Bean등록, 의존성 주입, AOP, JPA, 트랜잭션 등이 실제처럼 돌아가게 된다.

빌더패턴의 필요성

 @Test
    void it_will_return_item_including_pk(){
        final String NAME = "상품1";
        final String CODE = UUID.randomUUID().toString();
        final Integer PRICE = 10_000;

        Item item = new Item(CODE, NAME, PRICE);
    }

위 코드는 읽어보면 아무런 위험이 느껴지지 않지만 의미적으로 속성값이 잘못 들어가게 된다.
이는 생성자 방식의 한계로 필드가 많아지면 위험해진다. 하지만 빌더 패턴을 만들면 생성자 인자 순서를 안전하게 만들고, 객체 생성을 명확하게 할 수가 있다.

Item item = Item.builder()
        .code(CODE)
        .name(NAME)
        .price(PRICE)
        .build();

장점

  • 의미가 명확해진다(테스트 코드에서 큰 장점을 가진다.)
  • 순서 실수를 방지할 수가 있다
  • 필드가 많아져도 안전하다
  • 선택적 필드 처리가 가능하다
  • 체이닝, 각 메서드가 자기자신을 return한다.
    • 계속 이어붙이는 것이 가능하다.

AssertJ

java 테스트 코드에서 사용하는 Assertion(검증) 라이브러리이다. 보통 JUnit이랑 함께 사용한다.

기본적인 검증 메서드들

  • isEqualTo
  • isGreaterThan
  • isNotNull
  • isTrue
 assertThat(findItem.getPrice()).isEqualTo(TARGET_PRICE);

일치하면 테스트 성공이 반환 되고 만약 일치하지 않는다면 테스트 실패가 반환된다.

assertThatThrownBy()

예외를 발생시키는 검증, 이 코드를 실행했을 때 특정 예외가 발생하는지 검증하는 AssertJ함수

assertThatThrownBy(() ->repository.updatePrice(INVALID_CODE, TARGET_PRICE))
                .isInstanceOf(NullPointerException.class);

람다 식과 함께 활용을 하게 되는데 뒤의 isInstanceOf()는 발생해야 할 오류 종류를 넣어주게 된다.

테스트에서는 실패 상황에 관해 검증을 할 필요가 있기 때문에 사용한다.

테스트 생명주기 어노테이션

보통 테스트 준비, 초기화, 정리 작업에 사용

  • BeforeAll : 모든 테스트가 실행하기 전
  • BeforeEach : 각각의 테스트가 실행하기 전
  • AfterAll : 모든 테스트가 실행한 후
  • AfterEach : 각각의 테스트가 실행한 후

BeforeAll, AfterAll은 객체가 생성되기 이전에 실행되고 static에만 사용할 수 있다

테스트 커버리지

내 코드가 테스트에 의해 얼마나 실행되었는가를 수치로 나타낸 것

  • Line Coverage
    코드 한 줄이 실행되었는가
  • Branch Coverage
    if/else 모두 테스트 되었는지

비지니스 로직의 핵심부분을 구현한 서비스 계층에서 측정하는 것이 좋을 것 같다.

참고

값을 가져올때 단순한 하나의 값이 아닌 여러 개의 값을 가져와야 하는 경우가 있다. 이 객경우 대량의 쿼리를 날리는데 이때 bulk 쿼리를 사용해서 가져오도록 한다.

bulk 쿼리는 배치작업으로 수행해서 전부 다 완료되면 commit을 하고 만약 하나라도 실패하면 rollback을 시켜 안정성을 확보하는 것도 하나의 방법이 될 수가 있다.

참고
관리자 모듈을 만드는 것도 연습해봐야 한다. 웹을 구현할 때 관리자 모듈을 통해 통계를 해주는 것이 골치아프다.
관리자 모듈을 만들 때 어느페이지에서 유저별로 특정 사항에 관한 조회, 통계, 집계 쿼리를 짜야한다. 그리고 각 부분에 대해 어디서부터 잘못되었는지에 대해 인지를 할 필요가 있다.
그리고 각 요구사항에 대해 쿼리를 짜는것이 도움이 된다.