[TIL] Spring Boot - 스프링 빈, 컴포넌트, Test코드와 JUnit 5
오랜만에 다시 시작하는 스프링 공부다!
저번에 배우던 책이 너무 실무 위주이기도 하고 여러 유용한 정보를 담기엔 그래도 조금 분량이 적은 거 같아 OAuth2 부분만 마무리하고 뒤에 배포단계는 다음에 해보기로 했다.
오늘은 새로운 교재를 발견하고 그에 대한 공부한 내용을 적어 보려고 한다.
배운 교재
[무료] 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다., - 강의 소개 | 인프런
www.inflearn.com
평점이나 리뷰가 매우 좋다못해 거의 찬양글이 빼곡해서 약간 혹하고 배우기 시작해 봤다. 초반 부분 조금 늘어지는 느낌도 있지만 꼼꼼히 배우고 넘어가는 느낌이 확실히 드는 지라 지금까지는 괜찮다고 생각한다 :)
아래는 배운 내용 중 핵심만 따로 빼두고 정리해보았다.
Test코드와 JUnit5
소프트웨어를 개발할 때 내가 짠 코드나 로직이 정상으로 작동하는지 확인을 위해 작성하는 코드다.
모두가 아마 System.out.println()으로 내가 작성한 코드가 잘 작동하는 지 확인하는 코드를 짜본 경험이 있을 것이다. 그런 코드도 테스트 코드이지만, 여기선 테스트 라이브러리를 사용하는 코드를 이야기하려고 한다.
JUnit이란
단위 테스트 (함수, 클래스 등 작은 단위로 쪼개어 기능을 시험하는 것) 에 사용되는 대표적인 라이브러리로, 가볍고 다양한 곳에서 적극적으로 사용되는 라이브러리이다. 예시를 한번 들어보자.
사용자를 저장, 관리하는 Repository 클래스를 생성했다고 가정해 보자.
package com.hadoo.kcdevdes.repository;
import com.hadoo.kcdevdes.domain.Member;
import org.springframework.stereotype.Repository;
import java.util.*;
public class MemoryMemberRepository {
private static Map<Long, Member> store = new HashMap<>();
private static long sequence = 0L;
@Override
public Member save(Member member) {
member.setId(++sequence);
store.put(member.getId(), member);
return member;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.ofNullable((store.get(id)));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
코드는 잘 만들어졌지만 이걸 확인하기 위해 우린 테스트 코드를 작성할 것이다. 테스트 코드는 다음과 같이 작성한다.
package com.hadoo.kcdevdes.repository;
import com.hadoo.kcdevdes.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
class MemoryMemberRepositoryTest {
MemoryMemberRepository repository = new MemoryMemberRepository();
@AfterEach
public void afterEach() {
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("Spring");
repository.save(member);
Member result = repository.findById(member.getId()).get();
assertThat(member).isEqualTo(result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("Spring");
repository.save(member1);
Member member2 = new Member();
member2.setName("Spring2");
repository.save(member2);
Member result = repository.findByName("Spring2").get();
assertThat(result).isEqualTo(member2);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("Spring1");
repository.save(member1);
Member member2 = new Member();
member2.setName("Spring2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
코드 컨벤션은 보통 테스트하고자 하는 단위(여기선 클래스 이름) 뒤에 Test를 붙이고 테스트를 진행한다. @Test 어노테이션 1개당 1개의 테스트를 진행하고 전체 테스트를 돌리면 위에선 3개의 어노테이션이 붙어있으니 3번의 테스트가 진행된다!
여기서 중요한 도구가 몇 가지 있는데, @AfterEach와 @BeforeEach이다.
Test에서 우린 모든 테스트가 1개의 MemoryMemberRepository를 공유하도록 만들었다. 문제는 테스트 코드는 랜덤 하게 실행되는 거라 어떤 테스트가 먼저 끝나고 먼저 시작되는지 우린 장담할 수 없기에 이전 테스트가 다음 테스트에 영향을 끼치는 경우가 있을 수 있다는 거다.
예를 들어 책을 3 상자에 나누어 넣어두고 옮기려 하는 데 상자 옮기는 순서는 아무도 모르고, 빼는 도중에 누가 다시 책을 상자에 담고 그러면 모든 게 꼬이지 않겠는가??
그렇기에 @AfterEach 어노테이션은 각 테스트가 마무리될 때의 행동을 정의해 준다. 여기선 모든 repository에 들어가있는 데이터를 삭제하는 역할을 한다.
마찬가지로 @BeforeEach는 각 테스트가 시작되기 전에 행동을 정의해준다. 여기선 안 적혀있지만, 새로운 객체 할당이나 값을 새롭게 정의하는 등 응용 방법은 많이 있을 수 있다.
끝!
스프링 빈
스프링은 우리가 다루는 라이브러리인 것은 알고 있는 데, 스프링 빈이란 무엇일까? 빈은 스프링 컨테이너에 의해 관리되는 재사용 가능한 객체를 의미한다. 즉, 인스턴스화된 자바 객체이면서 스프링 컨테이너가 관리하는 것이 스프링 빈이란 것이다.
스프링 빈은 꽤나 중요한 요소인데, 스프링은 프로그래머가 객체를 생성하는 것이 아닌, 스프링 컨테이너가 객체를 적재적소에 넣어주는 (주입)을 하기 때문이다. 프로그래머가 여기저기 객체를 만들고 뿌려놓으면 서로 간의 관계가 복잡하고 유지보수가 힘들어지지만, 빈이란 객체들을 스프링 컨테이너가 관리하고 넣고 활용하기에 우린 그런 복잡한 의존 관계를 생각하지 않고, 그저 필요할 때 코드를 가져다 사용만 하면 되는 것(재사용성 상승)이 된다. 이것이 의존성 주입이다.
그렇기에 우린 우리가 만든 클래스를 스프링 빈으로 등록하는 과정이 필요하다. 일반적으로 생성자를 통한 주입이 있다.
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired // 의존성 주입 (스프링 컨테이너가 판단 하에 주입을 함)
public MemberController(MemberService memberService){
this.memberService = memberService;
}
}
@Controller란 어노테이션은 @Component란 어노테이션을 담고 있고, @Component는 이 컴포넌트가 스프링 빈이란 것을 스프링에게 알려주는 역할을 한다. 그렇기에 MemberController는 빈이 되었다.
@Autowired는 자동으로 필요한 객체를 넣어달라고 스프링에게 요구하는 어노테이션이다. 스프링이 public MemberController()란 생성자에 적재적소에 필요한 MemberService 객체를 넣어주도록 요구하는 것이다. 물론 여기에 들어갈 객체도 미리 @Component로 빈 등록이 되어있어야 한다. ^^
그래서 우린 new를 사용하지 않고 해당 객체의 기능을 불러다 사용할 수 있게 되는 것이다.
@Contoller와 @Service, @Repository는 모두 @Component를 세분화시킨 어노테이션이다. 즉, 모두 똑같은 Component 어노테이션이랑 다를 것이 없는 것!
+) 스프링 빈은 별도 설정이 없는 한, 모두 싱글톤으로 동작한다. 즉, 다른 Controller에서 사용한 Service 빈이 여기서도 똑같이 사용될 수 있다는 것이다. (싱글톤은 여러 개 생성해도 동일한 객체 (메모리 주소가 동일함)가 반환되도록 하는 디자인 패턴을 의미!)
이다음엔 자바 객체를 수동으로 등록하는 것을 배워 볼 예정이다~