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

2026-05-11 일지
web spring thymeleaf html

AOP

핵심 비지니스 로직과 공통 기능(횡단 관심사)을 코드 레벨에서 분리하여 관리하는 프로그래밍 방식

  • 주 관심사(Core Concern)
    핵심 비지니스 로직 (프로그램의 본연의 핵심 기능)

  • 횡단 관심사(Cross Cutting Concern)
    여러 곳에서 반복되는 부가적인 기능
    ex: 로그, 보안, 트랜잭션, 캐싱

TDD (Test Driven Development)

테스트 주도 개발 방법론. 개발 방향을 테스트를 먼저 만들고 개발 하는 것.
TDD 회의론도 등장 (테스트 때문에 개발이 밀려 주객전도가 일어남)

단위 테스트

비지니스 로직 중의 가장 작은 핵심 비지니스 단위(메서)를 테스트 하는 것
Bean을 띄우지 않음(컨테이너 미사용) → Mock 사용

슬라이스 테스트

특정 계층 또는 특정 관심 영역만 잘라서 테스트하는 방식
관련된 Bean만 띄움

  • 종류
    1. @WebMvcTest
  • Controller 계층 테스트에 사용한다.
  • 웹상의 요청과 응답에 관한 테스트
  • URL 매핑, HTTP Method, Request Parameter, Request Body
    Response Status, View 이름, JSON 응답, Validation을 다루게 된다.
  • 시큐리티, 필터까지 자동 포함
@WebMvcTest(PostController.class)
class PostControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    PostService postService;

    @Test
    void 게시글_목록을_조회한다() throws Exception {
        mockMvc.perform(get("/posts"))
                .andExpect(status().isOk());
    }
}

여기서 중요한 점은 Service는 실제 객체가 아니라 보통 @MockBean으로 대체한다는 것이다.

Controller O Service Mock Repository X DB X

  1. @DataJpaTest
    • Repository와 JPA 테스트에 사용한다.
    • @Transactional 어노테이션을 포함해 테스트 완료시 자동으로 롤백
    • Entity 매핑, Repository 메서드, JPQL, 쿼리 메서드, 연관관계 매핑
      저장/조회/삭제을 검증하는데 사용한다.
@DataJpaTest
class PostRepositoryTest {

    @Autowired
    PostRepository postRepository;

    @Test
    void 게시글을_저장한다() {
        Post post = new Post("제목", "내용");

        Post savedPost = postRepository.save(post);

        assertThat(savedPost.getId()).isNotNull();
    }
}

구성은 보통 이렇다.
Controller X, Service X, Repository O, Entity O
JPA O, DB O

보통 테스트용 내장 DB인 H2를 사용해서 테스트를 한다.

  1. @JsonTest
    JSON 직렬화/역직렬화 테스트에 사용한다.
    예를 들어 DTO가 JSON으로 잘 변환되는지 확인한다.
@JsonTest
class PostRequestJsonTest {

    @Autowired
    JacksonTester<PostRequest> json;

    @Test
    void JSON을_DTO로_변환한다() throws Exception {
        String content = """
            {
                "title": "제목",
                "content": "내용"
            }
        """;

        PostRequest request = json.parseObject(content);

        assertThat(request.getTitle()).isEqualTo("제목");
    }
}

통합 테스트

애플리케이션을 통합하여 테스트하는 방식    
단위테스트는 어떤 bean도 안띄운다. 따라서 어떠한 spring container도 없이 그 기능만을 테스트하겠다는 것을 의미한다.     

통합테스트는 모든 bean을 띄워서 실행하게 된다. 전부 다 띄운다는 의미는 redis, kafka, elk등의 툴도 띄우고 상호작용하는 것도 포함한다.

프록시 패턴

실제 객체 대신 대리 객체인 프록시가 먼저 요청을 받아 처리하는 패턴
  • 코드예시
    @Slf4j
    public class Proxy implements Subject{
    
      private Subject targetObject;
    
      public Proxy(Subject targetObject) {
          this.targetObject = targetObject;
      }
    
      @Override
      public void operation() {
          log.info("[prefix] Proxy.operation()");
          targetObject.operation();
          log.info("[postfix] Proxy.operation()");
      }
    }
    
  • 횡단관심사와 프록시를 적용한 코드 예시
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {
}

@Target: 이 어노테이션을 어디 붙일 수 있을지 설명
ElementType.TYPE : 클래스에 붙일 수 있다는 것을 의미
Element.METHOD : 메서드에 붙일 수 있다는 것을 의미
@Retension(RetentionPolicy.RUNTIME) : 어노테이션의 정보를 언제까지 유지할 것인지 지정, 생존 범위라고 봐도 무방

  • Retension 종류

    종류 설명
    SOURCE 컴파일 후 제거
    CLASS 클래스 파일까지만 저장
    RUNTIME 실행 중에도 유지
@Slf4j
@Service
@RequiredArgsConstructor
public class NonAopService {

    private final AopRepository repository;

    public void logic1(){
        log.info("[NonAopService] 서비스 로직 시작");
        repository.logic();
        log.info("[NonAopService] 서비스 로직 종료");

    }

    @Logging
    public void specialLogic(){
        log.info("[NonAopService] 스페셜 로직 시작");
        repository.logic();
        log.info("[NonAopService] 스페셜 로직 종료");

    }
}

커스텀한 인터페이스 Logging을 특정 메서드가 실행될 때 달아놓아서

보호 프록시

클라이언트의 권한에 따라 실제 객체의 메서드 호출을 제한
특정 클라이언트만 서비스 객체를 사용 할 수 있도록 하는 경우

가상 프록시

원본 객체가 생성이 너무 무거워서 오래 걸릴 때, 실제 객체가 생성될때까지 생성을 지연시키는 프록시 패턴
생성이 오래 걸리는 것은 첫번째만 오래 걸리고, 두번째는 캐싱을 해서 바로 준다
필요한 시점에는 대신 생성을 해준다

JDK 동적 프록시 (JDK Dymamic Proxy)

리플랙션 기술을 이용하여 동적으로 만들어 주는 프록시.
반드시 인터페이스가 있어야 한다. InvocationHandler라는 인터페이스를 상속받아서 사용한다.
invoke는 요청하다라는 의미를 가지는 단어로 call과는 다르지만 간접적인 호출이라는 의미가 있다.

@Slf4j
@RequiredArgsConstructor
public class PaymentHandler implements InvocationHandler {
    private final PaymentService paymentService;
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("결제 전에 로깅을 수행하겠습니다");
        Object result = method.invoke(paymentService, args);
        log.info("결제 후에 로깅을 수행하겠습니다");
        return result;
    }
}

CGLIB (Code Generation Library)

인터페이스가 없는 클래스에 대하여 동적으로 적용해준다. 바이트코드 조작(java 소스코드를 직접 작성하지 않고, Runtime중에 클래스파일의 내용을 동적으로 생성하거나 수정하는 것)으로 프록시 생성한다.

기존 클래스를 상속받아서 동적으로 프록시 객체를 생성하는 방식으로 동작

  • 주의
    • 구현체가 final이면 안됨
    • 부모 클래스의 생성자를 체크해야 한다. 기본 생성자가 필요하다
@Slf4j
@RequiredArgsConstructor
public class SubLogicInterceptor implements MethodInterceptor {

    private final Object target;

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {

        log.info("전처리");
        Object result = proxy.invoke(target, args);
        log.info("후처리");

        return result;
    }

}

MethodInterceptor을 스프링이 제공받아서 이를 사용해서 구현하는 것도 가능하다.