[Spring] 스프링 빈과 의존관계~AOP
스프링 빈과 의존관계
지금까지 서비스와 리포지토리 만들고 멤버 객체 만들고 서비스를 통해서 멤버를 타입할수 있고 리포지토리에 저장하고 테스트 만들고.
이번엔 화면 붙일거.
private final MemberService memberService = new MemberService();
이렇게 하면 다른 여러 멤버 컨트롤러들이 가져다 쓸 수 있다.
스프링 컨테이너에 등록하면 하나만 등록이 됨.
MemberService를 찾을 수 없다는데 ..?
@Autowired는 스프링 컨테이너에서 가져옴.
연결을 시켜주는데 안되네 왜? 멤버서비스 가면 순수한 자바 클래스인데 에노테이션 보고 규칙이 있는데 순수한 ㄴ자바코드라 아무것도 안됨 그래서 @Service를 넣어주면 됨.
스프링을 시작할 때 스프링 컨테이너라는 통이 생기는데 거기에 @Controller 어노테이션이 있는 클래스는 객체를 생성해서 넣어두고 관리를 해준다.
Controller, Service, Repository = 정형화 된 패턴 Controller에서 외부 요청을 받고, Service에서 비즈니스 로직을 만들고 Repository에서 데이터 저장을 한다.
생성자에 @Autowired 가 있으면 스프링이 연관된 객체를 스프링 컨테이너에서 찾아서 넣어준다. 이렇게 객체 의존관계를 외부에서 넣어주는 것을 DI (Dependency Injection), 의존성 주입이라 한다.
이전 테스트에서는 개발자가 직접 주입했고, 여기서는 @Autowired에 의해 스프링이 주입해준다.
출처: https://velog.io/@abcdana/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88%EA%B3%BC-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84
컴포넌트 스캔 원리 @Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다. @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다. @Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다. @Controller @Service @Repository
스프링 딱 올라올때 컴포넌트 관련 어노테이션이 있으면 객체 생성해서 딱 등록. Autowired는 이 연관관계. 선을 연결하는거
memberService 와 memberRepository 가 스프링 컨테이너에 스프링 빈으로 등록되었다.
참고: 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만 등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
스프링 빈을 등록하는 2가지 방법
컴포넌트 스캔과 자동 의존관계 설정
@Controller, .. 등 어노테이션을 이용해서 한 것이 컴포넌트 스캔 방식 자바 코드로 직접 스프링 빈 등록하기 컴포넌트 스캔 원리 @Component 어노테이션이 있으면 스프링 빈으로 자동 등록된다. @Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다. @Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다. @Controller @Service @Repositor 👩🏻💻여기서 생기는 궁금한 점 ! ❓ 아무데나 @Component 어노테이션을 붙여도 되나요?
예를 들어 쌩뚱맞게 main > java > demo 패키지에 demo 클래스를 만들어서 컨트롤러 어노테이션 붙여도 되나요??? 📢 기본적으로는 컴포넌트 스캔이 되지 않습니다.
지금 우리가 실행 시키는 클래스 폴더가 hello.hellospring 패키지이므로 이 하위에 포함된 파일들만 스프링이 다 뒤져서 스캔합니다. 추가 설정을 통해 등록할 ‘수’는 있습니다.
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
//인터페이스는 뉴가 안된다.
//멤버 서비스랑 멤버리포지토리를 스프링에 등록하고
}
}
이 그림을 이렇게 구현했다.
멤버 서비스랑 리포지토리를 스프링 올라올떄 컨테이너에 올리고 멤버서비스는 멤버리포지토리 호출하고 스프링에 등록된 리포지토리를 넣어줌. 컨트롤러는 스프링이 관리하는거
이건 컴포넌트 스캔이라 Autowired 해주면 됨.
XML로 설정하는 방식도 있지만 최근에는 잘 사용하지 않으므로 생략한다. 이럴쑤…가… 난 수업 때 이렇게 배웠는걸?ㅠㅜ DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다. → 의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다. 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다. 📢주의 : @Autowired 를 통한 DI는 helloConroller , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. → 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
참고 : https://velog.io/@abcdana/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%B9%88%EA%B3%BC-%EC%9D%98%EC%A1%B4%EA%B4%80%EA%B3%84
- 참고: XML로 설정하는 방식도 있지만 최근에는 잘 사용하지 않으므로 생략한다.
- 참고: DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다. 의존관계가 실행중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.
- 참고: 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그리고 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
- 주의: @Autowired 를 통한 DI는 helloConroller , memberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
- 스프링 컨테이너, DI 관련된 자세한 내용은 스프링 핵심 원리 강의에서 설명한다.
그리고
@Autowired지우고 SpringConfig 파일 생성 후 실행해보니 bean이 이미 정의 되어 있다는 식의 오류가 떴습니다.
구글링 해보니 spring boot 2.1 이후로는 bean을 overriding 못하도록 설정되어있다고 하더라고요.
application.properties 파일에 spring.main.allow-bean-definition-overriding=true 를 추가하니 작동하긴 하는데, 빈이 오버라이드 될 경우에 무슨 문제점이 발생하나요?
뭔가 문제점이 있으니까 스프링에서 디폴트 설정을 바꾼 것 같은데... 구글링 해도 해결법만 나오고 왜 그런지는 설명이 없네요ㅜㅜ
이런 에러 났었는데 @service를 지워주니 해결이 되었다.
메시지를 분석해보면 MemberService가 이미 스프링 빈으로 등록되어서 SpringConfig에 정의한 memberService 빈 등록 설정을 적용할 수 없다는 오류입니다.
컴포넌트 스캔과 자동 의존관계 설정에서는 다음과 같이 코드를 적용했습니다.
@Service
public class MemberService {}
자바 코드로 직접 스프링 빈 등록하기에서는 다음과 같이 @Service를 제거했습니다.
//@Service -> 이 라인을 지워주세요
public class MemberService {}
이렇게 @Service를 제거해주시면 컴포넌트 스캔의 대상에서 제외되기 때문에, 이후부터는 직접 빈 등록을 진행하셔도 될꺼에요.
회원 관리 예제
관련 컨트롤러 찾고 없으면 스태틱 찾는다고 설명했다.
여기서 매핑된게 있으니까 바로 찾아간다.
회원 웹 기능 - 홈 화면 추가 홈 컨트롤러 추가
package hello.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
회원 관리용 홈
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<h1>Hello Spring</h1>
<p>회원 기능</p>
<p>
<a href="/members/new">회원 가입</a>
<a href="/members">회원 목록</a>
</p>
</div>
</div> <!-- /container -->
</body>
</html>
참고 : 컨트롤러가 정적 파일(ex, index.html)보다 우선순위가 높다.
회원 웹 기능 - 등록 회원 등록 폼 개발 회원 등록 폼 컨트롤러
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping("/members/new")
public String createForm() {
return "members/createMemberForm";
}
}
회원 등록 폼 HTML
(resources/templates/members/createMemberForm)
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<form action="/members/new" method="post">
<div class="form-group">
<label for="name">이름</label>
<input type="text" id="name" name="name" placeholder="이름을 입력하세요">
</div>
<button type="submit">등록</button>
</form>
</div> <!-- /container -->
</body>
</html>
회원 등록 컨트롤러
웹 등록 화면에서 데이터를 전달 받을 폼 객체
package hello.hellospring.controller;
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
회원 컨트롤러에서 회원을 실제 등록하는 기능
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
회원가입을 들어가면 members/new로 들어온다 (get방식으로 그냥 들어옴) → createForm()을 반환 → 정적으로 생성된 members/createMemberForm.html을 그냥 브라우저에 뿌려준다. → 이 때 input태그안에 입력한 값을 담아서 (여기서 name이 중요! server로 넘어올때 key가 된다.) post방식으로 다시 넘어온다. → url은 똑같지만 이번엔 post 방식이기 때문에 postMapping된 create() 메소드가 호출이 된다. → memberForm에 데이터를 담아서 join() 메소드를 통해 회원가입을 한다. 회원 웹 기능 - 조회 회원 컨트롤러에서 조회 기능
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
회원 리스트 HTML
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="container">
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
</tr>
</thead>
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
<tr th:each="member : ${members}"> : 반복하는 thymeleaf 문법
Java의 for Each 와 비슷 아직까지는 memory에 데이터를 저장했기 때문에 서버를 내렸다 다시켜면 회원데이터가 다 사라진다.
화면처럼 나와야 하며
이 test.mv.db가 있어야 한다.
순수 Jdbc 환경 설정 build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
윗줄은 jdbc와 연결하는거고 밑은 디비와 붙을때 디비가 제공하는 클라이언트가 필요한데 그것.
스프링 부트 데이터베이스 연결 설정 추가 resources/application.properties spring.datasource.url=jdbc:h2:tcp://localhost/~/test spring.datasource.driver-class-name=org.h2.Driver spring.datasource.username=sa
주의!: 스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해주어야 한다. 그렇지 않으면 Wrong user name or password 오류가 발생한다. 참고로 다음과 같이 마지막에 공백이 들어가면 같은 오류가 발생한다. spring.datasource.username=sa 공백 주의, 공백은 모두 제거해야 한다.
참고: 인텔리J 커뮤니티(무료) 버전의 경우 application.properties 파일의 왼쪽이 다음 그림고 같이 회색으로 나온다. 엔터프라이즈(유료) 버전에서 제공하는 스프링의 소스 코드를 연결해주는 편의 기능이 빠진 것인데, 실제 동작하는데는 아무런 문제가 없다.
MemberService는 MemberRepository에 의존 그리고 MemberRepository는 MemoryMemberRepository와 JDBCMemberRepository에 의
스프링은 기존에 Memory 버전으로 등록했다면 jdbc로 등록한다.
개방-폐쇄 원칙(OCP, Open-Closed Principle)
- 확장에는 열려있고, 수정, 변경에는 닫혀있다.
스프링의 DI (Dependencies Injection)을 사용하면 기존 코드를 전혀 손대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
- 회원을 등록하고 DB에 결과가 잘 입력되는지 확인하자. 데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
@SpringBootTest
@Transactional
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//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));//예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
여기서 @Transactional이 나오는데 메우메우 중요하다. 디비는 커밋을 해야 적용 되는데 이 트랜잭션은 커밋을 안하고 롤백을 해준다. 그래서 저걸 주석처리하고 실행하면 디비에 적용이 안된다.(롤백해서)
@SpringBootTest : 스프링 컨테이너와 테스트를 함께 실행한다.(실제실행)
@Transactional : 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다.
스프링 JdbcTemplate
- 순수 Jdbc와 동일한 환경설정을 하면 된다.
- 스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해준다. 하지만 SQL은 직접 작성해야 한다.
참고지만
생성자가 1개면 @Autowired 생략 가능
JPA
jdbc->JdbcTemplate까지 바꿔봤다. 근데 다 좋은데 JdbcTemplate도 결국 개발자가 직접 SQL을 작성해야 한다.
JPA 쓰면 쿼리도 JPA가 자동으로 처리해줌.
객체를 메모리에 넣듯이 JPA가 쿼리를 다 처리해준다.
그럼 우리는 객체중심의 설계가 가능해진다.
- JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
- JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
- JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
spring-boot-starter-data-jpa 는 내부에 jdbc 관련 라이브러리를 포함한다. 따라서 jdbc는 제거해도
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
application.properties에 추가해줘야 jpa가 가능하다.
- show-sql : JPA가 생성하는 SQL을 출력한다.
- ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끈다. create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해준다. 해보자.
엔티티 쓰려면 엔티티 매니저를 주입받아야 한다.
em 에서 영구 저장하다 (영속성)
AOP (C의 포인터마냥 방지턱)
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import java.util.List;
import java.util.Optional;
//@Service
public class MemberService{
// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
/*회원가입*/
public Long join(Member member) {
//같은 이름이 있는 중복회원 X
long start = System.currentTimeMillis();
// Optional<Member> result = memberRepository.findByName(member.getName());
//옵셔널로 반환
try {
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}finally{
long finish = System.currentTimeMillis();
long timeMs=finish-start;
System.out.println("join="+timeMs+"ms");
}
}
private void validateDuplicateMember(Member member) {
memberRepository.findByName(member.getName())
.ifPresent(m->{ //만약 멤버에 값이 있으면
throw new IllegalStateException("이미 존재하는 회원입니다");
});
}
/*전체 회원 조회*/
public List<Member> findMembers() {
long start = System.currentTimeMillis();
try {
return memberRepository.findAll();
}finally {
long finish = System.currentTimeMillis();
long timeMs = finish-start;
System.out.println("findMember"+timeMs+"ms");
}
}
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다. 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다. 핵심 관심 사항을 깔끔하게 유지할 수 있다. 변경이 필요하면 이 로직만 변경하면 된다. 원하는 적용 대상을 선택할 수 있다.
스프링의 AOP 동작 방식 설명
AOP 적용 전 의존관계
AOP 적용 전 전체 그림
Hello COntroller가 호출하는건 가짜스프링빈 해서 진짜 멤버서비스가 아니라 프록시로 발생하는 가짜 멤버서비스를 호출.
AOP 적용 후 전체 그림
이러면 어디서 밀리는지 어디서 병목이 나오는지 다 콘솔창에 나온다.
이런식으로 제공되는게 SearchBoardRepository 호출이 될떄마 여기로 jouinPoint에다 조작 가능.