김영한 - 스프링 입문
인프런 김영한님의 Spring 입문 강의를 수강한 내용 정리한 포스트입니다.
Spring은 java엔터프라이즈 웹 애플리케이션 개발과 관련된 전반의 생태계를 제공함, 엄청나게 많은 내용이 있어서 머리에 다 담을 수가 없음
고로, 필요한걸 찾아 사용하는 능력이 필요함 https://spring.io/projects/ 전반적인 ref가 있음
@Controller
public class HelloController {
@GetMapping("hello")
public String hello(Model model){
//attributeName은 key값, attributeValue는 value값
model.addAttribute("data","hello !!");
return "hello";
}
}
Controller의 return "hello"는 템플릿에 hello라는 html파일을 실행하라는 의미
issue
(intellij terminal에서 git bash(리눅스 명령어)을 사용하고싶으면 Setting -> terminal에서 변경가능)
build했을때, libs 디렉토리가 생성이안되면, ./gradlew build 대신에 ./gradlew clean build 실행
정적 컨텐츠 : 그냥 쉽게 말해서, 그냥 html내용 웹브라우저로 옮겨주는거
MVC 와 템플릿 : 지목당한 컨트롤러가 모델의 속성값 등 정보를 return에 템플릿에 담아서 viewResolver에 보냄, view에서는 전달받은 템플릿을 렌더링해서 웹에 전달하는 방식
api : @Responsebody를 선언해주면 viewResolver사용하지않고, http의 body부에 반환값을 넘겨줌 밑에 상세 과정
- 데이터: 회원ID, 이름
- 기능: 회원 등록, 조회
- 아직 데이터 저장소가 선정되지 않음(가상의 시나리오)

현업 tips(메소드 네이밍 팁)
service는 비즈니스에 가까운 네이밍을해야됨
repository는 기계적으로 우리가하던대로 네이밍함

아직 어떤 DB를 사용할지 안정한 상태라서, 인터페이스로 구현해 언제든지 DB를 갈아낄 수 있도록함 여기서는 그냥 메모리에 저장하는 클래스 구현
자바는 Junit이라는 프레임워크를 제공해줘서 테스트하기 용이함
여기 예제에서는 먼저 클래스를 구현한 다음 테스트코드를 구현하는데
테스트 클래스를 먼저 작성한다음에 해당 클래스를 만들 수 있음 이걸 TDD라고함
테스트 주도 개발 (TDD) : 테스트를 먼저 만들어서 구현 클래스를 만들어서 돌려보는것
TDD 매우 중요함 따로 공부하자
@AfterEach
한번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있음.
이렇게되면 다음 이전 테스트 때문에 다음 테스트가 실패할 수 있음.
@AfterEach는 각 테스트 종료 후 콜백되는 함수이므로 이를 이용해서 메모리DB에 저장된 데이터를 clean해주는것으로 문제해결 가능
그리고 테스틑 각각 독립적으로 실행되어야함, 테스트 순서에 의존관계가있는건 좋은 테스트가 아님
EX : findAll(), findByName()각각 독립적으로 실행하면 통과되는데, 한번에 클래스로 실행하면 통과안됨
@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);
}
여기서 생성된 member1이 findAll로 인해 이미 저장됨
@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);
}
그래서 findByName에서 findAll에서 저장된 member1의 spring1 이 나와버려서 테스트 통과가 안되는것, 고로 @AfterEach로 각 테스트가 끝난 후에 DB를 clear해줘야됨
(각각 독립적으로 실행하면 잘됨,)
레포지토리 테스트처럼 서비스도 테스트코드 구현해야됨
@Test
void join() {
//given - 주어진 상황(데이터)
Member member = new Member();
member.setName("kibum");
//when - 이게 실행됐을떄
long saveId = memberService.join(member);
//then - 이게 나와야됨
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
given, when , then 테스트 코드 패턴
@BeforeEach : 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고,
의존관계도 새로 맺어줌
멤버 컨트롤러가 멤버 서비스를 사용하고 멤버 서비스가 멤버 리포지토리를 사용하는 관계를 형성,(의존관계형성)

컴포넌트 스캔과 자동 의존관계 설정
자바 코드로 직접 스프링 빈 등록하기
@Controller , @Service, @Repository를 각각 컨트롤러,서비스,리포지토리 클래스에 추가해줌
위 세개의 애노테이션은 @Component가 포함되어 있어서 스프링 빈에 자동으로 추가됨 그 후 생성자 부분에 @Autowired애노테이션을
추가해주면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입을 해준다.(생성자가 하나면 @Autowired생략가능)
스프링 빈에 등록되어있지 않으면 @Autowired가 동작하지않음
Ex : MemberController.java
@Controller //@Component 포함, 자동으로 스프링 빈에 추가됨
public class MemberController {
private final MemberService memberService;
@Autowired //멤버 컨트롤러가 생성될때, 스프링 빈에 등록되어있는 멤버서비스 객체를 가져다가 끌어다줌 (의존관계 주입)
public MemberController(MemberService memberService){
this.memberService = memberService;
}
}
참고로, 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다(유일하게 하나만
등록해서 공유한다) 따라서 같은 스프링 빈이면 모두 같은 인스턴스임 설정으로 싱글톤이 아니게 설정할 수
있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용함
DI(의존성 주입)에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있는데 의존관계가 실행중에
동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장함
스프링 패키지에 SpringConfig 클래스 추가
추가하면 @Controller @Service @Repository등의 애노테이션을 추가하지않아도 이 설정파일을 통해 스프링 빈에 등록됨
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
실무에서는 주로 정형화된 컨트롤러,서비스,리포지토리 같은 코드는 컴포넌트 스캔방식을 사용하는데
이 실습에서는 아직 어떤 DB를 사용할지 몰라서 멤버리포지토리를 인터페이스로 만들고 임시로 메모리리포지토리 구현클래스를 사용했음
따라서 자바 코드로 직접 스프링 빈을 등록하는 방식을 채택해서 나중에 이 클래스의 멤버리포지토리 메소드에서 반환부만 바꾸면 작동하도록 하기위해서 자바 코드로 직접 스프링 빈을 등록하는 방식을 채택
--> 이게 설정파일을 직접 만들어서 스프링 빈을 등록하는 방식의 장점임
H2 데이터베이스(간단한 데모버전을 위한 가벼운 디비)를 설치하고, 이를 통해 순수 jdbc(자바에서 제공하는 데이터베이스 접근 api)리포지토리를 구현
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
issue
스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해주어야 한다. 그렇지
않으면 Wrong user name or password 오류가 발생한다. 참고로 다음과 같이 마지막에 공백이
들어가면 같은 오류가 발생한다. spring.datasource.username=sa 공백 주의, 공백은 모두
제거해야 한다.
Jdbc 리포지토리인 JdbcMemberRepository를 구현(MemberRepository 인터페이스를 구현하는 구현 클래스)
JDBC API로 직접 코딩하는 것은 20년 전 이야기임, 매우 과거의 방식
스프링 설정 파일 변경
SpringConfig클래스에서 스프링컨테이너에 리포지토리를 빈으로 올리기 위한 memberRepository메소드의 반환부를 위에서 구현한 JdbcMemberRepository로 변경.
@Bean
public MemberRepository memberRepository(){
//return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
위에 JdbcMemberRepository에서 인자로 넘겨주는 DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둠 그래서 DI를 받을 수 있음
클래스 관계도

SpringConfig를 통해 설정해준 이미지

--> 메모리기반 구현체의 연결을 끊고 새로 구현해준 jdbc버전의 구현체로 연결
스프링 컨테이너와 DB까지 연결한걸 MemberServiceIntegrationTest코드를 추가해서 테스트 진행
MemberServiceIntegrationTest.java
@SpringBootTest
@Transactional //테스트 시작전에 트랜잭션을 시작하고, 테스트완료 후에는 항상 롤백함 --> DB에 데이터가 안남아서 다음 테스트에 영향x
class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
.
.
.
}
@Test
public void 중복_회원_예외() throws Exception {
.
.
.
}
}
JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해줌
JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있음
JPA를 사용하면 개발 생산성을 크게 높일 수 있음
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
show-sql : JPA가 생성하는 SQL을 출력
ddl-auto : JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none 를 사용하면 해당 기능을 끔
create 를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해줌
@Entity //jpa가 관리하는 entity임을 선엄
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//@Column(name = "username") 만약 DB상에서 Colume명이 username이나 다른거면 이 애노테이션으로 매핑해줘야됨
private String name;
.
.
.
JPA 회원 리포지토리구현 (iv로 private final EntityManager em; 추가)
서비스 계층에 트랜잭션 추가(@Transactional)
org.springframework.transaction.annotation.Transactional 를 사용.
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을
커밋함 만약 런타임 예외가 발생하면 롤백해준다.
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 함
JPA를 사용하도록 스프링 설정 변경, EntityManager em;를 추가해 인젝션하도록 생성자에 추가하고 메모리리포지토리 반환부 교환
스프링 데이터 JPA를 사용하면, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있음(지렸음..)
그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공함
public interface SpringDataJpaMemberRepository extends JpaRepository<Member,Long>, MemberRepository {
Optional<Member> findByName(String name);
}
이게 코드의 전부, JpaRepsitory, MemberRepository 두개를 상속받는걸로 끝..
findByName은 스프링데이터 JPA가 제공하는 find가 pk기반이다 보니깐 추가된것 이마저도 구현부의 메소드 이름만 규칙성에 따르게 지어주면 알아서 구현해준다....
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
스프링 데이터 JPA가 SpringDataJpaMemberRepository를 스프링 빈으로 자동 등록해줌 이전처럼 별도의 메소드로 스프링 빈에 등록작업을 안해줘도됨

AOP를 쓰는 이유
AOP를 사용하지않고, 공통적으로 실행되는 로직을 추가할때 문제
MemberService에 각 메소드마다 시간을 측정하는 로직을 추가함
- 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아님
- 시간을 측정하는 로직은 공통 관심 사항
- 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어려움
- 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어려움
- 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야됨(시간 측정 단위를 s에서 m/s로 변경할때 등)
AOP 적용
TimeTraceAop.java
@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");
}
}
}
기존

AOP 적용

프록시라는 가짜? 스프링 빈을 등록하여 실제 스프링 빈에 DI하기전에 프록시 빈을 거쳐가도록 동작하게됨