지금까지 사용했던 annotation을 정리해보겠다.

 

@Autowired

스프링 컨테이너에 빈으로 등록되어있는 객체를 자동으로 주입해준다.

 

@PersistenceContext

스프링에서 영속성 관리를 위해 Entity Manager가 존재한다.

그래서 스프링이 처음 시작할 때, entity manager를 만들어서 빈으로 등록을 해둔다.

entity manager는 @Autowired가 아니고 특별하게 @PersistenceContext라는 어노테이션으로 주입을 해줄 수 있다.

근데 최신 스프링부트에서는 @Autowired로도 할 수 있다고 한다.

 

@RequiredArgsConstructor

private final Member member;

라고 final로 선언한 필드가 있다고 했을 때

위 어노테이션을 사용하면 클래스에서 final 붙은 필드만 가지고 생성자를 만들어서 알아서 주입해준다.

예를 들어 아래의 코드처럼 작성하였다면

@RequiredArgsConstructor
public class MemberRepository{
	private final EntityManager em;
}

@RequiredArgsConstructor 어노테이션을 사용했기 때문에 final 붙은 필드인 em에 entity manager를 주입해준다.

무슨얘기냐 하면

public class MemberRepository{
	
    private final EntityManager em;
    
    @Autowired
    public MemberRepository(EntityManager em){
    	this.em = em;
    }

}

이렇게 긴 코드를 @RequiredArgsConstructor로 쉽게 구현할 수 있다는 것이다.

그리고 클래스에 생성자가 하나만 있으면 @Autowired를 삭제해도 알아서 빈으로 등록된 em을 주입시켜준다! 꿀팁!

 

@Transactional

db에 접근하는 모든 객체 또는 메소드 위에 @Transactional을 사용해야한다.

성공하면 commit이 되어 db에 저장되고 실패하면 rollback이 된다.

test에서는 default가 rollback이라 테스트가 진행되고 끝나면 rollback이 되어 db에 저장이 되지 않는다.

test에서도 commit 되는 것을 눈으로 확인하고 싶으면 @Rollback(false)를 써주면 된다.

 

@Test

테스트를 위한 메소드 위에 써준다.

예외를 일으키기위해 테스트코드를 짤 때 @Test(expected = IllegalStateException.class) 처럼 예상되는 예외를 써주면 예외가 났을 때 통과를 하게된다.

@Test(expected = IllegalStateException.class)
    public void 중복_회원_예외() throws Exception{
        //given
        Member member1 = new Member();
        member1.setName("kim");

        Member member2 = new Member();
        member2.setName("kim");

        //when
        memberService.join(member1);
        memberService.join(member2); 
        
        //then
        fail("예외가 발생해야 한다.");
    }

 

@SpringBootTest

통합테스트를 할 때 사용하는 어노테이션이다.

이 어노테이션을 써야지만 빈으로 등록되어있는 객체들을 다 로드해서 사용할 수 있다.

 

@Entity

도메인 객체 위에 쓴다.

도메인은 실제 db와 매칭될 클래스이다.

 

@Id @GeneratedValue

@Id - primary key와 매칭될 필드위에 쓴다.

@GeneratedValue - auto_increment와 같은 기능을 담당해준다.

 

@Column(name=" ")

테이블에서의 컬럼명을 지정해줄 수 있다.

 

@OneToMany, @OneToOne, @ManyToOne, @ManyToMany

@OneToMany - 1대다 관계에 일 때 사용한다.

@OneToOne - 1대1 관계일 때 사용, fetch 타입을 LAZY로 적용. ~ToOne 은 defaul fetch type이 EAGER이다. EAGER는 즉시로딩이라 연관된 테이블을 다 불러와서 LAZY로 쓰는 걸 권장한다.

@ManyToOne - 다대1 관계, 이 역시 fetch type을 LAZY로 사용하는 걸 권장

@ManyToMany - 대다다 관계..

 

 

 

 

 

Live Templates

인텔리제이에서 개발을 할 때, sout을 치고 tab을 누르면

이렇게 자동으로 print해주는 코드를 만들어주는 것처럼

자주 사용하는 코드를 일종의 단축어로 만들어놓으면 참으로 편리하다.

 

그래서 나는 테스트 코드를 짤 때 유용하게 사용하고 있는 라이브 템플릿 만드는 방법을 공유하고자 한다. 윈도우를 기준으로 작성할거다!

 

Live Templates 생성하기

 

1. File -> Setting에 들어간다.

 

2. Live Templates를 검색한다.

 

3. 우측에 + 버튼을 누르고 Template Group을 클릭한다.

 

4. 내가 만들 템플릿이라 custom이라고 그룹명을 지었는데 그냥 마음에 드는 이름으로 짓는다.

 

5. 지금 만든 그룹을 선택하고 이번엔 Live Template를 클릭한다.

 

6. 템플릿 이름을 정하고 설명도 써주고 코드도 써주고 Applicable도 설정해주고 OK를 누른다.

 

7. tdd(자기가 만든 템플릿 이름)을 치고 tab을 누르면 아주 잘 나오는 것을 확인할 수 있다.

 

 

 

Template text

@Test
public void $NAME$() throws Exception{
    //given
    $END$
    //when
    
    //then
}

이번에 생성한 라이브 템플릿의 코드이다.

라이브 템플릿을 통해 좀 더 간편하게 개발하자!!

:)

이번 강의에서는 AOP 기술을 알아보았다.

 

AOP란 무엇인가?

예시를 들어서 설명해보겠다.

내가 다니는 회사의 악덕사장님이 우리 회사 코드의 모든 메소드의 실행시간을 알고싶다고 하신다..

그럼 모든 메소드의 첫 시작 시간과 끝나는 시간의 차이를 구해서 실행시간을 알 수 있다. 말이야 간단하지

메소드가 193824개라면? 오늘까지 하라면? 절망적인 하루가 될 것이다..

우리 시스템의 핵심 관심 사항은 회원가입을 하고, 회원 list를 볼 수 있는 기능인데

모든 메소드의 공통 관심 사항은 실행 시간을 구하는 기능인 것이다.

만약 위에서 말한대로 코드를 짠다면 핵심 관심 사항과 공통 관심 사항이 섞여서 아주 복잡한 코드가 될 것이다.

또한 실행시간을 ms 단위 말고 s 단위로 바꿔봐! 하는 순간 다시 모든 메소드에 가서 단위를 변경해줘야하는 끔찍한 일이 생길 것이다..

하지만 AOP를 사용하면 핵심 관심 사항과 공통 관심 사항을 분리하여 원하는 적용 대상에 적용만 시키면 유지보수도 쉽고 깔끔한 코드가 완성될 수 있다.

아참.. AOP는 Aspect-Oriented Programming의 약자이다..^^

 

AOP 적용 전 코드

모든 메소드에 시간을 구할 수 있는 코드를 작성했다. 2개만 작성했는데도 한숨이 나왔다.. 똑같은 코드가 내눈에 벌써 두개나 보인다.. 윽윽

 

AOP 적용 후

TimeTraceAop.java

따로 클래스를 두어 진행한다.

  • @Component 어노테이션을 통해 빈에 등록을 한다. 지금까지 했던 것처럼 따로 @Bean을 통해 등록할 수도 있지만 이번 강의에서는 @Component를 사용했다.
  • @Aspect 는 AOP를 적용시키기위해 필요한 어노테이션이다.
  • execute() 위에 @Around 어노테이션을 추가한다. 이는 적용 대상을 지정하는 것이다. hello.helloSpring 아래에 모든 메소드에 적용한다는 의미이다.

AOP 작동 원리

강의코드를 기준으로 설명을 하자면, 회원가입한 회원들의 목록을 볼 수 있는 기능이 있다.

그렇게 되면 아래와 같이 동작을 한다.

  1. 컨트롤러가 페이지 띄운다.
  2. 서비스가 레포지토리를 통해 회원 목록을 가져온다.

그러면 Controller가 Service를 호출하고 Serivce가 Repository를 호출할까? AOP를 적용하면 그렇지 않게된다.

AOP를 적용하면 스프링 부트는 프록시라는 기술을 통해 가짜 controller를 빈에 등록하고 그 가짜 controller가 진짜 controller를 부르는 식이 되는 것이다.

위의 AOP 코드를 보면 joinPoint.proceed()가 있는 데 이 함수를 통해 가짜가 진짜를 부르는 것이라고 한다.

김영한 강사님 pdf에서 캡쳐

 

스프링 JdbcTemplate

  • JdbcTemplate을 이용하여 구현체를 만들면 코드가 확~ 줄어든다. 반복 코드를 제거해주기 때문이다. 하지만 SQL 문은 직접 작성해줘야한다. ㅜ.ㅜ
  • 쿼리문을 작성하여 db에서 데이터를 가져올 때는 jdbcTemplate.query()를 사용해서 RowMapper로 감싸준다음 가져올 수 있다.

jdbcTemplate.query(), RowMapper<Member>

  • insert를 위한 기능은 SimpleJdbcInsert()를 사용하면 된다. usingGenerateKeyColumns에는 pk 컬럼명을 넣어주면 된다. executeAndReturnKey는 쿼리문을 실행해서 레코드를 Map을 통해 만든 형식에 따라 가져와준다.

SimpleJdbcInsert()

 

JPA

  • 객체를 테이블로 mapping 해주는 프레임 워크.
  • JPA는 인터페이스다. 그러므로 구현체가 필요한데 이러한 구현체에 Hibernate라는 것이 있고 이 강좌에서는 이를 사용한다.
  • JPA를 사용하면 JdbcTemplate을 사용할 때 불편했던 바로바로 sql문을 작성하지 않아도 된다. JPA가 기본적인 SQL을 직접 만들어서 실행시켜준다.

em.persist()

  • 모든 쿼리문을 쓰지 않아도 가능한 것은 아니다. 위의 insert나 pk(기본키)를 기준으로 데이터를 찾는 기능은 구현되어있지만, 그 외의 기능은 직접 쿼리문을 JPQL로 짜줘야한다.

em.find() -> 기본키를 이용하여 데이터 찾기
em.createQuery()

  • JPA는 통해 데이터를 변경하려면 트랜잭션 안에서 실행해야한다. 따라서 @Transactional 어노테이션을 service 클래스 위에 써준다.

@Transactional

  • JPA는 엔티티 매니저(EntityManager)로 모두 동작한다. build.gradle에 data-jpa를 가져오면 spring boot가 EntityManager를 생성하고 db랑 연결을 하고 이런 모든 동작을 해준다.

data-jpa

  • 우리는 생성된 EntityManager를 가져와서 사용하면 된다. 여기서 @Autowired 안해줘도 가져올 수 있는 이유는 클래스에 생성자가 하나일때는 @Autowired안해도 알아서 해준다고 한다... 꿀팁~

Entitymanager, constructor

  • 도메인 객체 위에 @Entity 어노테이션을 써서 mapping 시켜줘야 한다. 또한 pk를 알려주기 위해 @Id 라는 어노테이션도 써줘야하고, id는 시스템상에서 1씩 증가해서 db에 넣어주기 때문에(이를 IDENTITY 방식이라 함) GenerationType.IDENTITY도 추가해야한다.

@Entity, @Id, @GenerateValue(strategy = GenerationType.IDENTITY)

  • JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다. -> 개발 생산성 크게 향상

 

스프링 데이터 JPA

  • 스프링 데이터 JPA는 인터페이스다. 구현체를 구현해야 사용할 수 있지만 얘는 뭔가 다르다. 필요없다.. 즉 구현체 없이 인터페이스 클래스 하나로 사용이 가능하다는 것이다.
  • 스프링 데이터 JPA가 자동적으로 스프링 빈에 등록해준다.
  • 사용법은 JpaRepository(인터페이스)를 extends(상속) 해주면 된다. jpaRepository<T, ID> 형식으로 써주면 된다. T는 테이블(?), ID는 PK(기본키) 데이터  자료형을 써주면 된다.

스프링 데이터 JPA 구현 끝...

  • JpaRepository를 타고 가보면 구현하려던 메쏘드들이 이미 구현이 되어있다~

 

깨달은 점

스프링 데이터 JPA 짱이다..

구조

 

코드

//HomeController.java
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";
    }
}
//MemberController.java
package hello.helloSpring.controller;

import hello.helloSpring.domain.Member;
import hello.helloSpring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@Controller
public class MemberController {
    private final MemberService memberService;

    @Autowired // 스프링이 뜰 때, 컨테이너에서 관리하는 memberService 객체와 연결해준다.
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }

    @GetMapping("/members/new")
    public String createMemberForm(){
        return "/members/createMemberForm";
    }

    @PostMapping("/members/new")
    public String create(MemberForm memberForm){
        Member member = new Member();
        member.setName(memberForm.getName());

        memberService.join(member);
        return "redirect:/";
    }

    @GetMapping("/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members",members);
        return "members/memberList";
    }
}
//MemberForm
package hello.helloSpring.controller;

public class MemberForm {
    private String name;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

html 파일들은 생략..

 

결과

홈화면
회원가입 화면
회원 목록 화면

 

고작 이거 만드는 게 이렇게 힘들다니~

컴포넌트 스캔과 자동 의존관계 설정

스프링을 처음 실행시킬 때, 스프링 컨테이너라는 통이 하나 생긴다.

우리가 아무렇지 않게 클래스 위에 @Controller 이런식으로 쓰는 어노테이션들은 이 클래스 객체를 컨테이너에 생성해서 넣어주고 컨테이너에서 관리하겠다는 뜻으로 쓰는 것이다.

이것을 스프링 컨테이너에서 스프링 빈이 관리된다고 표현한다.

 

스프링이 관리를 하게 되면 이제부터는 다~ 스프링 컨테이너에 등록하고 컨테이너로부터 받아서 써야한다.

즉, 컨테이너안에서 모든 것을 해결해야한다.

 

저번 시간까지 service와 repository를 만들었는데 이제 뷰를 사용자에게 보여주기 위해 controller를 만들어야한다.

Controller, Service, Repository는 정형화된 패턴이다.

Controller를 통해서 외부의 요청을 받고

Service에서 비즈니스 로직을 만들고

Repository에서 데이터를 저장한다.

 

여기서 보면 Controller는 요청을 받아서 Service를 통해 회원가입을 하고, 조회를 할 것이다. 이 때, 의존관계가 있다고 표현한다. 의존관계가 있으므로 Controller와 Service를 연결해야하고 이를 위해 생성자 위에 @Autowired라는 어노테이션을 쓰면 자동으로 연결해준다.

자동으로 연결해준다는 의미는 아래 예시 코드를 통해 보여주도록 하겠다.

package hello.helloSpring.controller;

import hello.helloSpring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

@Controller
public class MemberController {
    private final MemberService memberService;

    @Autowired // 스프링이 뜰 때, 컨테이너에서 관리하는 memberService 객체와 연결해준다.
    public MemberController(MemberService memberService) {
        this.memberService = memberService;
    }
}

스프링이 시작될 때, 스프링은 @Controller를 보고 MemberController가 컨트롤러 역할이구나 라고 생각하고 객체를 생성하여 컨테이너에 넣는다. (==빈을 생성한다.)

이 때, Controller는 Service를 통해 기능을 수행하므로 연관 관계가 있다. 이를 위해 @Autowired를 생성자 위에 써주면 이 Controller 객체를 생성할 때 생성자를 통해 컨테이너에 존재하는 MemberService를 넣어주면서 빈이 생성되는 것이다.

한 마디로 말하자면, 스프링은 @Controller를 보고 Controller 객체를 생성하고 그 때 생성자를 호출하면서 스프링 컨테이너에 있던 MemberService를 가져다가 넣어주는 것이다. 이해 완료?!!?

 

여기서 주의할 점은 저번 시간에 한 코드에서 위의 코드를 추가한다면 에러가 난다.

에러

왜냐하면 이전 코드의 MemberService 클래스는 컨테이너에 빈으로 등록되지 않았기 때문이다. (어노테이션 안썼음)

따라서 MemberService 클래스 위에 @Service라는 어노테이션을, 똑같은 방식으로 MemoryMemberRepository 클래스 위에 @Repository어노테이션을 쓰면 스프링이 시작될 때 컨테이너에 넣어서 관리가 된다.

위와 같은 @Controller, @Service, @Repository 를  컴포넌트 스캔이라고 하며

@Autowired를 자동 의존관계 설정이라고 한다.

 


자바 코드로 직접 스프링 빈 등록하기

어노테이션을 쓰지 않고 직접 코드로 등록하기 위해 @Controller를 제외한 @Service, @Repository, @Autowired를 다 지우고 시작한다.

 

1. hello.helloSpring 밑에 SpringConfig 클래스를 만들어준다.

구조

2. 코드를 작성해준다.

package hello.helloSpring;

import hello.helloSpring.repository.MemberRepository;
import hello.helloSpring.repository.MemoryMemberRepository;
import hello.helloSpring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringConfig {

    @Bean
    public MemberService memberService(){
        return new MemberService(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }
}

@Configuration을 통해 스프링에게 알리고 @Bean을 통해 빈을 등록하겠다고 하면 된다.

MemberService를 생성할때는 memberRepository가 필요한데(연관 관계) 이때 memberRepository()를 호출해주면 된다.

 

직접 코드를 작성하는 것의 장점

현재 어떤 db를 사용할 것인지 정해져있지 않은 상황이라고 가정이 되어있다. 나중에 db가 결정되면 저 Repository만 바꿔주면 되는 것이다. 즉, 다른 코드 손 볼 필요 없이 return new dbMemberRepository()와 같은 다른 레포지토리를 생성해주면 된다.

 

재밌다!

비즈니스 요구사항 ( 최대한 간단히 개발 )

  1. 데이터: id, name
  2. 기능: 회원가입, 조회
  3. 아직 어떤 db를 쓸 지 정하지 않았음 -> interface로 구현

김영한 강사님 pdf에서 캡쳐

 

 

일반적인 웹 어플리케이션 계층 구조

김영한 강사님 pdf에서 캡쳐

  • 도메인: db에 저장할 객체
  • 리포지토리: db에 접근, 도메인 객체를 db에 저장하고 관리
  • 서비스: 비즈니스 기능 로직(비즈니스 요구사항에서 요구한)
  • 컨트롤러: MVC의 Controller

[main 코드 작성]

구조

 

1. domain/Member

package hello.helloSpring.domain;

public class Member {
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

DB에 저장할 멤버를 class로 구현한다.

id는 시스템에서 정해주고 name은 회원가입할 때 사용자가 입력한다.

 

2. repository/MemberRepository, repository/MemoryMemberRepository

//MemberRepository
package hello.helloSpring.repository;

import java.util.List;
import hello.helloSpring.domain.Member;
import java.util.Optional;

public interface MemberRepository {
    Member save(Member member);
    Optional<Member> findById(Long id);
    Optional<Member> findByName(String name);
    List<Member> findAll();
}

MemoryMemberRepository에서 사용할 메서드를 적어준다.

#MemoryMemberRepository
package hello.helloSpring.repository;
import hello.helloSpring.domain.Member;
import java.util.*;

public class MemoryMemberRepository implements MemberRepository{
    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) {
    	//get을 했을때 null일 수 있어서 Optional로 감싸고 return 한다.
        return Optional.ofNullable(store.get(id));
    }

    @Override
    public Optional<Member> findByName(String name) {
    	//변수 store의 value는 Member 객체이기 때문에 다 돌면서 name과 같은 member가 있는지 확인
        return store.values().stream()
                .filter(member -> member.getName().equals(name))
                .findAny();
    }

    @Override
    public List<Member> findAll() {
    	// store의 모든 member를 List로 반환
        return new ArrayList<>(store.values());
    }

    public void clearStore(){
    	// store를 깨끗이 비운다~
        store.clear();
    }
}

 

 

여기서 꿀팁이 있다면 implements MemberRepository를 쓰고 alt + Enter를 누르고 Implement methods를 누르면

알아서 생성해야할 메서드들의 틀을 잡아준다. 

비어있는 메서드들을 알아서 써준다!!

 

3. service/MemberService

package hello.helloSpring.service;

import hello.helloSpring.domain.Member;
import hello.helloSpring.repository.MemberRepository;

import java.util.*;

public class MemberService {
    private final MemberRepository memberRepository;

    //Dependency Injection
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    //회원가입
    public Long join(Member member){
        //같은 이름이 있는 중복 회원 X
        validateDuplicateMember(member); // 중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }

    private void validateDuplicateMember(Member member) {
        memberRepository.findByName(member.getName())
                .ifPresent(m -> {
                    throw new IllegalStateException("이미 존재하는 회원입니다.");
                });
    }

    // 전체 회원 조회
    public List<Member> findMembers(){
        return memberRepository.findAll();
    }

    public Optional<Member> findOne(Long memberId){
        return memberRepository.findById(memberId);
    }
}

Dependency Injection이란?

쉽게 말하면

public class MemberService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
}

선언과 동시에 생성을 해주는게 아니라

public class MemberService {
    private final MemberRepository memberRepository;

    //Dependency Injection
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

생성자를 통해 인자로 받아서 넣어주는 방식이다.

MemberService 입장에서는 injection되는 입장이라 Dependency Injection이라고 한다.

 

validateDuplicateMember 메소드에서 드래그한 코드와 안한 코드가 기능적으로는 같다.

하지만 Optional로 바로 뽑아서 사용하는(드래그한 코드) 건 별로 안좋다고 한다.

따라서 체이닝 메서드를 이용하여 코드를 작성했다.


[테스트 코드 작성]

구조

테스트 코드는 service와 repository를 테스트하기 위해 작성하였다.

테스트하기위한 클래스에 Test라는 문자열을 붙여서 이름을 짓는게 일반적이라고 한다.. 절대적인가? 하여튼!!

테스트 코드를 작성할 때, 어떤 상태가 주어지고->given(), 어떤 행동을 할 때->when(), 그러면 이런 결과가 나와야한다-> then() 를 작성하면 된다고 한다. (아래 코드 참조)

 

1. repository/MemoryMemberRepositoryTest

package hello.helloSpring.repository;

import hello.helloSpring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {

    MemoryMemberRepository repository = new MemoryMemberRepository();

    // 각 test를 마치고 항상 호출되는 메서드
    @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(); //get()을 통해 Optional에서 Member를 뽑아냄
        assertThat(member).isEqualTo(result);
        //Assertions.assertEquals(member, result); -> static으로 import 해줘서 윗 줄처럼 사용가능
    }

    @Test
    public void findByName(){
        Member member1 = new Member();
        member1.setName("spring1");
        repository.save(member1);

        Member member2 = new Member();
        member2.setName("spring2");
        repository.save(member2);

        Member result = repository.findByName("spring1").get();
        assertThat(result).isEqualTo(member1);

    }

    @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);
    }

}

 

2. service/MemberServiceTest

package hello.helloSpring.service;

import hello.helloSpring.domain.Member;

import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;

import hello.helloSpring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.*;


class MemberServiceTest {

    MemberService memberService;
    MemoryMemberRepository memberRepository;

	// 각 test가 시작되기 전 호출되는 메서드
    @BeforeEach
    public void beforeEach() {
        memberRepository = new MemoryMemberRepository();
        memberService = new MemberService(memberRepository);
    }

	//// 각 test를 마치고 항상 호출되는 메서드
    @AfterEach
    public void afterEach() {
        memberRepository.clearStore();
    }

    @Test
    void join() {
        //given
        Member member = new Member();
        member.setName("hello");

        //when
        Long saveId = memberService.join(member);

        //then
        Member findMember = memberService.findOne(saveId).get();
        assertThat(member.getName()).isEqualTo(findMember.getName()); //member.getName()대신 member도 가능
    }

    @Test
    public void 중복_회원_예외() { //test코드는 메서드 이름을 한글로 해도 무방하다고 한다!!
        //given(같은 이름을 가진 회원이 존재한다면)
        Member member1 = new Member();
        member1.setName("spring");

        Member member2 = new Member();
        member2.setName("spring");

        //when(join할 때 중복 회원 검증인 validateDuplicateMember할 때 터져야 정상)
        memberService.join(member1);
        // 원래 exception error는 try-catch로 잡는데, 이걸 편하게 해주는게 assertThrows()
//        try {
//            memberService.join(member2);
//        }catch (IllegalStateException e){
//            assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
//        }
        // 에러 메세지 비교를 통한 test
        IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join((member2)));
        assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");

        //then

    }

    @Test
    void findMembers() {
    }

    @Test
    void findOne() {
    }
}

 

IntelliJ 꿀팁 단축어

  • alt + insert -> getter, setter, constructor 등 자동 생성
  • ctrl + alt + m -> 메소드 추출하기
  • shift + F6 -> 같은 단어 찾고 바꿀 수 있음

정적 컨텐츠

client가 요청을 하면 내장 서버인 tomcat이 ~~이런 요청이 들어왔어!라고 하면서 스프링부트 쪽으로 넘겨준다.

그럼 제일 먼저 호출되는 것은 Controller이다. 따라서 url에 get 방식으로 요청이 들어왔을 때, 그와 mapping(@GetMapping)된 함수가 없다면 /static 폴더에서 그에 맞는 파일을 찾아서 넘겨준다.

 

MVC 템플릿 엔진

MVC: Model, View, Controller 

이전 강의에서 배웠듯이, Controller는 Model에 데이터를 담아 View(/templates)에게 쏴준다.

정적 컨텐츠를 쏴줄 때와 다른 점은 Controller가 mapping된 함수를 찾아서 model에 데이터를 넣고 viewResolver에게 이에 맞는 view를 찾아서 변환해줘라~ 라고 한다. 그럼 viewResolver는 템플릿엔진으로 처리하여 변환된 html파일을 client에 반환하여 주는 방식이다.

hello-template.html
view를 통해 반환된 화면의 소스코드

 

API

스프링에서 API 방식은 데이터를 넘겨줄 때 사용한다.

여기서 데이터란 간단한 문자열이 될 수도 있고, 객체가 될 수도 있다. 객체를 넘겨줄 때 기본 형식은 json형식이라고 한다.

이때 annotation은 @ResponseBody이다.

위 코드에서 보면 mapping이 "hello_api"로 되어있으므로 localhost:8080/hello_api(get 방식 요청) 라고 요청했을 때, Controller는 이 함수를 찾을 것이다.

이 때, @RequestParam("name") String name 이라고 해서 파라미터로 name의 값을 받아 name에 넣겠다는 뜻이다.

그럼 요청을 할 때, localhost:8080/hello_api?name=h32j00 라고 하면 name에는 h32j00가 저장된다.

그리고 코드를 보면 return 하는 값이 단순 문자열이 아닌 hello라는 객체임을 확인할 수 있다. 

이 때, Controller가 hello 객체를 보내면 viewResolver가 아닌 HttpMessageConverter(MappingJackson2HttpMessageConverter)가 동작한다. 이 converter가 객체를 json 형식으로 바꿔서 http의 body부분에 넣고 client로 보낸다.

 

@ResponseBody를 사용하여 단순 문자열을 반환했을 때의 소스코드보기 화면
json 형식으로 객체를 넘길 때

 

[2021.01.03] 스프링 공부시작

스프링을 공부해보기 위해 아주 좋은 평의 강의를 찾아냈다.

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EC%9E%85%EB%AC%B8-%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8

 

스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 - 인프런

스프링 입문자가 예제를 만들어가면서 스프링 웹 애플리케이션 개발 전반을 빠르게 학습할 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 서버 개발 Back-End Java Spring MVC Spring Boot 온라인

www.inflearn.com

 

하루에 글 하나를 써보며 그 날 배운 내용을 정리하면서 진행하기로 하였다.

 

프로젝트를 쉽게 만들기 위한 페이지

https://start.spring.io/

 

웹 개발을 하기 위해 최소 필요한 라이브러리

Spring Web과 템플릿 엔진인 Thymeleaf이다.

IntelliJ에서 Gradle이라는 곳을 보면 각 라이브러리의 의존성에 대한 라이브러리들이 쭈르륵 하고 나온다.

 

간단한 동작방식

1. localhost:8080에 접속한다.

spring-boot-starter-web을 땡겨오면 tomcat도 땡겨온다. 즉, 내장서버를 가지고 있다는 사실..

그래서 기본적으로 8080포트를 가지고 접속하면 된다.

 

2. 스프링부트의 welcome page는 /resources/static/index.html 라고 한다.

이 코드의 맞는 정적인 페이지를 보여준다.

3. localhost:8080/hello에 접속한다.

url창에 localhost:8080/hello라고 치면 get 방식으로 통신이 된다.

 

3-1. 내장 톰캣 서버가 hello 아니? 하면서 물어보면

hello라는 문자열에 GetMapping된 클래스를 호출한다. 그때 Model을 만들어서 넘겨주는 데 여기서 Model에 값을 넣어서 return "hello"를 한다.

 

3-2. return "hello"는 이 값을 담은 Model을 hello.html로 보낸다는 의미이다.

 

3-3. hello.html은 /resources/templates/ 아래에 존재한다.

 

3-4. /resources/templates/hello.html

위 코드에서 ${data}라고 받는 부분이 아까 controller에서 보낸 Model에서 attributeName이 'data'인 attributeValue를 뿌려주겠다는 것이다. 즉 ${data}는 hello!!가 되겠지??

 

오늘은 여기까지 하고 내일 다시 공부해야겠다!!