MemberController에서 MemberService를 통해 회원가입을 하고 MemberService를 통해서 데이터를 조회할 수 있어야 한다.
MemberController와 MemberService는 의존관계이다.
MemberController가 MemberService를 의존한다.
스프링이 시작할 때 스프링 컨테이너에 @Controller, @Service, @Repository 등의 어노테이션을 가진 클래스 객체들을 생성하고 스프링 빈으로 등록해서 관리한다.
스프링 컨테이너 : 스프링에서 객체를 관리하는 것. 객체의 생명주기를 관리하고 컨테이너에 담겨있는 객체들을 스프링 빈(Bean)이라고 부른다.
@Controller, @Service, @Repository 어노테이션이 존재하는 클래스들의 경우 객체를 스프링 빈(Bean)으로 등록해 스프링 컨테이너에서 관리한다.
위처럼 MemberService() 객체를 new로 할당해서 사용할 수도 있지만, Spring 컨테이너에 저장된 빈을 불러서 사용하면된다.
new를 통해 객체를 생성해서 할당할 경우, 모든 Controller가 각각의 Service 객체를 사용하게 된다.
-> 자원이 낭비되며 비효율 적이다.
스프링 컨테이너에는 각 객체들이 하나만 빈으로 등록되어 해당 빈을 공유하는 형태로 사용하기 때문에 new를통한 객체 할당을 통해서 사용하는것보다 훨씬 효율적으로 사용이 가능하다.
생성자에 @Autowired 어노테이션을 사용하면 스프링 컨테이너에 빈으로 존재하는 MemberService 객체를 연결해 준다.
-> MemberController가 memberService를 의존하게 된다. (Dependency Injection : 의존성 주입)
@Controller 어노테이션이 존재하는 MemberController 클래스는 스프링이 시작할 때 자동으로 스프링 컨테이너에 스프링 빈으로 등록된다.
@Autowired 어노테이션을 통해 스프링 빈을 연결하는 경우 해당 객체또한 스프링 컨테이너의 스프링빈으로 등록되어 있어야한다. 즉 위 코드에서는 memberService가 스프링 빈으로 등록되어 있어야 한다는 것이다.
@Service 어노테이션의 경우 @Controller와 마찬가지로 스프링이 시작할때 컴포넌트 스캔에 의해 스프링 빈으로 등록된다. MemberService에서도 생성자에서 @Autowired를 사용하는데 이때 사용되는 MemberRepository 또한 스프링 빈으로 등록되어 있어야한다.
@Repository 어노테이션도 스프링 시작시 스프링 빈으로 등록시켜준다.
코드들은 위 그림과 같이 의존관계가 연결된다.
컴포넌트 스캔과 자동 의존관계 설정
@Controller, @Service, @Repository와 같이 어노테이션으로 스프링 빈을 등록하는 경우는 컴포넌트 스캔 방식에 해당한다. @Component 어노테이션이 스프링 빈으로 등록하는 어노테이션이지만, @Controller, @Service, @Repository 어노테이션의 내부에 @Component가 존재해 세가지 어노테이션 모두 스프링 빈으로 등록된다.
스프링이 시작할 때 @Component 어노테이션들을 스캔해서 스프링 빈으로 등록하는 것을 컴포넌트 스캔이라고 한다.
@Autowired는 스프링빈을 연결하는 역할을 한다.(의존 관계 설정)
@Component스캔의 범위는 @SpringBootApplication 어노테이션이 존재하는 클래스의 패키지와 해당 패키지의 하위패키지에서만 작동한다.
@SpringBootApplication 어노테이션 내부에 @ComponentScan 어노테이션이 존재한다.
**스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본적으로 싱글 톤 등록을 한다.**
싱글 톤 : 하나만 등록하여 해당 객체를 공유한다.
예를 들어 MemberController의 경우도 하나의 객체만 컨테이너에 스프링 빈으로 등록해서 공유한다. MemberController 객체가 여러개 스프링빈으로 등록되지않는다.
자바 코드로 직접 스프링 빈 등록하기
@Service, @Repository, @Autowired 어노테이션을 제거한 후 코드를 작성한다.
SpringConfig 파일을 새로 하나 생성한다.
@Configuration 어노테이션은 설정 파일을 만들기 위한 어노테이션이며, Bean을 등록하기 위한 어노테이션이다.
@Configuration 클래스 내부에 @Bean 어노테이션 작성시 해당 객체를 Bean에 등록한다는 의미를 가진다.
위 코드는 MemberService와 MemoryMemberRepository 객체를 Bean으로 등록하는 코드이다.
위코드를 통해 코드로 직접 스프링 빈을 등록한 경우에도 아래처럼 구조가 가능해진다.
스프링이 시작될 때, memberRepository와 memberService를 스프링 컨테이너에 올려 빈으로 등록한다.
MemberService 생성자에 파라미터로 memberRepository를 넣어주는데 이때 들어가는 memberRepository는 빈으로 등록된 memberRepository이다.
자바코드로 직접 등록할 때에도 Controller는 컴포넌트 스캔으로 올라가야 하기 때문에 @Controller 어노테이션을 꼭 써줘야한다. 또한 Controller 생성자에서 스프링 빈 객체를 가져올 때도 컴포넌트 스캔 방법인 @Autowired 어노테이션을 작성해주어야한다.
Dependency Injection의 3가지 방법
1.생성자 주입(생성자에 @Autowired 어노테이션 사용)
2.필드주입(필드에 @Autowired 어노테이션 사용)
3.Setter 주입(set 메소드가 public으로 사용되며 어디서든 호출이 가능해져 개발중 문제가 생길 수 있다.)
가장 좋은 방법은 생성자 주입이다.(생성자에 @Autowired 어노테이션을 통해 빈 객체를 연결하는것)
실무에서는 주로 Controller, Service, Repository와 같은 코드들은 컴포넌트 스캔을 사용한다고 한다. 정형화 되지 않거나, 상황에 따라 구현 클래스를 변경해야 하는 경우 설정을 통해 코드로 직접 스프링 빈으로 등록한다고 한다.
MemberRepository의 경우 Memory를 사용하는 구현체에서 DB를 사용하는 구현체로 바꿀 예정이기 때문에 컴포넌트 스캔이아닌 코드로 직접 빈에 등록하는 코드를 남겨둔다.
@Autowired를 통해 Dependency Injection을 하는 경우 MemberController와 MemberService 와 같이 스프링이 관리하는 객체 즉 스프링 빈 객체들에 한해서만 가능하다. 스프링 빈으로 등록되지 않은 객체들은 @Autowired 사용이 불가능하다.
해당 인터페이스의 save, findById, findByName, findAll 메소드들을 해당 클래스에서 구현한다.
1씩 sequence를 더하면서 id값을 부여, store(Map)라는 임시저장소(Memory DB)에 member 저장
findById : id값과 일치하는 id를 store에서 탐색
Null이 반환될 수 있기 때문에 Optional.ofNullable로 탐색
findByName : name값을 기반으로 store에 저장된 값 탐색
stream()의 경우 for문으로 모든원소에 접근하듯이 모든 원소들에대해 접근한다고 생각하면 된다.
filter를 통해 name과 일치하는 멤버들을 걸러내고, findAny()를 통해 걸러진 값을 반환한다.
즉 store에 저장된 값중 이름이 name과 일치하는 경우를 반환한다.
모든 값들을 Member List로 반환한다.
회원 리포지토리 Test코드 작성
개발한 기능들을 main메서드를 실행시켜 테스트하는 경우 시간이 오래걸리고, 한번에 모두 테스트하기 어렵다는 단점이 있기 때문에 테스트코드를 생성하여 메소드들을 테스트하는것이 좋다.
test내에 리포지토리 패키지를 생성하여 test클래스를 작성해준다.
@Test 어노테이션을 사용하여 해당 메소드가 테스트 메소드임을 명시해준다.
위처럼 해당 테스트 메소드를 구현한뒤 해당 메소드만 실행시켜 테스트가 가능하다.
save를 실행시킬경우 해당 테스트코드 내에서 오류가 발생하거나 실패를 하게되면 해당 실행에서 경고를 반환한다.
테스트코드가 성공할경우 아래처럼 정상 종료가 된다.
테스트코드는 항상 해당 코드를 실행시키고 검증까지 완료해야한다. save 메소드의 경우 Member 데이터 생성과 save함수 실행, 이후 findById를 통해 해당 id를 검색하고 assertThat(member).isEqualTo(result)를 통해 검증을 완료했다.
findByName 테스트 코드
findByName을 검증하기 위해 데이터를 만들고 findByName을 통해 result를 반환받고 assertThat(result).isEqualTo(member1)을 통해 검증했다.
findAll() 테스트 코드
2개의 member를 생성 및 save하고 findAll()을 실행한다.
이후 findAll()의 반환의 결과가 2개 인지 확인하여 검증한다.
테스트코드의 경우 모든 메소드들을 한꺼번에 실행하여 테스트하는것도 가능하다.
여러 메소드를 한꺼번에 실행할 경우 각 메소드들이 순서와 상관없이 실행된다.
각 메소드들이 실행되면서 같은 데이터를 save하는 경우 에러가 발생한다.(중복 저장 예외)
이러한 문제를 해결하기 위해 아래의 코드를 추가해준다.
@AfterEach는 해당 클래스내의 각 메소드들이 실행을 끝낼때 마다 실행되게하는 어노테이션이다.
MemoryMemberRepository의 clearStore메소드
위 메소드를 통해 각 메소드들이 끝날 때마다 저장소를 비워준다.
-> 중복 저장 예외를 방지할 수 있다.
위의 개발과정은 MemoryMemberRepository를 모두 구현하고, 테스트 코드를 통해 해당 클래스의 메소드들을 검증 했다. 해당 개발 과정을 뒤집어 테스트 코드를 먼저 작성하고 검증이 완료된 후 메소드를 구현할 경우 테스트 주도개발(TDD)이라고 한다.
MemberService 메소드 작성
Join() 메소드
join 내부에서 member중복을 검사하는 코드를 작성한 뒤 외부의 새로운 메소드로 생성했다.
ifPresent를 통해 findByName의 반환값이 존재한다면 throw new IllegalStateExceptioon("이미 존재하는 회원입니다.")를 실행시킨다.
ifPresent는 값이 존재할 경우 내부 로직을 실행시키는 기능을 제공한다.
위와 같이 MemberService 코드를 구현했다.
회원가입, 전체 회원조회, 회원ID를 통한 회원 조회 등 비즈니스적인 메소드들이 구현된다.
MemberService 테스트 코드 구현
위처럼 테스트코드의 메소드명을 한글로 지정해도 상관없다.
테스트 코드를 구현할 때 given-when-then 구성으로 코드를 작성하는것이 좋다.
given : 주어지는 것
when : 실행했을 때
then : 결과
어떤 것이 주어진 상황에서 코드를 실행했을 때 결과가 이렇게 나와야 한다는 과정을 정리하여 작성하는 방법이다.
given-when-then 의 회원가입 테스트코드
멤버가 given으로 주어지고 join메소드를 실행했을 때 검증을 위해 findOne을 통해 나오는 결과가 무엇인지 확인하는 과정이다.
중복 회원가입 예외 테스트코드
assertThrows는 두번째 인자로 주어진 로직 실행시 첫번째 인자에 해당하는 예외가 발생하는지 검사하는 함수입니다.
첫번째 인자가 IllegalStateException이 아닌 NullPointerException이라면 assertThrows에서 에러를 반환한다.
해당 로직에서 정상적으로 IllegalStateException 을 발생시키면 해당 예외를 예외객체 e에 저장하고,
assertThat을 통해 해당 예외 메세지를 "이미 존재하는 회원입니다." 와 비교하여 검증을 완료한다.
Service테스트 코드에서도 테스트별로 멤버들을 생성하고 저장하기 때문에 아래 코드를 넣어준다.
MemberService의 생성자 코드
MemberService의 생성자 인자로 memberRepository가 주어지며 각 MemberService마다 memberRepository를 별도로 갖게 된다.
테스트코드에서의 MemberService
@BeforEach를 통해 모든 메소드들이 실행되기전 해당 코드가 실행되도록 했다.
테스트코드 메소드들이 실행될 때 마다 MemoryMemberRepository()를 생성하고 서비스를 따로 생성해주며 서비스 객체 생성때마다 memberRepository를 생성해준다.
즉 각 테스트 메소드들이 각각의 memberService와 memberRepository를 갖게된다.
2.MVC와 템플릿 엔진 : 서버에서 일부 동작을 통해 HTML을 가공하여 파일을 내려주는것 (JSP, PHP)
3.API : HTML로 파일을 내리는 것이아니라 JSON 형태의 데이터를 반환하는 방법
정적컨텐츠
스프링에서도 정적컨텐츠(Static Content) 기능을 제공한다.
서버 실행후 해당 파일 url 연결
해당 html 파일의 내용이 그대로 출력된다.
정적컨텐츠의 경우 요청한 html파일을 가공없이 그대로 출력해준다. 또한 동적 프로그래밍이 불가능하다.
MVC와 템플릿 엔진
MVC : Model, View, Controller
MVC 모델이 도입되기 이전에는 View에서 데이터가공까지 모두 처리하였다. ex) JSP, PHP
View는 화면과 관련된 일, 비즈니스 로직이나 서버 뒷단에 관련된 동작들은 Controller나 Back-end 비즈니스 로직에서 , View와 Controller, Back-end 에서 주고받는 데이터를 Model이라고 하는 데이터에 담아서 주고받는 구조로 동작한다.
Hello-mvc로 Parameter name을 가지고 GET 요청을 통해 서버에 요청하면 model객체에 addAttribute를 통해 파라미터로 받은 name값을 담아서 hello-template으로 전송한다. return의 목적지로 model 객체가 전송된다.
데이터를 전송할 때 사용하는 Model 객체의 경우 메소드의 파라미터로 선언만 해주면 Spring에서 만들어준다. Model 객체를 사용하기 위해 따로 메모리를 할당할 필요가 없으며 Spring에서 만들어준것을 사용하기만 하면된다.
addAttribute를 통해 저장된 값은 JSON형태이며 "name"이 key이고 파라미터로 전달받은 name이 value가 된다.
spring코드에서 반환되는 데이터가 전달되는 hello-template.html 이다.
Model 객체로 전달된 데이터의 key가 name인 value를 출력한다.
붉은색 선으로 밑줄 쳐져있는 name이 넘겨주는 파라미터 이름이고 푸른색 선이 가리키는 spring은 해당 파라미터의 값이된다.
전달받은 값을 addAttribute("name", name) 으로 Model 객체에 넣어주는데, key가 "name"이 되고 파라미터로 전달받은 값이 name에 들어가게된다. 즉 key : name, value : spring이 된다.
@RequestParam 어노테이션에 require=false를 넣어주는것은 해당 파라미터에 값이 없어도 실행되게 하는것이다.
파라미터 값을 채우지 않을 경우 default text인 hello! empty가 출력된다.
API
@ResponseBody 어노테이션을 사용할 경우 return에 의한 반환값이 그대로 HTTP response의 body에 삽입되어 HTML에 출력된다.
return "hello "+name 에 의해 name파라미터로 전송된 pw4ngc0가 삽입되어 출력됨을 알 수 있다.
API를 통한 객체 반환
Hello 객체에 name값을 넣어 반환한다.
객체를 반환할 경우 해당 객체의 데이터를 JSON형태로 출력해준다.
프론트에서 해당 객체를 가공하여 사용하는 것 같다.
@ResponseBody 어노테이션의 메소드가 객체를 반환할 경우 해당 객체를 JSON형태로 변환하여 HTTP 응답으로 넘겨준다.