이 글은 실제 히빗 프로젝트(ver.2)를 혼자서 개발하면서 경험한 내용을 정리한 글입니다.
@EntityGraph
가 무엇이고 어떻게 적용해야할지 궁금한 개발자현재, 특정 게시글 조회를 위한 API에 대한 성능 테스트를 진행하면서 다음 가정을 설정했다.
아래와 같이 조회수는 정상적으로 1000
이 나오는 걸 볼 수 있다.
{
"id": 1,
"writerId": 1,
"writerName": "mjy",
"title": "디뮤지엄 전시 보러가요",
"content": "오스린티 전시회 보러가실 분 있나요?",
"exhibition": "오스틴리 전시",
"exhibitionAttendanceAndTogetherActivity": [
"4인 관람",
"맛집 가기"
],
"possibleTime": "2024-01-05 12:00",
"openChatUrl": "https://kakao",
"postStatus": "HOLDING",
"imageName": "exhibition.png",
"viewCount": 1000
}
JMeter
를 통한 성능 테스트에서 요약 보고서를 확인했을 때, 아래와 같이 평균 처리량(TPS)이 27.9/sec
나오는 걸 확인할 수 있다.
TPS가 27.9
라는 것은 Traffic Per Second(초당 처리 건수)가 27.9건이라는 의미이다.
이는 1초 동안 시스템이 27.9번의 요청을 처리할 수 있다는 것을 나타낸다.
TPS에 대한 그래프를 확인했을 때, 최대 30
이 나왔다.
현재 게시글(Post)와 게시글을 작성한 회원(Member)의 관계는 다대일 단방향 매핑 으로 이루어져 있고, 지연 로딩을 사용했다.
여기서 지연 로딩
을 사용한 이유는 Post
엔티티와 연관된 엔티티(Member
)를 실제로 사용할 때 엔티티를 조회할 때 프록시를 통해 가져오기 때문이다.
즉, Post 엔티티를 로드할 때 Member 엔티티의 정보는 필요한 시점에만 로드되고, 이때 프록시(Proxy)를 통해 지연 로딩이 이루어진다.
그 후 해당 프록시 객체를 호출할 때마다 그때그때 select 쿼리가 실행된다. (자세한 내용은 이전에 작성한 [JPA] 프록시와 연관관계 관리 글을 참고하자)
Post(게시글) 엔티티
@Table(name = "posts")
@Entity
public class Post extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "members_id")
private Member member;
// ...
}
Member(회원) 엔티티
@Table(name = "members")
@Entity
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
// ...
}
그리고 하나의 게시글을 조회할 때 JPA에서 제공해주는 findById()
를 사용했다.
그래서 PostRepository 인터페이스에는 아무런 코드를 입력하지 않았다.
PostService 클래스
@Service
@Transactional(readOnly = true)
public class PostService {
// ...
@Transactional
public PostDetailResponse findPost(final Long postId, final LoginMember loginMember, final String cookieValue) {
if (viewCountManager.isFirstAccess(cookieValue, postId)) {
postRepository.updateViewCount(postId);
}
Post foundPost = findPostObject(postId);
return PostDetailResponse.of(foundPost, loginMember);
}
// ...
private Post findPostObject(final Long postId) {
return postRepository.findById(postId)
.orElseThrow(NotFoundPostException::new);
}
}
그 결과, 특정 게시글에 대한 조회 api에 대한 쿼리를 확인했을 때 아래와 같이 나왔다.
Hibernate:
select
post0_.id as id1_2_0_,
post0_.created_date_time as created_2_2_0_,
post0_.updated_date_time as updated_3_2_0_,
post0_.content as content4_2_0_,
post0_.exhibition as exhibiti5_2_0_,
post0_.exhibition_attendance as exhibiti6_2_0_,
post0_.image_name as image_na7_2_0_,
post0_.members_id as members14_2_0_,
post0_.open_chat_url as open_cha8_2_0_,
post0_.possible_time as possible9_2_0_,
post0_.post_status as post_st10_2_0_,
post0_.title as title11_2_0_,
post0_.together_activity as togethe12_2_0_,
post0_.view_count as view_co13_2_0_
from
posts post0_
where
post0_.id=?
// ...
Hibernate:
select
member0_.id as id1_0_0_,
member0_.created_date_time as created_2_0_0_,
member0_.updated_date_time as updated_3_0_0_,
member0_.display_name as display_4_0_0_,
member0_.email as email5_0_0_,
member0_.is_profile as is_profi6_0_0_,
member0_.main_image as main_ima7_0_0_,
member0_.social_type as social_t8_0_0_
from
members member0_
where
member0_.id=?
게시글을 조회할 때, 연관된 회원도 같이 조회가 된다. 근데, 여기서 select 절이 2번 조회되는 걸 볼 수 있다.
매번 특정 게시글을 조회할 때마다 게시글안에 회원 정보도 필요하기 때문에, 한 번에 select 하는게 더 낫지 안을까?
라는 생각이 들었다.
기본적으로 Post
엔티티는 Member
엔티티를 로딩할 때 지연 로딩으로 사용되는데, 위와 같이 특정 상황에서는 즉시 로딩
전략이 필요 할 수 있다.
이때 사용하는 방법이 바로 @EntityGraph
어노테이션이다.
@EntityGraph
는 Data JPA에서 fect 조인을 어노테이션으로 사용할 수 있도록 만들어 준 기능이다.
@EntityGraph
를 사용하면 특정 엔티티나 연관된 엔티티들을 로딩할 때 그래프를 명시적으로 정의할 수 있다.
PostService 클래스, PostRepository 인터페이스
// PostService
@Service
@Transactional(readOnly = true)
public class PostService {
// ...
@Transactional
public PostDetailResponse findPost(final Long postId, final LoginMember loginMember, final String cookieValue) {
if (viewCountManager.isFirstAccess(cookieValue, postId)) {
postRepository.updateViewCount(postId);
}
Post foundPost = findPostObject(postId);
return PostDetailResponse.of(foundPost, loginMember);
}
// ...
private Post findPostObject(final Long postId) {
return postRepository.findPostById(postId)
.orElseThrow(NotFoundPostException::new);
}
}
// PostRepository
public interface PostRepository extends JpaRepository<Post, Long> {
// ...
@EntityGraph(attributePaths = "member")
@Query("SELECT p FROM Post p")
Optional<Post> findPostById(Long id);
}
위와 같이 특정 게시글(Post
)을 조회할 때 해당 게시물에 대한 회원(Member
) 정보를 함께 조회하게 된다.
이렇게 함으로써 지연 로딩을 사용하더라도 특정 게시글을 가져올 때마다 작성자 정보를 필요한 경우에만 가져오게 되어 효율적인 쿼리를 수행할 수 있다.
그 결과 쿼리는 아래와 같이 나오게 된다.
Hibernate:
select
post0_.id as id1_2_0_,
member1_.id as id1_0_1_,
post0_.created_date_time as created_2_2_0_,
post0_.updated_date_time as updated_3_2_0_,
post0_.content as content4_2_0_,
post0_.exhibition as exhibiti5_2_0_,
post0_.exhibition_attendance as exhibiti6_2_0_,
post0_.image_name as image_na7_2_0_,
post0_.members_id as members14_2_0_,
post0_.open_chat_url as open_cha8_2_0_,
post0_.possible_time as possible9_2_0_,
post0_.post_status as post_st10_2_0_,
post0_.title as title11_2_0_,
post0_.together_activity as togethe12_2_0_,
post0_.view_count as view_co13_2_0_,
member1_.created_date_time as created_2_0_1_,
member1_.updated_date_time as updated_3_0_1_,
member1_.display_name as display_4_0_1_,
member1_.email as email5_0_1_,
member1_.is_profile as is_profi6_0_1_,
member1_.main_image as main_ima7_0_1_,
member1_.social_type as social_t8_0_1_
from
posts post0_
left outer join
members member1_
on post0_.members_id=member1_.id
where
post0_.id=?
위의 쿼리를 통해 알 수 있듯이, @EntityGraph
어노테이션을 적용하면 left outer join
을 사용하는 걸 확인할 수 있다.
그리고 처음과 같이, 1초 동안 서로 다른 100명의 사용자가 동시에 특정 게시글을 10번 반복해서 조회하는 성능 테스트를 실행했다.
결과를 확인해보니, 평균 TPS 값이 39.1
로 이전 평균 TPS(27.9) 보다 40.14% 개선된 것을 확인할 수 있다.
TPS에 대한 그래프에서는 최대 TPS 값이 42
로 이전 최대 TPS(30)보다 40% 개선된 것을 확인할 수 있다.
이처럼 쿼리를 명시적으로 작성하지 않아도 fetch join
과 같은 효과를 나타낼 수 있으나, 약간의 차이점이 있다.
@EntityGraph
의 경우 fetchType을 eager로 변환하는 방식으로 outer left join
을 수행하여 데이터를 가져오지만,
fetch join
의 경우 따로 outer join으로 명시하지 않는 경우 inner join
을 수행한다는 점이다.
이 때문에 1:N(일대다) 연관 관계에서 fetch join
을 사용할 때, 일반적으로 1개의 컬렉션만 함께 조회할 수 있다.
그러나 이로 인해 distinct가 필요한 상황에서도 @EntityGraph
를 사용하면 이러한 단점을 극복할 수 있다.
@EntityGraph
은 다음과 같이 2가지 유형이 있다. 기본 값은 FETCH
이다.
FETCH
: entity graph에 명시한 attribute는 EAGER
로 패치하고, 나머지 attribute는 LAZY
로 패치
LOAD
: entity graph에 명시한 attribute는 EAGER
로 패치하고, 나머지 attribute는 entity에 명시한 fetch type
이나 디폴트 FetchType으로 패치
(@OneToMany는 LAZY
, @ManyToOne은 EAGER
등이 기본 값이다)
위에서 소개한 @EntityGraph
를 이용한 성능 개선은 현재 프로젝트에 적용하기에는 아직 간단한 수준이다.
현재 개발 중인 히빗 프로젝트(ver.2)
는 EC2 인스턴스 서버 2개를 활용하고 있다. (NGINX 서버 - 백엔드 서버 인스턴스 유형: t2.micro
)
데이터베이스 레플리케이션을 활용한 쿼리 성능 개선은 고려사항 중 하나이지만, 혼자 개발 중인 프로젝트의 비용 부담도 고려해야 한다.
따라서 추가로 비용을 발생시키지 않는 방법을 고민하며 성능을 향상시킬 수 있는 다양한 접근 방법을 모색해보자.
이 글은 실제 히빗 프로젝트(ver.2)를 혼자서 개발하면서 경험한 내용을 정리한 글입니다.
예상 독자: 동시성 처리로 인한 데이터 정합성 문제를 해결하는 것에 대해 궁금한 개발자
히빗 (ver.1)에서 발생한 데이터 정합성 문제를 히빗2 (ver.2)를 통해 해결한 과정에 대해 정리했습니다.
서버 개발을 하다 보면 여러 요청에서 동시에 공유 자원을 활용하는 경우 동시성 이슈가 발생할 수 있다.
특히 java를 활용한 웹 애플리케이션의 경우 멀티 쓰레드로 동작하기 때문에 데이터 정합성을 위해서는 공유 자원에 대한 관리가 필요하다.
동시성 이슈 해결을 위해서는 다양한 방법이 존재한다. 요청 쓰레드가 순차적으로 처리할 수 있도록 제한하거나 낙관적 락/비관적 락을 활용하거나 쿼리를 통해 해결하는 등 다양한 방법을 통해 막을 수 있다.
이번 글에서는 각각의 해결 방법을 사용해보고 고민해서 가장 적합한 해결 방법을 히빗2에 적용하는 과정을 정리하였다.
기존 히빗 서비스(ver.1)
에서는 ‘게시글 전체보기’ 에서 사용자가 하나의 게시글을 클릭할 때, 조회수 1이 증가되도록 구현했다.
게시글에 대한 엔티티 클래스인 Post
는 제목, 내용 등 여러 필드가 있고, 그 중에서 조회수를 나타내는 viewCount
를 가지고 있다.
```java @Entity public class Post extends BaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = “post_id”) private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@Column(name = "title", nullable = false)
@Embedded
private Title title;
@Column(name = "content", nullable = false)
@Embedded
private Content content;
이 글은 실제 히빗 프로젝트(ver.2)를 혼자서 개발하면서 경험한 내용을 정리한 글입니다.
예상 독자: 성능 테스트에 대한 도구와 설치 과정에 대해 궁금한 개발자
아래 글부터는 히빗 프로젝트(ver.2) 를 히빗2
로 줄여서 작성했습니다.
히빗2 서비스에서 특정 게시글에 대한 조회를 할 때마다 조회수 1이 증가되도록 했다.
사용자 1명이 특정 게시글에 대한 조회를 할 때 1이 증가하는 것을 확인하기 위해 테스트 코드를 작성했다.
class PostServiceTest extends IntegrationTestSupport {
@Autowired
private PostRepository postRepository;
@Autowired
private ProfileRepository profileRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
private PostService postService;
@AfterEach
void tearDown() {
postRepository.deleteAll();
profileRepository.deleteAll();
memberRepository.deleteAll();
}
@DisplayName("특정 게시글을 조회하면 조회수를 1 증가시킨다.")
@Test
void 특정_게시글을_조회하면_조회수를_1_증가시킨다() {
// given
Member 팬시 = 팬시();
memberRepository.save(팬시);
Member member = memberRepository.getById(팬시.getId());
Profile 팬시_프로필 = 팬시_프로필(member);
Profile profile = profileRepository.save(팬시_프로필);
memberRepository.save(팬시);
Post post = 프로젝트_해시테크(profile.getMember());
postRepository.save(post);
// when
int viewCount = post.getViewCount();
postService.findPost(post.getId());
int updatedViewCount = postRepository.findById(post.getId()).get().getViewCount();
// then
Assertions.assertThat(viewCount + 1).isEqualTo(updatedViewCount);
}
}
이런 경우에는 문제 없이 잘 동작하는 것을 확인했다.
여기서 만약 동시 사용자가 100명이 10초동안 5번 반복했을 때, 오류 없이 조회수 증가가 제대로 처리되고 있는지 확인하고 싶었다.
그래서 성능 테스트를 도입하게 되었고, 이번 글에서는 성능 테스트를 위한 서버 환경을 구축하고 재연하는 과정을 시도했다.
성능 테스드에 대한 개념을 학습한 과정은 이전에 성능 테스트, 부하 테스트, 스트레스 테스트란에 정리했다.
Jmeter 는 아파치 소프트웨어 재단에서 개발한 성능 테스트를 위한 도구로 순수 100% Java로 개발한 웹 애플리케이션이다.
웹 애플리케이션 서버 성능 테스트를 위해 개발했지만, 현재는 데이터베이스, 파일 시스템, FTP, TCP 등 다양한 애플리케이션/서버/프로토콜 유형의 성능을 테스트할 수 있게 발전되었다.
Jmeter
은 최근 2주전까지 릴리즈하는 만큼 많은 릴리즈와 개선 사항을 통해 고도로 유지 및 관리하고 있다.
그리고 스프링과 통합하기 위한 여러가지 다양한 플러그인을 제공한다.
하지만, 애플리케이션 자체가 오래되어서 복잡한 스크립트와 문서가 가독성이 좋지 않다.
또한 Jmeter
는 단계별 스레드 할당 방식이라고 해서 스레드가 하나 생성될 때마다 리소스를 새로 할당해야 한다.
그래서 하나의 Worker에서 1000개 이상의 스레드를 할당하게 되면 조금 무리가 있을 수 있다. 즉, 한 대의 Wokrer에서 사용할 수 있는 사용자 수가 매우 한정적이라는 말이다.
그리고 실제로 Jmeter
를 설치해서 사용해봤지만, 위에 나온 그림처럼 기본적으로 제공되는 그래프가 나한테는 그렇게 이쁘지 않는 것 같다는 생각이 들었다.
nGrinder 는 서버 성능 테스트를 위해 Naver가 ‘The Grinder’를 기반으로 개발한 오픈소스 성능 테스트 도구이며 국내 개발자들이 많이 사용한다.
성능 측정 도구로 nGrinder
는 groovy
스크립트로 테스트 시나리오를 작성할 수 있다는 것이다.
그래서 기존에 Junit 테스트에 관심이 있는 개발자라면 거부감 없게 쓸 수 있을 정도로 비슷하다는 평을 받고 있다.
한국 기업인 네이버가 개발하고 유지보수하는 만큼 도구 자체에서 한국어를 지원한다. (물론, 나는 찾지 못했지만) 한국어로 작성된 문서는 못본 것 같다.
또한 완성도 높은 뼈대 코드를 지원해준다. (스크립트 이름 + 도메인 주소 + 요청을 보낼 API) 실제로 스크립트 이름과 도메인 주소, API 주소만 입력하면 완성도 높은 뼈대를 지원해준다.
nGrinder
는 Controller, Agent, Target으로 구성된다.
Controller
는 WAS 기반으로 웹 브라우저에 접속하여 테스트를 위한 웹 인터페이스를 제공하며, Agent
에게 신호를 보내 테스트 스크립트를 실행 및 nGrinder
에 대한 전반적인 기능 관리를 제공한다.
Agent
는 직접 부하를 발생시키는 머신이고, Controller
로부터 신호를 받아 동작한다.
Target
은 부하가 발생할 대상 서버를 의미한다.
WAS 기반으로 동작하기 때문에 젠킨스 같이 개발자 각각의 계정을 가질 수 있고, 계정별 부하 테스트 히스토리를 관리할 수 있는 장점이 있다.
반면, nGrinder
자체가 프로세스가 분리되어 있어서 Controller
와 Agent
를 각각 따로 실행해줘야 한다는 단점이 있다.
그리고 groovy
라는 문법이 흔하지 않아서 IDE를 지원해주지 않는 경우가 많다. 그래서 nGrinder
내부에 직접 작성해야 한다는 불편함이 있다.
두 도구를 비교하여 초기에는 nGrinder
를 먼저 사용해보았으나, 설치하는 과정이 생각보다 번거로웠고, 참고할 레퍼런스 자료가 많지 않았다.
또한, Jmeter
는 최근까지(2주전) 릴리즈가 이루어져 활발하게 업데이트가 이루어지고 있었지만, nGrinder
는 마지막 릴리즈 날짜가 2022년 12월 30일 정도로 1년 가까이 최신화가 잘되어 있지 않았다.
nGrinder
를 활용하려면 Controller, Agent 각각 별도의 서버를 구축하고 실행해야 하는데, 현재 히빗2는 백엔드 서버 EC2 서버 한개만 있었고, 추가 서버를 두는 것이 번거로웠고 비용적인 측면에서도 조금은 부담이 되었다.
반면에 Jmeter
는 기본적으로 GUI로 제공되고 있고, 학습 비용이 낮고 빠르게 테스트 시나리오를 작성할 수 있다.
만약 히빗2 프로젝트가 개인 프로젝트가 아닌 팀 프로젝트이고 실제 서비스를 해야하는 상황이라면 nGrinder
를 사용할텐데, 지금은 게시글 조회에 대한 성능 테스트만 하는게 주 목적이였다.
또한 Jmeter
는 자체적으로 Plugin Manager를 내장하고 있어서 별도의 설정 없이 플러그인을 설치할 수 있다.
이러한 이유로 히빗2에서는 높은 점유율과 확장성, 풍부한 레퍼런스, 낮은 초기 학습 비용 등을 고려하여 Jmeter
를 사용하게 되었다.
Apache JMeter 공식 웹 사이트에서 Binaries 부분 아래에 apache.jmeter-5.6.3.zip
를 다운받는다.
해당 파일을 다운로드 받은 뒤 압축을 풀고, bin
디렉터리에서 아래와 같은 명령어를 입력한다.
$ ./jmeter.sh
그러면 Jmeter
가 실행되면서 위와 같은 창이 나오게 된다.
[1] Jmeter = Thread Group 생성
테스트 계획 우클릭 -> 추가 -> 쓰레드들 -> 쓰레드 그룹 선택
쓰레드들의 수(사용자 수, Number of Threads) : 모든 Thread 개수(가상의 사용자 수)
Ramp-up 시간(단위: 초, Ramp-Up Period) : 쓰레드 수들을 얼마 시간 동안 테스트할지에 대한 설정
Loop Count: 한 Thread당 모두 몇 번의 테스트를 수행하는 지 정의
ex. 쓰레드들의 수
가 1000이고, Ramp-up 시간
이 10이라면 동시의 사용자 1000명이 10초 안에 실행한다는 걸 의미한다.
만약 `쓰레드들의 수`가 1000, `Loop Count`이 5이라면, 1000명이 5번 요청해서 총 10,000번의 요청이 전송되는 걸 의미한다.
그래서 `쓰레드들의 수`가 1000이고, `Ramp-up 시간`이 10, `Loop Count`이 5이면 동시의 사용자 1000명이 10초 안에 실행하는 것을 5번 반복한다는 걸 말한다. (총 5,000번의 요청이 전송됨)
나는 100, 10, 5 순으로 입력했다. -> 동시 사용자 100명이 10초동안 5번 반복하도록 실행한다. (500번)
[2] Jmeter - HTTP Request 생성
쓰레드 그룹 우클릭 -> 추가 -> 표본추출기 -> HTTP 요청(Request) 선택
[+] HTTP 헤더 관리자 추가
HTTP 요청(Request) 우클릭 -> 추가 -> 설정 엘리먼트 -> HTTP 헤더 관리자
필자는 히빗2 서비스에 구글 소셜 로그인을 이용했기 때문에 Header 이름에 Authorization
을 넣고, 값에는 액세스 토큰/리프레시 토큰 값을 넣었다.
[3] Jmeter - 요약 보고서
, 결과 그래프
, 결과들의 트리 보기
추가
쓰레드 그룹(히빗2 테스트) -> 추가 -> 리스너 -> 원하는 형태의 결과창 선택
[4] 실행 후 결과 확인
요약 보고서를 통해 결과를 확인해보니, 다행히 오류없이 정상적으로 처리가 되었다.
여기서 처리량은 TPS
(초당 처리한 요청 수)를 의미한다.
TPS
는 Transaction Per Seconds의 약자로 요청 단위로 초당 얼마나 많은 요청을 처리할 수 있는지에 대한 수치이다. (= 초당 처리된 트랜잭션 수)
TPS
가 높을수록 시스템이 더 많은 요청을 처리할 수 있음을 의미하기 때문에 성능 테스트에 있어 중요한 지표로 여겨진다.
JMeter
도구에서 측정하는 TPS
는 우리가 설정한 테스트 구성을 가지고 결과를 측정하기 때문에 설정값을 가지고 해당 테스트 상에서 최대 TPS 값을 구할 수 있다.
앞으로 이 TPS라는 지표를 활용하여 서비스의 성능이 개선되었음을 판단할 예정이다.
24.01.24 추가) JMeter Plugins 설치 - Transactions Per Second
Custom Plugins for Apache JMeter 사이트에서 Download Version
아래 2.0 부분 클릭해서 다운로드 후 압축 해제한다.
압축 해제한 이후에 jar 파일들을 jmeter 폴더(apache-jmeter-5.6.3/lib/ext/
)안에 넣는다.
그리고 jmeter을 재실행후 왼쪽 메뉴 영역에서 히빗2
우클릭으로 jp@gc 관련 플러그인이 잘 포홤되었는지 확인한다
그러면 아래와 같이 플러그인 옵션이 생기는 걸 확인할 수 있다.
https://www.youtube.com/watch?v=YGYQ-f01TjE
성능 테스트
는 특정 상황에서 시스템의 구성 요소가 어떻게 수행되는지 확인하기 위해 수행되는 테스트이다.
또한 이 테스트를 통해 제품의 리소스 사용량, 확장성 및 신뢰성을 확인할 수 있다.
이 테스트는 소프트웨어 제품의 설계 및 아키텍처에서 성능 문제를 해결하는 데 중점을 둔 성능 엔지니어링의 하위 집합이다.
위의 이미지는 성능 테스트가 부하 및 스트레스 테스트 모두에 대한 상위 집합이라는 것을 명확하게 설명한다. 성능 테스트에 포함된 다른 유형의 테스트는 스파이크 테스트, 볼륨 테스트, 내구성 테스트 및 확장성 테스트가 있다. 따라서 성능 테스트는 기본적으로 광범위한 용어이다.
성능 테스트의 주요 목표
에는 시스템의 벤치마크 동작을 설정하는 것이 포함된다.
성능 테스트는 애플리케이션의 결함을 찾는 것을 목표로 하지 않고, 애플리케이션의 벤치마크 및 표준을 설정하는 중요한 작업을 처리한다.
그래서 실제 트래픽 상황에서 정상적으로 동작되는지, 애플리케이션/시스템 성능에 대한 모니터링이 성능 테스트의 주요 특성을 의미한다.
성능 테스트는 속도(speed), 응답 시간(response time), 처리량(throughput), 자원 사용량(resource usage), 안정성(stability) 등의 속성을 기준으로 애플리케이션의 벤치마크와 표준을 설정해야 하고, 이 모든 속성을 테스트한다.
성능 테스트는 다음과 같은 상황에서 주로 사용된다.
실제 트래픽 상황에서의 정상 동작
기존 시스템 대비 BenchMarking
성능 테스트는 가용성
과 서비스에서 설정한 목표치를 달성하기 위해서이다.
가용성
이란 시스템이 서비스를 정상적으로 제공할 수 있는 상태를 말한다.
성능 테스트를 확인하기 위해 대표적으로 확인해야할 지표는 처리량
(throughput), 응답 시간
(response time)이다.
초당 1000건의 요청 처리 - 처리량(throughput)
모든 조회 요청을 1초 이내로 응답 - 응답 시간(response time)
처리량
은 초당 처리하는 작업의 수를 의미한다. 일반적으로 RPS
(Request Per Second)라고 많이 부르는데, 이는 1초에 처리하는 HTTP 요청의 수를 의미한다.
처리량은 서브 시스템중 가장 처리량이 낮은 부분으로 계산한다.
예를 들어, 클라이언트 <-> Nginx 가 500rps, Nginx <-> Tomcat 이 200 rps, Tomcat <-> MySQL 이 100 rps가 있다고 가정했을 때
Tomcat <-> MySQL 이 해당한다고 보면 된다. 이는 병목 구간
으로 말한다. (병목 구간
은 하위 시스템 중 가장 낮은 처리량을 가지는 부분이다)
응답 시간
은 시스템이 요청을 받고 응답할 때까지의 시간을 의미한다.
시스템이 요청을 처리할 때 까지 대기하는 시간도 응답 시간에 포함된다.
응답 시간은 각 서브 시스템 응답 시간의 총합으로 계산한다.
예를 들어, 클라이언트 <-> Nginx 가 500rps, Nginx <-> Tomcat 이 200 rps, Tomcat <-> MySQL 이 100 rps가 있다고 가정했을 때
응답 시간은 750ms (150ms + 500ms + 100ms)으로 어떤 부분을 개선하더라도 총 시스템 응답 시간에 영향을 주게 된다.
처리량과 응답 시간을 정리하면 다음과 같다.
처리량: 초당 처리하는 작업의 수
응답 시간: 시스템의 요청 처리 시간
둘 지표간에는 상관관계를 가지고 있다.
성능 테스트에는 다양한 테스트가 있는데, 그 중에서 부하 테스트와 스트레스 테스트를 알아보고자 한다.
부하 테스트
는 성능 테스트의 하위 집합으로, 임계값에 도달할 때까지 시스템의 부하를 지속적으로 꾸준히 증가시켜 시스템을 테스트하는 것을 의미한다.
부하 테스트의 유일한 목적
은 시스템이 감당할 수 있는 가장 큰 작업을 할당하여 시스템의 내구성을 시험하고 결과를 모니터링하는 것이다.
부하 테스트에서 모니터링되는 속성에는 최대 성능, 서버 처리량, 다양한 부하 레벨에서 응답 시간, H/W 환경의 적정성, 성능에 영향을 주지 않고 처리할 수 있는 사용자 애플리케이션 수 등이 포함된다.
부하 테스트의 목표
는 애플리케이션과 데이터베이스 등의 최대 성능을 파악하는 것이다.
부하 테스트는 다음과 상황에서 사용된다.
리소스 병목 탐색, 애플리케이션
이벤트 상황과 같은 순간 트래픽 최대치, 한계치를 탐색
신규 스펙 장비에서 MYSQL 설정 최적화 탐색
스트레스 테스트
에서는 기존 자원에 초과 작업으로 과부하를 주는 다양한 활동을 수행하며 시스템을 무너졌을 때 어떤 동작을 보이는지 확인하는 테스트이다.
스트레스 테스트의 목적
은 시스템의 고장을 확인하고 시스템이 어떻게 정상적으로 복구되는지를 모니터링 하는 것이다.
따라서 스트레스 테스트는 최대 부하 및 정상 조건을 넘어서는 애플리케이션의 동작을 평가한다. 이러한 스트레스 테스트는 피로 테스트라고도 불린다.
스트레스 테스트의 목표
는 시스템이 망가진 후에 애플리케이션이 어떻게 동작을 정의하는 것이다.
가장 중요한 점은 시스템이 장애 발생 후 중요한 데이터를 손상시키지 않도록 하는 것이다.
스트레스 테스트를 통해 가장 이상적인 시스템은 극도의 부하가 발생한 이후에도 모든 구성 요소와 함께 정상 상태로 복귀하는 것이다.
스트레스 테스트는 다음과 같은 상황에서 사용된다
장기간 부하 발생에 대한 한계치를 탐색, 예외 동작 상황 확인
데이터베이스 failover 상황, 자동 복구, 예외 동작 상황 확인
외부 요인(PG사)의 예외 상황 동작 확인
이 글은 실제 히빗 프로젝트(ver.2)를 혼자서 개발하면서 경험한 내용을 정리한 글입니다.
예상 독자: Spring Boot 기반 테스트 코드를 작성하는 개발자
이 글은 히빗 프로젝트(ver.2)에 Jacoco
를 설정한 과정에 대해 공유하고자 합니다.
아래 글부터는 히빗 프로젝트(ver.2) 를 히빗2
로 줄여서 작성했습니다.
히빗 프로젝트(ver.1)에서는 테스트 코드가 거의 없어서 QA 작업때 정말 많은 시간을 할애했다. (QA 작업에 대한 내용과 테스트 코드를 도입하게된 배경은 좋은 단위 테스트란? (feat. 히빗) 글에서 확인할 수 있습니다.)
그래서 이번에 히빗2 에서는 테스트 코드를 도입하게 되면서, 테스트 커버리지도 신경써야 한다고 생각했다.
(여기서 말하는 테스트 커버리지
란 시스템 및 소프트웨어 대해 충분한 테스트가 되었는지를 나태내는 정도를 말한다.)
토스 유튜브 채널에서 토스뱅크 이응준님이 발표하신 SLASH21 - 테스트 커버리지 100% 영상을 우연히 보게 되었다.
이응준님은 “클린 코더” 책을 읽다가 ‘테스트 커러비지 100%를 강력히 요구한다’ 는 글을 보고서 실제로 적용해보셨다고 한다. 토스 홈 리뉴얼 프로젝트에 2개월 동안 9천 라인의 테스트 코드를 작성하여 달성하였고, 커버리지 100%가 아니면 배포가 안되게 설정하셨다고 한다.
이응준님이 발표하신 내용에 따르면 높은 테스트 커버리지에는 다음과 같은 이점이 존재한다고 하셨다.
자신있게 누를 수 있는 배포 버튼
거침없는 리팩터링을 할 수 있음
전체 코드의 10% 이상을 수정하는 리팩터링을 master 브랜치에 걱정없이 merge하여 괜찮은 수준의 코드 가독성을 유지할 수 있음
불필요한 프로덕션 코드가 사라짐
시간이 갈수록 점점 쉬워지는 테스트 작성 (이미 작성한 테스트를 참조해서 새 테스트를 작성할 수 있음)
테스트가 없으면 리팩터링을 할 수 없고, 리팩터링을 하지 않는 코드는 이해할 수 없게 되며, 이러한 코드는 수정할 수 없다는 확신이 없다는 말씀을 들으면서 많은 공감이 되었다.
이로 인해 테스트의 중요성을 새롭게 깨달았고, 이번 프로젝트에서는 코드 커버리지
를 적극적으로 도입하기로 결정하게 되었다.
코드 커버리지
란 소프트웨어의 테스트 케이스가 얼마나 충족되었는지를 나타내는 지표 중 하나이다. 테스트를 진행하였을 때 ‘코드 자체가 얼마나 실행되었느냐’ 는 것이고, 이는 수치를 통해 확인할 수 있다.
Jacoco 는 Java 코드의 커버리지를 체크하는 라이브러리이다. 테스트 코드를 돌리고 그 커버리지 결과를 눈으로 보기 좋도록 html이나 xml, csv와 같은 리포트로 생성한다.
그리고 테스트 결과가 내가 설정한 커버리지 기준을 만족하는지 확인하는 기능도 있다.
여기서는 Java 코드가 섞인 Gradle 기준의 히빗2에 Jacoco
를 도입하여 코드 커버리지를 분석하고, 코드 커버리지가 80% 미만일 경우에 빌드가 실패되도록 설정했다.
아래 build.gradle
파일에 jacoco 플러그인을 가져오고 버전을 설정한다.
plugins {
// ...
id 'jacoco'
}
jacoco {
// JaCoCo 버전
toolVersion = '0.8.7'
}
jacoco에 플러그인을 불러왔다면, 프로젝트의 테스트 코드를 한번 실행시켜보면 아래와 같이 test.exec
파일이 생성된 것을 확인할 수 있다.
해당 파일은 jacoco가 테스트 코드를 실행하고, 코드 커버리지를 분석하여 만들어준 보고서 파일이다.
jacoco 폴더 안에 indext.html, index.csv, index.xml 파일은 밑에서 설명할
jacocoTestReport
을 이미 실행했기 때문에 생긴 파일이다.
Jacoco Gradle 플러그인에는 jacocoTestReport
와 jacocoTestCoverageVerification
task가 있다.
jacocoTestReport
: 바이너리 커버리지 결과를 사람이 읽기 좋은 형태의 리포트로 저장한다.
html 파일로 생성해 사람이 쉽게 눈으로 확인할 수 있고, SonarQube 등으로 연동하기 위해 html, csv, xml 같은 형태로도 리포트를 생성할 수 있다.
Jacoco는 위에 생성한 바이너리 커버리지(test.exec
) 파일을 사람이 읽을 수 있는 html, csv, xml 파일로 생성하는 기능을 제공한다.
jacocoTestReport {
reports {
// 원하는 리포트를 켜고 끌 수 있습니다.
html.enabled true
xml.enabled true
csv.enabled true
// 각 리포트 타입마다 리포트 저장 경로를 설정할 수 있습니다.
html.destination file("${buildDir}/jacoco/index.html")
xml.destination file("${buildDir}/jacoco/index.xml")
csv.destination file("${buildDir}/jacoco/index.csv")
}
}
위에 있는
${buildDir}
는 디렉터리 경로를 의미한다.
위에 이미 실행되어 test.exec
파일이 생성되었다고 가정하고 아래 명령어를 통해 jacocoTestReport
태스크를 실행하면 html, csv, xml 파일이 생기게 된다.
build.gradle -> jacoco에서 index.html
폴더와 index.csv
, index.xml
파일이 생성된 것을 확인할 수 있다.
html
파일은 index.html
폴더 내부에 존재한다.
jacocoTestReport
에 대한 자세한 내용은 JacocoReport 참고하자
jacocoTestCoverageVerification
: 내가 원하는 커버리지 기준
을 만족하는지 확인해주는 task이다.
예를 들어, 브랜치 커버리지를 최소한 80% 이상으로 유지하고 싶다면, 이 task에 설정하면 된다. test
task 처럼 Gradle 빌드의 성공/실패로 결과를 보여준다.
아래는 Hibit2에서 설정한 jacocoTestCoverageVerification
이다.
jacocoTestCoverageVerification {
violationRules {
rule {
enabled = true
element = 'CLASS'
limit {
counter = 'LINE'
value = 'COVEREDRATIO'
minimum = 0.80
}
}
rule {
// 규칙을 여러개 추가할 수 있다.
}
}
}
enabled : 규칙의 활성화 여부를 나타낸다. 기본값은 true
이다.
element : 커버리지를 체크할 단위를 설정한다. 아래와 같은 옵션이 있다. (Default값은 BUNDLE
)
BUNDLE
: 패키지 번들(프로젝트 모든 파일을 합친 것)
CLASS
: 클래스
GROUP
: 논리적 번들 그룹
METHOD
: 메서드
PACKAGE
: 패키지
SOURCEFILE
: 소스 파일
counter : 코드 커버리지를 측정할 때 사용되는 지표이다. (Default값은 INSTRUCTION
)
LINE
: 빈 줄을 제외한 실제 코드의 라인 수, 라인이 한 번이라도 실행되면 실행된 것으로 간주
BRANCH
: 조건문 등의 분기 수
CLASS
: 클래스 수, 내부 메서드가 한 번이라도 실행된다면 실행된 것으로 간주
METHOD
: 메서드 수, 메서드가 한 번이라도 실행된다면 실행된 것으로 간주
COMPLEXITY
: 복잡도
INSTRUCTION
: Java 바이트코드 명령 수
value : limit 메서드를 통해 지정할 수 있으며 측정한 커버리지를 어떠한 방식으로 보여줄 것인지를 말한다. (Default 값은 COVEREDRATIO
)
COVEREDRATIO
: 커버된 비율, 0부터 1사이의 숫자로 1이 100%
COVEREDCOUNT
: 커버된 개수
MISSEDCOUNT
: 커버되지 않은 개수
MISSEDRATIO
: 커버되지 않은 비율, 0부터 1사이의 숫자로 1이 100%
TOTALCOUNT
: 전체 개수
minimum : limit 메서드를 통해 지정할 수 있으며 counter 값을 value 에 맞게 표현했을 때 최솟값을 말한다.
이 값을 통해 jacocoTestCoverageVerification
의 성공 여부가 결정된다. (Default 값이 존재하지 않는다)
jacocoTestReport
에 대한 자세한 내용은 JacocoCoverageVerification 참고하자
이 글은 실제 히빗 프로젝트(ver.2)를 혼자서 개발하면서 경험한 내용을 정리한 글입니다.
이전에 작성한 좋은 단위 테스트란? (feat. 히빗) 글에 이어서 이번에는 통합 테스트와 슬라이드 테스트에 대한 글을 작성했다.
결론부터 말하면 나는 통합 테스트와 슬라이드 테스트를 각각의 레이어에 맞게 적용했다.
통합 테스트
@SpringBootTest
적용슬라이스 테스트
Presentation Layer(Controller) - @WebMvcTest
적용
Persistence Layer(Repository) - @DataJpaTest
적용
아래에는 Spring Boot 기반으로 통합 테스트와 슬라이스 테스트가 무엇이고, 어떻게 적용했는지 작성했다.
통합 테스트
(Integration Testing)는 시스템의 각 구성 요소가 올바르게 연동되는지 확인하는 것을 의미한다.
기능 테스트
가 사용자 입장에서 테스트하는 반면, 통합 테스트
는 소프트웨어 코드를 직접 테스트한다.
웹을 예로 들면 기능 테스트는 웹을 통해 게시글 기능을 테스트한다면, 통합 테스트는 게시글 기능에서 등록, 수정, 조회, 삭제를 직접 테스트하는 식이다.
요즘에는 기능 테스트를 인수 테스트
로 더 자주 부르는 경향이 있다. (인수 테스트
는 사용자 스토리(시나리오)에 맞춰 수행하는 테스트이다.)
일반적인 웹 애플리케이션은 프레임워크, 라이브러리, 데이터베이스, 구현한 코드가 주요 통합 테스트 대상이다. 게시글의 세부 기능에 대한 통합 테스트를 수행하면 스프링 프레임워크나 JPA 설정이 올바른지, SQL 쿼리가 맞는지, DB 트랜잭션이 잘 동작하는지 검증할 수 있다.
통합 테스트의 장점은 실제 환경에서 시스템이 어떻게 동작할지를 보다 정확하게 확인할 수 있다는 점이다. 비록 완벽하게는 아니지만, 통합 테스트는 거의 유사한 조건에서 시스템이 동작하는 것을 확인할 수 있다.
그러나 통합 테스트는 단위 테스트보다 여러 구성 요소를 포함하므로 단위 테스트보다 비용과 시간이 더 많이 소요된다. 여러 구성 요소가 함께 작동하기 때문에 문제 발생시 디버깅이 어려울 수 있다.
통합 테스트를 실행하려면 준비할 것이 많고 단위 테스트에 비해 실행 시간도 길지만, 그럼에도 불구하고 필요한 부분이라고 생각한다.
각 구성 요소가 올바르게 연동되는 것을 확인해야 하는데 이를 자동화
하기 좋은 수단이 통합 테스트 코드 이기 때문이다.
Spring Boot 에서는 클래스 상단에 @SpringBootTest
어노테이션을 붙여 통합 테스트를 수행할 수 있다.
@SpringBootTest
어노테이션을 사용하면 스프링 애플리케이션을 통해 테스트에 사용되는 ApplicationContext를 생성함으로써 작동한다.
이러한 어노테이션을 사용하면 우리 애플리케이션에서 사용하고 있는 모든 빈을 등록한 뒤 간편하게 테스트를 진행할 수 있다.
하지만 모든 빈을 등록하기 때문에 아래와 같은 단점을 가질 수 있다.
모든 빈을 등록하기 때문에 비교적 오랜 시간이 걸린다
모든 빈을 등록하기 대문에 의존성을 고려하지 않고 테스트를 진행할 수 있다. -> 테스트하고자 하는 객체의 의존성을 무시한채 테스트하게 된다.
결과적으로 웹을 실행시키지 않고 테스트 코드를 통해 빠른 피드백을 받을 수 있다는 장점이 희석된다.
이러한 @SpringBootTest
는 모든 빈을 등록한 채 테스트를 진행하는 통합 테스트에 적합한 어노테이션이다.
모든 빈을 등록하지 않으려면, classes 속성
을 통해 빈을 생성할 클래스를 지정하면 해당 클래스와 관련된 빈만 등록하게 된다.
@SpringBootTest
는 주로 Business Layer
에 사용된다.
Business Layer
(Service) 는 비즈니스 로직을 구현하는 역할로 Persistence Layer
와의 상호작용(Data를 읽고 쓰는 행위)를 통해 비즈니스 로직을 전개시키는 레이어이다.
아래는 @SpringBootTest
을 이용하여 실제 프로젝트에 작성한 테스트 중 일부를 가져왔다.
PostServiceTest
@ActiveProfiles("test")
@SpringBootTest
class PostServiceTest {
@Autowired
private PostRepository postRepository;
@Autowired
private ProfileRepository profileRepository;
@Autowired
private MemberRepository memberRepository;
@Autowired
private PostService postService;
@AfterEach
void tearDown() {
postRepository.deleteAll();
profileRepository.deleteAll();
memberRepository.deleteAll();
}
@DisplayName("게시글을 등록한 페이지를 반환한다.")
@Test
void 게시글을_등록한_페이지를_반환한다() {
// given
Member 팬시 = 팬시();
memberRepository.save(팬시);
Profile 팬시_프로필 = 팬시_프로필(팬시);
profileRepository.save(팬시_프로필);
PostCreateRequest request = PostCreateRequest.builder()
.title(게시글제목1)
.content(게시글내용1)
.exhibition(전시회제목1)
.exhibitionAttendance(전시관람인원1)
.possibleTime(전시관람희망날짜1)
.openChatUrl(오픈채팅방Url1)
.togetherActivity(함께하고싶은활동1)
.imageName(게시글이미지1)
.postStatus(모집상태1)
.build();
// when
Long newPostId = postService.save(팬시_프로필.getMember().getId(), request);
Post savedPost = postRepository.findByMemberId(팬시.getId()).orElse(null);
// then
assertThat(newPostId).isNotNull();
assertEquals("프로젝트_해시테크", savedPost.getTitle());
}
}
통합 테스트 코드를 작성할 때 보통은 2가지 입장(관점)으로 작성하는 경우가 있다.
Classicist
- 진짜 객체로 테스트를 하자. -> 상태 검증을 통한 의존 객체의 구현보다는 실제 입/출력 값을 통해 검증하는 테스트이다.
Mockist
- 모든 걸 mocking 위주(가짜 객체) 로 테스트를 하자. -> 행위 검증을 통해 의존 객체의 특정 행동이 이루어졌는지를 검증하는 테스트이다.
나의 경우 Business Layer
에 대해 테스트 할 때, Repository의 실체 객체를 사용하여 통합 테스트를 진행하였다. 즉, Classicist
관점으로 작성했다.
메일 전송 혹은 AWS S3 같은 외부 시스템을 요청하거나 연결할 때는 Mocking을 사용한다. 왜냐하면 이런 외부 시스템은 우리가 개발한 게 아니기 때문이다.
Mocking 위주로 테스트를 작성한다면 과연 실제 프로덕션 코드에서 런타임 시점에 일어날 일을 정확하게 Mocking 했다고 단언할 수 있을까에 대한 의구심이 들었다.
테스트를 했다고 실제 프로덕션과 100% 재현한다고 보장은 할 수 없다. 그런 리스크를 감당할 바에는 비용을 조금 들여서 실제 객체를 가져와서 테스트를 하는게 더 좋을 것 같다는 생각이 들어서 나는 이 방식을 택했다.
슬라이드 테스트
는 스프링에서 특정 영역의 레이어를 테스트하는 방법 중 하나이다.
즉, 특정 계층은 다른 계층에 의존하지 않기 때문에 필요한 빈들만 주입받아 독립적으로 테스트를 수행할 수 있다.
이를 통해 전체 Application Context를 로드하는 것보다 더 가볍고 빠른 테스트를 작성할 수 있다.
아래는 대표적인 슬라이드 테스트 어노테이션이다.
@WebMvcTest
@DataJpaTest
@WebMvcTest
는 웹 컨트롤러 관련 빈들만 테스트하는 어노테이션으로 주로 웹 계층의 테스트에 사용된다.
@WebMvcTest
어노테이션이 사용되면 웹 계층의 테스트를 위한 빈들이 주입되는데, 주로 주입되는 빈들로는 다음과 같은 것들이 있다.
Controller
Interceptor
ViewResolver
Converter, Formatter
MessageConverter
HandlerMapping
ExceptionHandler
ResourceHandler
이 빈들을 사용하여 웹 레이어의 동작을 테스트하고, 의존성 주입 및 상호 작용을 확인할 수 있다.
@WebMvcTest
는 주로 Presentation Layer
에 사용된다.
Presentation Layer
(Controller)는 외부 세계의 요청을 가장 먼저 받는 계층 으로 파라미터에 대한 최소한의 검증을 수행한다.
아래는 @WebMvcTest
를 이용한 실제 프로젝트의 일부 테스트 코드를 가져왔다.
PostControllerTest
@WebMvcTest(controllers = PostController.class)
class PostControllerTest {
private static final String AUTHORIZATION_HEADER_NAME = "Authorization";
private static final String AUTHORIZATION_HEADER_VALUE = "Bearer aaaaaaaa.bbbbbbbb.cccccccc";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private PostService postService;
@MockBean
private AuthService authService;
@DisplayName("신규 게시글을 등록한다.")
@Test
void 신규_게시글을_등록한다() throws Exception {
// given
PostCreateRequest request = PostCreateRequest.builder()
.title(게시글제목1)
.content(게시글내용1)
.exhibition(전시회제목1)
.exhibitionAttendance(전시관람인원1)
.openChatUrl(오픈채팅방Url1)
.togetherActivity(함께하고싶은활동1)
.possibleTime(전시관람희망날짜1)
.openChatUrl(오픈채팅방Url1)
.togetherActivity(함께하고싶은활동1)
.imageName(게시글이미지1)
.postStatus(모집상태1)
.build();
// when & then
mockMvc.perform(post("/api/posts/new")
.header(AUTHORIZATION_HEADER_NAME, AUTHORIZATION_HEADER_VALUE)
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON)
)
.andDo(print())
.andExpect(status().isCreated());
}
}
사실상 비즈니스 로직 보다는 넘겨온 값이 중요하기 때문에 하위 2개의 Layer를 Mocking
(가짜 객체로 대신)하고 테스트를 진행했다.
여기서 MockMvc
이란 Mock(가짜) 객체를 이용해서 Spring MVC 동작을 재현할 수 있는 테스트 프레임워크이다.
Spring Data JPA를 사용했기 때문에, 테스트를 위해 DataJpaTest
를 활용할 수 있다.
@Entity
, JpaRepository
등 JPA 사용에 필요한 빈들을 등록하여 테스트할 때 사용된다.
@DataJpaTest
는 주로 Persisence Layer
에 사용된다.
Persistence Layer
(Repository)는 Data Access의 역할로 비즈니스 가공 로직이 포함되어서는 안된다. Data에 대한 CRUD 작업에만 집중한 레이어이다.
아래는 @DataJpaTest
를 실제 프로젝트의 일부 테스트 코드를 가져왔다.
PostRepositoryTest
@ActiveProfiles("test")
@DataJpaTest
class PostRepositoryTest {
@Autowired
private MemberRepository memberRepository;
@Autowired
private ProfileRepository profileRepository;
@Autowired
private PostRepository postRepository;
private Member member;
private Profile profile;
private Post post;
@BeforeEach
void setUp() {
member = 팬시();
memberRepository.save(member);
profile = 팬시_프로필(member);
profileRepository.save(profile);
post = 프로젝트_해시테크(member);
postRepository.save(post);
}
@DisplayName("게시글과 회원 테이블이 정상적으로 매핑이 된다.")
@Test
void 게시글과_회원_테이블이_정상적으로_매핑이_된다() {
// given
Post foundPost = postRepository.getById(post.getId());
// when & then
Assertions.assertThat(foundPost.getMember().getId()).isNotNull();
}
@DisplayName("특정 회원 ID에 해당하는 게시글을 찾는다.")
@Test
void 특정_회원_ID에_해당하는_게시글을_찾는다() {
// given
Long memberId = post.getMember().getId();
// when
Optional<Post> actual = postRepository.findByMemberId(memberId);
// then
assertThat(actual).isPresent();
Post foundPost = actual.get();
assertThat(foundPost.getMember().getId()).isEqualTo(memberId);
}
}
@DataJpaTest
는 스프링 서버를 띄어서 테스트를 하는데, @SpringBootTest
보다 가볍다(속도가 빠르다).
JPA 관련된 Bean 들만 주입을 해줘서 서버를 띄어준다.
또한 @DataJpaTest
는 기본적으로 @Transactional
어노테이션이 있기 때문에 테스트가 완료되면 자동으로 롤백된다.
@DataJpaTest
내부에 들어가서 확인해보면, @Transactional
이 있다.
PostRepositoryTest
의 경우 @DataJpaTest
로 인해 트랜잭션 롤백이 자동으로 이루어지므로 테스트가 데이터베이스에 영향을 미치지 않는다.
그래서 @DataJpaTest
내부에 @Transactional
어노테이션이 있어서 PostRepositoryTest
에는 수동으로 삭제하는 tearDown() 메서드의 필요가 없어지게 된다.
통합 테스트와 슬라이드 테스트의 어노테이션을 적용함으로써 전보다 테스트 코드에 대한 이해를 높일 수 있게 되었다.
테스트 코드는 상대적으로 운영 코드에 비해 관심이 덜 받는 편이지만, 이는 실제로 안정성과 효율성, 그리고 확장성을 유지하기 위해 필수적으로 작성해야하는 부분이라고 생각한다.
테스트의 목적
과 필요한 빈
들에 대한 고민을 통해 적절한 어노테이션을 선택하는 것이 효과적인 테스트 코드 작성의 핵심이지 않을까 한다.
이를 통해 불필요한 리소스를 최소화하고, 운영 코드의 변화에 유연하게 대응할 수 있게 되었다.
테스트 코드를 관리하는 것은 운영 코드
의 안정성과 효율성을 높이는 핵심 요소임을 다시 한번 강조하고 싶다.