스프링 입문 - 코드로 배우는 스프링 부트, 웹MVC, DB접근 기술
웹 브라우저로부터 URL을 전송받았을 때 동작 방식(웹 MVC)
웹 브라우저에서 서버로 URL을 전송했을 때 작동 방식은 아래 2가지 경우로 나눌 수 있다.
1. Controller에서 처리할 수 있는 URL을 전송받았을 경우
톰캣 서버를 거쳐 스프링 컨테이너에 도착했을 때,
웹 브라우저로부터 받은 URl에 대응되는 명령이 Controller에 있는 경우 Controller 내부 로직을 수행한다.
이 때, return을 통해 반환하는 데이터가 문자열인 경우
위의 그림에서처럼 viewResolver가 대응 되는 view 파일이 있는지 살펴본다.
(만약 전송할 model이 있다면 view로 같이 전달한다.)
있는 경우엔 문자열의 앞뒤로 문자열을 덧붙여 페이지로 만들고 이를 웹 브라우저에 html 형식으로 전송한다.
2. Controller에서 처리할 수 없는 URL을 전송받았을 경우
톰캣 서버를 거쳐 스프링 컨테이너에 도착했을때,
웹 브라우저로부터 받은 URL에 대응되는 Controller가 없는 경우
resources 폴더의 static에 존재하는 정적 파일을 검색한다.
그리고 대응 되는 정적파일(이미지, html 등)이 있으면 해당 파일을 웹 브라우저로 전송한다.
(Controller가 정적 파일보다 우선순위가 높다.)
스프링에서 자주 쓰이는 애노테이션
@ResponseBody
viewResolver를 사용하지 않도록 한다.
즉, 웹 브라우저로부터 전송받은 데이터를 Controller를 통해 가공하여 다시 웹 브라우저로 반환하는 경우
html 방식이 아닌 해당 데이터 자체를 HTTP 내부 BODY에 직접 넣어 반환한다.(JSON 방식으로 객체 반환)
반환하는 데이터가 객체면 JsonConverter를 통해 JSON 방식으로,
반환하는 데이터가 단순 문자열이면 StringConverter를 통해 문자열로 반환한다.
(@ResponseBody 사용을 통해 viewResolver 대신 HttpMessageConverter가 수행됨)
@BeforeEach
각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고,
의존관계도 새로 맺어준다.
@AfterEach
각 테스트 실행 후에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고,
의존관계도 새로 맺어준다.
@Autowired
스프링에 연관된 객체를 스프링 컨테이너에서 찾아 넣어준다.
이처럼 객체 의존관계를 외부에서 주입해주는 것을 DI(Dependency Injection)라고 한다.
변수, 생성자, setter 앞에 사용할 수 있다.
(단 , Spring에서는 생성자가 1개만 있는 경우 @Autowired를 생략해도, 생성자 안의
변수에 빈 객체를 자동으로 주입시켜준다.)
스프링 빈을 등록하는 2가지 방법
1. 컴포넌트 스캔과 자동 의존관계 설정
클래스 앞에 @Componenet 애노테이션을 붙여 해당 클래스의 객체를 스프링 빈에 자동 등록한다.
@Controller, @Service, @Repository도 @Componet를 포함하는 애노테이션으로
명칭만 다를 뿐 같은 용도로 사용된다.
2. 자바 코드로 직접 스프링 빈 등록하기
이전 1번에서 스프링 빈에 등록하고 싶은 객체의 클래스 앞에 @Component 애노테이션을 붙혔었다.
이번엔 그것들을 다 지우고, 아래와 같이 @Configuration 애노테이션을 통해 설정 파일을 만든 뒤
메소드를 통해 빈 객체를 등록한다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
그리고 위 방식으로 생성한 빈 객체를 아래와 같이 MemberController에서
@Autowired를 통해 MemberService 객체를 주입받을 수 있다.
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberService(MemberService memberService) {
this.memberService = memberService;
}
}
그러면 위 그림처럼, Controller는 Service를 주입받고, Service는 Repository를 주입 받는다.
참고. 스프링은 스프링 컨테이너에 스프링 빈을 동록할 때 기본적으로 싱글톤으로 등록한다.
이로 인해 객체를 유일하게 하나만 등록해서 모두가 공유한다.
싱글톤 방식의 빈 객체을 사용하지 않고, Serevice에서 Repository를 new 해서 새로운 객체로 생성해 주입했다고 생각해보자.
그러면 하나의 Repository를 사용하는 Service가 많을 경우, 각 Service에서 사용하는 Repository가 다른 객체일 것이다.
이는 데이터 저장소가 하나가 아니라 여러개가 된다는 것으로 의도하지 않은 결과가 발생할 수 있다.
따라서 스프링 빈 객체는 이를 방지하기위해 싱글톤으로 등록된다.
(실무에서 사용되지 않으나 설정에 따라 이를 싱글톤이 아니게도 할 수는 있다.)
DB 접근 기술
DataSource
데이터베이스 커넥션을 획득할 때 사용하는 객체
스프링 부트는 application.properties에 작성된 데이터베이스 커넥션 정보를 바탕으로 DataSource를 자동으로 생성하고 빈으로 만들어둔다.
@Configuration
public class SpringConfig {
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
}
그래서 위와 같은 방법으로 의존 주입을 받을 수 있다.
(생성자가 하나이므로 @Autowired를 사용하지 않아도 스프링에서 필요한 빈 객체를 자동으로 주입해준다.)
@Transactional
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작한다.
메서드가 정상 종료되면 트랜잭션을 커밋하고, 만약 런타임 예외가 발생하면 롤백하여 취소시킨다.
만약 JPA를 사용하는 경우, 모든 데이터 변경은 해당 애노테이션 사용을 통해 트랜잭션 안에서
실행되어야 한다.
단, Test에서 @Transactional 애노테이션을 사용되는 경우는 의미가 다르다.
Test 실행 중 변경했던 db 내용들을 원래대로 다시 돌리는 역할을 수행한다.
이유는 테스트는 계속해서 반복할 수 있어야 하기 때문으로,
해당 트랜잭션을 메서드 Test 종료시 commit하지 않고 Rollbak시켜 db에 반영하지 않도록 한다.
(만약 @Test에서 @Transactional 애노테이션이 쓰여져 있어도 commit하고 싶다면 @Commit 애노테이션을 사용한다.)
AOP
AOP(Aspect-Oriented Programming) : 관점 지향 프로그래밍
공통 관심 사항(cross-cuting concern)과 핵심 관심 사항(core-concern)을 구분하고
개발자가 원하는 핵심 관심 사항 로직 앞 또는 뒤에 공통 관심 사항을 작동시키도록 한다.
@Component
@Aspect
public class TimeTraceAop {
@Around("execution(* hello.hellospring..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
System.out.println("START: " + joinPoint.toString());
try {
return joinPoint.proceed();
} finally {
long finish = System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("END: " + joinPoint.toString()+ " " + timeMs +"ms");
}
}
}
1) @Component : 애노테이션을 통해 AOP를 빈 객체로 스프링 빈에 등록한다.
2) @Aspect :
3) @Around : AOP를 적용시킬 클래스들이 담긴 패키지를 작성한다.
4) joinPoint.proceed()를 통해 핵심 관심 사항을 작동시킨다.
( AOP를 적용시킨 Controller, Service, Repository 등의 실제 구현 클래스의 메서드가 작동하는 것)
AOP 적용 전 의존 관계
AOP 적용 후 의존 관계
AOP 적용 전 전체 그림
AOP 적용 후 전체그림
TDD
Ttest-Driven Development, 테스트 주도 개발
테스트 클래스를 먼저 작성한 다음에 레포지토리 등의 실제 사용될 규현 클래스를 작성하는 방식
구현 클래스 작성 시 반드시 테스트 케이스를 작성하고 구현하는 습관을 들이는게 많이 중요하다.
개발의 70% 이상은 Test 클래스를 작성하는 경우가 많음.
@Test
public void 중복_회원_예약(){
//given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
//when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,()->memberService.join(member2));
//then
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
@Test : 다음 메서드가 단위 테스트로 수행되도록 한다.
assertThrows : memberService.join(member2) 메서드 수행 도중 IllegalStateException 클래스가 반환되면
해당 에러 반환받음
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.") : 위에서 받은 에러 메시지 내용이 "이미 존재하는 회원입니다." 이면 테스트 통과, 아니면 테스트 실패다.