강의 소개
강의 소개
학습 방법: 처음부터 끝까지 직접 코딩
프로젝트 환경설정
프로젝트 생성
Maven이랑 Gradle이란, 필요한 라이브러리를 가져오고, 빌드하는 라이프 사이클까지 관리해 주는 툴.
요즘엔 Gradle을 거의 쓴다.
스프링 부트 3.0 스프링 부트 3.0을 선택하게 되면 다음 부분을 꼭 확인해주세요.
1. Java 17 이상을 사용해야 합니다.
2. javax 패키지 이름을 jakarta로 변경해야 합니다. 오라클과 자바 라이센스 문제로 모든 javax 패키지를 jakarta로 변경하기로 했습니다.
3. H2 데이터베이스를 2.1.214 버전 이상 사용해주세요.
패키지 이름 변경 예) JPA 애노테이션 javax.persistence.Entity jakarta.persistence.Entity 스프링에서 자주 사용하는 @PostConstruct 애노테이션 javax.annotation.PostConstruct jakarta.annotation.PostConstruct 스프링에서 자주 사용하는 검증 애노테이션 javax.validation jakarta.validation 스프링 부트 3.0 관련 자세한 내용은 다음 링크를 확인해주세요: https://bit.ly/springboot3
group: 기업 도메인 이름 같은 걸 주로 넣는다. com.example
artifact: 빌드되어 나올 때 결과물(프로젝트 명 같은)
Dependencies: 어떤 라이브러리 가져올지
HTML을 만들어 주는 템플릿 엔진 중 하나가 Thymeleaf
.idea: 인텔리제이가 사용하는 어떤 설정 파일
main 하위의 resources: 자바 코드 파일을 제외한 xml이나 html이나 설정 파일 등이 들어간다. 자바 파일 제외한 나머지.
build.gradle: 간단히 말하면 버전 설정하고 라이브러리 가져오는 것
repositories에 적힌 mavenCentral()의 의미: dependencies에 나온 라이브러리들을 mavenCentral에서 다운로드하겠다는 의미. 특정 사이트 URL을 넣어도 된다.
.gitignore: git에는 필요한 소스 코드 파일만 올라가야 하고, 빌드된 결과물 등은 올라가면 안 된다.
이 간단한 코드를 실행하면 톰캣이라는 웹 서버를 자체적으로 띄우면서 스프링 부트가 같이 올라온다.
최근 IntelliJ 버전은 Gradle을 통해서 실행하는 것이 기본 설정이다. 이렇게 하면 실행 속도가 느리다. 다음과 같이 변경하면 자바로 바로 실행해서 실행 속도가 더 빠르다.
File - Settings - Build, Execution, Deployment - Build Tools - Gradle에서
Build and run using: IntelliJ IDEA
Run tests using: IntelliJ IDEA
라이브러리 살펴보기
이번 시간은 가볍게 보기
앞 시간에 언급된 Thymeleaf 같은 라이브러리 말고도
External Libraries 들어가 보면 가져온 라이브러리들이 더 많다.
요즘엔 gradle이나 maven 같은 빌드 툴들이 의존 관계를 다 관리해 준다. 예를 들어 spring-boot-starter-web을 가져오면, 이 라이브러리가 필요한 라이브러리들 톰캣, 스프링 MVC 등 필요한 걸 가져온다.
즉, 우리는 spring-boot-starter-web이 필요하지만, spring-boot-starter-web이 필요로 하는 것들, 그리고 그것들이 또 필요로 하는 것들을 가져온다.
(의존 관계가 있는 라이브러리들을 함께 다운로드한다.)
인텔리제이 오른쪽의 Gradle 버튼의 Dependencies 항목에서 의존 관계를 확인할 수 있다.
옛날엔 톰캣 같은 웹 서버를 직접 서버에 설치하고, 거기에 자바 코드를 넣는 식으로, 웹 서버와 개발 라이브러리가 완전히 분리되어 있었다.
요즘엔 소스 라이브러리에서 웹 서버를 들고 있다.(내장하고 있다.(임베디드)). 자바 메인 메서드만 실행하는데 웹 서버가 뜬다. 그래서 8080으로 들어갈 수 있다.
현업에선 System.out.println()은 잘 안 쓰고 logging을 주로 쓴다.
slf4j: 쉽게 말해서 인터페이스
logback: 실제 로그를 어떤 구현체로 출력할지를 logback을 요즘에 많이 선택한다.
요즘엔 slf4j와 logback 조합을 많이 써서 스프링에서도 spring-boot-starter-logging을 가져오면 기본적으로 이 2개의 라이브러리도 자동으로 가져온다.
자바에선 테스트할 때 junit이라는 라이브러리를 많이 쓴다. 그래서 스프링에서도 이 라이브러리를 쓴다. 요즘엔 junit 5 버전을 많이 쓴다.
영상에선 확인 안 했지만 spring-test는 스프링과 통합해서 테스트하도록 도와주는 라이브러리
(스프링 부트 라이브러리)
spring-boot-starter-web
spring-boot-starter-tomcat: 톰캣 (웹서버)
spring-webmvc: 스프링 웹 MVC
spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
spring-boot
spring-core
spring-boot-starter-logging
logback, slf4j
(테스트 라이브러리)
spring-boot-starter-test
junit: 테스트 프레임워크
mockito: 목 라이브러리
assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
spring-test: 스프링 통합 테스트 지원
View 환경설정
스프링 부트는 static 폴더에 index.html 넣어 두면 이것을 웰컴 페이지로 해 준다.
웰컴 페이지는 도메인만 누르고 왔을 때의 첫 화면이다.
static의 index.html은 정적 페이지이다. 프로그래밍이 아니다.
템플릿 엔진을 쓰면 루프를 쓰는 등, 모양을 바꿀 수 있다. 예를 들면 Thymeleaf 등.
웹 애플리케이션에서 첫 번째 진입점이 Controller이다.
Controller 클래스엔 @Controller이란 Annotation을 적어야 한다.
@GetMapping("hello") 하면 웹 애플리케이션에서 /hello가 들어오면 이 메서드를 호출한다.
여기서의 get은 HTTP의 get 메서드이다.
URL에 매칭이 된다.
@Controller
public class HelloController {
@GetMapping("hello")
public String hello(Model model) {
model.addAttribute("data", "hello!!");
return "hello";
}
}
스프링이 Model을 하나 만들어서 넣어준다.
return이 hello인 건 templates 폴더의 hello.html를 찾아서 이 화면을 실행시키라는 뜻.

컨트롤러에서 리턴값으로 문자를 반환하면 뷰 리졸버( viewResolver )가 화면을 찾아서 처리한다. 스프링 부트 템플릿 엔진 기본 viewName 매핑 resources:templates/ +{ViewName}+ .html
스프링 웹 개발 기초
정적 컨텐츠
웹을 개발한다는 것은 크게 3가지 방법이 있다.
정적 컨텐츠: 이전의 웰컴 페이지처럼 서버에서 뭐 하는 거 없이 파일을 그대로 웹 브라우저에 올려 주는 것
MVC와 템플릿 엔진: JSP나 PHP도 템플릿 엔진이다. HTML을 그냥 주는 게 아니라 서버에서 프로그래밍해서 동적으로 바꾼다. 그걸 하기 위해 MVC 패턴으로 많이 개발한다.
API: json 등 데이터가 오고 갈 때..(깊이 있게 가면 정의가 달라질 수 있다.)
templates 폴더 하위의 hello.html은
localhost:8080/hello였지만
static 폴더 하위의 hello-static.html은 localhost:8080/hello-static.html
정적 컨텐츠엔 프로그래밍을 할 수는 없고, 그대로 반환한다.

MVC 깊게 들어가면 여러 가지가 있지만 일단 대략적으로만 나타낸 그림이다.
웹 브라우저에서 local~~hello-static.html을 입력 후 엔터하면 우선
내장 톰캣 서버가 요청을 받고 스프링에 넘긴다.
스프링은 우선 컨트롤러 쪽에서 hello-static이 있는지 찾는다.(즉, 컨트롤러가 우선순위를 갖는다.)
그런데 없기 때문에, 그다음엔 내부의 resources 내부에서 static/hello-static.html을 찾는다.
있으면 그걸 반환한다.
MVC와 템플릿 엔진
예전엔 MVC 방식이 아니라 JSP를 통해 View에 모든 걸 넣었다. View 안에서 DB도 접근하고 Controller 로직도 다 있는 등 수천 라인이 될 수 있다.
그것을 Model1 방식이라 했다.
지금은 MVC 방식으로 많이 한다.
View는 화면을 그리는 데에만 집중한다.
Controller나 Model은 비즈니스 로직과 관련 있거나, 내부적인 걸 처리하는 데 집중한다.
요즘엔 Controller와 View를 쪼개는 게 기본이다.
View는 화면과 관련된 일만, 비즈니스 로직이나 서버 뒤쪽에 관련된 건 Controller나 뒤쪽 비즈니스 로직에서 다 처리하고, Model에다 화면에 필요한 것들을 담아서 화면에 넘기는 패턴을 많이 사용한다.
@GetMapping("hello-mvc")
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello-template";
}
localhost:8080/hello-mvc?name=spring
http get 방식
localhost:8080/hello-mvc?name=spring
이렇게 하면 Controller에서 name이 spring으로 바뀌고 Model에 담긴다. 그게 템플릿으로 넘어간다.
hello-template.html에 있는 ${name}은 Model에서 값을 꺼내서 치환한다.

웹 브라우저에서 주소창에 localhost:8080에 hello-mvc 넘기면 스프링 부트 띄울 때 같이 띄우는 내장 톰캣 서버를 먼저 거친다. 내장 톰캣 서버는 hello-mvc를 스프링에게 건네준다. 스프링은 HelloController의 메서드에 매핑되어 있는 것을 확인 후 그 메서드를 호출한다.
viewResolver, 화면과 관련된 해결자가 동작한다.
viewResolver는 View를 찾아 주고 템플릿 엔진을 연결시켜 준다. (MVC 강의에서 더 자세히 설명)
viewResolver가 templates의 hello-template.html(리턴값과 똑같은)을 찾아서 타임리프 템플릿 엔진에게 처리하도록 넘긴다.
템플릿 엔진이 렌더링 해서 변환한 HTML을 웹 브라우저에 반환한다.
정적일 땐 변환하지 않고 그대로 반환했었지만, 이런 템플릿 엔진에선 변환 후 웹 브라우저에 넘긴다.
API
정적 컨텐츠를 제외하면 2가지만 기억하면 된다.
HTML로 넘기는 MVC 방식, API 방식으로 데이터를 바로 넘기는 방식
@ResponseBody는 HTML에 나오는 body 태그를 의미하는 게 아니라, HTTP의 body를 말한다.(HTTP엔 header와 body가 있다.)
HTTP의 body에 데이터를 내가 직접 넣어 준다는 의미이다.
이전의 템플릿 엔진과는 다르게 View가 없다. 단지 요청한 클라이언트에 문자가 그대로 전해진다.
페이지 소스 보기로 확인해 봐도 HTML 태그가 전혀 없이 문자만 뜬다.
이전의 템플릿 엔진은 화면을 가지고, 뷰라는 템플릿이 있는 상황에서 조작하는 방식이고
API 방식은 데이터를 그대로 준다. API 방식으로 HTML 태그를 넘겨 주어도 되긴 하는 듯
@ResponseBody 를 사용하면 뷰 리졸버( viewResolver )를 사용하지 않는다.
API 방식은 지금 설명한 방식으로는 거의 안 쓴다.
데이터를 달라고 하는 상황에서 많이 쓰인다.
Hello 클래스를 만든 후
@GetMapping("hello-api")
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
이걸 추가한 후
localhost:8080/hello-api?name=spring
들어가면
{"name":"spring"}가 출력된다. (JSON 형태)
@ResponseBody 를 사용하고, 객체를 반환하면 객체가 JSON으로 변환된다.
과거엔 XML 방식도 많이 쓰였었다. HTML 태그도 XML 방식으로 쓰인 거다.
JSON이 더 심플해서 요즘엔 거의 JSON 방식으로 통일됐다.
스프링에서도 객체를 반환하고 @ResponseBody로 해 놓으면 JSON으로 반환하는 게 디폴트이다. 물론 XML로 변경할 수도 있다.

웹 브라우저에서 localhost:8080/hello-api
를 치면 내장 톰캣 서버에서 hello-api를 스프링에게 준다. 스프링은 hello-api에 대응하는 것을 인식하지만 @ResponseBody가 붙어 있는 것을 확인한다.
만약 이게 없었다면 템플릿처럼 ViewResolver에게 주는데, @ResponseBody가 있으면 HTTP 응답에 그대로 데이터를 넘기는 식으로 동작한다.
그런데 hello는 문자가 아니라 객체이다. 문자라면 HTTP 응답에 바로 넘겼지만
객체가 오면 JSON 방식으로 데이터를 만들어서 HTTP 응답에 반환한다. 이것이 기본 정책이다.(디폴트)
즉, @ResponseBody이면 hello 객체를 보고 몇 가지 조건을 보는데,
우선 HttpMessageConverter가 동작한다.(기존의 MVC에선 ViewResolver가 동작했다.)
단순 문자였다면 StringConverter가 동작한다.
그런데 객체이면 JsonConverter가 기본으로 동작하여 JSON 스타일로 바꾼다.
JSON으로 바꾼 것을 나를 요청한 웹 브라우저 혹은 안드로이드 클라이언트 혹은 서버 등에게 응답한다.
@ResponseBody 를 사용
HTTP의 BODY에 문자 내용을 직접 반환
ViewResolver 대신에 HttpMessageConverter 가 동작
기본 문자처리: StringHttpMessageConverter
기본 객체처리: MappingJackson2HttpMessageConverter
byte 처리 등등 기타 여러 HttpMessageConverter가 기본으로 등록되어 있음
여기서 Jackson은? 객체를 JSON으로 바꿔 주는 유명한 라이브러리가 몇 개 있다. 대표적으로 2개가 있는데 실무에서 많이 보는 건 Jackson, Gson이다. 스프링은 Jackson을 기본적으로 탑재하도록 선택했다. 물론 Gson으로 바꿀 수 있다.
객체를 JSON으로 바꾸는 것이 여기서의 MappingJackson2HttpMessageConverter가 해 준다.
참고: 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보 둘을 조합해서 HttpMessageConverter 가 선택된다. 더 자세한 내용은 스프링 MVC 강의에서 설명하겠다.
HTTP가 요청할 때 받길 원하는 포맷을 말하는 Accept 헤더가 있는데 거기서 JSON을 요청하면 JSON으로 받고, 아무것도 안 보내면 스프링이 알아서 보낸다. 그런데 XML 등 특정 포맷으로 받을 거라는 요청이 있으면 그에 해당하는 HttpMessageConverter가 동작한다.
하지만 요즘엔 거의 JSON을 사용한다.
정리하자면,
정적 컨텐츠: 파일을 그냥 준다.
MVC와 템플릿 엔진: 템플릿 엔진을 MVC 방식을 쪼개서 View를 템플릿 엔진으로 HTML을 좀 더 프로그래밍한 거로 렌더링 해서 렌더링이 된 HTML을 클라이언트에게 전달해 준다.
API: HttpMessageConverter를 통해 객체를 JSON 스타일로 변환해서 반환한다. View 없이 바로 HTTP 응답 메시지에 넣어 반환한다.
회원 관리 예제 - 백엔드 개발
비즈니스 요구사항 정리
이 강의에서의 비즈니스 요구사항은 가장 쉬운 걸로 한다.
데이터 저장소가 선정되지 않은 가상의 시나리오이다. 개발자는 개발해야 하는데 DB가 선정이 되지 않은 상태로 가정한다.(RDB로 할지 NoSQL로 할지 등..) 그럼에도 개발해야 하는 상황이다.
컨트롤러는 앞에서 봤다. 웹 MVC에서 Controller 역할 혹은 API 만들 때 Controller의 역할을 한다.
서비스는 서비스 클래스에 핵심 비즈니스 로직이 들어가 있다. 예를 들어 회원은 중복 가입이 안 된다거나 등의 로직들이 서비스 객체에 있다.
도메인은 회원, 주문, 쿠폰 등 DB에 주로 저장하고 관리되는 비즈니스 도메인 객체이다.
회원을 저장하는 건 인터페이스로 설계할 것이다. 왜냐하면 아직 데이터 저장소가 선정되지 않았기 때문에, 인터페이스를 만든 후 구현체를 MemoryMemberRepository 메모리 구현체로 만들 것이다. 일단 개발을 해야 하니 메모리로 단순하게(이건 금방 만든다.) 만들 것이고 향후에 이것을 RDB로 할지, JPA로 할지 등 구체적인 기술이 선정되고 나면 이것을 바꿔 끼울 것이다. 바꿔 끼우려면 인터페이스가 필요하다.
회원 도메인과 리포지토리 만들기
findById나 findByName으로 가져올 때 null이 반환될 수 있다.
요즘엔 null을 그대로 반환하기보단 Optional로 감싸서 반환하는 방법을 많이 선호한다.
Optional은 자바8에 들어간 기능이다.
실무에선 동시성 문제가 있을 수 있어서 공유되는 변수일 경우 ConcurrentHashMap을 써야 하는데, 지금은 단순한 예제이므로 그냥 HashMap 사용한다.
sequence도 실무에선 long보단 동시성 문제를 고려해서 AtomicLong 등을 쓴다. 지금은 그냥 단순하게 long 하겠다.
예전엔 return store.get(id); 이렇게 했었지만
이 결과가 없으면 null이 나올 수 있다.
요즘엔 null이 나올 가능성이 있으면 Optional로 감싼다.
Optioanl.ofNullable(store.get(id));
이렇게 하면 store.get(id)가 null이어도 감싼다.
감싸서 반환하면 클라이언트에서 무언가를 할 수 있다.
findAny()는 하나라도 찾으면 그걸 Optional로 반환한다. 만약 끝까지 없으면 Optional에 null이 포함되어서 반환된다.
실무에선 List를 많이 쓴다. 루프 돌리기 편하는 등의 이유로
이것들이 제대로 동작하는지 검증하는 게 테스트 케이스 작성이다.
회원 리포지토리 테스트 케이스 작성
<회원 리포지토리 테스트 케이스 작성>
개발한 기능을 실행해서 테스트할 때 자바의 main 메서드를 통해서 실행하거나, 웹 애플리케이션의 컨트롤러를 통해서 해당 기능을 실행한다. 이러한 방법은 준비하고 실행하는 데 오래 걸리고, 반복 실행하기 어렵고 여러 테스트를 한 번에 실행하기 어렵다는 단점이 있다. 자바는 JUnit이라는 프레임워크로 테스트를 실행해서 이러한 문제를 해결한다.
org.junit.jupiter가 제공하는 Assertions.assertEquals(Object expected, Object actual);
로 테스트해 본다.
같다면 출력되는 건 없다. 녹색 불이 뜬다.
만약 다르다면 빨간 불과 함께 아래 메시지 뜬다.
org.opentest4j.AssertionFailedError:
Expected :hello.hellospring.domain.Member@2002fc1d
Actual :null
요즘엔 org.assertj의 Assertions 많이 쓴다.
Assertions.assertThat(member).isEqualTo(result);
혹은 static import 하면
assertThat(member).isEqualTo(result);
만약 다르면 빨간 불과 함께
org.opentest4j.AssertionFailedError:
expected: null
but was: hello.hellospring.domain.Member@22e357dc
Expected :null
Actual :hello.hellospring.domain.Member@22e357dc
실무에선 빌드 툴과 엮어서 빌드 툴에서 빌드할 때 테스트 케이스 통과하지 않으면 다음 단계로 못 넘어가게 막는다.
shift F6은 rename해 준다.
테스트 케이스의 좋은 장점은 클래스 레벨에서도 테스트해 볼 수 있다.
클래스 레벨에서 테스트해 볼 때 메서드들의 실행 순서는 보장이 안 된다. 모든 테스트는 순서와 상관없이 메서드별로 따로 동작하게 설계해야 한다.
테스트는 순서에 의존적으로 설계하면 절대 안 된다.
테스트가 하나 끝나고 나면 Repository 데이터를 클리어해 줘야 한다.
@AfterEach
public void afterEach() {
repository.clearStore();
}
각 메서드가 끝날 때마다 동작하는 콜백 메서드이다.
@AfterEach : 한 번에 여러 테스트를 실행하면 메모리 DB에 직전 테스트의 결과가 남을 수 있다. 이렇게 되면 이전 테스트 때문에 다음 테스트가 실패할 가능성이 있다. @AfterEach를 사용하면 각 테스트가 종료될 때마다 이 기능을 실행한다. 여기서는 메모리 DB에 저장된 데이터를 삭제한다. 테스트는 각각 독립적으로 실행되어야 한다. 테스트 순서에 의존 관계가 있는 것은 좋은 테스트가 아니다.
지금까지 한 건 먼저 개발을 하고, 그다음에 테스트를 작성했다.
그런데 반대로 테스트 클래스를 먼저 작성하고 개발을 할 수도 있다. 내가 뭔가를 만들어야 하는데 이를 검증할 수 있는 틀을 먼저 만들어야 할 때 필요하다.
이를 테스트 주도 개발(TDD)이라고 한다.
즉, 테스트 클래스를 먼저 만들고, 구현 클래스를 만들어서 돌려 보는 것이다.
오늘 한 건 TDD가 아니다.
여러 명이서 개발할 때 테스트 코드 없이 개발하는 건 어렵다.
테스트는 깊이 있게 공부하는 게 좋다.
회원 서비스 개발
회원 서비스 테스트
클래스 내에서 Ctrl + Shift + T를 하면 테스트 클래스 만든다.
테스트 클래스에선 한글로 쓰기도 한다.(외국 기업에서 일하는 게 아니면)
빌드될 때 테스트 코드는 실제 코드에 포함되지 않는다.
테스트할 땐 주석으로 given, when, then으로 나눠서 하는 게 초보일 땐 낫다.(항상 이렇게 나뉘는 건 아니므로 점점 변형해도 된다.)
테스트는 정상일 때를 확인하는 것도 중요하지만
예외일 때를 확인하는 것도 중요하다.
join 메서드의 중복 회원 검증을 테스트할 때 try catch를 사용해도 되지만,
assertThrows(IllegalStateException.class, () -> memberService.join(member2));
이걸 사용해도 된다. memberService.join(member2)를 실행하면 IllegalStateException 예외가 발생해야 한다.
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.")
이렇게 해도 된다.
MemberService에 있는 memberRepository 변수는 new MemoryMemberRepository()
그리고 MemberServiceTest에서 새로 만든 memberRepository도 new MemoryMemberRepository()로 만들었다.
즉, 2개는 다르다. 물론 MemoryMemberRepository에서 store 변수가 static이기 때문에 지금의 경우엔 상관없다. 그런데 static이 아니면 문제가 생길 수 있다.
즉, memberRepository가 2개의 다른 객체로 생성되면 내용물이 달라지는 등 문제가 생길 수 있다.
지금의 문제는 MemberService 객체에서 사용하는 new로 만든 MemoryMemberRepository랑 테스트 케이스에서 만든 MemoryMemberRepository가 서로 다른 객체이다.
즉, 같은 거로 테스트해야 하는 게 맞다.
해결하려면 우선
MemberService 클래스에 있는
private final MemberRepository memberRepository = new MemoryMemberRepository();
이 부분을
private final MemberRepository memberRepository;
이렇게 바꾸고
public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
이렇게 외부에서 넣어 주도록 생성자를 추가한다.
MemberServiceTest 부분에서도 아래 코드처럼 한다.
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
이러면 같은 MemoryMemberRepository가 사용된다.
이를 DI(Dependency Injection)이라고 한다.
beforeEach()에서 객체를 매번 새로 생성하므로
afterEach()의 clear가 필요 없을 것 같지만
store가 static 변수이기 때문에 객체를 새로 생성해도 store는 기존값을 공유한다. 그래서 afterEach()는 필요하다.
@BeforeEach: 각 테스트 실행 전에 호출된다. 테스트가 서로 영향이 없도록 항상 새로운 객체를 생성하고, 의존 관계도 새로 맺어준다.
스프링 빈과 의존관계
컴포넌트 스캔과 자동 의존관계 설정
지금까지 만든 것들을 화면으로 붙이고 싶은데 그러려면
컨트롤러 뷰 템플릿 등이 필요하다.
멤버 컨트롤러를 만들어야 하는데
멤버 컨트롤러가 멤버 서비스를 통해 회원가입하고, 멤버 서비스를 통해 데이터를 조회할 수 있어야 한다.
이렇게 되는 것을 의존 관계가 있다고 표현한다.
멤버 컨트롤러가 멤버 서비스를 의존한다고 표현한다.
스프링이 처음에 뜰 때 스프링 컨테이너라는 게 생기는데, 거기에 @Controller가 있으면 이 MemberController 객체를 생성해서 스프링에 넣어 둔다. 그리고 스프링이 관리한다.
스프링 컨테이너에서 스프링 빈이 관리된다고 표현한다.
스프링이 관리하게 되면 다 스프링 컨테이너에 등록하고, 스프링 컨테이너로부터 받아서 쓰도록 바꿔야 한다.
왜냐하면
public class MemberController {
private final MemberService = new MemberService();
}
이렇게 new로 하면 무슨 문제가 생기냐면, MemberController 말고 다른 여러 Controller들이 MemberService를 가져다 쓸 수 있는데, 예를 들어 주문 컨트롤러에서도 MemberService 가져다 쓸 수 있다. 회원은 여러 군데에서 쓰인다.
여기서 MemberService는 별 기능이 없다. 여러 개 생성할 필요가 없고 하나만 생성해서 같이 공용으로 쓰면 된다.
그렇기 때문에 이렇게 쓰는 것보단 스프링 컨테이너에 등록하고 쓰면 된다. 스프링 컨테이너에 등록하면 딱 하나만 등록된다. 그 외에도 여러 효과가 있는데 이건 나중에 설명한다.
@Controller
public class MemberController {
private final MemberService memberService;
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
이런 식으로 연결한다.
스프링 컨테이너가 뜰 때 MemberController 생성자를 호출하는데 생성자에 @Autowired가 있으면 스프링이 스프링 컨테이너에 있는 memberService를 가져다 연결한다.
이전 테스트에서는 개발자가 직접 주입했고, 여기서는 @Autowired에 의해 스프링이 주입해 준다.
이렇게 바로 실행하면 MemberService를 찾을 수 없다고 뜬다. 이유는?
@Autowired라고 하면 스프링 컨테이너에서 MemberService를 가져온다.
MemberController는 스프링이 뜰 때 스프링 컨테이너에 등록된다. 그리고 @Autowired라고 되어 있으면 스프링 컨테이너에서 관리하는 MemberService를 가져다 스프링이 연결시켜 준다. 그런데 안 되는 이유는
MemberService 클래스가 현재로서는 순수한 자바 클래스이기 때문이다. 스프링이 MemberService를 알 수 있는 방법이 없다.
그렇기 때문에 @Service를 지정해야 한다.
@Service로 지정하면 스프링이 올라올 때 스프링이 MemberService를 스프링 컨테이너에 등록한다.
마찬가지로 Repository 구현체로 가서도 @Repository로 지정하면 된다.
Controller를 통해 외부 요청을 받고, Service에서 비즈니스 로직을 만들고, Repository에서 데이터를 저장하는 건 정형화되어 있는 패턴이다.

Controller랑 Service랑 연결시켜 줘야 한다. 연결시켜 줄 때 생성자에 @Autowired 쓰면 된다. 그러면 MemberController가 생성될 때 스프링 빈에 등록되어 있는 MemberService 객체를 가져다 넣어 준다. 이것이 Dependency Injection(DI)이다.
마찬가지로 MemberService는 MemberRepository가 필요하다. 스프링이 MemberService 생성할 때 @Service를 확인하고 스프링 컨테이너에 등록하면서 생성자를 호출한다. 생성자에 @Autowired가 있으면 스프링 컨테이너에 있는 MemberRepository를 넣어 준다. 지금 같은 경우 구현체로 MemoryMemberRepository가 있으므로 이를 Service에 주입한다.
이 상태에서 HelloSpringApplication의 메인 메서드 실행하면 문제없이 잘 뜬다. 다만 Controller 관련된 어떤 기능도 없고, 지금은 연결이 잘 되는지만 확인한 것이다.
<스프링 빈을 등록하는 2가지 방법>
컴포넌트 스캔과 자동 의존 관계 설정
자바 코드로 직접 스프링 빈 등록하기
원래라면 @Service @Controller @Repository 대신
@Component라고 하면 된다.
다만 위 3개의 어노테이션 내부에 @Component가 등록되어 있다.
스프링이 올라올 때 @Component 관련된 어노테이션이 있으면 그것들은 다 객체로 생성해서 스프링 컨테이너에 등록한다.
<컴포넌트 스캔 원리>
@Component 애노테이션이 있으면 스프링 빈으로 자동 등록된다.
@Controller 컨트롤러가 스프링 빈으로 자동 등록된 이유도 컴포넌트 스캔 때문이다. @Component 를 포함하는 다음 애노테이션도 스프링 빈으로 자동 등록된다. @Controller @Service @Repository
참고: 생성자에 @Autowired를 사용하면 객체 생성 시점에 스프링 컨테이너에서 해당 스프링 빈을 찾아서 주입한다. 생성자가 1개만 있으면 @Autowired는 생략할 수 있다.
스프링을 사용하면 웬만한 건 다 스프링 빈으로 등록해야 한다. 그래야 얻는 이점이 많다. 자세한 건 나중에서 설명한다.
그럼 아무 곳에서나 @Component가 있어도 되는가?
우리는 HelloSpringApplication을 실행했다. 그게 속한
package hello.hellospring;
이 패키지와 하위 패키지들은 자동으로 스프링이 다 뒤져서 스프링 빈으로 등록한다.
즉, 이 패키지 또는 하위 패키지가 아니면 컴포넌트 스캔을 안 한다.
어떤 설정을 해 주면 되긴 하는데 기본적으론 컴포넌트 스캔 대상이 아니다.
참고: 스프링은 스프링 컨테이너에 스프링 빈을 등록할 때, 기본으로 싱글톤으로 등록한다.(유일하게 하나만 등록해서 공유한다.) 따라서 같은 스프링 빈이면 모두 같은 인스턴스다. 설정으로 싱글톤이 아니게 설정할 수 있지만, 특별한 경우를 제외하면 대부분 싱글톤을 사용한다.
메모리 절약 효과도 있다.
자바 코드로 직접 스프링 빈 등록하기
이전엔 컴포넌트 스캔과 @Autowired를 통해 스프링이 자동으로 인식하는 방법이었고,
이번엔 직접 설정 파일에 등록한다.
스프링 빈을 등록하는 2가지 방법 중 이번엔 자바 코드로 직접 스프링 빈 등록하는 법을 배운다.
MemberService에서 @Service와 @Autowired를 지우고
MemoryMemberRepository에서도 @Repository를 지운다.
Controller에선 그대로 둔다.
이 상태로 실행하면 컴포넌트 스캔이 안 되니까 MemberService가 스프링 빈에 등록이 안 되어 있다.
Consider defining a bean of type 'hello.hellospring.service.MemberService' in your configuration.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
이렇게 하면 스프링이 뜰 때 @Configuration을 읽고 스프링 빈에 등록하라는 뜻인 것을 인식한다.
그리고 @Bean의 MemberService를 로직대로 호출해서 스프링 빈에 등록한다.
MemberService는 스프링 빈에 있는 MemberRepository를 엮어 줘야 하므로 생성자 매개값으로 준다.
이렇게 하면 스프링이 올라올 때 MemberService와 MemberRepository를 스프링 빈에 등록하고, 스프링 빈에 등록되어 있는 MemberRepository를 MemberService에 넣어 준다. 그러면
아래 그림이 완성된다.

스프링이 올라올 때 MemberService를 스프링 컨테이너에 올리고 MemberRepository도 스프링 빈에 올려 준다.
스프링 빈에 등록된 MemberRepository를 MemberService에 넣어 준다.
Controller는 그대로 둬서 컴포넌트 스캔으로 올라가고, 컴포넌트 스캔이기 때문에 @Autowired로 한다. 그러면 스프링 빈으로 등록된 MemberService를 Controller에 넣는다.
과거엔 자바 코드로 빈 등록하는 방법 말고 XML로 설정하기도 했었는데 지금은 XML을 실무에서 잘 안 사용한다.
참고: DI에는 필드 주입, setter 주입, 생성자 주입 이렇게 3가지 방법이 있다.
필드 주입 방법은 스프링 뜰 때만 넣어 주고, 중간에 내가 바꿀 수 있는 방법이 없어서 안 좋다.
setter 주입 방법의 경우, 생성된 후에 setter를 호출해서 MemberService를 주입하는데, 이 경우엔 아무 개발자가 MemberController를 호출했을 때 setter 메서드가 public으로 열려 있다. 세팅이 되고 나면 MemberService를 중간에 바꿀 이유가 없는데도 public으로 노출되기 때문에 잘못하면 문제가 생길 수 있다.
의존 관계가 실행 중에 동적으로 변하는 경우는 거의 없으므로 생성자 주입을 권장한다.
스프링 컨테이너가 올라가고 다 세팅이 되는 시점에 생성자로 딱 한 번만 조립해서 끝내면 그다음엔 변경을 못 하도록 막아버릴 수 있다.
참고: 실무에서는 주로 정형화된 컨트롤러, 서비스, 리포지토리 같은 코드는 컴포넌트 스캔을 사용한다. 그리고 정형화되지 않거나, 상황에 따라 구현 클래스를 변경해야 하면 설정을 통해 스프링 빈으로 등록한다.
정형화된 컨트롤러, 서비스, 리포지토리라는 건 우리가 일반적으로 작성하는 컨트롤러, 서비스, 리포지토리 코드이다.
상황에 따라 구현 클래스를 변경해야 하는 경우는 이 강의에서 만든 비즈니스 요구사항처럼 아직 데이터 저장소가 선정되지 않는 경우 등이 있다.
그래서 지금은 MemberRepository 인터페이스로 설계하고, 구현체로 MemoryMemberRepository를 사용하는 그림이 되었다. 나중에 이 MemoryMemberRepository를 다른 Repository로 바꿀 거다.
그런데 기존의 운영 중인 코드를 하나도 손대지 않고 바꿀 수 있는 방법이 있다.
기존 MemberService나 나머지 코드에 일절 손대지 않고 바꿀 수 있다. 그런 상황을 위해 설정을 통해 스프링 빈으로 등록하는 방법을 쓴다.
@Configuration
public class SpringConfig {
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
나중에 DB를 연결하게 되면
return new MemoryMemberRepository();
이것을
return new DbMemberRepository();
이렇게 설정 코드만 바꾸면 된다. 다른 코드는 바꿀 필요 없다.
이것이 설정 파일을 직접 운영할 때의 장점이다.
컴포넌트 스캔을 사용하면 여러 코드를 바꿔야 한다.
실무에서 서버 실행 중에 중간에 바꾸는 일은 거의 없다. 그럴 일이 있으면 Config 파일을 수정하고 서버를 다시 띄운다.
주의: @Autowired를 통한 DI는 MemberController , MemberService 등과 같이 스프링이 관리하는 객체에서만 동작한다. 스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.
예를 들어 MemberService가 스프링 빈에 등록되어 있고 스프링이 관리해야 @Autowired가 적용될 수 있다.
그리고 new를 통해 MemberService 객체를 직접 생성하는 경우에도 @Autowired가 동작하지 않는다.
스프링 컨테이너에 올라가는 것들만 @Autowired가 동작한다.
회원 관리 예제 - 웹 MVC 개발
회원 웹 기능 - 홈 화면 추가
이전 강의에서 MemberController를 만들고 의존 관계를 설정했다. 이제 MemberController를 통해 회원을 등록하고 조회하는 것을 만들어 보겠다.
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
@GetMapping("/")에서의 /는 도메인 첫 번째, localhost:8080으로 들어올 때 이것이 호출된다.
return "home";은 templates의 home.html이 호출된다.
static에 index.html 만들었었다. 아무것도 없으면 웰컴 페이지인 index.html로 가는데 이번엔 안 간 이유는
우선순위 때문이다. 앞서 정적 컨텐츠에서 설명했듯이,
요청이 오면 먼저 관련 Controller가 있는지 찾고, 없으면 static 파일을 찾는다.
웰컴도 마찬가지이다.
localhost:8080 요청이 오면 먼저 Controller에서 찾는다.
매핑된 게 있으므로 바로 그 컨트롤러 호출되고 끝난다. 기존의 index.html은 무시된다. 물론 컨트롤러를 제거하면 다시 index.html이 등장한다.
회원 웹 기능 - 등록
이름을 spring 쓰고 등록 버튼 누르면
name이라는 이름의 key와 spring이라는 value가 서버로 넘어간다.
MemberForm의 name과 createMemberForm.html의 name = "name"의 "name"과 매칭이 되면서 값이 들어온다.
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
여기서 return "redirect:/";를 통해 홈 화면으로 보낸다.
@GetMapping("/members/new")
public String createForm() {
return "members/createMemberForm";
}
URL에 직접 치는 방식을 HTTP의 GET 방식이라고 한다.
/members/new가 매핑된다. 이 메서드는 따로 뭔가를 하는 게 없이 members/createMemberForm.html로 이동한다.(템플릿에서 ViewResolver를 통해 createMemberForm.html이 선택되고 타임리프 템플릿 엔진이 이것을 렌더링 한다. 지금은 타임리프가 크게 관여할 게 없다.)
그냥 이 HTML이 뿌려진다.
<!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>
여기서 form 태그는 값을 입력할 수 있는 HTML 태그이다.
action은 "/members/new"라고 되어 있고,
method는 "post"라고 되어 있다.
input에서 type이 "text"인 건 텍스트를 입력할 수 있는 입력 박스가 생긴 거다.
중요한 건 name="name"인데 서버로 넘어올 때 key가 된다.
웹 페이지에서 입력 창에 "spring"이라고 적고 등록 버튼을 누르면,
action에 적힌 "/members/new"로 post 방식으로 넘어온다. 그러면 MemberController에 적은 @PostMapping("/members/new")로 매핑된다.
기본적으로 URL 창에 엔터만 치는 건 GET 매핑이다.
POST 매핑은 보통 데이터를 폼 등에 넣어서 전달할 때 쓴다.
GET은 조회할 때 주로 쓴다.
URL은 똑같지만 GET이냐 POST냐에 따라 다르게 매핑할 수 있다.
보통 데이터를 등록할 때 POST를 쓴다. 조회할 땐 GET 쓴다.
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
System.out.println(member.getName());
return "redirect:/";
}
@PostMapping("/members/new")에 매핑되면 create 메서드가 호출된다. 그러면서 값이 들어오게 되는데, 매개 변수인 MemberForm의 name에 입력했던 "spring"이라는 값이 들어온다. HTML 코드의 input에 있는 name="name"에서의 "name"을 보고 스프링이 MemberForm의 name에 넣어 준다.(MemberForm의 setName 메서드를 통해 값이 들어간다.)
스프링이 MemberForm의 setName 메서드를 호출한다. 우리는 그것을 getName()으로 꺼낸다.
<강의 외에 내가 알아낸 내용>
<!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>
여기서 name="name"을
name="name12"로 바꾼다면
MemberForm 클래스에서 name을 전부 name12로 바꾸고,
setName 메서드를 setName12 메서드로 바꾸면 되는 듯
<지식인에서 알아낸 내용>
label 태그는 쉽게 말해 사용자의 편의성을 위해 존재한다고 생각하면 된다.
이름을 입력하기 위해서 입력 창에 마우스를 클릭하고 입력할 수도 있지만
label 부분의 이름을 클릭하면 자동으로 이름을 입력하는 공간으로 포커스가 이동한다.
물론 이 label 태그를 사용하기 위해서는 사용할 태그의 id를 입력해 주어야 한다.
즉 label for ="name"의 name은 밑의 input 태그의 id="name"을 활성화하기 위해 적은 것이다.
label을 클릭하면 id 가 name인 태그를 찾는다고 보면 된다.
또 input 태그의 name="name" 은 input 태그의 입력값을 가져오기 위한 속성 이름이라고 보면 된다.
회원 웹 기능 - 조회
<!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>
${members}는 Model 안에 있는 값을 꺼낸다
@GetMapping("members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
여기서 model에다 key가 "members"이고 이 안엔 리스트로 모든 회원을 다 조회해서 담아 둔다.
<tbody>
<tr th:each="member : ${members}">
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
</tr>
</tbody>
이를 루프를 돌린다.(th:each) 타임리프 문법이다.
루프를 돌면서
<td th:text="${member.id}"></td>
<td th:text="${member.name}"></td>
로직을 실행한다. 자바의 향상된 for문처럼.
그런데 id나 name은 private이라 접근이 원래 안 된다.
그래서 자바 프로퍼티 방식 접근이라고 하는데, getter, setter(getId와 getName) 메서드에 접근해서 값을 가져와서 출력한다.
메모리에 있기 때문에 서버를 껐다가 다시 켜면 데이터가 다 지워진다. 메모리 안에 있기 때문에 자바를 내리면 당연히 데이터가 사라진다. 실무에서 이렇게 하면 안 된다.
그래서 데이터들을 파일이나 데이터베이스에 저장해야 한다.
참고: HTTP, HTML form등 웹 MVC와 관련된 자세한 내용은 스프링 웹 MVC 강의에서 다룰 예정이다.
스프링 DB 접근 기술
H2 데이터베이스 설치
스프링 데이터 엑세스
H2 데이터베이스 설치
순수 Jdbc
스프링 통합 테스트
스프링 JdbcTemplate
JPA
스프링 데이터 JPA
우선 심플하게 H2 데이터베이스부터 설치해 보겠다.
그다음 시간엔 DB가 설치되어 있으니깐, 데이터베이스 SQL을 가지고 애플리케이션 서버와 DB를 연결할 것이다. 연결할 때 필요한 기술이 Jdbc란 기술이다. 20년 전쯤엔 어떤 식으로 개발했는지를 순수한 Jdbc로 경험할 것이다.
순수한 Jdbc로 개발하기 어려우니, 스프링이 중복을 다 제거해서 스프링이 제공하는 JdbcTemplate을 가지고 애플리케이션에서 DB로 SQL을 편리하게 날릴 수 있다.
이것보다 더 혁신적인 방법이 JPA이다. SQL조차 개발자들이 직접 짜는 게 아니라, SQL을 아예 JPA라는 기술이 그냥 DB에 등록, 수정, 삭제 등 쿼리를 직접 만들어서 다 날려 준다. JPA라는 기술을 쓰면 객체를 바로 DB에 쿼리 없이 저장할 수 있다.
JPA도 스프링만큼 오래된 기술이다. 스프링 데이터 JPA라는 기술이 있는데, JPA를 굉장히 편리하게 쓸 수 있도록 한번 감싼 기술이다.
지금까지 사용했던 단순한 회원 객체(id와 name만 있는)를 이 기술들을 적용해 보며 바꿔 볼 것이다.
MemoryRepository가 JdbcRepository, JPA Repository 이런 식으로 바뀔 것이다.
H2 데이터베이스는 교육용으로 좋다.
최초엔 DB 파일을 만들어야 한다.
jdbc:h2:~/test
이건 파일 경로를 말해 준다. home에 있는 test라는 파일을 말한다.
파일로 접근하게 되면 동시에 애플리케이션이랑 웹 콘솔이랑 같이 접근이 안 될 수 있다.(파일 충돌 오류)
JDBC URL을
jdbc:h2:tcp://localhost/~/test
이렇게 바꾸면 파일을 직접 접근하는 게 아니라 소켓을 통해 접근한다. 이렇게 해야 여러 군데에서 접근할 수 있다.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
자바에선 long인데 DB에선 bigint이다.
generated by default as identity는 값을 세팅하지 않고 insert하면 DB가 들어왔을 때 자동으로 id 값을 채워 준다.
insert into member(name) values('spring');
"spring" 이렇게 큰따옴표 쓰면 오류인 듯
MySQL에선 큰따옴표 써도 오류 안 나는 듯하다.
MemoryMemberRepository에서의
member.setId(++sequence);도 generated by default as identity와 비슷하다.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
이건 스프링 프로젝트에서 관리하면 Git에서도 같이 관리되어서 편리하다.
cmd 끄고 select 조회하면 조회 안 된다. 실행되는 cmd는 켜 놔야 한다.
이번 시간엔 웹 콘솔로 들어갔다.
다음 시간엔 우리가 만든 애플리케이션에서 DB에 접근해서 데이터를 넣고 빼고 해 보겠다.
순수 JDBC
애플리케이션에서 DB에 연동해서 저장하는 게 기존처럼 메모리에 저장하는 게 아니라, DB에 insert 쿼리를 날리고 select 쿼리를 날려서 DB에 넣고 빼는 것을 한번 해 보겠다.
가장 오래된 20년 전 방식으로 해 보겠다.
이번 시간은 편하게 듣고 필요할 때 찾아 보면 된다.
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
자바는 기본적으로 DB랑 연동하려면 Jdbc 드라이버가 꼭 필요하다.
runtimeOnly 'com.h2database:h2' 이것은 DB랑 붙을 때 DB가 제공하는 클라이언트가 필요한데 그것이 이것이다.
접속 정보 등을 넣어야 하는데 옛날엔 개발자가 직접 설정했다면, 요즘엔 스프링 부트가 다 해 준다. 경로만 넣어 주면 된다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
이걸 넣으면 된다.
h2 DB에 접근할 것이기 때문에 h2.Driver를 넣어야 한다.
원래는 여기에 아이디, 비밀번호도 적는데 h2 DB는 생략해도 된다. 이제 DB에 접근하기 위한 준비는 끝났다.
주의!: 스프링부트 2.4부터는 spring.datasource.username=sa 를 꼭 추가해 주어야 한다. 그렇지 않으면 Wrong user name or password 오류가 발생한다. 참고로 다음과 같이 마지막에 공백이 들어가면 같은 오류가 발생한다. 'spring.datasource.username=sa ' 공백 주의, 공백은 모두 제거해야 한다.
참고: 인텔리J 커뮤니티(무료) 버전의 경우 application.properties 파일의 왼쪽이 다음 그림 같이 회색으로 나온다. 엔터프라이즈(유료) 버전에서 제공하는 스프링의 소스 코드를 연결해 주는 편의 기능이 빠진 것인데, 실제 동작하는 데는 아무런 문제가 없다.
이렇게 하면 스프링이 DB랑 접근하기 위한 작업을 다 한다.
기존엔 MemoryMemberRepository를 만들었는데
일단 인터페이스를 만들었으니 인터페이스의 구현체를 만들면 된다.
JdbcMemberRepository를 만들겠다.
회원을 저장한다는 역할은 MemberRepository가 하지만
구현을 메모리에 할지, DB랑 연동해서 Jdbc로 할 건지의 차이가 있다.
JdbcMemberRepository 클래스는 복사 + 붙여넣기로 대체
DB에 붙으려면 DataSource가 필요하다.
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
DataSource를 스프링으로부터 주입받아야 한다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
이렇게 세팅했으니 스프링 부트가 DataSource를 만들어 놓고 주입한다.
dataSource.getConnection()을 통해 DB와 연결된 소켓(?)을 얻을 수 있다. 여기에 sql을 날려서 DB에 전달한다.
DB에 실제 쿼리가
pstmt.executeUpdate();
이때 날아간다.
finally {
close(conn, pstmt, rs);
}
자원을 꼭 반환시켜야 한다. 안 그러면 DB 커넥션이 쌓이면서 장애가 생길 수 있다.
try with resource로 하는 게 더 좋다.
데이터 조회의 경우엔 executeUpdate()가 아니라
rs = pstmt.executeQuery();
(참고로만 알기)
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
DataSourceUtils를 통해 커넥션을 획득해야 한다. DB 커넥션을 똑같은 걸 유지시켜 준다.
닫을 때도 DataSourceUtils를 통해 릴리즈 시켜야 한다.
스프링 쓸 땐 이런 식으로 해야 한다.
나중에 DB 강의에서 더 깊게 하는 듯
우리는 그동안 MemoryMemberRepository를 사용했었다.
SpringConfig를 통해 스프링 컨테이너에 올리고 조립했다고 배웠다.
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
MemoryMemberRepository를 스프링 빈으로 등록하고 있었다. 이것을
return new JdbcMemberRepository(dataSource);로 바꾼다.
이것의 매개값으로 DataSource가 필요하다. 이것은 스프링이 제공한다. 어떻게 하냐면
private DataSource dataSource;
@Autowired
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
이런 식으로 한다.
@Configuration 한 것도 스프링 빈으로 관리한다.
스프링 부트가 application.properties를 보고 스프링이 자체적으로 빈도 생성해 준다. DataSource를 만든다.(DB와 연결할 수 있는, 그런 정보가 있는)
그리고 생성자를 통해 주입해 준다.(DI)
DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어둔다. 그래서 DI를 받을 수 있다.
다른 어떤 코드도 변경하지 않고 오직 JdbcMemberRepository라는 클래스를 만들어 인터페이스를 구현체를 만들어 확장했고, SpringConfig만 손 댔다.
스프링을 쓰는 이유가
@Bean
public MemberRepository memberRepository() {
//return new MemoryMemberRepository();
return new JdbcMemberRepository(dataSource);
}
이런 것 때문이다. 객체지향적인 설계가 좋은 이유가 다형성을 활용할 수 있기 때문이다. 인터페이스를 두고, 구현체를 바꿔 끼울 수 있다. 스프링은 이것을 굉장히 편리하게 되도록 스프링 컨테이너가 지원해 준다. 그리고 DI 덕분에 그것을 편리하게 해 준다.
과거엔 MemberService, OrderService 등 여러 개들을 다 고쳐야 한다.
지금은 기존의 코드는 손 대지 않고 오직 애플리케이션을 설정(조립: assembly)하는 코드만 손 대면 나머지 실제 애플리케이션과 관련된 코드는 손 댈 게 없다.

MemberService는 MemberRepository를 의존하고 있다.
MemberRepository는 구현체로 MemoryMemberRepository와 JdbcMemberRepository가 있다.
그런데

기존엔 MemoryMemberRepository를 스프링 빈으로 등록했다면, 이젠 이것을 빼고 JdbcMemberRepository를 등록하고 나머지는 손 대지 않았다.
이것을 개방-폐쇄 원칙이라고 한다. SOLID 원칙 중에서도 중요한 편이다.
개방-폐쇄 원칙(OCP, Open-Closed Principle)
확장에는 열려 있고, 수정(변경)에는 닫혀있다.
스프링의 DI(Dependencies Injection)를 사용하면 기존 코드를 전혀 손 대지 않고, 설정만으로 구현 클래스를 변경할 수 있다.
회원을 등록하고 DB에 결과가 잘 입력되는지 확인하자.
데이터를 DB에 저장하므로 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
객체지향에서 말하는 다형성을 잘 활용하면 기능을 변경해도 애플리케이션 전체를 수정할 필요가 없다.(물론 조립하는 코드는 수정해야 한다.) 애플리케이션 동작하는 데 필요한 코드는 하나도 변경하지 않을 수 있다.
이 코드는 개방-폐쇄 원칙이 지켜진 것이다.
나는 빈으로 등록해 줬고, 스프링은 인젝션해 주었다.
스프링 통합 테스트
방금 만든 Repository는 DB까지 연결되니, 테스트도 스프링도 올리고 DB까지 연결해서 동작하는 통합 테스트를 해 보겠다.
스프링 컨테이너와 DB까지 연결한 통합 테스트를 하겠다.
이전에 했었던 테스트들은 스프링과 관련 없이 순수하게 자바 코드를 가지고 테스트한 것이었다.
그러나 지금은 순수한 자바 코드로 테스트할 수 없다. 왜냐하면 DB 커넥션 정보도 스프링 부트가 들고 있다.
지금부터는 테스트를 스프링과 엮어서 해 보겠다.
이번 강의는 편하게 들어도 된다.
이전의 MemberServiceTest는 자바 JVM 안에서 끝난다.
이제는 스프링 컨테이너한테 MemberService, MemberRepository를 달라고 해야 한다. 생성자 주입 방식의 DI를 써도 되지만 테스트는 끝단에 있기 때문에 편하게 필드 주입을 써도 된다.
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
MemoryMemberRepository를 직접 하지 않고, MemberRepository로 선언한다. 그러면 구현체는 SpringConfig에서 한 대로 올라올 것이다.
@AfterEach나 @BeforeEach도 필요 없다.
메모리 DB에 있는 데이터를 다음 테스트에 영향 없도록 지워주려고 했던 건데 이제는 @Transactional 때문에 필요 없다.
DB에 "spring"이 있는 상태에서 "spring" 이름으로 회원 가입 테스트를 하면 @Transactional을 하더라도 중복되는 이름 메시지 뜬다. 그러므로 DB의 데이터를 지우고 다시 실행해 본다.
실무에선 테스트 전용 DB를 따로 구축하거나 로컬 PC에서 테스트한다.
메모리를 가지고 한 테스트랑 다르게 이번 테스트를 하면 스프링이 올라온다. SpringConfig도 다 올라온다. 테스트가 끝나면 스프링이 내려간다.
@Transactional 없이 테스트하면 실제 DB에도 반영된다.
하지만 테스트는 반복할 수 있어야 한다. @Transactional을 하지 않으면 실제 DB에 반영되기 때문에 테스트를 한 번 더 하면 중복으로 인해 오류가 날 수 있다.
물론 @Transactional을 하지 않고 @AfterEach, @BeforeEach를 해도 가능은 하지만 귀찮다.
DB는 기본적으로 트랜잭션이라는 개념이 있다. DB에 데이터를 insert 쿼리한 다음에 commit을 해야 DB에 반영이 된다. 혹은 오토 커밋 모드로 commit을 한다. commit을 자동으로 하냐 안 하냐의 차이이지 무조건 commit은 들어간다.
insert 후에 commit하기 전까진 DB에 반영이 안 된다.
테스트 후에 롤백하면? DB에서 데이터가 없어져서 반영되지 않는다.
그렇게 하는 방법이 @Transactional을 테스트 케이스에 다는 것이다. 그러면 테스트를 실행할 때, 테스트를 하기 전에 트랜잭션을 먼저 실행하고, DB에 insert 등 쿼리를 해서(join()도 하고 findOne()도 한다.) 데이터를 다 넣은 다음에, 테스트가 끝나면 데이터를 롤백한다. DB에 넣었던 데이터가 반영되지 않는다.
즉, 다음 테스트를 반복할 수 있다.
@SpringBootTest: 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional: 테스트 케이스에 이 애노테이션이 있으면, 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 이렇게 하면 DB에 데이터가 남지 않으므로 다음 테스트에 영향을 주지 않는다
테스트 하나하나마다 롤백한다.
트랜잭션 시작하고 테스트 하나 실행하고 끝나면 롤백하고
또 트랜잭션 시작하고 테스트 하나 실행하고 끝나면 롤백하는 식으로 테스트 메서드마다 일일이 동작한다.
@Transactional이 Service 같은 곳에 붙으면 롤백하지 않고 정상적으로 돈다. 테스트 케이스에 붙었을 때만 롤백하도록 동작한다.
@Commit을 하면 그냥 commit을 한다.
그렇다면 스프링 없이 하는 테스트는 필요 없을까?
통합 테스트가 이전의 순수 자바를 통한 테스트보다 오래 걸린다.
테스트 케이스가 많아지면 훨씬 오래 걸린다.
순수하게 자바 코드로 최소한의 단위로 테스트하는 것을 단위 테스트라고 한다.
스프링 컨테이너와 DB까지 연동해서 하는 것을 통합 테스트라고 한다.
단위 테스트가 훨씬 좋은 테스트일 확률이 높다.(무조건은 아니다.)
스프링 컨테이너 없이 할 수 있도록 훈련하는 게 좋다.
스프링 컨테이너까지 어쩔 수 없이 올려야 하는 상황이면 그 테스트는 잘못 설계되었을 확률이 높다. 물론 통합 테스트가 필요할 때도 있다.
@SpringBootTest 어노테이션을 테스트에서 사용할 경우, 스프링 애플리케이션을 실행시키는것과 마찬가지로 스프링 컨테이너가 스프링 빈을 등록한다. 그렇기 때문에 스프링이 의존성을 주입해 줄 수 있다.
스프링 JdbcTemplate
순수 Jdbc와 동일한 환경 설정을 하면 된다.
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분 제거해 준다. 하지만 SQL은 직접 작성해야 한다.
JdbcTemplate은 인젝션을 받을 수 있는 건 아니다.
DataSource가 필요하다.
생성자가 하나만 있을 때, 스프링 빈으로 등록되면 @Autowired를 생략할 수 있다.(생성자가 2개일 땐 안 된다.)
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
스프링이 DataSource를 자동으로 인젝션해 준다.
RowMapper를 만든다.
private RowMapper<Member> memberRowMapper() {
return new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
};
}
Alt + Enter로 다음과 같이 람다식으로 바꿀 수 있다.
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
findById() 메서드는 다음과 같다.
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
jdbcTemplate에서 select * from member where id = ? 쿼리를 날리고, 그 결과를 RowMapper를 통해 매핑을 하고 그것을 List로 받아서, Optional로 바꿔서 반환한다.
순수 Jdbc 때와 비교하면 훨씬 짧다.
JdbcTemplate인 이유는 여러 이유가 있을 수 있지만, 디자인 패턴 중에 템플릿 메서드 패턴이 있는데 그게 들어가 있다. 그래서 코드가 짧아졌다.
save()는 길어서 복사 붙여넣기로 대체
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new
MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
SimpleJdbcInsert는 매개값으로 jdbcTemplate를 넘겨서 만든다.
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
.
.
이렇게 하면 쿼리를 짤 필요가 없다. 테이블 명이랑 PK랑 name 있으면 Insert 쿼리 만들 수 있다.
그리고
Number key = jdbcInsert.executeAndReturnKey(new
MapSqlParameterSource(parameters));
에서 key를 받고
member.setId(key.longValue());
return member;
이렇게 member에 넣어 준다.
document 보고 쓰면 된다.
그냥 이렇게 하는 구나 정도로 생각하면 된다.
이 강의에선 감을 잡는 정도로만 보면 된다.
RowMapper는 콜백을 이용하는 듯
사용법은 JdbcTemplate 메뉴얼 찾아 보면 된다. 혹은 나중에 DB 접근 기술 강의에서 배울 수도 있다.
SpringConfig에서도 수정한다.
@Bean
public MemberRepository memberRepository() {
//return new MemoryMemberRepository();
//return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
스프링 통합 테스트를 만들어 놨기 때문에 웹을 띄어서 직접 확인할 필요 없다. 통합 테스트를 해 보면 된다.
테스트 코드 잘 짜는 게 중요하다. 실무에서도 테스트 코드를 프로덕션 코드보다 더 오래 짜기도 한다.
작은 버그가 수억 원 손실을 일으킬 수 있다.
개발 잘하는 사람들은 테스트도 잘 만든다.
다음 시간에 배울 JPA는 쿼리도 없앨 수 있다.
JPA
Jdbc에서 JdbcTemplate로 바꾸니 반복적인 코드가 많이 줄었다. 그렇지만 여전히 SQL은 개발자가 직접 작성해야 한다.
그런데 JPA 기술을 사용하면 SQL 쿼리도 JPA가 자동으로 처리한다. 그렇게 해서 개발 생산성을 크게 높일 수 있다.
마치 객체를 MemoryMemberRepository에 넣듯이, JPA에 넣으면 JPA가 중간에서 DB에 SQL 날리고, DB를 통해 데이터 가져온다.
단순히 SQL을 만들어 주는 걸 넘어서서, JPA를 사용하면 SQL보다 객체 중심으로 고민할 수 있다.
JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해 준다.
JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
JPA도 스프링만큼 넓고 깊은 기술이다. 스프링에서도 JPA를 많이 지원한다.
JPA를 쓰려면 build.gradle에
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
를 추가해야 한다. 이 안에 Jdbc도 포함하기 때문에
기존의
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
는 지워도 된다.
spring-boot-starter-data-jpa는 내부에 jdbc 관련 라이브러리를 포함한다. 따라서 jdbc는 제거해도 된다.
application.properties에도 아래 코드 추가한다.
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.show-sql=true
이렇게 하면 JPA가 날리는 SQL 볼 수 있다.
spring.jpa.hibernate.ddl-auto=none
JPA를 사용하면 회원 객체를 보고 알아서 테이블도 만들 수 있다. 하지만 우리는 이미 테이블 만들었고 만든 테이블을 쓸 것이기 때문에 자동으로 테이블 생성해 주는 기능은 끌 것이다.
none을 create로 바꾸면 테이블까지 자동으로 만든다.
주의!: 스프링부트 2.4부터는 spring.datasource.username=sa를 꼭 추가해 주어야 한다. 그렇지 않으면 오류가 발생한다.
show-sql: JPA가 생성하는 SQL을 출력한다.
ddl-auto: JPA는 테이블을 자동으로 생성하는 기능을 제공하는데 none을 사용하면 해당 기능을 끈다. create를 사용하면 엔티티 정보를 바탕으로 테이블도 직접 생성해 준다.
JPA 쓰려면 Entity라는 걸 매핑해야 한다.
JPA라는 건 인터페이스만 제공되는 거고, 구현체로 hibernate 같은 기술들이 여러 개 있는데
우리는 JPA 인터페이스에 hibernate만 쓴다.
JPA는 자바 진영의 표준 인터페이스이고, 구현은 여러 업체들이 한다. 업체마다 성능이 더 좋거나 쓰기 편하거나 한 것들이 있다.
JPA는 객체랑 ORM이라는 기술이다.
Object 객체와
Relational 관계형 데이터베이스 테이블을
Mapping 매핑한다.
매핑은 애노테이션으로 한다.
package hello.hellospring.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
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;
}
}
@Entity를 붙이면 JPA가 관리하는 Entity가 된다.
PK를 매핑하기 위해 @Id를 한다.
우리는 쿼리에 id를 넣는 게 아니라 DB가 id를 자동으로 생성시켜 줬었다. 이런 걸 identity 전략(?)이라 한다.
@Generatedvalue(strategy = GenerationType.IDENTITY)
오라클에선 시퀀스 쓰기도 하고, 직접 넣어줄 수도 있고 여러 가지가 있다(?). DB가 알아서 생성해 주는 건 IDENTITY라고 한다.
name은 DB에서도 name이라 그대로 하면 된다.
만약 DB에서의 컬럼명이 만약 username이라면
@Column(name = "username")
private String name;
이렇게 하면 된다. 그러면 DB의 username과 매핑된다.
이제 이 정보들을 가지고 insert, update, delete, select 쿼리 다 만들 수 있다. JPA가 이렇게 동작한다.
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
.
.
JPA는 EntityManager라는 걸로 모든 게 동작한다.
build.gradle에서
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
이렇게 data-jpa 라이브러리 받았었다.
이렇게 하면 스프링 부트가 자동으로 EntitiyManager라는 걸 생성해 준다. 현재 DB랑 다 연결하고 EntityManager를 만들어 준다. 우리는 이 만들어진 것을 인젝션 받으면 된다.
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
이렇게 세팅해 둔 정보나 DB 커넥션 정보랑 다 자동으로 합쳐서 스프링 부트가 EntityManager라는 걸 만들어 준다.
내부적으로 DataSource를 가지고 있어서 DB와의 통신을 다 내부에서 처리한다.
아무튼 JPA를 쓰려면 EntityManager를 주입받아야 한다.
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
이렇게 하면 JPA가 insert 쿼리 다 만들어서 DB에 넣고, setId까지 다 해 준다.
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
조회할 타입으로 Member.class랑 PK인 id 넣어 주면 조회된다.
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
Ctrl + Alt + Shift + T: 리팩터링 관련 전체 항목을 조회한다.
"select m from Member m" 이게 JPQL이라는 쿼리 언어이다.
우리는 보통 테이블 대상으로 SQL을 날리는데, 그게 아니라 객체를 대상으로 쿼리를 날린다. 그럼 이게 SQL로 번역된다.
정확히는 Member 엔터티를 대상으로 쿼리를 날린다.
select의 대상은 기존 SQL의 경우 *이거나 m.id 같은 건데
여기선 Member 엔터티 자체를 select한다.(객체 자체를)
저장하고, 조회하고, 업데이트하고 삭제하는 건 SQL을 짤 필요 없다. 다 자동으로 된다.
그런데 findByName() 같은 거나 findAll()처럼 여러 개 리스트 가지고 돌릴 때는 즉, PK 기반이 아닌 나머지들은 JPQL을 작성해야 한다.
다음 시간에 배울 스프링 데이터 JPA를 위해 라이브러리를 data-jpa를 받은 건데, 스프링 데이터 JPA를 사용하면 findByName()이나 findAll()도 JPQL 안 짜도 된다.
@Override
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
JPA 쓰려면 항상 @Transactional 있어야 한다.
서비스 계층에 @Transactional 입력한다.
@Transactional
public class MemberService {
데이터를 저장하거나 변경할 땐 항상 @Transactional 있어야 한다.
이렇게 클래스 위에 써도 되지만, 지금 같은 경우 회원 가입 할 때만 필요하니 join() 메서드 위에 써도 된다.
JPA는 join() 들어올 때 모든 데이터 변경이 디 트랜잭션 안에서 실행되어야 한다.
org.springframework.transaction.annotation.Transactional을 사용하자.
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을 커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
SpringConfig도 수정한다.
@Configuration
public class SpringConfig {
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
// private DataSource dataSource;
//
// @Autowired
// public SpringConfig(DataSource dataSource) {
// this.dataSource = dataSource;
// }
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
//return new MemoryMemberRepository();
//return new JdbcMemberRepository(dataSource);
//return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
결국 DB엔 SQL이 나가야 한다.
통합 테스트(@Transactional을 통해 롤백하도록 했던)로 테스트할 때 @Commit 쓰면 DB에 반영되는 거 확인할 수 있다.
참고: JPA도 스프링만큼 성숙한 기술이고, 학습해야 할 분량도 방대하다.
스프링 데이터 JPA
스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야 할 코드도 확연히 줄어듭니다. 여기에 스프링 데이터 JPA를 사용하면, 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다.(디테일하게 들어가면 좀 다르다.) 그리고 반복 개발해 온 기본 CRUD 기능도 스프링 데이터 JPA가 모두 제공합니다.(내가 적을 코드가 많이 없어진다.) 스프링 부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 환상적인 프레임워크를 더하면 개발이 정말 즐거워집니다. 지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어듭니다. 따라서 개발자는 핵심 비즈니스 로직을 개발하는 데 집중할 수 있습니다. 실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 이제 선택이 아니라 필수입니다.
주의: 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술입니다. 따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 합니다.
앞의 JPA 설정을 그대로 사용한다.
인터페이스를 다음과 같이 만든다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
JpaRepository라는 걸 받아야 한다.
JpaRepositiory<Member, Long>
Member의 id가 Long 타입이어서 Long이다.
인터페이스는 다중 상속이 된다.
이 인터페이스 만들면 따로 구현 클래스 만들 필요 없다.
스프링 데이터 JPA가 JpaRepository 받고 있는 SpringDataJpaMemberRepository의 구현체를 자동으로 만들어 준다. 스프링 빈에 자동으로 등록해 준다. 내가 스프링 빈에 등록하는 게 아니라, 스프링 데이터 JPA가 자동으로 만들어 준다.
우리는 그것을 가져다 쓰면 된다.
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
그냥 이렇게 인젝션 받으면 된다. 그러면 스프링 데이터 JPA가 만든 구현체가 등록된다.
그리고 MemberService에 의존 관계를 세팅해야 한다.(코드에 나와 있듯이)
생성자가 하나뿐이므로 @Autowired는 생략해도 된다.
스프링 컨테이너에서 MemberRepository를 찾는다. 그런데 내가 등록한 게 없다. 그런데 하나가 있다. JpaRepository를 받는 SpringDataJpaMemberRepository 인터페이스를 만들어 놓으면 스프링 데이터 JPA가 인터페이스에 대한 구현체를 자기가 만든다. 그리고 스프링 빈에 등록한다. 그래서 우리가 인젝션을 받을 수 있다.
통합 테스트 해 보면 잘 된다.
출력 결과를 보면 이전처럼 hibernate 뜬다. 스프링 데이터 JPA가 JPA 기술을 가져다 쓰는 거다.
<스프링 데이터 JPA 제공 클래스>

기본적인 save() 같은 메서드를 적지 않은 이유는 위 그림에 다 있기 때문이다.(옛날 그림이라 차이 있을 수 있다.)
JpaRepository를 보면 findAll() 같은 기본 메서드들이 있다.
PagingAndSortingRepository에도 있다. 그 상위인 CrudRepositiory에는 save()나 findById()도 있다.
우리가 생각할 수 있는 공통적인 기능들은 이미 만들어져 있다. 그걸 가져다 쓰면 된다.
이 예제의 save() 등을 처음 만들 때 이걸 고려해서 만든 것이다.
스프링 데이터 JPA 제공 기능
인터페이스를 통한 기본적인 CRUD
findByName() , findByEmail()처럼 메서드 이름만으로 조회 기능 제공
페이징 기능 자동 제공
다만 비즈니스가 다 다르기 때문에 공통화하기 어려운 것들도 있다. 그런 건 공통 클래스로 제공할 수 없다.
내 프로젝트에선 name이지만 다른 프로젝트에선 userName일 수 있다. 상품 이름으로 조회할 수도 있는데 그런 걸 일일이 공통화할 수 없다.
그래서 SpringDataJpaMemberRepository에서
@Override
Optional<Member> findByName(String name);
이런 식으로 findByName이라고 하면 규칙에 따라 "select m from Member m where m.name = ?" 이런 식으로 JPQL 쿼리를 짜고 이게 SQL로 번역되어서 실행된다.
findByNameAndId(String name, Long id) 등등 되게 많다.
이런 메서드 이름이나 반환 타입이나 매개 변수 등을 리플렉션 기술로 읽어서 풀어낸다.
이런 걸 가지고 인터페이스 이름만으로도 개발이 가능하다. 이런 거로 할 수 없는 복잡한 쿼리도 있지만 대부분의 경우는 단순하고 인터페이스만으로 끝난다.
참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는 라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate을 사용하면 된다.
실무에서 JPA를 딥하게 하는 사람들은 JPA, 스프링 데이터 JPA, Querydsl들을 다 조합해서 쓴다. 이 조합으로 해결하기 어려운 건 순수한 SQL로 해결하면 된다.(혹은 JdbcTemplate) JPA나 MyBatis를 섞기도 한다.
지금까지를 정리하면,
H2 데이터베이스를 설치해 봤고,
순수 Jdbc를 써 봤는데 쿼리 양이 많다.
이걸 가지고 통합 테스트 만들어 보고,
스프링 JdbcTemplate으로 바꿔 봤다. 이것은 반복되는 코드는 많이 줄지만 SQL은 직접 작성해야 한다.
JPA는 기본적인 CRUD를 하는 데 내가 쿼리를 작성할 필요가 없었다. 물론 select할 땐 JPQL을 작성했었다.
스프링 데이터 JPA는 내가 구현 클래스 작성할 필요도 없이 인터페이스만으로 개발이 끝났다. 그리고 findByName() 등을 쉽게 만들 수 있었다.
AOP
AOP가 필요한 상황
AOP가 C언어의 포인터처럼 처음엔 어려울 수 있다.
디테일한 부분을 처음부터 배우면 어려울 수 있다.
그러나 AOP를 언제, 왜 쓰는지 알면 어렵지는 않다.
만약 모든 메서드의 호출 시간을 측정해야 한다면?
한 가지 방법은 모든 메서드마다 일일이
long start = System.currentTimeMillis();
long finish = System.currentTimeMillis();
long timeMs = finish - start;
이런 식으로 구할 수 있다. 예를 들면
public Long join(Member member) {
long start = System.currentTimeMillis();
try {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
} finally {
long finish= System.currentTimeMillis();
long timeMs = finish - start;
System.out.println("join = " + timeMs + "ms");
}
}
템플릿 메서드 패턴 등을 쓰면 공통 메서드로 만들 수도 있지만 복잡하다.
처음 실행할 땐 시간이 더 걸린다. 클래스 메타데이터 로딩하는 등의 시간 때문에 더 걸린다.
그래서 실제 운영에선 처음에 서버 올리고, 이것저것 호출해서 웜업하기도 한다.
AOP가 필요한 상황
모든 메서드의 호출 시간을 측정하고 싶다면?
공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)
회원 가입 시간, 회원 조회 시간을 측정하고 싶다면?
회원 가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항이 아니다.
시간을 측정하는 로직은 공통 관심 사항이다.
시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지 보수가 어렵다.
시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어렵다.
시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 한다.
공통적으로 여러 메서드에 시간을 측정하고 싶어하는 건 공통 관심 사항이다.
메서드의 핵심 기능이 핵심 관심 사항이다.
AOP 적용
이전 같은 문제를 해결하는 기술을 AOP라고 한다.
AOP: 관점 지향 프로그래밍 (Aspect Oriented Programming)
공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern) 분리
이전 강의에선 다음 그림과 같이 시간 측정 로직을 메서드에 다 붙였다.

AOP를 적용하면 다음 그림과 같이 시간 측정 로직을 한곳에 다 모으고, 내가 원하는 곳에 적용한다.

시간 측정 로직을 한곳에 모아 놓고 이것을 내가 원하는 대로 MemberController에 적용하거나, MemberService에 적용하는 식으로 내가 원하는 곳에 지정하면 된다.
aop 패키지를 만들고 TimeTraceAop 클래스를 만든다.
클래스 이름 위에 @Aspect를 적어야 AOP로 쓸 수 있다.
매뉴얼 보고 그대로 따라하면 된다.
@Aspect
@Component
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");
}
}
}
joinPoint.proceed()라고 하면 다음 메서드로 진행된다.
Object result = joinPoint.proceed();
return result;
이것을 Ctrl + Alt + N으로 인라인으로 합칠 수 있다.
return joinPoint.proceed();
TimeTraceAop를 스프링 빈으로 등록해야 한다.
TimeTraceAop 위에 @Component를 해도 된다.
하지만 SpringConfig에 스프링 빈으로 등록해서 쓰는 게 더 선호된다. Service, Repository 이런 건 정형화되어 있는데 AOP는 정형화되어 있다기보단 AOP 쓴다는 걸 인지할 수 있도록 SpringConfig에 등록하는 게 낫다.
@Bean
public TimeTraceAop timeTraceAop() {
return new TimeTraceAop();
}
이 강의에선 그냥 컴포넌트 스캔 방식을 쓰겠다.
@Around()를 통해 시간 측정 로직을 어디에 적용할지 타겟팅해 줄 수 있다.
매뉴얼 보고 하면 된다. 실무에서 쓰이는 건 한정되어 있다.
@Around("execution(* hello.hellospring..*(..))")
패키지 명, 클래스 명, 파라미터 타입 등..
이 경우엔 hello.hellospring 패키지 하위에 다 적용한다는 뜻이다.
서버 띄운 후, 회원 목록 들어가면 인텔리제이에 다음 사진처럼 출력된다.

이러면 어디서 병목이 있는지 찾을 수 있다.
@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이다.
해결
회원 가입, 회원 조회 등 핵심 관심 사항과 시간을 측정하는 공통 관심 사항을 분리한다.
시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.(앞으로 변경 사항이 있으면 TimeTraceAop만 바꾸면 된다.)
핵심 관심 사항을 깔끔하게 유지할 수 있다.
변경이 필요하면 이 로직만 변경하면 된다.
원하는 적용 대상을 선택할 수 있다.
보통은 패키지 레벨로 한다.
@Around("execution(* hello.hellospring.service..*(..))")
이렇게 하면 Service 하위에 있는 것들만 된다.

AOP 동작은 여러 가지 방법이 있다.

AOP 적용 전엔 Controller에서 Service 호출할 때 그냥 의존 관계 가지고 호출했다.
MemberController가 MemberService를 의존하고 있고, MemberController에서 메서드 호출하면 MemberService에서도 메서드 호출한다.

AOP가 있으면 가짜 MemberService를 만든다. 프록시라고 한다.
스프링이 올라오고 컨테이너에 스프링 빈 등록할 때, 진짜 스프링 빈 말고 가짜 스프링 빈을 세워 둔다. 가짜 스프링 빈이 끝나면 joinPoint.proceed() 하면 그때 진짜 MemberService 호출한다.
즉, MemberController가 호출하는 건 진짜 MemberService가 아니라 프록시라는 기술로 발생하는 가짜 MemberService이다.
프록시는 나중에 핵심 강의에서 깊은 설명을 할 것 같다.
프록시라는 걸 확인할 수 있다.
MemberController 클래스의 생성자를 다음과 같이 한다.
@Autowired
public MemberController(MemberService memberService) {
this.memberService = memberService;
System.out.println("memberService = " + memberService.getClass());
}
그러면 인텔리제이에 다음과 같이 출력된다.
memberService = class hello.hellospring.service.MemberService$$EnhancerBySpringCGLIB$$fe71f313
MemberService로 끝나는 게 아니라 뒤에 CGLIB 등 여러 용어 나오는데
이건 MemberService를 가지고 복제해서 코드를 조작하는 기술이다.
스프링 컨테이너는 AOP가 적용되면 MemberService에 AOP 적용된 거 확인하면 프록시(가짜)가 나온다. 이 프록시를 통해 AOP가 다 실행되고, joinPoint.proceed()하면 진짜 MemberService가 호출된다.
AOP 적용 전 전체 그림

AOP 적용 후 전체 그림(타겟을 다 설정했을 때)

AOP 기술 중엔 이렇게 스프링 컨테이너에서 스프링 빈을 관리하면 가짜를 만들어서 그것을 DI 해 주면 된다. 이것도 DI의 장점이다. 이런 것들을 할 수 있게 만드는 기반이 된다.
DI를 안 하고 내가 직접 MemberController에서 MemberService를 넣으면 이런 기술이 불가능하다.
DI를 해 주면 MemberController에서 뭔지는 몰라도 그냥 받아서 쓰는데 프록시가 들어온다.
이런 방식을 프록시 방식의 AOP라고 한다.
자바 컴파일 타임에 코드를 아예 생성해서 자바 코드를 위아래로 넣어 주는 기술들도 있다.
다음으로
다음으로
H2는 실무에서 로컬 DB로 쓰일 수 있다.
운영은 MySQL 계열 DB를 많이 쓴다. AWS 오로라 DB 등
지금까지 스프링으로 웹 애플리케이션을 개발하는 방법에 대해서 얇고 넓게 학습했다. 이제부터는 각각의 기술들을 깊이 있게 이해해야 한다. 거대한 스프링의 모든 것을 세세하게 알 필요는 없다. 우리는 스프링을 만드는 개발자가 아니다. 스프링을 활용해서 실무에서 발생하는 문제들을 잘 해결하는 것이 훨씬 중요하다. 따라서 핵심 원리를 이해하고, 문제가 발생했을 때, 대략 어디쯤부터 찾아 들어가면 될지, 필요한 부분을 찾아서 사용할 수 있는 능력이 더 중요하다.
스프링 완전 정복 시리즈(준비중)
스프링을 완전히 마스터할 수 있는 다음 시리즈를 준비 중이다. 실제 실무에서 사용하는 핵심 스프링 기능 위주로 설명하고 실무에서 사용하지 않거나 오래된 기능은 과감하게 삭제했다. 그리고 실무 노하우를 전수한다.
웹 MVC에서 장애가 생기면 오류 정도이지만
DB와 관련돼서 오류가 나면 돈과 직결되는 문제이다.
스프링 부트와 JPA 실무 완전 정복 로드맵
최신 실무 기술로 웹 애플리케이션을 만들어 보면서 학습하고 싶으면 스프링 부트와 JPA 실무 완전 정복 로드맵을 추천한다.