리눅스에서 alias로 명령어 효율적으로 사용하기

리눅스 작업은 대부분 터미널에서 이뤄집니다. 매번 긴 명령어를 일일이 입력하다 보면 상당히 번거롭고 비효율적이죠.
이럴 때 유용하게 쓸 수 있는 기능이 바로 alias(별칭) 입니다.

특히 자주 이동해야 하는 깊은 디렉토리 경로반복적으로 사용하는 긴 명령어는 alias를 활용해 짧은 별칭으로 등록해두면 작업 효율이 크게 향상됩니다.


alias란?

alias명령어에 별칭을 붙여 짧게 사용할 수 있도록 해주는 쉘 내부 명령어입니다.
복잡하거나 긴 명령어를 간단한 이름으로 바꿔 등록하면, 매번 반복 입력할 필요 없이 짧은 명령어만 입력해도 동일한 작업을 수행할 수 있습니다.


alias 사용법

1) 현재 등록된 alias 확인

# 현재 등록된 별칭 보기
alias

alias 명령어만 입력하면 현재 등록된 별칭 목록을 확인할 수 있습니다.


2) alias 등록

# 형식: alias 별칭='명령어'
alias upapp='docker compose -f /home/user/myproject/docker-compose.yml up -d'

예를 들어 위와 같이 등록하면, upapp 명령어만 입력해도 docker compose 명령어를 실행할 수 있습니다.

주의: 별칭과 명령어 사이의 띄어쓰기를 잘못 입력하면 invalid alias name 오류가 발생할 수 있습니다.


3) alias 해제

# 등록된 별칭 삭제
unalias upapp

등록된 별칭을 삭제하려면 unalias 명령어를 사용하면 됩니다.


alias 영구 등록하기

터미널에서 등록한 alias는 시스템을 재부팅하면 초기화됩니다.
이를 방지하려면 ~/.bashrc 또는 ~/.bash_aliases 파일에 등록해두어야 합니다. 이렇게 하면 재부팅 이후에도 계속 사용할 수 있습니다.

1) .bashrc 파일 열기

# 숨김파일 보기
ls -al

# .bashrc 파일 수정
vi ~/.bashrc

.bashrc는 사용자 홈 디렉토리에 있는 숨김파일입니다. 없으면 새로 생성하면 됩니다.


2) alias 추가

# .bashrc 내부 예시
alias cc='clear'

# Source global definitions
if [ -f /etc/bashrc ]; then
    . /etc/bashrc
fi

원하는 별칭을 .bashrc 파일에 추가해줍니다.


3) 변경사항 적용

source ~/.bashrc

.bashrc를 수정했다고 바로 적용되는 건 아닙니다. source 명령어로 변경사항을 반영해야 합니다.


마무리

alias는 작은 습관이지만 업무 효율을 크게 높여줍니다.
자주 사용하는 긴 명령어나 반복되는 디렉토리 이동은 꼭 alias로 등록해서 귀찮은 반복 타이핑을 줄여봅시다.

과거 실무에서 웹뷰를 사용한 하이브리드앱의 자바스크립트단을 처음으로 간단하게 수정해야 할 일이 있었다.

수정 후에 극소수의 몇몇 사용자들이 수정한 페이지에 접근이 안되는 이슈가 발생했다.

 

처음엔 옵셔널 체이닝 사용이 문제일거라곤 생각도 못해서 다른 부분을 의심하며 방황하다가, 몇몇 사용자들에게만 발생하는점을 근거로 혹시 버전에 따른 지원이슈가 있나? 하여 안드로이드 OS 버전을 의심하게 되었고, 같은 OS 버전임에도 잘 되는 사용자가 있는 반면 안되는 사용자가 있어서 webview 버전까지 의심을 하게 되었다.

 

결국 Chrome webview 가 80 미만이면 옵셔널 체이닝 문법을 지원하지 않는다는걸 알게되었다. 아파치 로그를 확인한 결과 이슈가 발생한 사용자들은 모두 chrome 버전이 80 미만이였다. 사용자들 태블릿에 보안앱이 깔려있어서 크롬 버전 업데이트가 안된 구버전 기기들이 존재했다..!

 

서버단 개발만 하다보니 자바스크립트단에서 문법 지원 이슈를 생각하지 못했다. 설마 안될줄이야..

아무튼 곧바로 수정해서 배포하였고, 잘 마무리 되긴 했다.

 

https://stackoverflow.com/questions/75514114/why-javascript-with-optional-chaining-not-work-in-webview

 

Spring Boot 기반의 웹 애플리케이션에서 @ExceptionHandler를 통해 예외를 처리하던 도중, 간헐적으로 다음과 같은 경고 로그가 발생했다.

 
Failure in @ExceptionHandler ~~~~
 

본 게시글에서는 해당 로그가 발생하게 된 실무 사례를 바탕으로, 발생 원인을 분석하고  대응 방안을 제시한다.


문제 상황 개요

해당 문제는 다음과 같은 구조의 시스템에서 발생하였다.

[Client] → [Load Balancer (timeout: 60s)] → [WAS (Spring Boot)] → [외부 API (timeout: 60s)]
 

클라이언트는 LB를 통해 WAS로 요청을 보내고, WAS는 외부 연계 시스템에 API 요청을 수행한다.

재현된 시나리오

  1. 클라이언트가 API를 요청함.
  2. WAS는 내부에서 외부 API를 호출함.
  3. 외부 API 응답이 60초를 초과하여 지연됨.
  4. LB는 설정된 60초의 timeout에 도달하여 클라이언트와의 커넥션을 강제 종료함.
  5. 이후 WAS에서 외부 API 타임아웃 발생 → ServiceException 발생 → @ExceptionHandler 호출
  6. 하지만 이미 클라이언트 커넥션이 끊긴 상태이므로 응답 전송 중 ClientAbortException 발생
  7. 이 과정에서 다음의 Spring 내부 로그가 출력됨:
Failure in @ExceptionHandler com.~~~.exception.ServiceExceptionHandler#handleServiceException(ServiceException)org.apache.catalina.connector.ClientAbortException: java.io.IOException: Connection reset by peer
 

이는 @ExceptionHandler 메서드에서 ResponseEntity로 클라이언트에 오류 응답을 내려주는 과정에서,
클라이언트가 이미 연결을 종료한 상태이므로 발생한 예외이다.

Spring MVC는 @ExceptionHandler 메서드에서 예외가 발생할 경우
ExceptionHandlerExceptionResolver 클래스 내부에서 해당 상황을 감지하고 다음과 같은 로그를 출력한다.

catch (Throwable invocationEx) {
    if (logger.isWarnEnabled()) {
        logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
    }
    return null;
}

위 소스는 Spring Framework의 ExceptionHandlerExceptionResolver.java 내부에 포함된 코드로,
핸들러 실행 도중 예외가 재발생할 경우, 이를 warn 로그로 출력하고 이후 resolver chain으로 처리를 넘긴다.

 


원인 정리

외부 API 응답 지연 외부 시스템의 응답이 60초를 초과함
LB timeout 설정 LB가 60초 이상 응답이 없자 커넥션 종료
클라이언트 커넥션 끊김 WAS가 응답을 전송하려는 시점에는 클라이언트가 이미 연결을 끊음
예외 핸들러 재예외 발생 @ExceptionHandler 실행 중 ClientAbortException 발생하여 처리 실패
 

대응 방안

외부 API Timeout 설정을 LB보다 짧게 조정

외부 API 호출 시 사용되는 HTTP 클라이언트 (RestTemplate, WebClient, 또는 커스텀 library)에 설정된 timeout 값을
LB timeout보다 작게 설정해야 한다.

ex) LB timeout이 60초라면, 외부 API timeout은 50초 이하로 설정


결론

“Failure in @ExceptionHandler” 로그는 @ExceptionHandler 메서드의 처리 중
다시 예외가 발생했을 때 출력되는 Spring의 내부 경고 로그이다.
이 사례에선 클라이언트가 먼저 커넥션을 종료한 경우에 발생하며, 구조적 타임아웃 설정 불일치가 주요 원인이다.

적절한 timeout 설정과 예외 처리 방식을 적용함으로써, 불필요한 에러 로그를 방지하고 시스템 안정성을 높일 수 있다.

Spring Boot 로그 시스템 정리 – SLF4J, Logback, Log4j2 차이는 뭘까?

Spring Boot 프로젝트를 하다 보면 @Slf4j 써서 로그 찍고, logback-spring.xml 만들어서 설정하고, 가끔 누군가는 @Log4j2를 쓰기도 한다. 나도 Spring을 처음 배울땐 아무 생각 없이 @Slf4j 를 사용하고 있었고, 블로그 글을 보면서 대충 넘겼다. 시간이 많이 지난 지금에서야 내 블로그에 정리해본다.


1. SLF4J: 그저 추상화일 뿐

SLF4J(Simple Logging Facade for Java)는 말 그대로 Facade다. 즉, 진짜 로그를 찍는 애가 아니라, "찍어줘~"라고 부탁만 하는 애다.

개발자는 log.info("hello")라고 쓰면 되고, 실제로 어디에 어떻게 출력할지는 SLF4J 뒤에 있는 **구현체(Logback, Log4j, Log4j2 등)**가 알아서 처리한다.

장점?

  • 코드에서는 SLF4J만 쓰면 되니까, 나중에 로깅 구현체를 바꿔도 코드 손댈 필요가 없다.
  • 유연하고 중립적이다.

2. Logback: Spring Boot 기본값

Spring Boot는 기본적으로 Logback을 로깅 구현체로 쓴다.
이건 명시적으로 선언하지 않아도 spring-boot-starter-web만 넣으면 자동으로 따라온다. 왜냐하면 이 안에 spring-boot-starter-logging이 포함돼 있고, 그 안에 Logback 관련 의존성이 들어있기 때문이다.

그리고 설정은 logback-spring.xml로 한다.
이 파일을 보면 <appender>나 <springProfile> 같은 태그가 있고, 로그 패턴이나 저장 경로, 파일 롤링 등을 세세하게 지정할 수 있다.

장점?

  • Spring Boot랑 찰떡궁합
  • 별도 설정 없이도 바로 동작
  • 대부분의 프로젝트에 무난하고 안정적임

3. Log4j2: 성능 괴물

Log4j의 후속 버전인 Log4j2는 성능에 진심이다. 아파치에 따르면 멀티쓰레드 환경에서 비동기 로거의 경우 처리량이 18배 높고 대기시간이 더 짧다.
GC 영향 적은 구조, 비동기 로깅, 커스텀 Appender 등 고급 기능이 많다. 로그양이 많거나 성능 튜닝이 필요한 경우엔 Logback보다 낫다.

근데 Spring Boot에서 기본으로 안 쓰기 때문에 Logback을 제거하고 Log4j2로 갈아끼워야 한다.

갈아끼우는 법은?

 

  1. spring-boot-starter-logging 제거
  2. spring-boot-starter-log4j2 추가
  3. 설정 파일도 log4j2-spring.xml로 바꿔야 함

설정 문법도 완전 다르다. <Configuration>부터 <Appenders>, <Loggers> 같은 구조로 되어 있어서, Logback 쓰던 감성으로는 안 된다.


4. @Slf4j vs @Log4j2

Lombok이 제공하는 이 로그 어노테이션들… 똑같아 보이지만 내부는 다르다.

어노테이션내부 Logger 타입
@Slf4j SLF4J API (org.slf4j.Logger)
@Log4j2 Log4j2 API (org.apache.logging.log4j.Logger)
 

즉, @Slf4j는 어떤 구현체가 뒤에 있든 상관없이 SLF4J 인터페이스만 쓴다.

유연하고 유지보수하기 좋다. 나중에 Logback → Log4j2로 갈아탈 때도 코드 안 고쳐도 된다. 설정만 바꾸면 된다.

@Log4j2는 아예 Log4j2 전용이다.


5. 정리

  1. SLF4J는 그냥 추상화. 실제로 로그는 안 찍는다.
  2. Logback은 Spring Boot의 기본 로깅 구현체. logback-spring.xml로 설정.
  3. @Slf4j는 SLF4J 기반이라 유연함. @Log4j2는 Log4j2 전용.
  4. Log4j2를 쓰면 될 듯 하다. 하지만 ThreadContext등 Log4j2 전용 기능을 사용하면 구현체(Log4j2)에 강하게 묶이게 될 수 있다.

덧붙임

“왜 로그 라이브러리 하나에 이렇게 복잡해?”라고 생각할 수도 있다.
근데 나중에 로그가 수천 줄씩 터질 때, 누가 메모리를 많이 잡아먹는지, 비동기로 돌릴 수는 있는지 등등이 중요해진다.
언젠가 꼭 필요한 개념이다.

JPA를 사용할 때, 엔티티의 데이터를 변경한 후 save() 메서드를 호출해야만 쿼리가 실행된다고 생각하는 경우가 많다. 그러나 이는 JPA의 핵심 기능 중 하나인 더티 체킹(dirty checking) 개념을 이해하지 못한 데서 비롯된 오해다.

영속성 컨텍스트에 관리되는 엔티티에 한해서는 save()는 의미가 없다.

이 글에서는 Spring Data JPA의 save()가 언제 쿼리를 날리는지, 그리고 더티 체킹과 어떤 차이가 있는지 예제와 함께 정리한다.


더티 체킹(DIRTY CHECKING)이란?

더티 체킹이란 JPA가 트랜잭션 내에서 영속 상태로 관리되는 엔티티의 변경 여부를 감지하고, 트랜잭션이 종료되는 시점에 자동으로 update 쿼리를 수행하는 기능이다. 이 기능 덕분에 개발자는 명시적으로 save()를 호출하지 않아도 변경된 내용을 데이터베이스에 반영할 수 있다.

 

더티 체킹이 실행되기 위해선 아래와 같은 조건이 있다.

 

  • 엔티티가 영속 상태(Persistent) 여야 한다
  • 트랜잭션 안에서 엔티티의 필드를 변경했을 때
  • flush 시점 (보통은 트랜잭션 commit 직전에 자동 flush됨)

 

 

예를 들어, 다음과 같은 코드가 있다고 가정해보자.

@Transactional
public void updateTodo(Long id, String newTitle) {
    Todo todo = todoRepository.findById(id).orElseThrow();
    todo.setTitle(newTitle); // setter만 호출
}

 

위 코드에서는 save() 메서드를 호출하지 않았지만, 트랜잭션이 종료될 때 Hibernate는 todo 객체의 변경사항을 감지하여 update 쿼리를 자동으로 생성한다(더티체킹) .


그렇다면 save()는 언제 필요한가?

결론만 말하자면 save()는 영속성 컨텍스트에서 관리되지 않는 엔티티를 DB에 저장해야하는 경우에만 필요하다.

Spring Data JPA의 save() 메서드 내부는 isNew()라는 판별 메소드가 있는데, 해당 메소드안에서 조건에 따라 다르게 작동한다.

isNew()는 다음 조건에 따라 동작한다.

    public boolean isNew(T entity) {
        ID id = this.getId(entity);
        Class<ID> idType = this.getIdType();
        if (!idType.isPrimitive()) {
            return id == null;
        } else if (id instanceof Number) {
            Number n = (Number)id;
            return n.longValue() == 0L;
        } else {
            throw new IllegalArgumentException(String.format("Unsupported primitive id type %s", idType));
        }
    }

 

  • 신규 엔티티(id == null || id == 0L)인 경우 => EntityManager.persist()를 호출하여 insert 쿼리를 실행한다.
  • 기존 엔티티(id != null  || id != 0L) 이 아닌 경우 => EntityManager.merge()를 호출하여 병합 과정을 수행한다.

사실상, 객체를 영속화 시키는 과정이라고 볼 수 있다.

 

여기서 persist()와 merge()를 다시 짚어보자.

1. persist

  • 새로운(비영속) 엔티티를 영속 상태로 만듦
  • 즉, 아직 ID가 없는 "처음 만든 객체"를 영속성 컨텍스트에 등록한다
  • 이후 트랜잭션이 flush될 때 insert 쿼리가 실행된다
  • persist()는 이미 영속성 컨텍스트에 있는 엔티티에 대해 호출하면 예외가 발생한다.

2. merge

  • 비영속 또는 준영속(detached) 상태의 엔티티를 병합하여 영속 상태로 만듦
  • 영속성 컨텍스트(1차 캐시)에 동일한 ID가 존재하면 그 객체를 사용하고, 없으면 DB에서 select로 로딩한다
  • DB에도 없으면?
    • JPA는 새로운 영속 엔티티를 만들어 복사본을 생성하고, 이를 영속화한다.
    • 그리고 트랜잭션 flush 시 insert 쿼리를 실행한다.
  • 이후 변경 사항은 더티 체킹 대상이 된다.

 

영속화를 persist로 하든 merge로 하든 영속화 된 객체에 대해선 더티체킹은 항상 실행될 수 있다.

persist는 insert 쿼리를 실행하기 때문에 더티체킹이 필요 없을 뿐이다.

 


 

save() 사용시 주의사항

 

그럼 id가 GeneratedValue(strategy = GenerationType.IDENTITY) 인 경우가 아니라, 수동으로 만들어 save() 한다면 어떻게 될까? 기존 엔티티로 취급하여 merge하기 때문에 select 될 것이다. 따라서 insert만 해도 되는데 불필요한 select 쿼리까지 실행되는 것이다. 이를 해결하기 위해서 객체에서 Persistable 인터페이스를 구현하고 isNew()를 오버라이드하는 방법이 있다.

 

아래는 예시다.

package com.mallapi.mall.domain;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.domain.Persistable;


@Entity
@ToString
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "tbl_todo")
public class Todo implements Persistable<Long> {

    @Id
    @GeneratedValue(strategy =  GenerationType.IDENTITY)
    private Long id;
    
    // isNew()가 항상 true를 반환해 persist()로 동작
    @Override
    public boolean isNew() {
        return true;
    }
}

 

 


실습 예제: save()와 더티 체킹 비교

다음은 테스트 코드에서 findById()를 통해 조회한 엔티티에 값을 변경하고 save()를 호출한 예제다.

@Test
@Transactional
@Rollback(false)
public void testSaveCall() {
    Todo todo = todoRepository.findById(1L).orElseThrow();
    todo.setTitle("변경 - save 호출");

    todoRepository.save(todo); // 이미 영속 상태

    log.info("After Save: {}", todo);
}
 
Hibernate:
    select
        t1_0.id,
        t1_0.complete,
        t1_0.content,
        t1_0.due_date,
        t1_0.title
    from
        tbl_todo t1_0
    where
        t1_0.id=?

Hibernate:
    update
        tbl_todo
    set
        complete=?,
        content=?,
        due_date=?,
        title=?
    where
        id=?
 

여기서 주목할 점은 save() 호출 시 별도의 select 쿼리가 발생하지 않는다는 점이다. 이는 todo 객체가 이미 영속 상태이기 때문에, save()는 내부적으로 merge()를 호출하더라도 select 없이 1차캐시(영속성 컨텍스트)에서 조회되며, 이후 더티 체킹에 의한 update만 수행된다.

 

여기서 주의할 점은 JPA의 영속성 컨텍스트(Persistence Context)는 트랜잭션 안에서만 유지된다. 따라서, 만약 @Transactional이 없다면 findById로 가져온 객체는 detached(준영속) 상태일 것이고, save(todo) 호출 시 select + update 쿼리가 실행된다. 영속성 컨텍스트가 없으므로 더티체킹도 되지 않는다. 따라서 트랜잭션 어노테이션을 빼먹지 말도록 하자.


반대로 save()가 필요한 경우

save()가 실제로 의미를 가지는 경우는 다음과 같다.

 

1. 비영속(new) 객체를 저장할 때

Todo newTodo = new Todo();
newTodo.setTitle("새로운 할 일");
todoRepository.save(newTodo); // persist → insert 쿼리 발생

 

2. 준영속(detached) 객체를 다시 병합할 때

Todo detached = new Todo();
detached.setId(1L);
detached.setTitle("준영속 상태");

todoRepository.save(detached); // merge → select + (insert 또는 update) 발생
 

이 경우에는 JPA가 해당 ID를 가진 엔티티가 존재하는지 DB에서 조회한 뒤 병합을 수행하므로 select 쿼리가 발생한다.


정리: save() vs 더티 체킹


 

상황 쿼리 발생 save() 의미
영속 상태 객체 수정 후 save() 호출 update 1회 merge 호출이지만, 이미 컨텍스트에 있으므로 아무 일도 안 함 (더티 체킹만)
ID 있는 비/준영속 상태 객체 save() 호출 select + update (DB에 있으면) /
select + insert (DB에 없으면)
Merge 실행
ID 없는 비/준영속 상태 객체 save() 호출
(이 경우 ID가 auto increment여야함)
insert Persist 실행
 

결론

  • findById()로 조회한 객체는 이미 영속 상태이므로, 값을 변경하기만 해도 트랜잭션 종료 시점에 더티체킹에 의해 자동으로 update된다.
  • 이 경우 save()를 호출하는 것은 불필요하며, 코드 가독성에도 혼동을 줄 수 있다.
  • save()는 영속성 컨텍스트에서 관리되지 않는 신규 객체 또는 비/준영속 객체일 경우에만 필요하다.

JPA를 효과적으로 활용하기 위해서는 **엔티티의 생명주기(영속/준영속/비영속)**를 잘 이해하는게 중요하다.

Spring Boot는 @SpringBootTest 어노테이션을 통해 테스트에서 실제 애플리케이션 컨텍스트를 실행할 수 있는 강력한 기능을 제공한다. 이번 포스팅 에서는 아래 내용을 상세하게 설명하려고 한다.

  • @SpringBootTest의 역할과 사용 시점
  • 슬라이스 테스트(@WebMvcTest, @DataJpaTest, @DataJdbcTest 등)와의 비교
  • 컨텍스트 커스터마이징 방법
  • 테스트 시간을 어떻게 줄일 수 있는지

“테스트”란 무엇을 의미할까?

유닛 테스트(Unit Test)

  • 단일 클래스 또는 기능 단위를 테스트
  • 외부 의존성은 mock 처리
  • 빠르고 격리된 테스트
class UserServiceTest {
  @Test
  void testLogic() {
    // 단일 클래스 내 로직만 테스트
  }
}

통합 테스트(Integration Test)

여러 계층의 조합과 실제 흐름을 테스트

통합 테스트는 다음 중 하나일 수 있다:

  1. 여러 객체 간 상호작용 (예: 서비스 ↔ 레포지토리)
  2. 여러 계층 간 테스트 (컨트롤러 → 서비스 → DB)
  3. 전체 애플리케이션 플로우 (요청 → 응답 + DB 상태 확인)
@SpringBootTest
@AutoConfigureMockMvc
class RegisterUseCaseIntegrationTest {
  @Autowired MockMvc mockMvc;
  @Autowired UserRepository userRepository;

  @Test
  void registrationWorks() throws Exception {
    mockMvc.perform(post("/register")
        .contentType("application/json")
        .content("{\"name\": \"jihun\"}"))
      .andExpect(status().isOk());

    assertTrue(userRepository.findByName("jihun").isPresent());
  }
}

@SpringBootTest 란?

@SpringBootTest는 다음을 제공:

  • 실제 운영 환경처럼 전체 컨텍스트를 로딩
  • 컨트롤러, 서비스, 리포지토리, 빈 모두 포함
  • 테스트 내에서 전체 요청/응답 흐름 시뮬레이션 가능

내부적으로

  • @SpringBootConfiguration을 찾기 위해 패키지 상단까지 탐색
  • 보통 @SpringBootApplication이 붙은 메인 클래스를 사용

다양한 슬라이스 테스트 어노테이션

Spring Boot는 특정 레이어만 테스트할 수 있도록 여러 "Test Slice" 어노테이션을 제공한다.

@WebMvcTest

  • 컨트롤러 계층만 로딩
  • @Controller, @ControllerAdvice, MockMvc만 포함
  • 서비스, 레포지토리는 mock 필요
@WebMvcTest(UserController.class)
class UserControllerTest {
  @Autowired MockMvc mockMvc;
  ...
}

@WebFluxTest

  • WebFlux 환경에서 사용하는 슬라이스 테스트
  • WebTestClient로 WebFlux 컨트롤러 테스트

@DataJpaTest

  • JPA 엔티티, Repository 테스트
  • H2 등 임베디드 DB 자동 설정
  • DB 쿼리, 매핑, 제약 조건 검증에 적합
@DataJpaTest
class UserRepositoryTest {
  @Autowired UserRepository userRepository;
  ...
}

@DataJdbcTest

  • Spring Data JDBC 사용 시 적합
  • 복잡한 매핑보다는 단순한 SQL 기반 테스트에 유용

@JsonTest

  • Jackson, Gson 직렬화/역직렬화 검증
  • ObjectMapper, JacksonTester 자동 구성

@RestClientTest

  • RestTemplate 기반 외부 API 호출 테스트 전용
  • MockRestServiceServer로 응답 모킹 가능

@SpringBootTest 커스터마이징 방법

1. MockMvc 사용: @AutoConfigureMockMvc

@SpringBootTest
@AutoConfigureMockMvc
class MyTest {
  @Autowired MockMvc mockMvc;
}

2. 특정 프로퍼티 설정

@SpringBootTest(properties = "feature.enabled=true")
class FeatureTest {
  @Value("${feature.enabled}")
  boolean enabled;
}

3. 프로필 지정

@SpringBootTest
@ActiveProfiles("test")
class ProfileTest {}

application-test.yml에서 설정된 값 자동 로딩

4. @TestPropertySource로 외부 파일 지정

@SpringBootTest
@TestPropertySource("classpath:test.properties")
class FilePropertyTest {}

5. @MockBean으로 빈 대체

@SpringBootTest
class MockTest {
  @MockBean UserRepository userRepository; // 진짜 빈을 mock으로 대체
}

6. @Import로 외부 빈 등록

@SpringBootTest
@Import(Foo.class)
class ImportTest {}

고급: @TestConfiguration, @SpringBootTest(classes = ...)

운영 클래스가 아닌 테스트 전용 애플리케이션 클래스를 사용할 수도 있다:

@SpringBootTest(classes = CustomTestApplication.class)
class CustomAppTest {}

혹은 @EnableScheduling 같은 설정을 켜고 끄는 조건부 빈을 작성할 수 있다:

@Configuration
@EnableScheduling
@ConditionalOnProperty(name = "scheduling.enabled", havingValue = "true")
public class SchedulingConfig {}

→ 테스트에서는 이렇게 끄기:

@SpringBootTest(properties = "scheduling.enabled=false")

왜 통합 테스트는 느릴까?

  • @SpringBootTest는 전체 컨텍스트를 매번 로딩 → 느림
  • 여러 커스터마이징이 다르면 캐시되지 않음
  • 가능하면 공통 설정 재사용이 테스트 속도 향상에 도움

참고: JUnit Insights로 테스트 시간 시각화 가능


결론

  • @SpringBootTest전체 경로(Controller → Service → Repository) 를 테스트할 때 가장 강력함
  • 단순 레이어 테스트라면 @WebMvcTest, @DataJpaTest슬라이스 테스트를 우선 고려
  • 컨텍스트 커스터마이징은 운영 환경과의 차이를 줄이는 선에서 최소화할 것

+ Recent posts