Framework : 스프링부트 2.6.11
templete, View : 타임리프
환경 : gradel
DB : 로컬 & H2 1.4.200
JDK : 11
: Spring Initializr 으로 쉽게 설정할 수 있다.
(2.7.2로 진행하다가 2.7 버전은 인텔리제이에서 제대로 안 된다고 해서 2.6.10으로 진행했다.)
파일을 다운 받고, 압축을 푼 뒤 인텔리제이로 해당 폴더를 오픈한다.
: 인텔리제이 환경설정을 확인한다. 프로젝트 JDK를 11버전으로 gradle의 run, testing을 인텔리제이 idea로 설정. JVM을 11버전으로 설정한다.
HelloController.java
를 run하면 바로 톰캣서버로 올리기 가능.
// path : resources/static/index.html
<!DOCTYPE HTML>
<html>
<head>
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
</html>
// path : java/com.led.hellospring.controller/HelloController.java
package com.led.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("hello")
public String hello(Model model) {
model.addAttribute("data", "hello!!");
return "hello";
}
}
// path : resources/templates/hello.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Hello</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p>
</body>
</html>
./gradlew build
cd build/libs
java -jar hello-spring-0.0.1-SNAPSHOT.jar
→ 실행됨!```bash
// build.gradle에 의존성 추가.
dependencies {
developmentOnly 'org.springframework.boot:spring-boot-devtools'
}
```
- 자동적으로 컴파일하거나, 수동으로 build > recompile 하면 완료.
// java/com.led.hellospring.controller/HelloController.java
@GetMapping("hello-mvc") // 1. MVC 방식
public String helloMvc(@RequestParam("name") String name, Model model) {
model.addAttribute("name", name);
return "hello-template";
}
// resources/template/hello-template.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
<p th:text="'hello ' + ${name}">hello! empty</p>
</body>
</html>
http://localhost:8080/hello-mvc?name=value
ResponseBody 문자 반환 : 문자 그대로를 날려준다.
@GetMapping("hello-string") // 2. API 방식 (문자 냅다 반환)
@ResponseBody
public String helloString(@RequestParam("name") String name) {
return "hello " + name;
}
실행 : http://localhost:8080/hello-string?name=value
ResponseBody 객체 반환 : json형식으로 객체를 날려준다.
@GetMapping("hello-api") // 2. API 방식 (객체 반환)
@ResponseBody
public Hello helloApi(@RequestParam("name") String name) {
Hello hello = new Hello();
hello.setName(name);
return hello;
}
static class Hello {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
http://localhost:8080/hello-api?name=spring
데이터: 회원ID, 이름
기능: 회원 등록, 조회
아직 데이터 저장소가 선정되지 않음(가상의 시나리오)
-> 인터페이스로 구현 클래스를 변경할 수 있도록 설계한다. 구현체로 가벼운 메모리 기반의 데이터 저장소 사용
[MemberService]->[(interface)MemberRepository]<-[MemoryMemberRepository]
package com.led.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;
}
}
save : 멤버를 세이브
findById : 해당 id를 가진 멤버를 검색
findByName : 해당 name을 가진 멤버를 검색
❕ Optional : Null이 나올 수 있는 값일 경우, Optional을 이용하여 처리할 수 있다.package com.led.hellospring.repository;
import com.led.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {
Member save(Member member);
Optional<Member> findById(Long id);
Optional<Member> findByName(String name);
List<Member> findAll();
}
리포지토리는 map 구조로 구현된다. (ID : member)
package com.led.hellospring.repository;
import com.led.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) {
return Optional.ofNullable(store.get(id));
}
@Override
public Optional<Member> findByName(String name) {
return store.values().stream()
.filter(member -> member.getName().equals(name))
.findAny();
}
@Override
public List<Member> findAll() {
return new ArrayList<>(store.values());
}
public void clearStore() {
store.clear();
}
}
package com.led.hellospring.repository;
import com.led.hellospring.domain.Member;
import org.assertj.core.api.Assertions;
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();
@AfterEach
public void afterEach() { // 테스트가 순서 의존적으로 작동하지 않도록, 각각 테스트의 끝마다 repository 변수를 비워주어야 함.
repository.clearStore();
}
@Test
public void save() {
Member member = new Member();
member.setName("snuggle");
repository.save(member);
Member result = repository.findById(member.getId()).get();
//Assertions.assertEquals(member, result);
assertThat(member).isEqualTo(result);
}
@Test
public void findByName() {
Member member1 = new Member();
member1.setName("nuggle1");
repository.save(member1);
Member member2 = new Member();
member2.setName("nuggle2");
repository.save(member2);
Member result = repository.findByName("nuggle1").get();
assertThat(result).isEqualTo(member1);
}
@Test
public void findAll() {
Member member1 = new Member();
member1.setName("nuggle1");
repository.save(member1);
Member member2 = new Member();
member2.setName("nuggle2");
repository.save(member2);
List<Member> result = repository.findAll();
assertThat(result.size()).isEqualTo(2);
}
}
join : 회원 가입 메소드. 회원 이름이 중복되지는 않았는지 확인하고, 리포지토리에 저장한다.
validateDuplicateMember : 해당 회원 이름이 리포지토리에 있는지 확인하는 메소드.
findMembers : 전체 회원 조회 메소드
findOne : 해당 멤버 id를 지니는 멤버를 찾는 메소드
package com.led.hellospring.service;
import com.led.hellospring.domain.Member;
import com.led.hellospring.repository.MemberRepository;
import com.led.hellospring.repository.MemoryMemberRepository;
import java.util.List;
import java.util.Optional;
public class MemberService {
private final MemberRepository memberRepository; // = new MemoryMemberRepository();
public MemberService(MemberRepository memberRepository) { // test, DI (dependency injection)
this.memberRepository = memberRepository;
}
/**
* 회원 가입
*/
public Long join (Member member) {
/* 회원 이름 중복 확인 */
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
// Optional<Member> result = memberRepository.findByName(member.getName());
// result.ifPresent(m -> {
// throw new IllegalStateException("이미 존재하는 회원입니다.");
// });
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);
}
}
package com.led.hellospring.service;
import com.led.hellospring.domain.Member;
import com.led.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach() {
memberRepository = new MemoryMemberRepository(); // 리포지토리 인스턴스가 다르지 않게 (오직 하나) !
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach() { // 테스트가 순서 의존적으로 작동하지 않도록, 각각 테스트의 끝마다 repository 변수를 비워주어야 함.
memberRepository.clearStore();
}
@Test
void join() {
Member member = new Member();
member.setName("spring");
Long saveId = memberService.join(member);
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
// 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("이미 존재하는 회원입니다.");
// try {
// memberService.join(member2);
// fail();
// } catch (IllegalStateException e) {
// assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// }
}
@Test
void findMembers() {
}
@Test
void findOne() {
}
}
package com.led.hellospring.controller;
import com.led.hellospring.domain.Member;
import com.led.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를 스프링이 연결해준다. <- memberService는 굳이 여러 객체를 생성해서 연결해줄 필요없기 때문에 하나만 생성하여 연결한다
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
}
Autowired
어노테이션으로 표시된) 객체 의존관계를 스프링 컨테이너에서 찾아서 넣어준다. 스프링 컨테이너에 들어가는 객체는 (스프링 빈)어노테이션 등으로 정의해주어야 한다.<방법1> 컴포넌트 스캔 - 자동 의존관계 설정
@Service
public class MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
@Repository
public class MemoryMemberRepository implements MemberRepository {}
<방법2> 직접 스프링 빈 등록 (위 방법1에서 했던 내용은 제거!. 추후 메모리 리포지토리를 변경할 것이므로 변경에 쉽게 스프링 빈을 직접 등록하는 방식으로 한다.)
package com.led.hellospring;
import com.led.hellospring.repository.MemberRepository;
import com.led.hellospring.repository.MemoryMemberRepository;
import com.led.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();
}
}
예전에 만든 정적 파일 (Index.html)이 있지만, 정적 파일보다 컨트롤러가 적재 우선 순위가 높기 때문에 “/”에서 해당 컨트롤러가 실행된다.
package com.led.hellospring.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/")
public String home() {
return "home";
}
}
// resource/templetes/home.html
<!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>
public class MemberController {
...
@GetMapping("/members/new")
public String createForm() {
return "members/createMemberForm";
}
}
// resources/templetes/members/createMemberForm.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>
package com.led.hellospring.controller;
public class MemberForm {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class MemberController {
...
@PostMapping("/members/new")
public String create(MemberForm form) {
Member member = new Member();
member.setName(form.getName());
memberService.join(member);
return "redirect:/";
}
}
public class MemberController {
...
@GetMapping("/members")
public String list(Model model) {
List<Member> members = memberService.findMembers();
model.addAttribute("members", members);
return "members/memberList";
}
}
타임리프 문법 : member객체를 반복해서 꺼내와 해당 id, name 속성을 출력.
// resources/templetes/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>
chmod 755 [h2.sh](http://h2.sh)
후, 쉘 실행.jdbc:h2:~/test
로 연결. 그 이후로는 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)
};
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
...
}
// resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
package com.led.hellospring.repository;
import com.led.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName()); // ? 랑 매칭이됨.
pstmt.executeUpdate(); // 쿼리 날리기
rs = pstmt.getGeneratedKeys(); // 방금 생성한 키를 꺼내줌.
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findById(Long id) { // 조회
String sql = "select * from member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public List<Member> findAll() {
String sql = "select * from member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findByName(String name) {
String sql = "select * from member where name = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if (rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
}
return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
DataSource : 데이터베이스 커넥션을 획득할 때 사용하는 객체. 스프링 부트가 해당 datasource를 스프링 빈으로 만들어둔다.
package com.led.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
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;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
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);
}
}
package com.led.hellospring.service;
import com.led.hellospring.domain.Member;
import com.led.hellospring.repository.MemberRepository;
import com.led.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional //afterEach를 대신함. 트랜잭션을 실행하고, 테스트가 끝난뒤에 롤백해줌 -> 테스트 했던 데이터가 DB에 올라가지 않음.
class MemberServiceIntegrationTest {
@Autowired MemberService memberService; // 테스이므로, 생성자 대신 필드기반으로 받기.
@Autowired MemberRepository memberRepository;
@Test
void join() {
Member member = new Member();
member.setName("spring3");
Long saveId = memberService.join(member);
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
// given
Member member1 = new Member();
member1.setName("spring3");
Member member2 = new Member();
member2.setName("spring3");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
package com.led.hellospring.repository;
import com.led.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@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;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id);
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
@Configuration
public class SpringConfig {
private final DataSource dataSource;
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);
}
}
라이브러리 의존성 추가 (내부에 jdbc 관련 라이브러리를 포함하므로 주석처리)
// build.gradle
dependencies {
...
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa
...
}
JPA 설정 추가
// resources/application.properties
...
spring.jpa.show-sql=true // JPA가 생성하는 SQL을 출력.
spring.jpa.hibernate.ddl-auto=none // 테이블을 자동 생성하는 기능을 끔
JPA 엔티티 매핑
// com.led.hello-spring.domain/Member.java
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
...
JPA 회원 리포지토리
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
public Member save(Member member) {
em.persist(member);
return member;
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
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();
}
}
서비스 계층에 트랜잭션 추가 : 스프링은 해당 클래스의 메소드를 실행할 때, 트랜잭션을 시작하고, 메소드가 정상 종료되면 트랜잭션을 커밋한다.
@Transactional //org.springframework.transaction.annotation.Transactional
public class MemberService {
스프링 설정 변경
package com.led.hellospring;
import com.led.hellospring.repository.*;
import com.led.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
private final EntityManager em;
public SpringConfig(DataSource dataSource, EntityManager em) {
this.dataSource = dataSource;
this.em = em;
}
@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);
}
}
스프링 부트, JPA만 사용해도 개발 생산성 증가.
+ 스프링 데이터 JPA를 사용하면, 구현 클래스없이 인터페이스만으로 개발 완료.
참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl 라이브러리
Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편리하게 작성할 수 있다.
이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.
package com.led.hellospring.repository;
import com.led.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository { // JpaRepository를 지니고 있으면, 자동으로 스프링 빈으로 등록된다.
@Override
Optional<Member> findByName(String name);
}
package com.led.hellospring;
import com.led.hellospring.repository.*;
import com.led.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean // 스프링 빈에 등록한다.
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
package com.led.hellospring.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class TimeTraceApp {
@Around("execution(* com.led.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");
}
}
}
START: execution(MemberService com.led.hellospring.SpringConfig.memberService())
END: execution(MemberService com.led.hellospring.SpringConfig.memberService()) 20ms
START: execution(String com.led.hellospring.controller.HomeController.home())
END: execution(String com.led.hellospring.controller.HomeController.home()) 6ms
START: execution(String com.led.hellospring.controller.MemberController.list(Model))
START: execution(List com.led.hellospring.service.MemberService.findMembers())
START: execution(List org.springframework.data.jpa.repository.JpaRepository.findAll())
END: execution(List org.springframework.data.jpa.repository.JpaRepository.findAll()) 112ms
END: execution(List com.led.hellospring.service.MemberService.findMembers()) 135ms
END: execution(String com.led.hellospring.controller.MemberController.list(Model)) 169ms