import spock.lang.Specification
class UpdateBizFleetOrderVinsToPurchasedHandlerTest extends Specification {
}
* 이때, 주의할 점은 클래스를 ctrl + command + T 로 만들 때, 기존에 Test Library JUnit으로 사용하시던 분들은 Groovy로 바꿔줘야 합니다.
4. 라이프 사이클에 맞는 메소드 생성
def setupSpec() {} // 모든 테스트 케이스 실행 전 실행 (@BeforeClass)
def setup() {} // 각 테스트 케이스 실행 전 마다 실행 (@Before)
def cleanup() {} // 각 테스트 케이스 실행 후 마다 실행 (@After)
def cleanupSpec() {} // 모든 테스트 케이스 실행 후 실행 (@AfterClass)
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/~~?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=??
spring.datasource.password=??
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql 사용
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.hibernate.ddl-auto=none
spring.jpa.generate-ddl=false
# 로깅 레벨
logging.level.org.hibernate=info
# 하이버네이트가 실행한 모든 SQL문을 콘솔로 출력
spring.jpa.properties.hibernate.show_sql=true
# SQL문을 가독성 있게 표현
spring.jpa.properties.hibernate.format_sql=true
# 디버깅 정보 출력
spring.jpa.properties.hibernate.use_sql_comments=true
두번째 오류
Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set
[ 해결방법 ]
두 오류를 종합해봤을 때, application.properties 의 MySQL 설정을 읽지 못하고 있는 것으로 보입니다.
그 이유는 제가 META-INF 에 PersistenceUnit 을 설정하기위해 persistence.xml 을 만들어 놓고
server.address=localhost
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/TEST_DB?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.username=test_user
spring.datasource.password=admin
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# mysql 사용
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
# 로깅 레벨
logging.level.org.hibernate=info
# 하이버네이트가 실행한 모든 SQL문을 콘솔로 출력
spring.jpa.properties.hibernate.show_sql=true
# SQL문을 가독성 있게 표현
spring.jpa.properties.hibernate.format_sql=true
# 디버깅 정보 출력
spring.jpa.properties.hibernate.use_sql_comments=true
개발자는 new 키워드, Interface 호출, 팩토리 호출 방식으로 객체를 생성하고 소멸합니다.
하지만 Spring Container를 사용하면 이러한 역할을 대신 해줍니다.
1) 제어 흐름을 외부에서 관리하게 됩니다.
2) 객체들 간의 의존 관계를 스프링 컨테이너가 런타임 과정에서 알아서 생성해줍니다.
Spring Bean 등록 방식
1. Component Scan
- @Component 를 클래스 위에 명시하면 스프링이 알아서 등록해줍니다.
- @Controller, @Service, @Repository, @Configuration 은 @Componenet를 상속 받고 있으므로
모두 컴포넌트 스캔 대상입니다.
- Controller : Spring MVC Controller - Repository : 스프링 데이터 접근 계층으로 해당 계층의 예외는 모두 DataAccessException 으로 변환 - Service : 핵심 비지니스로 인식 ( 가독성 역할 ) - Configuration : Spring 설정 정보로 인식하고, 스프링 빈이 싱글톤 유지를 하도록 처리해줌
컴포넌트 스캔 방법1 ( @Component를 붙쳐주는 방법 )
1) 각 서비스/레포지토리 등에 어노테이션을 달아줌 ( @Repository, @Service, @Controller )
@Service
public class MemberServiceImp implements MemberServiceInterface{
private MemberRepository memberRepository;
@Autowired
public MemberServiceImp(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 회원가입
@Override
public Long join(Member member) {
// 휴대폰 번호 중복 체크
memberRepository.save(member);
return member.getId();
}
}
@Repository
public class JPQLMemberRepository implements MemberRepository{
public Member save(Member member) {
return member;
}
}
2) 빈 등록이 됬으면 의존성 주입
컴포넌트 스캔 방법2 ( @Bean를 붙쳐주는 방법 )
1) IoC컨테이너에 등록을 해줌 ( @Configuration )
@Configuration
public class SpringConfig {
//등록
@Bean
public EntityManager getEm() {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("5xik");
return emf.createEntityManager();
}
@Bean
public MemberServiceInterface memberService() {
return new MemberServiceImp(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new JPQLMemberRepository(getEm());
}
}
2) Repository / Serivce 에 필요한 의존성 주입
public class JPQLMemberRepository implements MemberRepository{
private EntityManager em;
@Autowired
public JPQLMemberRepository(EntityManager em) {
this.em = em;
}
public EntityTransaction getTx() {
return em.getTransaction();
}
public Member save(Member member) {
em.persist(member);
return member;
}
}
컴포넌트 스캔 방법3 ( AutoConfig 설정 방식 )
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =
Configuration.class))
public class AutoSpringConfig {
}
@ComponentScan 은 @SpringBootApplication 에 기본적으로 포함되어 있습니다.
그런데 만약 기존에 @Configuration 으로 컨테이너를 만들어 놨다면 두 설정파일을 읽어서 오류가 발생하겠죠??
이렇게 직접 컴포넌트 스캔을 설정해주면은 기존의 IoC컨테이너에 영향을 주지 않고
컴포넌트 스캔을 구현할 수 있습니다.
만약, 다른 IoC컨테이너가 없다면 따로 AutoConfig는 따로 설정할 필요 없습니다.
@SpringBootApplication 에 기본적으로 Component Scan 이 포함되어 있다는 의미는 IoC컨테이너가 없이도 @Component 관련 어노테이션을 다 읽는다는 의미이므로
@Repository 로 빈 등록하고, @Configuration 내부에서 @Bean으로 또 등록하는 실수를 하게되면
Unique~~~ 와 관련 에러가 발생하니 유의하시길 바랍니다.
< 관련 오류 > https://tjdwns4537.tistory.com/67?category=949574
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'JPQLMemberRepository' defined in file [/Repository/JPQLMemberRepository.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'javax.persistence.EntityManager' available: expected single matching bean but found 2: getEm,org.springframework.orm.jpa.SharedEntityManagerCreator#0
해결 과정.
1) IoC컨테이너에서 EntityManagerFactory, EntityManager를 따로 빈 등록하는 것이 아닌 하나의 함수에 묶어서 빈 등록
2) 레포지토리에서는 엔티티 매니저만 주입을 받음
3) 테스트코드에서는 레포지토리만 주입 받음
위의 과정 중 발생한 오류
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'xik.ShoppingMall.Repository.JPQLMemberRepositoryTest': Unsatisfied dependency expressed through field 'repository'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'xik.ShoppingMall.Repository.MemberRepository' available: expected single matching bean but found 2: JPQLMemberRepository,memberRepository
오류 키워드
1) NoUniqueBeanDefinitionException
2) available: expected single matching bean but found 2: JPQLMemberRepository,memberRepository
해당 키워드를 생각해보면 MemberRepository 두 개가 빈 등록 되어있어서 발생하는 오류임을 알 수 있습니다.
해결방법
: 기존에는 IoC컨테이너에서도 레포지토리를 빈 등록해주고, 레포지토리 클래스에서 @Repository를 해서 두번 빈 등록 해주고 있었습니다.
- JPQLMemberRepository 에서 @Repository 할 꺼면 컨테이너에 레포지토리에 대한 빈 등록은 필요가 없습니다.
- IoC컨테이너를 쓰려면 @Repository는 필요가없습니다.
- 만약 두 군대에 레포지토리 등록을 사용하고 싶다면 JPQLMemberRepository 에 @Primary 를 붙쳐주면 해결됩니다.
여러 클라이언트가 하나의 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하게 설계하면 안된다.
이는 아주 중요한 개념입니다.
왜 중요할까요 ??
공유값이 바뀌는 에러가 발생
코드로 예를 들어보겠습니다.
[ SingleTon class ]
public class StateFulService {
private int price; // 상태 유지 필드
public void order(String name, int price) {
System.out.println("name:" + name + " price:" + price);
this.price = price;
}
public int getPrice() {
return price;
}
}
[ Test code ]
class StateFulServiceTest {
@Test
void FailSigleTon() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StateFulService stateFulService1 = ac.getBean("stateFulService", StateFulService.class);
StateFulService stateFulService2 = ac.getBean("stateFulService", StateFulService.class);
stateFulService1.order("parkA", 10000);
stateFulService2.order("parkB", 20000);
int price = stateFulService1.getPrice();
System.out.println("price: " + price);
Assertions.assertThat(stateFulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig{
@Bean
public StateFulService stateFulService() {
return new StateFulService();
}
}
}
위의 코드에서 보면 과정은 이렇습니다.
[ 테스트 코드에서 생성된 스프링 컨테이너는 기본적으로 싱글톤을 사용할 수 있게 설계되어 있습니다. ]
1) parkA라는 주문을 후 getPrice함수로 바로 가격을 출력해줘야하는데
2) 그 중간에 parkB라는 주문이 들어옵니다.
3) 그러면 price 라는 객체를 공유하고 있기 때문에 10000 -> 20000으로 바꿔버리게 됩니다.
4) 그래서 stateFulService1 객체의 getPrice() 함수를 사용해도 20000이 출력되게 되는 오류입니다.
예시의 싱글톤이 이러한 에러가 발생하는 이유는 설계에 아주 큰 문제점이 있습니다.
*** " 테스트 코드 (클라이언트) 가 값을 변경한다 "라는 것입니다. ***
그래서 스프링 빈은 항상 무상태로 설계해야 하는 것입니다.
그렇다면 위의 예시를 다시 무상태로 설계해보겠습니다.
[ SingleTon class ]
public class StateFulService {
public int order(String name, int _price) {
System.out.println("name:" + name + " price:" + _price);
int price = _price;
return price;
}
}
[ 테스트 코드 ]
class StateFulServiceTest {
@Test
void FailSigleTon() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StateFulService stateFulService1 = ac.getBean("stateFulService", StateFulService.class);
StateFulService stateFulService2 = ac.getBean("stateFulService", StateFulService.class);
int price1 = stateFulService1.order("parkA", 10000);
int price2 = stateFulService2.order("parkB", 20000);
Assertions.assertThat(price1).isEqualTo(10000);
}
static class TestConfig{
@Bean
public StateFulService stateFulService() {
return new StateFulService();
}
}
}
이렇게 공유된 필드를 사용하는게 아니라 지역변수를 통해서 공유하지 못하게 해결할 수 있습니다.
이 외에 싱글톤의 문제점을 해결하는 방법으로 @Configuration 에노테이션을 사용하는 방법이 있습니다. 이 부분은 검색해보시면 많은 자료를 찾아볼 수 있습니다.
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'springConfig': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'xik.ShoppingMall.Repository.MemberRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
위와 같은 에러가 발생하였습니다. 위의 에러를 해석해보면 빈을 등록할 때 에러가 발생한것으로 보입니다.
클라이언트 코드에서 스프링 컨테이너를 사용하는 버젼으로 바꾸기 위해 사용하다가 에러가 발생했습니다.
뭐가 문제인지 에러 코드를 다시 읽어보겠습니다.
UnsatisfiedDependencyException : Error creating bean with name 'springConfig'
: springConfig 라는 이름을 찾고있네요 ??
근데 제 코드를 보면 applicationContext 객체를 불러올때도 SpringConfig를 사용해주고 있고,
애초에 클래스명도 SpringConfig로 적어둔 상태입니다.
근데 확인을 해보니 애초에 맨 앞 철자가 소문자로 변경된 형태로 등록되기 때문에 상관없는 문제라고 하네요.
그러면 다음 에러코드를 읽어보겠습니다.
: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'xik.ShoppingMall.Repository.MemberRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
위의 코드를 읽어보니 Repository에서의 bean을 등록해줄 때 생기는 문제 같습니다.
코드의 구성도를 보면
[ SpringConfig ]
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
에러 코드 : org.junit.jupiter.api.extension.ParameterResolutionException: No ParameterResolver registered for parameter [java.lang.Long arg0] in method [void xik.ShoppingMall.Service.OrderServiceImpTest.createOrder(java.lang.Long,java.lang.String,int)].
2) @RequestMapping 이 붙은 메서드들이 핸들러에 등록되는 것은 Application refresh되는 과정에서 일어납니다.
refresh과정에서 Spring Application 구동을 위해 많은 Bean들이 생성되고,
그 중 하나가 RequestMappingHandlerMapping 입니다. 이 Bean은 우리가 @RequestMapping 으로 등록한 메서드들을 가지고 있다가 요청이 들어오면 Mapping해주는 역할을 합니다.
3) Bean으로 등록된 HandlerMapping이 변수들을 찾아서 Adapter를 거쳐 실행합니다.
ㅇ Handler
: Spring MVC에서는 핸들러가 @Controller클래스를 의미합니다.
@GetMapping / @PostMapping 을 핸들러 메서드라고 부릅니다.
결국 Handler Mapping 이란 사용자의 요청과 이 요청을 처리하는 Handler를 매핑해주는 역할을 하는 것입니다.
ㅇ HandlerAdapter
: 스프링 부트가 아닌 다른 프레임워크의 핸들러를 Spring MVC에 통합하기 위해서는
HandlerAdapter를 사용할 수 있습니다.
ㅇ @RequestMapping
1) value
: URL값으로 매핑 조건을 부여합니다. 보통 호스트주소와 포트번호를 제외한 url주소를 넣어줍니다.
이는 다중 요청이 가능하여 @RequestMapping ( {"/hello". "/hello-rule", "/hello/**" } ) 형식으로 사용할 수 있습니다.
2) method
: HTTP request 메소드 값을 매핑조건으로 부여하는데 GET,POST,HEAD,OPTIONS,PUT,DELETE,TRACE 메소드가 존재합니다.
ㅇ PostMapping / GetMapping
: Post/Get method 로 RequestMapping을 합니다.
1) @PostMapping : HTTP Post Method에 해당하는 단축 표현으로 서버에 리소스를 등록(저장)할 때 사용합니다.
2) @GetMapping : HTTP Get Method에 해당하는 단축 표현으로 서버의 리소스를 조회할 때 사용합니다.
3) @DeleteMapping : 서버의 리소스를 삭제
4) @PutMapping : 서버의 리소스를 모두 수정
5) @PatchMapping : 서버의 리소스를 일부 수정
ㅇ 현재 저의 소스코드로 설명
@GetMapping("/new")
public String New() {
return "/Login/회원가입";
}
@PostMapping("/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
member.setPhoneNumber(form.getPhoneNumber());
memberService.join(member);
return "redirect:/";
}
: 이러한 문제점 세가지를 해결한게 org.assertj.core.api.Assertions 에서 제공하는 assertThat입니다.
필요한 메서드를 검색해서 import 해야하는 번거로움도 없고, 가독성도 좋습니다.
* 결론
: org.assertj.core.api.Assertions 에서 제공하는 Assetions 로 테스트를 진행하도록 하자.
* 그 외 테스트 코드
1) Assertions.assertThat().isInstanceOf()
: assertThat - 테스트할 대상, isInstanceOf - 앞의 테스트대상의 인스턴스인가? 라고 묻는 것으로
앞에 자식이 될 요소, 뒤를 부모가 될 요소를 넣는다. 즉, 참조나 상속을 받고있는지 묻는 것이다.
* org.junit.jupiter.api.Assertions.assertThrows 클래스를 사용
2) Assertions.assertThrows(에러클래스,에러발생)
: 일부러 오류를 발생시키는 테스트 코드이다. 이는 예외로 들어와서 오류가 제대로 나오는지 테스트하기 위함이다.
: assertThrows의 첫번째 인자 - 에러, 두번째 인자 - 에러 발생 요소
: 제대로 에러가 뜨면 True를 반환