SMALL

Spring의 Controller Layer를 테스트하기 위해서는 

요청을 수행하고 응답을 만들어내는 Servlet API Mock 객체를 사용한다.

 

오늘 소개할 객체는 MockMvc이다.

MockMvc : Main entry point for server-side Spring MVC test support

 

MockMvc를 주입받는 방법에는 크게 두가지 방법이 있다. 

  • @SpringBootTest + @AutoConfigureMockMvc
  • @WebMvcTest 

각각의 방식에 대해서 알아 보도록 하자.

 

@SpringBootTest + @AutoConfigureMockMvc

 

@SpringBootTest는 웹 애플리케이션 컨텍스트와 설정을 모두 불러와 실제 웹 서버에 연결을 시도한다.

만약 Web Layer에 해당하는 부분만 테스트를 진행하고 싶은 경우에는 불필요한 설정도 로드하게 되는 것이다. 

통합 테스트를 할때 사용된다.

 

@SpringBootTest
@AutoConfigureMockMvc
public class SbtControllerTest {
	@Autowired
	private MockMvc mockMvc;
    ....
}

 

@WebMvcTest 

 

해당 설정은 Web Layer에 요청을 처리하기 위해 필요한 Bean들만 로딩한다.

단순히 Spring MVC 컨트롤러를 이용한 요청과 응답을 테스트하고자 한다면 @WebMVCTest를 사용하면 보다 가볍게 테스트할 수 있다.

 

@WebMvcTest
public class WebMvcControllerTest {
	@Autowired
	private MockMvc mockMvc;
    ....
}

 

경량화되어서 주는 장점도 있지만, 그에 상응하는 단점도 존재한다.

보통의 WebApplication은 Controller - Service - Repository Layer로 구성되어서 

Controller와 Service가 Dependency가 존재하는데, @WebMvcTest를 사용하는 경우에는 

Controller에 Service 객체를 주입받을 수 없다.

 

그렇기 때문에 Test를 진행하는 사용자가 Mock 객체를 주입해 주어야 한다. 

 

@WebMvcTest
public class WebMvcControllerTest {
	@Autowired
	private MockMvc mockMvc;
    
	@MockBean //Added !!
	private WebMvcService service;
	...
}

 

Service에 해당하는 객체를 Mock으로 주입 받은 경우 테스트 코드 작성 예시는 아래와 같다.

 

import static org.hamcrest.CoreMatchers.is;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

    @Test
	public void testGet() throws Exception {

		ResponseDto dto = new ResponseDto("id2", "sName2", "sCode2", "this is s2");

		given(service.findById("id2")).willReturn(dto);

		mockMvc.perform(get("/api/{id}", "id2").contentType(MediaType.APPLICATION_JSON))
				.andDo(print())
				.andExpect(status().isOk())
				.andExpect(jsonPath("$.name", is("sName2")));

	}

 

Controller의 특정 요청을 했을 때, 호출되는 Service의 메서드의 행동을 미리 정의해 놓는 형태이다. 

다음으로 getList를 테스트하는 경우에 예시 코드를 보자.

 

import static org.hamcrest.CoreMatchers.is;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

    @Test
	public void testGetList() throws Exception {
		List<StageResponseDto> list = Arrays.asList(
				new StageResponseDto("id2", "sName2", "sCode2", "this is s2"));
		
		given(service.findAll()).willReturn(list);
        
		mockMvc.perform(get("/api").contentType(MediaType.APPLICATION_JSON))
					.andDo(print())
					.andExpect(status().isOk())
					.andExpect(jsonPath("$").isArray())
					.andExpect(jsonPath("$", Matchers.hasSize(1)))
					.andExpect(jsonPath("$[0].name", is("sName2")));
	}

 

 

 

 

LIST
SMALL

Interceptor

 

Interceptor는 "가로채다"라는 의미를 가지고 있다.

Spring에서도 해당 의미와 유사하게 

Client에서 Server로 보낸 Request 객체를 Dispatcher Servlet의 Controller에 도달하기 전에 가로채는 역할을 한다.

 

Interceptor를 통해 가로챈 요청에 추가 로직을 넣을 수도 있고, Request 객체를 검증하는 작업도 할 수 있다. 

출처 : http://www.egovframe.org/wiki/doku.php?id=egovframework:rte:ptl:dispatcherservlet

 

Interceptor를 사용했을 때의 장점은 아래와 같다

  • 공통 로직을 사용하는 경우 코드 누락에 대한 위험성 감수
  • 코드 재사용성 증가
    • 메모리 낭비, 서버 부하 감수 

 

어떠한 프로그램이든 공통 로직은 한 곳에서 관리하는 게 가장 좋다.

그런 면에서 Interceptor는 좋은 옵션을 제공한다. 

 

사용 예제

 

@Component
public class MyInterceptor implements HandlerInterceptor {

	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		// do what you want .. 
		return true;
	}

	@Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			ModelAndView modelAndView) throws Exception {
		// do what you want .. 
	}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
 		   Object handler, Exception ex)
			throws Exception {
		// do what you want .. 
	}

}

 

Interceptor를 구현하기 위해서는 "HandlerInterceptor"를 Implements 받으면 된다.

해당 Interface에는 아래의 3가지 메서드를 제공한다.

  • preHandler(...)
    • Controller가 호출되기 전에 실행된다
    • return 값은 boolean으로 해당 값이 false이면 Controller가 호출되지 않는다
  • postHandler
    • Controller가 호출 된 이후지만, View가 생성되기 전에 실행된다
    • ModelAndView 객체를 인자로 받아 View를 생성하기 전에 해당 객체의 정보를 조작할 수 있다
  • afterCompletion
    • View의 모든 작업이 완료된 후에 호출된다

 

@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
	
	private final MyInterceptor interceptor;
    
	@Override
	public void addInterceptors(InterceptorRegistry registry) {

		registry.addInterceptor(interceptor).addPathPatterns("/**");
	}
	
}

 

작성한 Interceptor를 Spring에 등록하기 위해서는 WebMvcConfigurer를 Implements 하여서 코드를 추가해야 한다. 

 

WebMvcConfigurer 해당 Interface에 addInterceptors라는 메서드에서 추가해 주면 된다. 

또한 해당 Interceptor가 어떠한 조건일 때 실행되고, 실행되지 않는 등의 설정을 추가할 수 있다.

 

 

 

 

LIST
SMALL

Spring Boot Appliction이 실행되는 시점에 특정 코드를 추가하고 싶은 경우가 존재할 것이다.

이런 경우에 어떻게 해야하는지 알아보도록 하자.

CommandLineRunner

 

@Component
public class InitCommand implements CommandLineRunner {

	@Override
	public void run(String... args) throws Exception {
		// do somethings ..
	}
}

 

ApplicationRunner

 

@Component
public class InitAppliation implements ApplicationRunner {
	@Override
	public void run(ApplicationArguments args) throws Exception {
		// do somethings .. 
	}
}

 

차이점

두 interface 모두 run이라는 메서드를 제공하고, 해당 메서드에서 원하는 로직을 작성하면 된다.

그렇다면 이 둘의 차이점은 무엇일까? 

 

차이점으로는 메서드에 arguments이다. 

CommandLineRunner는 String Array를 Application Runner는 ApplicationArguments를 인자로 받는다.

그렇 각각의 값들을 어떻게 세팅하고 어떻게 꺼내 쓰는지 알아보도록 하자.

 

java -jar SpringApp.jar agrs1 args2 --name=junsu --type=child

 

위와 같은 형태로 Application을 실행시켰다고 하면 

CommandLineRunner에서는 String Array로 해당 값들을 아래와 같이 받을 수 있다.

	@Override
	public void run(String... args) throws Exception {
		System.out.println("#######################################");
		for (String str : args) {
			System.out.println(str);
		}
		
	}

 

ApplicationRunner에서는 ApplicationArguments의 메서드들을 이용해서 값들을 가져올 수 있다.

NonOptionArgs와 Option(Name, Value)로 나누어서 값을 가져올 수 있게 되어있다. 

 

	@Override
	public void run(ApplicationArguments args) throws Exception {

		System.out.println("*******************************************");
		args.getNonOptionArgs().forEach(str -> System.out.println(str));

		args.getOptionNames().forEach(str -> {
			System.out.print(str + " -- ");
			System.out.println(args.getOptionValues(str).get(0));
		});
		
		System.out.println(args.getOptionValues("name"));
	}

 

LIST
SMALL

Fetch Join은 기본 SQL 문법이 아니다.

JPQL( Java Persistence Query Language)에서 성능 최적화를 위해  제공하는 기능이다. 

연관 Entity or Collection을 SQL 한 번에 조회하기 위해 사용된다. 

JPQL ( Java Persistence Query Language ) : 객체 지향 쿼리 언어 
 - 테이블 대상으로 쿼리를 하는것이 아닌 객체를 대상으로 쿼리
 - SQL를 추상화한 것으로 특정 DB의 SQL에 의존하지 않는다

보통 아래와 같은 형태로 사용된다.

 

@Query("select m from Member m join fetch m.team")

 

실제 사용되는 예시를 보도록하자.

Member, Team Entity가 존재하고 Member는 하나의 Team에 속할 수 있고 Team은 여러 명의 Member를 가질 수 있다.

 

 

위와 같은 형태로 Member의 정보를 조회했을 때 해당 Member가 속한 Team 정보를 조회하고 싶은 경우에 사용될 수 있다.

 

@Getter
@NoArgsConstructor
@Entity
public class Member {
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;
	
	@ManyToOne
	private Team team;
	
	@Builder
	public Member(String name) {
		this.name = name;
	}
	
	public void updateTeam(Team team) {
		this.team = team;
	}
}
@Getter
@NoArgsConstructor
@Entity
public class Team {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;
	
	@OneToMany(mappedBy = "team")
	private List<Member> members;
	
	@Builder
	public Team(String name) {
		this.name = name;
	}
}

 

기본적으로 JPA에서 제공하는 findAll 메서드를 호출하게 되면 Member List를 모두 조회해오는 Query와 각각의 Member에 매핑되는 Team을 조회하는 쿼리가 실행하게 된다. 

( 예를 들어 Member의 개수가 N 인 경우 Member List 조회 ( 1 ) + 각 Member에 매핑되는 Team 조회 ( N ) => N +1 )

 

memberRepository.findAll()

Hibernate:
    select member0_.id as id1_2_, member0_.name as name2_2_, member0_.team_id as team_id3_2_ from Member member0_
Hibernate:
    select team0_.id as id1_6_0_, team0_.name as name2_6_0_ from Team team0_ where team0_.id=?
Hibernate:
    select team0_.id as id1_6_0_, team0_.name as name2_6_0_ from Team team0_ where team0_.id=?

 

이러한 문제를 해결하기 위해서 JPQL에서 Fetch Join 기능을 제공한다.

 

public interface MemberRepository extends JpaRepository<Member, Long>{

	@Query("select m from Member m join fetch m.team")
	Set<Member> findAllJoinFetch();
}

 

memberRepository.findAllJoinFetch()

Hibernate: 
    select
        member0_.id as id1_2_0_,
        team1_.id as id1_6_1_,
        member0_.name as name2_2_0_,
        member0_.team_id as team_id3_2_0_,
        team1_.name as name2_6_1_ 
    from
        Member member0_ 
    inner join
        Team team1_ 
            on member0_.team_id=team1_.id

만약 실행되는 Query의 Join 형식도 정하고 싶은 경우에는 아래와 같이 지정하면 된다.

@Query("select m from Member m left join fetch m.team")

 

 

또한 Join을 한 대상에서 한 번 더 Join을 하고 싶은 경우에는 아래와 같이 작성하면 된다.

( Team에 연관 객체로 Coach가 있다고 가정 )

@Query("select m from Member m join fetch m.team t join fetch t.coach")

 

만약 이러한 형태가 아닌 기본 제공하는 findAll을 통해서 연관 객체를 조회하고 싶은 경우에는 FetchType을 EAGER로 지정하면 된다.

@Getter
@NoArgsConstructor
@Entity
public class Team {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	@Column
	private String name;
	
	@OneToMany(mappedBy = "team", fetch = FetchType.EAGER) // !!Added
	private List<Member> members;
	
	@Builder
	public Team(String name) {
		this.name = name;
	}
}

 

LIST

+ Recent posts